1#![cfg_attr(docsrs, feature(doc_cfg))]
2pub 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#[derive(Debug, Error)]
111pub enum GenicamError {
112 #[error(transparent)]
114 GenApi(#[from] GenApiError),
115 #[error("transport: {0}")]
117 Transport(String),
118 #[error("parse error: {0}")]
120 Parse(String),
121 #[error("chunk feature '{0}' not found; verify camera supports chunk data")]
123 MissingChunkFeature(String),
124 #[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#[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 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 pub fn transport(&self) -> &T {
166 &self.transport
167 }
168
169 pub fn transport_mut(&mut self) -> &mut T {
171 &mut self.transport
172 }
173
174 pub fn nodemap(&self) -> &NodeMap {
176 &self.nodemap
177 }
178
179 pub fn nodemap_mut(&mut self) -> &mut NodeMap {
181 &mut self.nodemap
182 }
183
184 pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenicamError> {
186 self.nodemap.enum_entries(name).map_err(Into::into)
187 }
188
189 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 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 pub fn set_exposure_time_us(&mut self, value: f64) -> Result<(), GenicamError> {
258 self.set_float_feature("ExposureTime", value)
260 }
261
262 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 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 pub fn map_dev_ts(&self, dev_ticks: u64) -> SystemTime {
383 self.time_sync.to_host_time(dev_ticks)
384 }
385
386 pub fn time_sync(&self) -> &TimeSync {
388 &self.time_sync
389 }
390
391 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
662pub struct ChunkConfig {
663 pub selectors: Vec<String>,
665 pub active: bool,
667}
668
669pub struct GigeRegisterIo {
676 handle: tokio::runtime::Handle,
677 device: Mutex<GigeDevice>,
678}
679
680impl GigeRegisterIo {
681 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}