umshctl/command/
message.rs

1//! `send` and `listen`: text messages to and from this tool's own
2//! identity.
3//!
4//! The tool already becomes a node on the mesh to administer a device;
5//! these do the same thing for the oldest reason there is to have a
6//! mesh. Both borrow the attached radio, so they need a local
7//! attachment — a mesh session has already lent that radio out.
8//!
9//! The identity is the administrator identity `admin-key` prints, so a
10//! message from this tool arrives from the same key a device authorizes
11//! for management.
12
13use 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
32/// How far a message floods when no route to the peer is known.
33///
34/// The MAC clamps this to a cached route's own distance when it has one,
35/// so this is the budget for a first message into an unmapped mesh.
36const DISCOVERY_HOPS: u8 = 5;
37
38#[derive(Debug, clap::Args)]
39pub struct SendArgs {
40    /// The node to send to, as its public key.
41    #[arg(value_name = "KEY")]
42    pub target: KeyArg,
43
44    /// The message. Several words are joined with spaces.
45    ///
46    /// A message that begins with a dash goes after `--`, as anywhere
47    /// else; the flags stay flags so `--timeout` still means what it
48    /// says.
49    #[arg(value_name = "TEXT", required = true)]
50    pub text: Vec<String>,
51
52    /// Give up waiting for the acknowledgment after this long.
53    #[arg(long, short = 'W', default_value_t = 30, value_name = "SECS")]
54    pub timeout: u64,
55
56    /// Send without asking for an acknowledgment.
57    ///
58    /// Nothing comes back, so nothing says the message arrived — which
59    /// is what you want for a message to somebody who is not listening
60    /// yet, and never what you want otherwise.
61    #[arg(long)]
62    pub no_ack: bool,
63}
64
65#[derive(Debug, clap::Args)]
66pub struct ListenArgs {
67    /// Stop after this long. Without it, listens until interrupted.
68    #[arg(long, short = 'W', value_name = "SECS")]
69    pub timeout: Option<u64>,
70
71    /// Also listen for this node, by public key. May be repeated.
72    ///
73    /// A unicast names its sender by a short hint, not by a whole key,
74    /// so a message can only be read by somebody who already knows who
75    /// might be sending. Every node this tool has a remembered route to
76    /// is listened for without asking; this adds the ones it has not
77    /// reached yet.
78    #[arg(long = "from", value_name = "KEY")]
79    pub from: Vec<KeyArg>,
80}
81
82/// What this command is being lent the radio for.
83enum 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
104/// `send`: one message, and what became of it.
105pub async fn send(app: &mut App, args: SendArgs) -> Result<()> {
106    mesh::borrowing_the_radio(app, Errand::Send(args)).await
107}
108
109/// `listen`: whatever arrives, until you stop it.
110pub 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    // Whatever an earlier invocation learned about reaching this node.
132    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    // What the radio had already sent, so the wait below can tell this
148    // frame's transmission from any that came before it.
149    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
165/// Pump the radio until the message is done, or until patience runs out.
166///
167/// "Done" means two different things. An acknowledged send is done when
168/// the MAC says so — an ack arrived, or every retransmission timed out.
169/// A send that asked for no acknowledgment has no receipt to track, so
170/// its ticket is finished the moment it is issued, before the frame has
171/// been anywhere near the radio; what finishes that one is the frame
172/// actually going out, which the MAC's own transmit counters report.
173async 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                // Nothing was asked for and nothing comes back. That the
191                // frame aired is the whole truth available.
192                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
223/// How long a single pump waits before the deadline is checked again.
224const 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    // A unicast carries a short hint for its sender rather than a whole
238    // key, so the receiver has to know in advance who might be calling.
239    // The remembered routes are this tool's address book of everyone it
240    // has reached; `--from` covers anyone it has not.
241    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        // `routes` prints the list itself; a count here keeps the
263        // banner short without hiding that the set is not empty.
264        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    // The subscription borrows nothing from the stack, so it can hold
272    // its own counter and outlive each turn of the pump below.
273    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            // Ctrl-C ends the command rather than the process, so the
288            // shell gets its prompt back and the radio comes home.
289            _ = tokio::signal::ctrl_c() => {
290                println!();
291                break;
292            }
293        }
294    }
295
296    // Every sender taught a route on the way in; keep them.
297    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
305/// Print one received packet, if it is a text message.
306///
307/// Returning false leaves the packet for any other handler: this one
308/// claims only what it printed.
309fn show(packet: &ReceivedPacketRef<'_>) -> bool {
310    // The payload type rides in the MAC header, so the payload itself is
311    // the text message from its first byte.
312    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            // A message whose sender is a hint rather than a key came in
326            // unauthenticated; saying so is the point.
327            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}