neurosky 0.0.1

Rust library and TUI for NeuroSky MindWave EEG headsets via the ThinkGear serial protocol
Documentation
//! ThinkGear binary protocol parser.
//!
//! Implements the [ThinkGear Communications Protocol](http://developer.neurosky.com/docs/doku.php?id=thinkgear_communications_protocol).
//!
//! ## Packet format
//!
//! ```text
//! [SYNC] [SYNC] [PLENGTH] [PAYLOAD...] [CHECKSUM]
//!  0xAA   0xAA   1 byte    N bytes      1 byte
//! ```
//!
//! Checksum = bitwise NOT of sum of payload bytes (low 8 bits).

use crate::types::*;

/// Parser state machine for the ThinkGear protocol.
pub struct Parser {
    state: State,
    payload: Vec<u8>,
    plength: u8,
    checksum: u8,
}

enum State {
    NoSync,
    FirstSync,
    ReadLength,
    ReadPayload,
}

impl Parser {
    pub fn new() -> Self {
        Parser {
            state: State::NoSync,
            payload: Vec::with_capacity(256),
            plength: 0,
            checksum: 0,
        }
    }

    /// Feed one byte. Returns parsed packets when a complete valid packet is decoded.
    pub fn parse_byte(&mut self, byte: u8) -> Option<Vec<Packet>> {
        match self.state {
            State::NoSync => {
                if byte == SYNC_BYTE {
                    self.state = State::FirstSync;
                }
                None
            }
            State::FirstSync => {
                if byte == SYNC_BYTE {
                    self.state = State::ReadLength;
                } else {
                    self.state = State::NoSync;
                }
                None
            }
            State::ReadLength => {
                if byte > MAX_PAYLOAD_LENGTH {
                    self.state = State::NoSync;
                    None
                } else {
                    self.plength = byte;
                    self.payload.clear();
                    self.checksum = 0;
                    self.state = State::ReadPayload;
                    None
                }
            }
            State::ReadPayload => {
                if self.payload.len() < self.plength as usize {
                    self.payload.push(byte);
                    self.checksum = self.checksum.wrapping_add(byte);
                    None
                } else {
                    // This byte is the checksum
                    self.state = State::NoSync;
                    let expected = !self.checksum;
                    if byte == expected {
                        Some(Self::decode_payload(&self.payload))
                    } else {
                        log::debug!("Checksum mismatch: got 0x{:02X}, expected 0x{:02X}", byte, expected);
                        None
                    }
                }
            }
        }
    }

    /// Feed a slice of bytes, collecting all decoded packets.
    pub fn parse(&mut self, data: &[u8]) -> Vec<Packet> {
        let mut packets = Vec::new();
        for &byte in data {
            if let Some(mut p) = self.parse_byte(byte) {
                packets.append(&mut p);
            }
        }
        packets
    }

    /// Decode a validated payload into packets.
    fn decode_payload(payload: &[u8]) -> Vec<Packet> {
        let mut packets = Vec::new();
        let mut i = 0;
        while i < payload.len() {
            let code = payload[i];
            i += 1;

            if code >= 0x80 {
                // Multi-byte: next byte is length
                if i >= payload.len() { break; }
                let len = payload[i] as usize;
                i += 1;
                if i + len > payload.len() { break; }
                let data = &payload[i..i + len];
                i += len;

                match code {
                    CODE_RAW_VALUE => {
                        if let Some(v) = decode_raw_value(data) {
                            packets.push(Packet::RawValue(v));
                        }
                    }
                    CODE_ASIC_EEG => {
                        if let Some(eeg) = decode_asic_eeg(data) {
                            packets.push(Packet::AsicEeg(eeg));
                        }
                    }
                    CODE_HEADSET_CONNECTED if len >= 2 => {
                        let id = (data[0] as u16) << 8 | data[1] as u16;
                        packets.push(Packet::HeadsetConnected(id));
                    }
                    CODE_HEADSET_NOT_FOUND => {
                        packets.push(Packet::HeadsetNotFound);
                    }
                    CODE_HEADSET_DISCONNECTED => {
                        packets.push(Packet::HeadsetDisconnected);
                    }
                    CODE_STANDBY => {
                        packets.push(Packet::Standby);
                    }
                    CODE_REQUEST_DENIED => {
                        packets.push(Packet::RequestDenied);
                    }
                    _ => {
                        log::debug!("Unknown multi-byte code: 0x{:02X}", code);
                    }
                }
            } else {
                // Single-byte: next byte is value
                if i >= payload.len() { break; }
                let value = payload[i];
                i += 1;

                match code {
                    CODE_POOR_SIGNAL => packets.push(Packet::PoorSignal(value)),
                    CODE_ATTENTION => packets.push(Packet::Attention(value)),
                    CODE_MEDITATION => packets.push(Packet::Meditation(value)),
                    CODE_BLINK => packets.push(Packet::Blink(value)),
                    _ => {
                        log::debug!("Unknown single-byte code: 0x{:02X}", code);
                    }
                }
            }
        }
        packets
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_packet(payload: &[u8]) -> Vec<u8> {
        let checksum = !payload.iter().fold(0u8, |a, &b| a.wrapping_add(b));
        let mut pkt = vec![0xAA, 0xAA, payload.len() as u8];
        pkt.extend_from_slice(payload);
        pkt.push(checksum);
        pkt
    }

    #[test]
    fn test_parse_attention() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_ATTENTION, 75]);
        let packets = parser.parse(&data);
        assert_eq!(packets.len(), 1);
        match &packets[0] {
            Packet::Attention(v) => assert_eq!(*v, 75),
            other => panic!("Expected Attention, got {:?}", other),
        }
    }

    #[test]
    fn test_parse_meditation() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_MEDITATION, 50]);
        let packets = parser.parse(&data);
        assert_eq!(packets.len(), 1);
        match &packets[0] { Packet::Meditation(v) => assert_eq!(*v, 50), _ => panic!() }
    }

    #[test]
    fn test_parse_poor_signal() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_POOR_SIGNAL, 200]);
        let packets = parser.parse(&data);
        match &packets[0] { Packet::PoorSignal(v) => assert_eq!(*v, 200), _ => panic!() }
    }

    #[test]
    fn test_parse_blink() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_BLINK, 128]);
        let packets = parser.parse(&data);
        match &packets[0] { Packet::Blink(v) => assert_eq!(*v, 128), _ => panic!() }
    }

    #[test]
    fn test_parse_raw_value() {
        let mut parser = Parser::new();
        // Raw: code=0x80, len=2, data=0x01 0x00 → 256
        let data = make_packet(&[CODE_RAW_VALUE, 0x02, 0x01, 0x00]);
        let packets = parser.parse(&data);
        match &packets[0] { Packet::RawValue(v) => assert_eq!(*v, 256), _ => panic!() }
    }

    #[test]
    fn test_parse_raw_value_negative() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_RAW_VALUE, 0x02, 0xFF, 0xFE]);
        let packets = parser.parse(&data);
        match &packets[0] { Packet::RawValue(v) => assert_eq!(*v, -2), _ => panic!() }
    }

    #[test]
    fn test_parse_asic_eeg() {
        let mut parser = Parser::new();
        let mut payload = vec![CODE_ASIC_EEG, 24]; // code + length
        // 8 bands × 3 bytes each = 24 bytes
        for i in 0..8u32 {
            let v = (i + 1) * 1000;
            payload.push((v >> 16) as u8);
            payload.push((v >> 8) as u8);
            payload.push(v as u8);
        }
        let data = make_packet(&payload);
        let packets = parser.parse(&data);
        match &packets[0] {
            Packet::AsicEeg(eeg) => {
                assert_eq!(eeg.delta, 1000);
                assert_eq!(eeg.theta, 2000);
                assert_eq!(eeg.low_alpha, 3000);
                assert_eq!(eeg.mid_gamma, 8000);
            }
            _ => panic!()
        }
    }

    #[test]
    fn test_parse_headset_connected() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_HEADSET_CONNECTED, 0x02, 0xAB, 0xCD]);
        let packets = parser.parse(&data);
        match &packets[0] { Packet::HeadsetConnected(id) => assert_eq!(*id, 0xABCD), _ => panic!() }
    }

    #[test]
    fn test_parse_standby() {
        let mut parser = Parser::new();
        let data = make_packet(&[CODE_STANDBY, 0x00]);
        let packets = parser.parse(&data);
        assert!(matches!(&packets[0], Packet::Standby));
    }

    #[test]
    fn test_bad_checksum_rejected() {
        let mut parser = Parser::new();
        let mut data = make_packet(&[CODE_ATTENTION, 75]);
        *data.last_mut().unwrap() ^= 0xFF; // corrupt checksum
        let packets = parser.parse(&data);
        assert!(packets.is_empty());
    }

    #[test]
    fn test_multiple_packets_in_stream() {
        let mut parser = Parser::new();
        let mut data = make_packet(&[CODE_ATTENTION, 80]);
        data.extend_from_slice(&make_packet(&[CODE_MEDITATION, 60]));
        let packets = parser.parse(&data);
        assert_eq!(packets.len(), 2);
    }

    #[test]
    fn test_noise_before_sync() {
        let mut parser = Parser::new();
        let mut data = vec![0x00, 0xFF, 0x42]; // garbage
        data.extend_from_slice(&make_packet(&[CODE_ATTENTION, 90]));
        let packets = parser.parse(&data);
        assert_eq!(packets.len(), 1);
        match &packets[0] { Packet::Attention(v) => assert_eq!(*v, 90), _ => panic!() }
    }
}