Skip to main content

viva_genicam/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! High level GenICam facade that re-exports the workspace crates and provides
3//! convenience wrappers.
4//!
5//! ```rust,no_run
6//! use viva_genicam::{gige, genapi, Camera, GenicamError};
7//! use std::time::Duration;
8//!
9//! # struct DummyTransport;
10//! # impl genapi::RegisterIo for DummyTransport {
11//! #     fn read(&self, _addr: u64, len: usize) -> Result<Vec<u8>, genapi::GenApiError> {
12//! #         Ok(vec![0; len])
13//! #     }
14//! #     fn write(&self, _addr: u64, _data: &[u8]) -> Result<(), genapi::GenApiError> {
15//! #         Ok(())
16//! #     }
17//! # }
18//! # #[allow(dead_code)]
19//! # fn load_nodemap() -> genapi::NodeMap {
20//! #     unimplemented!("replace with GenApi XML parsing")
21//! # }
22//! # #[allow(dead_code)]
23//! # async fn open_transport() -> Result<DummyTransport, GenicamError> {
24//! #     Ok(DummyTransport)
25//! # }
26//! # #[allow(dead_code)]
27//! # async fn run() -> Result<(), GenicamError> {
28//! let timeout = Duration::from_millis(500);
29//! let devices = gige::discover(timeout)
30//!     .await
31//!     .expect("discover cameras");
32//! println!("found {} cameras", devices.len());
33//! let mut camera = Camera::new(open_transport().await?, load_nodemap());
34//! camera.set("ExposureTime", "5000")?;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ```rust,no_run
40//! # async fn events_example(
41//! #     mut camera: viva_genicam::Camera<viva_genicam::GigeRegisterIo>,
42//! # ) -> Result<(), viva_genicam::GenicamError> {
43//! use std::net::Ipv4Addr;
44//! let ids = ["FrameStart", "ExposureEnd"];
45//! let iface = Ipv4Addr::new(127, 0, 0, 1);
46//! camera.configure_events(iface, 10020, &ids).await?;
47//! let stream = camera.open_event_stream(iface, 10020).await?;
48//! let event = stream.next().await?;
49//! println!("event id=0x{:04X} payload={} bytes", event.id, event.data.len());
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ```rust,no_run
55//! # async fn action_example() -> Result<(), std::io::Error> {
56//! use viva_genicam::gige::action::{send_action, ActionParams};
57//! use std::net::SocketAddr;
58//! let params = ActionParams {
59//!     device_key: 0,
60//!     group_key: 1,
61//!     group_mask: 0xFFFF_FFFF,
62//!     scheduled_time: None,
63//!     channel: 0,
64//! };
65//! let dest: SocketAddr = "255.255.255.255:3956".parse().unwrap();
66//! let summary = send_action(dest, &params, 200).await?;
67//! println!("acks={}", summary.acks);
68//! Ok(())
69//! # }
70//! ```
71
72pub use viva_genapi as genapi;
73pub use viva_gencp as gencp;
74pub use viva_gige as gige;
75pub use viva_pfnc as pfnc;
76pub use viva_sfnc as sfnc;
77#[cfg(feature = "u3v")]
78#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
79pub use viva_u3v as u3v;
80
81pub mod chunks;
82pub mod events;
83pub mod frame;
84pub mod stream;
85pub mod time;
86
87use std::net::{IpAddr, Ipv4Addr};
88use std::sync::{Arc, Mutex, MutexGuard};
89use std::time::{Duration, Instant, SystemTime};
90
91use crate::events::{
92    bind_socket as bind_event_socket_internal,
93    configure_message_channel_raw as configure_message_channel_fallback,
94    enable_event_raw as enable_event_fallback, parse_event_id,
95};
96use crate::genapi::{GenApiError, Node, NodeMap, RegisterIo, SkOutput};
97use gige::GigeDevice;
98use gige::gvcp::consts as gvcp_consts;
99use thiserror::Error;
100use tokio::time::sleep;
101use tracing::{debug, info, warn};
102
103pub use chunks::{ChunkKind, ChunkMap, ChunkValue, parse_chunk_bytes};
104pub use events::{Event, EventStream};
105pub use frame::Frame;
106pub use gige::action::{AckSummary, ActionParams};
107pub use stream::{FrameStream, Stream, StreamBuilder, StreamDest};
108#[cfg(feature = "u3v")]
109#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
110pub use stream::{U3vFrameStream, U3vStreamBuilder};
111pub use time::TimeSync;
112
113/// Error type produced by the high level GenICam facade.
114#[derive(Debug, Error)]
115#[non_exhaustive]
116pub enum GenicamError {
117    /// Wrapper around GenApi errors produced by the nodemap.
118    #[error(transparent)]
119    GenApi(#[from] GenApiError),
120    /// Transport level failure while accessing registers.
121    #[error("transport: {0}")]
122    Transport(String),
123    /// Parsing a user supplied value failed.
124    #[error("parse error: {0}")]
125    Parse(String),
126    /// Required chunk feature missing from the nodemap.
127    #[error("chunk feature '{0}' not found; verify camera supports chunk data")]
128    MissingChunkFeature(String),
129    /// The camera reported a pixel format without a conversion path.
130    #[error("unsupported pixel format: {0}")]
131    UnsupportedPixelFormat(viva_pfnc::PixelFormat),
132}
133
134impl GenicamError {
135    fn parse<S: Into<String>>(msg: S) -> Self {
136        GenicamError::Parse(msg.into())
137    }
138
139    fn transport<S: Into<String>>(msg: S) -> Self {
140        GenicamError::Transport(msg.into())
141    }
142}
143
144/// Camera facade combining a nodemap with a transport implementing [`RegisterIo`].
145#[derive(Debug)]
146pub struct Camera<T: RegisterIo> {
147    transport: T,
148    nodemap: NodeMap,
149    time_sync: TimeSync,
150}
151
152impl<T: RegisterIo> Camera<T> {
153    /// Create a new camera wrapper from a transport and a nodemap.
154    pub fn new(transport: T, nodemap: NodeMap) -> Self {
155        Self {
156            transport,
157            nodemap,
158            time_sync: TimeSync::with_capacity(64),
159        }
160    }
161
162    #[inline]
163    fn with_map<R>(&mut self, f: impl FnOnce(&mut NodeMap, &T) -> R) -> R {
164        let transport = &self.transport;
165        let nodemap = &mut self.nodemap;
166        f(nodemap, transport)
167    }
168
169    /// Return a reference to the underlying transport.
170    pub fn transport(&self) -> &T {
171        &self.transport
172    }
173
174    /// Return a mutable reference to the underlying transport.
175    pub fn transport_mut(&mut self) -> &mut T {
176        &mut self.transport
177    }
178
179    /// Access the nodemap metadata.
180    pub fn nodemap(&self) -> &NodeMap {
181        &self.nodemap
182    }
183
184    /// Mutable access to the nodemap.
185    pub fn nodemap_mut(&mut self) -> &mut NodeMap {
186        &mut self.nodemap
187    }
188
189    /// List available entries for an enumeration feature.
190    pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenicamError> {
191        self.nodemap.enum_entries(name).map_err(Into::into)
192    }
193
194    /// Retrieve a feature value as a string using the nodemap type to format it.
195    pub fn get(&self, name: &str) -> Result<String, GenicamError> {
196        match self.nodemap.node(name) {
197            Some(Node::Integer(_)) => {
198                Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
199            }
200            Some(Node::Float(_)) => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
201            Some(Node::Enum(_)) => self
202                .nodemap
203                .get_enum(name, &self.transport)
204                .map_err(Into::into),
205            Some(Node::Boolean(_)) => Ok(self.nodemap.get_bool(name, &self.transport)?.to_string()),
206            Some(Node::SwissKnife(sk)) => match sk.output {
207                SkOutput::Float => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
208                SkOutput::Integer => {
209                    Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
210                }
211            },
212            Some(Node::Converter(conv)) => match conv.output {
213                SkOutput::Float => Ok(self
214                    .nodemap
215                    .get_converter(name, &self.transport)?
216                    .to_string()),
217                SkOutput::Integer => {
218                    Ok((self.nodemap.get_converter(name, &self.transport)? as i64).to_string())
219                }
220            },
221            Some(Node::IntConverter(_)) => Ok(self
222                .nodemap
223                .get_int_converter(name, &self.transport)?
224                .to_string()),
225            Some(Node::String(_)) => self
226                .nodemap
227                .get_string(name, &self.transport)
228                .map_err(Into::into),
229            Some(Node::Command(_)) => {
230                Err(GenicamError::GenApi(GenApiError::Type(name.to_string())))
231            }
232            Some(Node::Category(_)) => Ok(String::new()),
233            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
234        }
235    }
236
237    /// Set a feature value using a string representation.
238    pub fn set(&mut self, name: &str, value: &str) -> Result<(), GenicamError> {
239        match self.nodemap.node(name) {
240            Some(Node::Integer(_)) => {
241                let parsed: i64 = value
242                    .parse()
243                    .map_err(|_| GenicamError::parse(format!("invalid integer for {name}")))?;
244                self.nodemap
245                    .set_integer(name, parsed, &self.transport)
246                    .map_err(Into::into)
247            }
248            Some(Node::Float(_)) => {
249                let parsed: f64 = value
250                    .parse()
251                    .map_err(|_| GenicamError::parse(format!("invalid float for {name}")))?;
252                self.nodemap
253                    .set_float(name, parsed, &self.transport)
254                    .map_err(Into::into)
255            }
256            Some(Node::Enum(_)) => self
257                .nodemap
258                .set_enum(name, value, &self.transport)
259                .map_err(Into::into),
260            Some(Node::Boolean(_)) => {
261                let parsed = parse_bool(value).ok_or_else(|| {
262                    GenicamError::parse(format!("invalid boolean for {name}: {value}"))
263                })?;
264                self.nodemap
265                    .set_bool(name, parsed, &self.transport)
266                    .map_err(Into::into)
267            }
268            Some(Node::SwissKnife(_)) => Err(GenApiError::Type(name.to_string()).into()),
269            Some(Node::Converter(_)) => {
270                // Converters are read-only from the user perspective
271                // (they transform values from underlying nodes)
272                Err(GenApiError::Type(name.to_string()).into())
273            }
274            Some(Node::IntConverter(_)) => Err(GenApiError::Type(name.to_string()).into()),
275            Some(Node::String(_)) => self
276                .nodemap
277                .set_string(name, value, &self.transport)
278                .map_err(Into::into),
279            Some(Node::Command(_)) => self
280                .nodemap
281                .exec_command(name, &self.transport)
282                .map_err(Into::into),
283            Some(Node::Category(_)) => Err(GenApiError::Type(name.to_string()).into()),
284            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
285        }
286    }
287
288    /// Convenience wrapper for exposure time features expressed in microseconds.
289    pub fn set_exposure_time_us(&mut self, value: f64) -> Result<(), GenicamError> {
290        // Use SFNC name directly to avoid cross-crate constant lookup issues in docs
291        self.set_float_feature("ExposureTime", value)
292    }
293
294    /// Convenience wrapper for gain features expressed in decibel.
295    pub fn set_gain_db(&mut self, value: f64) -> Result<(), GenicamError> {
296        self.set_float_feature("Gain", value)
297    }
298
299    fn set_float_feature(&mut self, name: &str, value: f64) -> Result<(), GenicamError> {
300        match self.nodemap.node(name) {
301            Some(Node::Float(_)) => self
302                .nodemap
303                .set_float(name, value, &self.transport)
304                .map_err(Into::into),
305            Some(_) => Err(GenApiError::Type(name.to_string()).into()),
306            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
307        }
308    }
309
310    /// Capture device/host timestamp pairs and fit a mapping model.
311    pub async fn time_calibrate(
312        &mut self,
313        samples: usize,
314        interval_ms: u64,
315    ) -> Result<(), GenicamError> {
316        if samples < 2 {
317            return Err(GenicamError::transport(
318                "time calibration requires at least two samples",
319            ));
320        }
321
322        let cap = samples.max(self.time_sync.capacity());
323        self.time_sync = TimeSync::with_capacity(cap);
324
325        let latch_cmd = self.find_alias(viva_sfnc::TS_LATCH_CMDS);
326        let value_node = self
327            .find_alias(viva_sfnc::TS_VALUE_NODES)
328            .ok_or_else(|| GenApiError::NodeNotFound("TimestampValue".into()))?;
329
330        let mut freq_hz = if let Some(name) = self.find_alias(viva_sfnc::TS_FREQ_NODES) {
331            match self.nodemap.get_integer(name, &self.transport) {
332                Ok(value) if value > 0 => Some(value as f64),
333                Ok(_) => None,
334                Err(err) => {
335                    debug!(node = name, error = %err, "failed to read timestamp frequency");
336                    None
337                }
338            }
339        } else {
340            None
341        };
342
343        info!(samples, interval_ms, "starting time calibration");
344        let mut first_sample: Option<(u64, Instant)> = None;
345        let mut last_sample: Option<(u64, Instant)> = None;
346
347        for idx in 0..samples {
348            if let Some(cmd) = latch_cmd {
349                self.nodemap
350                    .exec_command(cmd, &self.transport)
351                    .map_err(GenicamError::from)?;
352            }
353
354            let raw_ticks = self
355                .nodemap
356                .get_integer(value_node, &self.transport)
357                .map_err(GenicamError::from)?;
358            let dev_ticks = u64::try_from(raw_ticks).map_err(|_| {
359                GenicamError::transport("timestamp value is negative; unsupported camera")
360            })?;
361            let host = Instant::now();
362            self.time_sync.update(dev_ticks, host);
363            if idx == 0 {
364                first_sample = Some((dev_ticks, host));
365            }
366            last_sample = Some((dev_ticks, host));
367            if let Some(origin) = self.time_sync.origin_instant() {
368                let ns = host.duration_since(origin).as_nanos();
369                debug!(
370                    sample = idx,
371                    ticks = dev_ticks,
372                    host_ns = ns,
373                    "timestamp sample"
374                );
375            } else {
376                debug!(sample = idx, ticks = dev_ticks, "timestamp sample");
377            }
378
379            if interval_ms > 0 && idx + 1 < samples {
380                sleep(Duration::from_millis(interval_ms)).await;
381            }
382        }
383
384        if freq_hz.is_none()
385            && let (Some((first_ticks, first_host)), Some((last_ticks, last_host))) =
386                (first_sample, last_sample)
387            && last_ticks > first_ticks
388            && let Some(delta) = last_host.checked_duration_since(first_host)
389        {
390            let secs = delta.as_secs_f64();
391            if secs > 0.0 {
392                freq_hz = Some((last_ticks - first_ticks) as f64 / secs);
393            }
394        }
395
396        let (a, b) = self
397            .time_sync
398            .fit(freq_hz)
399            .ok_or_else(|| GenicamError::transport("insufficient samples for timestamp fit"))?;
400
401        if let Some(freq) = freq_hz {
402            info!(freq_hz = freq, a, b, "time calibration complete");
403        } else {
404            info!(a, b, "time calibration complete");
405        }
406
407        Ok(())
408    }
409
410    /// Map device tick counters to host time using the fitted model.
411    pub fn map_dev_ts(&self, dev_ticks: u64) -> SystemTime {
412        self.time_sync.to_host_time(dev_ticks)
413    }
414
415    /// Inspect the timestamp synchroniser state.
416    pub fn time_sync(&self) -> &TimeSync {
417        &self.time_sync
418    }
419
420    /// Reset the device timestamp counter when supported by the camera.
421    pub fn time_reset(&mut self) -> Result<(), GenicamError> {
422        if let Some(cmd) = self.find_alias(viva_sfnc::TS_RESET_CMDS) {
423            self.nodemap
424                .exec_command(cmd, &self.transport)
425                .map_err(GenicamError::from)?;
426            self.time_sync = TimeSync::with_capacity(self.time_sync.capacity());
427            info!(command = cmd, "timestamp counter reset");
428        }
429        Ok(())
430    }
431
432    /// Trigger acquisition start via the SFNC command feature.
433    pub fn acquisition_start(&mut self) -> Result<(), GenicamError> {
434        self.nodemap
435            .exec_command("AcquisitionStart", &self.transport)
436            .map_err(Into::into)
437    }
438
439    /// Trigger acquisition stop via the SFNC command feature.
440    pub fn acquisition_stop(&mut self) -> Result<(), GenicamError> {
441        self.nodemap
442            .exec_command("AcquisitionStop", &self.transport)
443            .map_err(Into::into)
444    }
445
446    /// Configure chunk mode and enable the requested selectors.
447    pub fn configure_chunks(&mut self, cfg: &ChunkConfig) -> Result<(), GenicamError> {
448        self.ensure_chunk_feature(viva_sfnc::CHUNK_MODE_ACTIVE)?;
449        self.ensure_chunk_feature(viva_sfnc::CHUNK_SELECTOR)?;
450        self.ensure_chunk_feature(viva_sfnc::CHUNK_ENABLE)?;
451
452        // SAFE: split-borrow distinct fields of `self`
453        self.with_map(|nm, tr| {
454            nm.set_bool(viva_sfnc::CHUNK_MODE_ACTIVE, cfg.active, tr)?;
455            for s in &cfg.selectors {
456                nm.set_enum(viva_sfnc::CHUNK_SELECTOR, s, tr)?;
457                nm.set_bool(viva_sfnc::CHUNK_ENABLE, cfg.active, tr)?;
458            }
459            Ok(())
460        })
461    }
462
463    /// Configure the GVCP message channel and enable delivery of the requested events.
464    pub async fn configure_events(
465        &mut self,
466        local_ip: Ipv4Addr,
467        port: u16,
468        enable_ids: &[&str],
469    ) -> Result<(), GenicamError> {
470        info!(%local_ip, port, "configuring GVCP events");
471        // Pre-compute aliases before taking a mutable borrow of the nodemap
472        let msg_sel = self.find_alias(viva_sfnc::MSG_SEL);
473        let msg_ip = self.find_alias(viva_sfnc::MSG_IP);
474        let msg_port = self.find_alias(viva_sfnc::MSG_PORT);
475        let msg_en = self.find_alias(viva_sfnc::MSG_EN);
476
477        let channel_configured = self.with_map(|nodemap, transport| {
478            let mut ok = true;
479
480            if let Some(selector) = msg_sel {
481                match nodemap.enum_entries(selector) {
482                    Ok(entries) => {
483                        if let Some(entry) = entries.into_iter().next() {
484                            if let Err(err) = nodemap.set_enum(selector, &entry, transport) {
485                                warn!(node = selector, error = %err, "failed to set message selector");
486                                ok = false;
487                            }
488                        } else {
489                            warn!(node = selector, "message selector missing entries");
490                            ok = false;
491                        }
492                    }
493                    Err(err) => {
494                        warn!(feature = selector, error = %err, "failed to query message selector");
495                        ok = false;
496                    }
497                }
498            } else {
499                ok = false;
500            }
501
502            if let Some(node) = msg_ip {
503                let value = u32::from(local_ip) as i64;
504                if let Err(err) = nodemap.set_integer(node, value, transport) {
505                    warn!(feature = node, error = %err, "failed to write message IP");
506                    ok = false;
507                }
508            } else {
509                ok = false;
510            }
511
512            if let Some(node) = msg_port {
513                if let Err(err) = nodemap.set_integer(node, port as i64, transport) {
514                    warn!(feature = node, error = %err, "failed to write message port");
515                    ok = false;
516                }
517            } else {
518                ok = false;
519            }
520
521            if let Some(node) = msg_en {
522                if let Err(err) = nodemap.set_bool(node, true, transport) {
523                    warn!(feature = node, error = %err, "failed to enable message channel");
524                    ok = false;
525                }
526            } else {
527                ok = false;
528            }
529
530            ok
531        });
532
533        if !channel_configured {
534            configure_message_channel_fallback(&self.transport, local_ip, port)?;
535        }
536
537        let mut used_sfnc = self.nodemap.node(viva_sfnc::EVENT_SELECTOR).is_some()
538            && self.nodemap.node(viva_sfnc::EVENT_NOTIFICATION).is_some();
539
540        used_sfnc = self.with_map(|nodemap, transport| {
541            if !used_sfnc {
542                return false;
543            }
544            for &name in enable_ids {
545                if let Err(err) = nodemap.set_enum(viva_sfnc::EVENT_SELECTOR, name, transport) {
546                    warn!(event = name, error = %err, "failed to select event via SFNC");
547                    return false;
548                }
549                if let Err(err) = nodemap.set_enum(
550                    viva_sfnc::EVENT_NOTIFICATION,
551                    viva_sfnc::EVENT_NOTIF_ON,
552                    transport,
553                ) {
554                    warn!(event = name, error = %err, "failed to enable event via SFNC");
555                    return false;
556                }
557            }
558            true
559        });
560
561        if !used_sfnc {
562            for &name in enable_ids {
563                let Some(event_id) = parse_event_id(name) else {
564                    return Err(GenicamError::transport(format!(
565                        "event '{name}' missing from nodemap and not numeric"
566                    )));
567                };
568                enable_event_fallback(&self.transport, event_id, true)?;
569            }
570        }
571
572        Ok(())
573    }
574
575    /// Configure the stream channel for multicast delivery.
576    pub fn configure_stream_multicast(
577        &mut self,
578        stream_idx: u32,
579        group: Ipv4Addr,
580        port: u16,
581    ) -> Result<(), GenicamError> {
582        if (group.octets()[0] & 0xF0) != 0xE0 {
583            return Err(GenicamError::transport(
584                "multicast group must be within 224.0.0.0/4",
585            ));
586        }
587        info!(stream_idx, %group, port, "configuring multicast stream");
588
589        // Precompute node names before taking &mut self.nodemap
590        let dest_addr_node = self.find_alias(viva_sfnc::SCP_DEST_ADDR);
591        let host_port_node = self.find_alias(viva_sfnc::SCP_HOST_PORT);
592        let mcast_en_node = self.find_alias(viva_sfnc::MULTICAST_ENABLE);
593
594        let mut used_sfnc = true;
595        self.with_map(|nm, tr| {
596            if nm.node(viva_sfnc::STREAM_CH_SELECTOR).is_some() {
597                if let Err(err) =
598                    nm.set_integer(viva_sfnc::STREAM_CH_SELECTOR, stream_idx as i64, tr)
599                {
600                    warn!(
601                        channel = stream_idx,
602                        error = %err,
603                        "failed to select stream channel via SFNC"
604                    );
605                    used_sfnc = false;
606                }
607            } else {
608                used_sfnc = false;
609            }
610
611            if let Some(node) = dest_addr_node {
612                if let Err(err) = nm.set_integer(node, u32::from(group) as i64, tr) {
613                    warn!(feature = node, error = %err, "failed to write multicast address");
614                    used_sfnc = false;
615                }
616            } else {
617                used_sfnc = false;
618            }
619
620            if let Some(node) = host_port_node {
621                if let Err(err) = nm.set_integer(node, port as i64, tr) {
622                    warn!(feature = node, error = %err, "failed to write multicast port");
623                    used_sfnc = false;
624                }
625            } else {
626                used_sfnc = false;
627            }
628
629            if let Some(node) = mcast_en_node {
630                let _ = nm.set_bool(node, true, tr);
631            }
632        });
633
634        if !used_sfnc {
635            let base = gvcp_consts::STREAM_CHANNEL_BASE
636                + stream_idx as u64 * gvcp_consts::STREAM_CHANNEL_STRIDE;
637            let addr_reg = base + gvcp_consts::STREAM_DESTINATION_ADDRESS;
638            self.transport
639                .write(addr_reg, &group.octets())
640                .map_err(|err| GenicamError::transport(format!("write multicast addr: {err}")))?;
641            let port_reg = base + gvcp_consts::STREAM_DESTINATION_PORT;
642            self.transport
643                .write(port_reg, &port.to_be_bytes())
644                .map_err(|err| GenicamError::transport(format!("write multicast port: {err}")))?;
645            info!(
646                stream_idx,
647                %group,
648                port,
649                "configured multicast destination via raw registers"
650            );
651        } else {
652            info!(
653                stream_idx,
654                %group,
655                port,
656                "configured multicast destination via SFNC"
657            );
658        }
659
660        Ok(())
661    }
662
663    /// Open a GVCP event stream bound to the provided local endpoint.
664    pub async fn open_event_stream(
665        &self,
666        local_ip: Ipv4Addr,
667        port: u16,
668    ) -> Result<EventStream, GenicamError> {
669        let socket = bind_event_socket_internal(IpAddr::V4(local_ip), port).await?;
670        let time_sync = if !self.time_sync.is_empty() {
671            Some(Arc::new(self.time_sync.clone()))
672        } else {
673            None
674        };
675        Ok(EventStream::new(socket, time_sync))
676    }
677
678    fn ensure_chunk_feature(&self, name: &str) -> Result<(), GenicamError> {
679        if self.nodemap.node(name).is_none() {
680            return Err(GenicamError::MissingChunkFeature(name.to_string()));
681        }
682        Ok(())
683    }
684
685    fn find_alias(&self, names: &[&'static str]) -> Option<&'static str> {
686        names
687            .iter()
688            .copied()
689            .find(|name| self.nodemap.node(name).is_some())
690    }
691}
692
693/// Configuration for enabling chunk data via SFNC features.
694#[derive(Debug, Clone, Default)]
695pub struct ChunkConfig {
696    /// Names of chunk selectors that should be enabled on the device.
697    pub selectors: Vec<String>,
698    /// Whether chunk mode should be active after configuration.
699    pub active: bool,
700}
701
702/// Blocking adapter turning an asynchronous [`GigeDevice`] into a [`RegisterIo`]
703/// implementation.
704///
705/// The adapter uses [`tokio::runtime::Handle::block_on`] to synchronously wait
706/// on GVCP register transactions.  When called from within a tokio runtime
707/// context it automatically wraps the call in [`tokio::task::block_in_place`]
708/// so the executor can keep making progress.  This makes it safe to call from
709/// both async and plain synchronous contexts.
710///
711/// **Note:** `block_in_place` requires a multi-thread runtime.  Using a
712/// `current_thread` runtime will still panic.
713pub struct GigeRegisterIo {
714    handle: tokio::runtime::Handle,
715    device: Mutex<GigeDevice>,
716}
717
718impl GigeRegisterIo {
719    /// Create a new adapter using the provided runtime handle and device.
720    pub fn new(handle: tokio::runtime::Handle, device: GigeDevice) -> Self {
721        Self {
722            handle,
723            device: Mutex::new(device),
724        }
725    }
726
727    /// Lock the underlying [`GigeDevice`] for direct async operations.
728    ///
729    /// This is intended for callers that need the raw device (e.g. stream
730    /// channel configuration) while the `Camera` wrapper holds the transport.
731    pub fn lock_device(&self) -> Result<MutexGuard<'_, GigeDevice>, GenicamError> {
732        self.device
733            .lock()
734            .map_err(|_| GenicamError::transport("gige device mutex poisoned"))
735    }
736
737    fn lock(&self) -> Result<MutexGuard<'_, GigeDevice>, GenApiError> {
738        self.device
739            .lock()
740            .map_err(|_| GenApiError::Io("gige device mutex poisoned".into()))
741    }
742}
743
744impl RegisterIo for GigeRegisterIo {
745    fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
746        let mut device = self.lock()?;
747        let fut = device.read_mem(addr, len);
748        if tokio::runtime::Handle::try_current().is_ok() {
749            tokio::task::block_in_place(|| self.handle.block_on(fut))
750        } else {
751            self.handle.block_on(fut)
752        }
753        .map_err(|err| GenApiError::Io(err.to_string()))
754    }
755
756    fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
757        let mut device = self.lock()?;
758        let fut = device.write_mem(addr, data);
759        if tokio::runtime::Handle::try_current().is_ok() {
760            tokio::task::block_in_place(|| self.handle.block_on(fut))
761        } else {
762            self.handle.block_on(fut)
763        }
764        .map_err(|err| GenApiError::Io(err.to_string()))
765    }
766}
767
768/// Connect to a GigE Vision camera and return a fully configured [`Camera`].
769///
770/// This convenience function handles all connection boilerplate:
771/// 1. Opens a GVCP control connection to the device
772/// 2. Fetches and parses the GenApi XML from the camera
773/// 3. Builds the nodemap
774/// 4. Creates the transport adapter
775///
776/// # Example
777///
778/// ```rust,ignore
779/// use std::time::Duration;
780/// use viva_genicam::{gige, connect_gige};
781///
782/// let devices = gige::discover(Duration::from_millis(500)).await?;
783/// let device = devices.into_iter().next().expect("no camera found");
784/// let mut camera = connect_gige(&device).await?;
785/// camera.set("ExposureTime", "5000")?;
786/// ```
787pub async fn connect_gige(
788    device: &gige::DeviceInfo,
789) -> Result<Camera<GigeRegisterIo>, GenicamError> {
790    let (camera, _xml) = connect_gige_with_xml(device).await?;
791    Ok(camera)
792}
793
794/// Connect to a GigE Vision camera and return both a [`Camera`] and the raw
795/// GenICam XML string fetched from the device.
796///
797/// This is useful when the caller needs the XML for purposes beyond node
798/// evaluation (e.g. forwarding it over a network API).
799pub async fn connect_gige_with_xml(
800    device: &gige::DeviceInfo,
801) -> Result<(Camera<GigeRegisterIo>, String), GenicamError> {
802    use std::net::{IpAddr, SocketAddr};
803    use std::sync::Arc;
804    use tokio::sync::Mutex as AsyncMutex;
805
806    let control_addr = SocketAddr::new(IpAddr::V4(device.ip), gige::GVCP_PORT);
807    info!(%control_addr, "connecting to GigE Vision camera");
808
809    let mut device = gige::GigeDevice::open(control_addr)
810        .await
811        .map_err(|e| GenicamError::transport(e.to_string()))?;
812
813    // Claim control privilege (required before configuration and streaming).
814    device
815        .claim_control()
816        .await
817        .map_err(|e| GenicamError::transport(e.to_string()))?;
818
819    let control = Arc::new(AsyncMutex::new(device));
820
821    // Fetch and parse the GenApi XML.
822    let xml = viva_genapi_xml::fetch_and_load_xml({
823        let control = control.clone();
824        move |address, length| {
825            let control = control.clone();
826            async move {
827                let mut dev = control.lock().await;
828                dev.read_mem(address, length)
829                    .await
830                    .map_err(|err| viva_genapi_xml::XmlError::Transport(err.to_string()))
831            }
832        }
833    })
834    .await
835    .map_err(|e| GenicamError::transport(e.to_string()))?;
836
837    let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
838    let nodemap = genapi::NodeMap::from(model);
839
840    // Extract the device and create the blocking adapter.
841    let handle = tokio::runtime::Handle::current();
842    let control_device = Arc::try_unwrap(control)
843        .map_err(|_| GenicamError::transport("control connection still in use"))?
844        .into_inner();
845    let transport = GigeRegisterIo::new(handle, control_device);
846
847    info!("GigE camera connected successfully");
848    Ok((Camera::new(transport, nodemap), xml))
849}
850
851// ---------------------------------------------------------------------------
852// USB3 Vision transport (behind `u3v` feature)
853// ---------------------------------------------------------------------------
854
855/// Blocking [`RegisterIo`] adapter wrapping a [`U3vDevice`](u3v::device::U3vDevice).
856///
857/// Generic over `T: UsbTransfer` so that real hardware (`RusbTransfer`) and
858/// test doubles (`MockUsbTransfer`, `FakeU3vTransport`) all work through the
859/// same code path. USB operations are inherently synchronous, so this adapter
860/// simply forwards calls through a `Mutex` for thread safety.
861#[cfg(feature = "u3v")]
862#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
863pub struct U3vRegisterIo<T: u3v::usb::UsbTransfer + 'static> {
864    device: Mutex<u3v::device::U3vDevice<T>>,
865}
866
867#[cfg(feature = "u3v")]
868impl<T: u3v::usb::UsbTransfer + 'static> U3vRegisterIo<T> {
869    /// Create a new adapter wrapping a [`U3vDevice`](u3v::device::U3vDevice).
870    pub fn new(device: u3v::device::U3vDevice<T>) -> Self {
871        Self {
872            device: Mutex::new(device),
873        }
874    }
875
876    /// Lock the underlying device for direct access (e.g. stream configuration).
877    pub fn lock_device(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenicamError> {
878        self.device
879            .lock()
880            .map_err(|_| GenicamError::transport("u3v device mutex poisoned"))
881    }
882
883    fn lock(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenApiError> {
884        self.device
885            .lock()
886            .map_err(|_| GenApiError::Io("u3v device mutex poisoned".into()))
887    }
888}
889
890#[cfg(feature = "u3v")]
891impl<T: u3v::usb::UsbTransfer + 'static> RegisterIo for U3vRegisterIo<T> {
892    fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
893        let mut device = self.lock()?;
894        device
895            .read_mem(addr, len)
896            .map_err(|e| GenApiError::Io(e.to_string()))
897    }
898
899    fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
900        let mut device = self.lock()?;
901        device
902            .write_mem(addr, data)
903            .map_err(|e| GenApiError::Io(e.to_string()))
904    }
905}
906
907/// Connect to a USB3 Vision camera and return a fully configured [`Camera`].
908///
909/// This convenience function handles all connection boilerplate:
910/// 1. Opens the USB device and claims U3V interfaces
911/// 2. Reads ABRM/SBRM bootstrap registers
912/// 3. Fetches and parses the GenApi XML from the manifest table
913/// 4. Builds the nodemap and creates the transport adapter
914///
915/// # Example
916///
917/// ```rust,ignore
918/// use viva_genicam::{u3v, connect_u3v};
919///
920/// let devices = u3v::discovery::discover()?;
921/// let device = devices.into_iter().next().expect("no U3V camera found");
922/// let mut camera = connect_u3v(&device)?;
923/// camera.set("ExposureTime", "5000")?;
924/// ```
925#[cfg(feature = "u3v-usb")]
926#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
927pub fn connect_u3v(
928    device: &u3v::discovery::U3vDeviceInfo,
929) -> Result<Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, GenicamError> {
930    let (camera, _xml) = connect_u3v_with_xml(device)?;
931    Ok(camera)
932}
933
934/// Connect to a USB3 Vision camera and return both a [`Camera`] and the raw
935/// GenICam XML string fetched from the device.
936#[cfg(feature = "u3v-usb")]
937#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
938pub fn connect_u3v_with_xml(
939    device_info: &u3v::discovery::U3vDeviceInfo,
940) -> Result<(Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, String), GenicamError> {
941    info!(
942        vendor_id = device_info.vendor_id,
943        product_id = device_info.product_id,
944        "connecting to USB3 Vision camera"
945    );
946
947    let mut device = u3v::device::U3vDevice::open_device(device_info)
948        .map_err(|e| GenicamError::transport(e.to_string()))?;
949
950    let xml = device
951        .fetch_xml()
952        .map_err(|e| GenicamError::transport(e.to_string()))?;
953
954    let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
955    let nodemap = genapi::NodeMap::from(model);
956    let transport = U3vRegisterIo::new(device);
957
958    info!("USB3 Vision camera connected successfully");
959    Ok((Camera::new(transport, nodemap), xml))
960}
961
962/// Create a [`Camera`] from an already-opened [`U3vDevice`](u3v::device::U3vDevice)
963/// with any [`UsbTransfer`](u3v::usb::UsbTransfer) backend.
964///
965/// This is the generic entry point for testing with fake or mock transports.
966/// The device must have been opened and bootstrapped (ABRM/SBRM read)
967/// before calling this function.
968#[cfg(feature = "u3v")]
969#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
970pub fn open_u3v_device<T: u3v::usb::UsbTransfer + 'static>(
971    mut device: u3v::device::U3vDevice<T>,
972) -> Result<(Camera<U3vRegisterIo<T>>, String), GenicamError> {
973    let xml = device
974        .fetch_xml()
975        .map_err(|e| GenicamError::transport(e.to_string()))?;
976    let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
977    let nodemap = genapi::NodeMap::from(model);
978    let transport = U3vRegisterIo::new(device);
979    Ok((Camera::new(transport, nodemap), xml))
980}
981
982fn parse_bool(value: &str) -> Option<bool> {
983    match value.trim().to_ascii_lowercase().as_str() {
984        "1" | "true" => Some(true),
985        "0" | "false" => Some(false),
986        _ => None,
987    }
988}