1use clap::{Args, Subcommand, ValueEnum, ValueHint};
2use std::time::Duration;
3use twinleaf::device::DeviceRoute;
4
5use crate::{parse_device_route, TioOpts};
6
7#[derive(Args, Debug)]
8#[command(args_conflicts_with_subcommands = true)]
9pub struct LogCli {
10 #[command(flatten)]
11 pub tio: TioOpts,
12
13 #[command(subcommand)]
14 pub subcommands: Option<LogSubcommands>,
15
16 #[arg(short = 'f', default_value_t = default_log_path())]
18 pub file: String,
19
20 #[arg(short = 'u')]
22 pub unbuffered: bool,
23
24 #[arg(long)]
26 pub raw: bool,
27
28 #[arg(long = "depth")]
30 pub depth: Option<usize>,
31
32 #[arg(long, value_parser = humantime::parse_duration)]
34 pub duration: Option<Duration>,
35}
36
37#[derive(Subcommand, Debug)]
38pub enum LogSubcommands {
39 #[command(args_conflicts_with_subcommands = true)]
41 Meta {
42 #[command(flatten)]
43 tio: TioOpts,
44
45 #[command(subcommand)]
46 subcommands: Option<MetaSubcommands>,
47
48 #[arg(short = 'f', default_value = "meta.tio")]
50 file: String,
51 },
52
53 Dump {
55 #[arg(value_hint = ValueHint::FilePath, required = true, num_args = 1..)]
57 files: Vec<String>,
58
59 #[arg(short = 'd', long = "data")]
61 data: bool,
62
63 #[arg(short = 'm', long = "meta")]
65 meta: bool,
66
67 #[arg(short = 's', long = "sensor", default_value = "/", value_parser = parse_device_route)]
69 sensor: DeviceRoute,
70
71 #[arg(long = "depth")]
73 depth: Option<usize>,
74 },
75
76 Inspect {
78 #[arg(value_hint = ValueHint::FilePath, required = true, num_args = 1..)]
80 files: Vec<String>,
81 },
82
83 Csv {
85 #[arg(value_hint = ValueHint::FilePath)]
88 args: Vec<String>,
89
90 #[arg(short = 's', value_parser = parse_device_route)]
93 sensor: Option<DeviceRoute>,
94
95 #[arg(short = 'o')]
97 output: Option<String>,
98
99 #[arg(short = 'f', long)]
101 force: bool,
102 },
103
104 #[command(alias = "hdf5")]
106 Hdf {
107 #[arg(value_hint = ValueHint::FilePath, required = true, num_args = 1..)]
109 files: Vec<String>,
110
111 #[arg(short = 'o')]
113 output: Option<String>,
114
115 #[arg(short = 'g', long = "glob")]
117 filter: Option<String>,
118
119 #[arg(short = 'c', long = "compress")]
121 compress: bool,
122
123 #[arg(short = 'd', long)]
125 debug: bool,
126
127 #[arg(short = 'l', long = "split", default_value = "none")]
129 split_level: SplitLevel,
130
131 #[arg(short = 'p', long = "policy", default_value = "continuous")]
133 split_policy: SplitPolicy,
134 },
135}
136
137#[derive(Subcommand, Debug)]
138pub enum MetaSubcommands {
139 Reroute {
141 #[arg(value_hint = ValueHint::FilePath)]
143 input: String,
144
145 #[arg(short = 's', long = "sensor", value_parser = parse_device_route)]
147 route: DeviceRoute,
148
149 #[arg(short = 'o', long = "output")]
151 output: Option<String>,
152 },
153}
154
155#[derive(Clone, Debug)]
157pub enum StreamSel {
158 Id(u8),
160 Name(String),
162}
163
164#[derive(Clone, Debug)]
170pub struct CsvTarget {
171 pub route: Option<DeviceRoute>,
172 pub stream: StreamSel,
173}
174
175pub fn parse_csv_target(s: &str) -> Result<CsvTarget, String> {
176 if s.is_empty() {
177 return Err("empty stream selector".into());
178 }
179 let last = s.rsplit('/').next().unwrap_or(s);
181 if last.is_empty() {
182 return Err(format!("missing stream name or id in {s:?}"));
183 }
184 let stream = match last.parse::<u8>() {
185 Ok(id) => StreamSel::Id(id),
186 Err(_) => StreamSel::Name(last.to_string()),
187 };
188 let route = if s.contains('/') {
189 let prefix = s[..s.len() - last.len()].trim_end_matches('/');
191 Some(parse_device_route(prefix)?)
192 } else {
193 None
194 };
195 Ok(CsvTarget { route, stream })
196}
197
198fn default_log_path() -> String {
199 chrono::Local::now()
200 .format("log.%Y%m%d-%H%M%S.tio")
201 .to_string()
202}
203
204#[derive(ValueEnum, Clone, Debug, Default)]
206pub enum SplitPolicy {
207 #[default]
209 Continuous,
210 Monotonic,
212}
213
214#[cfg(feature = "hdf5")]
215impl From<SplitPolicy> for twinleaf::data::export::SplitPolicy {
216 fn from(policy: SplitPolicy) -> Self {
217 match policy {
218 SplitPolicy::Continuous => Self::Continuous,
219 SplitPolicy::Monotonic => Self::Monotonic,
220 }
221 }
222}
223
224#[derive(ValueEnum, Clone, Debug, Default)]
226pub enum SplitLevel {
227 #[default]
229 None,
230 Stream,
232 Device,
234 Global,
236}
237
238#[cfg(feature = "hdf5")]
239impl From<SplitLevel> for twinleaf::data::export::RunSplitLevel {
240 fn from(level: SplitLevel) -> Self {
241 match level {
242 SplitLevel::None => Self::None,
243 SplitLevel::Stream => Self::PerStream,
244 SplitLevel::Device => Self::PerDevice,
245 SplitLevel::Global => Self::Global,
246 }
247 }
248}