1use alloc::rc::Rc;
28use alloc::string::String;
29use alloc::vec::Vec;
30use core::cell::RefCell;
31use core::fmt::Write as _;
32
33use heapless::{Deque, FnvIndexMap, String as HString, Vec as HVec};
34use umsh_core::PublicKey;
35use umsh_mac::SendOptions;
36use umsh_node::{LocalNode, MacBackend, NodeError, OwnedMacCommand, Subscription};
37use umsh_text::UnicastTextChatWrapper;
38
39use crate::commands::{Command, ParseError, parse};
40use crate::events::{CliEvent, EVENT_LINE_MAX, EVENT_RAW_MAX};
41use crate::io::{CliInput, CliOutput};
42use crate::logger::{CliLogger, LogLevel};
43use crate::settings::SessionSettings;
44use crate::stats::Stats;
45use umsh_hal::{ChannelStore, PeerStore, PowerControl};
46use umsh_sync::AsyncCondition;
47
48#[derive(Debug)]
61pub enum CliError<IoErr: core::fmt::Debug> {
62 Io(IoErr),
63 Node(String),
64}
65
66impl<IoErr: core::fmt::Debug> From<IoErr> for CliError<IoErr> {
67 fn from(e: IoErr) -> Self {
68 CliError::Io(e)
69 }
70}
71
72type SharedQueue<T> = Rc<RefCell<T>>;
73
74#[derive(Debug, Clone)]
76pub struct PeerEntry {
77 pub key: PublicKey,
78 pub alias: Option<HString<16>>,
79}
80
81#[derive(Debug, Clone)]
84pub struct ChannelEntry {
85 pub name: HString<16>,
86 pub key_bytes: [u8; 32],
87}
88
89#[derive(Debug, PartialEq, Eq)]
91enum ExecOutcome {
92 Continue,
93 Quit,
94}
95
96pub struct CliSession<
100 M,
101 OUT,
102 LOG,
103 PS,
104 CS,
105 PC,
106 const N_PEERS: usize,
107 const N_ALIASES: usize,
108 const N_CHANNELS: usize,
109 const N_EVENTS: usize,
110 const LINE_MAX: usize,
111> where
112 M: MacBackend,
113 M::SendError: core::fmt::Debug,
114 M::CapacityError: core::fmt::Debug,
115 OUT: CliOutput,
116 LOG: CliLogger,
117 PS: PeerStore,
118 CS: ChannelStore,
119 PC: PowerControl,
120{
121 pub(crate) node: LocalNode<M>,
122 pub(crate) local_key: PublicKey,
123 pub(crate) out: OUT,
124 pub(crate) logger: LOG,
125 pub(crate) peer_store: PS,
126 pub(crate) channel_store: CS,
127 pub(crate) power: PC,
128 pub(crate) peers: FnvIndexMap<PublicKey, PeerEntry, N_PEERS>,
129 pub(crate) aliases: FnvIndexMap<HString<16>, PublicKey, N_ALIASES>,
130 pub(crate) channels: FnvIndexMap<HString<16>, ChannelEntry, N_CHANNELS>,
131 pub(crate) events: SharedQueue<Deque<CliEvent, N_EVENTS>>,
132 pub(crate) events_dropped: SharedQueue<u64>,
133 pub(crate) stats: SharedQueue<Stats>,
134 pub(crate) wake: Rc<AsyncCondition>,
135 pub(crate) current_peer: Option<PublicKey>,
136 pub(crate) settings: SessionSettings,
137 _subs: Vec<Subscription>,
139}
140
141impl<
142 M,
143 OUT,
144 LOG,
145 PS,
146 CS,
147 PC,
148 const N_PEERS: usize,
149 const N_ALIASES: usize,
150 const N_CHANNELS: usize,
151 const N_EVENTS: usize,
152 const LINE_MAX: usize,
153> CliSession<M, OUT, LOG, PS, CS, PC, N_PEERS, N_ALIASES, N_CHANNELS, N_EVENTS, LINE_MAX>
154where
155 M: MacBackend,
156 M::SendError: core::fmt::Debug,
157 M::CapacityError: core::fmt::Debug,
158 OUT: CliOutput,
159 LOG: CliLogger,
160 PS: PeerStore,
161 CS: ChannelStore,
162 PC: PowerControl,
163{
164 pub fn new(
176 node: LocalNode<M>,
177 local_key: PublicKey,
178 out: OUT,
179 logger: LOG,
180 peer_store: PS,
181 channel_store: CS,
182 power: PC,
183 ) -> Self {
184 let events: SharedQueue<Deque<CliEvent, N_EVENTS>> = Rc::new(RefCell::new(Deque::new()));
185 let events_dropped: SharedQueue<u64> = Rc::new(RefCell::new(0));
186 let stats: SharedQueue<Stats> = Rc::new(RefCell::new(Stats::default()));
187 let wake = Rc::new(AsyncCondition::new());
188
189 let subs = register_subscriptions(
190 &node,
191 events.clone(),
192 events_dropped.clone(),
193 stats.clone(),
194 wake.clone(),
195 );
196
197 Self {
198 node,
199 local_key,
200 out,
201 logger,
202 peer_store,
203 channel_store,
204 power,
205 peers: FnvIndexMap::new(),
206 aliases: FnvIndexMap::new(),
207 channels: FnvIndexMap::new(),
208 events,
209 events_dropped,
210 stats,
211 wake,
212 current_peer: None,
213 settings: SessionSettings::default(),
214 _subs: subs,
215 }
216 }
217
218 pub fn resolve_peer(&self, token: &str) -> Option<PublicKey> {
221 if let Ok(s) = HString::<16>::try_from(token) {
222 if let Some(k) = self.aliases.get(&s) {
223 return Some(*k);
224 }
225 }
226 crate::peer_ref::try_parse_pubkey(token)
227 }
228
229 pub async fn register_peer(&mut self, key: PublicKey, alias: Option<&str>) -> bool {
237 if self.peers.contains_key(&key) {
238 return true;
239 }
240 let alias_heap = alias.and_then(|a| HString::<16>::try_from(a).ok());
241 if self
242 .peers
243 .insert(
244 key,
245 PeerEntry {
246 key,
247 alias: alias_heap.clone(),
248 },
249 )
250 .is_err()
251 {
252 return false;
253 }
254 if let Some(a) = alias_heap {
255 if self.aliases.insert(a, key).is_err() {
256 let _ = self.peers.remove(&key);
257 return false;
258 }
259 }
260 if self.node.peer(key).await.is_err() {
263 let _ = self.peers.remove(&key);
264 if let Some(a) = alias.and_then(|s| HString::<16>::try_from(s).ok()) {
265 let _ = self.aliases.remove(&a);
266 }
267 return false;
268 }
269 true
270 }
271
272 #[cfg(feature = "software-crypto")]
279 pub async fn register_channel(&mut self, name: &str, key_bytes: [u8; 32]) -> bool {
280 let hname = match HString::<16>::try_from(name) {
281 Ok(s) => s,
282 Err(_) => return false,
283 };
284 if self.channels.contains_key(&hname) {
285 return true;
286 }
287 let channel_key = umsh_core::ChannelKey(key_bytes);
288 let channel = umsh_node::Channel::private(channel_key, name);
289 if self.node.join(&channel).await.is_err() {
290 return false;
291 }
292 let entry = ChannelEntry {
293 name: hname.clone(),
294 key_bytes,
295 };
296 if self.channels.insert(hname, entry).is_err() {
297 let _ = self.node.leave(&channel).await;
298 return false;
299 }
300 true
301 }
302
303 pub async fn load_from_stores(&mut self) {
310 let mut peers: Vec<([u8; 32], Option<HString<16>>)> = Vec::new();
313 let _ = self
314 .peer_store
315 .for_each_peer(&mut |pk, alias| {
316 let alias_str = alias
317 .and_then(|a| core::str::from_utf8(a).ok())
318 .and_then(|s| HString::<16>::try_from(s).ok());
319 let _ = peers.push((*pk, alias_str));
320 })
321 .await;
322 for (pk, alias) in peers {
323 let _ = self
324 .register_peer(PublicKey(pk), alias.as_ref().map(|s| s.as_str()))
325 .await;
326 }
327
328 #[cfg(feature = "software-crypto")]
329 {
330 let mut channels: Vec<(HString<16>, [u8; 32])> = Vec::new();
331 let _ = self
332 .channel_store
333 .for_each_channel(&mut |name, key| {
334 if let Ok(s) = core::str::from_utf8(name) {
335 if let Ok(h) = HString::<16>::try_from(s) {
336 let _ = channels.push((h, *key));
337 }
338 }
339 })
340 .await;
341 for (name, key_bytes) in channels {
342 let _ = self.register_channel(name.as_str(), key_bytes).await;
343 }
344 }
345 }
346
347 fn send_opts(&self) -> SendOptions {
349 SendOptions::default()
350 .with_flood_hops(self.settings.flood_hops)
351 .with_ack_requested(self.settings.ack_requested)
352 }
353
354 pub async fn run<IN>(&mut self, input: &mut IN) -> Result<(), CliError<OUT::Error>>
366 where
367 IN: CliInput<Error = OUT::Error>,
368 {
369 use futures::future::{Either, select};
370
371 self.load_from_stores().await;
372
373 self.service_events().await?;
376
377 let mut buf = [0u8; LINE_MAX];
378 let wake = self.wake.clone();
379 loop {
380 let read_fut = input.read_line(&mut buf);
382 futures::pin_mut!(read_fut);
383 let owned: Option<String> = loop {
384 let wait_fut = wake.wait();
385 futures::pin_mut!(wait_fut);
386 match select(read_fut.as_mut(), wait_fut).await {
387 Either::Left((result, _)) => match result {
388 Ok(Some(s)) => break Some(String::from(s)),
389 Ok(None) => break None,
390 Err(e) => return Err(CliError::Io(e)),
391 },
392 Either::Right(((), _)) => {
393 self.service_events().await?;
395 }
396 }
397 };
398
399 match owned {
400 None => return Ok(()), Some(line) => match parse(&line) {
402 Err(ParseError::Empty) => {}
403 Err(e) => {
404 let msg = format_parse_error(&e);
405 self.write_err(&msg).await?;
406 }
407 Ok(cmd) => match self.execute(cmd).await? {
408 ExecOutcome::Continue => {}
409 ExecOutcome::Quit => return Ok(()),
410 },
411 },
412 }
413
414 self.service_events().await?;
416 }
417 }
418
419 async fn service_events(&mut self) -> Result<(), CliError<OUT::Error>> {
422 loop {
423 let event = self.events.borrow_mut().pop_front();
425 let Some(event) = event else { break };
426 self.handle_event(event).await?;
427 }
428 Ok(())
429 }
430
431 async fn handle_event(&mut self, event: CliEvent) -> Result<(), CliError<OUT::Error>> {
432 match event {
433 CliEvent::Received {
434 from,
435 hops: _,
436 rssi,
437 snr,
438 prefix,
439 } => {
440 if let Some([pt, rest @ ..]) = prefix.as_slice().get(0..) {
442 if *pt == umsh_core::PayloadType::TextMessage as u8 {
443 if let Ok(msg) = umsh_text::parse_text_message(rest) {
444 let alias = self.peer_alias_display(&from);
445 let mut line: HString<EVENT_LINE_MAX> = HString::new();
446 let _ = write!(
447 &mut line,
448 "<{}> {}",
449 alias,
450 msg.body_str().unwrap_or("<invalid utf-8>")
451 );
452 self.out.write_line(&line).await?;
453 if self.settings.show_hex {
454 let mut hex: HString<EVENT_LINE_MAX> = HString::new();
455 let _ = write!(&mut hex, " hex:");
456 for b in prefix.iter() {
457 let _ = write!(&mut hex, " {:02x}", b);
458 }
459 self.out.write_line(&hex).await?;
460 }
461 return Ok(());
462 }
463 }
464 }
465 let alias = self.peer_alias_display(&from);
467 let mut line: HString<EVENT_LINE_MAX> = HString::new();
468 let _ = write!(
469 &mut line,
470 "[pkt from {} rssi={:?} snr={:?}]",
471 alias, rssi, snr
472 );
473 self.out.write_line(&line).await?;
474 }
475
476 CliEvent::AckReceived { peer } => {
477 let alias = self.peer_alias_display(&peer);
478 let mut line: HString<EVENT_LINE_MAX> = HString::new();
479 let _ = write!(&mut line, "[ack from {}]", alias);
480 self.out.write_line(&line).await?;
481 }
482
483 CliEvent::AckTimeout { peer } => {
484 let alias = self.peer_alias_display(&peer);
485 let mut line: HString<EVENT_LINE_MAX> = HString::new();
486 let _ = write!(&mut line, "[ack timeout → {}]", alias);
487 self.out.write_line(&line).await?;
488 }
489
490 CliEvent::NodeDiscovered { from, name } => {
491 let alias = self.peer_alias_display(&from);
492 let mut line: HString<EVENT_LINE_MAX> = HString::new();
493 let _ = write!(
494 &mut line,
495 "[node discovered: {}{}]",
496 alias,
497 name.as_deref().unwrap_or(""),
498 );
499 self.out.write_line(&line).await?;
500 }
501
502 CliEvent::Beacon { hint, from } => {
503 let mut line: HString<EVENT_LINE_MAX> = HString::new();
504 match from {
505 Some(key) => {
506 let alias = self.peer_alias_display(&key);
507 let _ = write!(&mut line, "[beacon from {}]", alias);
508 }
509 None => {
510 let _ = write!(&mut line, "[beacon hint:{}]", hint);
512 }
513 }
514 self.out.write_line(&line).await?;
515 }
516
517 CliEvent::PfsEstablished { peer } => {
518 let alias = self.peer_alias_display(&peer);
519 let mut line: HString<EVENT_LINE_MAX> = HString::new();
520 let _ = write!(&mut line, "[pfs established with {}]", alias);
521 self.out.write_line(&line).await?;
522 }
523
524 CliEvent::PfsEnded { peer } => {
525 let alias = self.peer_alias_display(&peer);
526 let mut line: HString<EVENT_LINE_MAX> = HString::new();
527 let _ = write!(&mut line, "[pfs ended with {}]", alias);
528 self.out.write_line(&line).await?;
529 }
530
531 CliEvent::PfsFailed { peer, reason } => {
532 let alias = self.peer_alias_display(&peer);
533 let mut line: HString<EVENT_LINE_MAX> = HString::new();
534 let _ = write!(
535 &mut line,
536 "[pfs failed with {}: {}]",
537 alias,
538 pfs_failure_str(reason)
539 );
540 self.out.write_line(&line).await?;
541 }
542
543 CliEvent::Pong { peer, rtt_ms } => {
544 let alias = self.peer_alias_display(&peer);
545 let mut line: HString<EVENT_LINE_MAX> = HString::new();
546 let _ = write!(&mut line, "pong {} rtt={} ms", alias, rtt_ms);
547 self.out.write_line(&line).await?;
548 }
549
550 CliEvent::PingTimeout { peer } => {
551 let alias = self.peer_alias_display(&peer);
552 let mut line: HString<EVENT_LINE_MAX> = HString::new();
553 let _ = write!(&mut line, "[ping timeout: {}]", alias);
554 self.out.write_line(&line).await?;
555 }
556
557 CliEvent::UnknownMacCmdIn { peer, cmd_id } => {
558 let alias = self.peer_alias_display(&peer);
559 let mut line: HString<EVENT_LINE_MAX> = HString::new();
560 let _ = write!(&mut line, "[mac cmd 0x{:02x} from {}]", cmd_id, alias);
561 self.out.write_line(&line).await?;
562 }
563
564 CliEvent::OutputLine { line } => {
565 self.out.write_line(&line).await?;
566 }
567
568 CliEvent::RawTx { bytes } => {
569 if self.settings.show_raw {
570 let mut line: HString<EVENT_LINE_MAX> = HString::new();
571 let _ = write!(&mut line, "tx");
572 for b in bytes.iter() {
573 let _ = write!(&mut line, " {:02x}", b);
574 }
575 self.out.write_line(&line).await?;
576 }
577 }
578
579 CliEvent::RawRx { bytes } => {
580 if self.settings.show_raw {
581 let mut line: HString<EVENT_LINE_MAX> = HString::new();
582 let _ = write!(&mut line, "rx");
583 for b in bytes.iter() {
584 let _ = write!(&mut line, " {:02x}", b);
585 }
586 self.out.write_line(&line).await?;
587 }
588 }
589
590 CliEvent::SendText { .. }
594 | CliEvent::StartPfs { .. }
595 | CliEvent::EndPfs { .. }
596 | CliEvent::ChannelSend { .. }
597 | CliEvent::SendBeacon
598 | CliEvent::SendRaw { .. } => {}
599 }
600 Ok(())
601 }
602
603 async fn execute(&mut self, cmd: Command<'_>) -> Result<ExecOutcome, CliError<OUT::Error>> {
606 match cmd {
607 Command::Quit => Ok(ExecOutcome::Quit),
608 Command::Help(topic) => self.cmd_help(topic).await.map(|_| ExecOutcome::Continue),
609 Command::WhoAmI => self.cmd_whoami().await.map(|_| ExecOutcome::Continue),
610 Command::PeerAdd { pubkey, alias } => self
611 .cmd_peer_add(pubkey, alias)
612 .await
613 .map(|_| ExecOutcome::Continue),
614 Command::PeerAlias { peer, alias } => self
615 .cmd_peer_alias(peer, alias)
616 .await
617 .map(|_| ExecOutcome::Continue),
618 Command::PeerRm { peer } => self.cmd_peer_rm(peer).await.map(|_| ExecOutcome::Continue),
619 Command::Peers => self.cmd_peers().await.map(|_| ExecOutcome::Continue),
620 Command::Query { peer } => self.cmd_query(peer).await.map(|_| ExecOutcome::Continue),
621 Command::Set { var, val } => {
622 self.cmd_set(var, val).await.map(|_| ExecOutcome::Continue)
623 }
624 Command::SetShow => self.cmd_set_show().await.map(|_| ExecOutcome::Continue),
625 Command::Log { level } => self.cmd_log(level).await.map(|_| ExecOutcome::Continue),
626 Command::Stats => self.cmd_stats().await.map(|_| ExecOutcome::Continue),
627 Command::Counters => self.cmd_counters().await.map(|_| ExecOutcome::Continue),
628 Command::Channels => self.cmd_channels().await.map(|_| ExecOutcome::Continue),
629 Command::PfsStatus { peer } => self
630 .cmd_pfs_status(peer)
631 .await
632 .map(|_| ExecOutcome::Continue),
633 Command::Msg { peer, text } => self
634 .cmd_msg(peer, text)
635 .await
636 .map(|_| ExecOutcome::Continue),
637 Command::Text { body } => self.cmd_text(body).await.map(|_| ExecOutcome::Continue),
638 Command::Me { action } => self.cmd_me(action).await.map(|_| ExecOutcome::Continue),
639 Command::Ping { peer, bytes } => self
640 .cmd_ping(peer, bytes)
641 .await
642 .map(|_| ExecOutcome::Continue),
643 Command::PfsStart { peer, minutes } => self
644 .cmd_pfs_start(peer, minutes)
645 .await
646 .map(|_| ExecOutcome::Continue),
647 Command::PfsEnd { peer } => self.cmd_pfs_end(peer).await.map(|_| ExecOutcome::Continue),
648 Command::Beacon => self.cmd_beacon().await.map(|_| ExecOutcome::Continue),
649 Command::ChannelJoin { name, key } => self
650 .cmd_channel_join(name, key)
651 .await
652 .map(|_| ExecOutcome::Continue),
653 Command::ChannelLeave { name } => self
654 .cmd_channel_leave(name)
655 .await
656 .map(|_| ExecOutcome::Continue),
657 Command::ChannelSend { name, text } => self
658 .cmd_channel_send(name, text)
659 .await
660 .map(|_| ExecOutcome::Continue),
661 Command::Raw { peer, hex } => {
662 self.cmd_raw(peer, hex).await.map(|_| ExecOutcome::Continue)
663 }
664 Command::PowerOff => self.cmd_power_off().await.map(|_| ExecOutcome::Continue),
665 Command::Reboot => self.cmd_reboot().await.map(|_| ExecOutcome::Continue),
666 }
667 }
668
669 async fn cmd_power_off(&mut self) -> Result<(), CliError<OUT::Error>> {
670 self.out.write_line("powering off").await?;
671 self.power.request_power_off();
672 Ok(())
673 }
674
675 async fn cmd_reboot(&mut self) -> Result<(), CliError<OUT::Error>> {
676 self.out.write_line("rebooting").await?;
677 self.power.request_reboot();
678 Ok(())
679 }
680
681 async fn cmd_help(&mut self, topic: Option<&str>) -> Result<(), CliError<OUT::Error>> {
684 if let Some(t) = topic {
685 return self.cmd_help_topic(t).await;
686 }
687 let lines: &[&str] = &[
688 "session:",
689 " /help [command] show help, or detailed help for <command>",
690 " /quit exit the CLI",
691 " /whoami print the local public key",
692 " /log <level> set verbosity: error|warn|info|debug|trace",
693 " /poweroff request a hardware power-off (alias: /off)",
694 " /reboot request a soft reboot",
695 "",
696 "peers:",
697 " /peer add <pubkey> [alias] register a peer (base58/base64/hex, 32 bytes)",
698 " /peer alias <peer> <alias> rename a registered peer",
699 " /peer rm <peer-ref> remove a peer",
700 " /peers list registered peers with full public key",
701 " /query <peer-ref> set the current peer for bare text",
702 "",
703 "messaging:",
704 " /msg <peer-ref> <text> send text to peer",
705 " <text> bare text goes to the current peer",
706 " /me <action> emote (e.g. /me waves)",
707 " /raw <peer-ref> <hex> send raw hex payload bytes",
708 "",
709 "diagnostics:",
710 " /ping <peer-ref> [bytes] send an EchoRequest (default 8 bytes)",
711 " /beacon broadcast a beacon",
712 " /stats show TX/RX counters, RSSI, queue depth",
713 " /counters show frame-counter state (local TX + per-peer RX)",
714 "",
715 "pfs (perfect forward secrecy):",
716 " /pfs start <peer-ref> [min] request a PFS session (default 60 min)",
717 " /pfs end <peer-ref> end a PFS session",
718 " /pfs status [peer-ref] show PFS session state",
719 "",
720 "channels:",
721 " /channel join <name> <key> join a channel (key is base58)",
722 " /channel leave <name> leave a channel",
723 " /channel send <name> <txt> send multicast text",
724 " /channels list joined channels",
725 "",
726 "settings:",
727 " /set show current settings",
728 " /set <var> <val> flood_hops|ack_requested|show_hex|show_raw",
729 ];
730 for l in lines {
731 self.out.write_line(l).await?;
732 }
733 Ok(())
734 }
735
736 async fn cmd_help_topic(&mut self, topic: &str) -> Result<(), CliError<OUT::Error>> {
737 let t = topic.trim().trim_start_matches('/');
738 let detail: &[&str] = match t {
739 "quit" => &["/quit — exit the CLI (EOF does the same)."],
740 "help" => &[
741 "/help [command] — list all commands, or show detailed help for one.",
742 " example: /help ping",
743 ],
744 "whoami" => &["/whoami — print the local public key as hex."],
745 "log" => &[
746 "/log <level> — set log verbosity.",
747 " levels: error, warn, info, debug, trace",
748 ],
749 "poweroff" | "off" => &[
750 "/poweroff — request a controlled power-off.",
751 " Persists any pending counters, sleeps the display, drops the",
752 " peripheral rail, and enters System OFF. On supported boards a",
753 " button press resumes the device (via reset + reboot).",
754 " Alias: /off.",
755 ],
756 "reboot" => &[
757 "/reboot — request a soft reboot.",
758 " Persists any pending counters, then triggers a system reset.",
759 " The device comes back up running the same firmware image.",
760 ],
761 "peer" => &[
762 "/peer add <pubkey> [alias] — register a peer.",
763 " <pubkey> accepts base58, base64, or hex (32-byte Ed25519 key).",
764 " Also registers the peer at the MAC layer so inbound frames validate.",
765 "/peer alias <peer-ref> <alias> — rename a registered peer.",
766 "/peer rm <peer-ref> — remove a peer. <peer-ref> is an alias or full key.",
767 ],
768 "peers" => &["/peers — list registered peers: alias and full public key (hex)."],
769 "query" => &[
770 "/query <peer-ref> — set the current peer for bare-text sends.",
771 " After /query bob, a bare line is sent to bob as a text message.",
772 ],
773 "msg" => &["/msg <peer-ref> <text> — send a text message to a peer."],
774 "me" => &[
775 "/me <action> — send an emote to the current peer.",
776 " example: /me waves → sent as \"* waves\"",
777 ],
778 "raw" => &[
779 "/raw <peer-ref> <hex> — send raw payload bytes as a unicast packet.",
780 " <hex> is an even-length hex string (no 0x prefix, no spaces).",
781 ],
782 "ping" => &[
783 "/ping <peer-ref> [bytes] — send a MAC-level EchoRequest.",
784 " [bytes] is the total payload size (2..=60, default 8).",
785 " The first 2 bytes are a nonce used to match the response.",
786 " Prints \"pong <peer> rtt=<ms>\" when the reply arrives.",
787 ],
788 "beacon" => &["/beacon — broadcast a beacon frame announcing this node."],
789 "stats" => &[
790 "/stats — print counters maintained by the CLI.",
791 " TX/RX packets, ACK outcomes, last RSSI/SNR, event-queue depth,",
792 " and events_dropped (non-zero if the inbound queue overflowed).",
793 ],
794 "counters" => &[
795 "/counters — show the local TX frame counter and each known peer's RX counter (live and persisted boundaries).",
796 ],
797 "pfs" => &[
798 "/pfs start <peer-ref> [minutes] — request a PFS session.",
799 " [minutes] is the requested lifetime (default 60).",
800 "/pfs end <peer-ref> — tear down an active PFS session.",
801 "/pfs status [peer-ref] — show PFS state for one peer or all.",
802 ],
803 "channel" | "channels" => &[
804 "/channel join <name> <key-b58> — bind a channel by name + shared key.",
805 "/channel leave <name> — leave a channel.",
806 "/channel send <name> <text> — send a multicast text message.",
807 "/channels — list currently joined channels.",
808 ],
809 "set" => &[
810 "/set — show current CLI-local settings.",
811 "/set <var> <val> — change one setting (resets on exit).",
812 " flood_hops u8 in 0..=15 (default 5) — max FHOPS_REM; a known route narrows it",
813 " ack_requested bool (default true) — request MAC acks on unicast",
814 " show_hex bool (default false) — also print inbound bytes as hex",
815 " show_raw bool (default false) — log every TX/RX packet as hex",
816 ],
817 other => {
818 let mut msg: HString<EVENT_LINE_MAX> = HString::new();
819 let _ = write!(
820 &mut msg,
821 "no help for '{}' — try /help for the full list",
822 other
823 );
824 return self.write_err(&msg).await;
825 }
826 };
827 for l in detail {
828 self.out.write_line(l).await?;
829 }
830 Ok(())
831 }
832
833 async fn cmd_whoami(&mut self) -> Result<(), CliError<OUT::Error>> {
834 let mut line: HString<EVENT_LINE_MAX> = HString::new();
835 let _ = write!(&mut line, "local: {}", self.local_key);
836 self.out.write_line(&line).await?;
837 Ok(())
838 }
839
840 async fn cmd_peer_add(
841 &mut self,
842 pubkey: &str,
843 alias: Option<&str>,
844 ) -> Result<(), CliError<OUT::Error>> {
845 let key = match crate::peer_ref::try_parse_pubkey(pubkey) {
846 Some(k) => k,
847 None => return self.write_err("invalid pubkey").await,
848 };
849 let alias_heap = match alias {
850 Some(a) => match HString::<16>::try_from(a) {
851 Ok(s) => Some(s),
852 Err(_) => return self.write_err("alias too long (max 16 chars)").await,
853 },
854 None => None,
855 };
856 if self.peers.contains_key(&key) {
857 return self.write_err("peer already registered").await;
858 }
859 if self
860 .peers
861 .insert(
862 key,
863 PeerEntry {
864 key,
865 alias: alias_heap.clone(),
866 },
867 )
868 .is_err()
869 {
870 return self.write_err("peer table full").await;
871 }
872 if let Some(a) = alias_heap.clone() {
873 if self.aliases.insert(a, key).is_err() {
874 let _ = self.peers.remove(&key);
875 return self.write_err("alias table full").await;
876 }
877 }
878 if let Err(e) = self.node.peer(key).await {
880 let _ = self.peers.remove(&key);
881 if let Some(a) = alias_heap {
882 let _ = self.aliases.remove(&a);
883 }
884 let msg = node_err_str(&e);
885 return self.write_err(&msg).await;
886 }
887 let alias_bytes = alias_heap.as_ref().map(|s: &HString<16>| s.as_bytes());
889 let _ = self.peer_store.store_peer(&key.0, alias_bytes).await;
890 self.out.write_line("ok").await?;
891 Ok(())
892 }
893
894 async fn cmd_peer_rm(&mut self, peer: &str) -> Result<(), CliError<OUT::Error>> {
895 let Some(key) = self.resolve_peer(peer) else {
896 return self.write_err("unknown peer").await;
897 };
898 let entry = match self.peers.remove(&key) {
899 Some(e) => e,
900 None => return self.write_err("peer not in table").await,
901 };
902 if let Some(a) = entry.alias {
903 let _ = self.aliases.remove(&a);
904 }
905 if self.current_peer == Some(key) {
906 self.current_peer = None;
907 }
908 let _ = self.peer_store.delete_peer(&key.0).await;
910 self.out.write_line("ok").await?;
911 Ok(())
912 }
913
914 async fn cmd_peer_alias(
915 &mut self,
916 peer: &str,
917 new_alias: &str,
918 ) -> Result<(), CliError<OUT::Error>> {
919 let Some(key) = self.resolve_peer(peer) else {
920 return self.write_err("unknown peer").await;
921 };
922 let new_alias_heap = match HString::<16>::try_from(new_alias) {
923 Ok(s) => s,
924 Err(_) => return self.write_err("alias too long (max 16 chars)").await,
925 };
926 if let Some(entry) = self.peers.get(&key) {
928 if let Some(old_alias) = entry.alias.clone() {
929 let _ = self.aliases.remove(&old_alias);
930 }
931 }
932 if self.aliases.insert(new_alias_heap.clone(), key).is_err() {
934 return self.write_err("alias table full").await;
935 }
936 if let Some(entry) = self.peers.get_mut(&key) {
938 entry.alias = Some(new_alias_heap.clone());
939 }
940 let _ = self
942 .peer_store
943 .store_peer(&key.0, Some(new_alias_heap.as_bytes()))
944 .await;
945 self.out.write_line("ok").await?;
946 Ok(())
947 }
948
949 async fn cmd_peers(&mut self) -> Result<(), CliError<OUT::Error>> {
950 if self.peers.is_empty() {
951 self.out.write_line("(no peers)").await?;
952 return Ok(());
953 }
954 let mut lines: Vec<String> = Vec::new();
955 for (_k, entry) in self.peers.iter() {
956 let mut line: HString<EVENT_LINE_MAX> = HString::new();
957 let alias = entry.alias.as_deref().unwrap_or("-");
958 let _ = write!(&mut line, "{:16} {}", alias, entry.key);
959 lines.push(String::from(line.as_str()));
960 }
961 for l in lines {
962 self.out.write_line(&l).await?;
963 }
964 Ok(())
965 }
966
967 async fn cmd_query(&mut self, peer: &str) -> Result<(), CliError<OUT::Error>> {
968 let Some(key) = self.resolve_peer(peer) else {
969 return self.write_err("unknown peer").await;
970 };
971 self.current_peer = Some(key);
972 self.out.write_line("ok").await?;
973 Ok(())
974 }
975
976 async fn cmd_set_show(&mut self) -> Result<(), CliError<OUT::Error>> {
977 let mut line: HString<EVENT_LINE_MAX> = HString::new();
978 let _ = write!(
979 &mut line,
980 "flood_hops={} ack_requested={} show_hex={} show_raw={}",
981 self.settings.flood_hops,
982 self.settings.ack_requested,
983 self.settings.show_hex,
984 self.settings.show_raw,
985 );
986 self.out.write_line(&line).await?;
987 Ok(())
988 }
989
990 async fn cmd_set(&mut self, var: &str, val: &str) -> Result<(), CliError<OUT::Error>> {
991 match var {
992 "flood_hops" => match val.parse::<u8>() {
993 Ok(v) if v <= 15 => {
994 self.settings.flood_hops = v;
995 self.out.write_line("ok").await?;
996 }
997 _ => self.write_err("flood_hops must be 0..=15").await?,
998 },
999 "ack_requested" => match parse_bool(val) {
1000 Some(b) => {
1001 self.settings.ack_requested = b;
1002 self.out.write_line("ok").await?;
1003 }
1004 None => self.write_err("ack_requested: expected true|false").await?,
1005 },
1006 "show_hex" => match parse_bool(val) {
1007 Some(b) => {
1008 self.settings.show_hex = b;
1009 self.out.write_line("ok").await?;
1010 }
1011 None => self.write_err("show_hex: expected true|false").await?,
1012 },
1013 "show_raw" => match parse_bool(val) {
1014 Some(b) => {
1015 self.settings.show_raw = b;
1016 self.out.write_line("ok").await?;
1017 }
1018 None => self.write_err("show_raw: expected true|false").await?,
1019 },
1020 _ => {
1021 self.write_err("unknown setting (flood_hops / ack_requested / show_hex / show_raw)")
1022 .await?
1023 }
1024 }
1025 Ok(())
1026 }
1027
1028 async fn cmd_log(&mut self, level: &str) -> Result<(), CliError<OUT::Error>> {
1029 let lvl = match level {
1030 "error" => LogLevel::Error,
1031 "warn" => LogLevel::Warn,
1032 "info" => LogLevel::Info,
1033 "debug" => LogLevel::Debug,
1034 "trace" => LogLevel::Trace,
1035 _ => return self.write_err("level: error|warn|info|debug|trace").await,
1036 };
1037 self.logger.set_level(lvl);
1038 self.out.write_line("ok").await?;
1039 Ok(())
1040 }
1041
1042 async fn cmd_stats(&mut self) -> Result<(), CliError<OUT::Error>> {
1043 let s = self.stats.borrow().clone();
1044 let dropped = *self.events_dropped.borrow();
1045 let depth = self.events.borrow().len();
1046 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1047 let _ = write!(
1048 &mut line,
1049 "rx={} tx={} ack_ok={} ack_timeout={} beacons={} discovered={} \
1050 event_depth={} dropped_events={}",
1051 s.packets_rx,
1052 s.packets_tx,
1053 s.acks_ok,
1054 s.acks_timeout,
1055 s.beacons_rx,
1056 s.nodes_discovered,
1057 depth,
1058 dropped,
1059 );
1060 self.out.write_line(&line).await?;
1061 if let Some(rssi) = s.last_rssi {
1062 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1063 let _ = write!(&mut line, "last_rssi={} last_snr={:?}", rssi, s.last_snr);
1064 self.out.write_line(&line).await?;
1065 }
1066 Ok(())
1067 }
1068
1069 async fn cmd_counters(&mut self) -> Result<(), CliError<OUT::Error>> {
1070 let tx = self.node.frame_counter().await.unwrap_or(0);
1071 let persisted = self.node.persisted_frame_counter().await.unwrap_or(0);
1072 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1073 let _ = write!(&mut line, "local: tx={} (persisted {})", tx, persisted);
1074 self.out.write_line(&line).await?;
1075
1076 let mut entries: alloc::vec::Vec<(PublicKey, u32, u32)> = alloc::vec::Vec::new();
1078 self.node
1079 .for_each_peer_counter(&mut |pk, last_accepted, persisted_rx| {
1080 entries.push((pk, last_accepted, persisted_rx));
1081 })
1082 .await;
1083
1084 for (pk, last_accepted, persisted_rx) in entries {
1085 let alias = self.peer_alias_display(&pk);
1086 let mut s: HString<EVENT_LINE_MAX> = HString::new();
1087 let _ = write!(
1088 &mut s,
1089 "{:16} rx={} (persisted {})",
1090 alias, last_accepted, persisted_rx
1091 );
1092 self.out.write_line(&s).await?;
1093 }
1094
1095 Ok(())
1096 }
1097
1098 async fn cmd_channels(&mut self) -> Result<(), CliError<OUT::Error>> {
1099 if self.channels.is_empty() {
1100 self.out.write_line("(no channels)").await?;
1101 return Ok(());
1102 }
1103 let mut lines: Vec<String> = Vec::new();
1104 for (_k, entry) in self.channels.iter() {
1105 lines.push(String::from(entry.name.as_str()));
1106 }
1107 for l in lines {
1108 self.out.write_line(&l).await?;
1109 }
1110 Ok(())
1111 }
1112
1113 async fn cmd_msg(&mut self, peer: &str, text: &str) -> Result<(), CliError<OUT::Error>> {
1116 let Some(key) = self.resolve_peer(peer) else {
1117 return self.write_err("unknown peer").await;
1118 };
1119 let pc = match self.node.peer(key).await {
1120 Ok(p) => p,
1121 Err(e) => return self.write_err(&node_err_str(&e)).await,
1122 };
1123 let chat = UnicastTextChatWrapper::from_peer(&pc);
1124 let opts = self.send_opts();
1125 match chat.send_text(text, &opts).await {
1126 Ok(_) => {
1127 self.stats.borrow_mut().packets_tx += 1;
1128 self.out.write_line("ok").await?;
1129 }
1130 Err(e) => {
1131 self.write_err(&alloc::format!("{:?}", e)).await?;
1132 }
1133 }
1134 Ok(())
1135 }
1136
1137 async fn cmd_text(&mut self, body: &str) -> Result<(), CliError<OUT::Error>> {
1138 let key = match self.current_peer {
1139 Some(k) => k,
1140 None => {
1141 return self
1142 .write_err("no current peer — use /query <peer-ref> first")
1143 .await;
1144 }
1145 };
1146 let pc = match self.node.peer(key).await {
1147 Ok(p) => p,
1148 Err(e) => return self.write_err(&node_err_str(&e)).await,
1149 };
1150 let chat = UnicastTextChatWrapper::from_peer(&pc);
1151 let opts = self.send_opts();
1152 match chat.send_text(body, &opts).await {
1153 Ok(_) => {
1154 self.stats.borrow_mut().packets_tx += 1;
1155 }
1156 Err(e) => {
1157 self.write_err(&alloc::format!("{:?}", e)).await?;
1158 }
1159 }
1160 Ok(())
1161 }
1162
1163 async fn cmd_me(&mut self, action: &str) -> Result<(), CliError<OUT::Error>> {
1164 let mut body: HString<EVENT_LINE_MAX> = HString::new();
1165 let _ = write!(&mut body, "* {}", action);
1166 let owned = String::from(body.as_str());
1167 self.cmd_text(&owned).await
1169 }
1170
1171 async fn cmd_ping(
1172 &mut self,
1173 peer: &str,
1174 bytes: Option<u16>,
1175 ) -> Result<(), CliError<OUT::Error>> {
1176 let Some(key) = self.resolve_peer(peer) else {
1177 return self.write_err("unknown peer").await;
1178 };
1179 let pc = match self.node.peer(key).await {
1180 Ok(p) => p,
1181 Err(e) => return self.write_err(&node_err_str(&e)).await,
1182 };
1183 let total = bytes.unwrap_or(8).min(60) as usize;
1184 let extra_bytes = total.saturating_sub(2);
1185 let opts = self
1189 .send_opts()
1190 .with_mic_size(umsh_node::PING_MIC_SIZE)
1191 .with_trace_route()
1192 .with_trace_signal();
1193 match pc.ping(extra_bytes, &opts, 30_000).await {
1194 Ok(_) => {
1195 self.stats.borrow_mut().packets_tx += 1;
1196 let alias_str = self.peer_alias_display(&key);
1197 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1198 let _ = write!(&mut line, "ping {} ({} bytes)", alias_str, total.max(2));
1199 self.out.write_line(&line).await?;
1200 }
1201 Err(e) => {
1202 self.write_err(&node_err_str(&e)).await?;
1203 }
1204 }
1205 Ok(())
1206 }
1207
1208 async fn cmd_pfs_start(
1209 &mut self,
1210 peer: &str,
1211 minutes: Option<u16>,
1212 ) -> Result<(), CliError<OUT::Error>> {
1213 #[cfg(feature = "software-crypto")]
1214 {
1215 let Some(key) = self.resolve_peer(peer) else {
1216 return self.write_err("unknown peer").await;
1217 };
1218 let minutes = minutes.unwrap_or(60);
1219 let opts = self.send_opts();
1220 match self.node.request_pfs(&key, minutes, &opts).await {
1221 Ok(_) => self.out.write_line("pfs request sent").await?,
1222 Err(e) => self.write_err(&node_err_str(&e)).await?,
1223 }
1224 }
1225 #[cfg(not(feature = "software-crypto"))]
1226 {
1227 let _ = (peer, minutes);
1228 self.write_err("pfs requires software-crypto feature")
1229 .await?;
1230 }
1231 Ok(())
1232 }
1233
1234 async fn cmd_pfs_end(&mut self, peer: &str) -> Result<(), CliError<OUT::Error>> {
1235 #[cfg(feature = "software-crypto")]
1236 {
1237 let Some(key) = self.resolve_peer(peer) else {
1238 return self.write_err("unknown peer").await;
1239 };
1240 let opts = self.send_opts();
1241 match self.node.end_pfs(&key, &opts).await {
1242 Ok(_) => self.out.write_line("pfs ended").await?,
1243 Err(e) => self.write_err(&node_err_str(&e)).await?,
1244 }
1245 }
1246 #[cfg(not(feature = "software-crypto"))]
1247 {
1248 let _ = peer;
1249 self.write_err("pfs requires software-crypto feature")
1250 .await?;
1251 }
1252 Ok(())
1253 }
1254
1255 async fn cmd_pfs_status(&mut self, peer: Option<&str>) -> Result<(), CliError<OUT::Error>> {
1256 #[cfg(feature = "software-crypto")]
1257 {
1258 let targets: Vec<PublicKey> = match peer {
1259 Some(r) => match self.resolve_peer(r) {
1260 Some(k) => alloc::vec![k],
1261 None => return self.write_err("unknown peer").await,
1262 },
1263 None => self.peers.keys().copied().collect(),
1264 };
1265 for key in targets {
1266 let result = self.node.pfs_status(&key).await;
1268 let alias = self.peer_alias_display(&key);
1269 match result {
1270 Ok(s) => {
1271 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1272 let _ = write!(&mut line, "{}: {:?}", alias, s);
1273 self.out.write_line(&line).await?;
1274 }
1275 Err(e) => {
1276 let msg = node_err_str(&e);
1277 self.write_err(&msg).await?;
1278 }
1279 }
1280 }
1281 }
1282 #[cfg(not(feature = "software-crypto"))]
1283 {
1284 let _ = peer;
1285 self.write_err("pfs requires software-crypto feature")
1286 .await?;
1287 }
1288 Ok(())
1289 }
1290
1291 async fn cmd_beacon(&mut self) -> Result<(), CliError<OUT::Error>> {
1292 use umsh_node::Transport as _;
1296 let opts = self.send_opts();
1297 match self.node.send_all(&[], &opts).await {
1298 Ok(_) => {
1299 self.stats.borrow_mut().packets_tx += 1;
1300 self.out.write_line("beacon sent").await?;
1301 }
1302 Err(e) => self.write_err(&node_err_str(&e)).await?,
1303 }
1304 Ok(())
1305 }
1306
1307 async fn cmd_channel_join(
1308 &mut self,
1309 name: &str,
1310 _key_b58: &str,
1311 ) -> Result<(), CliError<OUT::Error>> {
1312 #[cfg(feature = "software-crypto")]
1313 {
1314 let hname = match HString::<16>::try_from(name) {
1315 Ok(s) => s,
1316 Err(_) => return self.write_err("channel name too long (max 16 chars)").await,
1317 };
1318 if self.channels.contains_key(&hname) {
1319 return self.write_err("already joined").await;
1320 }
1321 let key_bytes = match umsh_core::base58::decode(_key_b58.as_bytes()) {
1323 Ok(bytes) => bytes,
1324 Err(_) => {
1325 return self
1326 .write_err("key must be a 44-character base58 channel key")
1327 .await;
1328 }
1329 };
1330 let channel_key = umsh_core::ChannelKey(key_bytes);
1331 let channel = umsh_node::Channel::private(channel_key, name);
1332 match self.node.join(&channel).await {
1333 Ok(_) => {
1334 let entry = ChannelEntry {
1335 name: HString::try_from(name).unwrap(),
1336 key_bytes,
1337 };
1338 if self.channels.insert(hname, entry).is_err() {
1339 let _ = self.node.leave(&channel).await;
1340 return self.write_err("channel table full").await;
1341 }
1342 let _ = self
1344 .channel_store
1345 .store_channel(name.as_bytes(), &key_bytes)
1346 .await;
1347 self.out.write_line("joined").await?;
1348 }
1349 Err(e) => self.write_err(&node_err_str(&e)).await?,
1350 }
1351 }
1352 #[cfg(not(feature = "software-crypto"))]
1353 {
1354 let _ = (name, _key_b58);
1355 self.write_err("channels require software-crypto feature")
1356 .await?;
1357 }
1358 Ok(())
1359 }
1360
1361 async fn cmd_channel_leave(&mut self, name: &str) -> Result<(), CliError<OUT::Error>> {
1362 #[cfg(feature = "software-crypto")]
1363 {
1364 let hname = match HString::<16>::try_from(name) {
1365 Ok(s) => s,
1366 Err(_) => return self.write_err("unknown channel").await,
1367 };
1368 let key_bytes = match self.channels.remove(&hname) {
1369 Some(e) => e.key_bytes,
1370 None => return self.write_err("not joined to that channel").await,
1371 };
1372 let channel_key = umsh_core::ChannelKey(key_bytes);
1373 let channel = umsh_node::Channel::private(channel_key, name);
1374 let _ = self.node.leave(&channel).await;
1375 let _ = self.channel_store.delete_channel(name.as_bytes()).await;
1377 self.out.write_line("left").await?;
1378 }
1379 #[cfg(not(feature = "software-crypto"))]
1380 {
1381 let _ = name;
1382 self.write_err("channels require software-crypto feature")
1383 .await?;
1384 }
1385 Ok(())
1386 }
1387
1388 async fn cmd_channel_send(
1389 &mut self,
1390 name: &str,
1391 text: &str,
1392 ) -> Result<(), CliError<OUT::Error>> {
1393 #[cfg(feature = "software-crypto")]
1394 {
1395 let hname = match HString::<16>::try_from(name) {
1396 Ok(s) => s,
1397 Err(_) => return self.write_err("unknown channel").await,
1398 };
1399 let key_bytes = match self.channels.get(&hname) {
1400 Some(e) => e.key_bytes,
1401 None => return self.write_err("not joined to that channel").await,
1402 };
1403 let channel_key = umsh_core::ChannelKey(key_bytes);
1404 let channel = umsh_node::Channel::private(channel_key, name);
1405 let bound = match self.node.bound_channel(&channel) {
1406 Some(b) => b,
1407 None => return self.write_err("channel no longer active").await,
1408 };
1409 let wrapper = umsh_text::MulticastTextChatWrapper::new(bound);
1410 let opts = self.send_opts();
1411 match wrapper.send_text(text, &opts).await {
1412 Ok(_) => {
1413 self.stats.borrow_mut().packets_tx += 1;
1414 self.out.write_line("ok").await?;
1415 }
1416 Err(e) => self.write_err(&alloc::format!("{:?}", e)).await?,
1417 }
1418 }
1419 #[cfg(not(feature = "software-crypto"))]
1420 {
1421 let _ = (name, text);
1422 self.write_err("channels require software-crypto feature")
1423 .await?;
1424 }
1425 Ok(())
1426 }
1427
1428 async fn cmd_raw(&mut self, peer: &str, hex: &str) -> Result<(), CliError<OUT::Error>> {
1429 let Some(key) = self.resolve_peer(peer) else {
1430 return self.write_err("unknown peer").await;
1431 };
1432 let mut bytes: HVec<u8, 128> = HVec::new();
1434 let hex = hex.trim_start_matches("0x");
1435 if hex.len() % 2 != 0 {
1436 return self.write_err("hex must have even length").await;
1437 }
1438 for chunk in hex.as_bytes().chunks(2) {
1439 let hi = hex_nib(chunk[0]);
1440 let lo = hex_nib(chunk[1]);
1441 match (hi, lo) {
1442 (Some(h), Some(l)) => {
1443 if bytes.push((h << 4) | l).is_err() {
1444 return self.write_err("hex too long (max 128 bytes)").await;
1445 }
1446 }
1447 _ => return self.write_err("invalid hex digit").await,
1448 }
1449 }
1450 let pc = match self.node.peer(key).await {
1451 Ok(p) => p,
1452 Err(e) => return self.write_err(&node_err_str(&e)).await,
1453 };
1454 let opts = self.send_opts();
1455 match pc.send(&bytes, &opts).await {
1456 Ok(_) => {
1457 self.stats.borrow_mut().packets_tx += 1;
1458 if self.settings.show_hex {
1459 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1460 let _ = write!(&mut line, "sent {} bytes:", bytes.len());
1461 for b in bytes.iter() {
1462 let _ = write!(&mut line, " {:02x}", b);
1463 }
1464 self.out.write_line(&line).await?;
1465 } else {
1466 self.out.write_line("ok").await?;
1467 }
1468 }
1469 Err(e) => self.write_err(&node_err_str(&e)).await?,
1470 }
1471 Ok(())
1472 }
1473
1474 async fn write_err(&mut self, msg: &str) -> Result<(), CliError<OUT::Error>> {
1477 let mut line: HString<EVENT_LINE_MAX> = HString::new();
1478 let _ = write!(&mut line, "error: {}", msg);
1479 self.out.write_line(&line).await?;
1480 Ok(())
1481 }
1482
1483 fn peer_alias_display(&self, key: &PublicKey) -> String {
1484 if let Some(entry) = self.peers.get(key) {
1485 if let Some(a) = &entry.alias {
1486 return String::from(a.as_str());
1487 }
1488 }
1489 let mut s = String::new();
1491 let _ = write!(&mut s, "{}", key.hint());
1492 s
1493 }
1494}
1495
1496fn push_event<const N: usize>(
1499 events: &SharedQueue<Deque<CliEvent, N>>,
1500 dropped: &SharedQueue<u64>,
1501 wake: &Rc<AsyncCondition>,
1502 event: CliEvent,
1503) {
1504 let mut q = events.borrow_mut();
1505 if q.push_back(event).is_err() {
1506 *dropped.borrow_mut() += 1;
1507 }
1508 drop(q);
1509 wake.trigger();
1510}
1511
1512fn register_subscriptions<M, const N: usize>(
1513 node: &LocalNode<M>,
1514 events: SharedQueue<Deque<CliEvent, N>>,
1515 dropped: SharedQueue<u64>,
1516 stats: SharedQueue<Stats>,
1517 wake: Rc<AsyncCondition>,
1518) -> Vec<Subscription>
1519where
1520 M: MacBackend,
1521{
1522 let mut subs: Vec<Subscription> = Vec::new();
1523
1524 {
1526 let ev = events.clone();
1527 let dr = dropped.clone();
1528 let st = stats.clone();
1529 let wk = wake.clone();
1530 subs.push(node.on_receive(move |pkt| {
1531 let from = match pkt.from_key() {
1532 Some(k) => k,
1533 None => return false,
1534 };
1535 let rssi = pkt.rssi().unwrap_or(0);
1536 let snr = pkt.snr().map(|s| s.as_centibels()).unwrap_or(0);
1537 let hops = pkt.flood_hops().map(|fh| fh.accumulated()).unwrap_or(0);
1538 {
1539 let mut s = st.borrow_mut();
1540 s.packets_rx += 1;
1541 s.last_rssi = pkt.rssi();
1542 s.last_snr = pkt.snr().map(|snr| snr.as_centibels());
1543 }
1544 let mut prefix: HVec<u8, 64> = HVec::new();
1545 let payload = pkt.payload_bytes();
1546 let n = payload.len().min(64);
1547 let _ = prefix.extend_from_slice(&payload[..n]);
1548 let wire = pkt.wire_bytes();
1549 let raw_n = wire.len().min(EVENT_RAW_MAX);
1550 let mut raw_bytes: HVec<u8, EVENT_RAW_MAX> = HVec::new();
1551 let _ = raw_bytes.extend_from_slice(&wire[..raw_n]);
1552 push_event(&ev, &dr, &wk, CliEvent::RawRx { bytes: raw_bytes });
1553 push_event(
1554 &ev,
1555 &dr,
1556 &wk,
1557 CliEvent::Received {
1558 from,
1559 hops,
1560 rssi,
1561 snr,
1562 prefix,
1563 },
1564 );
1565 false }));
1567 }
1568
1569 {
1571 let ev = events.clone();
1572 let dr = dropped.clone();
1573 let wk = wake.clone();
1574 subs.push(node.on_transmitted(move |wire: &[u8]| {
1575 let raw_n = wire.len().min(EVENT_RAW_MAX);
1576 let mut raw_bytes: HVec<u8, EVENT_RAW_MAX> = HVec::new();
1577 let _ = raw_bytes.extend_from_slice(&wire[..raw_n]);
1578 push_event(&ev, &dr, &wk, CliEvent::RawTx { bytes: raw_bytes });
1579 }));
1580 }
1581
1582 {
1584 let ev = events.clone();
1585 let dr = dropped.clone();
1586 let st = stats.clone();
1587 let wk = wake.clone();
1588 subs.push(node.on_ack_received(move |peer, _token| {
1589 st.borrow_mut().acks_ok += 1;
1590 push_event(&ev, &dr, &wk, CliEvent::AckReceived { peer });
1591 }));
1592 }
1593
1594 {
1596 let ev = events.clone();
1597 let dr = dropped.clone();
1598 let st = stats.clone();
1599 let wk = wake.clone();
1600 subs.push(node.on_ack_timeout(move |peer, _token| {
1601 st.borrow_mut().acks_timeout += 1;
1602 push_event(&ev, &dr, &wk, CliEvent::AckTimeout { peer });
1603 }));
1604 }
1605
1606 {
1608 let ev = events.clone();
1609 let dr = dropped.clone();
1610 let st = stats.clone();
1611 let wk = wake.clone();
1612 subs.push(node.on_node_discovered(move |peer, name| {
1613 st.borrow_mut().nodes_discovered += 1;
1614 let name_h = name.and_then(|n| HString::<32>::try_from(n).ok());
1615 push_event(
1616 &ev,
1617 &dr,
1618 &wk,
1619 CliEvent::NodeDiscovered {
1620 from: peer,
1621 name: name_h,
1622 },
1623 );
1624 }));
1625 }
1626
1627 {
1629 let ev = events.clone();
1630 let dr = dropped.clone();
1631 let st = stats.clone();
1632 let wk = wake.clone();
1633 subs.push(node.on_beacon(move |hint, key| {
1634 st.borrow_mut().beacons_rx += 1;
1635 push_event(&ev, &dr, &wk, CliEvent::Beacon { hint, from: key });
1636 }));
1637 }
1638
1639 {
1641 let ev = events.clone();
1642 let dr = dropped.clone();
1643 let wk = wake.clone();
1644 subs.push(node.on_pfs_established(move |peer| {
1645 push_event(&ev, &dr, &wk, CliEvent::PfsEstablished { peer });
1646 }));
1647 }
1648
1649 {
1651 let ev = events.clone();
1652 let dr = dropped.clone();
1653 let wk = wake.clone();
1654 subs.push(node.on_pfs_ended(move |peer| {
1655 push_event(&ev, &dr, &wk, CliEvent::PfsEnded { peer });
1656 }));
1657 }
1658
1659 {
1661 let ev = events.clone();
1662 let dr = dropped.clone();
1663 let wk = wake.clone();
1664 subs.push(node.on_pfs_failed(move |peer, reason| {
1665 push_event(&ev, &dr, &wk, CliEvent::PfsFailed { peer, reason });
1666 }));
1667 }
1668
1669 {
1674 let ev = events.clone();
1675 let dr = dropped.clone();
1676 let wk = wake.clone();
1677 subs.push(node.on_mac_command(move |peer, cmd| {
1678 let event = match cmd {
1679 OwnedMacCommand::EchoRequest { .. } | OwnedMacCommand::EchoResponse { .. } => {
1680 return;
1681 }
1682 other => {
1683 let cmd_id = mac_cmd_id(other);
1684 CliEvent::UnknownMacCmdIn { peer, cmd_id }
1685 }
1686 };
1687 push_event(&ev, &dr, &wk, event);
1688 }));
1689 }
1690
1691 {
1693 let ev = events.clone();
1694 let dr = dropped.clone();
1695 let wk = wake.clone();
1696 subs.push(node.on_pong(move |peer, rtt_ms| {
1697 push_event(&ev, &dr, &wk, CliEvent::Pong { peer, rtt_ms });
1698 }));
1699 }
1700
1701 {
1703 let ev = events.clone();
1704 let dr = dropped.clone();
1705 let wk = wake.clone();
1706 subs.push(node.on_ping_timeout(move |peer| {
1707 push_event(&ev, &dr, &wk, CliEvent::PingTimeout { peer });
1708 }));
1709 }
1710
1711 subs
1712}
1713
1714fn pfs_failure_str(reason: umsh_node::PfsFailure) -> &'static str {
1715 use umsh_node::PfsFailure;
1716 match reason {
1717 PfsFailure::Capacity => "no ephemeral identity slot",
1718 PfsFailure::SessionMissing => "no matching session",
1719 PfsFailure::Crypto => "crypto error",
1720 PfsFailure::Send => "send failed",
1721 PfsFailure::Timeout => "no response (timed out)",
1722 PfsFailure::Other => "error",
1723 }
1724}
1725
1726fn mac_cmd_id(cmd: &OwnedMacCommand) -> u8 {
1727 use umsh_node::CommandId;
1728 match cmd {
1729 OwnedMacCommand::IdentityRequest { .. } => CommandId::IdentityRequest as u8,
1730 OwnedMacCommand::SignalReportRequest => CommandId::SignalReportRequest as u8,
1731 OwnedMacCommand::SignalReportResponse { .. } => CommandId::SignalReportResponse as u8,
1732 OwnedMacCommand::EchoRequest { .. } => CommandId::EchoRequest as u8,
1733 OwnedMacCommand::EchoResponse { .. } => CommandId::EchoResponse as u8,
1734 OwnedMacCommand::PfsSessionRequest { .. } => CommandId::PfsSessionRequest as u8,
1735 OwnedMacCommand::PfsSessionResponse { .. } => CommandId::PfsSessionResponse as u8,
1736 OwnedMacCommand::EndPfsSession => CommandId::EndPfsSession as u8,
1737 OwnedMacCommand::Noop => CommandId::Noop as u8,
1738 OwnedMacCommand::PeerRepeatersRequest { .. } => CommandId::PeerRepeatersRequest as u8,
1739 OwnedMacCommand::PeerRepeatersResponse { .. } => CommandId::PeerRepeatersResponse as u8,
1740 }
1741}
1742
1743fn node_err_str<M>(e: &NodeError<M>) -> String
1744where
1745 M: MacBackend,
1746 M::SendError: core::fmt::Debug,
1747 M::CapacityError: core::fmt::Debug,
1748{
1749 alloc::format!("{:?}", e)
1750}
1751
1752fn parse_bool(s: &str) -> Option<bool> {
1753 match s {
1754 "true" | "1" | "yes" | "on" => Some(true),
1755 "false" | "0" | "no" | "off" => Some(false),
1756 _ => None,
1757 }
1758}
1759
1760fn format_parse_error(e: &ParseError) -> String {
1761 match e {
1762 ParseError::Empty => String::from("empty"),
1763 ParseError::UnknownCommand(name) => {
1764 let mut s = String::from("unknown command: ");
1765 s.push_str(name);
1766 s
1767 }
1768 ParseError::MissingArg(n) => {
1769 let mut s = String::from("missing argument: ");
1770 s.push_str(n);
1771 s
1772 }
1773 ParseError::BadNumber => String::from("bad number"),
1774 }
1775}
1776
1777fn hex_nib(b: u8) -> Option<u8> {
1778 match b {
1779 b'0'..=b'9' => Some(b - b'0'),
1780 b'a'..=b'f' => Some(b - b'a' + 10),
1781 b'A'..=b'F' => Some(b - b'A' + 10),
1782 _ => None,
1783 }
1784}