Skip to main content

mw75/
parse.rs

1//! Binary decoders for MW75 BLE notification and RFCOMM data payloads.
2//!
3//! All public functions in this module are pure (no I/O, no allocation beyond
4//! the returned collections) and are safe to call from any async or sync context.
5//!
6//! # Packet layout
7//!
8//! The MW75 streams 63-byte binary packets at 500 Hz over RFCOMM channel 25.
9//! Each packet starts with a `0xAA` sync byte and ends with a 16-bit
10//! little-endian checksum:
11//!
12//! ```text
13//! Offset  Size  Field
14//! ──────  ────  ─────
15//!   0       1   Sync byte (0xAA)
16//!   1       1   Event ID (239 = EEG)
17//!   2       1   Data length
18//!   3       1   Counter (0–255, wrapping)
19//!   4       4   REF electrode value (f32 LE)
20//!   8       4   DRL electrode value (f32 LE)
21//!  12      48   12 × EEG channels (f32 LE each)
22//!  60       1   Feature status byte
23//!  61       2   Checksum (u16 LE = sum of bytes[0..61] & 0xFFFF)
24//! ```
25//!
26//! # Buffered processing
27//!
28//! RFCOMM delivers arbitrary-sized chunks (e.g. 64 bytes) while MW75 packets
29//! are exactly 63 bytes.  [`PacketProcessor`] accumulates data across chunks
30//! and handles sync-byte alignment, checksum validation, and buffer overflow
31//! protection.
32
33use std::time::{SystemTime, UNIX_EPOCH};
34
35use crate::protocol::{
36    EEG_EVENT_ID, EEG_SCALING_FACTOR, NUM_EEG_CHANNELS, PACKET_SIZE, SYNC_BYTE,
37};
38use crate::types::{ChecksumStats, EegPacket, Mw75Event};
39
40// ── Timestamp helper ──────────────────────────────────────────────────────────
41
42/// Return the current wall-clock time as seconds since Unix epoch.
43fn now_secs() -> f64 {
44    SystemTime::now()
45        .duration_since(UNIX_EPOCH)
46        .expect("system clock is before Unix epoch")
47        .as_secs_f64()
48}
49
50// ── Checksum ──────────────────────────────────────────────────────────────────
51
52/// Validate the MW75 packet checksum.
53///
54/// The MW75 checksum is computed as:
55/// 1. Sum of the first 61 bytes (indices 0–60)
56/// 2. Masked to 16 bits (`& 0xFFFF`)
57/// 3. Stored as a little-endian `u16` at bytes 61–62
58///
59/// Returns `(is_valid, calculated_checksum, received_checksum)`.
60///
61/// Returns `(false, 0, 0)` if `packet` is shorter than [`PACKET_SIZE`].
62///
63/// # Example
64///
65/// ```
66/// # use mw75::parse::validate_checksum;
67/// let mut pkt = vec![0u8; 63];
68/// pkt[0] = 0xAA;
69/// // Set checksum to match: sum of bytes[0..61]
70/// let sum: u16 = pkt[..61].iter().map(|&b| b as u16).sum();
71/// pkt[61] = (sum & 0xFF) as u8;
72/// pkt[62] = (sum >> 8) as u8;
73/// let (valid, calc, recv) = validate_checksum(&pkt);
74/// assert!(valid);
75/// assert_eq!(calc, recv);
76/// ```
77pub fn validate_checksum(packet: &[u8]) -> (bool, u16, u16) {
78    if packet.len() < PACKET_SIZE {
79        return (false, 0, 0);
80    }
81    let calculated: u16 = packet[..61].iter().map(|&b| b as u16).sum::<u16>() & 0xFFFF;
82    let received: u16 = packet[61] as u16 | ((packet[62] as u16) << 8);
83    (calculated == received, calculated, received)
84}
85
86// ── EEG Packet Parsing ───────────────────────────────────────────────────────
87
88/// Parse a 63-byte MW75 packet into a structured [`EegPacket`].
89///
90/// Returns `None` if:
91/// * The packet is not exactly [`PACKET_SIZE`] (63) bytes
92/// * The first byte is not [`SYNC_BYTE`] (`0xAA`)
93/// * The checksum does not validate
94///
95/// Channel values are scaled from raw ADC floats to microvolts using
96/// [`EEG_SCALING_FACTOR`] (`0.023842`).
97///
98/// # Wire format
99///
100/// See the [module-level documentation](self) for the complete packet layout.
101///
102/// # Example
103///
104/// ```
105/// # use mw75::parse::parse_eeg_packet;
106/// # use mw75::simulate::build_eeg_packet;
107/// let pkt = build_eeg_packet(0);
108/// let eeg = parse_eeg_packet(&pkt).expect("valid packet");
109/// assert_eq!(eeg.channels.len(), 12);
110/// assert!(eeg.checksum_valid);
111/// ```
112pub fn parse_eeg_packet(packet: &[u8]) -> Option<EegPacket> {
113    if packet.len() != PACKET_SIZE || packet[0] != SYNC_BYTE {
114        return None;
115    }
116
117    let (is_valid, _calc, _recv) = validate_checksum(packet);
118    if !is_valid {
119        return None;
120    }
121
122    let event_id = packet[1];
123    let counter = packet[3];
124    let timestamp = now_secs();
125
126    // REF and DRL as f32 LE
127    let ref_value = f32::from_le_bytes([packet[4], packet[5], packet[6], packet[7]]);
128    let drl = f32::from_le_bytes([packet[8], packet[9], packet[10], packet[11]]);
129
130    // 12 EEG channels, each f32 LE, scaled to µV
131    let mut channels = Vec::with_capacity(NUM_EEG_CHANNELS);
132    for ch in 0..NUM_EEG_CHANNELS {
133        let offset = 12 + ch * 4;
134        if offset + 4 <= packet.len() {
135            let raw = f32::from_le_bytes([
136                packet[offset],
137                packet[offset + 1],
138                packet[offset + 2],
139                packet[offset + 3],
140            ]);
141            channels.push(raw * EEG_SCALING_FACTOR);
142        }
143    }
144
145    let feature_status = if packet.len() > 60 { packet[60] } else { 0 };
146
147    Some(EegPacket {
148        timestamp,
149        event_id,
150        counter,
151        ref_value,
152        drl,
153        channels,
154        feature_status,
155        checksum_valid: true,
156    })
157}
158
159// ── Packet Processor (continuous buffer) ──────────────────────────────────────
160
161/// Processes a continuous byte stream into MW75 packets.
162///
163/// Accumulates data across transport chunks (RFCOMM delivers arbitrary-sized
164/// reads while packets are exactly 63 bytes) and handles sync-byte alignment.
165///
166/// Mirrors the Python `PacketProcessor` class.
167///
168/// # Features
169///
170/// * **Split delivery** — a packet that spans two `process_data` calls is
171///   correctly reassembled.
172/// * **Sync recovery** — garbage bytes before a sync byte are silently skipped.
173/// * **Checksum validation** — invalid packets advance by 1 byte (not 63) to
174///   avoid skipping a valid alignment when the payload contains `0xAA`.
175/// * **Buffer overflow protection** — if the buffer grows beyond 10 packets
176///   worth of data without producing output, it is truncated to the last
177///   sync byte or cleared entirely.
178/// * **Statistics tracking** — valid / invalid / total packet counts are
179///   maintained in [`ChecksumStats`].
180///
181/// # Usage
182///
183/// ```
184/// # use mw75::parse::PacketProcessor;
185/// let mut proc = PacketProcessor::new(false);
186/// // Feed raw bytes from the transport:
187/// let events = proc.process_data(&[0xAA, /* ... 62 more bytes ... */]);
188/// ```
189pub struct PacketProcessor {
190    /// Internal accumulation buffer.
191    buffer: Vec<u8>,
192    /// Running checksum statistics.
193    pub stats: ChecksumStats,
194    /// When `true`, log warnings for individual checksum failures.
195    pub verbose: bool,
196}
197
198impl PacketProcessor {
199    /// Create a new processor.
200    ///
201    /// Set `verbose` to `true` to enable per-packet checksum failure logging.
202    pub fn new(verbose: bool) -> Self {
203        Self {
204            buffer: Vec::with_capacity(PACKET_SIZE * 10),
205            stats: ChecksumStats::default(),
206            verbose,
207        }
208    }
209
210    /// Feed raw bytes from the transport and return any complete events.
211    ///
212    /// The processor maintains an internal buffer across calls.  Incomplete
213    /// packets at the end of a chunk are retained until more data arrives.
214    ///
215    /// Returns a `Vec<Mw75Event>` containing:
216    /// * [`Mw75Event::Eeg`] for valid EEG packets (event ID 239)
217    /// * [`Mw75Event::OtherEvent`] for valid non-EEG packets
218    pub fn process_data(&mut self, data: &[u8]) -> Vec<Mw75Event> {
219        self.buffer.extend_from_slice(data);
220        let mut events = Vec::new();
221
222        let mut i = 0;
223        while i < self.buffer.len() {
224            if self.buffer[i] == SYNC_BYTE {
225                // Do we have a full packet?
226                if i + PACKET_SIZE <= self.buffer.len() {
227                    let packet = &self.buffer[i..i + PACKET_SIZE];
228
229                    // Validate checksum first
230                    let (is_valid, calc, recv) = validate_checksum(packet);
231                    self.stats.total_packets += 1;
232
233                    if !is_valid {
234                        self.stats.invalid_packets += 1;
235                        if self.verbose {
236                            log::warn!(
237                                "Checksum mismatch: calc=0x{:04x} recv=0x{:04x} (event={}, counter={})",
238                                calc, recv, packet[1], packet[3]
239                            );
240                        }
241                        // Slide by 1 byte to avoid skipping a valid alignment
242                        i += 1;
243                        continue;
244                    }
245
246                    self.stats.valid_packets += 1;
247
248                    if packet[1] == EEG_EVENT_ID {
249                        if let Some(eeg) = parse_eeg_packet(packet) {
250                            events.push(Mw75Event::Eeg(eeg));
251                        }
252                    } else {
253                        events.push(Mw75Event::OtherEvent {
254                            event_id: packet[1],
255                            counter: packet[3],
256                            raw: packet.to_vec(),
257                        });
258                    }
259
260                    i += PACKET_SIZE;
261                } else {
262                    // Not enough data for a complete packet — wait for more
263                    break;
264                }
265            } else {
266                i += 1;
267            }
268        }
269
270        // Remove processed data, keep the remainder
271        if i > 0 {
272            self.buffer.drain(..i);
273        }
274
275        // Prevent unbounded buffer growth
276        let max_buf = PACKET_SIZE * 10;
277        if self.buffer.len() > max_buf {
278            // Try to find a sync byte near the end
279            if let Some(pos) = self.buffer.iter().rposition(|&b| b == SYNC_BYTE) {
280                self.buffer.drain(..pos);
281                log::debug!("Buffer overflow — recovered sync at {pos}");
282            } else {
283                self.buffer.clear();
284                log::warn!("Buffer overflow — no sync byte found, cleared");
285            }
286        }
287
288        events
289    }
290
291    /// Return the number of bytes currently buffered (waiting for more data).
292    pub fn buffered_len(&self) -> usize {
293        self.buffer.len()
294    }
295
296    /// Return a snapshot of the current checksum statistics.
297    pub fn get_stats(&self) -> ChecksumStats {
298        self.stats.clone()
299    }
300
301    /// Reset the internal buffer and statistics.
302    pub fn reset(&mut self) {
303        self.buffer.clear();
304        self.stats = ChecksumStats::default();
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    // ── Test helpers ──────────────────────────────────────────────────────────
313
314    /// Build a minimal valid 63-byte packet with correct checksum.
315    fn make_packet(event_id: u8, counter: u8) -> Vec<u8> {
316        let mut pkt = vec![0u8; PACKET_SIZE];
317        pkt[0] = SYNC_BYTE;
318        pkt[1] = event_id;
319        pkt[2] = 58; // data length
320        pkt[3] = counter;
321        // Leave REF/DRL/channels as zero — just need valid checksum
322        fix_checksum(&mut pkt);
323        pkt
324    }
325
326    /// Build a packet with specific channel f32 values (before scaling).
327    fn make_packet_with_channels(counter: u8, raw_values: &[f32; 12]) -> Vec<u8> {
328        let mut pkt = vec![0u8; PACKET_SIZE];
329        pkt[0] = SYNC_BYTE;
330        pkt[1] = EEG_EVENT_ID;
331        pkt[2] = 58;
332        pkt[3] = counter;
333        // REF and DRL
334        pkt[4..8].copy_from_slice(&42.0_f32.to_le_bytes());
335        pkt[8..12].copy_from_slice(&(-7.5_f32).to_le_bytes());
336        // Channels
337        for (i, &val) in raw_values.iter().enumerate() {
338            let off = 12 + i * 4;
339            pkt[off..off + 4].copy_from_slice(&val.to_le_bytes());
340        }
341        pkt[60] = 0x01; // feature status
342        fix_checksum(&mut pkt);
343        pkt
344    }
345
346    /// Recalculate and store the checksum in-place.
347    fn fix_checksum(pkt: &mut [u8]) {
348        let sum: u16 = pkt[..61].iter().map(|&b| b as u16).sum::<u16>() & 0xFFFF;
349        pkt[61] = (sum & 0xFF) as u8;
350        pkt[62] = (sum >> 8) as u8;
351    }
352
353    // ── validate_checksum tests ──────────────────────────────────────────────
354
355    #[test]
356    fn checksum_valid_packet() {
357        let pkt = make_packet(EEG_EVENT_ID, 0);
358        let (valid, calc, recv) = validate_checksum(&pkt);
359        assert!(valid);
360        assert_eq!(calc, recv);
361    }
362
363    #[test]
364    fn checksum_invalid_corrupted_byte() {
365        let mut pkt = make_packet(EEG_EVENT_ID, 0);
366        pkt[62] ^= 0xFF; // corrupt checksum high byte
367        let (valid, _, _) = validate_checksum(&pkt);
368        assert!(!valid);
369    }
370
371    #[test]
372    fn checksum_invalid_corrupted_payload() {
373        let mut pkt = make_packet(EEG_EVENT_ID, 10);
374        pkt[30] = 0xFF; // corrupt a data byte
375        let (valid, _, _) = validate_checksum(&pkt);
376        assert!(!valid);
377    }
378
379    #[test]
380    fn checksum_too_short() {
381        let (valid, calc, recv) = validate_checksum(&[0xAA, 0x00]);
382        assert!(!valid);
383        assert_eq!(calc, 0);
384        assert_eq!(recv, 0);
385    }
386
387    #[test]
388    fn checksum_empty() {
389        let (valid, _, _) = validate_checksum(&[]);
390        assert!(!valid);
391    }
392
393    #[test]
394    fn checksum_exact_minimum_length() {
395        // Exactly PACKET_SIZE bytes (63)
396        let pkt = make_packet(EEG_EVENT_ID, 0);
397        assert_eq!(pkt.len(), 63);
398        let (valid, _, _) = validate_checksum(&pkt);
399        assert!(valid);
400    }
401
402    #[test]
403    fn checksum_longer_than_packet_still_valid() {
404        // Extra bytes after PACKET_SIZE are ignored by validate_checksum
405        let mut pkt = make_packet(EEG_EVENT_ID, 0);
406        pkt.extend_from_slice(&[0xFF, 0xFF, 0xFF]);
407        let (valid, _, _) = validate_checksum(&pkt);
408        assert!(valid);
409    }
410
411    // ── parse_eeg_packet tests ───────────────────────────────────────────────
412
413    #[test]
414    fn parse_basic_eeg_packet() {
415        let pkt = make_packet(EEG_EVENT_ID, 42);
416        let eeg = parse_eeg_packet(&pkt).expect("should parse");
417        assert_eq!(eeg.event_id, EEG_EVENT_ID);
418        assert_eq!(eeg.counter, 42);
419        assert_eq!(eeg.channels.len(), NUM_EEG_CHANNELS);
420        assert!(eeg.checksum_valid);
421        assert!(eeg.timestamp > 0.0);
422    }
423
424    #[test]
425    fn parse_rejects_wrong_sync_byte() {
426        let mut pkt = make_packet(EEG_EVENT_ID, 0);
427        pkt[0] = 0xBB; // wrong sync
428        fix_checksum(&mut pkt);
429        assert!(parse_eeg_packet(&pkt).is_none());
430    }
431
432    #[test]
433    fn parse_rejects_short_packet() {
434        assert!(parse_eeg_packet(&[0xAA]).is_none());
435        assert!(parse_eeg_packet(&[0xAA; 62]).is_none());
436    }
437
438    #[test]
439    fn parse_rejects_invalid_checksum() {
440        let mut pkt = make_packet(EEG_EVENT_ID, 0);
441        pkt[61] = 0; // zero out checksum
442        pkt[62] = 0;
443        assert!(parse_eeg_packet(&pkt).is_none());
444    }
445
446    #[test]
447    fn parse_channel_values_scaled() {
448        // Each channel raw value = 1000.0
449        // After scaling: 1000.0 * 0.023842 = 23.842
450        let raw = [1000.0_f32; 12];
451        let pkt = make_packet_with_channels(0, &raw);
452        let eeg = parse_eeg_packet(&pkt).unwrap();
453        for &ch in &eeg.channels {
454            assert!((ch - 23.842).abs() < 0.01, "Expected ~23.842, got {ch}");
455        }
456    }
457
458    #[test]
459    fn parse_negative_channel_values() {
460        let raw = [-5000.0_f32; 12];
461        let pkt = make_packet_with_channels(0, &raw);
462        let eeg = parse_eeg_packet(&pkt).unwrap();
463        for &ch in &eeg.channels {
464            let expected = -5000.0 * EEG_SCALING_FACTOR;
465            assert!((ch - expected).abs() < 0.1, "Expected ~{expected}, got {ch}");
466        }
467    }
468
469    #[test]
470    fn parse_ref_and_drl() {
471        let raw = [0.0_f32; 12];
472        let pkt = make_packet_with_channels(0, &raw);
473        let eeg = parse_eeg_packet(&pkt).unwrap();
474        assert!((eeg.ref_value - 42.0).abs() < 0.001);
475        assert!((eeg.drl - (-7.5)).abs() < 0.001);
476    }
477
478    #[test]
479    fn parse_feature_status() {
480        let raw = [0.0_f32; 12];
481        let pkt = make_packet_with_channels(0, &raw);
482        let eeg = parse_eeg_packet(&pkt).unwrap();
483        assert_eq!(eeg.feature_status, 0x01);
484    }
485
486    #[test]
487    fn parse_all_counter_values() {
488        for c in 0..=255u8 {
489            let pkt = make_packet(EEG_EVENT_ID, c);
490            let eeg = parse_eeg_packet(&pkt).unwrap();
491            assert_eq!(eeg.counter, c);
492        }
493    }
494
495    // ── PacketProcessor tests ────────────────────────────────────────────────
496
497    #[test]
498    fn processor_basic_single_packet() {
499        let mut proc = PacketProcessor::new(false);
500        let pkt = make_packet(EEG_EVENT_ID, 1);
501        let events = proc.process_data(&pkt);
502        assert_eq!(events.len(), 1);
503        assert!(matches!(&events[0], Mw75Event::Eeg(e) if e.counter == 1));
504        assert_eq!(proc.stats.valid_packets, 1);
505        assert_eq!(proc.stats.total_packets, 1);
506        assert_eq!(proc.stats.invalid_packets, 0);
507    }
508
509    #[test]
510    fn processor_multiple_packets_in_one_call() {
511        let mut proc = PacketProcessor::new(false);
512        let mut data = Vec::new();
513        for i in 0..5 {
514            data.extend_from_slice(&make_packet(EEG_EVENT_ID, i));
515        }
516        let events = proc.process_data(&data);
517        assert_eq!(events.len(), 5);
518        assert_eq!(proc.stats.valid_packets, 5);
519    }
520
521    #[test]
522    fn processor_split_delivery_across_two_calls() {
523        let mut proc = PacketProcessor::new(false);
524        let pkt = make_packet(EEG_EVENT_ID, 1);
525
526        // Deliver first 30 bytes
527        let events1 = proc.process_data(&pkt[..30]);
528        assert!(events1.is_empty());
529        assert_eq!(proc.buffered_len(), 30);
530
531        // Deliver remaining 33 bytes
532        let events2 = proc.process_data(&pkt[30..]);
533        assert_eq!(events2.len(), 1);
534        assert_eq!(proc.buffered_len(), 0);
535    }
536
537    #[test]
538    fn processor_split_at_every_byte() {
539        // Extreme case: deliver one byte at a time
540        let mut proc = PacketProcessor::new(false);
541        let pkt = make_packet(EEG_EVENT_ID, 99);
542
543        let mut total_events = 0;
544        for &byte in &pkt {
545            let events = proc.process_data(&[byte]);
546            total_events += events.len();
547        }
548        assert_eq!(total_events, 1);
549    }
550
551    #[test]
552    fn processor_garbage_prefix_skipped() {
553        let mut proc = PacketProcessor::new(false);
554        let pkt = make_packet(EEG_EVENT_ID, 5);
555
556        // Prepend garbage bytes (non-0xAA)
557        let mut data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
558        data.extend_from_slice(&pkt);
559
560        let events = proc.process_data(&data);
561        assert_eq!(events.len(), 1);
562        assert!(matches!(&events[0], Mw75Event::Eeg(e) if e.counter == 5));
563    }
564
565    #[test]
566    fn processor_garbage_between_packets() {
567        let mut proc = PacketProcessor::new(false);
568        let mut data = Vec::new();
569        data.extend_from_slice(&make_packet(EEG_EVENT_ID, 1));
570        data.extend_from_slice(&[0x01, 0x02, 0x03]); // garbage
571        data.extend_from_slice(&make_packet(EEG_EVENT_ID, 2));
572
573        let events = proc.process_data(&data);
574        assert_eq!(events.len(), 2);
575    }
576
577    #[test]
578    fn processor_other_event_type() {
579        let mut proc = PacketProcessor::new(false);
580        let pkt = make_packet(100, 7); // event_id=100, not EEG
581        let events = proc.process_data(&pkt);
582        assert_eq!(events.len(), 1);
583        assert!(matches!(
584            &events[0],
585            Mw75Event::OtherEvent { event_id: 100, counter: 7, .. }
586        ));
587    }
588
589    #[test]
590    fn processor_invalid_checksum_skips_and_counts() {
591        let mut proc = PacketProcessor::new(false);
592        let mut pkt = make_packet(EEG_EVENT_ID, 1);
593        pkt[30] = 0xFF; // corrupt data, checksum mismatch
594
595        let events = proc.process_data(&pkt);
596        assert!(events.is_empty());
597        assert!(proc.stats.invalid_packets > 0);
598    }
599
600    #[test]
601    fn processor_invalid_then_valid() {
602        let mut proc = PacketProcessor::new(false);
603
604        // Invalid packet (corrupted data)
605        let mut bad = make_packet(EEG_EVENT_ID, 1);
606        bad[30] = 0xFF;
607
608        // Valid packet
609        let good = make_packet(EEG_EVENT_ID, 2);
610
611        let mut data = Vec::new();
612        data.extend_from_slice(&bad);
613        data.extend_from_slice(&good);
614
615        let events = proc.process_data(&data);
616        // The good packet should still be found
617        assert!(!events.is_empty());
618        assert!(proc.stats.valid_packets >= 1);
619    }
620
621    #[test]
622    fn processor_sync_byte_in_payload() {
623        // Build a packet where some data bytes happen to be 0xAA
624        let mut pkt = vec![0u8; PACKET_SIZE];
625        pkt[0] = SYNC_BYTE;
626        pkt[1] = EEG_EVENT_ID;
627        pkt[2] = 58;
628        pkt[3] = 10;
629        // Put 0xAA in several data positions
630        pkt[15] = 0xAA;
631        pkt[20] = 0xAA;
632        pkt[40] = 0xAA;
633        fix_checksum(&mut pkt);
634
635        let mut proc = PacketProcessor::new(false);
636        let events = proc.process_data(&pkt);
637        assert_eq!(events.len(), 1);
638        assert!(matches!(&events[0], Mw75Event::Eeg(e) if e.counter == 10));
639    }
640
641    #[test]
642    fn processor_reset_clears_state() {
643        let mut proc = PacketProcessor::new(false);
644        let pkt = make_packet(EEG_EVENT_ID, 1);
645        proc.process_data(&pkt);
646        assert_eq!(proc.stats.valid_packets, 1);
647
648        proc.reset();
649        assert_eq!(proc.stats.valid_packets, 0);
650        assert_eq!(proc.stats.total_packets, 0);
651        assert_eq!(proc.buffered_len(), 0);
652    }
653
654    #[test]
655    fn processor_partial_packet_retained() {
656        let mut proc = PacketProcessor::new(false);
657        let pkt = make_packet(EEG_EVENT_ID, 1);
658
659        // Feed only first 40 bytes — no packet emitted, buffer retains them
660        let events = proc.process_data(&pkt[..40]);
661        assert!(events.is_empty());
662        assert_eq!(proc.buffered_len(), 40);
663
664        // Feed the rest — packet emitted, buffer empty
665        let events = proc.process_data(&pkt[40..]);
666        assert_eq!(events.len(), 1);
667        assert_eq!(proc.buffered_len(), 0);
668    }
669
670    #[test]
671    fn processor_buffer_overflow_protection() {
672        let mut proc = PacketProcessor::new(false);
673        // Feed a large amount of garbage with no sync bytes
674        let garbage = vec![0x01; PACKET_SIZE * 15];
675        let events = proc.process_data(&garbage);
676        assert!(events.is_empty());
677        // Buffer should have been truncated/cleared
678        assert!(proc.buffered_len() < PACKET_SIZE * 11);
679    }
680
681    #[test]
682    fn processor_stats_error_rate() {
683        let mut proc = PacketProcessor::new(false);
684
685        // 3 valid packets
686        for i in 0..3 {
687            let pkt = make_packet(EEG_EVENT_ID, i);
688            proc.process_data(&pkt);
689        }
690
691        assert_eq!(proc.stats.valid_packets, 3);
692        assert_eq!(proc.stats.total_packets, 3);
693        assert!((proc.stats.error_rate() - 0.0).abs() < f64::EPSILON);
694    }
695
696    #[test]
697    fn processor_two_packets_in_64_byte_chunk() {
698        // RFCOMM often delivers 64-byte chunks; this tests the boundary
699        // where a 63-byte packet fits in the first chunk with 1 byte spillover
700        let mut proc = PacketProcessor::new(false);
701
702        let pkt1 = make_packet(EEG_EVENT_ID, 1);
703        let pkt2 = make_packet(EEG_EVENT_ID, 2);
704
705        // Simulate 64-byte RFCOMM chunks
706        let mut combined = Vec::new();
707        combined.extend_from_slice(&pkt1);
708        combined.extend_from_slice(&pkt2);
709
710        // Deliver as 64+62 byte chunks
711        let events1 = proc.process_data(&combined[..64]);
712        // First packet parsed, 1 byte of second packet buffered
713        assert_eq!(events1.len(), 1);
714
715        let events2 = proc.process_data(&combined[64..]);
716        assert_eq!(events2.len(), 1);
717    }
718
719    #[test]
720    fn processor_empty_input() {
721        let mut proc = PacketProcessor::new(false);
722        let events = proc.process_data(&[]);
723        assert!(events.is_empty());
724        assert_eq!(proc.buffered_len(), 0);
725    }
726
727    #[test]
728    fn processor_verbose_mode() {
729        let mut proc = PacketProcessor::new(true);
730        assert!(proc.verbose);
731        // Should not panic even with verbose logging on invalid packets
732        let mut bad = make_packet(EEG_EVENT_ID, 0);
733        bad[50] = 0xFF;
734        proc.process_data(&bad);
735        assert!(proc.stats.invalid_packets > 0);
736    }
737
738    // ── ChecksumStats tests ──────────────────────────────────────────────────
739
740    #[test]
741    fn stats_default() {
742        let stats = ChecksumStats::default();
743        assert_eq!(stats.valid_packets, 0);
744        assert_eq!(stats.invalid_packets, 0);
745        assert_eq!(stats.total_packets, 0);
746        assert_eq!(stats.error_rate(), 0.0);
747    }
748
749    #[test]
750    fn stats_error_rate_calculation() {
751        let stats = ChecksumStats {
752            valid_packets: 90,
753            invalid_packets: 10,
754            total_packets: 100,
755        };
756        assert!((stats.error_rate() - 10.0).abs() < 0.01);
757    }
758
759    #[test]
760    fn stats_error_rate_zero_packets() {
761        let stats = ChecksumStats::default();
762        assert_eq!(stats.error_rate(), 0.0); // No division by zero
763    }
764
765    #[test]
766    fn stats_error_rate_all_invalid() {
767        let stats = ChecksumStats {
768            valid_packets: 0,
769            invalid_packets: 50,
770            total_packets: 50,
771        };
772        assert!((stats.error_rate() - 100.0).abs() < 0.01);
773    }
774}