1use std::process::ExitCode;
33use std::thread::sleep;
34use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
36use clap::Parser;
37use tag_tools::{BaseArgs, enum_name, exit_on_interrupt, init_logging, select_base, state_name};
38use tagcore::Tag;
39use tagcore::proto::{TagState, TestReq, TestResult, state};
40
41#[derive(Parser)]
43#[command(name = "tag-test")]
44struct Cli {
45 #[command(flatten)]
47 base: BaseArgs,
48
49 #[arg(short, long, default_value = "RUN_ALL")]
51 test: String,
52
53 #[arg(long, default_value_t = 30, value_name = "SECONDS")]
57 test_timeout: i32,
58}
59
60fn parse_c_int(text: &str) -> Option<i32> {
64 let s = text.trim_start();
65 let (negative, s) = match s.as_bytes().first() {
66 Some(b'-') => (true, &s[1..]),
67 Some(b'+') => (false, &s[1..]),
68 _ => (false, s),
69 };
70 let (radix, digits) = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
71 (16, hex)
72 } else if s.len() > 1 && s.starts_with('0') {
73 (8, &s[1..])
74 } else {
75 (10, s)
76 };
77 if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
78 return None;
79 }
80 let magnitude = i64::from_str_radix(digits, radix).ok()?;
81 i32::try_from(if negative { -magnitude } else { magnitude }).ok()
82}
83
84fn parse_test_req(name: &str) -> Option<TestReq> {
86 TestReq::from_str_name(name)
87 .or_else(|| parse_c_int(name).and_then(|v| TestReq::try_from(v).ok()))
88}
89
90fn rtc_drift(tag: &Tag) -> Option<f32> {
93 let status = tag.status().ok()?;
94 if !status.debug_message.is_empty() {
95 eprint!("{}", status.debug_message);
96 }
97 let now_ms = SystemTime::now()
98 .duration_since(UNIX_EPOCH)
99 .unwrap_or_default()
100 .as_millis() as i64;
101 Some(((status.millis - now_ms) as f64 / 1000.0) as f32)
102}
103
104fn main() -> ExitCode {
107 let cli = Cli::parse();
108 init_logging(&cli.base);
109
110 let tag = Tag::new();
111 let dev = select_base::<Cli>(&tag, &cli.base);
112 let Some(requested_test) = parse_test_req(&cli.test) else {
113 eprintln!("Invalid test: {}", cli.test);
114 return ExitCode::FAILURE;
115 };
116 if !tag.attach(&dev) {
117 eprintln!("Attach failed");
118 return ExitCode::FAILURE;
119 }
120 exit_on_interrupt();
121
122 let info = match tag.tag_info() {
123 Ok(info) => info,
124 Err(_) => {
125 eprintln!("GetTagInfo failed: {}", tag.debug_message());
126 return ExitCode::FAILURE;
127 }
128 };
129
130 println!("Tag Type: {}", info.tag_type);
133 println!("Board name: {}", info.board_desc);
134 println!("Build time and date: {}", info.build_time);
135 println!("Internal Flash size: {}kb", info.intflashsz);
136 println!("External Flash size: {}kb", info.extflashsz);
137 println!("Repo: {}", info.gitrepo);
138 println!("Repo hash: {}", info.githash);
139 println!("UUID: {}", info.uuid);
140 if let Some(v) = tag.voltage() {
141 println!("Voltage: {v:.2}");
142 }
143
144 if let Some(f) = rtc_drift(&tag) {
145 println!("Initial Clock drift: {f:.2}");
146 }
147 println!("# Checking RTC (2 second delay)");
148 if tag.set_rtc().is_err() {
149 eprintln!("SetRtc failed: {}", tag.debug_message());
150 return ExitCode::FAILURE;
151 }
152 sleep(Duration::from_millis(2000));
153 if let Some(f) = rtc_drift(&tag) {
154 println!("Clock drift after setting: {f:.2}");
155 }
156
157 let mut status = match tag.status() {
158 Ok(status) => status,
159 Err(_) => {
160 eprintln!("GetStatus failed: {}", tag.debug_message());
161 return ExitCode::FAILURE;
162 }
163 };
164 if !status.debug_message.is_empty() {
165 eprint!("{}", status.debug_message);
166 }
167 println!("State: {}", state_name(status.state));
168
169 let mut next = 0;
170 while let Ok(log) = tag.state_log(next) {
171 if log.states.is_empty() {
172 break;
173 }
174 next += log.states.len() as i32;
175 for marker in &log.states {
176 let s = marker.status.clone().unwrap_or_default();
177 println!("State: {}", state_name(s.state));
178 println!(
179 "{}",
180 enum_name(marker.transition_reason, state::Event::as_str_name)
181 );
182 }
183 }
184
185 if status.state != TagState::Idle as i32 {
186 println!("# State not idle ! Exiting");
188 return ExitCode::FAILURE;
189 }
190
191 println!("# Running test {}", requested_test.as_str_name());
192 if tag.test(requested_test).is_err() {
195 eprintln!("Test request failed: {}", tag.debug_message());
196 return ExitCode::FAILURE;
197 }
198 let test_poll_s = cli.test_timeout;
201 let mut status_read = false;
202 let mut completed = false;
203 for _ in 0..test_poll_s.max(0) {
204 sleep(Duration::from_millis(1000));
205 match tag.status() {
206 Ok(s) => status = s,
207 Err(_) => {
208 eprintln!("GetStatus failed: {}", tag.debug_message());
209 continue;
210 }
211 }
212 status_read = true;
213 if !status.debug_message.is_empty() {
214 eprint!("{}", status.debug_message);
215 }
216 if status.state == TagState::Idle as i32 {
217 completed = true;
218 break;
219 }
220 }
221 if !status_read {
222 eprintln!("No status was read after the test; not reporting a result");
223 return ExitCode::FAILURE;
224 }
225 if !completed {
226 eprintln!(
227 "Test did not finish within {test_poll_s} s; last state {}, no result to report. Use \
228 --test-timeout to allow longer.",
229 state_name(status.state)
230 );
231 return ExitCode::FAILURE;
232 }
233 println!(
234 "Test Result: {}",
235 enum_name(status.test_status, TestResult::as_str_name)
236 );
237 if tag.set_rtc().is_err() {
238 eprintln!("SetRtc after test failed: {}", tag.debug_message());
240 return ExitCode::FAILURE;
241 }
242 if status.test_status != TestResult::AllPassed as i32 {
244 eprintln!(
245 "Self-test failed: {}",
246 enum_name(status.test_status, TestResult::as_str_name)
247 );
248 return ExitCode::FAILURE;
249 }
250 ExitCode::SUCCESS
251}
252
253#[cfg(test)]
254mod tests {
256 use super::*;
257
258 #[test]
259 fn test_req_parses_names_and_numbers() {
262 assert_eq!(parse_test_req("RUN_ALL"), Some(TestReq::RunAll));
263 assert_eq!(parse_test_req("2"), Some(TestReq::RunRtc));
264 assert_eq!(parse_test_req("0x0e"), Some(TestReq::RunMx25u12843));
265 assert_eq!(parse_test_req("016"), Some(TestReq::RunMx25u12843));
266 assert_eq!(parse_test_req("9"), None);
267 assert_eq!(parse_test_req("2x"), None);
268 assert_eq!(parse_test_req("bogus"), None);
269 assert_eq!(parse_test_req("0x-2"), None);
270 }
271}