use clap::{ValueEnum, Subcommand};
#[derive(Parser, Debug)]
#[command(
name = "tio",
version,
about = "Twinleaf sensor management and data logging tool",
)]
pub struct TioCli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Proxy(ProxyCli),
Monitor {
#[command(flatten)]
tio: TioOpts,
#[arg(short = 'a', long = "all")]
all: bool,
#[arg(long = "fps", default_value_t = 20)]
fps: u32,
#[arg(short = 'c', long = "colors")]
colors: Option<String>,
},
Health(HealthCli),
NmeaProxy{
#[command(flatten)]
tio: TioOpts,
#[arg(
short = 'p',
long = "port",
default_value = "7800",
help = "TCP port to listen on"
)]
tcp_port: u16,
},
#[command(args_conflicts_with_subcommands = true)]
Rpc {
#[command(flatten)]
tio: TioOpts,
#[command(subcommand)]
subcommands: Option<RPCSubcommands>,
rpc_name: Option<String>,
#[arg(
allow_negative_numbers = true,
value_name = "ARG",
help_heading = "RPC Arguments"
)]
rpc_arg: Option<String>,
#[arg(short = 't', long = "req-type", help_heading = "Type Options")]
req_type: Option<String>,
#[arg(short = 'T', long = "rep-type", help_heading = "Type Options")]
rep_type: Option<String>,
#[arg(short = 'd', long)]
debug: bool,
},
#[command(args_conflicts_with_subcommands = true)]
Log {
#[command(flatten)]
tio: TioOpts,
#[command(subcommand)]
subcommands: Option<LogSubcommands>,
#[arg(short = 'f', default_value_t = default_log_path())]
file: String,
#[arg(short = 'u')]
unbuffered: bool,
#[arg(long)]
raw: bool,
#[arg(long = "depth")]
depth: Option<usize>,
},
MetaReroute {
input: String,
#[arg(short = 's', long = "sensor")]
route: String,
#[arg(short = 'o', long = "output")]
output: Option<String>,
},
#[command(args_conflicts_with_subcommands = true)]
Dump {
#[command(flatten)]
tio: TioOpts,
#[command(subcommand)]
subcommands: Option<DumpSubcommands>,
#[arg(short = 'd', long = "data")]
data: bool,
#[arg(short = 'm', long = "meta")]
meta: bool,
#[arg(long = "depth")]
depth: Option<usize>,
},
FirmwareUpgrade {
#[command(flatten)]
tio: TioOpts,
firmware_path: String,
},
}
#[derive(Subcommand, Debug)]
pub enum RPCSubcommands{
List {
#[command(flatten)]
tio: TioOpts,
},
Dump {
#[command(flatten)]
tio: TioOpts,
rpc_name: String,
#[arg(long)]
capture: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum LogSubcommands{
Metadata {
#[command(flatten)]
tio: TioOpts,
#[arg(short = 'f', default_value = "meta.tio")]
file: String,
},
Dump {
files: Vec<String>,
#[arg(short = 'd', long = "data")]
data: bool,
#[arg(short = 'm', long = "meta")]
meta: bool,
#[arg(short = 's', long = "sensor", default_value = "/")]
sensor: String,
#[arg(long = "depth")]
depth: Option<usize>,
},
#[command(hide = true)]
DataDump {
files: Vec<String>,
},
Csv {
args: Vec<String>,
#[arg(short = 's')]
sensor: Option<String>,
#[arg(short = 'o')]
output: Option<String>,
},
Hdf {
files: Vec<String>,
#[arg(short = 'o')]
output: Option<String>,
#[arg(short = 'g', long = "glob")]
filter: Option<String>,
#[arg(short = 'c', long = "compress")]
compress: bool,
#[arg(short = 'd', long)]
debug: bool,
#[arg(short = 'l', long = "split", default_value = "none")]
split_level: SplitLevel,
#[arg(short = 'p', long = "policy", default_value = "continuous")]
split_policy: SplitPolicy,
},
}
#[derive(Subcommand, Debug)]
pub enum DumpSubcommands{
#[command(hide = true)]
Data {
#[command(flatten)]
tio: TioOpts,
},
#[command(hide = true)]
DataAll {
#[command(flatten)]
tio: TioOpts,
},
#[command(hide = true)]
Meta {
#[command(flatten)]
tio: TioOpts,
},
}
fn default_log_path() -> String {
chrono::Local::now()
.format("log.%Y%m%d-%H%M%S.tio")
.to_string()
}
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum SplitPolicy {
#[default]
Continuous,
Monotonic,
}
#[cfg(feature = "hdf5")]
impl From<SplitPolicy> for twinleaf::data::export::SplitPolicy {
fn from(policy: SplitPolicy) -> Self {
match policy {
SplitPolicy::Continuous => Self::Continuous,
SplitPolicy::Monotonic => Self::Monotonic,
}
}
}
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum SplitLevel {
#[default]
None,
Stream,
Device,
Global,
}
#[cfg(feature = "hdf5")]
impl From<SplitLevel> for twinleaf::data::export::RunSplitLevel {
fn from(level: SplitLevel) -> Self {
match level {
SplitLevel::None => Self::None,
SplitLevel::Stream => Self::PerStream,
SplitLevel::Device => Self::PerDevice,
SplitLevel::Global => Self::Global,
}
}
}
#[derive(Parser, Debug, Clone)]
#[command(
name = "tio-health",
version,
about = "Live timing & rate diagnostics for TIO (Twinleaf) devices"
)]
pub struct HealthCli {
#[command(flatten)]
tio: TioOpts,
#[arg(
long = "jitter-window",
default_value = "10",
value_name = "SECONDS",
value_parser = clap::value_parser!(u64).range(1..),
help = "Seconds for jitter calculation window (>= 1)"
)]
jitter_window: u64,
#[arg(
long = "ppm-warn",
default_value = "100",
value_name = "PPM",
value_parser = nonneg_f64,
help = "Warning threshold in parts per million (>= 0)"
)]
ppm_warn: f64,
#[arg(
long = "ppm-err",
default_value = "200",
value_name = "PPM",
value_parser = nonneg_f64,
help = "Error threshold in parts per million (>= 0)"
)]
ppm_err: f64,
#[arg(
long = "streams",
value_delimiter = ',',
value_name = "IDS",
value_parser = clap::value_parser!(u8),
help = "Comma-separated stream IDs to monitor (e.g., 0,1,5)"
)]
streams: Option<Vec<u8>>,
#[arg(short = 'q', long = "quiet")]
quiet: bool,
#[arg(
long = "fps",
default_value = "30",
value_name = "FPS",
value_parser = clap::value_parser!(u64).range(1..=60),
help = "UI refresh rate for heartbeat animation and stale detection (1–60)"
)]
fps: u64,
#[arg(
long = "stale-ms",
default_value = "2000",
value_name = "MS",
value_parser = clap::value_parser!(u64).range(1..),
help = "Mark streams as stale after this many milliseconds without data (>= 1)"
)]
stale_ms: u64,
#[arg(
short = 'n',
long = "event-log-size",
default_value = "100",
value_name = "N",
value_parser = clap::value_parser!(u64).range(1..),
help = "Maximum number of events to keep in history (>= 1)"
)]
event_log_size: u64,
#[arg(
long = "event-display-lines",
default_value = "8",
value_name = "LINES",
value_parser = clap::value_parser!(u16).range(3..),
help = "Number of event lines to show (>= 3)"
)]
event_display_lines: u16,
#[arg(short = 'w', long = "warnings-only")]
warnings_only: bool,
}
impl HealthCli {
fn stale_dur(&self) -> Duration {
Duration::from_millis(self.stale_ms)
}
}
fn nonneg_f64(s: &str) -> Result<f64, String> {
let v: f64 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v < 0.0 {
Err("must be ≥ 0".into())
} else {
Ok(v)
}
}
#[derive(Parser, Debug)]
#[command(
name = "tio-proxy",
version,
about = "Multiplexes access to a sensor, exposing the functionality of tio::proxy via TCP"
)]
pub struct ProxyCli {
sensor_url: Option<String>,
#[arg(short = 'p', long = "port", default_value = "7855")]
port: u16,
#[arg(short = 'k', long)]
kick_slow: bool,
#[arg(short = 's', long = "subtree", default_value = "/")]
subtree: String,
#[arg(short = 'v', long)]
verbose: bool,
#[arg(short = 'd', long)]
debug: bool,
#[arg(short = 't', long = "timestamp", default_value = "%T%.3f ")]
timestamp_format: String,
#[arg(short = 'T', long = "timeout", default_value = "30")]
reconnect_timeout: u64,
#[arg(long)]
dump: bool,
#[arg(long)]
dump_data: bool,
#[arg(long)]
dump_meta: bool,
#[arg(long)]
dump_hb: bool,
#[arg(short = 'a', long = "auto")]
auto: bool,
#[arg(short = 'e', long = "enumerate", name = "enum")]
enumerate: bool,
}