1use super::scrambler::descramble;
23
24const TEXT_BLOCK_COUNT: u8 = 4;
26
27const TEXT_CHARS_PER_BLOCK: usize = 5;
29
30pub const MAX_MESSAGE_LEN: usize = 20;
32
33const TEXT_BLOCK_TYPE: u8 = 0x40;
35
36const SLOW_DATA_TYPE_MASK: u8 = 0xF0;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum HalfPhase {
42 First,
44 Second,
46}
47
48#[derive(Debug, Clone)]
55pub struct SlowDataTextCollector {
56 block_buffer: [u8; 6],
58 phase: HalfPhase,
60 slots: [[u8; TEXT_CHARS_PER_BLOCK]; 4],
62 seen_mask: u8,
64 last_frame_index: u8,
66}
67
68impl Default for SlowDataTextCollector {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl SlowDataTextCollector {
75 #[must_use]
77 pub const fn new() -> Self {
78 Self {
79 block_buffer: [0u8; 6],
80 phase: HalfPhase::First,
81 slots: [[b' '; TEXT_CHARS_PER_BLOCK]; 4],
82 seen_mask: 0,
83 last_frame_index: 1,
84 }
85 }
86
87 pub fn push(&mut self, fragment: [u8; 3], frame_index: u8) {
93 if frame_index == 0 {
94 self.phase = HalfPhase::First;
95 self.last_frame_index = 0;
96 return;
97 }
98 self.last_frame_index = frame_index;
99
100 let plain = descramble(fragment);
101 match self.phase {
102 HalfPhase::First => {
103 self.block_buffer[0] = plain[0];
104 self.block_buffer[1] = plain[1];
105 self.block_buffer[2] = plain[2];
106 self.phase = HalfPhase::Second;
107 }
108 HalfPhase::Second => {
109 self.block_buffer[3] = plain[0];
110 self.block_buffer[4] = plain[1];
111 self.block_buffer[5] = plain[2];
112 self.phase = HalfPhase::First;
113 self.commit_block();
114 }
115 }
116 }
117
118 fn commit_block(&mut self) {
120 let type_byte = self.block_buffer[0];
121 if type_byte & SLOW_DATA_TYPE_MASK != TEXT_BLOCK_TYPE {
122 return;
123 }
124 let block_index = type_byte & 0x0F;
125 if block_index >= TEXT_BLOCK_COUNT {
126 return;
127 }
128 let slot_idx = usize::from(block_index);
129 let Some(slot) = self.slots.get_mut(slot_idx) else {
130 return;
131 };
132 let Some(src) = self.block_buffer.get(1..=TEXT_CHARS_PER_BLOCK) else {
133 return;
134 };
135 for (dst, s) in slot.iter_mut().zip(src.iter()) {
136 *dst = *s;
137 }
138 self.seen_mask |= 1u8 << block_index;
139 tracing::trace!(
144 target: "dstar_gateway_core::slowdata::text_collector",
145 slot = slot_idx,
146 seen_mask = format_args!("{:#06b}", self.seen_mask),
147 type_byte = format_args!("{:#04x}", type_byte),
148 block = format_args!(
149 "{:02X} {:02X} {:02X} {:02X} {:02X} {:02X}",
150 self.block_buffer[0], self.block_buffer[1], self.block_buffer[2],
151 self.block_buffer[3], self.block_buffer[4], self.block_buffer[5]
152 ),
153 chars = format_args!("{:?}", String::from_utf8_lossy(src)),
154 "text block accepted"
155 );
156 }
157
158 #[must_use]
160 pub fn message(&self) -> Option<[u8; MAX_MESSAGE_LEN]> {
161 if self.seen_mask != 0b1111 {
162 return None;
163 }
164 let mut out = [0u8; MAX_MESSAGE_LEN];
165 for (i, slot) in self.slots.iter().enumerate() {
166 let start = i * TEXT_CHARS_PER_BLOCK;
167 let end = start + TEXT_CHARS_PER_BLOCK;
168 let dst = out.get_mut(start..end)?;
169 dst.copy_from_slice(slot);
170 }
171 Some(out)
172 }
173
174 pub fn take_message(&mut self) -> Option<[u8; MAX_MESSAGE_LEN]> {
176 let msg = self.message()?;
177 self.rearm();
178 Some(msg)
179 }
180
181 pub const fn reset(&mut self) {
183 self.rearm();
184 self.phase = HalfPhase::First;
185 self.block_buffer = [0u8; 6];
186 }
187
188 const fn rearm(&mut self) {
189 self.slots = [[b' '; TEXT_CHARS_PER_BLOCK]; 4];
190 self.seen_mask = 0;
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::super::scrambler::scramble;
197 use super::*;
198
199 type TestResult = Result<(), Box<dyn std::error::Error>>;
200
201 fn feed(collector: &mut SlowDataTextCollector, halves: &[[u8; 3]]) {
205 for (i, h) in halves.iter().enumerate() {
206 #[allow(clippy::cast_possible_truncation)]
207 let idx = (i as u8).wrapping_add(1);
208 collector.push(scramble(*h), idx);
209 }
210 }
211
212 #[test]
213 fn four_text_blocks_assemble_20_char_message() -> TestResult {
214 let mut c = SlowDataTextCollector::new();
215 feed(
216 &mut c,
217 &[
218 [0x40, b'C', b'Q'],
219 [b' ', b'w', b'o'],
220 [0x41, b'r', b'k'],
221 [b'i', b'n', b'g'],
222 [0x42, b' ', b' '],
223 [b' ', b' ', b' '],
224 [0x43, b' ', b' '],
225 [b' ', b' ', b' '],
226 ],
227 );
228 let msg = c.take_message().ok_or("complete message")?;
229 assert_eq!(&msg[..], b"CQ working ");
230 Ok(())
231 }
232
233 #[test]
234 fn out_of_order_blocks_still_assemble() -> TestResult {
235 let mut c = SlowDataTextCollector::new();
236 feed(
237 &mut c,
238 &[
239 [0x42, b'C', b'C'],
240 [b'C', b'C', b'C'],
241 [0x40, b'A', b'A'],
242 [b'A', b'A', b'A'],
243 [0x43, b'D', b'D'],
244 [b'D', b'D', b'D'],
245 [0x41, b'B', b'B'],
246 [b'B', b'B', b'B'],
247 ],
248 );
249 let msg = c.take_message().ok_or("complete message")?;
250 assert_eq!(&msg[..], b"AAAAABBBBBCCCCCDDDDD");
251 Ok(())
252 }
253
254 #[test]
255 fn partial_message_returns_none() {
256 let mut c = SlowDataTextCollector::new();
257 feed(
258 &mut c,
259 &[
260 [0x40, b'A', b'B'],
261 [b'C', b'D', b'E'],
262 [0x41, b'F', b'G'],
263 [b'H', b'I', b'J'],
264 ],
265 );
266 assert!(c.message().is_none());
267 assert!(c.take_message().is_none());
268 }
269
270 #[test]
271 fn reset_discards_partial_state() {
272 let mut c = SlowDataTextCollector::new();
273 feed(
274 &mut c,
275 &[
276 [0x40, b'X', b'Y'],
277 [b'Z', b'!', b'!'],
278 [0x41, b'1', b'2'],
279 [b'3', b'4', b'5'],
280 ],
281 );
282 c.reset();
283 feed(
284 &mut c,
285 &[
286 [0x40, b'N', b'E'],
287 [b'W', b' ', b' '],
288 [0x41, b' ', b' '],
289 [b' ', b' ', b' '],
290 ],
291 );
292 assert!(c.message().is_none());
293 }
294
295 #[test]
296 fn sync_frame_resyncs_without_corrupting_state() -> TestResult {
297 let mut c = SlowDataTextCollector::new();
298 c.push(scramble([0x40, b'A', b'A']), 1);
299 c.push([0x55, 0x55, 0x55], 0);
300 feed(
301 &mut c,
302 &[
303 [0x40, b'H', b'I'],
304 [b'!', b'!', b'!'],
305 [0x41, b' ', b' '],
306 [b' ', b' ', b' '],
307 [0x42, b' ', b' '],
308 [b' ', b' ', b' '],
309 [0x43, b' ', b' '],
310 [b' ', b' ', b' '],
311 ],
312 );
313 let msg = c.take_message().ok_or("message after resync")?;
314 assert_eq!(&msg[..], b"HI!!! ");
315 Ok(())
316 }
317
318 #[test]
319 fn non_text_blocks_are_ignored() {
320 let mut c = SlowDataTextCollector::new();
321 feed(
322 &mut c,
323 &[
324 [0x55, b'V', b'E'],
325 [b'3', b'O', b'E'],
326 [0x35, b'$', b'G'],
327 [b'P', b'G', b'G'],
328 [0xC0, 0x12, 0x34],
329 [0x56, 0x78, 0x9A],
330 ],
331 );
332 assert!(c.message().is_none());
333 }
334
335 #[test]
336 fn reserved_text_sub_codes_are_ignored() {
337 let mut c = SlowDataTextCollector::new();
338 feed(
339 &mut c,
340 &[
341 [0x44, b'X', b'X'],
342 [b'X', b'X', b'X'],
343 [0x4F, b'Y', b'Y'],
344 [b'Y', b'Y', b'Y'],
345 ],
346 );
347 assert!(c.message().is_none());
348 }
349
350 #[test]
351 fn take_message_rearms_collector() -> TestResult {
352 let mut c = SlowDataTextCollector::new();
353 feed(
354 &mut c,
355 &[
356 [0x40, b'H', b'e'],
357 [b'l', b'l', b'o'],
358 [0x41, b' ', b'w'],
359 [b'o', b'r', b'l'],
360 [0x42, b'd', b' '],
361 [b' ', b' ', b' '],
362 [0x43, b' ', b' '],
363 [b' ', b' ', b' '],
364 ],
365 );
366 let taken = c.take_message().ok_or("message ready")?;
367 assert_eq!(&taken[..], b"Hello world ");
368 assert!(c.message().is_none());
369 Ok(())
370 }
371
372 #[test]
373 fn default_creates_empty_collector() {
374 let c = SlowDataTextCollector::default();
375 assert!(c.message().is_none());
376 }
377}