use crate::types::{DataPacketType, EegReading};
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))
}
pub fn parse_luca_eeg_block(raw_buf: &[u8], sequence: u32) -> Option<EegReading> {
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 })
}
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);
let raw = if raw & 0x80_0000 != 0 {
raw - 0x100_0000
} else {
raw
};
channels.push(raw);
i += 3;
}
Some((counter, channels))
}
pub fn parse_battery(data: &[u8]) -> Option<u8> {
if data.len() >= 2 && data[0] == DataPacketType::Battery as u8 {
Some(data[1])
} else {
None
}
}
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
}
}
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[7] = 1;
data[28] = 42;
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() {
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() {
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() {
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)); }
#[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);
}
}