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::{format_duration, 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
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn off_and_zero_are_the_same_interval() {
144 assert_eq!("off".parse::<Period>().unwrap().0, 0);
145 assert_eq!("OFF".parse::<Period>().unwrap().0, 0);
146 assert_eq!("0".parse::<Period>().unwrap().0, 0);
147 }
148
149 #[test]
152 fn an_interval_outside_the_bounds_is_refused_with_its_reason() {
153 let error = "30".parse::<Period>().unwrap_err();
154 assert!(error.contains("20m"), "{error}");
155 assert!(error.contains("off"), "{error}");
156
157 let error = "90000".parse::<Period>().unwrap_err();
158 assert!(error.contains("1d"), "{error}");
161
162 assert_eq!(
164 "1200".parse::<Period>().unwrap().0,
165 MIN_AUTO_ANNOUNCE_INTERVAL_S
166 );
167 assert_eq!(
168 "86400".parse::<Period>().unwrap().0,
169 MAX_AUTO_ANNOUNCE_INTERVAL_S
170 );
171 }
172
173 #[test]
174 fn an_interval_reads_back_at_human_scale() {
175 assert_eq!(format_interval(0), "off");
176 assert_eq!(format_interval(14400), "every 14400 s (4h)");
177 assert_eq!(format_interval(3600), "every 3600 s (1h)");
178 assert_eq!(format_interval(1200), "every 1200 s (20m)");
179 assert_eq!(format_interval(5400), "every 5400 s (1h30m)");
180 assert_eq!(format_interval(86400), "every 86400 s (1d)");
183 }
184}