Skip to main content

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}