mendi 0.0.2

Rust client for the Mendi neurofeedback headband over BLE using btleplug
Documentation
use mendi::parse::*;
use mendi::wire;
use prost::Message;

#[test]
fn parse_frame_roundtrip() {
    let frame = wire::Frame {
        acc_x: 100,
        acc_y: -200,
        acc_z: 300,
        ang_x: 10,
        ang_y: -20,
        ang_z: 30,
        temp: 36.5,
        ir_l: 50000,
        red_l: 40000,
        amb_l: 1000,
        ir_r: 51000,
        red_r: 41000,
        amb_r: 1100,
        ir_p: 48000,
        red_p: 38000,
        amb_p: 900,
    };
    let data = frame.encode_to_vec();
    let reading = parse_frame(&data).expect("should decode");

    assert_eq!(reading.acc_x, 100);
    assert_eq!(reading.acc_y, -200);
    assert_eq!(reading.acc_z, 300);
    assert_eq!(reading.ang_x, 10);
    assert_eq!(reading.ang_y, -20);
    assert_eq!(reading.ang_z, 30);
    assert!((reading.temperature - 36.5).abs() < 0.01);
    assert_eq!(reading.ir_left, 50000);
    assert_eq!(reading.red_left, 40000);
    assert_eq!(reading.amb_left, 1000);
    assert_eq!(reading.ir_right, 51000);
    assert_eq!(reading.red_right, 41000);
    assert_eq!(reading.amb_right, 1100);
    assert_eq!(reading.ir_pulse, 48000);
    assert_eq!(reading.red_pulse, 38000);
    assert_eq!(reading.amb_pulse, 900);
    assert!(reading.timestamp > 0.0);
}

#[test]
fn parse_frame_empty_returns_none() {
    // Empty bytes are rejected — proto3 would decode to all-zero defaults,
    // but an empty BLE notification is not a real sensor frame.
    assert!(parse_frame(&[]).is_none());
}

#[test]
fn parse_frame_single_field() {
    // A frame with just one non-default field is valid
    let frame = wire::Frame {
        temp: 25.0,
        ..Default::default()
    };
    let data = frame.encode_to_vec();
    assert!(!data.is_empty()); // protobuf encodes non-default fields
    let reading = parse_frame(&data).expect("should decode");
    assert!((reading.temperature - 25.0).abs() < 0.01);
    assert_eq!(reading.acc_x, 0); // other fields default to 0
}

#[test]
fn parse_frame_garbage_fails() {
    // Invalid protobuf bytes
    assert!(parse_frame(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}

#[test]
fn is_valid_frame_checks() {
    let frame = wire::Frame {
        ir_l: 1,
        ..Default::default()
    };
    let data = frame.encode_to_vec();
    assert!(is_valid_frame(&data));
    assert!(!is_valid_frame(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]));
}

#[test]
fn parse_adc_roundtrip() {
    let adc = wire::Adc {
        voltage: 3850,
        charging: true,
        usb: true,
    };
    let data = adc.encode_to_vec();
    let reading = parse_adc(&data).expect("should decode");
    assert_eq!(reading.voltage_mv, 3850);
    assert!(reading.charging);
    assert!(reading.usb_connected);
    assert!((reading.voltage() - 3.85).abs() < 0.01);
}

#[test]
fn parse_calibration_roundtrip() {
    let cal = wire::Calibration {
        offset_l: -5.0,
        offset_r: 10.0,
        offset_p: 0.5,
        enable: true,
        low_power_mode: false,
    };
    let data = cal.encode_to_vec();
    let reading = parse_calibration(&data).expect("should decode");
    assert!((reading.offset_left - (-5.0)).abs() < 0.01);
    assert!((reading.offset_right - 10.0).abs() < 0.01);
    assert!((reading.offset_pulse - 0.5).abs() < 0.01);
    assert!(reading.auto_calibration);
    assert!(!reading.low_power_mode);
}

#[test]
fn parse_diagnostics_with_adc() {
    let diag = wire::Diagnostics {
        adc: Some(wire::Adc {
            voltage: 4000,
            charging: false,
            usb: true,
        }),
        imu_ok: true,
        sensor_ok: true,
    };
    let data = diag.encode_to_vec();
    let reading = parse_diagnostics(&data).expect("should decode");
    assert!(reading.imu_ok);
    assert!(reading.sensor_ok);
    let adc = reading.adc.expect("should have ADC");
    assert_eq!(adc.voltage_mv, 4000);
    assert!(!adc.charging);
    assert!(adc.usb_connected);
}

#[test]
fn parse_diagnostics_without_adc() {
    let diag = wire::Diagnostics {
        adc: None,
        imu_ok: false,
        sensor_ok: true,
    };
    let data = diag.encode_to_vec();
    let reading = parse_diagnostics(&data).expect("should decode");
    assert!(!reading.imu_ok);
    assert!(reading.sensor_ok);
    assert!(reading.adc.is_none());
}

#[test]
fn parse_sensor_roundtrip() {
    let sensor = wire::Sensor {
        read: true,
        address: 0x1A,
        data: 0x00ABCDEF,
    };
    let data = sensor.encode_to_vec();
    let reading = parse_sensor(&data).expect("should decode");
    assert_eq!(reading.address, 0x1A);
    assert_eq!(reading.data, 0x00ABCDEF);
}

// ── Edge cases ───────────────────────────────────────────────────────────────

#[test]
fn is_valid_frame_empty_rejected() {
    // Empty bytes should be rejected (even though proto3 decodes [] to defaults)
    assert!(!is_valid_frame(&[]));
}

#[test]
fn parse_adc_all_zeros() {
    let adc = wire::Adc {
        voltage: 0,
        charging: false,
        usb: false,
    };
    let data = adc.encode_to_vec();
    let reading = parse_adc(&data).expect("should decode");
    assert_eq!(reading.voltage_mv, 0);
    assert!(!reading.charging);
    assert!(!reading.usb_connected);
    assert_eq!(reading.percentage(), 0);
}

#[test]
fn parse_frame_negative_optical_values() {
    // The proto uses int32, so negative values are valid on the wire
    let frame = wire::Frame {
        ir_l: -100,
        red_l: -200,
        amb_l: -50,
        ..Default::default()
    };
    let data = frame.encode_to_vec();
    let reading = parse_frame(&data).expect("should decode");
    assert_eq!(reading.ir_left, -100);
    assert_eq!(reading.red_left, -200);
    assert_eq!(reading.amb_left, -50);
}

#[test]
fn parse_frame_max_i32_values() {
    let frame = wire::Frame {
        acc_x: i32::MAX,
        acc_y: i32::MIN,
        ir_l: i32::MAX,
        ..Default::default()
    };
    let data = frame.encode_to_vec();
    let reading = parse_frame(&data).expect("should decode");
    assert_eq!(reading.acc_x, i32::MAX);
    assert_eq!(reading.acc_y, i32::MIN);
    assert_eq!(reading.ir_left, i32::MAX);
}

#[test]
fn parse_calibration_boundary_offsets() {
    let cal = wire::Calibration {
        offset_l: -127.0,
        offset_r: 127.0,
        offset_p: -127.0,
        enable: false,
        low_power_mode: true,
    };
    let data = cal.encode_to_vec();
    let reading = parse_calibration(&data).expect("should decode");
    assert!((reading.offset_left - (-127.0)).abs() < 0.01);
    assert!((reading.offset_right - 127.0).abs() < 0.01);
    assert!(!reading.auto_calibration);
    assert!(reading.low_power_mode);
}

#[test]
fn parse_adc_garbage_fails() {
    assert!(parse_adc(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}

#[test]
fn parse_calibration_garbage_fails() {
    assert!(parse_calibration(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}

#[test]
fn parse_diagnostics_garbage_fails() {
    assert!(parse_diagnostics(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}

#[test]
fn parse_sensor_garbage_fails() {
    assert!(parse_sensor(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}