Skip to main content

tag_start/
tag-start.rs

1//! `tag-start`: program a configuration into an IDLE tag and start it.
2//!
3//! Port of `host/commandline/tag-start.cc`. Waits for the tag to report a
4//! definite state, optionally syncs its clock (`--set-rtc`), then starts from
5//! the configuration the tag already holds. `--config` replaces it with a
6//! JSON file, or with `--merge` overlays the file using
7//! [`ConfigShape::merge`]: only fields the tag supports and the file speaks
8//! for are copied, zeros included. `--start-now` forces `start_delay` to 0.
9//! It then polls until the tag leaves IDLE and prints the resulting status,
10//! since an acknowledged start is not proof the tag started.
11//!
12//! Exits 0 when the tag reached RUNNING or CONFIGURED, was already in one of
13//! them (no start is sent), or accepted the start but answered no status read
14//! within `--start-timeout` (reported as "not confirmed"); 1 otherwise.
15//!
16//! Usage: `tag-start [-d] [-b BUS:DEVICE] [-c|--config FILE [--merge]]
17//! [--set-rtc] [--start-now] [--start-timeout S] [--settle-timeout S]`
18//! (both timeouts default to 10 s; status is polled once a second). The
19//! configuration actually sent is printed as protobuf text before the
20//! start. A non-IDLE tag other than RUNNING or CONFIGURED is refused with
21//! exit 1, as is `--set-rtc` on a tag that is not IDLE. Option errors exit 2
22//! (clap).
23//!
24//! # Hardware side effects
25//!
26//! The attach resets the tag. `--set-rtc` overwrites its clock with the
27//! host's. The start writes the configuration to the tag and begins a run
28//! (CONFIGURED until `start_delay` elapses, then RUNNING), which the tool
29//! does not end: the tag is left running when it exits. After an accepted
30//! start, library link errors are silenced unless `--debug`, because a tag
31//! whose next state sleeps at once drops the debug link.
32//!
33//! This does not erase a previous log: a tag holding data is FINISHED or
34//! ABORTED, not IDLE, and must be reset with `tag-reset` first.
35
36use std::process::ExitCode;
37use std::thread::sleep;
38use std::time::Duration;
39
40use clap::Parser;
41use log::LevelFilter;
42use tag_tools::{
43    BaseArgs, exit_on_interrupt, init_logging, select_base, set_log_level, state_name,
44};
45use tagcore::Tag;
46use tagcore::configshape::ConfigShape;
47use tagcore::proto::{self, Config, Status, TagState, TagType};
48
49/// start a configured tag and print the resulting status
50#[derive(Parser)]
51#[command(name = "tag-start")]
52struct Cli {
53    /// The shared `--debug` and `--base` options.
54    #[command(flatten)]
55    base: BaseArgs,
56
57    /// Program this protobuf-JSON configuration before starting
58    #[arg(short, long, value_name = "FILE")]
59    config: Option<String>,
60
61    /// Overlay --config onto the tag's stored configuration rather than
62    /// replacing it: only fields the tag supports are copied
63    #[arg(long)]
64    merge: bool,
65
66    /// Synchronize the tag clock from the host before starting
67    #[arg(long)]
68    set_rtc: bool,
69
70    /// Force start_delay to zero so collection begins immediately
71    #[arg(long)]
72    start_now: bool,
73
74    /// Seconds to wait for the tag to leave IDLE after the start is accepted.
75    /// Start is posted, not performed: the state machine acts on it later, so
76    /// a status read straight afterwards can still say IDLE
77    #[arg(long, default_value_t = 10, value_name = "SECONDS")]
78    start_timeout: u64,
79
80    /// Seconds to wait after attach for the tag to report a definite state.
81    /// Attach connects under reset, so the first status can legitimately be
82    /// STATE_UNSPECIFIED while the tag boots
83    #[arg(long, default_value_t = 10, value_name = "SECONDS")]
84    settle_timeout: u64,
85}
86
87/// Interval between status polls while settling and while waiting for the
88/// tag to leave IDLE.
89const POLL: Duration = Duration::from_secs(1);
90
91/// Loads a Config from a protobuf-JSON file: replacing `cfg`, or with
92/// `merge` overlaying only the fields the tag supports and the file speaks
93/// for ([`ConfigShape::for_file`], so a stated zero is copied).
94fn load_config_json(path: &str, cfg: &mut Config, merge: bool) -> Result<(), String> {
95    let text =
96        std::fs::read_to_string(path).map_err(|_| format!("Cannot open config file: {path}"))?;
97    let parsed: Config =
98        proto::from_json(&text).map_err(|e| format!("Config JSON did not parse: {e}"))?;
99    if merge {
100        let file_shape =
101            ConfigShape::for_file(&text).map_err(|e| format!("Config JSON did not parse: {e}"))?;
102        let tag_type = TagType::try_from(cfg.tag_type).unwrap_or(TagType::TagUnspecified);
103        ConfigShape::for_tag(tag_type).merge(cfg, &parsed, &file_shape);
104    } else {
105        *cfg = parsed;
106    }
107    Ok(())
108}
109
110/// Polls status until the tag reports a state other than STATE_UNSPECIFIED,
111/// which it can legitimately report while booting after the attach reset.
112fn settle(tag: &Tag, timeout_s: u64) -> Result<Status, String> {
113    let tries = timeout_s.max(1);
114    for _ in 0..tries {
115        let status = tag
116            .status()
117            .map_err(|_| format!("GetStatus failed: {}", tag.debug_message()))?;
118        if status.state != TagState::StateUnspecified as i32 {
119            return Ok(status);
120        }
121        sleep(POLL);
122    }
123    Err(format!(
124        "tag still reports STATE_UNSPECIFIED after {timeout_s} s; it is not merely settling"
125    ))
126}
127
128/// Prints `message` to stderr and returns exit status 1.
129fn fail(message: impl std::fmt::Display) -> ExitCode {
130    eprintln!("{message}");
131    ExitCode::FAILURE
132}
133
134/// Attaches, settles, optionally sets the clock, starts an IDLE tag with the
135/// chosen configuration and polls until it leaves IDLE.
136fn main() -> ExitCode {
137    let cli = Cli::parse();
138    init_logging(&cli.base);
139
140    let tag = Tag::new();
141    let dev = select_base::<Cli>(&tag, &cli.base);
142    if !tag.attach(&dev) {
143        return fail("Attach failed");
144    }
145    exit_on_interrupt();
146
147    // Read status before syncing the clock: the firmware accepts set_rtc
148    // only in IDLE, and rejects one sent as the first request after attach.
149    let mut status = match settle(&tag, cli.settle_timeout) {
150        Ok(status) => status,
151        Err(message) => return fail(message),
152    };
153    let idle = TagState::Idle as i32;
154
155    if cli.set_rtc {
156        if status.state != idle {
157            return fail(format!(
158                "SetRtc skipped: tag is {}, the tag accepts a clock sync only when IDLE",
159                state_name(status.state)
160            ));
161        }
162        if tag.set_rtc().is_err() {
163            let why = tag.debug_message();
164            return fail(if why.is_empty() {
165                "SetRtc failed".to_owned()
166            } else {
167                format!("SetRtc failed: {why}")
168            });
169        }
170        println!("RTC synchronized");
171    }
172
173    let mut start_attempted = false;
174    let mut start_failed = false;
175    if status.state == idle {
176        // Starting from a default config would run settings nobody chose.
177        let mut cfg = match tag.config() {
178            Ok(cfg) => cfg,
179            Err(_) => return fail(format!("GetConfig failed: {}", tag.debug_message())),
180        };
181        if let Some(path) = &cli.config
182            && let Err(message) = load_config_json(path, &mut cfg, cli.merge)
183        {
184            return fail(message);
185        }
186        if cli.start_now {
187            cfg.start_delay = 0;
188        }
189        print!(
190            "Starting with configuration:\n{}",
191            proto::debug_string(&cfg)
192        );
193        start_attempted = true;
194        if tag.start(&cfg).is_ok() {
195            // From here the tag may leave the debug link at any moment (a tag
196            // whose next state sleeps at once must), so the link errors
197            // tagcore would log are expected; keep them unless --debug.
198            if !cli.base.debug {
199                set_log_level(LevelFilter::Off);
200            }
201        } else {
202            start_failed = true;
203            let message = tag.debug_message();
204            if message.is_empty() {
205                println!("Start failed");
206            } else {
207                println!("Start failed: {message}");
208            }
209        }
210    } else if status.state == TagState::Running as i32
211        || status.state == TagState::Configured as i32
212    {
213        println!("Start skipped: tag is already {}", state_name(status.state));
214    } else {
215        // A skipped start is not a success for a tool whose job is to start.
216        eprintln!(
217            "Start skipped: tag is {}; a start is accepted only in IDLE",
218            state_name(status.state)
219        );
220        println!("State: {}", state_name(status.state));
221        return ExitCode::FAILURE;
222    }
223
224    if start_attempted && start_failed {
225        // The tag refused the start ("Start failed" above).
226        println!("Last known state: {}", state_name(status.state));
227        return ExitCode::FAILURE;
228    }
229
230    // Start() returns when the request is accepted; the state machine acts
231    // on it later, so poll until the tag has left IDLE. A failed read is not
232    // a reason to stop: it is a tag that did not answer in time, which is
233    // what the remaining tries are for.
234    let tries = if start_attempted {
235        cli.start_timeout.max(1)
236    } else {
237        1
238    };
239    let mut read_ok = false;
240    for _ in 0..tries {
241        match tag.status() {
242            Ok(s) => {
243                status = s;
244                read_ok = true;
245                if !start_attempted || status.state != idle {
246                    break;
247                }
248            }
249            // A read that fails after one that still said IDLE is the normal
250            // case for a tag that sleeps as soon as it starts (UIUCTag): to
251            // sleep it gives up its debug interface, so later reads fail. The
252            // last read decides, so this ends as "not confirmed" and exit 0,
253            // as the C++ does (host/docs/src/cli/tag-start.md).
254            Err(_) => read_ok = false,
255        }
256        sleep(POLL);
257    }
258
259    if !read_ok && start_attempted {
260        println!(
261            "Start accepted, but no status read succeeded in {} s, so the tag's new state was \
262             not confirmed",
263            cli.start_timeout
264        );
265        println!(
266            "State: not confirmed (last read: {})",
267            state_name(status.state)
268        );
269        return ExitCode::SUCCESS;
270    }
271    if !read_ok {
272        eprintln!("GetStatus failed: {}", tag.debug_message());
273        println!("Last known state: {}", state_name(status.state));
274        return ExitCode::FAILURE;
275    }
276    if start_attempted && status.state == idle {
277        eprintln!(
278            "Tag did not leave IDLE within {} s of the start being accepted",
279            cli.start_timeout
280        );
281        println!("State: {}", state_name(status.state));
282        return ExitCode::FAILURE;
283    }
284    println!("State: {}", state_name(status.state));
285    // Leaving IDLE is not the same as having started: a start that fails in
286    // device setup goes IDLE -> ABORTED.
287    if start_attempted
288        && status.state != TagState::Running as i32
289        && status.state != TagState::Configured as i32
290    {
291        return fail(format!(
292            "Start did not lead to RUNNING or CONFIGURED: {}",
293            state_name(status.state)
294        ));
295    }
296    ExitCode::SUCCESS
297}