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