Skip to main content

fission_core/
platform_bluetooth.rs

1//! Bluetooth host capabilities.
2
3use crate::capability::{CapabilityType, OperationCapability};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
7pub enum BluetoothPermission {
8    #[default]
9    Unknown,
10    Granted,
11    Denied,
12    Restricted,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub enum BluetoothMode {
17    Classic,
18    LowEnergy,
19}
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
22pub struct BluetoothDevice {
23    pub id: String,
24    pub name: Option<String>,
25    pub address: Option<String>,
26    pub rssi: Option<i16>,
27    pub paired: bool,
28    pub modes: Vec<BluetoothMode>,
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
32pub struct BluetoothAvailability {
33    pub permission: BluetoothPermission,
34    pub enabled: bool,
35    pub supports_classic: bool,
36    pub supports_low_energy: bool,
37}
38
39#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct BluetoothPermissionRequest {
41    pub reason: Option<String>,
42}
43
44#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct BluetoothScanRequest {
46    pub service_uuids: Vec<String>,
47    pub timeout_ms: Option<u64>,
48    pub include_paired: bool,
49    pub allow_duplicates: bool,
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53pub struct BluetoothScanResult {
54    pub devices: Vec<BluetoothDevice>,
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct BluetoothConnectRequest {
59    pub device_id: String,
60    pub service_uuids: Vec<String>,
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64pub struct BluetoothConnection {
65    pub connection_id: String,
66    pub device: BluetoothDevice,
67}
68
69#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
70pub struct BluetoothDisconnectRequest {
71    pub connection_id: String,
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
75pub struct BluetoothReadRequest {
76    pub connection_id: String,
77    pub service_uuid: String,
78    pub characteristic_uuid: String,
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
82pub struct BluetoothReadResult {
83    pub value: Vec<u8>,
84}
85
86#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
87pub struct BluetoothWriteRequest {
88    pub connection_id: String,
89    pub service_uuid: String,
90    pub characteristic_uuid: String,
91    pub value: Vec<u8>,
92    pub with_response: bool,
93}
94
95#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
96pub struct BluetoothAdvertiseRequest {
97    pub service_uuid: String,
98    pub service_data: Vec<u8>,
99    pub local_name: Option<String>,
100    pub timeout_ms: Option<u64>,
101}
102
103#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
104pub struct BluetoothAdvertiseReceipt {
105    pub advertisement_id: String,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
109pub struct BluetoothStopAdvertiseRequest {
110    pub advertisement_id: String,
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
114pub struct BluetoothError {
115    pub code: String,
116    pub message: String,
117}
118
119impl BluetoothError {
120    /// Creates a portable bluetooth error payload.
121    ///
122    /// `code` should be a stable, machine-readable reason such as
123    /// `unsupported`, `permission_denied`, or `timeout`. `message` should be a
124    /// concise human-readable explanation suitable for logs or developer-facing
125    /// diagnostics.
126    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
127        Self {
128            code: code.into(),
129            message: message.into(),
130        }
131    }
132
133    /// Creates the standard unsupported-operation error for this capability.
134    ///
135    /// `operation` should name the attempted bluetooth operation. Use this
136    /// from hosts that implement the capability contract but cannot provide this
137    /// operation on the current platform or hardware.
138    pub fn unsupported(operation: impl Into<String>) -> Self {
139        Self::new(
140            "unsupported",
141            format!(
142                "Bluetooth operation `{}` is not supported by this host",
143                operation.into()
144            ),
145        )
146    }
147}
148
149pub struct GetBluetoothAvailabilityCapability;
150impl OperationCapability for GetBluetoothAvailabilityCapability {
151    type Request = ();
152    type Ok = BluetoothAvailability;
153    type Err = BluetoothError;
154}
155
156pub struct RequestBluetoothPermissionCapability;
157impl OperationCapability for RequestBluetoothPermissionCapability {
158    type Request = BluetoothPermissionRequest;
159    type Ok = BluetoothPermission;
160    type Err = BluetoothError;
161}
162
163pub struct ScanBluetoothDevicesCapability;
164impl OperationCapability for ScanBluetoothDevicesCapability {
165    type Request = BluetoothScanRequest;
166    type Ok = BluetoothScanResult;
167    type Err = BluetoothError;
168}
169
170pub struct ConnectBluetoothDeviceCapability;
171impl OperationCapability for ConnectBluetoothDeviceCapability {
172    type Request = BluetoothConnectRequest;
173    type Ok = BluetoothConnection;
174    type Err = BluetoothError;
175}
176
177pub struct DisconnectBluetoothDeviceCapability;
178impl OperationCapability for DisconnectBluetoothDeviceCapability {
179    type Request = BluetoothDisconnectRequest;
180    type Ok = ();
181    type Err = BluetoothError;
182}
183
184pub struct ReadBluetoothCharacteristicCapability;
185impl OperationCapability for ReadBluetoothCharacteristicCapability {
186    type Request = BluetoothReadRequest;
187    type Ok = BluetoothReadResult;
188    type Err = BluetoothError;
189}
190
191pub struct WriteBluetoothCharacteristicCapability;
192impl OperationCapability for WriteBluetoothCharacteristicCapability {
193    type Request = BluetoothWriteRequest;
194    type Ok = ();
195    type Err = BluetoothError;
196}
197
198pub struct StartBluetoothAdvertisingCapability;
199impl OperationCapability for StartBluetoothAdvertisingCapability {
200    type Request = BluetoothAdvertiseRequest;
201    type Ok = BluetoothAdvertiseReceipt;
202    type Err = BluetoothError;
203}
204
205pub struct StopBluetoothAdvertisingCapability;
206impl OperationCapability for StopBluetoothAdvertisingCapability {
207    type Request = BluetoothStopAdvertiseRequest;
208    type Ok = ();
209    type Err = BluetoothError;
210}
211
212pub const GET_BLUETOOTH_AVAILABILITY: CapabilityType<GetBluetoothAvailabilityCapability> =
213    CapabilityType::new("fission.bluetooth.get_availability");
214pub const REQUEST_BLUETOOTH_PERMISSION: CapabilityType<RequestBluetoothPermissionCapability> =
215    CapabilityType::new("fission.bluetooth.request_permission");
216pub const SCAN_BLUETOOTH_DEVICES: CapabilityType<ScanBluetoothDevicesCapability> =
217    CapabilityType::new("fission.bluetooth.scan_devices");
218pub const CONNECT_BLUETOOTH_DEVICE: CapabilityType<ConnectBluetoothDeviceCapability> =
219    CapabilityType::new("fission.bluetooth.connect_device");
220pub const DISCONNECT_BLUETOOTH_DEVICE: CapabilityType<DisconnectBluetoothDeviceCapability> =
221    CapabilityType::new("fission.bluetooth.disconnect_device");
222pub const READ_BLUETOOTH_CHARACTERISTIC: CapabilityType<ReadBluetoothCharacteristicCapability> =
223    CapabilityType::new("fission.bluetooth.read_characteristic");
224pub const WRITE_BLUETOOTH_CHARACTERISTIC: CapabilityType<WriteBluetoothCharacteristicCapability> =
225    CapabilityType::new("fission.bluetooth.write_characteristic");
226pub const START_BLUETOOTH_ADVERTISING: CapabilityType<StartBluetoothAdvertisingCapability> =
227    CapabilityType::new("fission.bluetooth.start_advertising");
228pub const STOP_BLUETOOTH_ADVERTISING: CapabilityType<StopBluetoothAdvertisingCapability> =
229    CapabilityType::new("fission.bluetooth.stop_advertising");
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn bluetooth_scan_request_round_trips() {
237        let request = BluetoothScanRequest {
238            service_uuids: vec!["180D".into()],
239            timeout_ms: Some(10_000),
240            include_paired: true,
241            allow_duplicates: false,
242        };
243
244        let bytes = serde_json::to_vec(&request).unwrap();
245        let decoded: BluetoothScanRequest = serde_json::from_slice(&bytes).unwrap();
246
247        assert_eq!(decoded, request);
248    }
249
250    #[test]
251    fn bluetooth_connection_round_trips() {
252        let connection = BluetoothConnection {
253            connection_id: "connection-1".into(),
254            device: BluetoothDevice {
255                id: "device-1".into(),
256                name: Some("Heart monitor".into()),
257                address: Some("00:11:22:33:44:55".into()),
258                rssi: Some(-42),
259                paired: true,
260                modes: vec![BluetoothMode::LowEnergy],
261            },
262        };
263
264        let bytes = serde_json::to_vec(&connection).unwrap();
265        let decoded: BluetoothConnection = serde_json::from_slice(&bytes).unwrap();
266
267        assert_eq!(decoded, connection);
268    }
269}