1use clap::{builder::ValueHint, Subcommand, ValueEnum};
2use clap_complete::Shell;
3
4#[derive(Parser, Debug)]
5#[command(
6 name = "tio",
7 version,
8 about = "Twinleaf sensor management and data logging tool",
9)]
10pub struct TioCli {
11 #[command(subcommand)]
12 pub command: Commands,
13}
14
15#[derive(Subcommand, Debug)]
16pub enum Commands {
17 Proxy(ProxyCli),
18 Monitor {
20 #[command(flatten)]
21 tio: TioOpts,
22 #[arg(short = 'a', long = "all")]
23 all: bool,
24 #[arg(long = "fps", default_value_t = 20)]
25 fps: u32,
26 #[arg(short = 'c', long = "colors")]
27 colors: Option<String>,
28 },
29 Health(HealthCli),
30 NmeaProxy{
32 #[command(flatten)]
33 tio: TioOpts,
34
35 #[arg(
36 short = 'p',
37 long = "port",
38 default_value = "7800",
39 help = "TCP port to listen on"
40 )]
41 tcp_port: u16,
42 },
43
44 #[command(args_conflicts_with_subcommands = true)]
45 Rpc {
47 #[command(flatten)]
48 tio: TioOpts,
49
50 #[command(subcommand)]
51 subcommands: Option<RPCSubcommands>,
52
53 #[arg(value_hint = ValueHint::Other)]
55 rpc_name: Option<String>,
56
57 #[arg(
59 allow_negative_numbers = true,
60 value_name = "ARG",
61 value_hint = ValueHint::Other,
62 help_heading = "RPC Arguments"
63 )]
64 rpc_arg: Option<String>,
65
66 #[arg(short = 't', long = "req-type", help_heading = "Type Options")]
68 req_type: Option<String>,
69
70 #[arg(short = 'T', long = "rep-type", help_heading = "Type Options")]
72 rep_type: Option<String>,
73
74 #[arg(short = 'd', long)]
76 debug: bool,
77 },
78
79 #[command(args_conflicts_with_subcommands = true)]
80 Log {
82 #[command(flatten)]
83 tio: TioOpts,
84
85 #[command(subcommand)]
86 subcommands: Option<LogSubcommands>,
87
88 #[arg(short = 'f', default_value_t = default_log_path())]
90 file: String,
91
92 #[arg(short = 'u')]
94 unbuffered: bool,
95
96 #[arg(long)]
98 raw: bool,
99
100 #[arg(long = "depth")]
102 depth: Option<usize>,
103 },
104 MetaReroute {
106 input: String,
108
109 #[arg(short = 's', long = "sensor")]
111 route: String,
112
113 #[arg(short = 'o', long = "output")]
115 output: Option<String>,
116 },
117 #[command(args_conflicts_with_subcommands = true)]
118 Dump {
120 #[command(flatten)]
121 tio: TioOpts,
122
123 #[command(subcommand)]
124 subcommands: Option<DumpSubcommands>,
125
126 #[arg(short = 'd', long = "data")]
128 data: bool,
129
130 #[arg(short = 'm', long = "meta")]
132 meta: bool,
133
134 #[arg(long = "depth")]
136 depth: Option<usize>,
137 },
138 FirmwareUpgrade {
140 #[command(flatten)]
141 tio: TioOpts,
142
143 #[arg(value_hint = ValueHint::FilePath)]
145 firmware_path: String,
146
147 #[arg(short = 'y', long = "yes")]
149 yes: bool,
150 },
151 #[command(long_about = "\
153Generate shell completions for tio.
154
155Add one of these lines to your shell's config file:
156
157 Bash (~/.bashrc):
158 eval \"$(tio completions bash)\"
159
160 Zsh (~/.zshrc):
161 eval \"$(tio completions zsh)\"
162
163 Fish (~/.config/fish/config.fish):
164 tio completions fish | source
165
166 PowerShell ($PROFILE):
167 tio completions powershell | Invoke-Expression")]
168 Completions {
169 #[arg(value_enum)]
170 shell: Shell,
171 },
172}
173
174#[derive(Subcommand, Debug)]
175pub enum RPCSubcommands{
176 List {
178 #[command(flatten)]
179 tio: TioOpts,
180 },
181 Dump {
183 #[command(flatten)]
184 tio: TioOpts,
185
186 #[arg(value_hint = ValueHint::Other)]
188 rpc_name: String,
189
190 #[arg(long)]
192 capture: bool,
193 },
194}
195
196#[derive(Subcommand, Debug)]
197pub enum LogSubcommands{
198 Metadata {
200 #[command(flatten)]
201 tio: TioOpts,
202
203 #[arg(short = 'f', default_value = "meta.tio")]
205 file: String,
206 },
207
208 Dump {
210 files: Vec<String>,
212
213 #[arg(short = 'd', long = "data")]
215 data: bool,
216
217 #[arg(short = 'm', long = "meta")]
219 meta: bool,
220
221 #[arg(short = 's', long = "sensor", default_value = "/")]
223 sensor: String,
224
225 #[arg(long = "depth")]
227 depth: Option<usize>,
228 },
229
230 #[command(hide = true)]
232 DataDump {
233 files: Vec<String>,
235 },
236
237 Csv {
239 args: Vec<String>,
241
242 #[arg(short = 's')]
244 sensor: Option<String>,
245
246 #[arg(short = 'o')]
248 output: Option<String>,
249 },
250
251 Hdf {
253 files: Vec<String>,
255
256 #[arg(short = 'o')]
258 output: Option<String>,
259
260 #[arg(short = 'g', long = "glob")]
262 filter: Option<String>,
263
264 #[arg(short = 'c', long = "compress")]
266 compress: bool,
267
268 #[arg(short = 'd', long)]
270 debug: bool,
271
272 #[arg(short = 'l', long = "split", default_value = "none")]
274 split_level: SplitLevel,
275
276 #[arg(short = 'p', long = "policy", default_value = "continuous")]
278 split_policy: SplitPolicy,
279 },
280}
281
282#[derive(Subcommand, Debug)]
283pub enum DumpSubcommands{
284 #[command(hide = true)]
286 Data {
287 #[command(flatten)]
288 tio: TioOpts,
289 },
290
291 #[command(hide = true)]
293 DataAll {
294 #[command(flatten)]
295 tio: TioOpts,
296 },
297
298 #[command(hide = true)]
300 Meta {
301 #[command(flatten)]
302 tio: TioOpts,
303 },
304}
305
306fn default_log_path() -> String {
307 chrono::Local::now()
308 .format("log.%Y%m%d-%H%M%S.tio")
309 .to_string()
310}
311
312#[derive(ValueEnum, Clone, Debug, Default)]
314pub enum SplitPolicy {
315 #[default]
317 Continuous,
318 Monotonic,
320}
321
322#[cfg(feature = "hdf5")]
323impl From<SplitPolicy> for twinleaf::data::export::SplitPolicy {
324 fn from(policy: SplitPolicy) -> Self {
325 match policy {
326 SplitPolicy::Continuous => Self::Continuous,
327 SplitPolicy::Monotonic => Self::Monotonic,
328 }
329 }
330}
331
332#[derive(ValueEnum, Clone, Debug, Default)]
334pub enum SplitLevel {
335 #[default]
337 None,
338 Stream,
340 Device,
342 Global,
344}
345
346#[cfg(feature = "hdf5")]
347impl From<SplitLevel> for twinleaf::data::export::RunSplitLevel {
348 fn from(level: SplitLevel) -> Self {
349 match level {
350 SplitLevel::None => Self::None,
351 SplitLevel::Stream => Self::PerStream,
352 SplitLevel::Device => Self::PerDevice,
353 SplitLevel::Global => Self::Global,
354 }
355 }
356}
357
358
359#[derive(Parser, Debug, Clone)]
360#[command(
361 name = "tio-health",
362 version,
363 about = "Live timing & rate diagnostics for TIO (Twinleaf) devices"
364)]
365pub struct HealthCli {
366 #[command(flatten)]
367 tio: TioOpts,
368
369 #[arg(
371 long = "jitter-window",
372 default_value = "10",
373 value_name = "SECONDS",
374 value_parser = clap::value_parser!(u64).range(1..),
375 help = "Seconds for jitter calculation window (>= 1)"
376 )]
377 jitter_window: u64,
378
379 #[arg(
381 long = "ppm-warn",
382 default_value = "100",
383 value_name = "PPM",
384 value_parser = nonneg_f64,
385 help = "Warning threshold in parts per million (>= 0)"
386 )]
387 ppm_warn: f64,
388
389 #[arg(
391 long = "ppm-err",
392 default_value = "200",
393 value_name = "PPM",
394 value_parser = nonneg_f64,
395 help = "Error threshold in parts per million (>= 0)"
396 )]
397 ppm_err: f64,
398
399 #[arg(
401 long = "streams",
402 value_delimiter = ',',
403 value_name = "IDS",
404 value_parser = clap::value_parser!(u8),
405 help = "Comma-separated stream IDs to monitor (e.g., 0,1,5)"
406 )]
407 streams: Option<Vec<u8>>,
408
409 #[arg(short = 'q', long = "quiet")]
411 quiet: bool,
412
413 #[arg(
415 long = "fps",
416 default_value = "30",
417 value_name = "FPS",
418 value_parser = clap::value_parser!(u64).range(1..=60),
419 help = "UI refresh rate for heartbeat animation and stale detection (1–60)"
420 )]
421 fps: u64,
422
423 #[arg(
425 long = "stale-ms",
426 default_value = "2000",
427 value_name = "MS",
428 value_parser = clap::value_parser!(u64).range(1..),
429 help = "Mark streams as stale after this many milliseconds without data (>= 1)"
430 )]
431 stale_ms: u64,
432
433 #[arg(
435 short = 'n',
436 long = "event-log-size",
437 default_value = "100",
438 value_name = "N",
439 value_parser = clap::value_parser!(u64).range(1..),
440 help = "Maximum number of events to keep in history (>= 1)"
441 )]
442 event_log_size: u64,
443
444 #[arg(
446 long = "event-display-lines",
447 default_value = "8",
448 value_name = "LINES",
449 value_parser = clap::value_parser!(u16).range(3..),
450 help = "Number of event lines to show (>= 3)"
451 )]
452 event_display_lines: u16,
453
454 #[arg(short = 'w', long = "warnings-only")]
456 warnings_only: bool,
457}
458
459impl HealthCli {
460 fn stale_dur(&self) -> Duration {
461 Duration::from_millis(self.stale_ms)
462 }
463}
464
465fn nonneg_f64(s: &str) -> Result<f64, String> {
466 let v: f64 = s
467 .parse()
468 .map_err(|e: std::num::ParseFloatError| e.to_string())?;
469 if v < 0.0 {
470 Err("must be ≥ 0".into())
471 } else {
472 Ok(v)
473 }
474}
475
476#[derive(Parser, Debug)]
477#[command(
478 name = "tio-proxy",
479 version,
480 about = "Multiplexes access to a sensor, exposing the functionality of tio::proxy via TCP"
481)]
482pub struct ProxyCli {
483 sensor_url: Option<String>,
486
487 #[arg(short = 'p', long = "port", default_value = "7855")]
489 port: u16,
490
491 #[arg(short = 'k', long)]
493 kick_slow: bool,
494
495 #[arg(short = 's', long = "subtree", default_value = "/")]
497 subtree: String,
498
499 #[arg(short = 'v', long)]
501 verbose: bool,
502
503 #[arg(short = 'd', long)]
505 debug: bool,
506
507 #[arg(short = 't', long = "timestamp", default_value = "%T%.3f ")]
509 timestamp_format: String,
510
511 #[arg(short = 'T', long = "timeout", default_value = "30")]
513 reconnect_timeout: u64,
514
515 #[arg(long)]
517 dump: bool,
518
519 #[arg(long)]
521 dump_data: bool,
522
523 #[arg(long)]
525 dump_meta: bool,
526
527 #[arg(long)]
529 dump_hb: bool,
530
531 #[arg(short = 'a', long = "auto")]
532 auto: bool,
533
534 #[arg(short = 'e', long = "enumerate", name = "enum")]
536 enumerate: bool,
537}