1pub mod actions;
19
20pub use actions::CoapClientProtocol;
21
22use std::collections::{HashMap, VecDeque};
23use std::net::SocketAddr;
24use std::sync::Arc;
25use std::time::Duration;
26
27use anyhow::{anyhow, Context, Result};
28use serde_json::{json, Value};
29use tokio::net::UdpSocket;
30use tokio::sync::{mpsc, oneshot};
31use tokio::time::Instant;
32use tracing::{debug, error, info, warn};
33
34use crate::client::coap::actions::{
35 request_from_action, CoapRequest, ObserveAction, COAP_CONNECTED_EVENT, COAP_ERROR_EVENT,
36 COAP_NOTIFICATION_EVENT, COAP_RESPONSE_EVENT, REQUEST_RESULT,
37};
38use crate::client::llm_budget::call_llm_for_client;
39use crate::llm::actions::client_trait::{Client, ClientActionResult};
40use crate::llm::actions::protocol_trait::Protocol;
41use crate::llm::ollama_client::OllamaClient;
42use crate::protocol::Event;
43use crate::server::coap::codec::{
44 code_class, code_to_string, content_format_name, szx_size, uint_option, BlockValue,
45 CoapMessage, MessageType, CODE_CONTINUE, CODE_DELETE, CODE_EMPTY, CODE_GET, CODE_POST,
46 CODE_PUT, MAX_MESSAGE_LEN, OPT_ACCEPT, OPT_CONTENT_FORMAT, OPT_ETAG, OPT_LOCATION_PATH,
47 OPT_MAX_AGE, OPT_URI_PATH, OPT_URI_QUERY,
48};
49pub use crate::server::coap::codec::{OPT_BLOCK1, OPT_BLOCK2, OPT_OBSERVE, OPT_SIZE1, OPT_SIZE2};
50use crate::state::app_state::AppState;
51use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
52use crate::state::{AccessLogOwner, ClientId, ClientStatus};
53
54pub const MAX_BODY: usize = 64 * 1024;
57
58pub const MAX_EXCHANGES: usize = 32;
60
61pub const RESPONSE_TIMEOUT: Duration = Duration::from_secs(30);
64
65const RECENT_MIDS: usize = 64;
68
69const TURN_QUEUE_CAPACITY: usize = 256;
70const OUTBOUND_CAPACITY: usize = 64;
71
72#[derive(Debug, Clone, Copy)]
74pub struct Reliability {
75 pub ack_timeout: Duration,
76 pub max_retransmit: u32,
77 pub block_szx: u8,
79}
80
81enum Outbound {
82 Request {
83 request: CoapRequest,
84 ack: oneshot::Sender<Result<usize, String>>,
85 },
86 Disconnect {
87 ack: oneshot::Sender<()>,
88 },
89}
90
91enum Applied {
92 Sent(usize),
93 Nothing,
94 Disconnect,
95}
96
97struct Exchange {
99 request: CoapRequest,
100 message_id: u16,
102 datagram: Vec<u8>,
103 awaiting_ack: bool,
105 retransmissions: u32,
106 interval: Duration,
107 deadline: Option<Instant>,
110 body: Vec<u8>,
112 blocks: u32,
113 next_block: u32,
114 observing: bool,
116 upload: Option<BlockValue>,
119 blocks_sent: u32,
121}
122
123pub struct CoapClient;
124
125impl CoapClient {
126 pub async fn connect_with_llm_actions(
127 remote_addr: String,
128 reliability: Reliability,
129 llm_client: OllamaClient,
130 app_state: Arc<AppState>,
131 status_tx: mpsc::UnboundedSender<String>,
132 client_id: ClientId,
133 ) -> Result<SocketAddr> {
134 if remote_addr.trim().is_empty() {
135 return Err(anyhow!(
136 "CoAP client needs a remote_addr (host:port); refusing to start without one"
137 ));
138 }
139 let remote = tokio::net::lookup_host(&remote_addr)
140 .await
141 .with_context(|| format!("cannot resolve CoAP server {remote_addr}"))?
142 .next()
143 .with_context(|| format!("{remote_addr} resolved to no address"))?;
144 let bind = if remote.is_ipv4() {
145 "0.0.0.0:0"
146 } else {
147 "[::]:0"
148 };
149 let socket = UdpSocket::bind(bind).await?;
150 socket.connect(remote).await?;
152 let local_addr = socket.local_addr()?;
153 info!("CoAP client {client_id} ready for {remote} (local {local_addr})");
154 app_state
155 .update_client_status(client_id, ClientStatus::Connected)
156 .await;
157 let _ = status_tx.send(format!("[CLIENT] CoAP client {client_id} ready"));
158 let _ = status_tx.send("__UPDATE_UI__".to_string());
159
160 let protocol = Arc::new(CoapClientProtocol::new());
161 let (outbound_tx, outbound_rx) = mpsc::channel::<Outbound>(OUTBOUND_CAPACITY);
162 let (turn_tx, turn_rx) = mpsc::channel::<Event>(TURN_QUEUE_CAPACITY);
163
164 let command_rx =
165 crate::client::command_support::register_command_channel(&app_state, client_id).await;
166 app_state
167 .spawn_client_task(
168 client_id,
169 command_loop(
170 command_rx,
171 protocol.clone(),
172 outbound_tx.clone(),
173 client_id,
174 app_state.clone(),
175 status_tx.clone(),
176 ),
177 )
178 .await;
179 let turn_abort = app_state
180 .spawn_client_task(
181 client_id,
182 run_turns(
183 turn_rx,
184 outbound_tx,
185 protocol,
186 llm_client,
187 app_state.clone(),
188 status_tx.clone(),
189 client_id,
190 ),
191 )
192 .await;
193 let _ = turn_tx.try_send(Event::new(
194 &COAP_CONNECTED_EVENT,
195 json!({"remote_addr": remote.to_string()}),
196 ));
197 app_state
198 .spawn_client_task(
199 client_id,
200 run_transport(
201 socket,
202 reliability,
203 outbound_rx,
204 turn_tx,
205 app_state.clone(),
206 status_tx,
207 client_id,
208 turn_abort,
209 ),
210 )
211 .await;
212 Ok(local_addr)
213 }
214}
215
216fn parse_block(value: &[u8]) -> Option<(u32, bool, u8)> {
218 BlockValue::parse(value).map(|b| (b.num, b.more, b.szx))
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223enum Part {
224 Whole,
226 Block1(BlockValue),
228 Block2(u32, u8),
230}
231
232fn first_block(request: &CoapRequest, szx: u8) -> Option<BlockValue> {
234 (request.payload.len() > szx_size(szx)).then_some(BlockValue {
235 num: 0,
236 more: true,
237 szx,
238 })
239}
240
241fn next_block(body_len: usize, sent: BlockValue, echo: Option<BlockValue>) -> Option<BlockValue> {
245 let szx = echo.map_or(sent.szx, |e| e.szx.min(sent.szx));
246 let acknowledged = sent.offset() + sent.size();
247 if acknowledged >= body_len {
248 return None;
249 }
250 let size = szx_size(szx);
251 let num = u32::try_from(acknowledged / size).ok()?;
252 Some(BlockValue {
253 num,
254 more: acknowledged + size < body_len,
255 szx,
256 })
257}
258
259fn method_code(method: &str) -> u8 {
260 match method {
261 "POST" => CODE_POST,
262 "PUT" => CODE_PUT,
263 "DELETE" => CODE_DELETE,
264 _ => CODE_GET,
265 }
266}
267
268fn request_message(
270 request: &CoapRequest,
271 message_id: u16,
272 token: &[u8],
273 part: Part,
274) -> CoapMessage {
275 let mut options: Vec<(u16, Vec<u8>)> = Vec::new();
276 if !matches!(part, Part::Block2(..)) {
278 match request.observe {
279 ObserveAction::Register => options.push((OPT_OBSERVE, Vec::new())),
280 ObserveAction::Cancel => options.push((OPT_OBSERVE, vec![1])),
281 ObserveAction::None => {}
282 }
283 }
284 for segment in &request.path {
285 options.push((OPT_URI_PATH, segment.as_bytes().to_vec()));
286 }
287 if let Some(cf) = request.content_format {
288 options.push((OPT_CONTENT_FORMAT, uint_option(u32::from(cf))));
289 }
290 for item in &request.query {
291 options.push((OPT_URI_QUERY, item.as_bytes().to_vec()));
292 }
293 if let Some(accept) = request.accept {
294 options.push((OPT_ACCEPT, uint_option(u32::from(accept))));
295 }
296 let payload = match part {
297 Part::Whole => request.payload.clone(),
298 Part::Block2(num, szx) => {
299 options.push((
300 OPT_BLOCK2,
301 BlockValue {
302 num,
303 more: false,
304 szx,
305 }
306 .encode(),
307 ));
308 Vec::new()
309 }
310 Part::Block1(block) => {
311 options.push((OPT_BLOCK1, block.encode()));
312 if block.num == 0 {
313 options.push((OPT_SIZE1, uint_option(request.payload.len() as u32)));
315 }
316 let start = block.offset().min(request.payload.len());
317 let end = (start + block.size()).min(request.payload.len());
318 request.payload[start..end].to_vec()
319 }
320 };
321 CoapMessage {
322 mtype: if request.confirmable {
323 MessageType::Confirmable
324 } else {
325 MessageType::NonConfirmable
326 },
327 code: method_code(request.method),
328 message_id,
329 token: token.to_vec(),
330 options,
331 payload,
332 }
333}
334
335fn status_name(code: u8) -> &'static str {
336 match code_to_string(code).as_str() {
337 "2.01" => "Created",
338 "2.02" => "Deleted",
339 "2.03" => "Valid",
340 "2.04" => "Changed",
341 "2.05" => "Content",
342 "2.31" => "Continue",
343 "4.00" => "Bad Request",
344 "4.01" => "Unauthorized",
345 "4.02" => "Bad Option",
346 "4.03" => "Forbidden",
347 "4.04" => "Not Found",
348 "4.05" => "Method Not Allowed",
349 "4.06" => "Not Acceptable",
350 "4.08" => "Request Entity Incomplete",
351 "4.12" => "Precondition Failed",
352 "4.13" => "Request Entity Too Large",
353 "4.15" => "Unsupported Content-Format",
354 "5.00" => "Internal Server Error",
355 "5.01" => "Not Implemented",
356 "5.02" => "Bad Gateway",
357 "5.03" => "Service Unavailable",
358 "5.04" => "Gateway Timeout",
359 "5.05" => "Proxying Not Supported",
360 _ => "Unknown",
361 }
362}
363
364fn response_data(request: &CoapRequest, msg: &CoapMessage, payload: &[u8], blocks: u32) -> Value {
367 let content_format = msg.option_uint(OPT_CONTENT_FORMAT);
368 let mut data = json!({
369 "method": request.method,
370 "path": request.path_string(),
371 "code": code_to_string(msg.code),
372 "status": status_name(msg.code),
373 "payload_size": payload.len(),
374 "blocks": blocks.max(1),
375 });
376 if let Some(cf) = content_format {
377 let name = u16::try_from(cf)
378 .ok()
379 .and_then(content_format_name)
380 .map(str::to_string)
381 .unwrap_or_else(|| cf.to_string());
382 data["content_format"] = json!(name);
383 }
384 if let Ok(text) = std::str::from_utf8(payload) {
385 if !text.is_empty() {
386 data["payload"] = json!(text);
387 if content_format == Some(50) {
388 if let Ok(parsed) = serde_json::from_str::<Value>(text) {
389 data["payload_json"] = parsed;
390 }
391 }
392 }
393 }
394 let mut options = serde_json::Map::new();
395 if let Some(v) = msg.option_uint(OPT_MAX_AGE) {
396 options.insert("max_age".into(), json!(v));
397 }
398 if let Some(v) = msg.option_values(OPT_ETAG).first() {
399 options.insert("etag".into(), json!(hex::encode(v)));
400 }
401 let location: Vec<String> = msg
402 .option_values(OPT_LOCATION_PATH)
403 .iter()
404 .map(|v| String::from_utf8_lossy(v).to_string())
405 .collect();
406 if !location.is_empty() {
407 options.insert(
408 "location_path".into(),
409 json!(format!("/{}", location.join("/"))),
410 );
411 }
412 if let Some(v) = msg.option_uint(OPT_OBSERVE) {
413 options.insert("observe".into(), json!(v));
414 }
415 if let Some(v) = msg.option_uint(OPT_SIZE2) {
416 options.insert("size2".into(), json!(v));
417 }
418 data["options"] = Value::Object(options);
419 data
420}
421
422fn error_event(request: &CoapRequest, kind: &str, message: String) -> Event {
423 Event::new(
424 &COAP_ERROR_EVENT,
425 json!({
426 "kind": kind,
427 "message": message,
428 "method": request.method,
429 "path": request.path_string(),
430 }),
431 )
432}
433
434fn enqueue(turn_tx: &mpsc::Sender<Event>, event: Event, client_id: ClientId) {
435 if turn_tx.try_send(event).is_err() {
436 warn!(
437 "CoAP client {client_id} dropped an event: the model is {TURN_QUEUE_CAPACITY} events \
438 behind decision=turn_queue_full"
439 );
440 }
441}
442
443struct Transport {
445 socket: UdpSocket,
446 reliability: Reliability,
447 exchanges: HashMap<Vec<u8>, Exchange>,
448 next_mid: u16,
449 recent: VecDeque<u16>,
450 turn_tx: mpsc::Sender<Event>,
451 client_id: ClientId,
452}
453
454impl Transport {
455 fn fresh_mid(&mut self) -> u16 {
456 let mid = self.next_mid;
457 self.next_mid = self.next_mid.wrapping_add(1);
458 mid
459 }
460
461 fn fresh_token(&self) -> Vec<u8> {
462 loop {
463 let token = rand::random::<u32>().to_be_bytes().to_vec();
464 if !self.exchanges.contains_key(&token) {
465 return token;
466 }
467 }
468 }
469
470 fn first_interval(&self) -> Duration {
471 let factor = 1.0 + rand::random::<f64>() * 0.5;
473 self.reliability.ack_timeout.mul_f64(factor)
474 }
475
476 async fn transmit(&mut self, token: &[u8], msg: CoapMessage) -> std::io::Result<usize> {
478 let datagram = msg
479 .encode()
480 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;
481 self.socket.send(&datagram).await?;
482 let confirmable = msg.mtype == MessageType::Confirmable;
483 let interval = self.first_interval();
484 let now = Instant::now();
485 if let Some(ex) = self.exchanges.get_mut(token) {
486 ex.message_id = msg.message_id;
487 ex.datagram = datagram.clone();
488 ex.awaiting_ack = confirmable;
489 ex.retransmissions = 0;
490 ex.interval = interval;
491 ex.deadline = Some(if confirmable {
492 now + interval
493 } else {
494 now + RESPONSE_TIMEOUT
495 });
496 }
497 Ok(datagram.len())
498 }
499
500 async fn start(&mut self, request: CoapRequest) -> Result<usize, String> {
501 let token = match request.observe {
502 ObserveAction::Cancel => {
503 let path = request.path_string();
504 let Some(token) = self
505 .exchanges
506 .iter()
507 .find(|(_, ex)| {
508 ex.request.observe == ObserveAction::Register
509 && ex.request.path_string() == path
510 })
511 .map(|(t, _)| t.clone())
512 else {
513 enqueue(
514 &self.turn_tx,
515 error_event(
516 &request,
517 "not_observing",
518 format!("there is no observation of {path} to cancel"),
519 ),
520 self.client_id,
521 );
522 return Ok(0);
523 };
524 if let Some(ex) = self.exchanges.get_mut(&token) {
526 ex.request = request.clone();
527 ex.body.clear();
528 ex.blocks = 0;
529 ex.next_block = 0;
530 ex.upload = None;
531 }
532 token
533 }
534 _ => {
535 if self.exchanges.len() >= MAX_EXCHANGES {
536 return Err(format!(
537 "{MAX_EXCHANGES} exchanges (requests and observations) are already open"
538 ));
539 }
540 let token = self.fresh_token();
541 self.exchanges.insert(
542 token.clone(),
543 Exchange {
544 request: request.clone(),
545 message_id: 0,
546 datagram: Vec::new(),
547 awaiting_ack: false,
548 retransmissions: 0,
549 interval: Duration::ZERO,
550 deadline: None,
551 body: Vec::new(),
552 blocks: 0,
553 next_block: 0,
554 observing: false,
555 upload: None,
556 blocks_sent: 0,
557 },
558 );
559 token
560 }
561 };
562 let mid = self.fresh_mid();
563 let part = match first_block(&request, self.reliability.block_szx) {
564 Some(block) => {
565 if let Some(ex) = self.exchanges.get_mut(&token) {
566 ex.upload = Some(block);
567 ex.blocks_sent = 1;
568 }
569 Part::Block1(block)
570 }
571 None => Part::Whole,
572 };
573 let msg = request_message(&request, mid, &token, part);
574 match self.transmit(&token, msg).await {
575 Ok(n) => {
576 debug!(
577 "CoAP client {} sent {} {} mid {mid}",
578 self.client_id,
579 request.method,
580 request.path_string()
581 );
582 Ok(n)
583 }
584 Err(e) => {
585 self.exchanges.remove(&token);
586 Err(format!("send failed: {e}"))
587 }
588 }
589 }
590
591 async fn send_empty(&self, mtype: MessageType, message_id: u16) {
592 let msg = CoapMessage {
593 mtype,
594 code: CODE_EMPTY,
595 message_id,
596 token: Vec::new(),
597 options: Vec::new(),
598 payload: Vec::new(),
599 };
600 if let Ok(bytes) = msg.encode() {
601 let _ = self.socket.send(&bytes).await;
602 }
603 }
604
605 async fn expire(&mut self) {
607 let now = Instant::now();
608 let due: Vec<Vec<u8>> = self
609 .exchanges
610 .iter()
611 .filter(|(_, ex)| ex.deadline.is_some_and(|d| d <= now))
612 .map(|(t, _)| t.clone())
613 .collect();
614 for token in due {
615 let Some(ex) = self.exchanges.get_mut(&token) else {
616 continue;
617 };
618 if ex.awaiting_ack && ex.retransmissions < self.reliability.max_retransmit {
619 ex.retransmissions += 1;
620 ex.interval *= 2;
621 ex.deadline = Some(now + ex.interval);
622 debug!(
623 "CoAP client {} retransmission {} of mid {}",
624 self.client_id, ex.retransmissions, ex.message_id
625 );
626 let datagram = ex.datagram.clone();
627 let _ = self.socket.send(&datagram).await;
628 continue;
629 }
630 let ex = self.exchanges.remove(&token).expect("present above");
631 let message = if ex.awaiting_ack {
632 format!(
633 "no acknowledgement after {} retransmissions",
634 ex.retransmissions
635 )
636 } else {
637 format!("no response within {} seconds", RESPONSE_TIMEOUT.as_secs())
638 };
639 warn!(
640 "CoAP client {} {} {} timed out decision=timeout",
641 self.client_id,
642 ex.request.method,
643 ex.request.path_string()
644 );
645 enqueue(
646 &self.turn_tx,
647 error_event(&ex.request, "timeout", message),
648 self.client_id,
649 );
650 }
651 }
652
653 async fn handle_datagram(&mut self, datagram: &[u8]) {
654 let msg = match CoapMessage::decode(datagram) {
655 Ok(m) => m,
656 Err(e) => {
657 warn!(
658 "CoAP client {} dropped an undecodable datagram: {e} decision=undecodable",
659 self.client_id
660 );
661 return;
662 }
663 };
664 match msg.mtype {
665 MessageType::Acknowledgement => {
666 let Some(token) = self
667 .exchanges
668 .iter()
669 .find(|(_, ex)| ex.awaiting_ack && ex.message_id == msg.message_id)
670 .map(|(t, _)| t.clone())
671 else {
672 return; };
674 if let Some(ex) = self.exchanges.get_mut(&token) {
675 ex.awaiting_ack = false;
676 ex.deadline = Some(Instant::now() + RESPONSE_TIMEOUT);
677 }
678 if msg.code != CODE_EMPTY && msg.token == token {
679 self.handle_response(token, msg).await;
680 }
681 }
682 MessageType::Reset => {
683 let Some(token) = self
684 .exchanges
685 .iter()
686 .find(|(_, ex)| ex.message_id == msg.message_id)
687 .map(|(t, _)| t.clone())
688 else {
689 return;
690 };
691 if let Some(ex) = self.exchanges.remove(&token) {
692 enqueue(
693 &self.turn_tx,
694 error_event(
695 &ex.request,
696 "reset",
697 "the server rejected the message with RST".to_string(),
698 ),
699 self.client_id,
700 );
701 }
702 }
703 MessageType::Confirmable | MessageType::NonConfirmable => {
704 let is_response = code_class(msg.code) >= 2;
705 let known = is_response && self.exchanges.contains_key(&msg.token);
706 if !known {
707 self.send_empty(MessageType::Reset, msg.message_id).await;
711 return;
712 }
713 if msg.mtype == MessageType::Confirmable {
714 self.send_empty(MessageType::Acknowledgement, msg.message_id)
715 .await;
716 }
717 if self.recent.contains(&msg.message_id) {
718 return; }
720 self.recent.push_back(msg.message_id);
721 if self.recent.len() > RECENT_MIDS {
722 self.recent.pop_front();
723 }
724 let token = msg.token.clone();
725 self.handle_response(token, msg).await;
726 }
727 }
728 }
729
730 async fn handle_response(&mut self, token: Vec<u8>, msg: CoapMessage) {
731 let client_id = self.client_id;
732 let Some(ex) = self.exchanges.get_mut(&token) else {
733 return;
734 };
735 let has_observe = msg.option_uint(OPT_OBSERVE).is_some();
736
737 if let Some(sent) = ex.upload {
741 let echo = msg
742 .option_values(OPT_BLOCK1)
743 .first()
744 .and_then(|v| BlockValue::parse(v));
745 if msg.code == CODE_CONTINUE {
746 if echo.is_some_and(|e| e.num != sent.num) {
747 let ex = self.exchanges.remove(&token).expect("present above");
748 enqueue(
749 &self.turn_tx,
750 error_event(
751 &ex.request,
752 "bad_block",
753 format!(
754 "the server continued block {} while block {} was sent",
755 echo.map_or(0, |e| e.num),
756 sent.num
757 ),
758 ),
759 client_id,
760 );
761 return;
762 }
763 let Some(next) = next_block(ex.request.payload.len(), sent, echo) else {
764 let ex = self.exchanges.remove(&token).expect("present above");
765 enqueue(
766 &self.turn_tx,
767 error_event(
768 &ex.request,
769 "bad_block",
770 "the server asked for more after the last block".into(),
771 ),
772 client_id,
773 );
774 return;
775 };
776 ex.upload = Some(next);
777 ex.blocks_sent += 1;
778 let request = ex.request.clone();
779 let mid = self.fresh_mid();
780 let msg = request_message(&request, mid, &token, Part::Block1(next));
781 if let Err(e) = self.transmit(&token, msg).await {
782 if let Some(ex) = self.exchanges.remove(&token) {
783 enqueue(
784 &self.turn_tx,
785 error_event(&ex.request, "timeout", format!("send failed: {e}")),
786 client_id,
787 );
788 }
789 }
790 return;
791 }
792 ex.upload = None;
793 }
794
795 if ex.request.observe == ObserveAction::Cancel {
796 if has_observe {
797 return; }
799 let ex = self.exchanges.remove(&token).expect("present above");
800 let mut data = response_data(&ex.request, &msg, &msg.payload, 1);
801 data["observing"] = json!(false);
802 enqueue(
803 &self.turn_tx,
804 Event::new(&COAP_RESPONSE_EVENT, data),
805 client_id,
806 );
807 return;
808 }
809
810 if ex.observing {
812 let mut data = response_data(&ex.request, &msg, &msg.payload, 1);
813 data["sequence"] = json!(msg.option_uint(OPT_OBSERVE).unwrap_or(0));
814 if msg
815 .option_values(OPT_BLOCK2)
816 .first()
817 .and_then(|v| parse_block(v))
818 .is_some_and(|(_, more, _)| more)
819 {
820 data["truncated"] = json!(true);
821 }
822 enqueue(
823 &self.turn_tx,
824 Event::new(&COAP_NOTIFICATION_EVENT, data),
825 client_id,
826 );
827 return;
828 }
829
830 if let Some(value) = msg.option_values(OPT_BLOCK2).first() {
832 let Some((num, more, szx)) = parse_block(value) else {
833 let ex = self.exchanges.remove(&token).expect("present above");
834 enqueue(
835 &self.turn_tx,
836 error_event(&ex.request, "bad_block", "unreadable Block2 option".into()),
837 client_id,
838 );
839 return;
840 };
841 if num != ex.next_block {
842 let expected = ex.next_block;
843 let ex = self.exchanges.remove(&token).expect("present above");
844 enqueue(
845 &self.turn_tx,
846 error_event(
847 &ex.request,
848 "bad_block",
849 format!("the server sent block {num}; block {expected} was expected"),
850 ),
851 client_id,
852 );
853 return;
854 }
855 if ex.body.len() + msg.payload.len() > MAX_BODY {
856 let ex = self.exchanges.remove(&token).expect("present above");
857 warn!(
858 "CoAP client {client_id} {} passed {MAX_BODY} bytes decision=body_too_large",
859 ex.request.path_string()
860 );
861 enqueue(
862 &self.turn_tx,
863 error_event(
864 &ex.request,
865 "body_too_large",
866 format!("the Block2 transfer passed {MAX_BODY} bytes"),
867 ),
868 client_id,
869 );
870 return;
871 }
872 ex.body.extend_from_slice(&msg.payload);
873 ex.blocks += 1;
874 if has_observe && ex.request.observe == ObserveAction::Register {
875 ex.observing = true;
876 }
877 if more {
878 ex.next_block = num + 1;
879 let request = ex.request.clone();
880 let mid = self.fresh_mid();
881 let next = request_message(&request, mid, &token, Part::Block2(num + 1, szx));
882 if let Err(e) = self.transmit(&token, next).await {
883 if let Some(ex) = self.exchanges.remove(&token) {
884 enqueue(
885 &self.turn_tx,
886 error_event(&ex.request, "timeout", format!("send failed: {e}")),
887 client_id,
888 );
889 }
890 }
891 return;
892 }
893 } else {
894 ex.body = msg.payload.clone();
895 ex.blocks = 1;
896 if has_observe && ex.request.observe == ObserveAction::Register {
897 ex.observing = true;
898 }
899 }
900
901 let body = std::mem::take(&mut ex.body);
903 let mut data = response_data(&ex.request, &msg, &body, ex.blocks);
904 if ex.blocks_sent > 0 {
905 data["blocks_sent"] = json!(ex.blocks_sent);
906 }
907 if ex.request.observe == ObserveAction::Register {
908 data["observing"] = json!(ex.observing);
909 }
910 if ex.observing {
911 ex.deadline = None;
913 ex.awaiting_ack = false;
914 } else {
915 self.exchanges.remove(&token);
916 }
917 enqueue(
918 &self.turn_tx,
919 Event::new(&COAP_RESPONSE_EVENT, data),
920 client_id,
921 );
922 }
923}
924
925#[allow(clippy::too_many_arguments)]
927async fn run_transport(
928 socket: UdpSocket,
929 reliability: Reliability,
930 mut outbound_rx: mpsc::Receiver<Outbound>,
931 turn_tx: mpsc::Sender<Event>,
932 app_state: Arc<AppState>,
933 status_tx: mpsc::UnboundedSender<String>,
934 client_id: ClientId,
935 turn_abort: tokio::task::AbortHandle,
936) {
937 let mut t = Transport {
938 socket,
939 reliability,
940 exchanges: HashMap::new(),
941 next_mid: rand::random::<u16>(),
942 recent: VecDeque::new(),
943 turn_tx,
944 client_id,
945 };
946 let mut buf = vec![0u8; MAX_MESSAGE_LEN + 1];
949
950 let status = loop {
951 let deadline = t.exchanges.values().filter_map(|ex| ex.deadline).min();
952 tokio::select! {
953 received = t.socket.recv(&mut buf) => match received {
954 Ok(n) if n > MAX_MESSAGE_LEN => warn!(
955 "CoAP client {client_id} dropped a datagram over {MAX_MESSAGE_LEN} bytes \
956 decision=oversize"
957 ),
958 Ok(n) => {
959 let datagram = buf[..n].to_vec();
960 t.handle_datagram(&datagram).await;
961 }
962 Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
965 debug!("CoAP client {client_id}: {e}");
966 }
967 Err(e) => {
968 error!("CoAP client {client_id} socket error: {e}");
969 break ClientStatus::Error(e.to_string());
970 }
971 },
972 _ = async {
973 match deadline {
974 Some(d) => tokio::time::sleep_until(d).await,
975 None => std::future::pending::<()>().await,
976 }
977 } => t.expire().await,
978 out = outbound_rx.recv() => match out {
979 None => break ClientStatus::Disconnected,
980 Some(Outbound::Disconnect { ack }) => {
981 let _ = ack.send(());
982 info!("CoAP client {client_id} stopped on request");
983 break ClientStatus::Disconnected;
984 }
985 Some(Outbound::Request { request, ack }) => {
986 let _ = ack.send(t.start(request).await);
987 }
988 },
989 }
990 };
991
992 turn_abort.abort();
993 app_state.remove_client_handle(client_id).await;
994 app_state.update_client_status(client_id, status).await;
995 let _ = status_tx.send(format!("[CLIENT] CoAP client {client_id} stopped"));
996 let _ = status_tx.send("__UPDATE_UI__".to_string());
997}
998
999async fn run_turns(
1001 mut turn_rx: mpsc::Receiver<Event>,
1002 outbound_tx: mpsc::Sender<Outbound>,
1003 protocol: Arc<CoapClientProtocol>,
1004 llm_client: OllamaClient,
1005 app_state: Arc<AppState>,
1006 status_tx: mpsc::UnboundedSender<String>,
1007 client_id: ClientId,
1008) {
1009 while let Some(event) = turn_rx.recv().await {
1010 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
1011 continue;
1012 };
1013 let memory = app_state
1014 .get_memory_for_client(client_id)
1015 .await
1016 .unwrap_or_default();
1017 match call_llm_for_client(
1018 &llm_client,
1019 &app_state,
1020 client_id.to_string(),
1021 &instruction,
1022 &memory,
1023 Some(&event),
1024 protocol.as_ref(),
1025 &status_tx,
1026 )
1027 .await
1028 {
1029 Ok(result) => {
1030 if let Some(mem) = result.memory_updates {
1031 app_state.set_memory_for_client(client_id, mem).await;
1032 }
1033 info!(
1034 "CoAP client {client_id} {} decision={} actions={}",
1035 event.id(),
1036 if result.actions.is_empty() {
1037 "model_silent"
1038 } else {
1039 "model_actions"
1040 },
1041 result.actions.len()
1042 );
1043 for action in result.actions {
1044 match apply_action(protocol.as_ref(), &outbound_tx, action).await {
1045 Ok(Applied::Disconnect) => return,
1046 Ok(_) => {}
1047 Err(e) => {
1048 error!("CoAP client {client_id} action failed: {e}");
1049 let _ = status_tx.send(format!(
1050 "[ERROR] CoAP client {client_id} action failed: {e}"
1051 ));
1052 }
1053 }
1054 }
1055 }
1056 Err(e) => error!(
1057 "CoAP client {client_id} {} decision=llm_error: {e}",
1058 event.id()
1059 ),
1060 }
1061 }
1062}
1063
1064async fn apply_action(
1067 protocol: &CoapClientProtocol,
1068 outbound_tx: &mpsc::Sender<Outbound>,
1069 action: Value,
1070) -> Result<Applied> {
1071 match protocol.execute_action(action)? {
1072 ClientActionResult::Custom { name, data } if name == REQUEST_RESULT => {
1073 let request = request_from_action(&data)?
1074 .ok_or_else(|| anyhow!("a request action produced no request"))?;
1075 let (ack, done) = oneshot::channel();
1076 outbound_tx
1077 .send(Outbound::Request { request, ack })
1078 .await
1079 .map_err(|_| anyhow!("the client is stopped"))?;
1080 let bytes = done
1081 .await
1082 .map_err(|_| anyhow!("the client stopped before the request was sent"))?
1083 .map_err(|e| anyhow!(e))?;
1084 Ok(if bytes == 0 {
1085 Applied::Nothing
1086 } else {
1087 Applied::Sent(bytes)
1088 })
1089 }
1090 ClientActionResult::Disconnect => {
1091 let (ack, done) = oneshot::channel();
1092 if outbound_tx.send(Outbound::Disconnect { ack }).await.is_ok() {
1093 let _ = done.await;
1094 }
1095 Ok(Applied::Disconnect)
1096 }
1097 _ => Ok(Applied::Nothing),
1098 }
1099}
1100
1101async fn command_loop(
1102 mut command_rx: mpsc::Receiver<ClientCommand>,
1103 protocol: Arc<CoapClientProtocol>,
1104 outbound_tx: mpsc::Sender<Outbound>,
1105 client_id: ClientId,
1106 app_state: Arc<AppState>,
1107 status_tx: mpsc::UnboundedSender<String>,
1108) {
1109 while let Some(command) = command_rx.recv().await {
1110 let action = command.action.clone();
1111 let outcome = match apply_action(protocol.as_ref(), &outbound_tx, action.clone()).await {
1112 Ok(Applied::Sent(bytes_sent)) => ClientSendOutcome::Sent { bytes_sent },
1113 Ok(Applied::Nothing) => ClientSendOutcome::Executed {
1114 detail: "executed (nothing to send)".to_string(),
1115 },
1116 Ok(Applied::Disconnect) => ClientSendOutcome::Disconnected,
1117 Err(e) => ClientSendOutcome::Rejected {
1118 error: e.to_string(),
1119 },
1120 };
1121 app_state
1122 .record_access_log(
1123 AccessLogOwner::Client(client_id.as_u32()),
1124 protocol.protocol_name(),
1125 None,
1126 "injected_action",
1127 action,
1128 vec![serde_json::to_value(&outcome).unwrap_or_default()],
1129 )
1130 .await;
1131 let disconnect = matches!(outcome, ClientSendOutcome::Disconnected);
1132 let _ = status_tx.send("__UPDATE_UI__".to_string());
1133 crate::client::command_support::reply(command, Ok(outcome));
1134 if disconnect {
1135 break;
1136 }
1137 }
1138}