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}