#![cfg_attr(docsrs, feature(doc_cfg))]
pub use viva_genapi as genapi;
pub use viva_gencp as gencp;
pub use viva_gige as gige;
pub use viva_pfnc as pfnc;
pub use viva_sfnc as sfnc;
#[cfg(feature = "u3v")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
pub use viva_u3v as u3v;
pub mod chunks;
pub mod events;
pub mod frame;
pub mod stream;
pub mod time;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant, SystemTime};
use crate::events::{
bind_socket as bind_event_socket_internal,
configure_message_channel_raw as configure_message_channel_fallback,
enable_event_raw as enable_event_fallback, parse_event_id,
};
use crate::genapi::{GenApiError, Node, NodeMap, RegisterIo, SkOutput};
use gige::GigeDevice;
use gige::gvcp::consts as gvcp_consts;
use thiserror::Error;
use tokio::time::sleep;
use tracing::{debug, info, warn};
pub use chunks::{ChunkKind, ChunkMap, ChunkValue, parse_chunk_bytes};
pub use events::{Event, EventStream};
pub use frame::Frame;
pub use gige::action::{AckSummary, ActionParams};
pub use stream::{FrameStream, Stream, StreamBuilder, StreamDest};
#[cfg(feature = "u3v")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
pub use stream::{U3vFrameStream, U3vStreamBuilder};
pub use time::TimeSync;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum GenicamError {
#[error(transparent)]
GenApi(#[from] GenApiError),
#[error("transport: {0}")]
Transport(String),
#[error("parse error: {0}")]
Parse(String),
#[error("chunk feature '{0}' not found; verify camera supports chunk data")]
MissingChunkFeature(String),
#[error("unsupported pixel format: {0}")]
UnsupportedPixelFormat(viva_pfnc::PixelFormat),
}
impl GenicamError {
fn parse<S: Into<String>>(msg: S) -> Self {
GenicamError::Parse(msg.into())
}
fn transport<S: Into<String>>(msg: S) -> Self {
GenicamError::Transport(msg.into())
}
}
#[derive(Debug)]
pub struct Camera<T: RegisterIo> {
transport: T,
nodemap: NodeMap,
time_sync: TimeSync,
}
impl<T: RegisterIo> Camera<T> {
pub fn new(transport: T, nodemap: NodeMap) -> Self {
Self {
transport,
nodemap,
time_sync: TimeSync::with_capacity(64),
}
}
#[inline]
fn with_map<R>(&mut self, f: impl FnOnce(&mut NodeMap, &T) -> R) -> R {
let transport = &self.transport;
let nodemap = &mut self.nodemap;
f(nodemap, transport)
}
pub fn transport(&self) -> &T {
&self.transport
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
pub fn nodemap(&self) -> &NodeMap {
&self.nodemap
}
pub fn nodemap_mut(&mut self) -> &mut NodeMap {
&mut self.nodemap
}
pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenicamError> {
self.nodemap.enum_entries(name).map_err(Into::into)
}
pub fn get(&self, name: &str) -> Result<String, GenicamError> {
match self.nodemap.node(name) {
Some(Node::Integer(_)) => {
Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
}
Some(Node::Float(_)) => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
Some(Node::Enum(_)) => self
.nodemap
.get_enum(name, &self.transport)
.map_err(Into::into),
Some(Node::Boolean(_)) => Ok(self.nodemap.get_bool(name, &self.transport)?.to_string()),
Some(Node::SwissKnife(sk)) => match sk.output {
SkOutput::Float => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
SkOutput::Integer => {
Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
}
},
Some(Node::Converter(conv)) => match conv.output {
SkOutput::Float => Ok(self
.nodemap
.get_converter(name, &self.transport)?
.to_string()),
SkOutput::Integer => {
Ok((self.nodemap.get_converter(name, &self.transport)? as i64).to_string())
}
},
Some(Node::IntConverter(_)) => Ok(self
.nodemap
.get_int_converter(name, &self.transport)?
.to_string()),
Some(Node::String(_)) => self
.nodemap
.get_string(name, &self.transport)
.map_err(Into::into),
Some(Node::Command(_)) => {
Err(GenicamError::GenApi(GenApiError::Type(name.to_string())))
}
Some(Node::Category(_)) => Ok(String::new()),
None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
}
}
pub fn set(&mut self, name: &str, value: &str) -> Result<(), GenicamError> {
match self.nodemap.node(name) {
Some(Node::Integer(_)) => {
let parsed: i64 = value
.parse()
.map_err(|_| GenicamError::parse(format!("invalid integer for {name}")))?;
self.nodemap
.set_integer(name, parsed, &self.transport)
.map_err(Into::into)
}
Some(Node::Float(_)) => {
let parsed: f64 = value
.parse()
.map_err(|_| GenicamError::parse(format!("invalid float for {name}")))?;
self.nodemap
.set_float(name, parsed, &self.transport)
.map_err(Into::into)
}
Some(Node::Enum(_)) => self
.nodemap
.set_enum(name, value, &self.transport)
.map_err(Into::into),
Some(Node::Boolean(_)) => {
let parsed = parse_bool(value).ok_or_else(|| {
GenicamError::parse(format!("invalid boolean for {name}: {value}"))
})?;
self.nodemap
.set_bool(name, parsed, &self.transport)
.map_err(Into::into)
}
Some(Node::SwissKnife(_)) => Err(GenApiError::Type(name.to_string()).into()),
Some(Node::Converter(_)) => {
Err(GenApiError::Type(name.to_string()).into())
}
Some(Node::IntConverter(_)) => Err(GenApiError::Type(name.to_string()).into()),
Some(Node::String(_)) => self
.nodemap
.set_string(name, value, &self.transport)
.map_err(Into::into),
Some(Node::Command(_)) => self
.nodemap
.exec_command(name, &self.transport)
.map_err(Into::into),
Some(Node::Category(_)) => Err(GenApiError::Type(name.to_string()).into()),
None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
}
}
pub fn set_exposure_time_us(&mut self, value: f64) -> Result<(), GenicamError> {
self.set_float_feature("ExposureTime", value)
}
pub fn set_gain_db(&mut self, value: f64) -> Result<(), GenicamError> {
self.set_float_feature("Gain", value)
}
fn set_float_feature(&mut self, name: &str, value: f64) -> Result<(), GenicamError> {
match self.nodemap.node(name) {
Some(Node::Float(_)) => self
.nodemap
.set_float(name, value, &self.transport)
.map_err(Into::into),
Some(_) => Err(GenApiError::Type(name.to_string()).into()),
None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
}
}
pub async fn time_calibrate(
&mut self,
samples: usize,
interval_ms: u64,
) -> Result<(), GenicamError> {
if samples < 2 {
return Err(GenicamError::transport(
"time calibration requires at least two samples",
));
}
let cap = samples.max(self.time_sync.capacity());
self.time_sync = TimeSync::with_capacity(cap);
let latch_cmd = self.find_alias(viva_sfnc::TS_LATCH_CMDS);
let value_node = self
.find_alias(viva_sfnc::TS_VALUE_NODES)
.ok_or_else(|| GenApiError::NodeNotFound("TimestampValue".into()))?;
let mut freq_hz = if let Some(name) = self.find_alias(viva_sfnc::TS_FREQ_NODES) {
match self.nodemap.get_integer(name, &self.transport) {
Ok(value) if value > 0 => Some(value as f64),
Ok(_) => None,
Err(err) => {
debug!(node = name, error = %err, "failed to read timestamp frequency");
None
}
}
} else {
None
};
info!(samples, interval_ms, "starting time calibration");
let mut first_sample: Option<(u64, Instant)> = None;
let mut last_sample: Option<(u64, Instant)> = None;
for idx in 0..samples {
if let Some(cmd) = latch_cmd {
self.nodemap
.exec_command(cmd, &self.transport)
.map_err(GenicamError::from)?;
}
let raw_ticks = self
.nodemap
.get_integer(value_node, &self.transport)
.map_err(GenicamError::from)?;
let dev_ticks = u64::try_from(raw_ticks).map_err(|_| {
GenicamError::transport("timestamp value is negative; unsupported camera")
})?;
let host = Instant::now();
self.time_sync.update(dev_ticks, host);
if idx == 0 {
first_sample = Some((dev_ticks, host));
}
last_sample = Some((dev_ticks, host));
if let Some(origin) = self.time_sync.origin_instant() {
let ns = host.duration_since(origin).as_nanos();
debug!(
sample = idx,
ticks = dev_ticks,
host_ns = ns,
"timestamp sample"
);
} else {
debug!(sample = idx, ticks = dev_ticks, "timestamp sample");
}
if interval_ms > 0 && idx + 1 < samples {
sleep(Duration::from_millis(interval_ms)).await;
}
}
if freq_hz.is_none()
&& let (Some((first_ticks, first_host)), Some((last_ticks, last_host))) =
(first_sample, last_sample)
&& last_ticks > first_ticks
&& let Some(delta) = last_host.checked_duration_since(first_host)
{
let secs = delta.as_secs_f64();
if secs > 0.0 {
freq_hz = Some((last_ticks - first_ticks) as f64 / secs);
}
}
let (a, b) = self
.time_sync
.fit(freq_hz)
.ok_or_else(|| GenicamError::transport("insufficient samples for timestamp fit"))?;
if let Some(freq) = freq_hz {
info!(freq_hz = freq, a, b, "time calibration complete");
} else {
info!(a, b, "time calibration complete");
}
Ok(())
}
pub fn map_dev_ts(&self, dev_ticks: u64) -> SystemTime {
self.time_sync.to_host_time(dev_ticks)
}
pub fn time_sync(&self) -> &TimeSync {
&self.time_sync
}
pub fn time_reset(&mut self) -> Result<(), GenicamError> {
if let Some(cmd) = self.find_alias(viva_sfnc::TS_RESET_CMDS) {
self.nodemap
.exec_command(cmd, &self.transport)
.map_err(GenicamError::from)?;
self.time_sync = TimeSync::with_capacity(self.time_sync.capacity());
info!(command = cmd, "timestamp counter reset");
}
Ok(())
}
pub fn acquisition_start(&mut self) -> Result<(), GenicamError> {
self.nodemap
.exec_command("AcquisitionStart", &self.transport)
.map_err(Into::into)
}
pub fn acquisition_stop(&mut self) -> Result<(), GenicamError> {
self.nodemap
.exec_command("AcquisitionStop", &self.transport)
.map_err(Into::into)
}
pub fn configure_chunks(&mut self, cfg: &ChunkConfig) -> Result<(), GenicamError> {
self.ensure_chunk_feature(viva_sfnc::CHUNK_MODE_ACTIVE)?;
self.ensure_chunk_feature(viva_sfnc::CHUNK_SELECTOR)?;
self.ensure_chunk_feature(viva_sfnc::CHUNK_ENABLE)?;
self.with_map(|nm, tr| {
nm.set_bool(viva_sfnc::CHUNK_MODE_ACTIVE, cfg.active, tr)?;
for s in &cfg.selectors {
nm.set_enum(viva_sfnc::CHUNK_SELECTOR, s, tr)?;
nm.set_bool(viva_sfnc::CHUNK_ENABLE, cfg.active, tr)?;
}
Ok(())
})
}
pub async fn configure_events(
&mut self,
local_ip: Ipv4Addr,
port: u16,
enable_ids: &[&str],
) -> Result<(), GenicamError> {
info!(%local_ip, port, "configuring GVCP events");
let msg_sel = self.find_alias(viva_sfnc::MSG_SEL);
let msg_ip = self.find_alias(viva_sfnc::MSG_IP);
let msg_port = self.find_alias(viva_sfnc::MSG_PORT);
let msg_en = self.find_alias(viva_sfnc::MSG_EN);
let channel_configured = self.with_map(|nodemap, transport| {
let mut ok = true;
if let Some(selector) = msg_sel {
match nodemap.enum_entries(selector) {
Ok(entries) => {
if let Some(entry) = entries.into_iter().next() {
if let Err(err) = nodemap.set_enum(selector, &entry, transport) {
warn!(node = selector, error = %err, "failed to set message selector");
ok = false;
}
} else {
warn!(node = selector, "message selector missing entries");
ok = false;
}
}
Err(err) => {
warn!(feature = selector, error = %err, "failed to query message selector");
ok = false;
}
}
} else {
ok = false;
}
if let Some(node) = msg_ip {
let value = u32::from(local_ip) as i64;
if let Err(err) = nodemap.set_integer(node, value, transport) {
warn!(feature = node, error = %err, "failed to write message IP");
ok = false;
}
} else {
ok = false;
}
if let Some(node) = msg_port {
if let Err(err) = nodemap.set_integer(node, port as i64, transport) {
warn!(feature = node, error = %err, "failed to write message port");
ok = false;
}
} else {
ok = false;
}
if let Some(node) = msg_en {
if let Err(err) = nodemap.set_bool(node, true, transport) {
warn!(feature = node, error = %err, "failed to enable message channel");
ok = false;
}
} else {
ok = false;
}
ok
});
if !channel_configured {
configure_message_channel_fallback(&self.transport, local_ip, port)?;
}
let mut used_sfnc = self.nodemap.node(viva_sfnc::EVENT_SELECTOR).is_some()
&& self.nodemap.node(viva_sfnc::EVENT_NOTIFICATION).is_some();
used_sfnc = self.with_map(|nodemap, transport| {
if !used_sfnc {
return false;
}
for &name in enable_ids {
if let Err(err) = nodemap.set_enum(viva_sfnc::EVENT_SELECTOR, name, transport) {
warn!(event = name, error = %err, "failed to select event via SFNC");
return false;
}
if let Err(err) = nodemap.set_enum(
viva_sfnc::EVENT_NOTIFICATION,
viva_sfnc::EVENT_NOTIF_ON,
transport,
) {
warn!(event = name, error = %err, "failed to enable event via SFNC");
return false;
}
}
true
});
if !used_sfnc {
for &name in enable_ids {
let Some(event_id) = parse_event_id(name) else {
return Err(GenicamError::transport(format!(
"event '{name}' missing from nodemap and not numeric"
)));
};
enable_event_fallback(&self.transport, event_id, true)?;
}
}
Ok(())
}
pub fn configure_stream_multicast(
&mut self,
stream_idx: u32,
group: Ipv4Addr,
port: u16,
) -> Result<(), GenicamError> {
if (group.octets()[0] & 0xF0) != 0xE0 {
return Err(GenicamError::transport(
"multicast group must be within 224.0.0.0/4",
));
}
info!(stream_idx, %group, port, "configuring multicast stream");
let dest_addr_node = self.find_alias(viva_sfnc::SCP_DEST_ADDR);
let host_port_node = self.find_alias(viva_sfnc::SCP_HOST_PORT);
let mcast_en_node = self.find_alias(viva_sfnc::MULTICAST_ENABLE);
let mut used_sfnc = true;
self.with_map(|nm, tr| {
if nm.node(viva_sfnc::STREAM_CH_SELECTOR).is_some() {
if let Err(err) =
nm.set_integer(viva_sfnc::STREAM_CH_SELECTOR, stream_idx as i64, tr)
{
warn!(
channel = stream_idx,
error = %err,
"failed to select stream channel via SFNC"
);
used_sfnc = false;
}
} else {
used_sfnc = false;
}
if let Some(node) = dest_addr_node {
if let Err(err) = nm.set_integer(node, u32::from(group) as i64, tr) {
warn!(feature = node, error = %err, "failed to write multicast address");
used_sfnc = false;
}
} else {
used_sfnc = false;
}
if let Some(node) = host_port_node {
if let Err(err) = nm.set_integer(node, port as i64, tr) {
warn!(feature = node, error = %err, "failed to write multicast port");
used_sfnc = false;
}
} else {
used_sfnc = false;
}
if let Some(node) = mcast_en_node {
let _ = nm.set_bool(node, true, tr);
}
});
if !used_sfnc {
let base = gvcp_consts::STREAM_CHANNEL_BASE
+ stream_idx as u64 * gvcp_consts::STREAM_CHANNEL_STRIDE;
let addr_reg = base + gvcp_consts::STREAM_DESTINATION_ADDRESS;
self.transport
.write(addr_reg, &group.octets())
.map_err(|err| GenicamError::transport(format!("write multicast addr: {err}")))?;
let port_reg = base + gvcp_consts::STREAM_DESTINATION_PORT;
self.transport
.write(port_reg, &port.to_be_bytes())
.map_err(|err| GenicamError::transport(format!("write multicast port: {err}")))?;
info!(
stream_idx,
%group,
port,
"configured multicast destination via raw registers"
);
} else {
info!(
stream_idx,
%group,
port,
"configured multicast destination via SFNC"
);
}
Ok(())
}
pub async fn open_event_stream(
&self,
local_ip: Ipv4Addr,
port: u16,
) -> Result<EventStream, GenicamError> {
let socket = bind_event_socket_internal(IpAddr::V4(local_ip), port).await?;
let time_sync = if !self.time_sync.is_empty() {
Some(Arc::new(self.time_sync.clone()))
} else {
None
};
Ok(EventStream::new(socket, time_sync))
}
fn ensure_chunk_feature(&self, name: &str) -> Result<(), GenicamError> {
if self.nodemap.node(name).is_none() {
return Err(GenicamError::MissingChunkFeature(name.to_string()));
}
Ok(())
}
fn find_alias(&self, names: &[&'static str]) -> Option<&'static str> {
names
.iter()
.copied()
.find(|name| self.nodemap.node(name).is_some())
}
}
#[derive(Debug, Clone, Default)]
pub struct ChunkConfig {
pub selectors: Vec<String>,
pub active: bool,
}
pub struct GigeRegisterIo {
handle: tokio::runtime::Handle,
device: Mutex<GigeDevice>,
}
impl GigeRegisterIo {
pub fn new(handle: tokio::runtime::Handle, device: GigeDevice) -> Self {
Self {
handle,
device: Mutex::new(device),
}
}
pub fn lock_device(&self) -> Result<MutexGuard<'_, GigeDevice>, GenicamError> {
self.device
.lock()
.map_err(|_| GenicamError::transport("gige device mutex poisoned"))
}
fn lock(&self) -> Result<MutexGuard<'_, GigeDevice>, GenApiError> {
self.device
.lock()
.map_err(|_| GenApiError::Io("gige device mutex poisoned".into()))
}
}
impl RegisterIo for GigeRegisterIo {
fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
let mut device = self.lock()?;
let fut = device.read_mem(addr, len);
if tokio::runtime::Handle::try_current().is_ok() {
tokio::task::block_in_place(|| self.handle.block_on(fut))
} else {
self.handle.block_on(fut)
}
.map_err(|err| GenApiError::Io(err.to_string()))
}
fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
let mut device = self.lock()?;
let fut = device.write_mem(addr, data);
if tokio::runtime::Handle::try_current().is_ok() {
tokio::task::block_in_place(|| self.handle.block_on(fut))
} else {
self.handle.block_on(fut)
}
.map_err(|err| GenApiError::Io(err.to_string()))
}
}
pub async fn connect_gige(
device: &gige::DeviceInfo,
) -> Result<Camera<GigeRegisterIo>, GenicamError> {
let (camera, _xml) = connect_gige_with_xml(device).await?;
Ok(camera)
}
pub async fn connect_gige_with_xml(
device: &gige::DeviceInfo,
) -> Result<(Camera<GigeRegisterIo>, String), GenicamError> {
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use tokio::sync::Mutex as AsyncMutex;
let control_addr = SocketAddr::new(IpAddr::V4(device.ip), gige::GVCP_PORT);
info!(%control_addr, "connecting to GigE Vision camera");
let mut device = gige::GigeDevice::open(control_addr)
.await
.map_err(|e| GenicamError::transport(e.to_string()))?;
device
.claim_control()
.await
.map_err(|e| GenicamError::transport(e.to_string()))?;
let control = Arc::new(AsyncMutex::new(device));
let xml = viva_genapi_xml::fetch_and_load_xml({
let control = control.clone();
move |address, length| {
let control = control.clone();
async move {
let mut dev = control.lock().await;
dev.read_mem(address, length)
.await
.map_err(|err| viva_genapi_xml::XmlError::Transport(err.to_string()))
}
}
})
.await
.map_err(|e| GenicamError::transport(e.to_string()))?;
let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
let nodemap = genapi::NodeMap::from(model);
let handle = tokio::runtime::Handle::current();
let control_device = Arc::try_unwrap(control)
.map_err(|_| GenicamError::transport("control connection still in use"))?
.into_inner();
let transport = GigeRegisterIo::new(handle, control_device);
info!("GigE camera connected successfully");
Ok((Camera::new(transport, nodemap), xml))
}
#[cfg(feature = "u3v")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
pub struct U3vRegisterIo<T: u3v::usb::UsbTransfer + 'static> {
device: Mutex<u3v::device::U3vDevice<T>>,
}
#[cfg(feature = "u3v")]
impl<T: u3v::usb::UsbTransfer + 'static> U3vRegisterIo<T> {
pub fn new(device: u3v::device::U3vDevice<T>) -> Self {
Self {
device: Mutex::new(device),
}
}
pub fn lock_device(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenicamError> {
self.device
.lock()
.map_err(|_| GenicamError::transport("u3v device mutex poisoned"))
}
fn lock(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenApiError> {
self.device
.lock()
.map_err(|_| GenApiError::Io("u3v device mutex poisoned".into()))
}
}
#[cfg(feature = "u3v")]
impl<T: u3v::usb::UsbTransfer + 'static> RegisterIo for U3vRegisterIo<T> {
fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
let mut device = self.lock()?;
device
.read_mem(addr, len)
.map_err(|e| GenApiError::Io(e.to_string()))
}
fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
let mut device = self.lock()?;
device
.write_mem(addr, data)
.map_err(|e| GenApiError::Io(e.to_string()))
}
}
#[cfg(feature = "u3v-usb")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
pub fn connect_u3v(
device: &u3v::discovery::U3vDeviceInfo,
) -> Result<Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, GenicamError> {
let (camera, _xml) = connect_u3v_with_xml(device)?;
Ok(camera)
}
#[cfg(feature = "u3v-usb")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
pub fn connect_u3v_with_xml(
device_info: &u3v::discovery::U3vDeviceInfo,
) -> Result<(Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, String), GenicamError> {
info!(
vendor_id = device_info.vendor_id,
product_id = device_info.product_id,
"connecting to USB3 Vision camera"
);
let mut device = u3v::device::U3vDevice::open_device(device_info)
.map_err(|e| GenicamError::transport(e.to_string()))?;
let xml = device
.fetch_xml()
.map_err(|e| GenicamError::transport(e.to_string()))?;
let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
let nodemap = genapi::NodeMap::from(model);
let transport = U3vRegisterIo::new(device);
info!("USB3 Vision camera connected successfully");
Ok((Camera::new(transport, nodemap), xml))
}
#[cfg(feature = "u3v")]
#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
pub fn open_u3v_device<T: u3v::usb::UsbTransfer + 'static>(
mut device: u3v::device::U3vDevice<T>,
) -> Result<(Camera<U3vRegisterIo<T>>, String), GenicamError> {
let xml = device
.fetch_xml()
.map_err(|e| GenicamError::transport(e.to_string()))?;
let model = viva_genapi_xml::parse(&xml).map_err(|e| GenicamError::transport(e.to_string()))?;
let nodemap = genapi::NodeMap::from(model);
let transport = U3vRegisterIo::new(device);
Ok((Camera::new(transport, nodemap), xml))
}
fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" => Some(true),
"0" | "false" => Some(false),
_ => None,
}
}