Skip to main content

lager/nets/
ble.rs

1//! BLE (box-level): scan for and inspect Bluetooth Low Energy devices using
2//! the box's own Bluetooth adapter.
3//!
4//! There is one adapter per box and the box serializes all BLE and BluFi
5//! operations on it, so concurrent calls queue rather than fail.
6
7use super::box_handle;
8pub use crate::wire::{BleCharacteristic, BleDevice, BleDeviceInfo, BleService};
9
10pub(crate) mod ops {
11    use std::time::Duration;
12
13    use serde_json::json;
14
15    use crate::error::Result;
16    use crate::wire::{
17        box_command, unit, value_as, value_list_field, BleDevice, BleDeviceInfo, CommandResponse,
18        Op, Timeout,
19    };
20
21    const PATH: &str = "/ble/command";
22
23    /// Box-side connect timeout (s) for info/connect, matching the CLI.
24    const CONNECT_TIMEOUT_S: f64 = 10.0;
25
26    fn parse_devices(resp: CommandResponse) -> Result<Vec<BleDevice>> {
27        value_list_field(resp, "devices")
28    }
29
30    /// Budget: the box blocks for the scan/connect duration plus bleak
31    /// overhead, and may queue behind another BLE/BluFi operation on the
32    /// adapter lock.
33    fn budget(box_side: f64) -> Timeout {
34        Timeout::After(Duration::from_secs_f64(box_side + 30.0))
35    }
36
37    pub(crate) fn scan(timeout: f64) -> Op<Vec<BleDevice>> {
38        Op {
39            req: box_command(PATH, "scan", json!({ "timeout": timeout }), budget(timeout)),
40            parse: parse_devices,
41        }
42    }
43
44    pub(crate) fn scan_named(timeout: f64, name_contains: &str) -> Op<Vec<BleDevice>> {
45        Op {
46            req: box_command(
47                PATH,
48                "scan",
49                json!({ "timeout": timeout, "name_contains": name_contains }),
50                budget(timeout),
51            ),
52            parse: parse_devices,
53        }
54    }
55
56    pub(crate) fn info(address: &str) -> Op<BleDeviceInfo> {
57        Op {
58            req: box_command(
59                PATH,
60                "info",
61                json!({ "address": address, "timeout": CONNECT_TIMEOUT_S }),
62                budget(CONNECT_TIMEOUT_S),
63            ),
64            parse: value_as::<BleDeviceInfo>,
65        }
66    }
67
68    pub(crate) fn connect(address: &str) -> Op<BleDeviceInfo> {
69        Op {
70            req: box_command(
71                PATH,
72                "connect",
73                json!({ "address": address, "timeout": CONNECT_TIMEOUT_S }),
74                budget(CONNECT_TIMEOUT_S),
75            ),
76            parse: value_as::<BleDeviceInfo>,
77        }
78    }
79
80    pub(crate) fn disconnect(address: &str) -> Op<()> {
81        Op {
82            req: box_command(
83                PATH,
84                "disconnect",
85                json!({ "address": address }),
86                budget(CONNECT_TIMEOUT_S),
87            ),
88            parse: unit,
89        }
90    }
91}
92
93box_handle! {
94    /// Handle for the box's BLE adapter (from [`crate::LagerBox::ble`]).
95    sync: Ble,
96    async: AsyncBle,
97    methods: {
98        /// Scan for advertising devices for `timeout` seconds
99        /// (0.1–300, sorted named-first).
100        fn scan(timeout: f64) -> Vec<BleDevice> = ops::scan;
101        /// Scan, keeping only devices whose name contains `name_contains`
102        /// (case-insensitive).
103        fn scan_named(timeout: f64, name_contains: &str) -> Vec<BleDevice> = ops::scan_named;
104        /// Connect briefly and enumerate the device's GATT services.
105        fn info(address: &str) -> BleDeviceInfo = ops::info;
106        /// Connect to a device (and enumerate its services to verify).
107        fn connect(address: &str) -> BleDeviceInfo = ops::connect;
108        /// Ensure a device is disconnected from the box.
109        fn disconnect(address: &str) -> () = ops::disconnect;
110    }
111}