1use std::net::Ipv4Addr;
2use std::path::PathBuf;
3
4use anyhow::Result;
5use clap::{ArgAction, Parser, Subcommand};
6use tracing_subscriber::EnvFilter;
7use viva_gige::nic::IfaceSelector;
8
9use crate::cmd_bench::{self, BenchArgs};
10use crate::cmd_chunks;
11use crate::cmd_events;
12use crate::cmd_get;
13use crate::cmd_list;
14use crate::cmd_report::{self, ReportArgs};
15use crate::cmd_set;
16use crate::cmd_set_ip;
17use crate::cmd_stream::{self, StreamArgs};
18use crate::cmd_usb;
19use crate::cmd_xml::{self, XmlArgs};
20
21#[derive(Parser, Debug)]
22#[command(name = "viva-camctl", version, about = "GenICam CLI")]
23pub struct Cli {
24 #[arg(short, long, action = ArgAction::Count)]
26 verbose: u8,
27 #[arg(long)]
29 json: bool,
30 #[arg(long)]
33 iface: Option<IfaceSelector>,
34 #[command(subcommand)]
35 cmd: Cmd,
36}
37
38#[derive(Subcommand, Debug)]
39pub enum Cmd {
40 List {
42 #[arg(long, default_value_t = 1000)]
43 timeout_ms: u64,
44 #[arg(long)]
46 iface: Option<IfaceSelector>,
47 },
48 Get {
50 #[arg(long)]
51 ip: Option<Ipv4Addr>,
52 #[arg(long)]
53 index: Option<usize>,
54 #[arg(long)]
55 name: String,
56 },
57 Xml {
60 #[arg(long)]
61 ip: Option<Ipv4Addr>,
62 #[arg(long)]
63 index: Option<usize>,
64 #[arg(long)]
66 iface: Option<IfaceSelector>,
67 #[arg(long)]
69 out: Option<PathBuf>,
70 },
71 Report {
73 #[arg(long)]
74 ip: Option<Ipv4Addr>,
75 #[arg(long)]
76 index: Option<usize>,
77 #[arg(long)]
79 iface: Option<IfaceSelector>,
80 #[arg(long, default_value = "viva-report.txt")]
83 out: PathBuf,
84 #[arg(long, conflicts_with = "out")]
86 stdout: bool,
87 #[arg(long, default_value_t = 1000)]
88 timeout_ms: u64,
89 #[arg(long)]
91 no_xml: bool,
92 },
93 Set {
95 #[arg(long)]
96 ip: Option<Ipv4Addr>,
97 #[arg(long)]
98 index: Option<usize>,
99 #[arg(long)]
100 name: String,
101 #[arg(long)]
102 value: String,
103 },
104 Stream {
106 #[arg(long)]
107 ip: Option<Ipv4Addr>,
108 #[arg(long)]
109 index: Option<usize>,
110 #[arg(long)]
112 iface: Option<IfaceSelector>,
113 #[arg(long, default_value = "unicast")]
114 mode: String,
115 #[arg(long)]
116 group: Option<Ipv4Addr>,
117 #[arg(long, default_value_t = 10040)]
118 port: u16,
119 #[arg(long, conflicts_with = "packet_size")]
123 auto: bool,
124 #[arg(long, conflicts_with = "auto")]
127 packet_size: Option<u32>,
128 #[arg(long, default_value_t = 1)]
129 save: usize,
130 #[arg(long)]
131 rgb: bool,
132 #[arg(long, default_value_t = 0)]
133 duration_s: u64,
134 },
135 Events {
137 #[arg(long)]
138 ip: Option<Ipv4Addr>,
139 #[arg(long)]
140 index: Option<usize>,
141 #[arg(long)]
143 iface: Option<IfaceSelector>,
144 #[arg(long, default_value_t = 10020)]
145 port: u16,
146 #[arg(long, default_value = "FrameStart,ExposureEnd")]
147 enable: String,
148 #[arg(long, default_value_t = 10)]
149 count: u32,
150 },
151 Chunks {
153 #[arg(long)]
154 ip: Option<Ipv4Addr>,
155 #[arg(long)]
156 index: Option<usize>,
157 #[arg(long)]
158 enable: bool,
159 #[arg(long, default_value = "Timestamp")]
160 selectors: String,
161 },
162 Bench {
164 #[arg(long)]
165 ip: Option<Ipv4Addr>,
166 #[arg(long)]
167 index: Option<usize>,
168 #[arg(long)]
170 iface: Option<IfaceSelector>,
171 #[arg(long, default_value = "unicast")]
172 mode: String,
173 #[arg(long)]
174 group: Option<Ipv4Addr>,
175 #[arg(long, default_value_t = 10040)]
176 port: u16,
177 #[arg(long, default_value_t = 300)]
178 duration_s: u64,
179 #[arg(long)]
180 json_out: Option<PathBuf>,
181 },
182 SetIp {
184 #[arg(long)]
186 mac: String,
187 #[arg(long)]
189 ip: Ipv4Addr,
190 #[arg(long, default_value = "255.255.255.0")]
192 subnet: Ipv4Addr,
193 #[arg(long, default_value = "0.0.0.0")]
195 gateway: Ipv4Addr,
196 #[arg(long)]
198 force: bool,
199 },
200 ListUsb,
202 GetUsb {
204 #[arg(long)]
205 index: Option<usize>,
206 #[arg(long)]
207 name: String,
208 },
209 SetUsb {
211 #[arg(long)]
212 index: Option<usize>,
213 #[arg(long)]
214 name: String,
215 #[arg(long)]
216 value: String,
217 },
218 StreamUsb {
220 #[arg(long)]
221 index: Option<usize>,
222 #[arg(long, default_value_t = 1)]
224 save: usize,
225 #[arg(long)]
227 rgb: bool,
228 #[arg(long, default_value_t = 0)]
230 duration_s: u64,
231 },
232}
233
234fn init_tracing(verbose: u8) {
241 let level = match verbose {
242 0 => "info",
243 1 => "debug",
244 _ => "trace",
245 };
246 let _ = tracing_subscriber::fmt()
247 .with_env_filter(EnvFilter::new(
248 std::env::var("RUST_LOG").unwrap_or_else(|_| level.into()),
249 ))
250 .with_target(false)
251 .try_init();
252}
253
254pub async fn dispatch(cli: Cli) -> Result<()> {
256 let Cli {
257 verbose,
258 json,
259 iface,
260 cmd,
261 } = cli;
262
263 init_tracing(verbose);
264
265 match cmd {
266 Cmd::List {
267 timeout_ms,
268 iface: cmd_iface,
269 } => {
270 let iface = cmd_iface.or(iface);
271 cmd_list::run(timeout_ms, iface, json).await?
272 }
273 Cmd::Get { ip, index, name } => cmd_get::run(ip, index, name, iface, json).await?,
274 Cmd::Xml {
275 ip,
276 index,
277 iface: cmd_iface,
278 out,
279 } => {
280 let args = XmlArgs {
281 ip,
282 index,
283 iface: cmd_iface.or(iface),
284 out,
285 };
286 cmd_xml::run(args).await?
287 }
288 Cmd::Report {
289 ip,
290 index,
291 iface: cmd_iface,
292 out,
293 stdout,
294 timeout_ms,
295 no_xml,
296 } => {
297 let args = ReportArgs {
298 ip,
299 index,
300 iface: cmd_iface.or(iface),
301 out: (!stdout).then_some(out),
302 timeout_ms,
303 no_xml,
304 };
305 cmd_report::run(args).await?
306 }
307 Cmd::Set {
308 ip,
309 index,
310 name,
311 value,
312 } => cmd_set::run(ip, index, name, value, iface, json).await?,
313 Cmd::Stream {
314 ip,
315 index,
316 iface: cmd_iface,
317 mode,
318 group,
319 port,
320 packet_size,
321 auto,
322 save,
323 rgb,
324 duration_s,
325 } => {
326 let args = StreamArgs {
327 ip,
328 index,
329 iface: cmd_iface.or(iface),
330 mode,
331 group,
332 port,
333 packet_size,
334 auto,
335 save,
336 rgb,
337 duration_s,
338 };
339 cmd_stream::run(args).await?
340 }
341 Cmd::Events {
342 ip,
343 index,
344 iface: cmd_iface,
345 port,
346 enable,
347 count,
348 } => cmd_events::run(ip, index, cmd_iface.or(iface), port, enable, count, json).await?,
349 Cmd::Chunks {
350 ip,
351 index,
352 enable,
353 selectors,
354 } => cmd_chunks::run(ip, index, enable, selectors, iface, json).await?,
355 Cmd::Bench {
356 ip,
357 index,
358 iface: cmd_iface,
359 mode,
360 group,
361 port,
362 duration_s,
363 json_out,
364 } => {
365 let args = BenchArgs {
366 ip,
367 index,
368 iface: cmd_iface.or(iface),
369 mode,
370 group,
371 port,
372 duration_s,
373 json_out,
374 };
375 cmd_bench::run(args, json).await?
376 }
377 Cmd::SetIp {
378 mac,
379 ip,
380 subnet,
381 gateway,
382 force,
383 } => cmd_set_ip::run(&mac, ip, subnet, gateway, force, iface).await?,
384 Cmd::ListUsb => cmd_usb::run_list(json)?,
385 Cmd::GetUsb { index, name } => cmd_usb::run_get(index, name, json)?,
386 Cmd::SetUsb { index, name, value } => cmd_usb::run_set(index, name, value, json)?,
387 Cmd::StreamUsb {
388 index,
389 save,
390 rgb,
391 duration_s,
392 } => cmd_usb::run_stream(index, save, rgb, duration_s)?,
393 };
394
395 Ok(())
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn parse_list_defaults() {
404 let cli = Cli::parse_from(["viva-camctl", "list"]);
405 match cli.cmd {
406 Cmd::List { timeout_ms, .. } => assert_eq!(timeout_ms, 1000),
407 other => panic!("unexpected variant: {other:?}"),
408 }
409 }
410
411 #[test]
412 fn parse_stream_args() {
413 let cli = Cli::parse_from([
414 "viva-camctl",
415 "stream",
416 "--mode",
417 "multicast",
418 "--group",
419 "239.1.1.1",
420 "--port",
421 "12000",
422 ]);
423 match cli.cmd {
424 Cmd::Stream {
425 mode, port, group, ..
426 } => {
427 assert_eq!(mode, "multicast");
428 assert_eq!(port, 12000);
429 assert_eq!(group, Some("239.1.1.1".parse().unwrap()));
430 }
431 other => panic!("unexpected variant: {other:?}"),
432 }
433 }
434
435 #[test]
436 fn parse_bench_output_path() {
437 let cli = Cli::parse_from(["viva-camctl", "bench", "--json-out", "bench.json"]);
438 match cli.cmd {
439 Cmd::Bench { json_out, .. } => {
440 assert_eq!(json_out, Some(PathBuf::from("bench.json")));
441 }
442 other => panic!("unexpected variant: {other:?}"),
443 }
444 }
445}