pub mod characteristic {
pub const COMMAND: &str = "273e0001-4c4d-454d-96be-f03bac821358";
pub const COMMAND_U128: u128 = 0x273e0001_4c4d_454d_96be_f03bac821358;
pub const TP9: &str = "273e0003-4c4d-454d-96be-f03bac821358";
pub const TP9_U128: u128 = 0x273e0003_4c4d_454d_96be_f03bac821358;
pub const AF7: &str = "273e0004-4c4d-454d-96be-f03bac821358";
pub const AF7_U128: u128 = 0x273e0004_4c4d_454d_96be_f03bac821358;
pub const AF8: &str = "273e0005-4c4d-454d-96be-f03bac821358";
pub const AF8_U128: u128 = 0x273e0005_4c4d_454d_96be_f03bac821358;
pub const TP10: &str = "273e0006-4c4d-454d-96be-f03bac821358";
pub const TP10_U128: u128 = 0x273e0006_4c4d_454d_96be_f03bac821358;
pub const RIGHT_AUX: &str = "273e0007-4c4d-454d-96be-f03bac821358";
pub const RIGHT_AUX_U128: u128 = 0x273e0007_4c4d_454d_96be_f03bac821358;
pub const GYRO: &str = "273e0009-4c4d-454d-96be-f03bac821358";
pub const GYRO_U128: u128 = 0x273e0009_4c4d_454d_96be_f03bac821358;
pub const ACCEL: &str = "273e000a-4c4d-454d-96be-f03bac821358";
pub const ACCEL_U128: u128 = 0x273e000a_4c4d_454d_96be_f03bac821358;
pub const TELEMETRY: &str = "273e000b-4c4d-454d-96be-f03bac821358";
pub const TELEMETRY_U128: u128 = 0x273e000b_4c4d_454d_96be_f03bac821358;
pub const EEG_CHANNELS: [u128; 4] = [TP9_U128, AF7_U128, AF8_U128, TP10_U128];
pub const EEG_CHANNEL_NAMES: [&str; 4] = ["TP9", "AF7", "AF8", "TP10"];
}
pub mod spec {
pub const SAMPLE_RATE: u16 = 256;
pub const CHANNEL_COUNT: usize = 4;
pub const SAMPLES_PER_PACKET: usize = 12;
pub const PACKET_SIZE: usize = 20;
}
pub mod command {
pub const SET_PRESET: &str = "v1";
pub const ENABLE_AUX: &str = "p21";
pub const START_STREAM: &str = "d";
pub const STOP_STREAM: &str = "h";
pub const DEVICE_INFO: &str = "?";
}
pub fn encode_eeg_packet(sequence: u16, samples: &[f32]) -> [u8; 20] {
let mut packet = [0u8; 20];
packet[0] = (sequence >> 8) as u8;
packet[1] = (sequence & 0xFF) as u8;
let mut byte_idx = 2;
let total_samples = spec::SAMPLES_PER_PACKET;
for i in (0..total_samples).step_by(2) {
let s1 = samples.get(i).copied().unwrap_or(0.0);
let s2 = samples.get(i + 1).copied().unwrap_or(0.0);
let v1 = uv_to_adc(s1);
let v2 = uv_to_adc(s2);
packet[byte_idx] = (v1 >> 4) as u8;
packet[byte_idx + 1] = ((v1 & 0x0F) << 4 | (v2 >> 8)) as u8;
packet[byte_idx + 2] = (v2 & 0xFF) as u8;
byte_idx += 3;
}
packet
}
pub fn decode_eeg_packet(packet: &[u8]) -> (u16, Vec<f32>) {
if packet.len() < 20 {
return (0, Vec::new());
}
let sequence = ((packet[0] as u16) << 8) | (packet[1] as u16);
let mut samples = Vec::with_capacity(12);
for i in (2..20).step_by(3) {
if i + 2 >= packet.len() {
break;
}
let b0 = packet[i] as u16;
let b1 = packet[i + 1] as u16;
let b2 = packet[i + 2] as u16;
let v1_raw = (b0 << 4) | (b1 >> 4);
let v2_raw = ((b1 & 0x0F) << 8) | b2;
samples.push(adc_to_uv(v1_raw));
samples.push(adc_to_uv(v2_raw));
}
(sequence, samples)
}
fn uv_to_adc(uv: f32) -> u16 {
let adc = (uv * 256.0 / 125.0) + 2048.0;
(adc.clamp(0.0, 4095.0) as u16) & 0x0FFF
}
fn adc_to_uv(adc: u16) -> f32 {
((adc as i16 - 0x800) as f32) * 125.0 / 256.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adc_conversion_roundtrip() {
let test_values = [0.0, 10.0, -10.0, 100.0, -100.0, 200.0, -200.0];
for &uv in &test_values {
let adc = uv_to_adc(uv);
let back = adc_to_uv(adc);
assert!((uv - back).abs() < 0.5, "uv={uv}, adc={adc}, back={back}");
}
}
#[test]
fn test_packet_roundtrip() {
let samples: Vec<f32> = (0..12).map(|i| (i as f32 - 6.0) * 10.0).collect();
let packet = encode_eeg_packet(42, &samples);
let (seq, decoded) = decode_eeg_packet(&packet);
assert_eq!(seq, 42);
assert_eq!(decoded.len(), 12);
for (i, (&orig, &dec)) in samples.iter().zip(decoded.iter()).enumerate() {
assert!(
(orig - dec).abs() < 0.5,
"sample {i}: orig={orig}, decoded={dec}"
);
}
}
#[test]
fn test_decode_short_packet_returns_empty() {
let (seq, samples) = decode_eeg_packet(&[0u8; 10]);
assert_eq!(seq, 0);
assert!(samples.is_empty());
}
#[test]
fn test_packet_with_short_sample_input_zero_fills() {
let samples = vec![50.0, -25.0];
let packet = encode_eeg_packet(1, &samples);
let (_seq, decoded) = decode_eeg_packet(&packet);
assert_eq!(decoded.len(), 12);
assert!((decoded[0] - 50.0).abs() < 0.5);
assert!((decoded[1] + 25.0).abs() < 0.5);
for value in decoded.iter().skip(2) {
assert!(value.abs() < 0.5);
}
}
#[test]
fn test_packet_clamps_out_of_range_samples() {
let samples = [1_000_000.0, -1_000_000.0].repeat(6);
let packet = encode_eeg_packet(7, &samples);
let (_seq, decoded) = decode_eeg_packet(&packet);
let max_uv = ((0x0FFFu16 as i16 - 0x800) as f32) * 125.0 / 256.0;
let min_uv = ((0u16 as i16 - 0x800) as f32) * 125.0 / 256.0;
assert!((decoded[0] - max_uv).abs() < 0.5);
assert!((decoded[1] - min_uv).abs() < 0.5);
}
}