1use std::io::Write as _;
65use std::process::ExitCode;
66use std::thread::sleep;
67use std::time::{Duration, Instant};
68
69use clap::Parser;
70use tag_tools::{BaseArgs, exit_on_interrupt, init_logging, select_base, state_name};
71use tagcore::proto::{self, Ack, Status, TagState, TagType, ack};
72use tagcore::{LinkAdaptStats, Tag, TagMonitorStats};
73use taglogwriter::{LogHeader, LogWriter};
74
75#[derive(Parser)]
77#[command(name = "tag-dwnld")]
78struct Cli {
79 #[command(flatten)]
81 base: BaseArgs,
82
83 #[arg(short, long, value_name = "PATH")]
85 output: Option<String>,
86
87 #[arg(short, long, default_value = "default")]
89 format: String,
90
91 #[arg(short, long)]
93 stop: bool,
94
95 #[arg(long)]
98 rescue_exception: bool,
99
100 #[arg(long)]
102 profile: bool,
103
104 #[arg(long, default_value_t = 10, value_name = "SECONDS")]
108 settle_timeout: u64,
109
110 #[arg(long, default_value_t = 30, value_name = "SECONDS")]
114 stop_timeout: u64,
115}
116
117fn fail(message: impl std::fmt::Display) -> ExitCode {
119 eprintln!("{message}");
120 ExitCode::FAILURE
121}
122
123fn check_format(name: &str) -> Result<(), String> {
130 match name.to_lowercase().as_str() {
131 "default" | "sqlite" | "sql" | "db" | "db3" => Ok(()),
132 "text" | "txt" => Err("The text log format is retired; downloads are written as \
133 SQLite. tag-txt-import converts existing .txt logs."
134 .into()),
135 _ => Err(format!("Unknown output format: {name}")),
136 }
137}
138
139fn settle(tag: &Tag, timeout_s: u64) -> Result<Status, String> {
147 for _ in 0..timeout_s.max(1) {
148 let status = tag
149 .status()
150 .map_err(|_| format!("Could not read tag status: {}", tag.debug_message()))?;
151 if status.state != TagState::StateUnspecified as i32 {
152 return Ok(status);
153 }
154 sleep(Duration::from_secs(1));
155 }
156 Err(format!(
157 "tag still reports STATE_UNSPECIFIED after {timeout_s} s; it is not merely settling"
158 ))
159}
160
161fn stop(tag: &Tag, timeout_s: u64) -> Result<Status, String> {
169 tag.stop()
170 .map_err(|_| format!("Could not stop running tag: {}", tag.debug_message()))?;
171 let mut status = Status::default();
172 for _ in 0..timeout_s.div_ceil(2).max(1) {
173 status = tag.status().map_err(|_| {
174 format!(
175 "Could not read tag status after stop: {}",
176 tag.debug_message()
177 )
178 })?;
179 if status.state == TagState::Finished as i32 || status.state == TagState::Aborted as i32 {
180 return Ok(status);
181 }
182 sleep(Duration::from_secs(2));
183 }
184 Err(format!(
185 "Tag did not reach a stopped state within {timeout_s} s; last state {}",
186 state_name(status.state)
187 ))
188}
189
190fn is_downloadable(state: i32) -> bool {
193 state == TagState::Finished as i32
194 || state == TagState::Aborted as i32
195 || state == TagState::Exception as i32
196}
197
198fn read_header(tag: &Tag) -> Result<LogHeader, String> {
207 let config = tag
208 .config()
209 .map_err(|_| "Could not read tag config".to_string())?;
210 let info = tag
211 .tag_info()
212 .map_err(|_| "Could not read tag info".to_string())?;
213 let mut calibration = Vec::new();
214 while let Ok(constants) = tag.read_calibration(calibration.len() as i32) {
215 calibration.push(constants);
216 }
217 let mut states = Vec::new();
218 while let Ok(chunk) = tag.state_log(states.len() as i32) {
219 if chunk.states.is_empty() {
220 break;
221 }
222 states.extend(chunk.states);
223 }
224 Ok(LogHeader {
225 config,
226 info,
227 calibration,
228 states,
229 })
230}
231
232fn print_progress(current: usize, total: usize) {
234 const WIDTH: usize = 40;
235 let ratio = if total > 0 {
236 (current as f64 / total as f64).min(1.0)
237 } else {
238 1.0
239 };
240 let filled = (ratio * WIDTH as f64) as usize;
241 let bar: String = (0..WIDTH)
242 .map(|i| if i < filled { '#' } else { '-' })
243 .collect();
244 print!(
245 "\r[{bar}] {:>3}% {current}/{total}",
246 (ratio * 100.0) as usize
247 );
248 let _ = std::io::stdout().flush();
249}
250
251fn duration_line(label: &str, total: Duration, count: u64) {
253 let seconds = total.as_secs_f64();
254 if count > 0 {
255 println!(
256 "{label}: {seconds} s total, {} s/request",
257 seconds / count as f64
258 );
259 } else {
260 println!("{label}: {seconds} s");
261 }
262}
263
264fn transfer_line(label: &str, calls: u64, bytes: u64, ns: u64) {
266 let seconds = ns as f64 / 1e9;
267 let mut line = format!("{label}: calls={calls} bytes={bytes} time={seconds} s");
268 if ns > 0 {
269 line += &format!(" rate={} KiB/s", bytes as f64 / 1024.0 / seconds);
270 }
271 println!("{line}");
272}
273
274fn print_profile(
277 link: &LinkAdaptStats,
278 monitor: &TagMonitorStats,
279 get_log: Duration,
280 write_log: Duration,
281 requests: u64,
282) {
283 let ns = |ns: u64| Duration::from_nanos(ns);
284 println!("\nDownload profile");
285 duration_line("GetDataLog wait", get_log, requests);
286 duration_line("Decode/write log", write_log, requests);
287 if !link.enabled || !monitor.enabled {
288 println!("Host link instrumentation was compiled out");
289 return;
290 }
291 println!("\nMonitor RPC");
292 println!(
293 "rpc calls={} request_bytes={} response_bytes={}",
294 monitor.rpc_calls, monitor.request_bytes, monitor.response_bytes
295 );
296 let calls = monitor.rpc_calls;
297 duration_line("rpc total", ns(monitor.rpc_total_ns), calls);
298 duration_line("serialize request", ns(monitor.serialize_ns), calls);
299 duration_line("write request buffer", ns(monitor.write_request_ns), calls);
300 duration_line("monitor call", ns(monitor.monitor_call_ns), calls);
301 duration_line("read response buffer", ns(monitor.read_response_ns), calls);
302 duration_line("parse response", ns(monitor.parse_ns), calls);
303 println!("\nLink transport");
304 transfer_line(
305 "usb out",
306 link.usb_out_calls,
307 link.usb_out_bytes,
308 link.usb_out_ns,
309 );
310 transfer_line(
311 "usb in",
312 link.usb_in_calls,
313 link.usb_in_bytes,
314 link.usb_in_ns,
315 );
316 duration_line("stlink command", ns(link.cmd_ns), link.cmd_calls);
317 transfer_line(
318 "read mem32",
319 link.read_mem_calls,
320 link.read_mem_bytes,
321 link.read_mem_ns,
322 );
323 transfer_line(
324 "write mem32",
325 link.write_mem_calls,
326 link.write_mem_bytes,
327 link.write_mem_ns,
328 );
329 duration_line(
330 "read debug32",
331 ns(link.read_debug_ns),
332 link.read_debug_calls,
333 );
334 duration_line(
335 "write debug32",
336 ns(link.write_debug_ns),
337 link.write_debug_calls,
338 );
339 duration_line("rw status", ns(link.rw_status_ns), link.rw_status_calls);
340}
341
342fn main() -> ExitCode {
344 let cli = Cli::parse();
345 init_logging(&cli.base);
346 taglogwriter::set_debug(cli.base.debug);
347
348 if let Err(message) = check_format(&cli.format) {
351 return fail(message);
352 }
353
354 let tag = Tag::new();
355 let dev = select_base::<Cli>(&tag, &cli.base);
356 if !tag.attach(&dev) {
357 return fail("Attach failed");
358 }
359 exit_on_interrupt();
360
361 let mut status = match settle(&tag, cli.settle_timeout) {
362 Ok(status) => status,
363 Err(message) => return fail(message),
364 };
365 if cli.base.debug && !status.debug_message.is_empty() {
366 eprint!("{}", status.debug_message);
367 }
368
369 let rescue = cli.rescue_exception;
370 if status.state == TagState::Exception as i32 && rescue {
371 eprintln!("Rescue: changing monitor-visible state from EXCEPTION to ABORTED");
375 if !tag.force_backup_state(TagState::Aborted) {
376 return fail(format!(
377 "Could not rescue exception state: {}",
378 tag.debug_message()
379 ));
380 }
381 status.state = TagState::Aborted as i32;
382 eprintln!("Rescue: backup state verified; continuing with pre-rescue log counts");
383 }
384
385 if status.state == TagState::Running as i32 && cli.stop {
386 status = match stop(&tag, cli.stop_timeout) {
387 Ok(status) => status,
388 Err(message) => return fail(message),
389 };
390 }
391
392 if !is_downloadable(status.state) {
393 eprintln!("Can't dump logs from current state");
394 eprintln!("{}", proto::debug_string(&status));
395 return ExitCode::FAILURE;
396 }
397
398 let config = match tag.config() {
399 Ok(config) => config,
400 Err(_) => {
401 return fail(format!(
402 "Could not read tag config: {}",
403 tag.debug_message()
404 ));
405 }
406 };
407 let output = cli.output.unwrap_or_else(|| "tag-download.db3".into());
408 let mut writer = match LogWriter::create(output.as_ref(), &config, true) {
411 Ok(writer) => writer,
412 Err(error) => return fail(format!("Could not open output: {error}")),
413 };
414 println!("Writing sqlite log to {output}");
415
416 let header = match read_header(&tag) {
417 Ok(header) => header,
418 Err(message) => return fail(format!("Could not write log header: {message}")),
419 };
420 if let Err(error) = writer.write_header(&header) {
421 return fail(format!("Could not write log header: {error}"));
422 }
423
424 let bittag_rescue = rescue && config.tag_type == TagType::Bittag as i32;
425 let mut max_count = if status.external_data_count > 0 {
426 status.external_data_count as usize
427 } else {
428 status.internal_data_count.max(0) as usize
429 };
430 if bittag_rescue && max_count == 0 {
431 match tag.count_bittag_log_headers() {
434 Some(count) => max_count = count,
435 None => return fail("Could not scan BitTag log headers"),
436 }
437 eprintln!("Rescue: found {max_count} BitTag log records");
438 }
439 if max_count == 0 {
440 println!("No log records to download");
441 return ExitCode::SUCCESS;
442 }
443
444 let (mut total, mut requests) = (0usize, 0u64);
445 let (mut get_log, mut write_log) = (Duration::ZERO, Duration::ZERO);
446 print_progress(total, max_count);
447 tag.reset_link_stats();
448 tag.reset_monitor_stats();
449
450 let start = Instant::now();
451 if let Err(error) = writer.begin_log() {
452 println!();
453 return fail(format!("Could not begin log download: {error}"));
454 }
455 write_log += start.elapsed();
456
457 loop {
458 let start = Instant::now();
459 let page: Option<Ack> = if bittag_rescue {
462 tag.bittag_log_from_flash(total as i32)
463 } else {
464 tag.data_log(total as i32).ok()
465 };
466 let Some(page) = page else {
467 println!();
468 return fail(format!(
469 "GetDataLog failed at entry {total}: {}",
470 tag.debug_message()
471 ));
472 };
473 get_log += start.elapsed();
474 requests += 1;
475
476 if !page.error_message().is_empty() {
477 println!();
478 return fail(page.error_message());
479 }
480 if page.err == ack::Err::Nodata as i32 && total < max_count {
481 total += 1;
482 print_progress(total, max_count);
483 continue;
484 }
485
486 let start = Instant::now();
487 let consumed = match writer.write_log(&page) {
488 Ok(consumed) => consumed,
489 Err(error) => {
490 println!();
491 return fail(format!("Could not write log data: {error}"));
492 }
493 };
494 write_log += start.elapsed();
495 if consumed == 0 {
496 break;
497 }
498 total += consumed;
499 print_progress(total, max_count);
500 }
501
502 let start = Instant::now();
503 if let Err(error) = writer.end_log() {
504 println!();
505 return fail(format!("Could not finish log download: {error}"));
506 }
507 write_log += start.elapsed();
508
509 println!();
510 println!("Downloaded {total} records in {requests} requests");
511 println!("Decoding time {} seconds", write_log.as_secs_f64());
512 if cli.profile {
513 print_profile(
514 &tag.link_stats(),
515 &tag.monitor_stats(),
516 get_log,
517 write_log,
518 requests,
519 );
520 }
521 ExitCode::SUCCESS
522}