awear 0.2.0

Rust client for AWEAR EEG devices over BLE using btleplug
Documentation
//! Binary parsers for AWEAR BLE data packets.

use crate::types::{DataPacketType, EegReading};

/// Parse a LUCA header (36 bytes).
///
/// Returns `(data_type, sequence, payload_hint)`.
pub fn parse_luca_header(data: &[u8]) -> Option<(u32, u32, u16)> {
    if data.len() < 36 || &data[..4] != b"LUCA" {
        return None;
    }
    let data_type = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
    let sequence = u32::from_le_bytes([data[28], data[29], data[30], data[31]]);
    let payload_hint = u16::from_le_bytes([data[32], data[33]]);
    Some((data_type, sequence, payload_hint))
}

/// Finalize a LUCA EEG block from accumulated hex-encoded data.
///
/// The AWEAR device sends EEG data as ASCII hex text. Each byte of actual
/// data is encoded as 2 hex ASCII characters. The decoded bytes contain
/// 16-bit signed big-endian samples.
pub fn parse_luca_eeg_block(raw_buf: &[u8], sequence: u32) -> Option<EegReading> {
    // Try ASCII hex decode first
    let buf = match std::str::from_utf8(raw_buf) {
        Ok(hex_str) => {
            let hex_str = hex_str.trim();
            hex::decode(hex_str).unwrap_or_else(|_| raw_buf.to_vec())
        }
        Err(_) => raw_buf.to_vec(),
    };

    let bytes_per_sample = 2;
    let num_samples = buf.len() / bytes_per_sample;
    if num_samples == 0 {
        return None;
    }

    let mut samples = Vec::with_capacity(num_samples);
    for s in 0..num_samples {
        let offset = s * bytes_per_sample;
        if offset + 1 < buf.len() {
            let raw = i16::from_be_bytes([buf[offset], buf[offset + 1]]);
            samples.push(raw);
        }
    }

    Some(EegReading { sequence, samples })
}

/// Parse a legacy EEG data packet (non-LUCA).
///
/// Format: `[type(1)] [counter(1)] [ch1(3)] [ch2(3)] …`
/// Each channel is 24-bit signed big-endian.
pub fn parse_legacy_eeg(data: &[u8]) -> Option<(u8, Vec<i32>)> {
    if data.len() < 2 {
        return None;
    }
    let counter = data[1];
    let payload = &data[2..];
    let mut channels = Vec::new();
    let mut i = 0;
    while i + 2 < payload.len() {
        let raw = ((payload[i] as i32) << 16)
            | ((payload[i + 1] as i32) << 8)
            | (payload[i + 2] as i32);
        // Sign extend from 24-bit
        let raw = if raw & 0x80_0000 != 0 {
            raw - 0x100_0000
        } else {
            raw
        };
        channels.push(raw);
        i += 3;
    }
    Some((counter, channels))
}

/// Parse battery packet: `[type(1)] [level(1)]`.
pub fn parse_battery(data: &[u8]) -> Option<u8> {
    if data.len() >= 2 && data[0] == DataPacketType::Battery as u8 {
        Some(data[1])
    } else {
        None
    }
}

/// Parse signal packet: `[type(1)] [rssi(1)]` (signed i8).
pub fn parse_signal(data: &[u8]) -> Option<i8> {
    if data.len() >= 2 && data[0] == DataPacketType::Signal as u8 {
        Some(data[1] as i8)
    } else {
        None
    }
}

/// Convert battery mV to rough percentage (3200–4200mV range).
pub fn battery_mv_to_percent(mv: u32) -> u8 {
    if mv <= 3200 {
        0
    } else if mv >= 4200 {
        100
    } else {
        ((mv - 3200) / 10).min(100) as u8
    }
}

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

    #[test]
    fn luca_header_valid() {
        let mut data = [0u8; 36];
        data[..4].copy_from_slice(b"LUCA");
        // data_type = 1 (big-endian at offset 4)
        data[7] = 1;
        // sequence = 42 (little-endian at offset 28)
        data[28] = 42;
        // payload_hint = 24 (little-endian at offset 32)
        data[32] = 24;

        let (dtype, seq, hint) = parse_luca_header(&data).unwrap();
        assert_eq!(dtype, 1);
        assert_eq!(seq, 42);
        assert_eq!(hint, 24);
    }

    #[test]
    fn luca_header_wrong_magic() {
        let mut data = [0u8; 36];
        data[..4].copy_from_slice(b"NOPE");
        assert!(parse_luca_header(&data).is_none());
    }

    #[test]
    fn luca_header_too_short() {
        assert!(parse_luca_header(b"LUCA").is_none());
    }

    #[test]
    fn eeg_block_hex_encoded() {
        // Two 16-bit BE samples: 0x0100 (256) and 0xFF00 (-256)
        let hex_data = b"0100FF00";
        let reading = parse_luca_eeg_block(hex_data, 7).unwrap();
        assert_eq!(reading.sequence, 7);
        assert_eq!(reading.samples.len(), 2);
        assert_eq!(reading.samples[0], 0x0100);
        assert_eq!(reading.samples[1], -256);
    }

    #[test]
    fn eeg_block_empty() {
        assert!(parse_luca_eeg_block(b"", 0).is_none());
    }

    #[test]
    fn legacy_eeg_24bit_signed() {
        // type=0x01, counter=5, one channel: 0xFF0001 → sign-extended = -65535
        let data = [0x01, 5, 0xFF, 0x00, 0x01];
        let (counter, channels) = parse_legacy_eeg(&data).unwrap();
        assert_eq!(counter, 5);
        assert_eq!(channels.len(), 1);
        assert_eq!(channels[0], 0xFF0001_i32 - 0x1000000);
    }

    #[test]
    fn legacy_eeg_positive() {
        // type=0x01, counter=0, one channel: 0x000100 = 256
        let data = [0x01, 0, 0x00, 0x01, 0x00];
        let (_, channels) = parse_legacy_eeg(&data).unwrap();
        assert_eq!(channels[0], 256);
    }

    #[test]
    fn parse_battery_valid() {
        assert_eq!(parse_battery(&[0x02, 85]), Some(85));
    }

    #[test]
    fn parse_battery_wrong_type() {
        assert_eq!(parse_battery(&[0x01, 85]), None);
    }

    #[test]
    fn parse_signal_valid() {
        assert_eq!(parse_signal(&[0x03, 0xD0]), Some(-48)); // 0xD0 as i8
    }

    #[test]
    fn battery_mv_boundaries() {
        assert_eq!(battery_mv_to_percent(3000), 0);
        assert_eq!(battery_mv_to_percent(3200), 0);
        assert_eq!(battery_mv_to_percent(3700), 50);
        assert_eq!(battery_mv_to_percent(4200), 100);
        assert_eq!(battery_mv_to_percent(5000), 100);
    }
}