use crate::protocol::ATHENA_EEG_SCALE;
use crate::types::{EegReading, ImuData, MuseEvent, PpgReading, TelemetryData, XyzSample};
pub fn decode_unsigned_12bit(data: &[u8]) -> Vec<u16> {
let mut out = Vec::with_capacity(data.len() * 2 / 3);
let mut i = 0;
while i < data.len() {
if i + 1 < data.len() {
out.push(((data[i] as u16) << 4) | ((data[i + 1] as u16) >> 4));
}
if i + 2 < data.len() {
out.push((((data[i + 1] as u16) & 0x0F) << 8) | (data[i + 2] as u16));
}
i += 3;
}
out
}
pub fn decode_eeg_samples(data: &[u8]) -> Vec<f64> {
decode_unsigned_12bit(data)
.into_iter()
.map(|n| 0.48828125 * (n as f64 - 0x800 as f64))
.collect()
}
pub fn decode_unsigned_24bit(data: &[u8]) -> Vec<u32> {
data.chunks_exact(3)
.map(|c| ((c[0] as u32) << 16) | ((c[1] as u32) << 8) | (c[2] as u32))
.collect()
}
pub fn decode_ppg_samples(data: &[u8]) -> Vec<u32> {
decode_unsigned_24bit(data)
}
pub fn parse_telemetry(data: &[u8]) -> Option<TelemetryData> {
if data.len() < 10 {
return None;
}
Some(TelemetryData {
sequence_id: u16::from_be_bytes([data[0], data[1]]),
battery_level: u16::from_be_bytes([data[2], data[3]]) as f32 / 512.0,
fuel_gauge_voltage: u16::from_be_bytes([data[4], data[5]]) as f32 * 2.2,
temperature: u16::from_be_bytes([data[8], data[9]]),
})
}
fn read_i16_be(data: &[u8], offset: usize) -> i16 {
i16::from_be_bytes([data[offset], data[offset + 1]])
}
fn parse_imu_reading(data: &[u8], scale: f32) -> Option<ImuData> {
if data.len() < 20 {
return None;
}
let seq = u16::from_be_bytes([data[0], data[1]]);
let sample = |off: usize| XyzSample {
x: scale * read_i16_be(data, off) as f32,
y: scale * read_i16_be(data, off + 2) as f32,
z: scale * read_i16_be(data, off + 4) as f32,
};
Some(ImuData {
sequence_id: seq,
samples: [sample(2), sample(8), sample(14)],
})
}
pub fn parse_accelerometer(data: &[u8]) -> Option<ImuData> {
parse_imu_reading(data, 0.0000610352)
}
pub fn parse_gyroscope(data: &[u8]) -> Option<ImuData> {
parse_imu_reading(data, 0.0074768)
}
pub fn parse_ppg_reading(data: &[u8], ppg_channel: usize, timestamp: f64) -> Option<PpgReading> {
if data.len() < 2 {
return None;
}
let index = u16::from_be_bytes([data[0], data[1]]);
let samples = decode_ppg_samples(&data[2..]);
Some(PpgReading {
index,
ppg_channel,
timestamp,
samples,
})
}
pub struct ControlAccumulator {
buffer: String,
depth: usize,
}
impl ControlAccumulator {
pub fn new() -> Self {
Self {
buffer: String::new(),
depth: 0,
}
}
pub fn push(&mut self, fragment: &str) -> Option<String> {
for ch in fragment.chars() {
if ch == '{' {
if self.depth == 0 {
self.buffer.clear();
}
self.depth += 1;
self.buffer.push(ch);
} else if ch == '}' {
if self.depth > 0 {
self.buffer.push(ch);
self.depth -= 1;
if self.depth == 0 {
let json = self.buffer.clone();
self.buffer.clear();
return Some(json);
}
}
} else {
if self.depth > 0 {
self.buffer.push(ch);
}
}
}
None
}
}
impl Default for ControlAccumulator {
fn default() -> Self {
Self::new()
}
}
fn parse_uint_le_bits(data: &[u8], bit_width: usize) -> Vec<u32> {
let n = (data.len() * 8) / bit_width;
(0..n)
.map(|i| {
let mut val = 0u32;
for bit in 0..bit_width {
let total = i * bit_width + bit;
let byte_off = total / 8;
let bit_in_byte = total % 8;
if byte_off < data.len() && (data[byte_off] >> bit_in_byte) & 1 != 0 {
val |= 1 << bit;
}
}
val
})
.collect()
}
pub fn athena_payload_len(tag: u8) -> Option<usize> {
match tag {
0x11 | 0x12 => Some(28),
0x34 => Some(30),
0x35 => Some(40),
0x36 => Some(40),
0x47 => Some(36),
0x53 => Some(24),
0x88 => None, 0x98 => Some(20),
_ => None,
}
}
pub fn parse_athena_notification(data: &[u8]) -> Vec<MuseEvent> {
const HEADER: usize = 9;
const META: usize = 4;
if data.len() < HEADER + 1 {
return vec![];
}
let pkt_len = data[0] as usize;
let mut events = Vec::new();
let mut idx = HEADER;
while idx < data.len() {
let tag = data[idx];
let payload_start = idx + 1 + META;
match tag {
0x11 => {
let end = payload_start + 28;
if end > data.len() { idx += 1; continue; }
let raw = parse_uint_le_bits(&data[payload_start..end], 14);
let n_ch = 4usize;
let n_samp = 4usize;
for ch in 0..n_ch {
let samples: Vec<f64> = (0..n_samp)
.filter_map(|s| raw.get(s * n_ch + ch))
.map(|&v| (v as f64 - 8192.0) * ATHENA_EEG_SCALE)
.collect();
events.push(MuseEvent::Eeg(EegReading {
index: 0,
electrode: ch,
timestamp: 0.0,
samples,
}));
}
idx = end;
}
0x12 => {
let end = payload_start + 28;
if end > data.len() { idx += 1; continue; }
let raw = parse_uint_le_bits(&data[payload_start..end], 14);
let n_ch = 8usize;
let n_samp = 2usize;
for ch in 0..n_ch {
let samples: Vec<f64> = (0..n_samp)
.filter_map(|s| raw.get(s * n_ch + ch))
.map(|&v| (v as f64 - 8192.0) * ATHENA_EEG_SCALE)
.collect();
events.push(MuseEvent::Eeg(EegReading {
index: 0,
electrode: ch,
timestamp: 0.0,
samples,
}));
}
idx = end;
}
0x34 => {
let end = payload_start + 30;
if end > data.len() { idx += 1; continue; }
let raw = parse_uint_le_bits(&data[payload_start..end], 20);
let n_ch = 4usize;
let n_samp = 3usize;
for ch in 0..n_ch.min(3) {
let samples: Vec<u32> = (0..n_samp)
.filter_map(|s| raw.get(s * n_ch + ch).copied())
.collect();
events.push(MuseEvent::Ppg(PpgReading {
index: 0,
ppg_channel: ch,
timestamp: 0.0,
samples,
}));
}
idx = end;
}
0x35 => {
let end = payload_start + 40;
if end > data.len() { idx += 1; continue; }
let raw = parse_uint_le_bits(&data[payload_start..end], 20);
let n_ch = 8usize;
let n_samp = 2usize;
for ch in 0..n_ch.min(3) {
let samples: Vec<u32> = (0..n_samp)
.filter_map(|s| raw.get(s * n_ch + ch).copied())
.collect();
events.push(MuseEvent::Ppg(PpgReading {
index: 0,
ppg_channel: ch,
timestamp: 0.0,
samples,
}));
}
idx = end;
}
0x36 => {
let end = payload_start + 40;
if end > data.len() { idx += 1; continue; }
idx = end;
}
0x47 => {
let end = payload_start + 36;
if end > data.len() { idx += 1; continue; }
let vals: Vec<i16> = data[payload_start..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
if vals.len() >= 6 {
const AS: f32 = 0.0000610352;
const GS: f32 = -0.0074768;
let acc = XyzSample {
x: vals[0] as f32 * AS,
y: vals[1] as f32 * AS,
z: vals[2] as f32 * AS,
};
let gyro = XyzSample {
x: vals[3] as f32 * GS,
y: vals[4] as f32 * GS,
z: vals[5] as f32 * GS,
};
events.push(MuseEvent::Accelerometer(ImuData {
sequence_id: 0,
samples: [acc, acc, acc],
}));
events.push(MuseEvent::Gyroscope(ImuData {
sequence_id: 0,
samples: [gyro, gyro, gyro],
}));
}
idx = end;
}
0x53 => {
let end = payload_start + 24;
idx = if end > data.len() { idx + 1 } else { end };
}
0x88 => {
if payload_start + 2 > data.len() { idx += 1; continue; }
let raw = u16::from_le_bytes([data[payload_start], data[payload_start + 1]]);
let battery_level = (raw as f32 / 256.0).clamp(0.0, 100.0);
events.push(MuseEvent::Telemetry(TelemetryData {
sequence_id: 0,
battery_level,
fuel_gauge_voltage: 0.0,
temperature: 0,
}));
idx = pkt_len.min(data.len());
}
0x98 => {
let end = payload_start + 20;
if end > data.len() { idx += 1; continue; }
let raw = u16::from_le_bytes([data[payload_start], data[payload_start + 1]]);
let battery_level = (raw as f32 / 256.0).clamp(0.0, 100.0);
events.push(MuseEvent::Telemetry(TelemetryData {
sequence_id: 0,
battery_level,
fuel_gauge_voltage: 0.0,
temperature: 0,
}));
idx = end;
}
_ => {
log::debug!("Athena: unknown tag 0x{tag:02x} at offset {idx}");
idx += 1;
}
}
}
events
}