dstar_gateway_core/slowdata/
assembler.rs1use crate::header::{DStarHeader, ENCODED_LEN};
9
10use super::block::{SlowDataBlock, SlowDataBlockKind, SlowDataText};
11use super::scrambler::descramble;
12
13const SCRATCH_SIZE: usize = 48;
17
18#[derive(Debug)]
26pub struct SlowDataAssembler {
27 scratch: [u8; SCRATCH_SIZE],
28 cursor: usize,
29 type_byte: Option<u8>,
30 expected_len: Option<usize>,
31}
32
33impl Default for SlowDataAssembler {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl SlowDataAssembler {
40 #[must_use]
42 pub const fn new() -> Self {
43 Self {
44 scratch: [0u8; SCRATCH_SIZE],
45 cursor: 0,
46 type_byte: None,
47 expected_len: None,
48 }
49 }
50
51 pub fn push(&mut self, fragment: [u8; 3]) -> Option<SlowDataBlock> {
57 let descrambled = descramble(fragment);
58
59 for &byte in &descrambled {
61 if self.cursor >= SCRATCH_SIZE {
62 self.reset();
63 return None;
64 }
65 if let Some(slot) = self.scratch.get_mut(self.cursor) {
66 *slot = byte;
67 }
68 self.cursor += 1;
69 }
70
71 if self.type_byte.is_none() && self.cursor >= 1 {
74 let t = self.scratch.first().copied().unwrap_or(0);
75 self.type_byte = Some(t);
76 self.expected_len = Some(usize::from(t & 0x0F));
81 }
82
83 let expected = self.expected_len?;
85 if self.cursor > expected {
86 let type_byte = self.type_byte.unwrap_or(0);
90 let block = self.decode_block(type_byte, expected);
91 self.reset();
92 return Some(block);
93 }
94
95 None
96 }
97
98 fn decode_block(&self, type_byte: u8, payload_len: usize) -> SlowDataBlock {
99 let kind = SlowDataBlockKind::from_type_byte(type_byte);
100 let payload_end = 1 + payload_len;
102 let payload = self.scratch.get(1..payload_end).unwrap_or(&[]);
103
104 match kind {
105 SlowDataBlockKind::Gps => {
106 let text = String::from_utf8_lossy(payload).to_string();
107 SlowDataBlock::Gps(text)
108 }
109 SlowDataBlockKind::Text => {
110 let raw = String::from_utf8_lossy(payload).to_string();
111 let trimmed = raw.trim_end_matches([' ', '\0']).to_string();
112 SlowDataBlock::Text(SlowDataText { text: trimmed })
113 }
114 SlowDataBlockKind::HeaderRetx => {
115 if payload.len() >= ENCODED_LEN {
118 let mut arr = [0u8; ENCODED_LEN];
119 if let Some(src) = payload.get(..ENCODED_LEN) {
120 arr.copy_from_slice(src);
121 }
122 let header = DStarHeader::decode(&arr);
123 SlowDataBlock::HeaderRetx(header)
124 } else {
125 SlowDataBlock::Unknown {
126 type_byte,
127 payload: payload.to_vec(),
128 }
129 }
130 }
131 SlowDataBlockKind::FastData1 | SlowDataBlockKind::FastData2 => {
132 SlowDataBlock::FastData(payload.to_vec())
133 }
134 SlowDataBlockKind::Squelch => {
135 let code = payload.first().copied().unwrap_or(0);
136 SlowDataBlock::Squelch { code }
137 }
138 SlowDataBlockKind::Unknown { .. } => SlowDataBlock::Unknown {
139 type_byte,
140 payload: payload.to_vec(),
141 },
142 }
143 }
144
145 const fn reset(&mut self) {
146 self.cursor = 0;
147 self.type_byte = None;
148 self.expected_len = None;
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::super::scrambler::scramble;
155 use super::*;
156
157 type TestResult = Result<(), Box<dyn std::error::Error>>;
158
159 fn push_descrambled(asm: &mut SlowDataAssembler, bytes: [u8; 3]) -> Option<SlowDataBlock> {
162 asm.push(scramble(bytes))
163 }
164
165 #[test]
166 fn empty_assembler_returns_zero_length_text_block() {
167 let mut asm = SlowDataAssembler::new();
168 let block = push_descrambled(&mut asm, [0x40, 0x00, 0x00]);
172 assert!(block.is_some(), "zero-length text block should complete");
173 assert!(
174 matches!(&block, Some(SlowDataBlock::Text(t)) if t.text.is_empty()),
175 "expected Text with empty string, got {block:?}"
176 );
177 }
178
179 #[test]
180 fn text_block_assembles_across_two_frames() -> TestResult {
181 let mut asm = SlowDataAssembler::new();
183 assert!(push_descrambled(&mut asm, [0x45, b'H', b'E']).is_none());
185 let block = push_descrambled(&mut asm, [b'L', b'L', b'O'])
187 .ok_or("expected block after second frame")?;
188 assert!(
189 matches!(&block, SlowDataBlock::Text(t) if t.text == "HELLO"),
190 "expected Text(\"HELLO\"), got {block:?}"
191 );
192 Ok(())
193 }
194
195 #[test]
196 fn gps_block_assembles() -> TestResult {
197 let mut asm = SlowDataAssembler::new();
199 assert!(push_descrambled(&mut asm, [0x34, b'T', b'E']).is_none());
200 let block = push_descrambled(&mut asm, [b'S', b'T', 0x00])
201 .ok_or("expected block after second frame")?;
202 assert!(
204 matches!(&block, SlowDataBlock::Gps(text) if text == "TEST"),
205 "expected Gps(\"TEST\"), got {block:?}"
206 );
207 Ok(())
208 }
209
210 #[test]
211 fn squelch_block_captures_code() -> TestResult {
212 let mut asm = SlowDataAssembler::new();
214 let block =
215 push_descrambled(&mut asm, [0xC1, 0x42, 0x00]).ok_or("expected squelch block")?;
216 assert!(
217 matches!(block, SlowDataBlock::Squelch { code } if code == 0x42),
218 "expected Squelch {{ code: 0x42 }}, got {block:?}"
219 );
220 Ok(())
221 }
222
223 #[test]
224 fn unknown_kind_preserves_type_byte_and_payload() -> TestResult {
225 let mut asm = SlowDataAssembler::new();
227 let block =
228 push_descrambled(&mut asm, [0xA2, 0x11, 0x22]).ok_or("expected unknown block")?;
229 assert!(
230 matches!(&block, SlowDataBlock::Unknown { type_byte, payload }
231 if *type_byte == 0xA2 && *payload == vec![0x11, 0x22]),
232 "expected Unknown {{ type_byte: 0xA2, payload: [0x11, 0x22] }}, got {block:?}"
233 );
234 Ok(())
235 }
236
237 #[test]
238 fn fast_data_block_two_frames() -> TestResult {
239 let mut asm = SlowDataAssembler::new();
241 assert!(push_descrambled(&mut asm, [0x83, 0xDE, 0xAD]).is_none());
242 let block =
243 push_descrambled(&mut asm, [0xBE, 0x00, 0x00]).ok_or("expected fast data block")?;
244 assert!(
245 matches!(&block, SlowDataBlock::FastData(payload) if *payload == vec![0xDE, 0xAD, 0xBE]),
246 "expected FastData([0xDE, 0xAD, 0xBE]), got {block:?}"
247 );
248 Ok(())
249 }
250}