bluetooth_core 0.0.1

Cross-platform Bluetooth LE (btleplug wrapper) with a small C ABI.
Documentation
//! Wire models serialized as JSON across the C ABI.

use std::collections::HashMap;

use btleplug::api::{Characteristic, PeripheralProperties, Service};
use serde::{Deserialize, Serialize};

/// A discovered BLE peripheral.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleDevice {
    /// Peripheral identity string.
    pub id: String,
    /// Hardware address.
    pub address: String,
    /// Advertised local name.
    pub name: Option<String>,
    /// Signal strength in dBm.
    pub rssi: Option<i16>,
    /// Transmit power in dBm.
    pub tx_power: Option<i16>,
    /// Manufacturer-specific advertisement data, keyed by company id.
    pub manufacturer_data: HashMap<String, Vec<u8>>,
    /// Service advertisement data, keyed by service UUID.
    pub service_data: HashMap<String, Vec<u8>>,
    /// Advertised service UUIDs.
    pub services: Vec<String>,
    /// Connected to this peripheral.
    pub connected: bool,
}

impl BleDevice {
    /// Builds a device record from a btleplug id and its properties.
    pub fn from_properties(id: String, props: Option<PeripheralProperties>, connected: bool) -> Self {
        match props {
            Some(p) => BleDevice {
                id,
                address: p.address.to_string(),
                name: p.local_name.or(p.advertisement_name),
                rssi: p.rssi,
                tx_power: p.tx_power_level,
                manufacturer_data: p
                    .manufacturer_data
                    .into_iter()
                    .map(|(k, v)| (k.to_string(), v))
                    .collect(),
                service_data: p
                    .service_data
                    .into_iter()
                    .map(|(k, v)| (k.to_string(), v))
                    .collect(),
                services: p.services.into_iter().map(|u| u.to_string()).collect(),
                connected,
            },
            None => BleDevice {
                id,
                address: String::new(),
                name: None,
                rssi: None,
                tx_power: None,
                manufacturer_data: HashMap::new(),
                service_data: HashMap::new(),
                services: Vec::new(),
                connected,
            },
        }
    }
}

/// A GATT service with its characteristics.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleService {
    /// Service UUID.
    pub uuid: String,
    /// Primary service.
    pub primary: bool,
    /// Characteristics.
    pub characteristics: Vec<BleCharacteristic>,
}

/// A GATT characteristic.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleCharacteristic {
    /// Characteristic UUID.
    pub uuid: String,
    /// Parent service UUID.
    pub service_uuid: String,
    /// Property flags.
    pub properties: Vec<String>,
}

impl From<&Service> for BleService {
    fn from(s: &Service) -> Self {
        BleService {
            uuid: s.uuid.to_string(),
            primary: s.primary,
            characteristics: s.characteristics.iter().map(BleCharacteristic::from).collect(),
        }
    }
}

impl From<&Characteristic> for BleCharacteristic {
    fn from(c: &Characteristic) -> Self {
        use btleplug::api::CharPropFlags as F;
        let mut props = Vec::new();
        let flags = [
            (F::READ, "read"),
            (F::WRITE, "write"),
            (F::WRITE_WITHOUT_RESPONSE, "write-without-response"),
            (F::NOTIFY, "notify"),
            (F::INDICATE, "indicate"),
            (F::BROADCAST, "broadcast"),
            (F::AUTHENTICATED_SIGNED_WRITES, "authenticated-signed-writes"),
            (F::EXTENDED_PROPERTIES, "extended"),
        ];
        for (flag, label) in flags {
            if c.properties.contains(flag) {
                props.push(label.to_string());
            }
        }
        BleCharacteristic {
            uuid: c.uuid.to_string(),
            service_uuid: c.service_uuid.to_string(),
            properties: props,
        }
    }
}

/// An event from the central's event stream.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BleEvent {
    /// Peripheral discovered or updated.
    Device {
        /// The device.
        device: BleDevice,
    },
    /// Peripheral connected.
    Connected {
        /// Peripheral id.
        id: String,
    },
    /// Peripheral disconnected.
    Disconnected {
        /// Peripheral id.
        id: String,
    },
    /// Adapter state changed.
    StateUpdate {
        /// Adapter state.
        state: String,
    },
    /// Notification from a subscribed characteristic.
    Notification {
        /// Peripheral id.
        id: String,
        /// Characteristic UUID.
        characteristic: String,
        /// Payload.
        value: Vec<u8>,
    },
}

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

    #[test]
    fn device_event_round_trips_through_json() {
        let mut manufacturer_data = HashMap::new();
        manufacturer_data.insert("76".to_string(), vec![1, 2, 3]);
        let device = BleDevice {
            id: "AABB".into(),
            address: "00:11:22:33:44:55".into(),
            name: Some("Widget".into()),
            rssi: Some(-60),
            tx_power: Some(4),
            manufacturer_data,
            service_data: HashMap::new(),
            services: vec!["180d".into()],
            connected: false,
        };
        let event = BleEvent::Device { device };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("\"type\":\"device\""));
        let back: BleEvent = serde_json::from_str(&json).unwrap();
        match back {
            BleEvent::Device { device } => {
                assert_eq!(device.name.as_deref(), Some("Widget"));
                assert_eq!(device.rssi, Some(-60));
                assert_eq!(device.manufacturer_data.get("76"), Some(&vec![1, 2, 3]));
            }
            _ => panic!("wrong variant"),
        }
    }

    #[test]
    fn notification_event_tag_is_snake_case() {
        let event = BleEvent::Notification {
            id: "id".into(),
            characteristic: "2a37".into(),
            value: vec![9, 9],
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("\"type\":\"notification\""));
        assert!(json.contains("\"characteristic\":\"2a37\""));
    }
}