umshctl/command/
advert.rs1use anyhow::{Result, bail};
14
15use umsh::ulcp::AdvertPolicy;
16use umsh::ulcp_wire::ids::{MAX_AUTO_ANNOUNCE_INTERVAL_S, MIN_AUTO_ANNOUNCE_INTERVAL_S};
17
18use super::persist;
19use crate::App;
20use crate::output::{field, subfield};
21
22#[derive(Debug, clap::Subcommand)]
23pub enum AdvertOp {
24 Status,
26 Interval {
28 #[arg(value_name = "SECONDS|off")]
29 period: Period,
30 },
31 BeaconInterval {
33 #[arg(value_name = "SECONDS|off")]
34 period: Period,
35 },
36 Startup {
38 #[command(subcommand)]
39 op: ToggleOp,
40 },
41}
42
43#[derive(Debug, Clone, Copy)]
48pub struct Period(u32);
49
50impl std::str::FromStr for Period {
51 type Err = String;
52
53 fn from_str(text: &str) -> Result<Self, Self::Err> {
54 if text.eq_ignore_ascii_case("off") || text == "0" {
55 return Ok(Self(0));
56 }
57 let seconds: u32 = text
58 .parse()
59 .map_err(|_| format!("expected a number of seconds or `off`, got `{text}`"))?;
60 if seconds < MIN_AUTO_ANNOUNCE_INTERVAL_S {
61 return Err(format!(
62 "the shortest accepted interval is {} ({MIN_AUTO_ANNOUNCE_INTERVAL_S} s); use `off` to send none",
63 format_duration(MIN_AUTO_ANNOUNCE_INTERVAL_S)
64 ));
65 }
66 if seconds > MAX_AUTO_ANNOUNCE_INTERVAL_S {
67 return Err(format!(
68 "the longest accepted interval is {} ({MAX_AUTO_ANNOUNCE_INTERVAL_S} s); use `off` to send none",
69 format_duration(MAX_AUTO_ANNOUNCE_INTERVAL_S)
70 ));
71 }
72 Ok(Self(seconds))
73 }
74}
75
76#[derive(Debug, clap::Subcommand)]
77pub enum ToggleOp {
78 On,
79 Off,
80}
81
82impl ToggleOp {
83 fn enabled(&self) -> bool {
84 matches!(self, Self::On)
85 }
86}
87
88pub async fn run(app: &mut App, op: Option<AdvertOp>) -> Result<()> {
89 let no_save = app.no_save;
90 let device = app.device()?;
91 match op.unwrap_or(AdvertOp::Status) {
92 AdvertOp::Status => {
93 let Some(policy) = device.advert_policy().await? else {
94 bail!("device does not advertise CAP_ADVERT");
95 };
96 report(&policy);
97 return Ok(());
98 }
99 AdvertOp::Interval { period } => {
100 let stored = device.set_advert_interval(period.0).await?;
101 println!("advertisement {}", format_interval(stored));
102 }
103 AdvertOp::BeaconInterval { period } => {
104 let stored = device.set_beacon_interval(period.0).await?;
105 println!("beacon {}", format_interval(stored));
106 }
107 AdvertOp::Startup { op } => {
108 let enabled = device.set_startup_beacon(op.enabled()).await?;
109 println!("startup beacon {}", if enabled { "on" } else { "off" });
110 }
111 }
112 persist(device, no_save).await
113}
114
115fn report(policy: &AdvertPolicy) {
116 field("advert", format_interval(policy.advert_interval_s));
117 subfield("beacon", format_interval(policy.beacon_interval_s));
118 subfield(
119 "startup beacon",
120 if policy.startup_beacon { "on" } else { "off" },
121 );
122 if policy.advert_interval_s > 0 || policy.beacon_interval_s > 0 {
125 subfield("scatter", "each period runs up to 25% longer");
126 }
127}
128
129fn format_interval(seconds: u32) -> String {
132 if seconds == 0 {
133 return "off".to_string();
134 }
135 format!("every {seconds} s ({})", format_duration(seconds))
136}
137
138fn format_duration(seconds: u32) -> String {
139 match seconds {
140 s if s >= 3600 && s % 3600 == 0 => format!("{}h", s / 3600),
141 s if s >= 3600 => format!("{}h{}m", s / 3600, (s % 3600) / 60),
142 s if s >= 60 && s % 60 == 0 => format!("{}m", s / 60),
143 s if s >= 60 => format!("{}m{}s", s / 60, s % 60),
144 s => format!("{s}s"),
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn off_and_zero_are_the_same_interval() {
154 assert_eq!("off".parse::<Period>().unwrap().0, 0);
155 assert_eq!("OFF".parse::<Period>().unwrap().0, 0);
156 assert_eq!("0".parse::<Period>().unwrap().0, 0);
157 }
158
159 #[test]
162 fn an_interval_outside_the_bounds_is_refused_with_its_reason() {
163 let error = "30".parse::<Period>().unwrap_err();
164 assert!(error.contains("20m"), "{error}");
165 assert!(error.contains("off"), "{error}");
166
167 let error = "90000".parse::<Period>().unwrap_err();
168 assert!(error.contains("24h"), "{error}");
169
170 assert_eq!(
172 "1200".parse::<Period>().unwrap().0,
173 MIN_AUTO_ANNOUNCE_INTERVAL_S
174 );
175 assert_eq!(
176 "86400".parse::<Period>().unwrap().0,
177 MAX_AUTO_ANNOUNCE_INTERVAL_S
178 );
179 }
180
181 #[test]
182 fn an_interval_reads_back_at_human_scale() {
183 assert_eq!(format_interval(0), "off");
184 assert_eq!(format_interval(14400), "every 14400 s (4h)");
185 assert_eq!(format_interval(3600), "every 3600 s (1h)");
186 assert_eq!(format_interval(1200), "every 1200 s (20m)");
187 assert_eq!(format_interval(5400), "every 5400 s (1h30m)");
188 }
189}