netget/server/bluetooth_ble_running/
actions.rs1use crate::llm::actions::{
14 protocol_trait::{ActionResult, Protocol, Server},
15 ActionDefinition, ParameterDefinition,
16};
17use crate::protocol::EventType;
18use crate::server::bluetooth_ble::actions::BluetoothBleProtocol;
19use crate::state::app_state::AppState;
20use anyhow::Result;
21use serde_json::json;
22
23pub struct BluetoothBleRunningProtocol;
25
26impl BluetoothBleRunningProtocol {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn base() -> BluetoothBleProtocol {
33 BluetoothBleProtocol::new()
34 }
35}
36
37impl Default for BluetoothBleRunningProtocol {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Protocol for BluetoothBleRunningProtocol {
44 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
45 vec![ParameterDefinition {
46 name: "device_name".to_string(),
47 type_hint: "string".to_string(),
48 description: "Bluetooth device name to advertise (default: NetGet-Running)".to_string(),
49 required: false,
50 example: json!("NetGet-Running"),
51 default: None,
52 }]
53 }
54
55 fn get_async_actions(&self, state: &AppState) -> Vec<ActionDefinition> {
57 Self::base().get_async_actions(state)
58 }
59
60 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
63 Self::base().get_sync_actions()
64 }
65
66 fn get_event_types(&self) -> Vec<EventType> {
71 Self::base().get_event_types()
72 }
73
74 fn protocol_name(&self) -> &'static str {
75 "BLUETOOTH_BLE_RUNNING"
76 }
77
78 fn stack_name(&self) -> &'static str {
79 "BLUETOOTH_BLE_RUNNING"
80 }
81
82 fn keywords(&self) -> Vec<&'static str> {
83 vec![
84 "bluetooth",
85 "ble",
86 "running",
87 "jogging",
88 "cadence",
89 "fitness",
90 ]
91 }
92
93 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
94 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
95
96 ProtocolMetadataV2::builder()
97 .state(DevelopmentState::Experimental)
98 .implementation(
99 "bluetooth-ble base stack (ble-peripheral-rust) plus an instruction preamble for the Running Speed and Cadence Service (0x1814)",
100 )
101 .llm_control(
102 "Base BLE GATT control (add_service, start_advertising, stop_advertising, respond_to_read, respond_to_write, send_notification); the LLM builds the Running Speed and Cadence Service (0x1814) itself.",
103 )
104 .e2e_testing(
105 "Two automated suites, neither of which is evidence for a rating above Experimental. tests/server/bluetooth_ble_running/e2e_test.rs covers the wiring only: open_server reaches this protocol's spawn, the base brings the radio up, and a bluetooth_ble_started event is raised and answered - it builds no service and puts no byte on the wire, and it claims the machine's Bluetooth adapter. gatt_examples_test.rs needs no adapter and pins every UUID and value byte in the startup examples against the Bluetooth SIG layout, byte order included. Proving the profile works still needs a real central (nRF Connect, btleplug) completing a read or a subscription against a service this profile built; nothing in the tree does that.",
106 )
107 .notes(
108 "Thin profile wrapper over the bluetooth-ble base stack. It prepends an instruction describing the Running Speed and Cadence Service (0x1814) and otherwise reuses the base entirely: the base hardcodes BluetoothBleProtocol when it calls the LLM, so the action vocabulary, the event types and the executor are the base's. This protocol deliberately declares no actions or events of its own - one that did would be documented to the model but never reachable at runtime.",
109 )
110 .build()
111 }
112
113 fn description(&self) -> &'static str {
114 "BLE Running Speed and Cadence Service (0x1814) - pace, cadence and stride length"
115 }
116
117 fn example_prompt(&self) -> &'static str {
118 "Act as a running pod reporting a 5:30 min/km pace at 180 steps per minute"
119 }
120
121 fn group_name(&self) -> &'static str {
122 "Network"
123 }
124
125 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
126 use crate::llm::actions::StartupExamples;
127
128 StartupExamples::new(
132 json!({
134 "type": "open_server",
135 "port": 0,
136 "base_stack": "bluetooth-ble-running",
137 "instruction": "Act as a running pod reporting a 5:30 min/km pace at 180 steps per minute",
138 "startup_params": {
139 "device_name": "NetGet-Running"
140 }
141 }),
142 json!({
144 "type": "open_server",
145 "port": 0,
146 "base_stack": "bluetooth-ble-running",
147 "startup_params": {
148 "device_name": "NetGet-Running"
149 },
150 "event_handlers": [
151 {
152 "event_pattern": "bluetooth_read_request",
153 "handler": {
154 "type": "script",
155 "language": "python",
156 "code": "import json,sys\ne=json.load(sys.stdin)['event']\nv={'00002a54-0000-1000-8000-00805f9b34fb':'0300'}.get(str(e.get('characteristic_uuid','')).lower())\nprint(json.dumps({'actions':[{'type':'respond_to_read','value':v}] if v else []}))"
157 }
158 }
159 ]
160 }),
161 json!({
163 "type": "open_server",
164 "port": 0,
165 "base_stack": "bluetooth-ble-running",
166 "startup_params": {
167 "device_name": "NetGet-Running"
168 },
169 "event_handlers": [
170 {
171 "event_pattern": "bluetooth_ble_started",
172 "handler": {
173 "type": "static",
174 "actions": [
175 {
176 "type": "add_service",
177 "uuid": "00001814-0000-1000-8000-00805f9b34fb",
178 "primary": true,
179 "characteristics": [
180 {
181 "uuid": "00002a53-0000-1000-8000-00805f9b34fb",
182 "properties": [
183 "notify"
184 ],
185 "permissions": []
186 },
187 {
188 "uuid": "00002a54-0000-1000-8000-00805f9b34fb",
189 "properties": [
190 "read"
191 ],
192 "permissions": [
193 "readable"
194 ],
195 "initial_value": "0300"
196 }
197 ]
198 },
199 {
200 "type": "start_advertising",
201 "device_name": "NetGet-Running",
202 "service_uuids": [
203 "00001814-0000-1000-8000-00805f9b34fb"
204 ]
205 }
206 ]
207 }
208 },
209 {
210 "event_pattern": "bluetooth_read_request",
211 "handler": {
212 "type": "static",
213 "actions": [
214 {
215 "type": "respond_to_read",
216 "value": "0300"
217 }
218 ]
219 }
220 }
221 ]
222 }),
223 )
224 }
225}
226
227impl Server for BluetoothBleRunningProtocol {
228 fn spawn(
229 &self,
230 ctx: crate::protocol::SpawnContext,
231 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
232 {
233 Box::pin(async move {
234 let device_name = ctx
235 .startup_params
236 .as_ref()
237 .map(|p| p.get_optional_string("device_name"))
238 .transpose()?
239 .flatten()
240 .unwrap_or_else(|| "NetGet-Running".to_string());
241
242 let instruction = ctx
245 .state
246 .get_server(ctx.server_id)
247 .await
248 .map(|s| s.instruction)
249 .unwrap_or_default();
250
251 crate::server::bluetooth_ble_running::BluetoothBleRunning::spawn_with_llm_actions(
252 device_name,
253 ctx.llm_client,
254 ctx.state,
255 ctx.status_tx,
256 ctx.server_id,
257 instruction,
258 )
259 .await
260 })
261 }
262
263 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
266 Self::base().execute_action(action)
267 }
268}