Skip to main content

netget/server/bluetooth_ble_mouse/
mod.rs

1//! Bluetooth Low Energy (BLE) HID Mouse implementation
2//!
3//! Builds on bluetooth-ble to provide HID over GATT mouse functionality.
4//! Supports connection tracking and targeted messages to specific devices.
5
6pub mod actions;
7
8use anyhow::Result;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use tracing::info;
12
13use crate::console_error;
14use crate::llm::ollama_client::OllamaClient;
15use crate::server::bluetooth_ble::BluetoothBle;
16use crate::state::app_state::AppState;
17
18/// BLE HID Mouse server.
19///
20/// A unit struct: this profile owns no state. `ble-peripheral-rust` 0.2 gives the
21/// peripheral no per-central identity, and the base stack emits no connect or
22/// disconnect events, so there is nothing a connection table could be keyed on or
23/// populated from.
24pub struct BluetoothBleMouse;
25
26impl BluetoothBleMouse {
27    /// Spawn BLE HID mouse server
28    #[cfg(all(unix, feature = "bluetooth"))]
29    pub async fn spawn_with_llm_actions(
30        device_name: String,
31        llm_client: OllamaClient,
32        app_state: Arc<AppState>,
33        status_tx: mpsc::UnboundedSender<String>,
34        server_id: crate::state::ServerId,
35        instruction: String,
36    ) -> Result<std::net::SocketAddr> {
37        info!("Starting BLE HID Mouse server: {}", device_name);
38
39        // Create the underlying BLE server with HID mouse configuration
40        let mouse_instruction = format!(
41            "Configure as a BLE HID mouse with HID Service (UUID: 0x1812). {} {}",
42            instruction,
43            "Add HID Report Map, HID Report Input, HID Information, and HID Control Point characteristics for mouse."
44        );
45
46        // Use the base bluetooth-ble server.
47        //
48        // This profile decides nothing once the radio is up: the base stack owns the event
49        // loop, and every `decision=` a read, write or subscribe produces is emitted there
50        // against `BluetoothBleProtocol`. The one terminal outcome the profile does own is
51        // this — the shared radio would not come up, so no HID service was ever published.
52        let log_tx = status_tx.clone();
53        let log_name = device_name.clone();
54        match BluetoothBle::spawn_with_llm_actions(
55            device_name,
56            llm_client,
57            app_state,
58            status_tx,
59            server_id,
60            mouse_instruction,
61        )
62        .await
63        {
64            Ok(addr) => Ok(addr),
65            Err(e) => {
66                console_error!(log_tx, "{}", radio_start_failure(&log_name, &e));
67                Err(e)
68            }
69        }
70    }
71}
72
73/// The single source of the text used when the shared BLE radio could not be brought up.
74///
75/// `pub` so a test can assert the `decision=` tag without claiming a Bluetooth adapter: this is
76/// the profile's only terminal outcome of its own, and the project forbids `#[cfg(test)]`
77/// modules in `src/`. No model is consulted on this path — the radio fails before any event
78/// exists — so the token is deliberately not one of the `fail_closed_llm_*` pair, which would
79/// send someone to restart Ollama over an adapter that is switched off.
80pub fn radio_start_failure(device_name: &str, err: &anyhow::Error) -> String {
81    format!(
82        "BLE HID mouse '{device_name}' could not start \
83         (decision=refused_adapter_unavailable): {err}. No HID service was published and \
84         nothing is advertising."
85    )
86}
87
88#[cfg(any(all(unix, not(feature = "bluetooth")), not(unix)))]
89impl BluetoothBleMouse {
90    pub async fn spawn_with_llm_actions(
91        _device_name: String,
92        _llm_client: OllamaClient,
93        _app_state: Arc<AppState>,
94        _status_tx: mpsc::UnboundedSender<String>,
95        _server_id: crate::state::ServerId,
96        _instruction: String,
97    ) -> Result<std::net::SocketAddr> {
98        anyhow::bail!("BLE mouse support not enabled - compile with --features bluetooth-ble-mouse")
99    }
100}
101
102/// HID mouse button bits
103pub mod hid_mouse_buttons {
104    pub const BUTTON_LEFT: u8 = 0x01;
105    pub const BUTTON_RIGHT: u8 = 0x02;
106    pub const BUTTON_MIDDLE: u8 = 0x04;
107}
108
109/// Length in bytes of the input report [`HID_MOUSE_REPORT_DESCRIPTOR`] describes.
110///
111/// One byte of three button bits plus five padding bits, then one signed byte each of X, Y
112/// and Wheel. The startup examples in `actions.rs` size the input report characteristic's
113/// initial value from it.
114pub const HID_MOUSE_INPUT_REPORT_LEN: usize = 4;
115
116/// HID Report Descriptor for mouse.
117///
118/// This is the single source of truth for the report map: `actions.rs` hex-encodes these
119/// bytes into its startup examples rather than carrying a second copy, and
120/// `tests/server/bluetooth_ble_mouse/report_descriptor_test.rs` walks every item and
121/// asserts the total is `HID_MOUSE_INPUT_REPORT_LEN` bytes.
122///
123/// X, Y and Wheel are **signed** and **relative**: Logical Minimum is `0x81` (-127) and
124/// Logical Maximum `0x7F` (127), and the Input item sets the Relative bit (0x06, not 0x02).
125/// Both matter — a host reading these as unsigned sees every leftward movement as a large
126/// rightward one.
127pub const HID_MOUSE_REPORT_DESCRIPTOR: &[u8] = &[
128    0x05, 0x01, // Usage Page (Generic Desktop)
129    0x09, 0x02, // Usage (Mouse)
130    0xA1, 0x01, // Collection (Application)
131    0x09, 0x01, //   Usage (Pointer)
132    0xA1, 0x00, //   Collection (Physical)
133    0x05, 0x09, //     Usage Page (Buttons)
134    0x19, 0x01, //     Usage Minimum (1)
135    0x29, 0x03, //     Usage Maximum (3)
136    0x15, 0x00, //     Logical Minimum (0)
137    0x25, 0x01, //     Logical Maximum (1)
138    0x95, 0x03, //     Report Count (3)
139    0x75, 0x01, //     Report Size (1)
140    0x81, 0x02, //     Input (Data, Variable, Absolute) - Button bits
141    0x95, 0x01, //     Report Count (1)
142    0x75, 0x05, //     Report Size (5)
143    0x81, 0x01, //     Input (Constant) - Padding
144    0x05, 0x01, //     Usage Page (Generic Desktop)
145    0x09, 0x30, //     Usage (X)
146    0x09, 0x31, //     Usage (Y)
147    0x09, 0x38, //     Usage (Wheel)
148    0x15, 0x81, //     Logical Minimum (-127)
149    0x25, 0x7F, //     Logical Maximum (127)
150    0x75, 0x08, //     Report Size (8)
151    0x95, 0x03, //     Report Count (3)
152    0x81, 0x06, //     Input (Data, Variable, Relative) - X, Y, Wheel
153    0xC0, //   End Collection (Physical)
154    0xC0, // End Collection (Application)
155];