netget/server/bluetooth_ble_data_stream/
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 BluetoothBleDataStreamProtocol;
25
26impl BluetoothBleDataStreamProtocol {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn base() -> BluetoothBleProtocol {
33 BluetoothBleProtocol::new()
34 }
35}
36
37impl Default for BluetoothBleDataStreamProtocol {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Protocol for BluetoothBleDataStreamProtocol {
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-Stream)".to_string(),
49 required: false,
50 example: json!("NetGet-Stream"),
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_DATA_STREAM"
76 }
77
78 fn stack_name(&self) -> &'static str {
79 "BLUETOOTH_BLE_DATA_STREAM"
80 }
81
82 fn keywords(&self) -> Vec<&'static str> {
83 vec!["ble data stream", "ble telemetry", "ble sensor"]
88 }
89
90 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
91 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
92
93 ProtocolMetadataV2::builder()
94 .state(DevelopmentState::Experimental)
95 .implementation(
96 "bluetooth-ble base stack (ble-peripheral-rust) plus an instruction preamble for the custom streaming service",
97 )
98 .llm_control(
99 "Base BLE GATT control (add_service, start_advertising, stop_advertising, respond_to_read, respond_to_write, send_notification); the LLM builds the custom streaming service itself.",
100 )
101 .e2e_testing(
102 "Two automated suites, neither of which is evidence for a rating above Experimental. tests/server/bluetooth_ble_data_stream/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 checks the startup examples for internal coherence (this profile's UUIDs are custom, so there is no SIG layout to check the bytes against). Proving the profile works still needs a real central (nRF Connect, btleplug) exercising a service this profile built; nothing in the tree does that.",
103 )
104 .notes(
105 "Thin profile wrapper over the bluetooth-ble base stack. It prepends an instruction describing the custom streaming service 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.",
106 )
107 .build()
108 }
109
110 fn description(&self) -> &'static str {
111 "BLE data streaming - custom GATT service pushing real-time sensor/telemetry data"
112 }
113
114 fn example_prompt(&self) -> &'static str {
115 "Stream IMU accelerometer samples at 10 Hz over a custom notify characteristic"
116 }
117
118 fn group_name(&self) -> &'static str {
119 "Network"
120 }
121
122 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
123 use crate::llm::actions::StartupExamples;
124
125 StartupExamples::new(
129 json!({
131 "type": "open_server",
132 "port": 0,
133 "base_stack": "bluetooth-ble-data-stream",
134 "instruction": "Stream IMU accelerometer samples at 10 Hz over a custom notify characteristic",
135 "startup_params": {
136 "device_name": "NetGet-Stream"
137 }
138 }),
139 json!({
141 "type": "open_server",
142 "port": 0,
143 "base_stack": "bluetooth-ble-data-stream",
144 "startup_params": {
145 "device_name": "NetGet-Stream"
146 },
147 "event_handlers": [
148 {
149 "event_pattern": "bluetooth_read_request",
150 "handler": {
151 "type": "script",
152 "language": "python",
153 "code": "import json,sys\ne=json.load(sys.stdin)['event']\nv={'0000f00f-0000-1000-8000-00805f9b34fb':'00'}.get(str(e.get('characteristic_uuid','')).lower())\nprint(json.dumps({'actions':[{'type':'respond_to_read','value':v}] if v else []}))"
154 }
155 }
156 ]
157 }),
158 json!({
160 "type": "open_server",
161 "port": 0,
162 "base_stack": "bluetooth-ble-data-stream",
163 "startup_params": {
164 "device_name": "NetGet-Stream"
165 },
166 "event_handlers": [
167 {
168 "event_pattern": "bluetooth_ble_started",
169 "handler": {
170 "type": "static",
171 "actions": [
172 {
173 "type": "add_service",
174 "uuid": "0000f00d-0000-1000-8000-00805f9b34fb",
175 "primary": true,
176 "characteristics": [
177 {
178 "uuid": "0000f00e-0000-1000-8000-00805f9b34fb",
179 "properties": [
180 "notify"
181 ],
182 "permissions": []
183 },
184 {
185 "uuid": "0000f00f-0000-1000-8000-00805f9b34fb",
186 "properties": [
187 "read",
188 "write"
189 ],
190 "permissions": [
191 "readable",
192 "writeable"
193 ],
194 "initial_value": "00"
195 }
196 ]
197 },
198 {
199 "type": "start_advertising",
200 "device_name": "NetGet-Stream",
201 "service_uuids": [
202 "0000f00d-0000-1000-8000-00805f9b34fb"
203 ]
204 }
205 ]
206 }
207 },
208 {
209 "event_pattern": "bluetooth_read_request",
210 "handler": {
211 "type": "static",
212 "actions": [
213 {
214 "type": "respond_to_read",
215 "value": "01"
216 }
217 ]
218 }
219 }
220 ]
221 }),
222 )
223 }
224}
225
226impl Server for BluetoothBleDataStreamProtocol {
227 fn spawn(
228 &self,
229 ctx: crate::protocol::SpawnContext,
230 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
231 {
232 Box::pin(async move {
233 let device_name = ctx
234 .startup_params
235 .as_ref()
236 .map(|p| p.get_optional_string("device_name"))
237 .transpose()?
238 .flatten()
239 .unwrap_or_else(|| "NetGet-Stream".to_string());
240
241 let instruction = ctx
244 .state
245 .get_server(ctx.server_id)
246 .await
247 .map(|s| s.instruction)
248 .unwrap_or_default();
249
250 crate::server::bluetooth_ble_data_stream::BluetoothBleDataStream::spawn_with_llm_actions(
251 device_name,
252 ctx.llm_client,
253 ctx.state,
254 ctx.status_tx,
255 ctx.server_id,
256 instruction,
257 )
258 .await
259 })
260 }
261
262 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
265 Self::base().execute_action(action)
266 }
267}