1use anyhow::{Result, bail};
9
10use umsh::node::location::NodeLocation;
11use umsh::ulcp::GnssStatus;
12use umsh::ulcp_wire::gnss::FixKind;
13
14use super::persist;
15use crate::App;
16use crate::output::{field, note, subfield};
17
18#[derive(Debug, clap::Subcommand)]
19pub enum GnssOp {
20 Status,
22 On,
24 Off,
26 IdentUpdate {
29 #[command(subcommand)]
30 op: ToggleOp,
31 },
32 IdentPrecision {
35 #[arg(value_name = "1-7")]
36 bytes: u8,
37 },
38 Trust {
41 #[command(subcommand)]
42 op: ToggleOp,
43 },
44}
45
46#[derive(Debug, clap::Subcommand)]
47pub enum ToggleOp {
48 On,
49 Off,
50}
51
52impl ToggleOp {
53 fn enabled(&self) -> bool {
54 matches!(self, Self::On)
55 }
56}
57
58pub async fn run(app: &mut App, op: Option<GnssOp>) -> Result<()> {
59 let no_save = app.no_save;
60 let device = app.device()?;
61 match op.unwrap_or(GnssOp::Status) {
62 GnssOp::Status => {
63 let Some(status) = device.gnss_status().await? else {
64 bail!("device does not advertise CAP_GNSS");
65 };
66 report(&status);
67 return Ok(());
68 }
69 GnssOp::On => {
70 let enabled = device.set_gnss_enabled(true).await?;
71 println!("gnss {}", if enabled { "on" } else { "off" });
72 }
73 GnssOp::Off => {
74 let enabled = device.set_gnss_enabled(false).await?;
75 println!("gnss {}", if enabled { "on" } else { "off" });
76 }
77 GnssOp::IdentUpdate { op } => {
78 let enabled = device.set_gnss_ident_update(op.enabled()).await?;
79 println!(
80 "gnss identity update {}",
81 if enabled { "on" } else { "off" }
82 );
83 }
84 GnssOp::IdentPrecision { bytes } => {
85 let stored = device.set_gnss_ident_precision(bytes).await?;
86 println!(
87 "gnss identity precision {stored} bytes ({})",
88 precision_cell(stored)
89 );
90 }
91 GnssOp::Trust { op } => {
92 let trust = device.set_gnss_time_trust(op.enabled()).await?;
93 println!("gnss time trust {}", if trust { "on" } else { "off" });
94 if !trust {
95 note("receiver time will not set the clock; set it with `time sync`");
96 }
97 }
98 }
99 persist(device, no_save).await
100}
101
102fn report(status: &GnssStatus) {
103 field("gnss", if status.enabled { "on" } else { "off" });
104 subfield(
105 "fix",
106 match status.fix.fix {
107 FixKind::None if status.enabled => "none (searching)",
108 FixKind::None => "none (receiver off)",
109 FixKind::TwoD => "2D",
110 FixKind::ThreeD => "3D",
111 },
112 );
113 match status.fix.sats_in_view {
114 Some(in_view) => subfield(
115 "satellites",
116 format!("{} used of {in_view} in view", status.fix.sats_used),
117 ),
118 None => subfield("satellites", format!("{} used", status.fix.sats_used)),
119 }
120 let location = status.fix.location();
121 if location.is_empty() {
122 subfield("location", "none");
123 } else {
124 subfield("location", format_location(location));
125 subfield("position", format_position(location));
126 }
127 match status.fix.altitude_m {
128 Some(meters) => subfield("altitude", format!("{meters} m")),
129 None => subfield("altitude", "unknown"),
130 }
131 match status.fix.accuracy_dm {
132 Some(dm) => subfield(
136 "precision",
137 format!("~{}.{} m (estimated)", dm / 10, dm % 10),
138 ),
139 None => subfield("precision", "unknown"),
140 }
141 subfield(
142 "identity update",
143 match status.ident_update {
144 true => format!(
145 "on, clamped to {} bytes ({})",
146 status.ident_precision,
147 precision_cell(status.ident_precision)
148 ),
149 false => format!("off (would clamp to {} bytes)", status.ident_precision),
150 },
151 );
152 subfield(
153 "time trust",
154 match status.time_trust {
155 true => "on (fixes set the clock)",
156 false => "off (fixes never set the clock)",
157 },
158 );
159}
160
161fn format_location(bytes: &[u8]) -> String {
165 let mut out = String::with_capacity(bytes.len() * 2 + 16);
166 for byte in bytes {
167 out.push_str(&format!("{byte:02x}"));
168 }
169 out.push_str(&format!(
170 " ({} bytes, {})",
171 bytes.len(),
172 precision_cell(bytes.len() as u8)
173 ));
174 out
175}
176
177fn format_position(bytes: &[u8]) -> String {
181 format!("{} (lat, lon)", NodeLocation::from_bytes(bytes))
182}
183
184fn precision_cell(bytes: u8) -> &'static str {
188 match bytes {
189 1 => "~2500 km",
190 2 => "~156 km",
191 3 => "~9.8 km",
192 4 => "~610 m",
193 5 => "~38 m",
194 6 => "~2.4 m",
195 7 => "~15 cm",
196 _ => "out of range",
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn precision_names_the_area_it_discloses() {
206 assert_eq!(precision_cell(5), "~38 m");
207 assert_eq!(precision_cell(0), "out of range");
208 assert_eq!(precision_cell(8), "out of range");
209 }
210
211 #[test]
212 fn a_location_renders_as_the_grid_code_it_is() {
213 assert_eq!(
214 format_location(&[0x8a, 0x1f, 0x4c]),
215 "8a1f4c (3 bytes, ~9.8 km)"
216 );
217 }
218
219 #[test]
220 fn a_position_renders_as_degrees_at_the_encoded_precision() {
221 assert_eq!(
224 format_position(&[0x8a, 0x1f, 0x4c]),
225 "0.90, 67.19 (lat, lon)"
226 );
227 }
228}