umshctl/command/
repeater.rs1use anyhow::{Result, bail};
10
11use umsh::core::RegionCode;
12use umsh::ulcp_wire::ids::prop;
13
14use super::persist;
15use super::values::{MinRssiArg, MinSnrArg, OptRegionArg, RegionArg, RegionListArg};
16use crate::App;
17use crate::output::{subfield, warn};
18
19#[derive(Debug, clap::Subcommand)]
20pub enum RepeaterOp {
21 Show,
23 On,
25 Off,
27 Regions {
29 #[command(subcommand)]
30 op: Option<RegionOp>,
31 },
32 DefaultRegion {
35 #[arg(value_name = "CODE|none")]
36 code: OptRegionArg,
37 },
38 MinRssi {
40 #[arg(value_name = "DBM|none", allow_hyphen_values = true)]
41 dbm: MinRssiArg,
42 },
43 MinSnr {
45 #[arg(value_name = "DB|none", allow_hyphen_values = true)]
46 db: MinSnrArg,
47 },
48}
49
50#[derive(Debug, clap::Subcommand)]
54pub enum RegionOp {
55 List,
57 Add {
59 #[arg(value_name = "REGION")]
60 region: RegionArg,
61 },
62 Remove {
64 #[arg(value_name = "REGION")]
65 region: RegionArg,
66 },
67 Set {
70 #[arg(value_name = "REGION[,...]|none")]
71 list: RegionListArg,
72 },
73}
74
75pub async fn run(app: &mut App, op: Option<RepeaterOp>) -> Result<()> {
76 let no_save = app.no_save;
77 let device = app.device()?;
78 match op.unwrap_or(RepeaterOp::Show) {
79 RepeaterOp::Show => {
80 let Some(policy) = device.repeater_policy().await? else {
81 bail!("device does not advertise CAP_REPEATER");
82 };
83 print_enabled(Some(policy.enabled as u8));
84 subfield("regions", format_regions(&policy.regions));
85 match policy.default_region {
86 Some(code) => subfield("default", code),
87 None => subfield("default", "none (forwards untagged floods untagged)"),
88 }
89 match policy.min_rssi {
90 Some(dbm) => subfield("min rssi", format!("{dbm} dBm")),
91 None => subfield("min rssi", "any"),
92 }
93 match policy.min_snr {
94 Some(db) => subfield("min snr", format!("{db} dB")),
95 None => subfield("min snr", "any"),
96 }
97 if policy.enabled
98 && policy.default_region.is_some_and(|code| {
99 !policy.regions.is_empty() && !region_codes(&policy.regions).contains(&code)
100 })
101 {
102 warn(
107 "the default region is not in the forwarding list; floods this repeater \
108 tags will not be forwarded by it",
109 );
110 }
111 return Ok(());
112 }
113 RepeaterOp::On => set_enabled(device, true).await?,
114 RepeaterOp::Off => set_enabled(device, false).await?,
115 RepeaterOp::Regions { op } => match op.unwrap_or(RegionOp::List) {
116 RegionOp::List => {
117 let regions = device.repeater_regions().await?;
118 println!("repeater regions {}", format_regions(®ions));
119 return Ok(());
120 }
121 RegionOp::Add { region } => {
122 device.add_repeater_region(®ion.0).await?;
123 println!("repeater region added: {}", format_region(®ion.0));
124 }
125 RegionOp::Remove { region } => {
126 device.remove_repeater_region(®ion.0).await?;
127 println!("repeater region removed: {}", format_region(®ion.0));
128 }
129 RegionOp::Set { list } => {
130 let stored = device.set_repeater_regions(&list.0).await?;
131 println!("repeater regions {}", format_regions(&stored));
132 if stored.len() < list.0.len() {
133 warn(format!(
134 "the device kept {} of {} regions (repeats collapse)",
135 stored.len(),
136 list.0.len()
137 ));
138 }
139 }
140 },
141 RepeaterOp::DefaultRegion { code } => {
142 match device.set_repeater_default_region(code.0).await? {
143 Some(code) => println!("repeater default region {code}"),
144 None => println!("repeater default region none (forwards untagged)"),
145 }
146 }
147 RepeaterOp::MinRssi { dbm } => match device.set_repeater_min_rssi(dbm.0).await? {
148 Some(dbm) => println!("repeater min rssi {dbm} dBm"),
149 None => println!("repeater min rssi any"),
150 },
151 RepeaterOp::MinSnr { db } => match device.set_repeater_min_snr(db.0).await? {
152 Some(db) => println!("repeater min snr {db} dB"),
153 None => println!("repeater min snr any"),
154 },
155 }
156 persist(device, no_save).await
157}
158
159async fn set_enabled(
160 device: &mut umsh::ulcp::UlcpDevice<crate::connection::SessionLink>,
161 enabled: bool,
162) -> Result<()> {
163 let echoed = device
164 .set_prop(prop::MAC_REPEATER_ENABLED, &[enabled as u8])
165 .await?;
166 print_enabled(echoed.first().copied());
167 Ok(())
168}
169
170fn print_enabled(byte: Option<u8>) {
171 match byte {
172 Some(0) => println!("repeater off"),
173 Some(_) => println!("repeater on (on-board node forwards overheard frames)"),
174 None => println!("repeater state unknown (empty value)"),
175 }
176}
177
178fn region_codes(regions: &[String]) -> Vec<RegionCode> {
181 regions
182 .iter()
183 .filter_map(|region| region.parse::<RegionCode>().ok())
184 .collect()
185}
186
187pub fn format_region(region: &str) -> String {
195 let Ok(code) = region.parse::<RegionCode>() else {
196 return region.to_string();
197 };
198 let hex = format!("0x{:04X}", code.as_u16());
199 if region.eq_ignore_ascii_case(&hex) {
200 return hex;
201 }
202 match RegionCode::from_short_code(region).is_ok() {
203 true => format!("{} ({hex})", region.to_uppercase()),
204 false => format!("{region} ({hex})"),
205 }
206}
207
208pub fn format_regions(regions: &[String]) -> String {
211 if regions.is_empty() {
212 return "any (no regional restriction)".to_string();
213 }
214 regions
215 .iter()
216 .map(|region| format_region(region))
217 .collect::<Vec<_>>()
218 .join(", ")
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn the_empty_region_list_says_what_it_means() {
227 assert_eq!(format_regions(&[]), "any (no regional restriction)");
228 }
229
230 #[test]
231 fn a_region_prints_the_code_its_string_derives_to() {
232 assert_eq!(format_region("SJC"), "SJC (0x7853)");
233 assert_eq!(format_region("Rogue Valley"), "Rogue Valley (0xC0F9)");
234 assert_eq!(format_region("0x1234"), "0x1234");
236 }
237
238 #[test]
239 fn a_short_code_prints_uppercase_however_it_was_written() {
240 assert_eq!(format_region("sjc"), "SJC (0x7853)");
241 assert_eq!(format_region("Sjc"), "SJC (0x7853)");
242 assert_eq!(format_region("wa"), "WA (0x8FE8)");
243 assert_eq!(format_region("w7"), format!("W7 (0x{:04X})", short("w7")));
246 assert_eq!(format_region("rogue valley"), "rogue valley (0xC0F9)");
248 }
249
250 fn short(code: &str) -> u16 {
251 RegionCode::from_short_code(code).unwrap().as_u16()
252 }
253}