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 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: genicam::Camera<genicam::GigeRegisterIo>,
42//! # ) -> Result<(), 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 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 genapi_core as genapi;
73pub use genicp;
74pub use pfnc;
75pub use sfnc;
76pub use tl_gige as gige;
77
78pub mod chunks;
79pub mod events;
80pub mod frame;
81pub mod stream;
82pub mod time;
83
84use std::net::{IpAddr, Ipv4Addr};
85use std::sync::{Arc, Mutex, MutexGuard};
86use std::time::{Duration, Instant, SystemTime};
87
88use crate::events::{
89    bind_socket as bind_event_socket_internal,
90    configure_message_channel_raw as configure_message_channel_fallback,
91    enable_event_raw as enable_event_fallback, parse_event_id,
92};
93use crate::genapi::{GenApiError, Node, NodeMap, RegisterIo, SkOutput};
94use gige::gvcp::consts as gvcp_consts;
95use gige::GigeDevice;
96use thiserror::Error;
97use tokio::time::sleep;
98use tracing::{debug, info, warn};
99
100pub use chunks::{parse_chunk_bytes, ChunkKind, ChunkMap, ChunkValue};
101pub use events::{Event, EventStream};
102pub use frame::Frame;
103pub use gige::action::{AckSummary, ActionParams};
104pub use stream::{Stream, StreamBuilder, StreamDest};
105pub use time::TimeSync;
106
107use crate::time::TimeSync as TimeSyncModel;
108
109/// Error type produced by the high level GenICam facade.
110#[derive(Debug, Error)]
111pub enum GenicamError {
112    /// Wrapper around GenApi errors produced by the nodemap.
113    #[error(transparent)]
114    GenApi(#[from] GenApiError),
115    /// Transport level failure while accessing registers.
116    #[error("transport: {0}")]
117    Transport(String),
118    /// Parsing a user supplied value failed.
119    #[error("parse error: {0}")]
120    Parse(String),
121    /// Required chunk feature missing from the nodemap.
122    #[error("chunk feature '{0}' not found; verify camera supports chunk data")]
123    MissingChunkFeature(String),
124    /// The camera reported a pixel format without a conversion path.
125    #[error("unsupported pixel format: {0}")]
126    UnsupportedPixelFormat(pfnc::PixelFormat),
127}
128
129impl GenicamError {
130    fn parse<S: Into<String>>(msg: S) -> Self {
131        GenicamError::Parse(msg.into())
132    }
133
134    fn transport<S: Into<String>>(msg: S) -> Self {
135        GenicamError::Transport(msg.into())
136    }
137}
138
139/// Camera facade combining a nodemap with a transport implementing [`RegisterIo`].
140#[derive(Debug)]
141pub struct Camera<T: RegisterIo> {
142    transport: T,
143    nodemap: NodeMap,
144    time_sync: TimeSyncModel,
145}
146
147impl<T: RegisterIo> Camera<T> {
148    /// Create a new camera wrapper from a transport and a nodemap.
149    pub fn new(transport: T, nodemap: NodeMap) -> Self {
150        Self {
151            transport,
152            nodemap,
153            time_sync: TimeSyncModel::new(64),
154        }
155    }
156
157    #[inline]
158    fn with_map<R>(&mut self, f: impl FnOnce(&mut NodeMap, &T) -> R) -> R {
159        let transport = &self.transport;
160        let nodemap = &mut self.nodemap;
161        f(nodemap, transport)
162    }
163
164    /// Return a reference to the underlying transport.
165    pub fn transport(&self) -> &T {
166        &self.transport
167    }
168
169    /// Return a mutable reference to the underlying transport.
170    pub fn transport_mut(&mut self) -> &mut T {
171        &mut self.transport
172    }
173
174    /// Access the nodemap metadata.
175    pub fn nodemap(&self) -> &NodeMap {
176        &self.nodemap
177    }
178
179    /// Mutable access to the nodemap.
180    pub fn nodemap_mut(&mut self) -> &mut NodeMap {
181        &mut self.nodemap
182    }
183
184    /// List available entries for an enumeration feature.
185    pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenicamError> {
186        self.nodemap.enum_entries(name).map_err(Into::into)
187    }
188
189    /// Retrieve a feature value as a string using the nodemap type to format it.
190    pub fn get(&self, name: &str) -> Result<String, GenicamError> {
191        match self.nodemap.node(name) {
192            Some(Node::Integer(_)) => {
193                Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
194            }
195            Some(Node::Float(_)) => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
196            Some(Node::Enum(_)) => self
197                .nodemap
198                .get_enum(name, &self.transport)
199                .map_err(Into::into),
200            Some(Node::Boolean(_)) => Ok(self.nodemap.get_bool(name, &self.transport)?.to_string()),
201            Some(Node::SwissKnife(sk)) => match sk.output {
202                SkOutput::Float => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
203                SkOutput::Integer => {
204                    Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
205                }
206            },
207            Some(Node::Command(_)) => {
208                Err(GenicamError::GenApi(GenApiError::Type(name.to_string())))
209            }
210            Some(Node::Category(_)) => Ok(String::new()),
211            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
212        }
213    }
214
215    /// Set a feature value using a string representation.
216    pub fn set(&mut self, name: &str, value: &str) -> Result<(), GenicamError> {
217        match self.nodemap.node(name) {
218            Some(Node::Integer(_)) => {
219                let parsed: i64 = value
220                    .parse()
221                    .map_err(|_| GenicamError::parse(format!("invalid integer for {name}")))?;
222                self.nodemap
223                    .set_integer(name, parsed, &self.transport)
224                    .map_err(Into::into)
225            }
226            Some(Node::Float(_)) => {
227                let parsed: f64 = value
228                    .parse()
229                    .map_err(|_| GenicamError::parse(format!("invalid float for {name}")))?;
230                self.nodemap
231                    .set_float(name, parsed, &self.transport)
232                    .map_err(Into::into)
233            }
234            Some(Node::Enum(_)) => self
235                .nodemap
236                .set_enum(name, value, &self.transport)
237                .map_err(Into::into),
238            Some(Node::Boolean(_)) => {
239                let parsed = parse_bool(value).ok_or_else(|| {
240                    GenicamError::parse(format!("invalid boolean for {name}: {value}"))
241                })?;
242                self.nodemap
243                    .set_bool(name, parsed, &self.transport)
244                    .map_err(Into::into)
245            }
246            Some(Node::SwissKnife(_)) => Err(GenApiError::Type(name.to_string()).into()),
247            Some(Node::Command(_)) => self
248                .nodemap
249                .exec_command(name, &self.transport)
250                .map_err(Into::into),
251            Some(Node::Category(_)) => Err(GenApiError::Type(name.to_string()).into()),
252            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
253        }
254    }
255
256    /// Convenience wrapper for exposure time features expressed in microseconds.
257    pub fn set_exposure_time_us(&mut self, value: f64) -> Result<(), GenicamError> {
258        // Use SFNC name directly to avoid cross-crate constant lookup issues in docs
259        self.set_float_feature("ExposureTime", value)
260    }
261
262    /// Convenience wrapper for gain features expressed in decibel.
263    pub fn set_gain_db(&mut self, value: f64) -> Result<(), GenicamError> {
264        self.set_float_feature("Gain", value)
265    }
266
267    fn set_float_feature(&mut self, name: &str, value: f64) -> Result<(), GenicamError> {
268        match self.nodemap.node(name) {
269            Some(Node::Float(_)) => self
270                .nodemap
271                .set_float(name, value, &self.transport)
272                .map_err(Into::into),
273            Some(_) => Err(GenApiError::Type(name.to_string()).into()),
274            None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
275        }
276    }
277
278    /// Capture device/host timestamp pairs and fit a mapping model.
279    pub async fn time_calibrate(
280        &mut self,
281        samples: usize,
282        interval_ms: u64,
283    ) -> Result<(), GenicamError> {
284        if samples < 2 {
285            return Err(GenicamError::transport(
286                "time calibration requires at least two samples",
287            ));
288        }
289
290        let cap = samples.max(self.time_sync.capacity());
291        self.time_sync = TimeSyncModel::new(cap);
292
293        let latch_cmd = self.find_alias(sfnc::TS_LATCH_CMDS);
294        let value_node = self
295            .find_alias(sfnc::TS_VALUE_NODES)
296            .ok_or_else(|| GenApiError::NodeNotFound("TimestampValue".into()))?;
297
298        let mut freq_hz = if let Some(name) = self.find_alias(sfnc::TS_FREQ_NODES) {
299            match self.nodemap.get_integer(name, &self.transport) {
300                Ok(value) if value > 0 => Some(value as f64),
301                Ok(_) => None,
302                Err(err) => {
303                    debug!(node = name, error = %err, "failed to read timestamp frequency");
304                    None
305                }
306            }
307        } else {
308            None
309        };
310
311        info!(samples, interval_ms, "starting time calibration");
312        let mut first_sample: Option<(u64, Instant)> = None;
313        let mut last_sample: Option<(u64, Instant)> = None;
314
315        for idx in 0..samples {
316            if let Some(cmd) = latch_cmd {
317                self.nodemap
318                    .exec_command(cmd, &self.transport)
319                    .map_err(GenicamError::from)?;
320            }
321
322            let raw_ticks = self
323                .nodemap
324                .get_integer(value_node, &self.transport)
325                .map_err(GenicamError::from)?;
326            let dev_ticks = u64::try_from(raw_ticks).map_err(|_| {
327                GenicamError::transport("timestamp value is negative; unsupported camera")
328            })?;
329            let host = Instant::now();
330            self.time_sync.update(dev_ticks, host);
331            if idx == 0 {
332                first_sample = Some((dev_ticks, host));
333            }
334            last_sample = Some((dev_ticks, host));
335            if let Some(origin) = self.time_sync.origin_instant() {
336                let ns = host.duration_since(origin).as_nanos();
337                debug!(
338                    sample = idx,
339                    ticks = dev_ticks,
340                    host_ns = ns,
341                    "timestamp sample"
342                );
343            } else {
344                debug!(sample = idx, ticks = dev_ticks, "timestamp sample");
345            }
346
347            if interval_ms > 0 && idx + 1 < samples {
348                sleep(Duration::from_millis(interval_ms)).await;
349            }
350        }
351
352        if freq_hz.is_none() {
353            if let (Some((first_ticks, first_host)), Some((last_ticks, last_host))) =
354                (first_sample, last_sample)
355            {
356                if last_ticks > first_ticks {
357                    if let Some(delta) = last_host.checked_duration_since(first_host) {
358                        let secs = delta.as_secs_f64();
359                        if secs > 0.0 {
360                            freq_hz = Some((last_ticks - first_ticks) as f64 / secs);
361                        }
362                    }
363                }
364            }
365        }
366
367        let (a, b) = self
368            .time_sync
369            .fit(freq_hz)
370            .ok_or_else(|| GenicamError::transport("insufficient samples for timestamp fit"))?;
371
372        if let Some(freq) = freq_hz {
373            info!(freq_hz = freq, a, b, "time calibration complete");
374        } else {
375            info!(a, b, "time calibration complete");
376        }
377
378        Ok(())
379    }
380
381    /// Map device tick counters to host time using the fitted model.
382    pub fn map_dev_ts(&self, dev_ticks: u64) -> SystemTime {
383        self.time_sync.to_host_time(dev_ticks)
384    }
385
386    /// Inspect the timestamp synchroniser state.
387    pub fn time_sync(&self) -> &TimeSync {
388        &self.time_sync
389    }
390
391    /// Reset the device timestamp counter when supported by the camera.
392    pub fn time_reset(&mut self) -> Result<(), GenicamError> {
393        if let Some(cmd) = self.find_alias(sfnc::TS_RESET_CMDS) {
394            self.nodemap
395                .exec_command(cmd, &self.transport)
396                .map_err(GenicamError::from)?;
397            self.time_sync = TimeSyncModel::new(self.time_sync.capacity());
398            info!(command = cmd, "timestamp counter reset");
399        }
400        Ok(())
401    }
402
403    /// Trigger acquisition start via the SFNC command feature.
404    pub fn acquisition_start(&mut self) -> Result<(), GenicamError> {
405        self.nodemap
406            .exec_command("AcquisitionStart", &self.transport)
407            .map_err(Into::into)
408    }
409
410    /// Trigger acquisition stop via the SFNC command feature.
411    pub fn acquisition_stop(&mut self) -> Result<(), GenicamError> {
412        self.nodemap
413            .exec_command("AcquisitionStop", &self.transport)
414            .map_err(Into::into)
415    }
416
417    /// Configure chunk mode and enable the requested selectors.
418    pub fn configure_chunks(&mut self, cfg: &ChunkConfig) -> Result<(), GenicamError> {
419        self.ensure_chunk_feature(sfnc::CHUNK_MODE_ACTIVE)?;
420        self.ensure_chunk_feature(sfnc::CHUNK_SELECTOR)?;
421        self.ensure_chunk_feature(sfnc::CHUNK_ENABLE)?;
422
423        // SAFE: split-borrow distinct fields of `self`
424        self.with_map(|nm, tr| {
425            nm.set_bool(sfnc::CHUNK_MODE_ACTIVE, cfg.active, tr)?;
426            for s in &cfg.selectors {
427                nm.set_enum(sfnc::CHUNK_SELECTOR, s, tr)?;
428                nm.set_bool(sfnc::CHUNK_ENABLE, cfg.active, tr)?;
429            }
430            Ok(())
431        })
432    }
433
434    /// Configure the GVCP message channel and enable delivery of the requested events.
435    pub async fn configure_events(
436        &mut self,
437        local_ip: Ipv4Addr,
438        port: u16,
439        enable_ids: &[&str],
440    ) -> Result<(), GenicamError> {
441        info!(%local_ip, port, "configuring GVCP events");
442        // Pre-compute aliases before taking a mutable borrow of the nodemap
443        let msg_sel = self.find_alias(sfnc::MSG_SEL);
444        let msg_ip = self.find_alias(sfnc::MSG_IP);
445        let msg_port = self.find_alias(sfnc::MSG_PORT);
446        let msg_en = self.find_alias(sfnc::MSG_EN);
447
448        let channel_configured = self.with_map(|nodemap, transport| {
449            let mut ok = true;
450
451            if let Some(selector) = msg_sel {
452                match nodemap.enum_entries(selector) {
453                    Ok(entries) => {
454                        if let Some(entry) = entries.into_iter().next() {
455                            if let Err(err) = nodemap.set_enum(selector, &entry, transport) {
456                                warn!(node = selector, error = %err, "failed to set message selector");
457                                ok = false;
458                            }
459                        } else {
460                            warn!(node = selector, "message selector missing entries");
461                            ok = false;
462                        }
463                    }
464                    Err(err) => {
465                        warn!(feature = selector, error = %err, "failed to query message selector");
466                        ok = false;
467                    }
468                }
469            } else {
470                ok = false;
471            }
472
473            if let Some(node) = msg_ip {
474                let value = u32::from(local_ip) as i64;
475                if let Err(err) = nodemap.set_integer(node, value, transport) {
476                    warn!(feature = node, error = %err, "failed to write message IP");
477                    ok = false;
478                }
479            } else {
480                ok = false;
481            }
482
483            if let Some(node) = msg_port {
484                if let Err(err) = nodemap.set_integer(node, port as i64, transport) {
485                    warn!(feature = node, error = %err, "failed to write message port");
486                    ok = false;
487                }
488            } else {
489                ok = false;
490            }
491
492            if let Some(node) = msg_en {
493                if let Err(err) = nodemap.set_bool(node, true, transport) {
494                    warn!(feature = node, error = %err, "failed to enable message channel");
495                    ok = false;
496                }
497            } else {
498                ok = false;
499            }
500
501            ok
502        });
503
504        if !channel_configured {
505            configure_message_channel_fallback(&self.transport, local_ip, port)?;
506        }
507
508        let mut used_sfnc = self.nodemap.node(sfnc::EVENT_SELECTOR).is_some()
509            && self.nodemap.node(sfnc::EVENT_NOTIFICATION).is_some();
510
511        used_sfnc = self.with_map(|nodemap, transport| {
512            if !used_sfnc {
513                return false;
514            }
515            for &name in enable_ids {
516                if let Err(err) = nodemap.set_enum(sfnc::EVENT_SELECTOR, name, transport) {
517                    warn!(event = name, error = %err, "failed to select event via SFNC");
518                    return false;
519                }
520                if let Err(err) =
521                    nodemap.set_enum(sfnc::EVENT_NOTIFICATION, sfnc::EVENT_NOTIF_ON, transport)
522                {
523                    warn!(event = name, error = %err, "failed to enable event via SFNC");
524                    return false;
525                }
526            }
527            true
528        });
529
530        if !used_sfnc {
531            for &name in enable_ids {
532                let Some(event_id) = parse_event_id(name) else {
533                    return Err(GenicamError::transport(format!(
534                        "event '{name}' missing from nodemap and not numeric"
535                    )));
536                };
537                enable_event_fallback(&self.transport, event_id, true)?;
538            }
539        }
540
541        Ok(())
542    }
543
544    /// Configure the stream channel for multicast delivery.
545    pub fn configure_stream_multicast(
546        &mut self,
547        stream_idx: u32,
548        group: Ipv4Addr,
549        port: u16,
550    ) -> Result<(), GenicamError> {
551        if (group.octets()[0] & 0xF0) != 0xE0 {
552            return Err(GenicamError::transport(
553                "multicast group must be within 224.0.0.0/4",
554            ));
555        }
556        info!(stream_idx, %group, port, "configuring multicast stream");
557
558        // Precompute node names before taking &mut self.nodemap
559        let dest_addr_node = self.find_alias(sfnc::SCP_DEST_ADDR);
560        let host_port_node = self.find_alias(sfnc::SCP_HOST_PORT);
561        let mcast_en_node = self.find_alias(sfnc::MULTICAST_ENABLE);
562
563        let mut used_sfnc = true;
564        self.with_map(|nm, tr| {
565            if nm.node(sfnc::STREAM_CH_SELECTOR).is_some() {
566                if let Err(err) = nm.set_integer(sfnc::STREAM_CH_SELECTOR, stream_idx as i64, tr) {
567                    warn!(
568                        channel = stream_idx,
569                        error = %err,
570                        "failed to select stream channel via SFNC"
571                    );
572                    used_sfnc = false;
573                }
574            } else {
575                used_sfnc = false;
576            }
577
578            if let Some(node) = dest_addr_node {
579                if let Err(err) = nm.set_integer(node, u32::from(group) as i64, tr) {
580                    warn!(feature = node, error = %err, "failed to write multicast address");
581                    used_sfnc = false;
582                }
583            } else {
584                used_sfnc = false;
585            }
586
587            if let Some(node) = host_port_node {
588                if let Err(err) = nm.set_integer(node, port as i64, tr) {
589                    warn!(feature = node, error = %err, "failed to write multicast port");
590                    used_sfnc = false;
591                }
592            } else {
593                used_sfnc = false;
594            }
595
596            if let Some(node) = mcast_en_node {
597                let _ = nm.set_bool(node, true, tr);
598            }
599        });
600
601        if !used_sfnc {
602            let base = gvcp_consts::STREAM_CHANNEL_BASE
603                + stream_idx as u64 * gvcp_consts::STREAM_CHANNEL_STRIDE;
604            let addr_reg = base + gvcp_consts::STREAM_DESTINATION_ADDRESS;
605            self.transport
606                .write(addr_reg, &group.octets())
607                .map_err(|err| GenicamError::transport(format!("write multicast addr: {err}")))?;
608            let port_reg = base + gvcp_consts::STREAM_DESTINATION_PORT;
609            self.transport
610                .write(port_reg, &port.to_be_bytes())
611                .map_err(|err| GenicamError::transport(format!("write multicast port: {err}")))?;
612            info!(
613                stream_idx,
614                %group,
615                port,
616                "configured multicast destination via raw registers"
617            );
618        } else {
619            info!(
620                stream_idx,
621                %group,
622                port,
623                "configured multicast destination via SFNC"
624            );
625        }
626
627        Ok(())
628    }
629
630    /// Open a GVCP event stream bound to the provided local endpoint.
631    pub async fn open_event_stream(
632        &self,
633        local_ip: Ipv4Addr,
634        port: u16,
635    ) -> Result<EventStream, GenicamError> {
636        let socket = bind_event_socket_internal(IpAddr::V4(local_ip), port).await?;
637        let time_sync = if !self.time_sync.is_empty() {
638            Some(Arc::new(self.time_sync.clone()))
639        } else {
640            None
641        };
642        Ok(EventStream::new(socket, time_sync))
643    }
644
645    fn ensure_chunk_feature(&self, name: &str) -> Result<(), GenicamError> {
646        if self.nodemap.node(name).is_none() {
647            return Err(GenicamError::MissingChunkFeature(name.to_string()));
648        }
649        Ok(())
650    }
651
652    fn find_alias(&self, names: &[&'static str]) -> Option<&'static str> {
653        names
654            .iter()
655            .copied()
656            .find(|name| self.nodemap.node(name).is_some())
657    }
658}
659
660/// Configuration for enabling chunk data via SFNC features.
661#[derive(Debug, Clone, Default)]
662pub struct ChunkConfig {
663    /// Names of chunk selectors that should be enabled on the device.
664    pub selectors: Vec<String>,
665    /// Whether chunk mode should be active after configuration.
666    pub active: bool,
667}
668
669/// Blocking adapter turning an asynchronous [`GigeDevice`] into a [`RegisterIo`]
670/// implementation.
671///
672/// The adapter uses a [`tokio::runtime::Handle`] to synchronously wait on GVCP
673/// register transactions. All callers must ensure these methods are invoked
674/// from outside of the runtime context to avoid nested `block_on` panics.
675pub struct GigeRegisterIo {
676    handle: tokio::runtime::Handle,
677    device: Mutex<GigeDevice>,
678}
679
680impl GigeRegisterIo {
681    /// Create a new adapter using the provided runtime handle and device.
682    pub fn new(handle: tokio::runtime::Handle, device: GigeDevice) -> Self {
683        Self {
684            handle,
685            device: Mutex::new(device),
686        }
687    }
688
689    fn lock(&self) -> Result<MutexGuard<'_, GigeDevice>, GenApiError> {
690        self.device
691            .lock()
692            .map_err(|_| GenApiError::Io("gige device mutex poisoned".into()))
693    }
694}
695
696impl RegisterIo for GigeRegisterIo {
697    fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
698        let mut device = self.lock()?;
699        self.handle
700            .block_on(device.read_mem(addr, len))
701            .map_err(|err| GenApiError::Io(err.to_string()))
702    }
703
704    fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
705        let mut device = self.lock()?;
706        self.handle
707            .block_on(device.write_mem(addr, data))
708            .map_err(|err| GenApiError::Io(err.to_string()))
709    }
710}
711
712fn parse_bool(value: &str) -> Option<bool> {
713    match value.trim().to_ascii_lowercase().as_str() {
714        "1" | "true" => Some(true),
715        "0" | "false" => Some(false),
716        _ => None,
717    }
718}