umshctl/command/
lifecycle.rs1use anyhow::{Result, bail};
6
7use umsh::core::PublicKey;
8use umsh::ulcp::{FrameLink, UlcpDevice};
9use umsh::ulcp_wire::alert::AlertState;
10use umsh::ulcp_wire::ids::prop;
11
12use super::persist;
13use super::values::PinArg;
14use crate::App;
15use crate::connection::confirm;
16use crate::output::field;
17
18#[derive(Debug, clap::Subcommand)]
19pub enum IdentityOp {
20 Show,
22 Generate,
24}
25
26#[derive(Debug, clap::Subcommand)]
27pub enum AlertOp {
28 Show,
30 #[command(alias = "on", alias = "find")]
32 Locate,
33 #[command(name = "none", alias = "off", alias = "stop")]
35 None,
36}
37
38pub async fn save<L: FrameLink>(device: &mut UlcpDevice<L>) -> Result<()> {
39 device.save().await?;
40 println!("saved: live state persists across reboots");
41 Ok(())
42}
43
44pub async fn restore<L: FrameLink>(device: &mut UlcpDevice<L>) -> Result<()> {
45 let completion = device.restore().await?;
46 println!("restored live state from the saved snapshot ({completion:?} form)");
47 Ok(())
48}
49
50pub async fn clear<L: FrameLink>(device: &mut UlcpDevice<L>) -> Result<()> {
51 device.clear().await?;
52 println!(
53 "cleared: persisted state erased; live state keeps running until reboot \
54 (BLE bonds and pairing PIN are retained)"
55 );
56 Ok(())
57}
58
59pub async fn reset<L: FrameLink>(device: &mut UlcpDevice<L>) -> Result<()> {
60 let status = device.reset().await?;
61 println!("reset complete ({status:?})");
62 Ok(())
63}
64
65const FACTORY_RESET_WARNING: &str = "factory-reset erases ALL mutable state — persisted provisioning, the device identity, \
66 BLE bonds, and the pairing PIN — then reboots";
67
68pub async fn factory_reset(app: &mut App, yes: bool) -> Result<()> {
69 if !yes {
70 if !app.interactive {
71 bail!("{FACTORY_RESET_WARNING}; re-run with --yes to confirm");
72 }
73 println!("{FACTORY_RESET_WARNING}.");
74 if !confirm("erase everything and reboot?")? {
75 println!("cancelled");
76 return Ok(());
77 }
78 }
79 let device = app.device()?;
80 device.factory_reset().await?;
81 println!(
82 "factory reset sent; the radio is erasing ALL state (provisioning, device identity, \
83 BLE bonds, pairing PIN) and rebooting. The link will drop; re-pair to use it again."
84 );
85 app.detach();
88 Ok(())
89}
90
91pub async fn pin<L: FrameLink>(device: &mut UlcpDevice<L>, value: PinArg) -> Result<()> {
92 device.set_ble_pairing_pin(value.0).await?;
93 match value.0 {
94 Some(_) => println!("BLE pairing PIN set (persisted; applies to new pairings)"),
95 None => println!("BLE pairing PIN cleared"),
96 }
97 Ok(())
98}
99
100pub async fn identity<L: FrameLink>(
101 device: &mut UlcpDevice<L>,
102 op: Option<IdentityOp>,
103) -> Result<()> {
104 match op.unwrap_or(IdentityOp::Show) {
105 IdentityOp::Show => {
106 let value = device.get_prop(prop::DEV_KEY).await?;
107 match <[u8; 32]>::try_from(value.as_slice()) {
108 Ok(key) => field("device identity", PublicKey(key)),
109 Err(_) if value.is_empty() => {
110 println!("no device identity configured (run `identity generate`)");
111 }
112 Err(_) => bail!("malformed PROP_DEV_KEY"),
113 }
114 Ok(())
115 }
116 IdentityOp::Generate => {
117 let value = device.get_prop(prop::DEV_KEY).await?;
118 if let Ok(key) = <[u8; 32]>::try_from(value.as_slice()) {
119 println!("device identity already exists: {}", PublicKey(key));
120 println!("(identities are never regenerated in place; factory-reset discards one)");
121 return Ok(());
122 }
123 let key = device.ensure_device_identity().await?;
124 println!("generated device identity: {}", PublicKey(key));
125 println!("(persisted immediately; device identities are independent of save/restore)");
126 Ok(())
127 }
128 }
129}
130
131pub async fn name(app: &mut App, name: Option<String>) -> Result<()> {
132 let no_save = app.no_save;
133 let Some(name) = name else {
134 let device = app.device()?;
135 let current = device.device_name().await?;
136 field("device name", format!("{current:?}"));
137 return Ok(());
138 };
139 let device = app.device()?;
140 device.set_device_name(&name).await?;
141 println!("device name set to {name:?}");
142 persist(device, no_save).await?;
143 app.rename(name);
145 Ok(())
146}
147
148pub async fn alert<L: FrameLink>(device: &mut UlcpDevice<L>, op: Option<AlertOp>) -> Result<()> {
153 let desired = match op.unwrap_or(AlertOp::Show) {
154 AlertOp::Show => {
155 match device.alert().await? {
156 Some(state) => field("alert", display(state)),
157 None => field("alert", "unsupported (no CAP_ALERT)"),
158 }
159 return Ok(());
160 }
161 AlertOp::Locate => AlertState::Locate,
162 AlertOp::None => AlertState::None,
163 };
164 match device.set_alert(desired).await? {
165 AlertState::Locate => println!(
166 "locate alert started. It stops when you send `alert none`, when someone \
167 cancels it at the radio, or when the radio's own deadline expires — \
168 re-send `alert locate` to keep it going."
169 ),
170 AlertState::None => println!("locate alert stopped"),
171 }
172 Ok(())
173}
174
175fn display(state: AlertState) -> &'static str {
177 match state {
178 AlertState::None => "none",
179 AlertState::Locate => "locate (the radio is making itself conspicuous)",
180 }
181}