awear 0.2.0

Rust client for AWEAR EEG devices over BLE using btleplug
Documentation
//! AWEAR BLE protocol constants, UUIDs, and command helpers.

use uuid::Uuid;

/// AWEAR BLE service UUID.
pub const AWEAR_SERVICE_UUID: Uuid =
    Uuid::from_u128(0xFC740001_0291_41FC_A9B0_45951B5B01D7);

/// TX characteristic — write commands to device.
pub const AWEAR_TX_CHARACTERISTIC: Uuid =
    Uuid::from_u128(0xFC740002_0291_41FC_A9B0_45951B5B01D7);

/// RX characteristic — notifications from device.
pub const AWEAR_RX_CHARACTERISTIC: Uuid =
    Uuid::from_u128(0xFC740003_0291_41FC_A9B0_45951B5B01D7);

/// Nordic DFU service UUID.
pub const NORDIC_DFU_SERVICE_UUID: Uuid =
    Uuid::from_u128(0x8EC90001_F315_4F60_9FB8_838830DAEA50);

/// LUCA protocol magic bytes.
pub const LUCA_MAGIC: &[u8; 4] = b"LUCA";

/// LUCA header size in bytes.
pub const LUCA_HEADER_SIZE: usize = 36;

/// AWEAR connected prefix in handshake messages.
pub const AWEAR_CONNECTED_PREFIX: &str = "AWEAR_CONNECTED:";

/// AWEAR ready prefix.
pub const AWEAR_READY_PREFIX: &str = "AWEAR_READY:";

/// Challenge reply prefix.
pub const CHALLENGE_REPLY_PREFIX: &str = "CRPL:";

/// Magic constant for challenge-response HMAC.
pub const CHALLENGE_CONSTANT: u32 = 0xD4C3_B2A1;

/// 16-byte symmetric key for challenge-response HMAC.
pub const CHALLENGE_SYMMETRIC_KEY: [u8; 16] = [
    0x4e, 0xe1, 0xfb, 0xef, 0x87, 0x98, 0xe9, 0x4a,
    0x17, 0xbb, 0x94, 0x58, 0x98, 0xc1, 0x89, 0x8f,
];

/// EEG sampling rate (Hz).
pub const EEG_FREQUENCY: f64 = 256.0;

/// EEG sample bit depth.
pub const EEG_SAMPLE_BITS: usize = 16;

/// Max valid EEG value (24-bit signed).
pub const EEG_MAX_VALUE: i32 = 8_388_607;

/// Min valid EEG value (24-bit signed).
pub const EEG_MIN_VALUE: i32 = -8_388_608;

/// Default BLE scan timeout (seconds).
pub const SCAN_TIMEOUT_SECS: u64 = 10;

/// Connection timeout (seconds).
pub const CONNECT_TIMEOUT_SECS: u64 = 30;

/// Reconnection timeout (seconds).
pub const RECONNECT_TIMEOUT_SECS: u64 = 10;

/// Max reconnection attempts.
pub const RECONNECTION_MAX_RETRIES: u32 = 10;

/// Compute the AWEAR challenge-response HMAC.
///
/// 1. Parse the 8-char hex challenge → u32
/// 2. Byte-reverse (big-endian) → 4 bytes
/// 3. Append CHALLENGE_CONSTANT in little-endian → 8-byte message
/// 4. HMAC-SHA256(key=symmetric_key, message)
/// 5. Take first 8 bytes, format as uppercase hex
/// 6. Return "CRPL:<hex>"
pub fn compute_challenge_reply(challenge_hex: &str) -> Option<String> {
    let challenge_val = u32::from_str_radix(challenge_hex, 16).ok()?;

    let mut message = [0u8; 8];
    message[..4].copy_from_slice(&challenge_val.to_be_bytes());
    message[4..8].copy_from_slice(&CHALLENGE_CONSTANT.to_le_bytes());

    use hmac::{Hmac, Mac};
    use sha2::Sha256;

    type HmacSha256 = Hmac<Sha256>;
    let mut mac = HmacSha256::new_from_slice(&CHALLENGE_SYMMETRIC_KEY).ok()?;
    mac.update(&message);
    let result = mac.finalize().into_bytes();

    let reply_hex: String = result[..8]
        .iter()
        .map(|b| format!("{:02X}", b))
        .collect();

    Some(format!("{}{}", CHALLENGE_REPLY_PREFIX, reply_hex))
}

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

    #[test]
    fn challenge_reply_returns_crpl_prefix() {
        let reply = compute_challenge_reply("AABBCCDD").unwrap();
        assert!(reply.starts_with("CRPL:"));
        // CRPL: + 16 hex chars
        assert_eq!(reply.len(), 5 + 16);
    }

    #[test]
    fn challenge_reply_deterministic() {
        let a = compute_challenge_reply("12345678").unwrap();
        let b = compute_challenge_reply("12345678").unwrap();
        assert_eq!(a, b);
    }

    #[test]
    fn challenge_reply_different_inputs_differ() {
        let a = compute_challenge_reply("00000000").unwrap();
        let b = compute_challenge_reply("FFFFFFFF").unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn challenge_reply_invalid_hex_returns_none() {
        assert!(compute_challenge_reply("ZZZZZZZZ").is_none());
        assert!(compute_challenge_reply("").is_none());
    }

    #[test]
    fn uuids_are_distinct() {
        assert_ne!(AWEAR_SERVICE_UUID, AWEAR_TX_CHARACTERISTIC);
        assert_ne!(AWEAR_TX_CHARACTERISTIC, AWEAR_RX_CHARACTERISTIC);
    }
}