use std::collections::HashMap;
use btleplug::api::{Characteristic, PeripheralProperties, Service};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleDevice {
pub id: String,
pub address: String,
pub name: Option<String>,
pub rssi: Option<i16>,
pub tx_power: Option<i16>,
pub manufacturer_data: HashMap<String, Vec<u8>>,
pub service_data: HashMap<String, Vec<u8>>,
pub services: Vec<String>,
pub connected: bool,
}
impl BleDevice {
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,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleService {
pub uuid: String,
pub primary: bool,
pub characteristics: Vec<BleCharacteristic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleCharacteristic {
pub uuid: String,
pub service_uuid: String,
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,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BleEvent {
Device {
device: BleDevice,
},
Connected {
id: String,
},
Disconnected {
id: String,
},
StateUpdate {
state: String,
},
Notification {
id: String,
characteristic: String,
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\""));
}
}