1use std::fmt::Write as _;
18use std::net::Ipv4Addr;
19use std::path::PathBuf;
20use std::sync::Arc;
21use std::time::Duration;
22
23use anyhow::{Context, Result};
24use tokio::sync::Mutex;
25use viva_genapi_xml::XmlModel;
26use viva_genicam::genapi::NodeMap;
27use viva_gige::DeviceInfo;
28use viva_gige::gvcp::{GigeDevice, consts};
29use viva_gige::nic::{Iface, IfaceSelector};
30
31use crate::common;
32
33#[derive(Clone, Copy)]
38enum Fmt {
39 Bits,
41 Dec,
42 Ipv4,
43}
44
45const BOOTSTRAP: &[(u64, &str, Fmt)] = &[
51 (0x0000, "Version", Fmt::Bits),
52 (0x0004, "DeviceMode", Fmt::Bits),
53 (0x0008, "DeviceMACAddressHigh", Fmt::Bits),
54 (0x000C, "DeviceMACAddressLow", Fmt::Bits),
55 (0x0010, "SupportedIPConfiguration", Fmt::Bits),
56 (
57 consts::CURRENT_IP_CONFIG,
58 "CurrentIPConfiguration",
59 Fmt::Bits,
60 ),
61 (0x0024, "CurrentIPAddress", Fmt::Ipv4),
62 (0x0034, "CurrentSubnetMask", Fmt::Ipv4),
63 (0x0044, "CurrentDefaultGateway", Fmt::Ipv4),
64 (
65 consts::PERSISTENT_IP_ADDRESS,
66 "PersistentIPAddress",
67 Fmt::Ipv4,
68 ),
69 (
70 consts::PERSISTENT_SUBNET_MASK,
71 "PersistentSubnetMask",
72 Fmt::Ipv4,
73 ),
74 (
75 consts::PERSISTENT_DEFAULT_GATEWAY,
76 "PersistentDefaultGateway",
77 Fmt::Ipv4,
78 ),
79 (0x0900, "GevNumberOfMessageChannels", Fmt::Dec),
80 (0x0904, "GevNumberOfStreamChannels", Fmt::Dec),
81 (0x0938, "GevHeartbeatTimeout", Fmt::Dec),
82 (consts::CONTROL_CHANNEL_PRIVILEGE, "GevCCP", Fmt::Bits),
83 (consts::MESSAGE_DESTINATION_PORT, "GevMCP", Fmt::Dec),
84 (consts::MESSAGE_DESTINATION_ADDRESS, "GevMCDA", Fmt::Ipv4),
85 (consts::MESSAGE_CHANNEL_TIMEOUT, "GevMCTT", Fmt::Dec),
86 (consts::MESSAGE_CHANNEL_RETRY_COUNT, "GevMCRC", Fmt::Dec),
87 (
88 consts::STREAM_CHANNEL_BASE + consts::STREAM_DESTINATION_PORT,
89 "GevSCPHostPort[0]",
90 Fmt::Dec,
91 ),
92 (
93 consts::STREAM_CHANNEL_BASE + consts::STREAM_PACKET_SIZE,
94 "GevSCPSPacketSize[0]",
95 Fmt::Bits,
96 ),
97 (
98 consts::STREAM_CHANNEL_BASE + consts::STREAM_PACKET_DELAY,
99 "GevSCPD[0]",
100 Fmt::Dec,
101 ),
102 (
103 consts::STREAM_CHANNEL_BASE + consts::STREAM_DESTINATION_ADDRESS,
104 "GevSCDA[0]",
105 Fmt::Ipv4,
106 ),
107];
108
109fn decode(value: u32, fmt: Fmt) -> String {
110 match fmt {
111 Fmt::Bits => String::new(),
112 Fmt::Dec => value.to_string(),
113 Fmt::Ipv4 => Ipv4Addr::from(value).to_string(),
114 }
115}
116
117pub struct ReportArgs {
118 pub ip: Option<Ipv4Addr>,
119 pub index: Option<usize>,
120 pub iface: Option<IfaceSelector>,
121 pub out: Option<PathBuf>,
122 pub timeout_ms: u64,
123 pub no_xml: bool,
126}
127
128pub async fn run(args: ReportArgs) -> Result<()> {
129 let mut out = String::new();
130 let _ = writeln!(out, "# viva-genicam diagnostic report");
131 let _ = writeln!(out);
132
133 environment(&mut out);
134 interfaces(&mut out);
135 let devices = discovery(&mut out, args.iface.as_ref(), args.timeout_ms).await;
136 camera(&mut out, &args, &devices).await;
137
138 let _ = writeln!(out, "## End of report");
139
140 match args.out {
141 Some(path) => {
142 std::fs::write(&path, out.as_bytes())
143 .with_context(|| format!("write {}", path.display()))?;
144 eprintln!(
145 "wrote {} bytes to {}\n\nAttach this file to \
146 https://github.com/VitalyVorobyev/viva-genicam/issues",
147 out.len(),
148 path.display()
149 );
150 }
151 None => print!("{out}"),
152 }
153 Ok(())
154}
155
156fn environment(out: &mut String) {
157 let _ = writeln!(out, "## Environment");
158 let _ = writeln!(out);
159 let _ = writeln!(out, "viva-camctl: {}", env!("CARGO_PKG_VERSION"));
160 let _ = writeln!(
161 out,
162 "host: {} {}",
163 std::env::consts::OS,
164 std::env::consts::ARCH
165 );
166 let _ = writeln!(out);
167}
168
169fn interfaces(out: &mut String) {
170 let _ = writeln!(out, "## Network interfaces");
171 let _ = writeln!(out);
172 let _ = writeln!(
173 out,
174 "As the library sees them. An interface missing here is invisible to \
175 discovery no matter what the OS reports elsewhere (#57)."
176 );
177 let _ = writeln!(out);
178 let mtu_measured = cfg!(target_os = "linux");
182 match Iface::list() {
183 Ok(ifaces) if ifaces.is_empty() => {
184 let _ = writeln!(out, "(none reported)");
185 }
186 Ok(ifaces) => {
187 for iface in ifaces {
188 let addrs = iface.all_ipv4().unwrap_or_default();
189 let addrs = if addrs.is_empty() {
190 "-".to_string()
191 } else {
192 addrs
193 .iter()
194 .map(|ip| ip.to_string())
195 .collect::<Vec<_>>()
196 .join(", ")
197 };
198 let mtu = if mtu_measured {
199 viva_gige::nic::mtu(&iface)
200 .map(|mtu| mtu.to_string())
201 .unwrap_or_else(|err| format!("unknown ({err})"))
202 } else {
203 "assumed".to_string()
204 };
205 let _ = writeln!(
206 out,
207 "{:<16} index={:<5} mtu={:<9} ipv4=[{}]",
208 iface.name(),
209 iface.index(),
210 mtu,
211 addrs
212 );
213 }
214 if !mtu_measured {
215 let _ = writeln!(
216 out,
217 "\nMTU is not queried on {} — the library assumes 1500 and \
218 cannot select jumbo frames here (TC-11).",
219 std::env::consts::OS
220 );
221 }
222 }
223 Err(err) => {
224 let _ = writeln!(out, "FAILED to enumerate interfaces: {err}");
225 }
226 }
227 let _ = writeln!(out);
228}
229
230async fn discovery(
231 out: &mut String,
232 iface: Option<&IfaceSelector>,
233 timeout_ms: u64,
234) -> Vec<DeviceInfo> {
235 let _ = writeln!(out, "## Discovery");
236 let _ = writeln!(out);
237 let timeout = Duration::from_millis(timeout_ms);
238 match common::discover_devices(timeout, iface).await {
239 Ok(devices) if devices.is_empty() => {
240 let _ = writeln!(out, "no cameras answered within {timeout_ms} ms");
241 let _ = writeln!(out);
242 Vec::new()
243 }
244 Ok(devices) => {
245 for (index, dev) in devices.iter().enumerate() {
246 let _ = writeln!(out, "[{index}] {}", dev.ip);
247 let _ = writeln!(out, " mac: {}", dev.mac_string());
248 let _ = writeln!(out, " manufacturer: {}", opt(&dev.manufacturer));
249 let _ = writeln!(out, " model: {}", opt(&dev.model));
250 let _ = writeln!(out, " version: {}", opt(&dev.version));
251 let _ = writeln!(out, " serial: {}", opt(&dev.serial));
252 let _ = writeln!(out, " user name: {}", opt(&dev.user_name));
253 }
254 let _ = writeln!(out);
255 devices
256 }
257 Err(err) => {
258 let _ = writeln!(out, "FAILED: {err:#}");
259 let _ = writeln!(out);
260 Vec::new()
261 }
262 }
263}
264
265async fn camera(out: &mut String, args: &ReportArgs, discovered: &[DeviceInfo]) {
266 let _ = writeln!(out, "## Camera");
267 let _ = writeln!(out);
268
269 let device = match select(args, discovered) {
270 Some(device) => device,
271 None => {
272 let _ = writeln!(
273 out,
274 "No camera selected. Pass --ip or --index to include register, \
275 XML and feature sections."
276 );
277 let _ = writeln!(out);
278 return;
279 }
280 };
281 let _ = writeln!(out, "selected: {}", device.ip);
282 let _ = writeln!(out);
283
284 let control = match common::open_control(&device).await {
285 Ok(control) => control,
286 Err(err) => {
287 let _ = writeln!(out, "FAILED to open the control channel: {err:#}");
288 let _ = writeln!(out);
289 return;
290 }
291 };
292 let control = Arc::new(Mutex::new(control));
293
294 bootstrap_registers(out, &control).await;
295
296 let xml = match common::fetch_xml(Arc::clone(&control)).await {
297 Ok(xml) => xml,
298 Err(err) => {
299 let _ = writeln!(out, "## GenApi XML");
300 let _ = writeln!(out);
301 let _ = writeln!(out, "FAILED to fetch: {err:#}");
302 let _ = writeln!(out);
303 return;
304 }
305 };
306 genapi(out, &xml, args.no_xml);
307}
308
309fn select(args: &ReportArgs, discovered: &[DeviceInfo]) -> Option<DeviceInfo> {
310 if let Some(ip) = args.ip {
311 return Some(
315 discovered
316 .iter()
317 .find(|dev| dev.ip == ip)
318 .cloned()
319 .unwrap_or_else(|| DeviceInfo::from_ip(ip)),
320 );
321 }
322 args.index.and_then(|index| discovered.get(index).cloned())
323}
324
325async fn bootstrap_registers(out: &mut String, control: &Arc<Mutex<GigeDevice>>) {
326 let _ = writeln!(out, "## Bootstrap registers");
327 let _ = writeln!(out);
328 let mut guard = control.lock().await;
329 for (addr, name, fmt) in BOOTSTRAP {
330 let value = match guard.read_register(*addr as u32).await {
331 Ok(value) => format!("0x{value:08X} {}", decode(value, *fmt)),
332 Err(err) => format!("read failed: {err}"),
333 };
334 let _ = writeln!(out, "0x{addr:04X} {name:<26} {}", value.trim_end());
335 }
336 let _ = writeln!(out);
337}
338
339fn genapi(out: &mut String, xml: &str, no_xml: bool) {
340 let _ = writeln!(out, "## GenApi");
341 let _ = writeln!(out);
342 let _ = writeln!(out, "XML size: {} bytes", xml.len());
343
344 let model: Option<XmlModel> = match viva_genapi_xml::parse(xml) {
345 Ok(model) => {
346 let _ = writeln!(
347 out,
348 "schema: {}\nparsed: {} nodes",
349 model.version,
350 model.nodes.len()
351 );
352 Some(model)
353 }
354 Err(err) => {
355 let _ = writeln!(out, "PARSE FAILED: {err}");
356 None
357 }
358 };
359
360 if let Some(model) = model {
361 match NodeMap::try_from_xml(model) {
362 Ok(nodemap) => {
363 let _ = writeln!(out, "built: {} features", nodemap.node_names().count());
364 let _ = writeln!(out);
365 let skipped = nodemap.skipped();
366 if skipped.is_empty() {
367 let _ = writeln!(out, "No features were dropped.");
368 } else {
369 let _ = writeln!(
370 out,
371 "{} feature(s) this camera has that we cannot expose — \
372 these are the interesting ones:",
373 skipped.len()
374 );
375 let _ = writeln!(out);
376 for node in skipped {
377 let _ = writeln!(
378 out,
379 " <{}> {}: {}",
380 node.tag,
381 node.name.as_deref().unwrap_or("<unnamed>"),
382 node.error
383 );
384 }
385 }
386 }
387 Err(err) => {
388 let _ = writeln!(out, "NODEMAP BUILD FAILED: {err}");
389 }
390 }
391 }
392 let _ = writeln!(out);
393
394 if no_xml {
395 let _ = writeln!(
396 out,
397 "## GenApi XML\n\nOmitted (--no-xml). Re-run without it, or use \
398 `viva-camctl xml`, if asked for the document."
399 );
400 } else {
401 let _ = writeln!(out, "## GenApi XML");
402 let _ = writeln!(out);
403 let _ = writeln!(out, "{xml}");
404 }
405 let _ = writeln!(out);
406}
407
408fn opt(value: &Option<String>) -> &str {
409 value.as_deref().unwrap_or("-")
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn bootstrap_addresses_are_unique() {
418 let mut seen = std::collections::HashSet::new();
419 for (addr, name, _) in BOOTSTRAP {
420 assert!(seen.insert(*addr), "duplicate address for {name}");
421 }
422 }
423
424 #[test]
425 fn ip_registers_decode_to_dotted_quads() {
426 assert_eq!(decode(0x7F00_0001, Fmt::Ipv4), "127.0.0.1");
427 assert_eq!(decode(0xFF00_0000, Fmt::Ipv4), "255.0.0.0");
428 assert_eq!(decode(3000, Fmt::Dec), "3000");
429 assert_eq!(decode(0x8000_0001, Fmt::Bits), "");
430 }
431
432 #[test]
435 fn unparsable_xml_still_produces_a_section() {
436 let mut out = String::new();
437 genapi(&mut out, "<not-genicam>", true);
438 assert!(out.contains("PARSE FAILED"), "{out}");
439 assert!(out.contains("XML size: 13 bytes"), "{out}");
440 }
441
442 #[test]
443 fn skipped_features_are_listed() {
444 const XML: &str = r#"
445 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
446 <ConfRom Name="DeviceConfRom">
447 <Address>0x2000</Address>
448 <Length>512</Length>
449 </ConfRom>
450 </RegisterDescription>
451 "#;
452 let mut out = String::new();
453 genapi(&mut out, XML, true);
454 assert!(out.contains("<ConfRom> DeviceConfRom"), "{out}");
455 assert!(out.contains("cannot expose"), "{out}");
456 }
457
458 #[test]
459 fn the_xml_is_included_unless_opted_out() {
460 const XML: &str = r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0"/>"#;
461 let mut with = String::new();
462 genapi(&mut with, XML, false);
463 assert!(with.contains("SchemaMajorVersion"), "{with}");
464
465 let mut without = String::new();
466 genapi(&mut without, XML, true);
467 assert!(without.contains("Omitted (--no-xml)"), "{without}");
468 }
469}