1use std::time::Duration;
14
15use anyhow::{Result, anyhow, bail};
16use tokio::time::Instant;
17
18use umsh::core::{PayloadType, PublicKey};
19use umsh::crypto::software::SoftwareIdentity;
20use umsh::hal::Radio;
21use umsh::mac::{MacCounters, SendOptions};
22use umsh::node::{ReceivedPacketRef, SendProgressTicket};
23use umsh::text::{TextMessage, UnicastTextChatWrapper};
24use umsh_sync::AsyncRefCell;
25
26use super::values::KeyArg;
27use crate::App;
28use crate::mesh::{self, CtlMac, NodeStack};
29use crate::output::{field, note, subfield};
30use crate::routes::RouteCache;
31
32const DISCOVERY_HOPS: u8 = 5;
37
38#[derive(Debug, clap::Args)]
39pub struct SendArgs {
40 #[arg(value_name = "KEY")]
42 pub target: KeyArg,
43
44 #[arg(value_name = "TEXT", required = true)]
50 pub text: Vec<String>,
51
52 #[arg(long, short = 'W', default_value_t = 30, value_name = "SECS")]
54 pub timeout: u64,
55
56 #[arg(long)]
62 pub no_ack: bool,
63}
64
65#[derive(Debug, clap::Args)]
66pub struct ListenArgs {
67 #[arg(long, short = 'W', value_name = "SECS")]
69 pub timeout: Option<u64>,
70
71 #[arg(long = "from", value_name = "KEY")]
79 pub from: Vec<KeyArg>,
80}
81
82enum Errand {
84 Send(SendArgs),
85 Listen(ListenArgs),
86}
87
88impl mesh::RadioErrand for Errand {
89 async fn run<R: Radio>(
90 self,
91 mac: &AsyncRefCell<CtlMac<R>>,
92 identity: SoftwareIdentity,
93 ) -> Result<()>
94 where
95 R::Error: core::fmt::Debug,
96 {
97 match &self {
98 Errand::Send(args) => deliver(mac, identity, PublicKey(args.target.0), args).await,
99 Errand::Listen(args) => receive(mac, identity, args).await,
100 }
101 }
102}
103
104pub async fn send(app: &mut App, args: SendArgs) -> Result<()> {
106 mesh::borrowing_the_radio(app, Errand::Send(args)).await
107}
108
109pub async fn listen(app: &mut App, args: ListenArgs) -> Result<()> {
111 mesh::borrowing_the_radio(app, Errand::Listen(args)).await
112}
113
114async fn deliver<R: Radio>(
115 mac: &AsyncRefCell<CtlMac<R>>,
116 identity: SoftwareIdentity,
117 target: PublicKey,
118 args: &SendArgs,
119) -> Result<()>
120where
121 R::Error: core::fmt::Debug,
122{
123 let text = args.text.join(" ");
124 let (mut stack, local_key) = NodeStack::build(mac, identity).await?;
125 let peer = stack
126 .node
127 .peer(target)
128 .await
129 .map_err(|error| anyhow!("registering the node as a peer: {error:?}"))?;
130
131 let mut routes = RouteCache::load();
133 let known = routes.get(&target).is_some();
134 if let Some(record) = routes.get(&target) {
135 peer.restore_route(record.route.clone()).await;
136 }
137
138 field("from", local_key.to_string());
139 field("to", target.to_string());
140 if !known {
141 note("no route known; this message floods to find one");
142 }
143
144 let options = SendOptions::default()
145 .with_ack_requested(!args.no_ack)
146 .with_flood_hops(DISCOVERY_HOPS);
147 let before = stack.handle.counters().await;
150 let chat = UnicastTextChatWrapper::new(peer);
151 let ticket = chat
152 .send_text(&text, &options)
153 .await
154 .map_err(|error| anyhow!("sending: {error:?}"))?;
155
156 let outcome = settle(&mut stack, &ticket, args, before).await;
157 routes.harvest(&stack.handle).await;
158 if let Err(error) = routes.store() {
159 crate::output::warn(format!("could not save learned routes: {error:#}"));
160 }
161 let _ = stack.handle.service_counter_persistence().await;
162 outcome
163}
164
165async fn settle<R: Radio>(
174 stack: &mut NodeStack<'_, R>,
175 ticket: &SendProgressTicket,
176 args: &SendArgs,
177 before: MacCounters,
178) -> Result<()>
179where
180 R::Error: core::fmt::Debug,
181{
182 let give_up = Instant::now() + Duration::from_secs(args.timeout);
183 if args.no_ack {
184 loop {
185 let now = stack.handle.counters().await;
186 if now.tx_abandoned > before.tx_abandoned {
187 bail!("not sent: the channel stayed busy and the frame was abandoned");
188 }
189 if now.tx_frames > before.tx_frames {
190 field("sent", "no acknowledgment was requested");
193 return Ok(());
194 }
195 if Instant::now() >= give_up {
196 bail!(
197 "the frame was still waiting to go out after {} s",
198 args.timeout
199 );
200 }
201 stack.pump_until(give_up.min(Instant::now() + POLL)).await?;
202 }
203 }
204 while !ticket.is_finished() {
205 if Instant::now() >= give_up {
206 bail!(
207 "no acknowledgment after {} s; the message may still have arrived",
208 args.timeout
209 );
210 }
211 stack.pump_until(give_up.min(Instant::now() + POLL)).await?;
212 }
213 if ticket.was_acked() {
214 field("delivered", "acknowledged by the far end");
215 return Ok(());
216 }
217 if ticket.has_failed() {
218 bail!("not delivered: every retransmission went unacknowledged");
219 }
220 bail!("the send finished without an acknowledgment or a failure")
221}
222
223const POLL: Duration = Duration::from_millis(250);
225
226async fn receive<R: Radio>(
227 mac: &AsyncRefCell<CtlMac<R>>,
228 identity: SoftwareIdentity,
229 args: &ListenArgs,
230) -> Result<()>
231where
232 R::Error: core::fmt::Debug,
233{
234 let (mut stack, local_key) = NodeStack::build(mac, identity).await?;
235 field("listening as", local_key.to_string());
236
237 let mut routes = RouteCache::load();
242 let mut expected: Vec<PublicKey> = args.from.iter().map(|key| PublicKey(key.0)).collect();
243 for (key, _) in routes.iter() {
244 if !expected.contains(&key) {
245 expected.push(key);
246 }
247 }
248 for key in &expected {
249 let peer = stack
250 .node
251 .peer(*key)
252 .await
253 .map_err(|error| anyhow!("registering {key} as a peer: {error:?}"))?;
254 if let Some(record) = routes.get(key) {
255 peer.restore_route(record.route.clone()).await;
256 }
257 }
258
259 match expected.len() {
260 0 => note("no nodes known to listen for; name one with --from"),
261 1 => field("expecting", expected[0].to_string()),
262 count => field("expecting", format!("{count} known nodes")),
265 }
266 note(match args.timeout {
267 Some(seconds) => format!("for {seconds} s, or until interrupted"),
268 None => "until interrupted".to_string(),
269 });
270
271 let _subscription = stack.node.on_receive(move |packet| show(packet));
274
275 let deadline = args
276 .timeout
277 .map(|seconds| Instant::now() + Duration::from_secs(seconds));
278 loop {
279 if let Some(deadline) = deadline
280 && Instant::now() >= deadline
281 {
282 break;
283 }
284 let next = deadline.unwrap_or_else(|| Instant::now() + POLL);
285 tokio::select! {
286 result = stack.pump_until(next.min(Instant::now() + POLL)) => result?,
287 _ = tokio::signal::ctrl_c() => {
290 println!();
291 break;
292 }
293 }
294 }
295
296 routes.harvest(&stack.handle).await;
298 if let Err(error) = routes.store() {
299 crate::output::warn(format!("could not save learned routes: {error:#}"));
300 }
301 let _ = stack.handle.service_counter_persistence().await;
302 Ok(())
303}
304
305fn show(packet: &ReceivedPacketRef<'_>) -> bool {
310 if packet.payload_type() != PayloadType::TextMessage {
313 return false;
314 }
315 let Ok(message) = umsh::text::parse_text_message(packet.payload()) else {
316 return false;
317 };
318 let Ok(body) = message.body_str() else {
319 return false;
320 };
321 field(
322 "from",
323 match packet.from_key() {
324 Some(key) => key.to_string(),
325 None => "unknown sender".to_string(),
328 },
329 );
330 if !packet.source_authenticated() {
331 subfield("warning", "the sender is not authenticated");
332 }
333 if let Some(rssi) = packet.rssi() {
334 subfield("signal", format!("{rssi} dBm"));
335 }
336 print_body(&message, body);
337 true
338}
339
340fn print_body(message: &TextMessage<'_>, body: &str) {
341 if let Some(handle) = message.sender_handle {
342 subfield("handle", handle);
343 }
344 subfield("message", body);
345}