1pub mod advert;
8pub mod capture;
9pub mod duty;
10pub mod gnss;
11pub mod info;
12pub mod lifecycle;
13pub mod phy;
14pub mod provision;
15pub mod repeater;
16pub mod tables;
17pub mod time;
18pub mod values;
19
20use anyhow::{Result, bail};
21
22use umsh::ulcp::{FrameLink, UlcpDevice};
23use umsh::ulcp_wire::ids::prop;
24
25use crate::App;
26use crate::connection::{self, Target};
27use crate::output;
28use values::PinArg;
29
30#[derive(Debug, clap::Subcommand)]
31pub enum Command {
32 Info(info::InfoArgs),
35
36 Provision(provision::ProvisionArgs),
39
40 Identity {
42 #[command(subcommand)]
43 op: Option<lifecycle::IdentityOp>,
44 },
45
46 Name {
48 #[arg(value_name = "NEW-NAME")]
49 name: Option<String>,
50 },
51
52 Save,
54
55 Restore,
57
58 Clear,
60
61 Reset,
64
65 FactoryReset {
68 #[arg(long)]
70 yes: bool,
71 },
72
73 Pin {
75 #[arg(value_name = "6-DIGITS|clear")]
76 value: PinArg,
77 },
78
79 Phy {
81 #[command(subcommand)]
82 op: Option<phy::PhyOp>,
83 },
84
85 Duty {
87 #[command(subcommand)]
88 op: Option<duty::DutyOp>,
89 },
90
91 Repeater {
93 #[command(subcommand)]
94 op: Option<repeater::RepeaterOp>,
95 },
96
97 Time {
99 #[command(subcommand)]
100 op: Option<time::TimeOp>,
101 },
102
103 Gnss {
105 #[command(subcommand)]
106 op: Option<gnss::GnssOp>,
107 },
108
109 Advert {
111 #[command(subcommand)]
112 op: Option<advert::AdvertOp>,
113 },
114
115 DevChannel {
118 #[command(subcommand)]
119 op: Option<tables::TableOp>,
120 },
121
122 DevPeer {
124 #[command(subcommand)]
125 op: Option<tables::TableOp>,
126 },
127
128 Illuminance,
130
131 Alert {
134 #[command(subcommand)]
135 op: Option<lifecycle::AlertOp>,
136 },
137
138 Capture(capture::CaptureArgs),
141
142 Scan {
144 #[arg(long, default_value_t = 2, value_name = "SECS")]
146 timeout: u64,
147 },
148
149 Default {
152 #[command(subcommand)]
153 op: Option<DefaultOp>,
154 },
155}
156
157#[derive(Debug, clap::Subcommand)]
158pub enum DefaultOp {
159 Show,
161 Set {
163 #[arg(value_name = "SELECTOR")]
164 selector: Option<String>,
165 },
166 Clear,
168}
169
170impl Command {
171 pub fn needs_device(&self) -> bool {
173 match self {
174 Self::Scan { .. } => false,
175 Self::Default { op } => matches!(op, Some(DefaultOp::Set { selector: None })),
178 _ => true,
179 }
180 }
181
182 pub fn needs_tethered(&self) -> bool {
185 matches!(self, Self::Provision(_))
186 }
187
188 pub fn validate(&self) -> Result<()> {
195 match self {
196 Self::Capture(args) => args.validate(),
197 Self::Provision(args) => args.desired().map(drop),
198 _ => Ok(()),
199 }
200 }
201
202 pub async fn run(self, app: &mut App) -> Result<()> {
203 match self {
204 Self::Info(args) => info::run(app.device()?, args).await,
205 Self::Provision(args) => provision::run(app, args).await,
206 Self::Identity { op } => lifecycle::identity(app.device()?, op).await,
207 Self::Name { name } => lifecycle::name(app, name).await,
208 Self::Save => lifecycle::save(app.device()?).await,
209 Self::Restore => lifecycle::restore(app.device()?).await,
210 Self::Clear => lifecycle::clear(app.device()?).await,
211 Self::Reset => lifecycle::reset(app.device()?).await,
212 Self::FactoryReset { yes } => lifecycle::factory_reset(app, yes).await,
213 Self::Pin { value } => lifecycle::pin(app.device()?, value).await,
214 Self::Phy { op } => phy::run(app, op).await,
215 Self::Duty { op } => duty::run(app, op).await,
216 Self::Repeater { op } => repeater::run(app, op).await,
217 Self::Time { op } => time::run(app, op).await,
218 Self::Gnss { op } => gnss::run(app, op).await,
219 Self::Advert { op } => advert::run(app, op).await,
220 Self::DevChannel { op } => {
221 tables::run(app, prop::DEV_CHANNEL_KEYS, "channel", op).await
222 }
223 Self::DevPeer { op } => tables::run(app, prop::DEV_PEERS, "peer", op).await,
224 Self::Illuminance => info::illuminance(app.device()?).await,
225 Self::Alert { op } => lifecycle::alert(app.device()?, op).await,
226 Self::Capture(args) => capture::run(app, args).await,
227 Self::Scan { timeout } => scan(app, timeout).await,
228 Self::Default { op } => default(app, op.unwrap_or(DefaultOp::Show)),
229 }
230 }
231}
232
233async fn scan(app: &mut App, timeout: u64) -> Result<()> {
234 println!("scanning for ULCP radios ({timeout} s) ...");
235 let found = connection::scan(std::time::Duration::from_secs(timeout)).await?;
236 connection::render_found(&found);
237 app.last_scan = found;
239 Ok(())
240}
241
242fn default(app: &mut App, op: DefaultOp) -> Result<()> {
243 match op {
244 DefaultOp::Show => {
245 match &app.prefs.default_device {
246 Some(device) => output::field("default", device.display()),
247 None => println!("no default radio saved"),
248 }
249 if let Some(path) = connection::config_path() {
250 output::field("settings", path.display());
251 }
252 Ok(())
253 }
254 DefaultOp::Set { selector } => {
255 let device = match selector {
256 Some(selector) => connection::DefaultDevice {
257 selector,
258 name: None,
259 },
260 None => {
261 let session = app.session()?;
262 match &session.target {
263 Target::Ble { selector, .. } => connection::DefaultDevice {
264 selector: selector.clone(),
265 name: Some(session.label.clone()),
266 },
267 Target::Serial { .. } => bail!(
268 "the saved default is a BLE radio: serial port names change between \
269 plug-ins, so name one with --port or set UMSHCTL_PORT instead"
270 ),
271 }
272 }
273 };
274 app.prefs.default_device = Some(device.clone());
275 let path = app.prefs.store()?;
276 println!("default radio set to {}", device.display());
277 output::field("settings", path.display());
278 Ok(())
279 }
280 DefaultOp::Clear => {
281 app.prefs.default_device = None;
282 app.prefs.store()?;
283 println!("default radio cleared");
284 Ok(())
285 }
286 }
287}
288
289pub async fn persist<L: FrameLink>(device: &mut UlcpDevice<L>, no_save: bool) -> Result<()> {
296 if no_save {
297 output::note("--no-save — changes are live only; the save command persists them");
298 } else {
299 device.save().await?;
300 println!("saved: changes persist across reboots");
301 }
302 Ok(())
303}
304
305fn decode_u16(value: &[u8]) -> Option<u16> {
306 <[u8; 2]>::try_from(value).ok().map(u16::from_le_bytes)
307}
308
309fn decode_u32(value: &[u8]) -> Option<u32> {
310 <[u8; 4]>::try_from(value).ok().map(u32::from_le_bytes)
311}
312
313fn duty_percent(raw: u16) -> f64 {
315 f64::from(raw) * 100.0 / 65535.0
316}