1use clap::{ValueEnum, Subcommand};
2
3#[derive(Parser, Debug)]
4#[command(
5 name = "tio",
6 version,
7 about = "Twinleaf sensor management and data logging tool",
8)]
9pub struct TioCli {
10 #[command(subcommand)]
11 pub command: Commands,
12}
13
14#[derive(Subcommand, Debug)]
15pub enum Commands {
16 Proxy(ProxyCli),
17 Monitor {
19 #[command(flatten)]
20 tio: TioOpts,
21 #[arg(short = 'a', long = "all")]
22 all: bool,
23 #[arg(long = "fps", default_value_t = 20)]
24 fps: u32,
25 #[arg(short = 'c', long = "colors")]
26 colors: Option<String>,
27 },
28 Health(HealthCli),
29 NmeaProxy{
31 #[command(flatten)]
32 tio: TioOpts,
33
34 #[arg(
35 short = 'p',
36 long = "port",
37 default_value = "7800",
38 help = "TCP port to listen on"
39 )]
40 tcp_port: u16,
41 },
42
43 #[command(args_conflicts_with_subcommands = true)]
44 Rpc {
46 #[command(flatten)]
47 tio: TioOpts,
48
49 #[command(subcommand)]
50 subcommands: Option<RPCSubcommands>,
51
52 rpc_name: Option<String>,
54
55 #[arg(
57 allow_negative_numbers = true,
58 value_name = "ARG",
59 help_heading = "RPC Arguments"
60 )]
61 rpc_arg: Option<String>,
62
63 #[arg(short = 't', long = "req-type", help_heading = "Type Options")]
65 req_type: Option<String>,
66
67 #[arg(short = 'T', long = "rep-type", help_heading = "Type Options")]
69 rep_type: Option<String>,
70
71 #[arg(short = 'd', long)]
73 debug: bool,
74 },
75
76 #[command(args_conflicts_with_subcommands = true)]
77 Log {
79 #[command(flatten)]
80 tio: TioOpts,
81
82 #[command(subcommand)]
83 subcommands: Option<LogSubcommands>,
84
85 #[arg(short = 'f', default_value_t = default_log_path())]
87 file: String,
88
89 #[arg(short = 'u')]
91 unbuffered: bool,
92
93 #[arg(long)]
95 raw: bool,
96
97 #[arg(long = "depth")]
99 depth: Option<usize>,
100 },
101 MetaReroute {
103 input: String,
105
106 #[arg(short = 's', long = "sensor")]
108 route: String,
109
110 #[arg(short = 'o', long = "output")]
112 output: Option<String>,
113 },
114 #[command(args_conflicts_with_subcommands = true)]
115 Dump {
117 #[command(flatten)]
118 tio: TioOpts,
119
120 #[command(subcommand)]
121 subcommands: Option<DumpSubcommands>,
122
123 #[arg(short = 'd', long = "data")]
125 data: bool,
126
127 #[arg(short = 'm', long = "meta")]
129 meta: bool,
130
131 #[arg(long = "depth")]
133 depth: Option<usize>,
134 },
135 FirmwareUpgrade {
137 #[command(flatten)]
138 tio: TioOpts,
139
140 firmware_path: String,
142 },
143}
144
145#[derive(Subcommand, Debug)]
146pub enum RPCSubcommands{
147 List {
149 #[command(flatten)]
150 tio: TioOpts,
151 },
152 Dump {
154 #[command(flatten)]
155 tio: TioOpts,
156
157 rpc_name: String,
159
160 #[arg(long)]
162 capture: bool,
163 },
164}
165
166#[derive(Subcommand, Debug)]
167pub enum LogSubcommands{
168 Metadata {
170 #[command(flatten)]
171 tio: TioOpts,
172
173 #[arg(short = 'f', default_value = "meta.tio")]
175 file: String,
176 },
177
178 Dump {
180 files: Vec<String>,
182
183 #[arg(short = 'd', long = "data")]
185 data: bool,
186
187 #[arg(short = 'm', long = "meta")]
189 meta: bool,
190
191 #[arg(short = 's', long = "sensor", default_value = "/")]
193 sensor: String,
194
195 #[arg(long = "depth")]
197 depth: Option<usize>,
198 },
199
200 #[command(hide = true)]
202 DataDump {
203 files: Vec<String>,
205 },
206
207 Csv {
209 args: Vec<String>,
211
212 #[arg(short = 's')]
214 sensor: Option<String>,
215
216 #[arg(short = 'o')]
218 output: Option<String>,
219 },
220
221 Hdf {
223 files: Vec<String>,
225
226 #[arg(short = 'o')]
228 output: Option<String>,
229
230 #[arg(short = 'g', long = "glob")]
232 filter: Option<String>,
233
234 #[arg(short = 'c', long = "compress")]
236 compress: bool,
237
238 #[arg(short = 'd', long)]
240 debug: bool,
241
242 #[arg(short = 'l', long = "split", default_value = "none")]
244 split_level: SplitLevel,
245
246 #[arg(short = 'p', long = "policy", default_value = "continuous")]
248 split_policy: SplitPolicy,
249 },
250}
251
252#[derive(Subcommand, Debug)]
253pub enum DumpSubcommands{
254 #[command(hide = true)]
256 Data {
257 #[command(flatten)]
258 tio: TioOpts,
259 },
260
261 #[command(hide = true)]
263 DataAll {
264 #[command(flatten)]
265 tio: TioOpts,
266 },
267
268 #[command(hide = true)]
270 Meta {
271 #[command(flatten)]
272 tio: TioOpts,
273 },
274}
275
276fn default_log_path() -> String {
277 chrono::Local::now()
278 .format("log.%Y%m%d-%H%M%S.tio")
279 .to_string()
280}
281
282#[derive(ValueEnum, Clone, Debug, Default)]
284pub enum SplitPolicy {
285 #[default]
287 Continuous,
288 Monotonic,
290}
291
292#[cfg(feature = "hdf5")]
293impl From<SplitPolicy> for twinleaf::data::export::SplitPolicy {
294 fn from(policy: SplitPolicy) -> Self {
295 match policy {
296 SplitPolicy::Continuous => Self::Continuous,
297 SplitPolicy::Monotonic => Self::Monotonic,
298 }
299 }
300}
301
302#[derive(ValueEnum, Clone, Debug, Default)]
304pub enum SplitLevel {
305 #[default]
307 None,
308 Stream,
310 Device,
312 Global,
314}
315
316#[cfg(feature = "hdf5")]
317impl From<SplitLevel> for twinleaf::data::export::RunSplitLevel {
318 fn from(level: SplitLevel) -> Self {
319 match level {
320 SplitLevel::None => Self::None,
321 SplitLevel::Stream => Self::PerStream,
322 SplitLevel::Device => Self::PerDevice,
323 SplitLevel::Global => Self::Global,
324 }
325 }
326}
327
328
329#[derive(Parser, Debug, Clone)]
330#[command(
331 name = "tio-health",
332 version,
333 about = "Live timing & rate diagnostics for TIO (Twinleaf) devices"
334)]
335pub struct HealthCli {
336 #[command(flatten)]
337 tio: TioOpts,
338
339 #[arg(
341 long = "jitter-window",
342 default_value = "10",
343 value_name = "SECONDS",
344 value_parser = clap::value_parser!(u64).range(1..),
345 help = "Seconds for jitter calculation window (>= 1)"
346 )]
347 jitter_window: u64,
348
349 #[arg(
351 long = "ppm-warn",
352 default_value = "100",
353 value_name = "PPM",
354 value_parser = nonneg_f64,
355 help = "Warning threshold in parts per million (>= 0)"
356 )]
357 ppm_warn: f64,
358
359 #[arg(
361 long = "ppm-err",
362 default_value = "200",
363 value_name = "PPM",
364 value_parser = nonneg_f64,
365 help = "Error threshold in parts per million (>= 0)"
366 )]
367 ppm_err: f64,
368
369 #[arg(
371 long = "streams",
372 value_delimiter = ',',
373 value_name = "IDS",
374 value_parser = clap::value_parser!(u8),
375 help = "Comma-separated stream IDs to monitor (e.g., 0,1,5)"
376 )]
377 streams: Option<Vec<u8>>,
378
379 #[arg(short = 'q', long = "quiet")]
381 quiet: bool,
382
383 #[arg(
385 long = "fps",
386 default_value = "30",
387 value_name = "FPS",
388 value_parser = clap::value_parser!(u64).range(1..=60),
389 help = "UI refresh rate for heartbeat animation and stale detection (1–60)"
390 )]
391 fps: u64,
392
393 #[arg(
395 long = "stale-ms",
396 default_value = "2000",
397 value_name = "MS",
398 value_parser = clap::value_parser!(u64).range(1..),
399 help = "Mark streams as stale after this many milliseconds without data (>= 1)"
400 )]
401 stale_ms: u64,
402
403 #[arg(
405 short = 'n',
406 long = "event-log-size",
407 default_value = "100",
408 value_name = "N",
409 value_parser = clap::value_parser!(u64).range(1..),
410 help = "Maximum number of events to keep in history (>= 1)"
411 )]
412 event_log_size: u64,
413
414 #[arg(
416 long = "event-display-lines",
417 default_value = "8",
418 value_name = "LINES",
419 value_parser = clap::value_parser!(u16).range(3..),
420 help = "Number of event lines to show (>= 3)"
421 )]
422 event_display_lines: u16,
423
424 #[arg(short = 'w', long = "warnings-only")]
426 warnings_only: bool,
427}
428
429impl HealthCli {
430 fn stale_dur(&self) -> Duration {
431 Duration::from_millis(self.stale_ms)
432 }
433}
434
435fn nonneg_f64(s: &str) -> Result<f64, String> {
436 let v: f64 = s
437 .parse()
438 .map_err(|e: std::num::ParseFloatError| e.to_string())?;
439 if v < 0.0 {
440 Err("must be ≥ 0".into())
441 } else {
442 Ok(v)
443 }
444}
445
446#[derive(Parser, Debug)]
447#[command(
448 name = "tio-proxy",
449 version,
450 about = "Multiplexes access to a sensor, exposing the functionality of tio::proxy via TCP"
451)]
452pub struct ProxyCli {
453 sensor_url: Option<String>,
456
457 #[arg(short = 'p', long = "port", default_value = "7855")]
459 port: u16,
460
461 #[arg(short = 'k', long)]
463 kick_slow: bool,
464
465 #[arg(short = 's', long = "subtree", default_value = "/")]
467 subtree: String,
468
469 #[arg(short = 'v', long)]
471 verbose: bool,
472
473 #[arg(short = 'd', long)]
475 debug: bool,
476
477 #[arg(short = 't', long = "timestamp", default_value = "%T%.3f ")]
479 timestamp_format: String,
480
481 #[arg(short = 'T', long = "timeout", default_value = "30")]
483 reconnect_timeout: u64,
484
485 #[arg(long)]
487 dump: bool,
488
489 #[arg(long)]
491 dump_data: bool,
492
493 #[arg(long)]
495 dump_meta: bool,
496
497 #[arg(long)]
499 dump_hb: bool,
500
501 #[arg(short = 'a', long = "auto")]
502 auto: bool,
503
504 #[arg(short = 'e', long = "enumerate", name = "enum")]
506 enumerate: bool,
507}