1use std::process::ExitCode;
30
31use clap::Parser;
32use prost_reflect::{Kind, ReflectMessage};
33use tag_tools::{BaseArgs, enum_name, exit_on_interrupt, init_logging, select_base, state_name};
34use tagcore::Tag;
35use tagcore::proto::{self, State, TagInfo, state};
36use tagcore::textformat::{format_g, scalar};
37
38#[derive(Parser)]
43#[command(name = "tag-info")]
44struct Cli {
45 #[command(flatten)]
47 base: BaseArgs,
48
49 #[arg(short, long)]
52 json: bool,
53}
54
55const INIT_FAIL_LSM_CONFIG: u32 = 1 << 0;
57const INIT_FAIL_MAG: u32 = 1 << 1;
59const INIT_FAIL_PRESSURE: u32 = 1 << 2;
61const INIT_FAIL_STATUS_SHIFT: u32 = 8;
64const INIT_FAIL_STATUS_MASK: u32 = 0xff << INIT_FAIL_STATUS_SHIFT;
66
67fn ostream_float(v: f32) -> String {
69 format_g(f64::from(v), 6, 6, |_| true)
70}
71
72fn format_marker_time(millis: i64) -> String {
75 let seconds = millis / 1000;
76 if seconds <= 0 {
77 return format!("{seconds} <-- clock not set");
78 }
79 match chrono::DateTime::from_timestamp(seconds, 0) {
80 Some(t) => t.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
81 None => seconds.to_string(),
82 }
83}
84
85fn format_transition_detail(reason: i32, detail: u32) -> String {
89 if reason != state::Event::Unknown as i32 {
90 return format!("0x{detail:x}");
91 }
92 let mut stages = Vec::new();
93 if detail & INIT_FAIL_LSM_CONFIG != 0 {
94 stages.push("LSM6 config");
95 }
96 if detail & INIT_FAIL_MAG != 0 {
97 stages.push("magnetometer");
98 }
99 if detail & INIT_FAIL_PRESSURE != 0 {
100 stages.push("pressure");
101 }
102 let mut out = String::from("collection init failed: ");
103 if stages.is_empty() {
104 out.push_str(&format!("unrecognized (0x{detail:x})"));
105 } else {
106 out.push_str(&stages.join(", "));
107 }
108 let status = (detail & INIT_FAIL_STATUS_MASK) >> INIT_FAIL_STATUS_SHIFT;
109 if status != 0 {
110 out.push_str(&format!("; first driver status {status}"));
111 }
112 out
113}
114
115fn print_state_log(tag: &Tag) {
121 println!("State transition log:");
122 let mut next = 0;
123 for _ in 0..16 {
126 let Ok(log) = tag.state_log(next) else { break };
127 if log.states.is_empty() {
128 break;
129 }
130 for marker in &log.states {
131 print_marker(next, marker);
132 next += 1;
133 }
134 }
135 if next == 0 {
136 println!(
137 " (empty -- nothing recorded since the last reset; reset recovery reads this as idle)"
138 );
139 } else {
140 println!(
141 " entries: {next} (compare against the tag's capacity, 10 + TagState_MAX; at \
142 capacity, new markers are dropped silently and reset recovery may act on a stale \
143 final entry)"
144 );
145 }
146}
147
148fn print_marker(index: i32, marker: &State) {
154 let status = marker.status.clone().unwrap_or_default();
155 println!(
156 " [{index}] {} reason={} time={}",
157 state_name(status.state),
158 enum_name(marker.transition_reason, state::Event::as_str_name),
159 format_marker_time(status.millis)
160 );
161 if marker.transition_detail != 0 {
164 println!(
165 " detail={}",
166 format_transition_detail(marker.transition_reason, marker.transition_detail)
167 );
168 }
169 println!(
170 " internal_pages={} external_pages={} vdd={} temp={}",
171 status.internal_data_count,
172 status.external_data_count,
173 ostream_float(status.voltage),
174 ostream_float(status.temperature)
175 );
176}
177
178fn print_json(tag: &Tag) -> ExitCode {
182 let Ok(info) = tag.tag_info() else {
183 eprintln!("Info failed");
184 return ExitCode::FAILURE;
185 };
186 let mut out = String::from("{");
187 if let Some(base) = tag.base_identity() {
190 out.push_str(&format!("\"base\":\"{base}\","));
191 }
192 if let Some(sha) = tag.git_sha() {
193 out.push_str(&format!("\"tag_sha\":\"{sha}\","));
194 }
195 out.push_str(&format!("\"info\":{}", proto::to_json(&info)));
196 if let Ok(config) = tag.config() {
197 out.push_str(&format!(",\"config\":{}", proto::to_json(&config)));
198 }
199 if let Ok(status) = tag.status() {
200 out.push_str(&format!(",\"status\":{}", proto::to_json(&status)));
201 }
202 out.push('}');
203 println!("{out}");
204 ExitCode::SUCCESS
205}
206
207fn type_name(kind: &Kind) -> &'static str {
209 match kind {
210 Kind::Double => "double",
211 Kind::Float => "float",
212 Kind::Int64 => "int64",
213 Kind::Uint64 => "uint64",
214 Kind::Int32 => "int32",
215 Kind::Fixed64 => "fixed64",
216 Kind::Fixed32 => "fixed32",
217 Kind::Bool => "bool",
218 Kind::String => "string",
219 Kind::Bytes => "bytes",
220 Kind::Uint32 => "uint32",
221 Kind::Sfixed32 => "sfixed32",
222 Kind::Sfixed64 => "sfixed64",
223 Kind::Sint32 => "sint32",
224 Kind::Sint64 => "sint64",
225 Kind::Enum(_) => "enum",
226 Kind::Message(_) => "message",
227 }
228}
229
230fn label(name: &str) -> &str {
232 match name {
233 "monitor" => "Monitor Version",
234 "board_desc" => "Board Description",
235 "uuid" => "Chip UUID",
236 "intflashsz" => "Internal Flash (kB)",
237 "extflashsz" => "External Flash (kB)",
238 "firmware" => "Firmware",
239 "gitrepo" => "Git Repo",
240 "githash" => "Git Hash",
241 "build_time" => "Build Date",
242 other => other,
243 }
244}
245
246fn print_fields(info: &TagInfo) {
251 let msg = info.transcode_to_dynamic();
252 for field in msg.descriptor().fields() {
253 let value = if field.is_list() || field.is_map() {
254 "(repeated)".to_owned()
255 } else if field.supports_presence() && !msg.has_field(&field) {
256 "(not set)".to_owned()
257 } else {
258 match msg.get_field(&field).as_ref() {
259 prost_reflect::Value::F32(v) => ostream_float(*v),
260 prost_reflect::Value::F64(v) => format_g(*v, 6, 6, |_| true),
261 prost_reflect::Value::String(s) => s.clone(),
262 v => scalar(&field, v),
263 }
264 };
265 println!(
266 "{}: {} : {value}",
267 type_name(&field.kind()),
268 label(field.name())
269 );
270 }
271}
272
273fn main() -> ExitCode {
276 let cli = Cli::parse();
277 init_logging(&cli.base);
278 exit_on_interrupt();
279
280 let tag = Tag::new();
281 let dev = select_base::<Cli>(&tag, &cli.base);
282 if !tag.attach(&dev) {
283 return ExitCode::FAILURE;
284 }
285 if cli.json {
286 return print_json(&tag);
287 }
288
289 match tag.base_identity() {
290 Some(base) => println!("Base: {base}"),
291 None => println!("Base: unidentified (its firmware predates the identify command)"),
292 }
293 if let Some(sha) = tag.git_sha() {
294 println!("Tag SHA: {sha}");
295 }
296 let info = match tag.tag_info() {
297 Ok(info) => info,
298 Err(_) => {
299 eprintln!("Info failed");
300 return ExitCode::FAILURE;
301 }
302 };
303 println!("{}", proto::debug_string(&info));
304 print_fields(&info);
305
306 match tag.config() {
308 Ok(config) => println!("{}", proto::debug_string(&config)),
309 Err(_) => eprintln!("GetConfig failed: {}", tag.debug_message()),
310 }
311
312 if let Ok(status) = tag.status() {
313 println!("Current state: {}", state_name(status.state));
314 if !status.debug_message.is_empty() {
317 println!("Firmware report: {}", status.debug_message);
318 }
319 }
320 print_state_log(&tag);
321 ExitCode::SUCCESS
322}