tag_reset/tag-reset.rs
1//! `tag-reset`: stop, erase, and return a tag to the idle state.
2//!
3//! Port of `host/commandline/tag-reset.cc`. Waits for the tag to report a
4//! definite state after the attach reset, stops it if it is RUNNING,
5//! HIBERNATING or CONFIGURED and polls until it is FINISHED or ABORTED,
6//! erases it and polls until the erase reaches IDLE, and optionally syncs its
7//! clock (`--set-rtc`).
8//!
9//! Exits 0 with the tag IDLE; 1 if the attach, a status read, the stop, the
10//! erase or the clock sync fails, the tag never settles, does not stop
11//! within `--stop-timeout` or reach IDLE within `--reset-timeout`, or is in
12//! a state it cannot be reset from (TEST, CALIBRATE, EXCEPTION).
13//!
14//! Usage: `tag-reset [-d] [-b BUS:DEVICE] [--set-rtc] [--reset-timeout S]
15//! [--stop-timeout S] [--settle-timeout S]` (defaults 180, 30 and 10 s).
16//! Option errors exit 2 (clap).
17//!
18//! # Hardware side effects
19//!
20//! Destructive: the attach resets the tag, the stop ends any run, and the
21//! erase discards every stored sample and the state-transition log. The
22//! erase is done by the firmware already on the tag, which knows its own log
23//! layout, so run this before flashing a new image (step 2 of the flashing
24//! protocol in `AGENTS.md`). An IDLE tag is left as it is, apart from
25//! `--set-rtc`, which overwrites its clock with the host's. Status reads that
26//! fail while the erase is in progress are skipped, not fatal, since the
27//! firmware may be busy sweeping storage.
28
29use std::process::ExitCode;
30use std::thread::sleep;
31use std::time::Duration;
32
33use clap::Parser;
34use tag_tools::{BaseArgs, exit_on_interrupt, init_logging, select_base, state_name};
35use tagcore::Tag;
36use tagcore::proto::{self, Status, TagState};
37
38/// stop, erase, and return a tag to the idle state
39#[derive(Parser)]
40#[command(name = "tag-reset")]
41struct Cli {
42 /// The shared `--debug` and `--base` options.
43 #[command(flatten)]
44 base: BaseArgs,
45
46 /// Synchronize the tag clock from the host once idle
47 #[arg(long)]
48 set_rtc: bool,
49
50 /// Seconds to wait for the erase to finish
51 #[arg(long, default_value_t = 180, value_name = "SECONDS")]
52 reset_timeout: u64,
53
54 /// Seconds to wait for a running tag to reach a stopped state. The stop
55 /// request is posted and acknowledged before the state machine acts on it
56 #[arg(long, default_value_t = 30, value_name = "SECONDS")]
57 stop_timeout: u64,
58
59 /// Seconds to wait for the tag to report a definite state after attach.
60 /// Attach connects under reset, so the first status can legitimately be
61 /// STATE_UNSPECIFIED while the tag boots
62 #[arg(long, default_value_t = 10, value_name = "SECONDS")]
63 settle_timeout: u64,
64}
65
66/// Prints the firmware's debug message, if any, to stderr as is.
67fn report_debug(status: &Status) {
68 if !status.debug_message.is_empty() {
69 eprint!("{}", status.debug_message);
70 }
71}
72
73/// Prints `message` to stderr and returns exit status 1.
74fn fail(message: impl std::fmt::Display) -> ExitCode {
75 eprintln!("{message}");
76 ExitCode::FAILURE
77}
78
79/// Polls status a second apart until the tag reports a state other than
80/// STATE_UNSPECIFIED, which it can legitimately report while booting after
81/// the attach reset. A failed read is fatal: a tag that never answered must
82/// not be mistaken for one that is already idle.
83fn settle(tag: &Tag, timeout_s: u64) -> Result<Status, String> {
84 const SETTLE: Duration = Duration::from_secs(1);
85 for _ in 0..timeout_s {
86 let status = tag
87 .status()
88 .map_err(|_| format!("GetStatus failed: {}", tag.debug_message()))?;
89 if status.state != TagState::StateUnspecified as i32 {
90 return Ok(status);
91 }
92 sleep(SETTLE);
93 }
94 Err(format!(
95 "tag still reports STATE_UNSPECIFIED after {timeout_s} s; it is not merely settling"
96 ))
97}
98
99/// Attaches, settles, stops an acquiring tag, erases a stopped one, checks
100/// it ended IDLE and optionally sets its clock.
101fn main() -> ExitCode {
102 let cli = Cli::parse();
103 init_logging(&cli.base);
104
105 let tag = Tag::new();
106 let dev = select_base::<Cli>(&tag, &cli.base);
107 if !tag.attach(&dev) {
108 // Exiting 0 here would report success from a tool that never
109 // reached the tag.
110 return fail("Attach failed");
111 }
112 exit_on_interrupt();
113
114 let mut status = match settle(&tag, cli.settle_timeout) {
115 Ok(status) => status,
116 Err(message) => return fail(message),
117 };
118 report_debug(&status);
119 println!("{}", proto::debug_string(&status));
120
121 // The states in which the firmware accepts a stop as ending an
122 // acquisition (monitor_acquisition_active() in monitor.c).
123 let acquiring = [
124 TagState::Running,
125 TagState::Hibernating,
126 TagState::Configured,
127 ];
128 if acquiring.iter().any(|s| status.state == *s as i32) {
129 if tag.stop().is_err() {
130 return fail(format!("Stop failed: {}", tag.debug_message()));
131 }
132 // Stop is posted, not performed: poll for the stopped state as
133 // tag-stop does, rather than trust one read straight afterwards.
134 const STOP_POLL: Duration = Duration::from_secs(2);
135 let tries = cli.stop_timeout.div_ceil(2).max(1);
136 let mut stopped = false;
137 for _ in 0..tries {
138 status = match tag.status() {
139 Ok(status) => status,
140 Err(_) => {
141 return fail(format!(
142 "GetStatus after stop failed: {}",
143 tag.debug_message()
144 ));
145 }
146 };
147 if status.state == TagState::Finished as i32 || status.state == TagState::Aborted as i32
148 {
149 stopped = true;
150 break;
151 }
152 sleep(STOP_POLL);
153 }
154 report_debug(&status);
155 if !stopped {
156 return fail(format!(
157 "Tag did not reach a stopped state within {} s; last state {}",
158 cli.stop_timeout,
159 state_name(status.state)
160 ));
161 }
162 }
163
164 println!("State: {}", state_name(status.state));
165 if status.state == TagState::Finished as i32 || status.state == TagState::Aborted as i32 {
166 if tag.erase().is_err() {
167 return fail(format!("Erase failed: {}", tag.debug_message()));
168 }
169
170 // Erase() only posts the work; the state machine then sweeps
171 // external storage in batches whose length depends on the data
172 // present, so poll for IDLE rather than guess a duration.
173 const POLL_MS: u64 = 250;
174 let timeout_ms = cli.reset_timeout * 1000;
175 let mut waited_ms = 0;
176 let mut reached_idle = false;
177 while waited_ms < timeout_ms {
178 sleep(Duration::from_millis(POLL_MS));
179 waited_ms += POLL_MS;
180 let Ok(s) = tag.status() else { continue };
181 status = s;
182 if status.state == TagState::Idle as i32 {
183 reached_idle = true;
184 break;
185 }
186 }
187
188 println!(
189 "State: {} after {:.2} s",
190 state_name(status.state),
191 waited_ms as f64 / 1000.0
192 );
193 report_debug(&status);
194 if !reached_idle {
195 return fail(format!(
196 "Reset did not reach IDLE within {} s; last state {}",
197 cli.reset_timeout,
198 state_name(status.state)
199 ));
200 }
201 }
202
203 // Anything but IDLE here is a reset that did not happen. Checking first
204 // also keeps --set-rtc to IDLE, the only state the firmware accepts it in.
205 if status.state != TagState::Idle as i32 {
206 eprintln!(
207 "Tag is {}; it can be reset only from RUNNING, HIBERNATING, CONFIGURED, FINISHED \
208 or ABORTED",
209 state_name(status.state)
210 );
211 println!("Final state: {}", state_name(status.state));
212 return ExitCode::FAILURE;
213 }
214
215 if cli.set_rtc {
216 if tag.set_rtc().is_ok() {
217 println!("RTC synchronized");
218 } else {
219 // The firmware says why (e.g. NXIO "RTC sync failed while
220 // writing tag clock"), which tells a failed write from a refusal.
221 return fail(format!("SetRtc failed: {}", tag.debug_message()));
222 }
223 }
224
225 println!("Final state: {}", state_name(status.state));
226 ExitCode::SUCCESS
227}