1pub mod actions;
9
10pub use actions::MqttClientProtocol;
11
12use anyhow::{Context, Result};
13use rumqttc::{AsyncClient, Event, EventLoop, MqttOptions, Packet, QoS};
14use std::net::SocketAddr;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::mpsc;
19use tracing::{debug, error, info, trace, warn};
20
21use crate::protocol::StartupParams;
22
23use crate::client::llm_budget::call_llm_for_client;
24use crate::client::mqtt::actions::{MQTT_CLIENT_CONNECTED_EVENT, MQTT_MESSAGE_RECEIVED_EVENT};
25use crate::llm::actions::client_trait::{Client, ClientActionResult};
26use crate::llm::ollama_client::OllamaClient;
27use crate::llm::ClientLlmResult;
28use crate::protocol::Event as ProtocolEvent;
29use crate::state::app_state::AppState;
30use crate::state::{ClientId, ClientStatus};
31
32pub struct MqttClient;
34
35impl MqttClient {
36 pub async fn connect_with_llm_actions(
38 remote_addr: String,
39 llm_client: OllamaClient,
40 app_state: Arc<AppState>,
41 status_tx: mpsc::UnboundedSender<String>,
42 client_id: ClientId,
43 startup_params: Option<StartupParams>,
44 ) -> Result<SocketAddr> {
45 let parts: Vec<&str> = remote_addr.split(':').collect();
47 let host = parts
48 .get(0)
49 .context("Missing host in remote_addr")?
50 .to_string();
51 let port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(1883);
52
53 let mqtt_client_id = startup_params
55 .as_ref()
56 .map(|p| p.get_optional_string("client_id"))
57 .transpose()?
58 .flatten()
59 .unwrap_or_else(|| format!("netget-{}", client_id));
60
61 let username = startup_params
62 .as_ref()
63 .map(|p| p.get_optional_string("username"))
64 .transpose()?
65 .flatten();
66
67 let password = startup_params
68 .as_ref()
69 .map(|p| p.get_optional_string("password"))
70 .transpose()?
71 .flatten();
72
73 let keep_alive = startup_params
74 .as_ref()
75 .map(|p| p.get_optional_u64("keep_alive"))
76 .transpose()?
77 .flatten()
78 .unwrap_or(60);
79
80 let clean_session = startup_params
81 .as_ref()
82 .map(|p| p.get_optional_bool("clean_session"))
83 .transpose()?
84 .flatten()
85 .unwrap_or(true);
86
87 let version = startup_params
88 .as_ref()
89 .map(|p| p.get_optional_string("protocol_version"))
90 .transpose()?
91 .flatten()
92 .unwrap_or_else(|| DEFAULT_PROTOCOL_VERSION.to_string());
93 let v5 = match version.trim() {
94 "3.1.1" | "4" | "311" => false,
95 "5" | "5.0" | "v5" => true,
96 other => anyhow::bail!("protocol_version must be \"3.1.1\" or \"5.0\", not {other:?}"),
97 };
98
99 info!(
100 "MQTT client {} connecting to {}:{} with client_id={} (MQTT {})",
101 client_id,
102 host,
103 port,
104 mqtt_client_id,
105 if v5 { "5.0" } else { "3.1.1" }
106 );
107
108 let (mqtt_client, eventloop) = if v5 {
109 let mut options = rumqttc::v5::MqttOptions::new(&mqtt_client_id, &host, port);
110 options.set_keep_alive(Duration::from_secs(keep_alive));
111 options.set_clean_start(clean_session);
112 if let (Some(user), Some(pass)) = (username, password) {
113 options.set_credentials(user, pass);
114 }
115 let (c, e) = rumqttc::v5::AsyncClient::new(options, 10);
116 (Broker::V5(c), Transport::V5(Box::new(e)))
117 } else {
118 let mut options = MqttOptions::new(&mqtt_client_id, &host, port);
119 options.set_keep_alive(Duration::from_secs(keep_alive));
120 options.set_clean_session(clean_session);
121 if let (Some(user), Some(pass)) = (username, password) {
122 options.set_credentials(user, pass);
123 }
124 let (c, e) = AsyncClient::new(options, 10);
125 (Broker::V4(c), Transport::V4(Box::new(e)))
126 };
127
128 let local_addr: SocketAddr = format!("0.0.0.0:0").parse().unwrap();
132
133 let mqtt_client_clone = mqtt_client.clone();
135 let app_state_clone = app_state.clone();
136 let status_tx_clone = status_tx.clone();
137
138 let disconnecting = Arc::new(AtomicBool::new(false));
141
142 let command_rx =
147 crate::client::command_support::register_command_channel(&app_state, client_id).await;
148 let cmd_client = mqtt_client.clone();
149 let cmd_state = app_state.clone();
150 let cmd_status_tx = status_tx.clone();
151 let cmd_disconnecting = disconnecting.clone();
152 let cmd_task = tokio::spawn(async move {
153 command_loop(
154 command_rx,
155 cmd_client,
156 client_id,
157 cmd_state,
158 cmd_status_tx,
159 cmd_disconnecting,
160 )
161 .await;
162 });
163 app_state.register_client_task(client_id, cmd_task).await;
164
165 let (turn_tx, turn_rx) = mpsc::channel::<TurnEvent>(TURN_QUEUE_CAPACITY);
173 let turn_abort = app_state
174 .spawn_client_task(
175 client_id,
176 run_model_turns(
177 turn_rx,
178 mqtt_client_clone,
179 llm_client,
180 app_state_clone.clone(),
181 status_tx_clone.clone(),
182 client_id,
183 mqtt_client_id,
184 disconnecting.clone(),
185 ),
186 )
187 .await;
188 app_state
189 .spawn_client_task(
190 client_id,
191 poll_transport(
192 eventloop,
193 app_state_clone,
194 status_tx_clone,
195 client_id,
196 disconnecting,
197 turn_tx,
198 turn_abort,
199 ),
200 )
201 .await;
202
203 Ok(local_addr)
204 }
205}
206
207const TURN_QUEUE_CAPACITY: usize = 256;
216
217pub const DEFAULT_PROTOCOL_VERSION: &str = "3.1.1";
219
220enum TurnEvent {
222 Connected(Option<serde_json::Value>),
225 Message(Message),
227}
228
229struct Message {
231 topic: String,
232 payload: Vec<u8>,
233 qos: u8,
234 retain: bool,
235 properties: Option<serde_json::Value>,
237}
238
239#[derive(Clone)]
241enum Broker {
242 V4(AsyncClient),
243 V5(rumqttc::v5::AsyncClient),
244}
245
246#[derive(Clone, Copy, Default)]
248struct SubscribeOptions {
249 no_local: bool,
250 retain_as_published: bool,
251 retain_handling: u8,
252}
253
254impl Broker {
255 fn is_v5(&self) -> bool {
256 matches!(self, Broker::V5(_))
257 }
258
259 async fn subscribe(&self, topic: &str, qos: u8, opts: SubscribeOptions) -> Result<()> {
260 match self {
261 Broker::V4(c) => c.subscribe(topic, qos_from_u64(u64::from(qos))).await?,
262 Broker::V5(c) => {
263 use rumqttc::v5::mqttbytes::v5::{Filter, RetainForwardRule};
264 let mut filter = Filter::new(topic, v5_qos(qos));
265 filter.nolocal = opts.no_local;
266 filter.preserve_retain = opts.retain_as_published;
267 filter.retain_forward_rule = match opts.retain_handling {
268 1 => RetainForwardRule::OnNewSubscribe,
269 2 => RetainForwardRule::Never,
270 _ => RetainForwardRule::OnEverySubscribe,
271 };
272 c.subscribe_many(vec![filter]).await?
273 }
274 }
275 Ok(())
276 }
277
278 async fn publish(
279 &self,
280 topic: &str,
281 qos: u8,
282 retain: bool,
283 payload: Vec<u8>,
284 properties: Option<&serde_json::Value>,
285 ) -> Result<()> {
286 match self {
287 Broker::V4(c) => {
288 c.publish(topic, qos_from_u64(u64::from(qos)), retain, payload)
289 .await?
290 }
291 Broker::V5(c) => match properties {
292 Some(p) => {
293 c.publish_with_properties(
294 topic,
295 v5_qos(qos),
296 retain,
297 payload,
298 publish_properties(p)?,
299 )
300 .await?
301 }
302 None => c.publish(topic, v5_qos(qos), retain, payload).await?,
303 },
304 }
305 Ok(())
306 }
307
308 async fn unsubscribe(&self, topic: &str) -> Result<()> {
309 match self {
310 Broker::V4(c) => c.unsubscribe(topic).await?,
311 Broker::V5(c) => c.unsubscribe(topic).await?,
312 }
313 Ok(())
314 }
315
316 async fn disconnect(&self) -> Result<()> {
317 match self {
318 Broker::V4(c) => c.disconnect().await?,
319 Broker::V5(c) => c.disconnect().await?,
320 }
321 Ok(())
322 }
323}
324
325fn v5_qos(qos: u8) -> rumqttc::v5::mqttbytes::QoS {
326 use rumqttc::v5::mqttbytes::QoS as Q;
327 match qos {
328 1 => Q::AtLeastOnce,
329 2 => Q::ExactlyOnce,
330 _ => Q::AtMostOnce,
331 }
332}
333
334fn publish_properties(
336 p: &serde_json::Value,
337) -> Result<rumqttc::v5::mqttbytes::v5::PublishProperties> {
338 use crate::server::mqtt::v5 as props;
339 let text = |k: &str| p.get(k).and_then(|v| v.as_str()).map(str::to_string);
340 let correlation_data = match text("correlation_data") {
341 Some(c) => Some(
342 props::text_to_binary(
343 &c,
344 p.get("correlation_data_encoding").and_then(|e| e.as_str()),
345 )
346 .map_err(|e| anyhow::anyhow!("correlation_data: {e}"))?
347 .into(),
348 ),
349 None => None,
350 };
351 Ok(rumqttc::v5::mqttbytes::v5::PublishProperties {
352 payload_format_indicator: p
353 .get("payload_format_indicator")
354 .and_then(|v| v.as_u64())
355 .map(|v| v as u8),
356 message_expiry_interval: p
357 .get("message_expiry_interval")
358 .and_then(|v| v.as_u64())
359 .map(|v| v as u32),
360 topic_alias: None,
361 response_topic: text("response_topic"),
362 correlation_data,
363 user_properties: props::user_properties_from_json(p.get("user_properties"))
364 .map_err(|e| anyhow::anyhow!(e))?,
365 subscription_identifiers: Vec::new(),
366 content_type: text("content_type"),
367 })
368}
369
370enum Notice {
372 ConnAck(Option<serde_json::Value>),
373 Publish(Message),
374 Disconnect(Option<String>),
376 ProtocolViolation(&'static str),
379 Other,
380}
381
382pub const TOPIC_ALIAS_VIOLATION: &str = "topic alias";
384
385enum Transport {
387 V4(Box<EventLoop>),
388 V5(Box<rumqttc::v5::EventLoop>),
389}
390
391impl Transport {
392 async fn next(&mut self) -> std::result::Result<Notice, String> {
393 match self {
394 Transport::V4(e) => match e.poll().await.map_err(|e| e.to_string())? {
395 Event::Incoming(Packet::ConnAck(_)) => Ok(Notice::ConnAck(None)),
396 Event::Incoming(Packet::Publish(p)) => Ok(Notice::Publish(Message {
397 topic: p.topic.clone(),
398 payload: p.payload.to_vec(),
399 qos: p.qos as u8,
400 retain: p.retain,
401 properties: None,
402 })),
403 Event::Incoming(Packet::Disconnect) => Ok(Notice::Disconnect(None)),
404 other => {
405 trace!("MQTT event: {:?}", other);
406 Ok(Notice::Other)
407 }
408 },
409 Transport::V5(e) => {
410 use rumqttc::v5::mqttbytes::v5::Packet as P5;
411 use rumqttc::v5::Event as E5;
412 match e.poll().await.map_err(|e| e.to_string())? {
413 E5::Incoming(P5::ConnAck(ack)) => {
414 let mut info = serde_json::json!({
415 "reason_code": connack_code(&ack.code),
416 "session_present": ack.session_present,
417 });
418 if let Some(p) = &ack.properties {
419 info["properties"] = connack_properties_json(p);
420 }
421 Ok(Notice::ConnAck(Some(info)))
422 }
423 E5::Incoming(P5::Publish(p))
432 if p.properties
433 .as_ref()
434 .is_some_and(|props| props.topic_alias.is_some()) =>
435 {
436 Ok(Notice::ProtocolViolation(TOPIC_ALIAS_VIOLATION))
437 }
438 E5::Incoming(P5::Publish(p)) => Ok(Notice::Publish(Message {
439 topic: String::from_utf8_lossy(&p.topic).into_owned(),
440 payload: p.payload.to_vec(),
441 qos: p.qos as u8,
442 retain: p.retain,
443 properties: Some(
444 p.properties
445 .as_ref()
446 .map(publish_properties_json)
447 .unwrap_or_else(|| serde_json::json!({})),
448 ),
449 })),
450 E5::Incoming(P5::Disconnect(d)) => Ok(Notice::Disconnect(Some(format!(
451 "reason 0x{:02x}{}",
452 d.reason_code as u8,
453 d.properties
454 .as_ref()
455 .and_then(|p| p.reason_string.as_ref())
456 .map(|r| format!(" ({r})"))
457 .unwrap_or_default()
458 )))),
459 other => {
460 trace!("MQTT v5 event: {:?}", other);
461 Ok(Notice::Other)
462 }
463 }
464 }
465 }
466 }
467}
468
469fn connack_code(code: &rumqttc::v5::mqttbytes::v5::ConnectReturnCode) -> u8 {
471 use rumqttc::v5::mqttbytes::v5::ConnectReturnCode as C;
472 match code {
473 C::Success => 0x00,
474 C::RefusedProtocolVersion | C::UnsupportedProtocolVersion => 0x84,
475 C::BadClientId | C::ClientIdentifierNotValid => 0x85,
476 C::ServiceUnavailable | C::ServerUnavailable => 0x88,
477 C::UnspecifiedError => 0x80,
478 C::MalformedPacket => 0x81,
479 C::ProtocolError => 0x82,
480 C::ImplementationSpecificError => 0x83,
481 C::BadUserNamePassword => 0x86,
482 C::NotAuthorized => 0x87,
483 C::ServerBusy => 0x89,
484 C::Banned => 0x8A,
485 C::BadAuthenticationMethod => 0x8C,
486 C::TopicNameInvalid => 0x90,
487 C::PacketTooLarge => 0x95,
488 C::QuotaExceeded => 0x97,
489 C::PayloadFormatInvalid => 0x99,
490 C::RetainNotSupported => 0x9A,
491 C::QoSNotSupported => 0x9B,
492 C::UseAnotherServer => 0x9C,
493 C::ServerMoved => 0x9D,
494 C::ConnectionRateExceeded => 0x9F,
495 }
496}
497
498fn pairs_json(pairs: &[(String, String)]) -> serde_json::Value {
499 serde_json::Value::Array(
500 pairs
501 .iter()
502 .map(|(k, v)| serde_json::json!([k, v]))
503 .collect(),
504 )
505}
506
507fn connack_properties_json(p: &rumqttc::v5::mqttbytes::v5::ConnAckProperties) -> serde_json::Value {
509 let mut out = serde_json::Map::new();
510 let mut put = |k: &str, v: serde_json::Value| {
511 if !v.is_null() {
512 out.insert(k.to_string(), v);
513 }
514 };
515 put(
516 "session_expiry_interval",
517 serde_json::json!(p.session_expiry_interval),
518 );
519 put("receive_maximum", serde_json::json!(p.receive_max));
520 put("maximum_qos", serde_json::json!(p.max_qos));
521 put("retain_available", serde_json::json!(p.retain_available));
522 put("maximum_packet_size", serde_json::json!(p.max_packet_size));
523 put(
524 "assigned_client_identifier",
525 serde_json::json!(p.assigned_client_identifier),
526 );
527 put("topic_alias_maximum", serde_json::json!(p.topic_alias_max));
528 put("reason_string", serde_json::json!(p.reason_string));
529 put("server_keep_alive", serde_json::json!(p.server_keep_alive));
530 put(
531 "response_information",
532 serde_json::json!(p.response_information),
533 );
534 put("server_reference", serde_json::json!(p.server_reference));
535 if !p.user_properties.is_empty() {
536 put("user_properties", pairs_json(&p.user_properties));
537 }
538 serde_json::Value::Object(out)
539}
540
541fn publish_properties_json(p: &rumqttc::v5::mqttbytes::v5::PublishProperties) -> serde_json::Value {
543 let mut out = serde_json::Map::new();
544 if let Some(v) = p.payload_format_indicator {
545 out.insert("payload_format_indicator".into(), serde_json::json!(v));
546 }
547 if let Some(v) = p.message_expiry_interval {
548 out.insert("message_expiry_interval".into(), serde_json::json!(v));
549 }
550 if let Some(v) = &p.content_type {
551 out.insert("content_type".into(), serde_json::json!(v));
552 }
553 if let Some(v) = &p.response_topic {
554 out.insert("response_topic".into(), serde_json::json!(v));
555 }
556 if let Some(v) = &p.correlation_data {
557 let (text, encoding) = crate::server::mqtt::v5::binary_to_text(v);
558 out.insert("correlation_data".into(), serde_json::json!(text));
559 out.insert(
560 "correlation_data_encoding".into(),
561 serde_json::json!(encoding),
562 );
563 }
564 if !p.subscription_identifiers.is_empty() {
565 out.insert(
566 "subscription_identifiers".into(),
567 serde_json::json!(p.subscription_identifiers),
568 );
569 }
570 if !p.user_properties.is_empty() {
571 out.insert("user_properties".into(), pairs_json(&p.user_properties));
572 }
573 serde_json::Value::Object(out)
574}
575
576async fn poll_transport(
584 mut eventloop: Transport,
585 app_state: Arc<AppState>,
586 status_tx: mpsc::UnboundedSender<String>,
587 client_id: ClientId,
588 disconnecting: Arc<AtomicBool>,
589 turn_tx: mpsc::Sender<TurnEvent>,
590 turn_abort: tokio::task::AbortHandle,
591) {
592 let mut connected = false;
593
594 loop {
595 match eventloop.next().await {
596 Ok(notification) => {
597 match notification {
598 Notice::ConnAck(info) => {
599 if !connected {
600 connected = true;
601 info!("MQTT client {} connected to broker", client_id);
602 app_state
603 .update_client_status(client_id, ClientStatus::Connected)
604 .await;
605 let _ = status_tx
606 .send(format!("[CLIENT] MQTT client {} connected", client_id));
607 let _ = status_tx.send("__UPDATE_UI__".to_string());
608 enqueue_turn(&turn_tx, TurnEvent::Connected(info), client_id);
609 }
610 }
611 Notice::Publish(publish) => {
612 debug!(
613 "MQTT client {} received message on topic '{}': {} bytes",
614 client_id,
615 publish.topic,
616 publish.payload.len()
617 );
618 enqueue_turn(&turn_tx, TurnEvent::Message(publish), client_id);
619 }
620 Notice::Disconnect(why) => {
621 let why = why.map(|w| format!(" ({w})")).unwrap_or_default();
622 info!("MQTT client {} disconnected by broker{}", client_id, why);
623 app_state
624 .update_client_status(client_id, ClientStatus::Disconnected)
625 .await;
626 let _ = status_tx.send(format!(
627 "[CLIENT] MQTT client {} disconnected by broker{}",
628 client_id, why
629 ));
630 let _ = status_tx.send("__UPDATE_UI__".to_string());
631 break;
632 }
633 Notice::ProtocolViolation(category) => {
634 warn!(
637 "MQTT client {} closing: broker sent a {} \
638 decision=protocol_error",
639 client_id, category
640 );
641 app_state
642 .update_client_status(
643 client_id,
644 ClientStatus::Error(format!("protocol violation: {category}")),
645 )
646 .await;
647 let _ = status_tx.send(format!(
648 "[CLIENT] MQTT client {} closed the connection: protocol violation \
649 ({})",
650 client_id, category
651 ));
652 let _ = status_tx.send("__UPDATE_UI__".to_string());
653 break;
654 }
655 Notice::Other => {}
658 }
659 }
660 Err(e) => {
661 if disconnecting.load(Ordering::SeqCst) {
665 info!("MQTT client {} disconnected on request", client_id);
666 app_state
667 .update_client_status(client_id, ClientStatus::Disconnected)
668 .await;
669 let _ =
670 status_tx.send(format!("[CLIENT] MQTT client {} disconnected", client_id));
671 } else {
672 error!("MQTT client {} connection error: {}", client_id, e);
673 app_state
674 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
675 .await;
676 let _ =
677 status_tx.send(format!("[CLIENT] MQTT client {} error: {}", client_id, e));
678 }
679 let _ = status_tx.send("__UPDATE_UI__".to_string());
680 break;
681 }
682 }
683 }
684
685 turn_abort.abort();
689 app_state.remove_client_handle(client_id).await;
693 let _ = status_tx.send("__UPDATE_UI__".to_string());
694}
695
696fn enqueue_turn(turn_tx: &mpsc::Sender<TurnEvent>, event: TurnEvent, client_id: ClientId) {
698 match turn_tx.try_send(event) {
699 Ok(()) => {}
700 Err(mpsc::error::TrySendError::Full(TurnEvent::Message(publish))) => {
701 warn!(
702 "MQTT client {} dropped a message on '{}': {} events already wait for the \
703 model decision=turn_queue_full",
704 client_id, publish.topic, TURN_QUEUE_CAPACITY
705 );
706 }
707 Err(mpsc::error::TrySendError::Full(TurnEvent::Connected(_))) => {
708 warn!(
709 "MQTT client {} dropped its connected event decision=turn_queue_full",
710 client_id
711 );
712 }
713 Err(mpsc::error::TrySendError::Closed(_)) => {}
715 }
716}
717
718#[allow(clippy::too_many_arguments)]
720async fn run_model_turns(
721 mut turn_rx: mpsc::Receiver<TurnEvent>,
722 mqtt_client: Broker,
723 llm_client: OllamaClient,
724 app_state: Arc<AppState>,
725 status_tx: mpsc::UnboundedSender<String>,
726 client_id: ClientId,
727 mqtt_client_id: String,
728 disconnecting: Arc<AtomicBool>,
729) {
730 while let Some(turn) = turn_rx.recv().await {
731 match turn {
732 TurnEvent::Connected(info) => {
733 handle_connected(
734 info,
735 &mqtt_client,
736 &llm_client,
737 &app_state,
738 &status_tx,
739 client_id,
740 &mqtt_client_id,
741 &disconnecting,
742 )
743 .await;
744 }
745 TurnEvent::Message(publish) => {
746 handle_incoming_message(
747 &publish,
748 &mqtt_client,
749 &llm_client,
750 &app_state,
751 &status_tx,
752 client_id,
753 &disconnecting,
754 )
755 .await;
756 }
757 }
758 }
759}
760
761#[allow(clippy::too_many_arguments)]
763async fn handle_connected(
764 connack: Option<serde_json::Value>,
765 mqtt_client: &Broker,
766 llm_client: &OllamaClient,
767 app_state: &Arc<AppState>,
768 status_tx: &mpsc::UnboundedSender<String>,
769 client_id: ClientId,
770 mqtt_client_id: &str,
771 disconnecting: &Arc<AtomicBool>,
772) {
773 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
774 return;
775 };
776 let protocol = Arc::new(MqttClientProtocol::new());
777 let mut data = serde_json::json!({
778 "remote_addr": "connected",
779 "client_id": mqtt_client_id,
780 "protocol_version": if mqtt_client.is_v5() { "5.0" } else { "3.1.1" },
781 });
782 if let Some(serde_json::Value::Object(ack)) = connack {
784 for (k, v) in ack {
785 data[k] = v;
786 }
787 }
788 let event = ProtocolEvent::new(&MQTT_CLIENT_CONNECTED_EVENT, data);
789
790 let memory = app_state
791 .get_memory_for_client(client_id)
792 .await
793 .unwrap_or_default();
794
795 match call_llm_for_client(
796 llm_client,
797 app_state,
798 client_id.to_string(),
799 &instruction,
800 &memory,
801 Some(&event),
802 protocol.as_ref(),
803 status_tx,
804 )
805 .await
806 {
807 Ok(result) => {
808 handle_llm_actions(
809 result,
810 mqtt_client,
811 app_state,
812 client_id,
813 &protocol,
814 disconnecting,
815 )
816 .await;
817 }
818 Err(e) => {
819 error!("LLM error for MQTT client {}: {}", client_id, e);
820 }
821 }
822}
823
824#[allow(clippy::too_many_arguments)]
826async fn handle_incoming_message(
827 publish: &Message,
828 mqtt_client: &Broker,
829 llm_client: &OllamaClient,
830 app_state: &Arc<AppState>,
831 status_tx: &mpsc::UnboundedSender<String>,
832 client_id: ClientId,
833 disconnecting: &Arc<AtomicBool>,
834) {
835 let topic = publish.topic.clone();
836 let payload = String::from_utf8_lossy(&publish.payload).to_string();
837 let qos = publish.qos;
838 let retain = publish.retain;
839
840 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
842 let protocol = Arc::new(MqttClientProtocol::new());
843 let mut data = serde_json::json!({
844 "topic": topic,
845 "payload": payload,
846 "qos": qos,
847 "retain": retain,
848 });
849 if let Some(properties) = &publish.properties {
850 data["properties"] = properties.clone();
851 }
852 let event = ProtocolEvent::new(&MQTT_MESSAGE_RECEIVED_EVENT, data);
853
854 let memory = app_state
855 .get_memory_for_client(client_id)
856 .await
857 .unwrap_or_default();
858
859 match call_llm_for_client(
860 llm_client,
861 app_state,
862 client_id.to_string(),
863 &instruction,
864 &memory,
865 Some(&event),
866 protocol.as_ref(),
867 status_tx,
868 )
869 .await
870 {
871 Ok(result) => {
872 handle_llm_actions(
873 result,
874 mqtt_client,
875 app_state,
876 client_id,
877 &protocol,
878 disconnecting,
879 )
880 .await;
881 }
882 Err(e) => {
883 error!("LLM error for MQTT client {}: {}", client_id, e);
884 }
885 }
886 }
887}
888
889enum MqttApplied {
891 Queued(String),
895 NoWire(String),
897 Disconnect,
899}
900
901async fn apply_action(
908 action_result: ClientActionResult,
909 mqtt_client: &Broker,
910 client_id: ClientId,
911 disconnecting: &Arc<AtomicBool>,
912) -> Result<MqttApplied> {
913 match action_result {
914 ClientActionResult::Custom { name, data } => match name.as_str() {
915 "mqtt_subscribe" => {
916 let topics = data
917 .get("topics")
918 .and_then(|v| v.as_array())
919 .context("mqtt_subscribe result has no 'topics' array")?;
920 let qos = data.get("qos").and_then(|v| v.as_u64()).unwrap_or(0);
921 let flag = |k: &str| data.get(k).and_then(|v| v.as_bool()).unwrap_or(false);
922 let opts = SubscribeOptions {
923 no_local: flag("no_local"),
924 retain_as_published: flag("retain_as_published"),
925 retain_handling: data
926 .get("retain_handling")
927 .and_then(|v| v.as_u64())
928 .unwrap_or(0) as u8,
929 };
930
931 let mut subscribed: Vec<String> = Vec::new();
932 for topic in topics {
933 if let Some(topic_str) = topic.as_str() {
934 mqtt_client
935 .subscribe(topic_str, qos as u8, opts)
936 .await
937 .with_context(|| format!("subscribe to '{topic_str}' failed"))?;
938 info!(
939 "MQTT client {} subscribed to '{}' with QoS {}",
940 client_id, topic_str, qos
941 );
942 subscribed.push(topic_str.to_string());
943 }
944 }
945 if subscribed.is_empty() {
946 return Ok(MqttApplied::NoWire(
947 "mqtt_subscribe: no valid topic strings".to_string(),
948 ));
949 }
950 Ok(MqttApplied::Queued(format!(
951 "SUBSCRIBE to [{}] at QoS {} accepted by rumqttc",
952 subscribed.join(", "),
953 qos
954 )))
955 }
956 "mqtt_publish" => {
957 let topic = data
958 .get("topic")
959 .and_then(|v| v.as_str())
960 .context("mqtt_publish result has no 'topic'")?;
961 let payload = data
962 .get("payload")
963 .and_then(|v| v.as_str())
964 .context("mqtt_publish result has no 'payload'")?;
965 let qos = data.get("qos").and_then(|v| v.as_u64()).unwrap_or(0);
966 let retain = data
967 .get("retain")
968 .and_then(|v| v.as_bool())
969 .unwrap_or(false);
970
971 let properties = data.get("properties").filter(|p| !p.is_null());
972 if properties.is_some() && !mqtt_client.is_v5() {
973 anyhow::bail!(
974 "PUBLISH properties need MQTT 5; this client connected with 3.1.1 \
975 (start it with protocol_version \"5.0\")"
976 );
977 }
978 mqtt_client
979 .publish(
980 topic,
981 qos as u8,
982 retain,
983 payload.as_bytes().to_vec(),
984 properties,
985 )
986 .await
987 .with_context(|| format!("publish to '{topic}' failed"))?;
988 info!(
989 "MQTT client {} published to '{}': {}",
990 client_id, topic, payload
991 );
992 Ok(MqttApplied::Queued(format!(
993 "PUBLISH to '{}' ({} byte payload, QoS {}, retain {}) accepted by rumqttc",
994 topic,
995 payload.len(),
996 qos,
997 retain
998 )))
999 }
1000 "mqtt_unsubscribe" => {
1001 let topics = data
1002 .get("topics")
1003 .and_then(|v| v.as_array())
1004 .context("mqtt_unsubscribe result has no 'topics' array")?;
1005 let mut removed: Vec<String> = Vec::new();
1006 for topic in topics {
1007 if let Some(topic_str) = topic.as_str() {
1008 mqtt_client
1009 .unsubscribe(topic_str)
1010 .await
1011 .with_context(|| format!("unsubscribe from '{topic_str}' failed"))?;
1012 info!(
1013 "MQTT client {} unsubscribed from '{}'",
1014 client_id, topic_str
1015 );
1016 removed.push(topic_str.to_string());
1017 }
1018 }
1019 if removed.is_empty() {
1020 return Ok(MqttApplied::NoWire(
1021 "mqtt_unsubscribe: no valid topic strings".to_string(),
1022 ));
1023 }
1024 Ok(MqttApplied::Queued(format!(
1025 "UNSUBSCRIBE from [{}] accepted by rumqttc",
1026 removed.join(", ")
1027 )))
1028 }
1029 other => Err(anyhow::anyhow!(
1030 "MQTT client cannot apply custom result '{}'",
1031 other
1032 )),
1033 },
1034 ClientActionResult::Disconnect => {
1035 info!("MQTT client {} disconnecting", client_id);
1036 disconnecting.store(true, Ordering::SeqCst);
1039 mqtt_client
1040 .disconnect()
1041 .await
1042 .inspect_err(|_| disconnecting.store(false, Ordering::SeqCst))
1043 .context("disconnect request failed")?;
1044 Ok(MqttApplied::Disconnect)
1045 }
1046 ClientActionResult::WaitForMore => Ok(MqttApplied::NoWire(
1047 "waiting for the next broker message; nothing sent".to_string(),
1048 )),
1049 ClientActionResult::NoAction => {
1050 Ok(MqttApplied::NoWire("no_action: nothing sent".to_string()))
1051 }
1052 ClientActionResult::SendData(_) => {
1053 Err(anyhow::anyhow!("MQTT has no raw-byte channel; use publish"))
1054 }
1055 ClientActionResult::Multiple(_) => Err(anyhow::anyhow!(
1056 "MQTT client does not support Multiple action results"
1057 )),
1058 }
1059}
1060
1061fn qos_from_u64(qos: u64) -> QoS {
1062 match qos {
1063 1 => QoS::AtLeastOnce,
1064 2 => QoS::ExactlyOnce,
1065 _ => QoS::AtMostOnce,
1066 }
1067}
1068
1069async fn handle_llm_actions(
1071 result: ClientLlmResult,
1072 mqtt_client: &Broker,
1073 app_state: &Arc<AppState>,
1074 client_id: ClientId,
1075 protocol: &Arc<MqttClientProtocol>,
1076 disconnecting: &Arc<AtomicBool>,
1077) {
1078 if let Some(mem) = result.memory_updates {
1080 app_state.set_memory_for_client(client_id, mem).await;
1081 }
1082
1083 for action in result.actions {
1085 match protocol.execute_action(action) {
1086 Ok(action_result) => {
1087 match apply_action(action_result, mqtt_client, client_id, disconnecting).await {
1088 Ok(MqttApplied::Queued(detail)) => {
1089 debug!("MQTT client {}: {}", client_id, detail);
1090 }
1091 Ok(MqttApplied::NoWire(detail)) => {
1092 debug!("MQTT client {}: {}", client_id, detail);
1093 }
1094 Ok(MqttApplied::Disconnect) => {}
1095 Err(e) => {
1096 error!("MQTT client {} could not apply action: {}", client_id, e);
1097 }
1098 }
1099 }
1100 Err(e) => {
1101 error!("Error executing MQTT action: {}", e);
1102 }
1103 }
1104 }
1105}
1106
1107async fn command_loop(
1116 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
1117 mqtt_client: Broker,
1118 client_id: ClientId,
1119 app_state: Arc<AppState>,
1120 status_tx: mpsc::UnboundedSender<String>,
1121 disconnecting: Arc<AtomicBool>,
1122) {
1123 use crate::llm::actions::protocol_trait::Protocol;
1124 use crate::state::client_handles::ClientSendOutcome;
1125 use crate::state::AccessLogOwner;
1126
1127 let protocol = MqttClientProtocol::new();
1128
1129 while let Some(command) = command_rx.recv().await {
1130 let action = command.action.clone();
1131 let outcome: Result<ClientSendOutcome> = match protocol.execute_action(action.clone()) {
1132 Err(e) => Ok(ClientSendOutcome::Rejected {
1133 error: e.to_string(),
1134 }),
1135 Ok(result) => match apply_action(result, &mqtt_client, client_id, &disconnecting).await
1136 {
1137 Ok(MqttApplied::Queued(detail)) => Ok(ClientSendOutcome::Executed { detail }),
1141 Ok(MqttApplied::NoWire(detail)) => Ok(ClientSendOutcome::Executed { detail }),
1142 Ok(MqttApplied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
1143 Err(e) => Err(e),
1144 },
1145 };
1146
1147 let outcome_json = match &outcome {
1148 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
1149 Err(e) => serde_json::json!({"error": e.to_string()}),
1150 };
1151 app_state
1152 .record_access_log(
1153 AccessLogOwner::Client(client_id.as_u32()),
1154 protocol.protocol_name(),
1155 None,
1156 "injected_action",
1157 action,
1158 vec![outcome_json],
1159 )
1160 .await;
1161
1162 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
1163 if let Err(e) = &outcome {
1164 error!("MQTT client {} injected action failed: {}", client_id, e);
1165 let _ = status_tx.send(format!(
1166 "[WARN] Client {} injected action failed: {}",
1167 client_id, e
1168 ));
1169 }
1170 let _ = status_tx.send("__UPDATE_UI__".to_string());
1171 crate::client::command_support::reply(command, outcome);
1172
1173 if disconnect {
1174 app_state.remove_client_handle(client_id).await;
1177 break;
1178 }
1179 }
1180}