1#![cfg_attr(docsrs, feature(doc_cfg))]
2pub use viva_genapi as genapi;
72pub use viva_gencp as gencp;
73pub use viva_gige as gige;
74pub use viva_pfnc as pfnc;
75pub use viva_sfnc as sfnc;
76#[cfg(feature = "u3v")]
77#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
78pub use viva_u3v as u3v;
79
80pub mod chunks;
81pub mod events;
82pub mod frame;
83pub mod stream;
84pub mod time;
85
86use std::net::{IpAddr, Ipv4Addr};
87use std::sync::{Arc, Mutex, MutexGuard};
88use std::time::{Duration, Instant, SystemTime};
89
90use crate::events::{
91 bind_socket as bind_event_socket_internal,
92 configure_message_channel_raw as configure_message_channel_fallback,
93};
94use crate::genapi::{GenApiError, Node, NodeMap, RegisterIo, SkOutput};
95use gige::GigeDevice;
96use gige::gvcp::consts as gvcp_consts;
97use thiserror::Error;
98use tokio::time::sleep;
99use tracing::{debug, info, warn};
100
101pub use chunks::{ChunkKind, ChunkMap, ChunkValue, parse_chunk_bytes};
102pub use events::{Event, EventStream};
103pub use frame::Frame;
104pub use gige::action::{AckSummary, ActionParams};
105pub use stream::{FrameStream, Stream, StreamBuilder, StreamDest};
106#[cfg(feature = "u3v")]
107#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
108pub use stream::{U3vFrameStream, U3vStreamBuilder};
109pub use time::TimeSync;
110
111#[derive(Debug, Error)]
113#[non_exhaustive]
114pub enum GenicamError {
115 #[error(transparent)]
117 GenApi(#[from] GenApiError),
118 #[error("transport: {0}")]
120 Transport(String),
121 #[error("parse error: {0}")]
123 Parse(String),
124 #[error("chunk feature '{0}' not found; verify camera supports chunk data")]
126 MissingChunkFeature(String),
127 #[error("unsupported pixel format: {0}")]
129 UnsupportedPixelFormat(viva_pfnc::PixelFormat),
130}
131
132impl GenicamError {
133 fn parse<S: Into<String>>(msg: S) -> Self {
134 GenicamError::Parse(msg.into())
135 }
136
137 fn transport<S: Into<String>>(msg: S) -> Self {
138 GenicamError::Transport(msg.into())
139 }
140}
141
142#[derive(Debug)]
144pub struct Camera<T: RegisterIo> {
145 transport: T,
146 nodemap: NodeMap,
147 time_sync: TimeSync,
148}
149
150impl<T: RegisterIo> Camera<T> {
151 pub fn new(transport: T, nodemap: NodeMap) -> Self {
153 Self {
154 transport,
155 nodemap,
156 time_sync: TimeSync::with_capacity(64),
157 }
158 }
159
160 #[inline]
161 fn with_map<R>(&mut self, f: impl FnOnce(&mut NodeMap, &T) -> R) -> R {
162 let transport = &self.transport;
163 let nodemap = &mut self.nodemap;
164 f(nodemap, transport)
165 }
166
167 pub fn transport(&self) -> &T {
169 &self.transport
170 }
171
172 pub fn transport_mut(&mut self) -> &mut T {
174 &mut self.transport
175 }
176
177 pub fn nodemap(&self) -> &NodeMap {
179 &self.nodemap
180 }
181
182 pub fn nodemap_mut(&mut self) -> &mut NodeMap {
184 &mut self.nodemap
185 }
186
187 pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenicamError> {
189 self.nodemap.enum_entries(name).map_err(Into::into)
190 }
191
192 pub fn get(&self, name: &str) -> Result<String, GenicamError> {
194 match self.nodemap.node(name) {
195 Some(Node::Integer(_)) => {
196 Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
197 }
198 Some(Node::Float(_)) => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
199 Some(Node::Enum(_)) => self
200 .nodemap
201 .get_enum(name, &self.transport)
202 .map_err(Into::into),
203 Some(Node::Boolean(_)) => Ok(self.nodemap.get_bool(name, &self.transport)?.to_string()),
204 Some(Node::SwissKnife(sk)) => match sk.output {
205 SkOutput::Float => Ok(self.nodemap.get_float(name, &self.transport)?.to_string()),
206 SkOutput::Integer => {
207 Ok(self.nodemap.get_integer(name, &self.transport)?.to_string())
208 }
209 },
210 Some(Node::Converter(conv)) => match conv.output {
211 SkOutput::Float => Ok(self
212 .nodemap
213 .get_converter(name, &self.transport)?
214 .to_string()),
215 SkOutput::Integer => {
216 Ok((self.nodemap.get_converter(name, &self.transport)? as i64).to_string())
217 }
218 },
219 Some(Node::IntConverter(_)) => Ok(self
220 .nodemap
221 .get_int_converter(name, &self.transport)?
222 .to_string()),
223 Some(Node::String(_)) => self
224 .nodemap
225 .get_string(name, &self.transport)
226 .map_err(Into::into),
227 Some(Node::Command(_)) => {
228 Err(GenicamError::GenApi(GenApiError::Type(name.to_string())))
229 }
230 Some(Node::Register(_)) => {
236 Err(GenicamError::GenApi(GenApiError::Type(name.to_string())))
237 }
238 Some(Node::Category(_)) => Ok(String::new()),
239 Some(other) => Err(GenicamError::GenApi(GenApiError::Type(format!(
244 "{name} is a {} node, which Camera::get cannot render as a string",
245 other.kind_name()
246 )))),
247 None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
248 }
249 }
250
251 pub fn set(&mut self, name: &str, value: &str) -> Result<(), GenicamError> {
253 match self.nodemap.node(name) {
254 Some(Node::Integer(_)) => {
255 let parsed: i64 = value
256 .parse()
257 .map_err(|_| GenicamError::parse(format!("invalid integer for {name}")))?;
258 self.nodemap
259 .set_integer(name, parsed, &self.transport)
260 .map_err(Into::into)
261 }
262 Some(Node::Float(_)) => {
263 let parsed: f64 = value
264 .parse()
265 .map_err(|_| GenicamError::parse(format!("invalid float for {name}")))?;
266 self.nodemap
267 .set_float(name, parsed, &self.transport)
268 .map_err(Into::into)
269 }
270 Some(Node::Enum(_)) => self
271 .nodemap
272 .set_enum(name, value, &self.transport)
273 .map_err(Into::into),
274 Some(Node::Boolean(_)) => {
275 let parsed = parse_bool(value).ok_or_else(|| {
276 GenicamError::parse(format!("invalid boolean for {name}: {value}"))
277 })?;
278 self.nodemap
279 .set_bool(name, parsed, &self.transport)
280 .map_err(Into::into)
281 }
282 Some(Node::SwissKnife(_)) => Err(GenApiError::Type(name.to_string()).into()),
283 Some(Node::Converter(_)) => {
284 Err(GenApiError::Type(name.to_string()).into())
287 }
288 Some(Node::IntConverter(_)) => Err(GenApiError::Type(name.to_string()).into()),
289 Some(Node::String(_)) => self
290 .nodemap
291 .set_string(name, value, &self.transport)
292 .map_err(Into::into),
293 Some(Node::Command(_)) => self
294 .nodemap
295 .exec_command(name, &self.transport)
296 .map_err(Into::into),
297 Some(Node::Register(_)) => Err(GenApiError::Type(name.to_string()).into()),
300 Some(Node::Category(_)) => Err(GenApiError::Type(name.to_string()).into()),
301 Some(other) => Err(GenicamError::GenApi(GenApiError::Type(format!(
303 "{name} is a {} node, which Camera::set cannot write from a string",
304 other.kind_name()
305 )))),
306 None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
307 }
308 }
309
310 pub fn set_exposure_time_us(&mut self, value: f64) -> Result<(), GenicamError> {
312 self.set_float_feature("ExposureTime", value)
314 }
315
316 pub fn set_gain_db(&mut self, value: f64) -> Result<(), GenicamError> {
318 self.set_float_feature("Gain", value)
319 }
320
321 fn set_float_feature(&mut self, name: &str, value: f64) -> Result<(), GenicamError> {
322 match self.nodemap.node(name) {
323 Some(Node::Float(_)) => self
324 .nodemap
325 .set_float(name, value, &self.transport)
326 .map_err(Into::into),
327 Some(_) => Err(GenApiError::Type(name.to_string()).into()),
328 None => Err(GenApiError::NodeNotFound(name.to_string()).into()),
329 }
330 }
331
332 pub async fn time_calibrate(
334 &mut self,
335 samples: usize,
336 interval_ms: u64,
337 ) -> Result<(), GenicamError> {
338 if samples < 2 {
339 return Err(GenicamError::transport(
340 "time calibration requires at least two samples",
341 ));
342 }
343
344 let cap = samples.max(self.time_sync.capacity());
345 self.time_sync = TimeSync::with_capacity(cap);
346
347 let latch_cmd = self.find_alias(viva_sfnc::TS_LATCH_CMDS);
348 let value_node = self
349 .find_alias(viva_sfnc::TS_VALUE_NODES)
350 .ok_or_else(|| GenApiError::NodeNotFound("TimestampValue".into()))?;
351
352 let mut freq_hz = if let Some(name) = self.find_alias(viva_sfnc::TS_FREQ_NODES) {
353 match self.nodemap.get_integer(name, &self.transport) {
354 Ok(value) if value > 0 => Some(value as f64),
355 Ok(_) => None,
356 Err(err) => {
357 debug!(node = name, error = %err, "failed to read timestamp frequency");
358 None
359 }
360 }
361 } else {
362 None
363 };
364
365 info!(samples, interval_ms, "starting time calibration");
366 let mut first_sample: Option<(u64, Instant)> = None;
367 let mut last_sample: Option<(u64, Instant)> = None;
368
369 for idx in 0..samples {
370 if let Some(cmd) = latch_cmd {
371 self.nodemap
372 .exec_command(cmd, &self.transport)
373 .map_err(GenicamError::from)?;
374 }
375
376 let raw_ticks = self
377 .nodemap
378 .get_integer(value_node, &self.transport)
379 .map_err(GenicamError::from)?;
380 let dev_ticks = u64::try_from(raw_ticks).map_err(|_| {
381 GenicamError::transport("timestamp value is negative; unsupported camera")
382 })?;
383 let host = Instant::now();
384 self.time_sync.update(dev_ticks, host);
385 if idx == 0 {
386 first_sample = Some((dev_ticks, host));
387 }
388 last_sample = Some((dev_ticks, host));
389 if let Some(origin) = self.time_sync.origin_instant() {
390 let ns = host.duration_since(origin).as_nanos();
391 debug!(
392 sample = idx,
393 ticks = dev_ticks,
394 host_ns = ns,
395 "timestamp sample"
396 );
397 } else {
398 debug!(sample = idx, ticks = dev_ticks, "timestamp sample");
399 }
400
401 if interval_ms > 0 && idx + 1 < samples {
402 sleep(Duration::from_millis(interval_ms)).await;
403 }
404 }
405
406 if freq_hz.is_none()
407 && let (Some((first_ticks, first_host)), Some((last_ticks, last_host))) =
408 (first_sample, last_sample)
409 && last_ticks > first_ticks
410 && let Some(delta) = last_host.checked_duration_since(first_host)
411 {
412 let secs = delta.as_secs_f64();
413 if secs > 0.0 {
414 freq_hz = Some((last_ticks - first_ticks) as f64 / secs);
415 }
416 }
417
418 let (a, b) = self
419 .time_sync
420 .fit(freq_hz)
421 .ok_or_else(|| GenicamError::transport("insufficient samples for timestamp fit"))?;
422
423 if let Some(freq) = freq_hz {
424 info!(freq_hz = freq, a, b, "time calibration complete");
425 } else {
426 info!(a, b, "time calibration complete");
427 }
428
429 Ok(())
430 }
431
432 pub fn map_dev_ts(&self, dev_ticks: u64) -> SystemTime {
434 self.time_sync.to_host_time(dev_ticks)
435 }
436
437 pub fn time_sync(&self) -> &TimeSync {
439 &self.time_sync
440 }
441
442 pub fn time_reset(&mut self) -> Result<(), GenicamError> {
444 if let Some(cmd) = self.find_alias(viva_sfnc::TS_RESET_CMDS) {
445 self.nodemap
446 .exec_command(cmd, &self.transport)
447 .map_err(GenicamError::from)?;
448 self.time_sync = TimeSync::with_capacity(self.time_sync.capacity());
449 info!(command = cmd, "timestamp counter reset");
450 }
451 Ok(())
452 }
453
454 pub fn execute_command(&mut self, name: &str) -> Result<(), GenicamError> {
463 self.nodemap
464 .exec_command(name, &self.transport)
465 .map_err(Into::into)
466 }
467
468 pub fn acquisition_start(&mut self) -> Result<(), GenicamError> {
470 self.nodemap
471 .exec_command("AcquisitionStart", &self.transport)
472 .map_err(Into::into)
473 }
474
475 pub fn acquisition_stop(&mut self) -> Result<(), GenicamError> {
477 self.nodemap
478 .exec_command("AcquisitionStop", &self.transport)
479 .map_err(Into::into)
480 }
481
482 pub fn configure_chunks(&mut self, cfg: &ChunkConfig) -> Result<(), GenicamError> {
484 self.ensure_chunk_feature(viva_sfnc::CHUNK_MODE_ACTIVE)?;
485 self.ensure_chunk_feature(viva_sfnc::CHUNK_SELECTOR)?;
486 self.ensure_chunk_feature(viva_sfnc::CHUNK_ENABLE)?;
487
488 self.with_map(|nm, tr| {
490 nm.set_bool(viva_sfnc::CHUNK_MODE_ACTIVE, cfg.active, tr)?;
491 for s in &cfg.selectors {
492 nm.set_enum(viva_sfnc::CHUNK_SELECTOR, s, tr)?;
493 nm.set_bool(viva_sfnc::CHUNK_ENABLE, cfg.active, tr)?;
494 }
495 Ok(())
496 })
497 }
498
499 pub async fn configure_events(
501 &mut self,
502 local_ip: Ipv4Addr,
503 port: u16,
504 enable_ids: &[&str],
505 ) -> Result<(), GenicamError> {
506 info!(%local_ip, port, "configuring GVCP events");
507 let msg_sel = self.find_alias(viva_sfnc::MSG_SEL);
509 let msg_ip = self.find_alias(viva_sfnc::MSG_IP);
510 let msg_port = self.find_alias(viva_sfnc::MSG_PORT);
511 let msg_en = self.find_alias(viva_sfnc::MSG_EN);
512
513 let channel_configured = self.with_map(|nodemap, transport| {
514 let mut ok = true;
515
516 if let Some(selector) = msg_sel {
517 match nodemap.enum_entries(selector) {
518 Ok(entries) => {
519 if let Some(entry) = entries.into_iter().next() {
520 if let Err(err) = nodemap.set_enum(selector, &entry, transport) {
521 warn!(node = selector, error = %err, "failed to set message selector");
522 ok = false;
523 }
524 } else {
525 warn!(node = selector, "message selector missing entries");
526 ok = false;
527 }
528 }
529 Err(err) => {
530 warn!(feature = selector, error = %err, "failed to query message selector");
531 ok = false;
532 }
533 }
534 } else {
535 ok = false;
536 }
537
538 if let Some(node) = msg_ip {
539 let value = u32::from(local_ip) as i64;
540 if let Err(err) = nodemap.set_integer(node, value, transport) {
541 warn!(feature = node, error = %err, "failed to write message IP");
542 ok = false;
543 }
544 } else {
545 ok = false;
546 }
547
548 if let Some(node) = msg_port {
549 if let Err(err) = nodemap.set_integer(node, port as i64, transport) {
550 warn!(feature = node, error = %err, "failed to write message port");
551 ok = false;
552 }
553 } else {
554 ok = false;
555 }
556
557 if let Some(node) = msg_en {
558 if let Err(err) = nodemap.set_bool(node, true, transport) {
559 warn!(feature = node, error = %err, "failed to enable message channel");
560 ok = false;
561 }
562 } else {
563 ok = false;
564 }
565
566 ok
567 });
568
569 if !channel_configured {
570 configure_message_channel_fallback(&self.transport, local_ip, port)?;
571 }
572
573 let mut used_sfnc = self.nodemap.node(viva_sfnc::EVENT_SELECTOR).is_some()
574 && self.nodemap.node(viva_sfnc::EVENT_NOTIFICATION).is_some();
575
576 used_sfnc = self.with_map(|nodemap, transport| {
577 if !used_sfnc {
578 return false;
579 }
580 for &name in enable_ids {
581 if let Err(err) = nodemap.set_enum(viva_sfnc::EVENT_SELECTOR, name, transport) {
582 warn!(event = name, error = %err, "failed to select event via SFNC");
583 return false;
584 }
585 if let Err(err) = nodemap.set_enum(
586 viva_sfnc::EVENT_NOTIFICATION,
587 viva_sfnc::EVENT_NOTIF_ON,
588 transport,
589 ) {
590 warn!(event = name, error = %err, "failed to enable event via SFNC");
591 return false;
592 }
593 }
594 true
595 });
596
597 if !used_sfnc && !enable_ids.is_empty() {
598 return Err(GenicamError::transport(format!(
603 "cannot enable events {enable_ids:?}: camera exposes neither \
604 `{}` nor `{}`, and event selection has no bootstrap register",
605 viva_sfnc::EVENT_SELECTOR,
606 viva_sfnc::EVENT_NOTIFICATION,
607 )));
608 }
609
610 Ok(())
611 }
612
613 pub fn configure_stream_multicast(
615 &mut self,
616 stream_idx: u32,
617 group: Ipv4Addr,
618 port: u16,
619 ) -> Result<(), GenicamError> {
620 if (group.octets()[0] & 0xF0) != 0xE0 {
621 return Err(GenicamError::transport(
622 "multicast group must be within 224.0.0.0/4",
623 ));
624 }
625 info!(stream_idx, %group, port, "configuring multicast stream");
626
627 let dest_addr_node = self.find_alias(viva_sfnc::SCP_DEST_ADDR);
629 let host_port_node = self.find_alias(viva_sfnc::SCP_HOST_PORT);
630 let mcast_en_node = self.find_alias(viva_sfnc::MULTICAST_ENABLE);
631
632 let mut used_sfnc = true;
633 self.with_map(|nm, tr| {
634 if nm.node(viva_sfnc::STREAM_CH_SELECTOR).is_some() {
635 if let Err(err) =
636 nm.set_integer(viva_sfnc::STREAM_CH_SELECTOR, stream_idx as i64, tr)
637 {
638 warn!(
639 channel = stream_idx,
640 error = %err,
641 "failed to select stream channel via SFNC"
642 );
643 used_sfnc = false;
644 }
645 } else {
646 used_sfnc = false;
647 }
648
649 if let Some(node) = dest_addr_node {
650 if let Err(err) = nm.set_integer(node, u32::from(group) as i64, tr) {
651 warn!(feature = node, error = %err, "failed to write multicast address");
652 used_sfnc = false;
653 }
654 } else {
655 used_sfnc = false;
656 }
657
658 if let Some(node) = host_port_node {
659 if let Err(err) = nm.set_integer(node, port as i64, tr) {
660 warn!(feature = node, error = %err, "failed to write multicast port");
661 used_sfnc = false;
662 }
663 } else {
664 used_sfnc = false;
665 }
666
667 if let Some(node) = mcast_en_node {
668 let _ = nm.set_bool(node, true, tr);
669 }
670 });
671
672 if !used_sfnc {
673 let base = gvcp_consts::STREAM_CHANNEL_BASE
674 + stream_idx as u64 * gvcp_consts::STREAM_CHANNEL_STRIDE;
675 let addr_reg = base + gvcp_consts::STREAM_DESTINATION_ADDRESS;
676 self.transport
677 .write(addr_reg, &group.octets())
678 .map_err(|err| GenicamError::transport(format!("write multicast addr: {err}")))?;
679 let port_reg = base + gvcp_consts::STREAM_DESTINATION_PORT;
680 self.transport
681 .write(port_reg, &port.to_be_bytes())
682 .map_err(|err| GenicamError::transport(format!("write multicast port: {err}")))?;
683 info!(
684 stream_idx,
685 %group,
686 port,
687 "configured multicast destination via raw registers"
688 );
689 } else {
690 info!(
691 stream_idx,
692 %group,
693 port,
694 "configured multicast destination via SFNC"
695 );
696 }
697
698 Ok(())
699 }
700
701 pub async fn open_event_stream(
703 &self,
704 local_ip: Ipv4Addr,
705 port: u16,
706 ) -> Result<EventStream, GenicamError> {
707 let socket = bind_event_socket_internal(IpAddr::V4(local_ip), port).await?;
708 let time_sync = if !self.time_sync.is_empty() {
709 Some(Arc::new(self.time_sync.clone()))
710 } else {
711 None
712 };
713 Ok(EventStream::new(socket, time_sync))
714 }
715
716 fn ensure_chunk_feature(&self, name: &str) -> Result<(), GenicamError> {
717 if self.nodemap.node(name).is_none() {
718 return Err(GenicamError::MissingChunkFeature(name.to_string()));
719 }
720 Ok(())
721 }
722
723 fn find_alias(&self, names: &[&'static str]) -> Option<&'static str> {
724 names
725 .iter()
726 .copied()
727 .find(|name| self.nodemap.node(name).is_some())
728 }
729}
730
731#[derive(Debug, Clone, Default)]
733pub struct ChunkConfig {
734 pub selectors: Vec<String>,
736 pub active: bool,
738}
739
740pub struct GigeRegisterIo {
759 handle: tokio::runtime::Handle,
760 device: Arc<Mutex<GigeDevice>>,
761}
762
763impl GigeRegisterIo {
764 pub fn new(handle: tokio::runtime::Handle, device: GigeDevice) -> Self {
780 let device = Arc::new(Mutex::new(device));
781 spawn_keepalive(handle.clone(), Arc::downgrade(&device));
782 Self { handle, device }
783 }
784
785 pub fn lock_device(&self) -> Result<MutexGuard<'_, GigeDevice>, GenicamError> {
790 self.device
791 .lock()
792 .map_err(|_| GenicamError::transport("gige device mutex poisoned"))
793 }
794
795 fn lock(&self) -> Result<MutexGuard<'_, GigeDevice>, GenApiError> {
796 self.device
797 .lock()
798 .map_err(|_| GenApiError::Io("gige device mutex poisoned".into()))
799 }
800}
801
802impl RegisterIo for GigeRegisterIo {
803 fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
804 let mut device = self.lock()?;
805 let fut = device.read_mem(addr, len);
806 if tokio::runtime::Handle::try_current().is_ok() {
807 tokio::task::block_in_place(|| self.handle.block_on(fut))
808 } else {
809 self.handle.block_on(fut)
810 }
811 .map_err(|err| GenApiError::Io(err.to_string()))
812 }
813
814 fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
815 let mut device = self.lock()?;
816 let fut = device.write_mem(addr, data);
817 if tokio::runtime::Handle::try_current().is_ok() {
818 tokio::task::block_in_place(|| self.handle.block_on(fut))
819 } else {
820 self.handle.block_on(fut)
821 }
822 .map_err(|err| GenApiError::Io(err.to_string()))
823 }
824}
825
826const ASSUMED_HEARTBEAT_TIMEOUT_MS: u32 = 3_000;
835
836const KEEPALIVES_PER_WINDOW: u32 = 4;
842
843const MIN_KEEPALIVE_PERIOD: Duration = Duration::from_millis(100);
846
847const MAX_KEEPALIVE_PERIOD: Duration = Duration::from_secs(2);
850
851fn keepalive_period(reported_timeout_ms: Option<u32>) -> Duration {
862 let timeout_ms = match reported_timeout_ms {
863 Some(ms) if ms > 0 => ms,
864 _ => ASSUMED_HEARTBEAT_TIMEOUT_MS,
865 };
866 Duration::from_millis(u64::from(timeout_ms / KEEPALIVES_PER_WINDOW))
867 .clamp(MIN_KEEPALIVE_PERIOD, MAX_KEEPALIVE_PERIOD)
868}
869
870async fn on_blocking_pool<R, F>(
877 handle: &tokio::runtime::Handle,
878 device: &std::sync::Weak<Mutex<GigeDevice>>,
879 op: F,
880) -> Option<Result<R, String>>
881where
882 F: FnOnce(&tokio::runtime::Handle, &mut GigeDevice) -> Result<R, String> + Send + 'static,
883 R: Send + 'static,
884{
885 let device = device.upgrade()?;
886 let handle = handle.clone();
887 let joined = tokio::task::spawn_blocking(move || match device.lock() {
888 Ok(mut guard) => op(&handle, &mut guard),
889 Err(_) => Err("gige device mutex poisoned".to_string()),
890 })
891 .await;
892 Some(joined.unwrap_or_else(|err| Err(err.to_string())))
893}
894
895fn spawn_keepalive(handle: tokio::runtime::Handle, device: std::sync::Weak<Mutex<GigeDevice>>) {
906 handle.clone().spawn(async move {
907 let reported = on_blocking_pool(&handle, &device, |handle, dev| {
908 handle
909 .block_on(dev.heartbeat_timeout_ms())
910 .map_err(|err| err.to_string())
911 })
912 .await;
913
914 let period = match reported {
915 None => return,
917 Some(Ok(timeout_ms)) => {
918 debug!(timeout_ms, "device reported GevHeartbeatTimeout");
919 keepalive_period(Some(timeout_ms))
920 }
921 Some(Err(error)) => {
922 warn!(
923 %error,
924 assumed_timeout_ms = ASSUMED_HEARTBEAT_TIMEOUT_MS,
925 "could not read GevHeartbeatTimeout; assuming a short window"
926 );
927 keepalive_period(None)
928 }
929 };
930 debug!(?period, "control channel keepalive started");
931
932 let mut ticker = tokio::time::interval(period);
933 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
936 ticker.tick().await;
939
940 let mut consecutive_failures: u32 = 0;
941 let mut privilege_seen = false;
942 loop {
943 ticker.tick().await;
944 let pinged = on_blocking_pool(&handle, &device, |handle, dev| {
945 handle
946 .block_on(dev.ping_control_channel())
947 .map_err(|err| err.to_string())
948 })
949 .await;
950 match pinged {
951 None => {
952 debug!("control channel keepalive stopped: transport dropped");
953 return;
954 }
955 Some(Ok(true)) => {
956 if consecutive_failures > 0 {
957 info!(consecutive_failures, "control channel keepalive recovered");
958 }
959 consecutive_failures = 0;
960 privilege_seen = true;
961 }
962 Some(Ok(false)) if !privilege_seen => {
963 debug!("no control privilege held; keepalive not needed");
968 return;
969 }
970 Some(Ok(false)) => {
971 consecutive_failures += 1;
972 warn!(
973 consecutive_failures,
974 "control channel privilege lost; either another application took \
975 control or an earlier keepalive did not reach the device"
976 );
977 }
978 Some(Err(error)) => {
979 consecutive_failures += 1;
980 warn!(%error, consecutive_failures, "control channel keepalive failed");
981 }
982 }
983 }
984 });
985}
986
987pub async fn connect_gige(
1007 device: &gige::DeviceInfo,
1008) -> Result<Camera<GigeRegisterIo>, GenicamError> {
1009 let (camera, _xml) = connect_gige_with_xml(device).await?;
1010 Ok(camera)
1011}
1012
1013pub async fn connect_gige_with_xml(
1019 device: &gige::DeviceInfo,
1020) -> Result<(Camera<GigeRegisterIo>, String), GenicamError> {
1021 use std::net::{IpAddr, SocketAddr};
1022 use std::sync::Arc;
1023 use tokio::sync::Mutex as AsyncMutex;
1024
1025 let control_addr = SocketAddr::new(IpAddr::V4(device.ip), gige::GVCP_PORT);
1026 info!(%control_addr, "connecting to GigE Vision camera");
1027
1028 let mut device = gige::GigeDevice::open(control_addr)
1029 .await
1030 .map_err(|e| GenicamError::transport(e.to_string()))?;
1031
1032 device
1034 .claim_control()
1035 .await
1036 .map_err(|e| GenicamError::transport(e.to_string()))?;
1037
1038 let control = Arc::new(AsyncMutex::new(device));
1039
1040 let xml = viva_genapi_xml::fetch_and_load_xml({
1042 let control = control.clone();
1043 move |address, length| {
1044 let control = control.clone();
1045 async move {
1046 let mut dev = control.lock().await;
1047 dev.read_mem(address, length)
1048 .await
1049 .map_err(|err| viva_genapi_xml::XmlError::Transport(err.to_string()))
1050 }
1051 }
1052 })
1053 .await
1054 .map_err(|e| GenicamError::transport(e.to_string()))?;
1055
1056 let nodemap = build_nodemap(&xml)?;
1057
1058 let handle = tokio::runtime::Handle::current();
1060 let control_device = Arc::try_unwrap(control)
1061 .map_err(|_| GenicamError::transport("control connection still in use"))?
1062 .into_inner();
1063 let transport = GigeRegisterIo::new(handle, control_device);
1064
1065 info!("GigE camera connected successfully");
1066 Ok((Camera::new(transport, nodemap), xml))
1067}
1068
1069#[cfg(feature = "u3v")]
1080#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
1081pub struct U3vRegisterIo<T: u3v::usb::UsbTransfer + 'static> {
1082 device: Mutex<u3v::device::U3vDevice<T>>,
1083}
1084
1085#[cfg(feature = "u3v")]
1086impl<T: u3v::usb::UsbTransfer + 'static> U3vRegisterIo<T> {
1087 pub fn new(device: u3v::device::U3vDevice<T>) -> Self {
1089 Self {
1090 device: Mutex::new(device),
1091 }
1092 }
1093
1094 pub fn lock_device(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenicamError> {
1096 self.device
1097 .lock()
1098 .map_err(|_| GenicamError::transport("u3v device mutex poisoned"))
1099 }
1100
1101 fn lock(&self) -> Result<MutexGuard<'_, u3v::device::U3vDevice<T>>, GenApiError> {
1102 self.device
1103 .lock()
1104 .map_err(|_| GenApiError::Io("u3v device mutex poisoned".into()))
1105 }
1106}
1107
1108#[cfg(feature = "u3v")]
1109impl<T: u3v::usb::UsbTransfer + 'static> RegisterIo for U3vRegisterIo<T> {
1110 fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
1111 let mut device = self.lock()?;
1112 device
1113 .read_mem(addr, len)
1114 .map_err(|e| GenApiError::Io(e.to_string()))
1115 }
1116
1117 fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
1118 let mut device = self.lock()?;
1119 device
1120 .write_mem(addr, data)
1121 .map_err(|e| GenApiError::Io(e.to_string()))
1122 }
1123}
1124
1125#[cfg(feature = "u3v-usb")]
1144#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
1145pub fn connect_u3v(
1146 device: &u3v::discovery::U3vDeviceInfo,
1147) -> Result<Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, GenicamError> {
1148 let (camera, _xml) = connect_u3v_with_xml(device)?;
1149 Ok(camera)
1150}
1151
1152#[cfg(feature = "u3v-usb")]
1155#[cfg_attr(docsrs, doc(cfg(feature = "u3v-usb")))]
1156pub fn connect_u3v_with_xml(
1157 device_info: &u3v::discovery::U3vDeviceInfo,
1158) -> Result<(Camera<U3vRegisterIo<u3v::usb::RusbTransfer>>, String), GenicamError> {
1159 info!(
1160 vendor_id = device_info.vendor_id,
1161 product_id = device_info.product_id,
1162 "connecting to USB3 Vision camera"
1163 );
1164
1165 let mut device = u3v::device::U3vDevice::open_device(device_info)
1166 .map_err(|e| GenicamError::transport(e.to_string()))?;
1167
1168 let xml = device
1169 .fetch_xml()
1170 .map_err(|e| GenicamError::transport(e.to_string()))?;
1171
1172 let nodemap = build_nodemap(&xml)?;
1173 let transport = U3vRegisterIo::new(device);
1174
1175 info!("USB3 Vision camera connected successfully");
1176 Ok((Camera::new(transport, nodemap), xml))
1177}
1178
1179#[cfg(feature = "u3v")]
1186#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
1187pub fn open_u3v_device<T: u3v::usb::UsbTransfer + 'static>(
1188 mut device: u3v::device::U3vDevice<T>,
1189) -> Result<(Camera<U3vRegisterIo<T>>, String), GenicamError> {
1190 let xml = device
1191 .fetch_xml()
1192 .map_err(|e| GenicamError::transport(e.to_string()))?;
1193 let nodemap = build_nodemap(&xml)?;
1194 let transport = U3vRegisterIo::new(device);
1195 Ok((Camera::new(transport, nodemap), xml))
1196}
1197
1198fn build_nodemap(xml: &str) -> Result<NodeMap, GenicamError> {
1206 let model =
1207 viva_genapi_xml::parse(xml).map_err(|e| GenicamError::parse(format!("GenApi XML: {e}")))?;
1208 let nodemap = NodeMap::try_from_xml(model)
1210 .map_err(|e| GenicamError::parse(format!("GenApi model: {e}")))?;
1211 for skipped in nodemap.skipped() {
1212 debug!(
1213 kind = %skipped.tag,
1214 node = skipped.name.as_deref().unwrap_or("<unnamed>"),
1215 error = %skipped.error,
1216 "GenApi node unavailable"
1217 );
1218 }
1219
1220 if !nodemap.skipped().is_empty() {
1221 warn!(
1222 unavailable = nodemap.skipped().len(),
1223 features = nodemap.node_names().count(),
1224 "some camera features are unavailable; run `viva-camctl report` for the list \
1225 and please report them at https://github.com/VitalyVorobyev/viva-genicam/issues"
1226 );
1227 }
1228 Ok(nodemap)
1229}
1230
1231fn parse_bool(value: &str) -> Option<bool> {
1232 match value.trim().to_ascii_lowercase().as_str() {
1233 "1" | "true" => Some(true),
1234 "0" | "false" => Some(false),
1235 _ => None,
1236 }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 #[test]
1244 fn keepalive_period_fits_four_pings_in_the_reported_window() {
1245 assert_eq!(keepalive_period(Some(3_000)), Duration::from_millis(750));
1246 assert_eq!(keepalive_period(Some(1_000)), Duration::from_millis(250));
1247 }
1248
1249 #[test]
1250 fn an_unusable_heartbeat_timeout_falls_back_to_a_short_window() {
1251 let assumed = keepalive_period(Some(ASSUMED_HEARTBEAT_TIMEOUT_MS));
1254 assert_eq!(keepalive_period(None), assumed);
1255 assert_eq!(keepalive_period(Some(0)), assumed);
1256 }
1257
1258 #[test]
1259 fn the_period_is_clamped_at_both_ends() {
1260 assert_eq!(keepalive_period(Some(40)), MIN_KEEPALIVE_PERIOD);
1263 assert_eq!(keepalive_period(Some(600_000)), MAX_KEEPALIVE_PERIOD);
1264 }
1265}