1use std::cell::RefCell;
9use std::rc::Rc;
10use std::time::Duration;
11
12use anyhow::{Result, anyhow, bail};
13use rand::{Rng as _, rng};
14use tokio::time::Instant;
15
16use umsh::core::PublicKey;
17use umsh::crypto::software::SoftwareIdentity;
18use umsh::hal::Radio;
19use umsh::node_mgmt::NodeManager;
20use umsh::node_mgmt::admin::Outcome;
21use umsh::ulcp_wire::ids::prop;
22use umsh::ulcp_wire::{Status, capability_name, frame, reply};
23use umsh_sync::AsyncRefCell;
24
25use super::lifecycle::PairState;
26use super::props::PropArg;
27use super::tables::TableOp;
28use super::values::{AssignArg, BytesArg, KeyArg};
29use crate::App;
30use crate::connection::confirm;
31use crate::mesh::{self, CtlHandle, CtlMac, NodeStack, OPERATION_TIMEOUT, describe};
32use crate::output::{address, field, hex, note, subfield};
33
34#[derive(Debug, clap::Subcommand)]
37pub enum ManageOp {
38 Info,
40
41 Get {
43 #[arg(value_name = "PROP")]
44 key: PropArg,
45 },
46
47 Set {
49 #[arg(value_name = "PROP")]
50 key: PropArg,
51 #[arg(value_name = "VALUE")]
52 value: String,
53 },
54
55 Insert {
57 #[arg(value_name = "PROP")]
58 key: PropArg,
59 #[arg(value_name = "HEX")]
60 item: BytesArg,
61 },
62
63 Remove {
65 #[arg(value_name = "PROP")]
66 key: PropArg,
67 #[arg(value_name = "HEX")]
68 selector: BytesArg,
69 },
70
71 GetMany {
73 #[arg(value_name = "PROP", required = true)]
74 keys: Vec<PropArg>,
75 },
76
77 SetMany {
80 #[arg(value_name = "PROP=HEX", required = true)]
81 entries: Vec<AssignArg>,
82 },
83
84 Save,
86
87 Reset,
90
91 Reboot,
95
96 Ble {
101 #[command(subcommand)]
102 op: BleOp,
103 },
104
105 Admins {
107 #[command(subcommand)]
108 op: Option<TableOp>,
109 },
110}
111
112#[derive(Debug, clap::Subcommand)]
114pub enum BleOp {
115 Pair {
117 #[arg(value_enum, default_value_t = PairState::On)]
119 state: PairState,
120 },
121 Clear {
124 #[arg(long)]
126 yes: bool,
127 },
128}
129
130#[derive(Debug)]
140pub enum Operation {
141 Manage(ManageOp),
142 PeerRepeaters,
144 Ping(super::ping::PingArgs),
146}
147
148struct Errand {
150 target: PublicKey,
151 op: Operation,
152 no_save: bool,
153}
154
155impl mesh::RadioErrand for Errand {
156 async fn run<R: Radio>(
157 self,
158 mac: &AsyncRefCell<CtlMac<R>>,
159 identity: SoftwareIdentity,
160 ) -> Result<()>
161 where
162 R::Error: core::fmt::Debug,
163 {
164 operate(mac, identity, self.target, self.op, self.no_save).await
165 }
166}
167
168pub async fn run(app: &mut App, target: KeyArg, op: Operation) -> Result<()> {
171 if let Operation::Manage(ManageOp::Ble {
175 op: BleOp::Clear { yes: false },
176 }) = &op
177 {
178 const WARNING: &str = "manage ble clear forgets every paired host, the pairing PIN, \
179 and the pairing lockout on the target";
180 if !app.interactive {
181 bail!("{WARNING}; re-run with --yes to confirm");
182 }
183 println!("{WARNING}.");
184 if !confirm("forget every paired host?")? {
185 println!("cancelled");
186 return Ok(());
187 }
188 }
189 let errand = Errand {
190 target: PublicKey(target.0),
191 op,
192 no_save: app.no_save,
193 };
194 mesh::borrowing_the_radio(app, errand).await
195}
196
197async fn operate<R: Radio>(
198 mac: &AsyncRefCell<CtlMac<R>>,
199 identity: SoftwareIdentity,
200 target: PublicKey,
201 op: Operation,
202 no_save: bool,
203) -> Result<()>
204where
205 R::Error: core::fmt::Debug,
206{
207 let (stack, local_key) = NodeStack::build(mac, identity).await?;
208 let peer = stack
209 .node
210 .peer(target)
211 .await
212 .map_err(|error| anyhow!("registering the device as a peer: {error:?}"))?;
213 let mut routes = crate::routes::RouteCache::load();
218 if let Some(record) = routes.get(&target) {
219 peer.restore_route(record.route.clone()).await;
220 }
221
222 let mut seed = [0u8; 2];
227 rng().fill_bytes(&mut seed);
228 let manager = NodeManager::new(peer, u16::from_be_bytes(seed));
229
230 field("administrator", local_key.to_string());
231 field(
232 match op {
233 Operation::Manage(_) => "device",
234 Operation::PeerRepeaters => "repeater",
235 Operation::Ping(_) => "target",
236 },
237 target.to_string(),
238 );
239
240 let mut ctl = Ctl {
241 stack,
242 target,
243 manager,
244 };
245 let result = run_op(&mut ctl, op, no_save).await;
246 let _ = ctl.stack.handle.service_counter_persistence().await;
247 routes.harvest(&ctl.stack.handle).await;
248 if let Err(error) = routes.store() {
249 crate::output::warn(format!("could not save learned routes: {error:#}"));
250 }
251 result
252}
253
254pub struct Ctl<'a, R: Radio> {
256 pub(super) stack: NodeStack<'a, R>,
257 pub(super) target: PublicKey,
258 manager: NodeManager<CtlHandle<'a, R>>,
259}
260
261impl<'a, R: Radio> Ctl<'a, R>
262where
263 R::Error: core::fmt::Debug,
264{
265 async fn exchange(&mut self, request: &[u8]) -> Result<Outcome> {
268 let give_up = self.stack.started() + OPERATION_TIMEOUT;
269 self.stack
270 .exchange(&mut self.manager, request, give_up)
271 .await
272 }
273
274 async fn reply(&mut self, request: &[u8]) -> Result<Vec<u8>> {
276 match self.exchange(request).await? {
277 Outcome::Replied { .. } => Ok(self.manager.reply().to_vec()),
278 Outcome::NoResponse => bail!("the device answered nothing where a reply was due"),
279 Outcome::Failed(failure) => Err(describe(failure)),
280 }
281 }
282}
283
284async fn run_op<R: Radio>(ctl: &mut Ctl<'_, R>, op: Operation, no_save: bool) -> Result<()>
287where
288 R::Error: core::fmt::Debug,
289{
290 let op = match op {
291 Operation::Manage(op) => op,
292 Operation::PeerRepeaters => return peer_repeaters(ctl).await,
293 Operation::Ping(args) => return super::ping::run(ctl, args).await,
296 };
297 match op {
298 ManageOp::Info => info(ctl).await,
299 ManageOp::Get { key } => {
300 let reply = ctl
301 .reply(&encode(|buf| frame::prop_get(buf, 0, key.0))?)
302 .await?;
303 report_value(key.0, &reply)
304 }
305 ManageOp::Set { key, value } => {
306 let encoded = super::props::encode_value(key.0, &value)?;
307 let reply = ctl
308 .reply(&encode(|buf| frame::prop_set(buf, 0, key.0, &encoded))?)
309 .await?;
310 report_value(key.0, &reply)?;
311 save_if_asked(ctl, no_save).await
312 }
313 ManageOp::Insert { key, item } => {
314 let reply = ctl
315 .reply(&encode(|buf| frame::prop_insert(buf, 0, key.0, &item.0))?)
316 .await?;
317 report_value(key.0, &reply)?;
318 save_if_asked(ctl, no_save).await
319 }
320 ManageOp::Remove { key, selector } => {
321 let reply = ctl
322 .reply(&encode(|buf| {
323 frame::prop_remove(buf, 0, key.0, &selector.0)
324 })?)
325 .await?;
326 report_value(key.0, &reply)?;
327 save_if_asked(ctl, no_save).await
328 }
329 ManageOp::GetMany { keys } => {
330 let numbers: Vec<u32> = keys.iter().map(|key| key.0).collect();
331 let reply = ctl
332 .reply(&encode(|buf| frame::prop_multi_get(buf, 0, &numbers))?)
333 .await?;
334 report_entries(&numbers, &reply, "reissue the rest")
335 }
336 ManageOp::SetMany { entries } => {
337 let keys: Vec<u32> = entries.iter().map(|entry| entry.0).collect();
338 let borrowed: Vec<(u32, &[u8])> = entries
339 .iter()
340 .map(|entry| (entry.0, entry.1.as_slice()))
341 .collect();
342 let reply = ctl
343 .reply(&encode(|buf| frame::prop_multi_set(buf, 0, &borrowed))?)
344 .await?;
345 report_entries(
346 &keys,
347 &reply,
348 "the sequence stopped there; reissue the remainder",
349 )?;
350 save_if_asked(ctl, no_save).await
351 }
352 ManageOp::Save => {
353 let reply = ctl.reply(&encode(|buf| frame::save(buf, 0))?).await?;
354 report_value(prop::LAST_STATUS, &reply)
355 }
356 ManageOp::Reset => {
357 match ctl.exchange(&encode(|buf| frame::reset(buf, 0))?).await? {
358 Outcome::NoResponse => {
359 println!("reset delivered; the device answers a reset with nothing");
360 Ok(())
361 }
362 Outcome::Replied { .. } => report_value(prop::LAST_STATUS, ctl.manager.reply()),
366 Outcome::Failed(failure) => Err(describe(failure)),
367 }
368 }
369 ManageOp::Reboot => {
370 match ctl.exchange(&encode(|buf| frame::reboot(buf, 0))?).await? {
371 Outcome::NoResponse => {
372 println!("reboot delivered; the device is restarting");
373 Ok(())
374 }
375 Outcome::Replied { .. } => report_value(prop::LAST_STATUS, ctl.manager.reply()),
378 Outcome::Failed(failure) => Err(describe(failure)),
379 }
380 }
381 ManageOp::Ble { op } => match op {
382 BleOp::Pair { state } => {
385 let open = state == PairState::On;
386 let reply = ctl
387 .reply(&encode(|buf| {
388 frame::prop_set(buf, 0, prop::BLE_PAIRING, &[open as u8])
389 })?)
390 .await?;
391 report_value(prop::BLE_PAIRING, &reply)
392 }
393 BleOp::Clear { .. } => {
394 let frame = encode(|buf| frame::ble_clear_bonds(buf, 0))?;
396 match ctl.exchange(&frame).await? {
397 Outcome::Replied { .. } => report_value(prop::LAST_STATUS, ctl.manager.reply()),
398 Outcome::NoResponse => {
399 bail!("the device answered nothing; this command reports a status")
400 }
401 Outcome::Failed(failure) => Err(describe(failure)),
402 }
403 }
404 },
405 ManageOp::Admins { op } => admins(ctl, op.unwrap_or(TableOp::List), no_save).await,
406 }
407}
408
409async fn peer_repeaters<R: Radio>(ctl: &mut Ctl<'_, R>) -> Result<()>
415where
416 R::Error: core::fmt::Debug,
417{
418 use umsh::node::OwnedMacCommand;
419 use umsh::node::mac_command::PeerRepeatersResponseView;
420
421 let pages: Rc<RefCell<Vec<Vec<u8>>>> = Rc::new(RefCell::new(Vec::new()));
424 let sink = pages.clone();
425 let target = ctl.target;
426 let _subscription = ctl.stack.node.on_mac_command(move |from, command| {
427 if from != target {
428 return;
429 }
430 if let OwnedMacCommand::PeerRepeatersResponse { body } = command {
431 sink.borrow_mut().push(body.clone());
432 }
433 });
434
435 let peer = ctl
436 .stack
437 .node
438 .peer(target)
439 .await
440 .map_err(|error| anyhow!("registering the repeater as a peer: {error:?}"))?;
441
442 let mut seed = [0u8; 2];
443 rng().fill_bytes(&mut seed);
444 let mut nonce = u16::from_be_bytes(seed);
445 let mut cursor: Option<Vec<u8>> = None;
446 let mut listed = 0usize;
447 let mut total: Option<u8> = None;
448
449 loop {
450 pages.borrow_mut().clear();
451 peer.request_peer_repeaters(nonce, cursor.as_deref(), &Default::default())
452 .await
453 .map_err(|error| anyhow!("asking for the listing: {error:?}"))?;
454
455 let deadline = Instant::now() + PEER_REPEATERS_PAGE_TIMEOUT;
456 let body = loop {
457 if let Some(body) = pages
458 .borrow()
459 .iter()
460 .find(|body| PeerRepeatersResponseView::new(body).nonce() == Some(nonce))
461 .cloned()
462 {
463 break Some(body);
464 }
465 if Instant::now() >= deadline {
466 break None;
467 }
468 ctl.stack.pump_until(deadline).await?;
469 };
470
471 let Some(body) = body else {
472 if listed == 0 {
473 bail!(
474 "no answer — the repeater may be out of range, or may not answer peer-repeater \
475 requests"
476 );
477 }
478 note("the listing stopped part way; run the command again to start over");
479 break;
480 };
481
482 let view = PeerRepeatersResponseView::new(&body);
483 total = total.or_else(|| view.total());
484 for entry in view.entries() {
485 print_peer_repeater(&entry);
486 listed += 1;
487 }
488 match view.cursor() {
489 Some(next) => cursor = Some(next.to_vec()),
490 None => break,
491 }
492 nonce = nonce.wrapping_add(1);
495 }
496
497 match total {
498 Some(total) if usize::from(total) != listed => {
499 note(format!("listed {listed} of {total} peer repeaters"))
500 }
501 _ if listed == 0 => note("the repeater knows of no peers"),
502 _ => {}
503 }
504 Ok(())
505}
506
507const PEER_REPEATERS_PAGE_TIMEOUT: Duration = Duration::from_secs(30);
509
510fn print_peer_repeater(entry: &umsh::node::mac_command::PeerRepeaterEntryView<'_>) {
511 let Some(hint) = entry.hint() else {
512 return;
515 };
516 field(
517 "peer",
518 match entry.name() {
519 Some(name) => format!("{} ({name})", hex(hint)),
520 None => hex(hint),
521 },
522 );
523 if let Some((rssi, snr)) = entry.rssi_snr() {
524 subfield("signal", format!("{rssi} dBm, {snr}"));
525 }
526 if let Some(minutes) = entry.last_heard_min() {
527 subfield("last heard", format!("{minutes} min ago"));
528 }
529 if let Some(location) = entry.location() {
530 let (lat, lon) = location.center();
531 subfield("location", format!("{lat:.4}, {lon:.4}"));
532 }
533 let regions: Vec<String> = entry
534 .regions()
535 .map(|code| umsh::core::RegionCode::from_bytes(code).to_string())
536 .collect();
537 if !regions.is_empty() {
538 subfield("regions", regions.join(", "));
539 }
540}
541
542async fn info<R: Radio>(ctl: &mut Ctl<'_, R>) -> Result<()>
543where
544 R::Error: core::fmt::Debug,
545{
546 let keys = [
547 prop::PROTOCOL_VERSION,
548 prop::DEV_VERSION,
549 prop::DEV_MODEL,
550 prop::DEV_NAME,
551 prop::DEV_KEY,
552 prop::CAPS,
553 ];
554 let reply = ctl
555 .reply(&encode(|buf| frame::prop_multi_get(buf, 0, &keys))?)
556 .await?;
557 for (key, answer) in zip_answers(&keys, &reply)? {
558 match answer {
559 Err(status) => field(&label(key), format!("{status:?}")),
560 Ok(value) => match key {
561 prop::CAPS => {
562 field("capabilities", format!("{} advertised", value.len()));
563 for &number in &value {
564 match capability_name(u32::from(number)) {
565 Some(name) => subfield(&format!("{number}"), name),
566 None => subfield(&format!("{number}"), "unnamed"),
567 }
568 }
569 }
570 prop::DEV_VERSION | prop::DEV_MODEL | prop::DEV_NAME => field(
571 &label(key),
572 String::from_utf8_lossy(&value)
573 .trim_end_matches('\0')
574 .to_owned(),
575 ),
576 prop::DEV_KEY => field(&label(key), address(&value)),
577 _ => field(&label(key), hex(&value)),
578 },
579 }
580 }
581 Ok(())
582}
583
584async fn admins<R: Radio>(ctl: &mut Ctl<'_, R>, op: TableOp, no_save: bool) -> Result<()>
585where
586 R::Error: core::fmt::Debug,
587{
588 match op {
589 TableOp::List => {
590 let reply = ctl
591 .reply(&encode(|buf| frame::prop_get(buf, 0, prop::DEV_ADMINS))?)
592 .await?;
593 let value = value_of(prop::DEV_ADMINS, &reply)?;
594 if value.is_empty() {
595 println!("no administrators listed");
596 } else if !value.len().is_multiple_of(32) {
597 bail!("malformed administrator listing");
598 } else {
599 for key in value.chunks(32) {
600 println!("{}", address(key));
601 }
602 }
603 Ok(())
604 }
605 TableOp::Add { key } => {
606 let reply = ctl
607 .reply(&encode(|buf| {
608 frame::prop_insert(buf, 0, prop::DEV_ADMINS, &key.0)
609 })?)
610 .await?;
611 let digest = value_of(prop::DEV_ADMINS, &reply)?;
612 println!("administrator added (digest {})", address(&digest));
613 save_if_asked(ctl, no_save).await
614 }
615 TableOp::Remove { key } => {
616 if key.0 == ctl.manager.device().0 {
620 note("that key is the device's own, not an administrator's");
621 }
622 let reply = ctl
623 .reply(&encode(|buf| {
624 frame::prop_remove(buf, 0, prop::DEV_ADMINS, &key.0)
625 })?)
626 .await?;
627 let digest = value_of(prop::DEV_ADMINS, &reply)?;
628 println!("administrator removed (digest {})", address(&digest));
629 save_if_asked(ctl, no_save).await
630 }
631 }
632}
633
634async fn save_if_asked<R: Radio>(ctl: &mut Ctl<'_, R>, no_save: bool) -> Result<()>
636where
637 R::Error: core::fmt::Debug,
638{
639 if no_save {
640 note("--no-save — changes are live only; `manage <KEY> save` persists them");
641 return Ok(());
642 }
643 let reply = ctl.reply(&encode(|buf| frame::save(buf, 0))?).await?;
644 match reply::status_of(&reply) {
645 Some(Status::OK) | None => {
646 println!("saved: changes persist across reboots");
647 Ok(())
648 }
649 Some(status) => bail!("the device refused to save ({status:?})"),
650 }
651}
652
653fn encode(
658 build: impl FnOnce(&mut [u8]) -> Result<usize, umsh::ulcp_wire::frame::WriteError>,
659) -> Result<Vec<u8>> {
660 let mut buf = vec![0u8; umsh::node_mgmt::REQUEST_MAX];
661 let len = build(&mut buf).map_err(|error| {
662 anyhow!("the request does not fit one Node Management payload: {error:?}")
663 })?;
664 buf.truncate(len);
665 Ok(buf)
666}
667
668fn label(key: u32) -> String {
669 super::props::spell(key)
670}
671
672fn value_of(requested: u32, reply: &[u8]) -> Result<Vec<u8>> {
675 match reply::property(requested, reply)
676 .map_err(|error| anyhow!("unreadable reply: {error:?}"))?
677 {
678 reply::Answer::Value(value) => Ok(value.to_vec()),
679 reply::Answer::Refused(status) => bail!("the device refused: {status:?}"),
680 }
681}
682
683fn report_value(requested: u32, reply: &[u8]) -> Result<()> {
684 let value = value_of(requested, reply)?;
685 field(
686 &label(requested),
687 super::props::format_value(requested, &value),
688 );
689 Ok(())
690}
691
692type Answer = (u32, Result<Vec<u8>, Status>);
695
696fn zip_answers(keys: &[u32], reply: &[u8]) -> Result<Vec<Answer>> {
699 let entries = reply::entries(keys, reply).map_err(|error| match error {
700 reply::EntriesError::NotEntries => match reply::status_of(reply) {
703 Some(status) => anyhow!("the device refused the multi-property request: {status:?}"),
704 None => anyhow!("the device answered a multi-property request with something else"),
705 },
706 reply::EntriesError::Unreadable(error) => anyhow!("unreadable reply: {error:?}"),
707 })?;
708 entries
709 .map(|entry| {
710 let (key, answer) = entry.map_err(|error| anyhow!("malformed entry: {error:?}"))?;
711 Ok(match answer {
712 reply::Answer::Value(value) => (key, Ok(value.to_vec())),
713 reply::Answer::Refused(status) => (key, Err(status)),
714 })
715 })
716 .collect()
717}
718
719fn report_entries(keys: &[u32], reply: &[u8], short_advice: &str) -> Result<()> {
720 let answers = zip_answers(keys, reply)?;
721 for (key, answer) in &answers {
722 match answer {
723 Ok(value) => field(&label(*key), super::props::format_value(*key, value)),
724 Err(status) => field(&label(*key), format!("refused: {status:?}")),
725 }
726 }
727 if answers.len() < keys.len() {
728 field(
729 "short",
730 format!(
731 "{} of {} answered; {short_advice}",
732 answers.len(),
733 keys.len()
734 ),
735 );
736 }
737 Ok(())
738}