1pub mod actions;
3
4pub use actions::BluetoothClientProtocol;
5
6use anyhow::{Context, Result};
7use btleplug::api::{Central, CharPropFlags, Manager as _, Peripheral as _, ScanFilter, WriteType};
8use btleplug::platform::{Adapter, Manager, Peripheral};
9use futures::future::BoxFuture;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::{mpsc, Mutex};
14use tracing::{debug, error, info, trace, warn};
15use uuid::Uuid;
16
17use crate::client::bluetooth::actions::{
18 BLUETOOTH_CONNECTED_EVENT, BLUETOOTH_DATA_READ_EVENT, BLUETOOTH_NOTIFICATION_RECEIVED_EVENT,
19 BLUETOOTH_SCAN_COMPLETE_EVENT, BLUETOOTH_SERVICES_DISCOVERED_EVENT,
20};
21use crate::client::llm_budget::call_llm_for_client;
22use crate::llm::ollama_client::OllamaClient;
23use crate::llm::ClientLlmResult;
24use crate::protocol::Event;
25use crate::state::app_state::AppState;
26use crate::state::{ClientId, ClientStatus};
27
28#[derive(Debug, Clone, PartialEq)]
30#[allow(dead_code)]
31enum ConnectionState {
32 Idle,
33 Processing,
34 Accumulating,
35}
36
37struct ClientData {
39 #[allow(dead_code)]
40 state: ConnectionState,
41 memory: String,
42 peripheral: Option<Peripheral>,
43 #[allow(dead_code)]
44 manager: Manager,
45 adapter: Adapter,
46}
47
48pub enum BluetoothApplied {
54 Sent(usize),
56 Executed(String),
58 Disconnected,
60}
61
62pub struct BluetoothClient;
64
65impl BluetoothClient {
66 pub async fn connect_with_llm_actions(
71 remote_addr: String,
72 llm_client: OllamaClient,
73 app_state: Arc<AppState>,
74 status_tx: mpsc::UnboundedSender<String>,
75 client_id: ClientId,
76 ) -> Result<SocketAddr> {
77 info!("Bluetooth client {} initializing", client_id);
78
79 let manager = Manager::new()
81 .await
82 .context("Failed to create BLE manager")?;
83
84 let adapters = manager.adapters().await?;
85 let adapter = adapters
86 .into_iter()
87 .next()
88 .context("No Bluetooth adapters found")?;
89
90 info!(
91 "Bluetooth client {} using adapter: {:?}",
92 client_id,
93 adapter.adapter_info().await?
94 );
95
96 app_state
98 .update_client_status(client_id, ClientStatus::Connected)
99 .await;
100 let _ = status_tx.send(format!(
101 "[CLIENT] Bluetooth client {} initialized",
102 client_id
103 ));
104 let _ = status_tx.send("__UPDATE_UI__".to_string());
105
106 let client_data = Arc::new(Mutex::new(ClientData {
108 state: ConnectionState::Idle,
109 memory: String::new(),
110 peripheral: None,
111 manager: manager.clone(),
112 adapter: adapter.clone(),
113 }));
114
115 let command_rx =
120 crate::client::command_support::register_command_channel(&app_state, client_id).await;
121 let cmd_task = tokio::spawn({
122 let client_data = client_data.clone();
123 let app_state = app_state.clone();
124 let status_tx = status_tx.clone();
125 let llm_client = llm_client.clone();
126 async move {
127 Self::command_loop(
128 command_rx,
129 client_data,
130 app_state,
131 status_tx,
132 llm_client,
133 client_id,
134 )
135 .await;
136 }
137 });
138 app_state.register_client_task(client_id, cmd_task).await;
139
140 let client_data_clone = client_data.clone();
142 let app_state_clone = app_state.clone();
143 let status_tx_clone = status_tx.clone();
144 let llm_client_clone = llm_client.clone();
145
146 let task_registrar = app_state.clone();
149 let task_handle = tokio::spawn(async move {
150 if remote_addr.is_empty() || remote_addr == "scan" {
152 if let Err(e) = Self::perform_scan(
153 &client_data_clone,
154 &app_state_clone,
155 &status_tx_clone,
156 &llm_client_clone,
157 client_id,
158 5,
159 )
160 .await
161 {
162 error!("Bluetooth scan error: {}", e);
163 }
164 } else {
165 if let Err(e) = Self::connect_to_device(
167 &client_data_clone,
168 &app_state_clone,
169 &status_tx_clone,
170 &llm_client_clone,
171 client_id,
172 Some(remote_addr.clone()),
173 None,
174 )
175 .await
176 {
177 error!("Bluetooth connection error: {}", e);
178 app_state_clone
179 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
180 .await;
181 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
182 }
183 }
184
185 loop {
187 tokio::time::sleep(Duration::from_millis(100)).await;
188
189 if let Some(client) = app_state_clone.get_client(client_id).await {
191 if matches!(
192 client.status,
193 ClientStatus::Disconnected | ClientStatus::Error(_)
194 ) {
195 break;
196 }
197 } else {
198 break;
199 }
200 }
201
202 info!("Bluetooth client {} event loop ended", client_id);
203 app_state_clone.remove_client_handle(client_id).await;
206 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
207 });
208 task_registrar
209 .register_client_task(client_id, task_handle)
210 .await;
211
212 Ok("0.0.0.0:0".parse().unwrap())
214 }
215
216 async fn command_loop(
226 mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
227 client_data: Arc<Mutex<ClientData>>,
228 app_state: Arc<AppState>,
229 status_tx: mpsc::UnboundedSender<String>,
230 llm_client: OllamaClient,
231 client_id: ClientId,
232 ) {
233 use crate::llm::actions::protocol_trait::Protocol;
234 use crate::state::client_handles::ClientSendOutcome;
235 use crate::state::AccessLogOwner;
236
237 let protocol = BluetoothClientProtocol::new();
238
239 while let Some(command) = command_rx.recv().await {
240 let action = command.action.clone();
241
242 let decoded = {
247 use crate::llm::actions::client_trait::Client;
248 protocol.execute_action(action.clone())
249 };
250 let outcome = match decoded {
251 Err(e) => Ok(ClientSendOutcome::Rejected {
252 error: e.to_string(),
253 }),
254 Ok(_) => Self::execute_llm_action(
255 action.clone(),
256 &client_data,
257 &app_state,
258 &status_tx,
259 &llm_client,
260 client_id,
261 )
262 .await
263 .map(|applied| match applied {
264 BluetoothApplied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
265 BluetoothApplied::Executed(detail) => ClientSendOutcome::Executed { detail },
266 BluetoothApplied::Disconnected => ClientSendOutcome::Disconnected,
267 }),
268 };
269
270 let outcome_json = match &outcome {
271 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
272 Err(e) => serde_json::json!({"error": e.to_string()}),
273 };
274 app_state
275 .record_access_log(
276 AccessLogOwner::Client(client_id.as_u32()),
277 protocol.protocol_name(),
278 None,
279 "injected_action",
280 action,
281 vec![outcome_json],
282 )
283 .await;
284
285 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
286 if let Err(e) = &outcome {
287 error!(
288 "Bluetooth client {} injected action failed: {}",
289 client_id, e
290 );
291 let _ = status_tx.send(format!(
292 "[WARN] Client {} injected action failed: {}",
293 client_id, e
294 ));
295 }
296 let _ = status_tx.send("__UPDATE_UI__".to_string());
297 crate::client::command_support::reply(command, outcome);
298
299 if disconnect {
300 break;
301 }
302 }
303
304 app_state.remove_client_handle(client_id).await;
305 let _ = status_tx.send("__UPDATE_UI__".to_string());
306 }
307
308 async fn perform_scan(
310 client_data: &Arc<Mutex<ClientData>>,
311 app_state: &Arc<AppState>,
312 status_tx: &mpsc::UnboundedSender<String>,
313 llm_client: &OllamaClient,
314 client_id: ClientId,
315 duration_secs: u64,
316 ) -> Result<usize> {
317 let adapter = {
318 let data = client_data.lock().await;
319 data.adapter.clone()
320 };
321
322 info!(
323 "Bluetooth client {} starting scan for {} seconds",
324 client_id, duration_secs
325 );
326 let _ = status_tx.send(format!("[CLIENT] Scanning for BLE devices..."));
327
328 adapter.start_scan(ScanFilter::default()).await?;
329 tokio::time::sleep(Duration::from_secs(duration_secs)).await;
330 adapter.stop_scan().await?;
331
332 let peripherals = adapter.peripherals().await?;
334
335 let mut devices = Vec::new();
336 for peripheral in peripherals {
337 if let Ok(Some(props)) = peripheral.properties().await {
338 let device_info = serde_json::json!({
339 "address": props.address.to_string(),
340 "name": props.local_name.unwrap_or_else(|| "Unknown".to_string()),
341 "rssi": props.rssi,
342 });
343 devices.push(device_info);
344 }
345 }
346
347 info!(
348 "Bluetooth client {} found {} devices",
349 client_id,
350 devices.len()
351 );
352 let _ = status_tx.send(format!("[CLIENT] Found {} BLE devices", devices.len()));
353 let device_count = devices.len();
354
355 let protocol = Arc::new(BluetoothClientProtocol::new());
357 let event = Event::new(
358 &BLUETOOTH_SCAN_COMPLETE_EVENT,
359 serde_json::json!({
360 "devices": devices,
361 }),
362 );
363
364 let memory_snapshot = client_data.lock().await.memory.clone();
368 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
369 match call_llm_for_client(
370 llm_client,
371 app_state,
372 client_id.to_string(),
373 &instruction,
374 &memory_snapshot,
375 Some(&event),
376 protocol.as_ref(),
377 status_tx,
378 )
379 .await
380 {
381 Ok(ClientLlmResult {
382 actions,
383 memory_updates,
384 }) => {
385 if let Some(mem) = memory_updates {
387 client_data.lock().await.memory = mem;
388 }
389
390 for action in actions {
392 if let Err(e) = Self::execute_llm_action(
393 action,
394 client_data,
395 app_state,
396 status_tx,
397 llm_client,
398 client_id,
399 )
400 .await
401 {
402 error!("Error executing Bluetooth action: {}", e);
403 }
404 }
405 }
406 Err(e) => {
407 error!("LLM error for Bluetooth client {}: {}", client_id, e);
408 }
409 }
410 }
411
412 Ok(device_count)
413 }
414
415 fn connect_to_device<'a>(
417 client_data: &'a Arc<Mutex<ClientData>>,
418 app_state: &'a Arc<AppState>,
419 status_tx: &'a mpsc::UnboundedSender<String>,
420 llm_client: &'a OllamaClient,
421 client_id: ClientId,
422 device_address: Option<String>,
423 device_name: Option<String>,
424 ) -> BoxFuture<'a, Result<()>> {
425 Box::pin(async move {
426 let adapter = {
427 let data = client_data.lock().await;
428 data.adapter.clone()
429 };
430
431 if device_name.is_some() {
433 info!("Bluetooth client {} scanning for device by name", client_id);
434 adapter.start_scan(ScanFilter::default()).await?;
435 tokio::time::sleep(Duration::from_secs(5)).await;
436 adapter.stop_scan().await?;
437 }
438
439 let peripherals = adapter.peripherals().await?;
440
441 let mut target_peripheral: Option<Peripheral> = None;
443 for peripheral in peripherals {
444 if let Ok(Some(props)) = peripheral.properties().await {
445 let matches = if let Some(ref addr) = device_address {
446 props.address.to_string().eq_ignore_ascii_case(addr)
447 } else if let Some(ref name) = device_name {
448 props
449 .local_name
450 .as_ref()
451 .map_or(false, |n| n.contains(name))
452 } else {
453 false
454 };
455
456 if matches {
457 target_peripheral = Some(peripheral);
458 break;
459 }
460 }
461 }
462
463 let peripheral = target_peripheral.context("Device not found")?;
464
465 info!("Bluetooth client {} connecting to device", client_id);
467 let _ = status_tx.send(format!("[CLIENT] Connecting to BLE device..."));
468
469 peripheral.connect().await?;
470 peripheral.discover_services().await?;
471
472 let device_props = peripheral
473 .properties()
474 .await?
475 .context("No device properties")?;
476 let device_addr = device_props.address.to_string();
477 let device_name_str = device_props
478 .local_name
479 .unwrap_or_else(|| "Unknown".to_string());
480
481 info!(
482 "Bluetooth client {} connected to {} ({})",
483 client_id, device_name_str, device_addr
484 );
485 let _ = status_tx.send(format!("[CLIENT] Connected to {}", device_name_str));
486
487 {
489 let mut data = client_data.lock().await;
490 data.peripheral = Some(peripheral.clone());
491 }
492
493 let client_data_clone = client_data.clone();
495 let app_state_clone = app_state.clone();
496 let status_tx_clone = status_tx.clone();
497 let llm_client_clone = llm_client.clone();
498 let peripheral_clone = peripheral.clone();
499
500 let task_owner = app_state.clone();
503 task_owner
504 .spawn_client_task(client_id, async move {
505 match peripheral_clone.notifications().await {
506 Ok(mut notification_stream) => {
507 use futures::StreamExt;
508 while let Some(notification) = notification_stream.next().await {
509 let client_data = client_data_clone.clone();
510 let app_state = app_state_clone.clone();
511 let status_tx = status_tx_clone.clone();
512 let llm_client = llm_client_clone.clone();
513
514 trace!(
515 "Bluetooth notification received from {:?}",
516 notification.uuid
517 );
518
519 let protocol = Arc::new(BluetoothClientProtocol::new());
521 let event = Event::new(
522 &BLUETOOTH_NOTIFICATION_RECEIVED_EVENT,
523 serde_json::json!({
524 "service_uuid": "unknown", "characteristic_uuid": notification.uuid.to_string(),
526 "value_hex": hex::encode(¬ification.value),
527 }),
528 );
529
530 let memory_snapshot = client_data.lock().await.memory.clone();
534 if let Some(instruction) =
535 app_state.get_instruction_for_client(client_id).await
536 {
537 match call_llm_for_client(
538 &llm_client,
539 &app_state,
540 client_id.to_string(),
541 &instruction,
542 &memory_snapshot,
543 Some(&event),
544 protocol.as_ref(),
545 &status_tx,
546 )
547 .await
548 {
549 Ok(ClientLlmResult {
550 actions,
551 memory_updates,
552 }) => {
553 if let Some(mem) = memory_updates {
555 client_data.lock().await.memory = mem;
556 }
557
558 for action in actions {
560 if let Err(e) = Self::execute_llm_action(
561 action,
562 &client_data,
563 &app_state,
564 &status_tx,
565 &llm_client,
566 client_id,
567 )
568 .await
569 {
570 error!(
571 "Error executing Bluetooth action: {}",
572 e
573 );
574 }
575 }
576 }
577 Err(e) => {
578 error!("LLM error for Bluetooth notification: {}", e);
579 }
580 }
581 }
582 }
583 }
584 Err(e) => {
585 error!("Failed to get notification stream: {}", e);
586 }
587 }
588 })
589 .await;
590
591 let protocol = Arc::new(BluetoothClientProtocol::new());
593 let event = Event::new(
594 &BLUETOOTH_CONNECTED_EVENT,
595 serde_json::json!({
596 "device_address": device_addr,
597 "device_name": device_name_str,
598 }),
599 );
600
601 let memory_snapshot = client_data.lock().await.memory.clone();
605 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
606 match call_llm_for_client(
607 llm_client,
608 app_state,
609 client_id.to_string(),
610 &instruction,
611 &memory_snapshot,
612 Some(&event),
613 protocol.as_ref(),
614 status_tx,
615 )
616 .await
617 {
618 Ok(ClientLlmResult {
619 actions,
620 memory_updates,
621 }) => {
622 if let Some(mem) = memory_updates {
624 client_data.lock().await.memory = mem;
625 }
626
627 for action in actions {
629 if let Err(e) = Self::execute_llm_action(
630 action,
631 client_data,
632 app_state,
633 status_tx,
634 llm_client,
635 client_id,
636 )
637 .await
638 {
639 error!("Error executing Bluetooth action: {}", e);
640 }
641 }
642 }
643 Err(e) => {
644 error!("LLM error for Bluetooth client {}: {}", client_id, e);
645 }
646 }
647 }
648
649 Ok(())
650 })
651 }
652
653 fn execute_llm_action<'a>(
655 action: serde_json::Value,
656 client_data: &'a Arc<Mutex<ClientData>>,
657 app_state: &'a Arc<AppState>,
658 status_tx: &'a mpsc::UnboundedSender<String>,
659 llm_client: &'a OllamaClient,
660 client_id: ClientId,
661 ) -> BoxFuture<'a, Result<BluetoothApplied>> {
662 Box::pin(async move {
663 use crate::llm::actions::client_trait::Client;
664 let protocol = BluetoothClientProtocol::new();
665
666 match protocol.execute_action(action)? {
667 crate::llm::actions::client_trait::ClientActionResult::Custom { name, data } => {
668 match name.as_str() {
669 "scan_devices" => {
670 let duration_secs = data["duration_secs"].as_u64().unwrap_or(5);
671 let found = Box::pin(Self::perform_scan(
672 client_data,
673 app_state,
674 status_tx,
675 llm_client,
676 client_id,
677 duration_secs,
678 ))
679 .await?;
680 Ok(BluetoothApplied::Executed(format!(
681 "scan_devices ({duration_secs}s): {found} devices found"
682 )))
683 }
684 "connect_device" => {
685 let device_address =
686 data["device_address"].as_str().map(|s| s.to_string());
687 let device_name = data["device_name"].as_str().map(|s| s.to_string());
688 let target = device_address
689 .clone()
690 .or_else(|| device_name.clone())
691 .unwrap_or_else(|| "<unspecified>".to_string());
692 Self::connect_to_device(
693 client_data,
694 app_state,
695 status_tx,
696 llm_client,
697 client_id,
698 device_address,
699 device_name,
700 )
701 .await?;
702 Ok(BluetoothApplied::Executed(format!(
703 "connect_device {target}: connected"
704 )))
705 }
706 "discover_services" => {
707 let count = Self::discover_services(
708 client_data,
709 app_state,
710 status_tx,
711 llm_client,
712 client_id,
713 )
714 .await?;
715 Ok(BluetoothApplied::Executed(format!(
716 "discover_services: {count} services"
717 )))
718 }
719 "read_characteristic" => {
720 let service_uuid = Uuid::parse_str(
721 data["service_uuid"]
722 .as_str()
723 .context("Missing service_uuid")?,
724 )?;
725 let char_uuid = Uuid::parse_str(
726 data["characteristic_uuid"]
727 .as_str()
728 .context("Missing characteristic_uuid")?,
729 )?;
730 let read = Self::read_characteristic(
731 client_data,
732 app_state,
733 status_tx,
734 llm_client,
735 client_id,
736 service_uuid,
737 char_uuid,
738 )
739 .await?;
740 Ok(BluetoothApplied::Executed(format!(
741 "read_characteristic {char_uuid}: {read} bytes read"
742 )))
743 }
744 "write_characteristic" => {
745 let service_uuid = Uuid::parse_str(
746 data["service_uuid"]
747 .as_str()
748 .context("Missing service_uuid")?,
749 )?;
750 let char_uuid = Uuid::parse_str(
751 data["characteristic_uuid"]
752 .as_str()
753 .context("Missing characteristic_uuid")?,
754 )?;
755 let value_bytes = crate::client::wire_values::bytes(
756 data["value_bytes"]
757 .as_array()
758 .context("Missing value_bytes")?,
759 )?;
760 let with_response = data["with_response"].as_bool().unwrap_or(true);
761 let written = value_bytes.len();
762 Self::write_characteristic(
763 client_data,
764 service_uuid,
765 char_uuid,
766 value_bytes,
767 with_response,
768 )
769 .await?;
770 Ok(BluetoothApplied::Sent(written))
773 }
774 "subscribe_notifications" => {
775 let service_uuid = Uuid::parse_str(
776 data["service_uuid"]
777 .as_str()
778 .context("Missing service_uuid")?,
779 )?;
780 let char_uuid = Uuid::parse_str(
781 data["characteristic_uuid"]
782 .as_str()
783 .context("Missing characteristic_uuid")?,
784 )?;
785 Self::subscribe_notifications(client_data, service_uuid, char_uuid)
786 .await?;
787 Ok(BluetoothApplied::Executed(format!(
788 "subscribe_notifications {char_uuid}: subscribed"
789 )))
790 }
791 "unsubscribe_notifications" => {
792 let service_uuid = Uuid::parse_str(
793 data["service_uuid"]
794 .as_str()
795 .context("Missing service_uuid")?,
796 )?;
797 let char_uuid = Uuid::parse_str(
798 data["characteristic_uuid"]
799 .as_str()
800 .context("Missing characteristic_uuid")?,
801 )?;
802 Self::unsubscribe_notifications(client_data, service_uuid, char_uuid)
803 .await?;
804 Ok(BluetoothApplied::Executed(format!(
805 "unsubscribe_notifications {char_uuid}: unsubscribed"
806 )))
807 }
808 _ => {
809 warn!("Unknown custom action: {}", name);
810 Ok(BluetoothApplied::Executed(format!(
811 "custom result '{name}' has no Bluetooth handler"
812 )))
813 }
814 }
815 }
816 crate::llm::actions::client_trait::ClientActionResult::Disconnect => {
817 Self::disconnect(client_data, app_state, client_id).await?;
818 app_state.remove_client_handle(client_id).await;
822 let _ = status_tx.send("__UPDATE_UI__".to_string());
823 Ok(BluetoothApplied::Disconnected)
824 }
825 other => {
826 debug!("Unhandled action result type");
827 Ok(BluetoothApplied::Executed(format!(
828 "unhandled action result {other:?}"
829 )))
830 }
831 }
832 })
833 }
834
835 async fn discover_services(
837 client_data: &Arc<Mutex<ClientData>>,
838 app_state: &Arc<AppState>,
839 status_tx: &mpsc::UnboundedSender<String>,
840 llm_client: &OllamaClient,
841 client_id: ClientId,
842 ) -> Result<usize> {
843 let peripheral = {
844 let data = client_data.lock().await;
845 data.peripheral.clone().context("Not connected to device")?
846 };
847
848 info!("Bluetooth client {} discovering services", client_id);
849
850 let services = peripheral.services();
851 let mut services_data = Vec::new();
852
853 for service in services {
854 let mut characteristics_data = Vec::new();
855
856 for char in service.characteristics {
857 let mut properties = Vec::new();
858 if char.properties.contains(CharPropFlags::READ) {
859 properties.push("read".to_string());
860 }
861 if char.properties.contains(CharPropFlags::WRITE) {
862 properties.push("write".to_string());
863 }
864 if char
865 .properties
866 .contains(CharPropFlags::WRITE_WITHOUT_RESPONSE)
867 {
868 properties.push("write_without_response".to_string());
869 }
870 if char.properties.contains(CharPropFlags::NOTIFY) {
871 properties.push("notify".to_string());
872 }
873 if char.properties.contains(CharPropFlags::INDICATE) {
874 properties.push("indicate".to_string());
875 }
876
877 characteristics_data.push(serde_json::json!({
878 "uuid": char.uuid.to_string(),
879 "properties": properties,
880 }));
881 }
882
883 services_data.push(serde_json::json!({
884 "uuid": service.uuid.to_string(),
885 "primary": service.primary,
886 "characteristics": characteristics_data,
887 }));
888 }
889
890 info!(
891 "Bluetooth client {} discovered {} services",
892 client_id,
893 services_data.len()
894 );
895 let service_count = services_data.len();
896
897 let protocol = Arc::new(BluetoothClientProtocol::new());
899 let event = Event::new(
900 &BLUETOOTH_SERVICES_DISCOVERED_EVENT,
901 serde_json::json!({
902 "services": services_data,
903 }),
904 );
905
906 let memory_snapshot = client_data.lock().await.memory.clone();
910 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
911 match call_llm_for_client(
912 llm_client,
913 app_state,
914 client_id.to_string(),
915 &instruction,
916 &memory_snapshot,
917 Some(&event),
918 protocol.as_ref(),
919 status_tx,
920 )
921 .await
922 {
923 Ok(ClientLlmResult {
924 actions,
925 memory_updates,
926 }) => {
927 if let Some(mem) = memory_updates {
929 client_data.lock().await.memory = mem;
930 }
931
932 for action in actions {
934 if let Err(e) = Self::execute_llm_action(
935 action,
936 client_data,
937 app_state,
938 status_tx,
939 llm_client,
940 client_id,
941 )
942 .await
943 {
944 error!("Error executing Bluetooth action: {}", e);
945 }
946 }
947 }
948 Err(e) => {
949 error!("LLM error for Bluetooth client {}: {}", client_id, e);
950 }
951 }
952 }
953
954 Ok(service_count)
955 }
956
957 async fn read_characteristic(
959 client_data: &Arc<Mutex<ClientData>>,
960 app_state: &Arc<AppState>,
961 status_tx: &mpsc::UnboundedSender<String>,
962 llm_client: &OllamaClient,
963 client_id: ClientId,
964 service_uuid: Uuid,
965 char_uuid: Uuid,
966 ) -> Result<usize> {
967 let peripheral = {
968 let data = client_data.lock().await;
969 data.peripheral.clone().context("Not connected to device")?
970 };
971
972 debug!(
973 "Reading characteristic {} from service {}",
974 char_uuid, service_uuid
975 );
976
977 let services = peripheral.services();
979 let characteristic = services
980 .iter()
981 .find(|s| s.uuid == service_uuid)
982 .and_then(|s| s.characteristics.iter().find(|c| c.uuid == char_uuid))
983 .context("Characteristic not found")?;
984
985 let value = peripheral.read(characteristic).await?;
986
987 info!(
988 "Read {} bytes from characteristic {}",
989 value.len(),
990 char_uuid
991 );
992
993 let protocol = Arc::new(BluetoothClientProtocol::new());
995 let event = Event::new(
996 &BLUETOOTH_DATA_READ_EVENT,
997 serde_json::json!({
998 "service_uuid": service_uuid.to_string(),
999 "characteristic_uuid": char_uuid.to_string(),
1000 "value_hex": hex::encode(&value),
1001 }),
1002 );
1003
1004 let memory_snapshot = client_data.lock().await.memory.clone();
1008 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
1009 match call_llm_for_client(
1010 llm_client,
1011 app_state,
1012 client_id.to_string(),
1013 &instruction,
1014 &memory_snapshot,
1015 Some(&event),
1016 protocol.as_ref(),
1017 status_tx,
1018 )
1019 .await
1020 {
1021 Ok(ClientLlmResult {
1022 actions,
1023 memory_updates,
1024 }) => {
1025 if let Some(mem) = memory_updates {
1027 client_data.lock().await.memory = mem;
1028 }
1029
1030 for action in actions {
1032 if let Err(e) = Self::execute_llm_action(
1033 action,
1034 client_data,
1035 app_state,
1036 status_tx,
1037 llm_client,
1038 client_id,
1039 )
1040 .await
1041 {
1042 error!("Error executing Bluetooth action: {}", e);
1043 }
1044 }
1045 }
1046 Err(e) => {
1047 error!("LLM error for Bluetooth client {}: {}", client_id, e);
1048 }
1049 }
1050 }
1051
1052 Ok(value.len())
1053 }
1054
1055 async fn write_characteristic(
1057 client_data: &Arc<Mutex<ClientData>>,
1058 service_uuid: Uuid,
1059 char_uuid: Uuid,
1060 value: Vec<u8>,
1061 with_response: bool,
1062 ) -> Result<()> {
1063 let peripheral = {
1064 let data = client_data.lock().await;
1065 data.peripheral.clone().context("Not connected to device")?
1066 };
1067
1068 debug!(
1069 "Writing {} bytes to characteristic {} from service {}",
1070 value.len(),
1071 char_uuid,
1072 service_uuid
1073 );
1074
1075 let services = peripheral.services();
1077 let characteristic = services
1078 .iter()
1079 .find(|s| s.uuid == service_uuid)
1080 .and_then(|s| s.characteristics.iter().find(|c| c.uuid == char_uuid))
1081 .context("Characteristic not found")?;
1082
1083 let write_type = if with_response {
1084 WriteType::WithResponse
1085 } else {
1086 WriteType::WithoutResponse
1087 };
1088
1089 peripheral.write(characteristic, &value, write_type).await?;
1090
1091 info!(
1092 "Wrote {} bytes to characteristic {}",
1093 value.len(),
1094 char_uuid
1095 );
1096
1097 Ok(())
1098 }
1099
1100 async fn subscribe_notifications(
1102 client_data: &Arc<Mutex<ClientData>>,
1103 service_uuid: Uuid,
1104 char_uuid: Uuid,
1105 ) -> Result<()> {
1106 let peripheral = {
1107 let data = client_data.lock().await;
1108 data.peripheral.clone().context("Not connected to device")?
1109 };
1110
1111 debug!(
1112 "Subscribing to characteristic {} from service {}",
1113 char_uuid, service_uuid
1114 );
1115
1116 let services = peripheral.services();
1118 let characteristic = services
1119 .iter()
1120 .find(|s| s.uuid == service_uuid)
1121 .and_then(|s| s.characteristics.iter().find(|c| c.uuid == char_uuid))
1122 .context("Characteristic not found")?;
1123
1124 peripheral.subscribe(characteristic).await?;
1125
1126 info!(
1127 "Subscribed to notifications from characteristic {}",
1128 char_uuid
1129 );
1130
1131 Ok(())
1132 }
1133
1134 async fn unsubscribe_notifications(
1136 client_data: &Arc<Mutex<ClientData>>,
1137 service_uuid: Uuid,
1138 char_uuid: Uuid,
1139 ) -> Result<()> {
1140 let peripheral = {
1141 let data = client_data.lock().await;
1142 data.peripheral.clone().context("Not connected to device")?
1143 };
1144
1145 debug!(
1146 "Unsubscribing from characteristic {} from service {}",
1147 char_uuid, service_uuid
1148 );
1149
1150 let services = peripheral.services();
1152 let characteristic = services
1153 .iter()
1154 .find(|s| s.uuid == service_uuid)
1155 .and_then(|s| s.characteristics.iter().find(|c| c.uuid == char_uuid))
1156 .context("Characteristic not found")?;
1157
1158 peripheral.unsubscribe(characteristic).await?;
1159
1160 info!(
1161 "Unsubscribed from notifications from characteristic {}",
1162 char_uuid
1163 );
1164
1165 Ok(())
1166 }
1167
1168 async fn disconnect(
1170 client_data: &Arc<Mutex<ClientData>>,
1171 app_state: &Arc<AppState>,
1172 client_id: ClientId,
1173 ) -> Result<()> {
1174 let peripheral = {
1175 let mut data = client_data.lock().await;
1176 data.peripheral.take()
1177 };
1178
1179 if let Some(peripheral) = peripheral {
1180 peripheral.disconnect().await?;
1181 info!("Bluetooth client {} disconnected", client_id);
1182 app_state
1183 .update_client_status(client_id, ClientStatus::Disconnected)
1184 .await;
1185 }
1186
1187 Ok(())
1188 }
1189}