Skip to main content

tag_info/
tag-info.rs

1//! `tag-info`: print a tag's information, configuration, status and state
2//! log, or (`--json`) all of it as one JSON object for recording.
3//!
4//! Port of `host/commandline/tag-info.cc`. Attaching connects under reset,
5//! so the state shown is the one the tag restored on boot, not the one it
6//! was in.
7//!
8//! Usage: `tag-info [-d] [-b BUS:DEVICE] [-j|--json]`.
9//!
10//! Without `--json` it prints, in order: the base identity (or
11//! "unidentified" for base firmware without the identify command), the
12//! tag's git SHA when the base reports one, the `TagInfo` message as protobuf
13//! text followed by one `type: label : value` line per field, the
14//! configuration, the current state with any firmware report, and the
15//! persistent state-transition log. A failed configuration read is reported
16//! on stderr and the rest still prints.
17//!
18//! # Exit status
19//!
20//! 0 on success; 1 if no base matches, the attach fails or `GetInfo` fails.
21//! Option errors exit 2 (clap).
22//!
23//! # Hardware side effects
24//!
25//! The tag is reset by the attach, and any run it was making is ended by
26//! that reset (see `docs/bench/verifying-firmware.md`). Nothing is written to
27//! the tag.
28
29use 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/// print tag information. Attaching connects under reset, so the state shown
39/// is the one the tag restored on boot, not the one it was in: a tag past its
40/// start time is reported RUNNING because the reset started it, and a tag
41/// that had stalled reports whatever its backup registers held
42#[derive(Parser)]
43#[command(name = "tag-info")]
44struct Cli {
45    /// The shared `--debug` and `--base` options.
46    #[command(flatten)]
47    base: BaseArgs,
48
49    /// Print tag information as a single JSON object, for recording rather
50    /// than reading
51    #[arg(short, long)]
52    json: bool,
53}
54
55/// IMUTag collection-init failure bits; see IMUTag inc/sensors.h.
56const INIT_FAIL_LSM_CONFIG: u32 = 1 << 0;
57/// Init-failure bit: the magnetometer failed to initialise.
58const INIT_FAIL_MAG: u32 = 1 << 1;
59/// Init-failure bit: the pressure sensor failed to initialise.
60const INIT_FAIL_PRESSURE: u32 = 1 << 2;
61/// Bit position of the first failing driver's status code in the detail
62/// word.
63const INIT_FAIL_STATUS_SHIFT: u32 = 8;
64/// Mask of the 8-bit first-failing-driver status code in the detail word.
65const INIT_FAIL_STATUS_MASK: u32 = 0xff << INIT_FAIL_STATUS_SHIFT;
66
67/// A float as `std::ostream` prints it by default (`%g`, six digits).
68fn ostream_float(v: f32) -> String {
69    format_g(f64::from(v), 6, 6, |_| true)
70}
71
72/// A marker epoch as UTC, or as a bare value when implausible: the tag
73/// stores -1 for an erased slot, and an unset RTC gives an epoch near zero.
74fn 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
85/// A marker's detail word, decoded for the reason that produced it. Only the
86/// collection-init failure (EVENT_UNKNOWN) is decoded; anything else is shown
87/// in hex rather than dropped, so newer firmware detail is still visible.
88fn 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
115/// Prints the tag's persistent state-transition marker log.
116///
117/// The tag records one marker per transition into a fixed, non-wrapping
118/// flash region that only a reset erases; once full, markers are dropped
119/// silently, so the entry count is the diagnostic of interest.
120fn print_state_log(tag: &Tag) {
121    println!("State transition log:");
122    let mut next = 0;
123    // A tag that keeps returning entries must not hang the tool; capacity
124    // is 10 + TagState_MAX on current firmware, so 16 pages is ample.
125    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
148/// Prints one state-transition marker: its index, state, transition reason
149/// and time, then any decoded detail word, then the flash page counts,
150/// supply voltage (V) and temperature recorded with it.
151///
152/// A marker without a status prints the protobuf defaults.
153fn 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    // Only present on targets with room for it, and only when the
162    // transition had something to say, so absence means nothing.
163    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
178/// Prints info, config and status as one JSON object, serialised by the
179/// library with the options tagcore uses to store them. Config and status
180/// are included when available rather than required.
181fn 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    // Plain ASCII from the base firmware's target name and git metadata,
188    // with nothing JSON would need escaped.
189    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
207/// The C++ `FieldDescriptor::type_name()` of a field.
208fn 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
230/// Human labels for some TagInfo fields.
231fn 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
246/// Lists every TagInfo field with its type, so a field added to the
247/// protocol prints without a change here. A field with explicit presence
248/// that is unset prints "(not set)", not a default indistinguishable from a
249/// real zero; repeated fields are not expanded.
250fn 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
273/// Parses the options, attaches (resetting the tag) and prints the report
274/// or, with `--json`, the JSON object.
275fn 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    // A default-constructed config would misreport the tag's settings.
307    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        // A boot reset-recovery trace when the firmware has nothing else to
315        // report; see statusRecoveryTraceWrite().
316        if !status.debug_message.is_empty() {
317            println!("Firmware report: {}", status.debug_message);
318        }
319    }
320    print_state_log(&tag);
321    ExitCode::SUCCESS
322}