1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static BLUETOOTH_SCAN_COMPLETE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "bluetooth_scan_complete",
18 "BLE device scan completed with list of discovered devices",
19 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
20 )
21 .with_parameters(vec![Parameter {
22 name: "devices".to_string(),
23 type_hint: "array".to_string(),
24 description: "List of discovered BLE devices with address, name, and RSSI".to_string(),
25 required: true,
26 }])
27});
28
29pub static BLUETOOTH_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31 EventType::new(
32 "bluetooth_connected",
33 "Successfully connected to BLE device",
34 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
35 )
36 .with_parameters(vec![
37 Parameter {
38 name: "device_address".to_string(),
39 type_hint: "string".to_string(),
40 description: "MAC address of connected device".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "device_name".to_string(),
45 type_hint: "string".to_string(),
46 description: "Name of connected device".to_string(),
47 required: false,
48 },
49 ])
50});
51
52pub static BLUETOOTH_SERVICES_DISCOVERED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
54 EventType::new(
55 "bluetooth_services_discovered",
56 "GATT services and characteristics discovered",
57 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
58 )
59 .with_parameters(vec![Parameter {
60 name: "services".to_string(),
61 type_hint: "array".to_string(),
62 description: "List of GATT services with their characteristics".to_string(),
63 required: true,
64 }])
65});
66
67pub static BLUETOOTH_DATA_READ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
69 EventType::new(
70 "bluetooth_data_read",
71 "Data read from BLE characteristic",
72 json!({"type": "placeholder", "event_id": "bluetooth_data_read"}),
73 )
74 .with_parameters(vec![
75 Parameter {
76 name: "service_uuid".to_string(),
77 type_hint: "string".to_string(),
78 description: "UUID of the service".to_string(),
79 required: true,
80 },
81 Parameter {
82 name: "characteristic_uuid".to_string(),
83 type_hint: "string".to_string(),
84 description: "UUID of the characteristic".to_string(),
85 required: true,
86 },
87 Parameter {
88 name: "value".to_string(),
89 type_hint: "string".to_string(),
90 description: "Human-readable value (if applicable)".to_string(),
91 required: false,
92 },
93 Parameter {
94 name: "value_hex".to_string(),
95 type_hint: "string".to_string(),
96 description: "Hex-encoded raw bytes".to_string(),
97 required: true,
98 },
99 ])
100});
101
102pub static BLUETOOTH_NOTIFICATION_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
104 EventType::new(
105 "bluetooth_notification_received",
106 "Notification received from subscribed BLE characteristic",
107 json!({"type": "write_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb", "value_hex": "01", "with_response": true}),
108 )
109 .with_parameters(vec![
110 Parameter {
111 name: "service_uuid".to_string(),
112 type_hint: "string".to_string(),
113 description: "UUID of the service".to_string(),
114 required: true,
115 },
116 Parameter {
117 name: "characteristic_uuid".to_string(),
118 type_hint: "string".to_string(),
119 description: "UUID of the characteristic".to_string(),
120 required: true,
121 },
122 Parameter {
123 name: "value".to_string(),
124 type_hint: "string".to_string(),
125 description: "Human-readable value (if applicable)".to_string(),
126 required: false,
127 },
128 Parameter {
129 name: "value_hex".to_string(),
130 type_hint: "string".to_string(),
131 description: "Hex-encoded raw bytes".to_string(),
132 required: true,
133 },
134 ])
135});
136
137pub struct BluetoothClientProtocol;
139
140impl BluetoothClientProtocol {
141 pub fn new() -> Self {
142 Self
143 }
144}
145
146impl Protocol for BluetoothClientProtocol {
148 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
149 vec![
150 ActionDefinition {
151 name: "scan_devices".to_string(),
152 description: "Scan for nearby BLE devices".to_string(),
153 parameters: vec![Parameter {
154 name: "duration_secs".to_string(),
155 type_hint: "number".to_string(),
156 description: "How long to scan in seconds (default: 5)".to_string(),
157 required: false,
158 }],
159 example: json!({
160 "type": "scan_devices",
161 "duration_secs": 5
162 }),
163 log_template: None,
164 },
165 ActionDefinition {
166 name: "connect_device".to_string(),
167 description: "Connect to a specific BLE device by address or name".to_string(),
168 parameters: vec![
169 Parameter {
170 name: "device_address".to_string(),
171 type_hint: "string".to_string(),
172 description: "MAC address of device (e.g., 'AA:BB:CC:DD:EE:FF')"
173 .to_string(),
174 required: false,
175 },
176 Parameter {
177 name: "device_name".to_string(),
178 type_hint: "string".to_string(),
179 description: "Name of device (if address not provided)".to_string(),
180 required: false,
181 },
182 ],
183 example: json!({
184 "type": "connect_device",
185 "device_address": "AA:BB:CC:DD:EE:FF"
186 }),
187 log_template: None,
188 },
189 ActionDefinition {
190 name: "discover_services".to_string(),
191 description: "Discover GATT services and characteristics on connected device"
192 .to_string(),
193 parameters: vec![],
194 example: json!({
195 "type": "discover_services"
196 }),
197 log_template: None,
198 },
199 ActionDefinition {
200 name: "disconnect".to_string(),
201 description: "Disconnect from the BLE device".to_string(),
202 parameters: vec![],
203 example: json!({
204 "type": "disconnect"
205 }),
206 log_template: None,
207 },
208 ]
209 }
210
211 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
212 vec![
213 ActionDefinition {
214 name: "read_characteristic".to_string(),
215 description: "Read value from a BLE characteristic".to_string(),
216 parameters: vec![
217 Parameter {
218 name: "service_uuid".to_string(),
219 type_hint: "string".to_string(),
220 description:
221 "UUID of the service (e.g., '0000180f-0000-1000-8000-00805f9b34fb')"
222 .to_string(),
223 required: true,
224 },
225 Parameter {
226 name: "characteristic_uuid".to_string(),
227 type_hint: "string".to_string(),
228 description: "UUID of the characteristic".to_string(),
229 required: true,
230 },
231 ],
232 example: json!({
233 "type": "read_characteristic",
234 "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb",
235 "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"
236 }),
237 log_template: None,
238 },
239 ActionDefinition {
240 name: "write_characteristic".to_string(),
241 description: "Write value to a BLE characteristic".to_string(),
242 parameters: vec![
243 Parameter {
244 name: "service_uuid".to_string(),
245 type_hint: "string".to_string(),
246 description: "UUID of the service".to_string(),
247 required: true,
248 },
249 Parameter {
250 name: "characteristic_uuid".to_string(),
251 type_hint: "string".to_string(),
252 description: "UUID of the characteristic".to_string(),
253 required: true,
254 },
255 Parameter {
256 name: "value_hex".to_string(),
257 type_hint: "string".to_string(),
258 description: "Hex-encoded bytes to write".to_string(),
259 required: true,
260 },
261 Parameter {
262 name: "with_response".to_string(),
263 type_hint: "boolean".to_string(),
264 description: "Whether to wait for write response (default: true)"
265 .to_string(),
266 required: false,
267 },
268 ],
269 example: json!({
270 "type": "write_characteristic",
271 "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb",
272 "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb",
273 "value_hex": "01",
274 "with_response": true
275 }),
276 log_template: None,
277 },
278 ActionDefinition {
279 name: "subscribe_notifications".to_string(),
280 description: "Subscribe to notifications from a BLE characteristic".to_string(),
281 parameters: vec![
282 Parameter {
283 name: "service_uuid".to_string(),
284 type_hint: "string".to_string(),
285 description: "UUID of the service".to_string(),
286 required: true,
287 },
288 Parameter {
289 name: "characteristic_uuid".to_string(),
290 type_hint: "string".to_string(),
291 description: "UUID of the characteristic".to_string(),
292 required: true,
293 },
294 ],
295 example: json!({
296 "type": "subscribe_notifications",
297 "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb",
298 "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"
299 }),
300 log_template: None,
301 },
302 ActionDefinition {
303 name: "unsubscribe_notifications".to_string(),
304 description: "Unsubscribe from notifications from a BLE characteristic".to_string(),
305 parameters: vec![
306 Parameter {
307 name: "service_uuid".to_string(),
308 type_hint: "string".to_string(),
309 description: "UUID of the service".to_string(),
310 required: true,
311 },
312 Parameter {
313 name: "characteristic_uuid".to_string(),
314 type_hint: "string".to_string(),
315 description: "UUID of the characteristic".to_string(),
316 required: true,
317 },
318 ],
319 example: json!({
320 "type": "unsubscribe_notifications",
321 "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb",
322 "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"
323 }),
324 log_template: None,
325 },
326 ]
327 }
328
329 fn protocol_name(&self) -> &'static str {
330 "Bluetooth (BLE)"
331 }
332
333 fn get_event_types(&self) -> Vec<EventType> {
334 vec![
335 EventType::new(
336 "bluetooth_scan_complete",
337 "Triggered when BLE scan completes",
338 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
339 ),
340 EventType::new(
341 "bluetooth_connected",
342 "Triggered when connected to BLE device",
343 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
344 ),
345 EventType::new(
346 "bluetooth_services_discovered",
347 "Triggered when GATT services are discovered",
348 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
349 ),
350 EventType::new(
351 "bluetooth_data_read",
352 "Triggered when data is read from characteristic",
353 json!({"type": "write_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb", "value_hex": "01", "with_response": true}),
354 ),
355 EventType::new(
356 "bluetooth_notification_received",
357 "Triggered when notification is received",
358 json!({"type": "write_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb", "value_hex": "01", "with_response": true}),
359 ),
360 EventType::new(
361 "bluetooth_disconnected",
362 "Triggered when disconnected from device",
363 json!({"type": "read_characteristic", "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb", "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"}),
364 ),
365 ]
366 }
367
368 fn stack_name(&self) -> &'static str {
369 "BLE>GATT"
370 }
371
372 fn keywords(&self) -> Vec<&'static str> {
373 vec![
374 "bluetooth",
375 "ble",
376 "bluetooth low energy",
377 "gatt",
378 "connect to bluetooth",
379 ]
380 }
381
382 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
383 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
384
385 ProtocolMetadataV2::builder()
386 .state(DevelopmentState::Experimental)
387 .implementation("btleplug library for cross-platform BLE")
388 .llm_control("Full control over scanning, connecting, service discovery, read/write/subscribe operations")
389 .e2e_testing("Real BLE device or simulator required")
390 .build()
391 }
392
393 fn description(&self) -> &'static str {
394 "Bluetooth Low Energy (BLE) client for connecting to BLE devices (Classic Bluetooth not supported)"
395 }
396
397 fn example_prompt(&self) -> &'static str {
398 "Scan for BLE devices, connect to 'Heart Rate Monitor', and read battery level"
399 }
400
401 fn group_name(&self) -> &'static str {
402 "Wireless"
403 }
404 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
405 use crate::llm::actions::StartupExamples;
406 use serde_json::json;
407
408 StartupExamples::new(
409 json!({
411 "type": "open_client",
412 "remote_addr": "ble:scan",
413 "base_stack": "bluetooth",
414 "instruction": "Scan for BLE devices, connect to heart rate monitor, and read battery level"
415 }),
416 json!({
418 "type": "open_client",
419 "remote_addr": "ble:scan",
420 "base_stack": "bluetooth",
421 "event_handlers": [{
422 "event_pattern": "bluetooth_connected",
423 "handler": {
424 "type": "script",
425 "language": "python",
426 "code": "<bluetooth_client_handler>"
427 }
428 }]
429 }),
430 json!({
432 "type": "open_client",
433 "remote_addr": "ble:scan",
434 "base_stack": "bluetooth",
435 "event_handlers": [
436 {
437 "event_pattern": "bluetooth_connected",
438 "handler": {
439 "type": "static",
440 "actions": [{
441 "type": "discover_services"
442 }]
443 }
444 },
445 {
446 "event_pattern": "bluetooth_services_discovered",
447 "handler": {
448 "type": "static",
449 "actions": [{
450 "type": "read_characteristic",
451 "service_uuid": "0000180f-0000-1000-8000-00805f9b34fb",
452 "characteristic_uuid": "00002a19-0000-1000-8000-00805f9b34fb"
453 }]
454 }
455 }
456 ]
457 }),
458 )
459 }
460}
461
462impl Client for BluetoothClientProtocol {
464 fn connect(
465 &self,
466 ctx: crate::protocol::ConnectContext,
467 ) -> std::pin::Pin<
468 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
469 > {
470 Box::pin(async move {
471 use crate::client::bluetooth::BluetoothClient;
472 BluetoothClient::connect_with_llm_actions(
473 ctx.remote_addr,
474 ctx.llm_client,
475 ctx.state,
476 ctx.status_tx,
477 ctx.client_id,
478 )
479 .await
480 })
481 }
482
483 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
484 let action_type = action
485 .get("type")
486 .and_then(|v| v.as_str())
487 .context("Missing 'type' field in action")?;
488
489 match action_type {
490 "scan_devices" => {
491 let duration_secs = action
492 .get("duration_secs")
493 .and_then(|v| v.as_u64())
494 .unwrap_or(5);
495
496 Ok(ClientActionResult::Custom {
497 name: "scan_devices".to_string(),
498 data: json!({
499 "duration_secs": duration_secs,
500 }),
501 })
502 }
503 "connect_device" => {
504 let device_address = action
505 .get("device_address")
506 .and_then(|v| v.as_str())
507 .map(|s| s.to_string());
508
509 let device_name = action
510 .get("device_name")
511 .and_then(|v| v.as_str())
512 .map(|s| s.to_string());
513
514 if device_address.is_none() && device_name.is_none() {
515 return Err(anyhow::anyhow!(
516 "Either device_address or device_name must be provided"
517 ));
518 }
519
520 Ok(ClientActionResult::Custom {
521 name: "connect_device".to_string(),
522 data: json!({
523 "device_address": device_address,
524 "device_name": device_name,
525 }),
526 })
527 }
528 "discover_services" => Ok(ClientActionResult::Custom {
529 name: "discover_services".to_string(),
530 data: json!({}),
531 }),
532 "read_characteristic" => {
533 let service_uuid = action
534 .get("service_uuid")
535 .and_then(|v| v.as_str())
536 .context("Missing 'service_uuid' field")?
537 .to_string();
538
539 let characteristic_uuid = action
540 .get("characteristic_uuid")
541 .and_then(|v| v.as_str())
542 .context("Missing 'characteristic_uuid' field")?
543 .to_string();
544
545 Ok(ClientActionResult::Custom {
546 name: "read_characteristic".to_string(),
547 data: json!({
548 "service_uuid": service_uuid,
549 "characteristic_uuid": characteristic_uuid,
550 }),
551 })
552 }
553 "write_characteristic" => {
554 let service_uuid = action
555 .get("service_uuid")
556 .and_then(|v| v.as_str())
557 .context("Missing 'service_uuid' field")?
558 .to_string();
559
560 let characteristic_uuid = action
561 .get("characteristic_uuid")
562 .and_then(|v| v.as_str())
563 .context("Missing 'characteristic_uuid' field")?
564 .to_string();
565
566 let value_hex = action
567 .get("value_hex")
568 .and_then(|v| v.as_str())
569 .context("Missing 'value_hex' field")?;
570
571 let value_bytes =
572 hex::decode(value_hex).context("Invalid hex data in value_hex")?;
573
574 let with_response = action
575 .get("with_response")
576 .and_then(|v| v.as_bool())
577 .unwrap_or(true);
578
579 Ok(ClientActionResult::Custom {
580 name: "write_characteristic".to_string(),
581 data: json!({
582 "service_uuid": service_uuid,
583 "characteristic_uuid": characteristic_uuid,
584 "value_bytes": value_bytes,
585 "with_response": with_response,
586 }),
587 })
588 }
589 "subscribe_notifications" => {
590 let service_uuid = action
591 .get("service_uuid")
592 .and_then(|v| v.as_str())
593 .context("Missing 'service_uuid' field")?
594 .to_string();
595
596 let characteristic_uuid = action
597 .get("characteristic_uuid")
598 .and_then(|v| v.as_str())
599 .context("Missing 'characteristic_uuid' field")?
600 .to_string();
601
602 Ok(ClientActionResult::Custom {
603 name: "subscribe_notifications".to_string(),
604 data: json!({
605 "service_uuid": service_uuid,
606 "characteristic_uuid": characteristic_uuid,
607 }),
608 })
609 }
610 "unsubscribe_notifications" => {
611 let service_uuid = action
612 .get("service_uuid")
613 .and_then(|v| v.as_str())
614 .context("Missing 'service_uuid' field")?
615 .to_string();
616
617 let characteristic_uuid = action
618 .get("characteristic_uuid")
619 .and_then(|v| v.as_str())
620 .context("Missing 'characteristic_uuid' field")?
621 .to_string();
622
623 Ok(ClientActionResult::Custom {
624 name: "unsubscribe_notifications".to_string(),
625 data: json!({
626 "service_uuid": service_uuid,
627 "characteristic_uuid": characteristic_uuid,
628 }),
629 })
630 }
631 "disconnect" => Ok(ClientActionResult::Disconnect),
632 _ => Err(anyhow::anyhow!(
633 "Unknown Bluetooth client action: {}",
634 action_type
635 )),
636 }
637 }
638}