Skip to main content

viva_genicam/
stream.rs

1//! Streaming builder and configuration helpers bridging `tl-gige` with
2//! higher-level GenICam consumers.
3//!
4//! The builder performs control-plane negotiation (packet size, delay) and
5//! prepares a UDP socket configured for reception. Applications can retrieve the
6//! socket handle to drive their own async pipelines while relying on the shared
7//! [`StreamStats`] accumulator for monitoring.
8//!
9//! # High-Level Streaming
10//!
11//! For most use cases, [`FrameStream`] provides an ergonomic async iterator over
12//! reassembled frames:
13//!
14//! ```rust,ignore
15//! let stream = FrameStream::new(raw_stream, None);
16//! while let Some(frame) = stream.next_frame().await? {
17//!     println!("{}x{} frame", frame.width, frame.height);
18//! }
19//! ```
20
21#[cfg(any(not(windows), test))]
22use std::collections::HashSet;
23#[cfg(windows)]
24use std::io::ErrorKind;
25use std::net::{IpAddr, Ipv4Addr};
26#[cfg(windows)]
27use std::sync::Arc;
28#[cfg(windows)]
29use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30#[cfg(windows)]
31use std::thread;
32#[cfg(not(windows))]
33use std::time::SystemTime;
34use std::time::{Duration, Instant};
35
36#[cfg(not(windows))]
37use bytes::Bytes;
38use bytes::BytesMut;
39use tokio::net::UdpSocket;
40// Used by the receive loop on non-Windows and by the packet-size probe
41// everywhere — the probe runs before the socket is handed to the Windows
42// reader thread, so it is a tokio socket on every platform.
43use tokio::time::timeout;
44#[cfg(not(windows))]
45use tracing::trace;
46use tracing::{debug, info, warn};
47use viva_pfnc::PixelFormat;
48
49use crate::GenicamError;
50use crate::frame::Frame;
51use crate::time::TimeSync;
52use viva_gige::gvcp::{GigeDevice, StreamParams};
53#[cfg(any(not(windows), test))]
54use viva_gige::gvsp::PacketBitmap;
55use viva_gige::gvsp::{self, GvspPacket, StreamConfig};
56use viva_gige::nic::{self, DEFAULT_RCVBUF_BYTES, Iface, McOptions};
57use viva_gige::stats::{StreamStats, StreamStatsAccumulator};
58
59pub use viva_gige::gvsp::StreamDest;
60
61/// Internal packet source abstraction.
62///
63/// Holds either a standard UDP socket or a custom transport backend.
64/// This avoids making `Stream`/`FrameStream` generic while supporting
65/// both paths.
66pub(crate) enum PacketSource {
67    Udp(UdpSocket),
68}
69
70impl PacketSource {
71    /// Receive raw packet bytes from the source.
72    #[cfg(not(windows))]
73    async fn recv(&self, buf: &mut [u8]) -> Result<Bytes, GenicamError> {
74        match self {
75            PacketSource::Udp(socket) => {
76                let (len, _) = socket
77                    .recv_from(buf)
78                    .await
79                    .map_err(|e| GenicamError::transport(format!("socket recv failed: {e}")))?;
80                Ok(Bytes::copy_from_slice(&buf[..len]))
81            }
82        }
83    }
84
85    /// Borrow the UDP socket, if this is the UDP path.
86    fn as_udp_socket(&self) -> Option<&UdpSocket> {
87        match self {
88            PacketSource::Udp(s) => Some(s),
89        }
90    }
91}
92
93/// Smallest GVSP packet size we will configure: the IPv4 minimum reassembly
94/// buffer.
95const MIN_PACKET_SIZE: u32 = 576;
96
97/// Largest GVSP packet size we will configure.
98///
99/// Both bounds bite at once: an IPv4 datagram cannot exceed 65 535 bytes, and
100/// `GevSCPSPacketSize` holds the size in 16 bits.
101const MAX_PACKET_SIZE: u32 = viva_gige::gvcp::STREAM_PACKET_SIZE_MASK;
102
103/// Write `GevSCPSPacketSize`, then read it back and return what the device
104/// actually holds.
105///
106/// A camera may clamp the requested size to what it supports, and the write
107/// succeeds when it does — nothing on the wire distinguishes "accepted" from
108/// "accepted and reduced". The receive path derives every reassembly offset
109/// from [`StreamParams::packet_size`] via `gvsp_payload_size`, so believing the
110/// request produces a stream that carries packets and completes no frame:
111/// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112), where a
112/// Vieworks FS3200T on a 16 114-byte link delivered zero frames on every Start
113/// and streamed correctly when forced to 1 500. Backlog SR-02.
114///
115/// The read comes first as well as last. Viva Studio rebuilds the stream on
116/// every Acquisition Start, so an unconditional write discards a working
117/// configuration once per Start; when the device already holds the size we
118/// want, the write is skipped entirely.
119///
120/// A device that will not answer the read-back is not failed — that would break
121/// streaming which works today on a camera whose only fault is refusing a
122/// READREG. It warns and keeps the requested value, which is exactly the old
123/// behaviour, now visible in the log rather than assumed.
124async fn configure_packet_size(
125    device: &mut GigeDevice,
126    channel: u32,
127    requested: u32,
128) -> Result<u32, GenicamError> {
129    if let Ok(current) = device.get_stream_packet_size(channel).await
130        && current == requested
131    {
132        debug!(
133            channel,
134            packet_size = requested,
135            "camera already holds the requested GVSP packet size; leaving it alone"
136        );
137        return Ok(requested);
138    }
139
140    device
141        .set_stream_packet_size(channel, requested)
142        .await
143        .map_err(|err| GenicamError::transport(err.to_string()))?;
144
145    let effective = match device.get_stream_packet_size(channel).await {
146        Ok(effective) => effective,
147        Err(err) => {
148            warn!(
149                channel,
150                packet_size = requested,
151                error = %err,
152                "could not read GevSCPSPacketSize back; assuming the requested size took effect. \
153                 If no frame completes, the camera may have clamped it — pass an explicit packet \
154                 size of 1500 to find out"
155            );
156            return Ok(requested);
157        }
158    };
159
160    if effective == requested {
161        return Ok(effective);
162    }
163
164    if effective < MIN_PACKET_SIZE {
165        return Err(GenicamError::transport(format!(
166            "camera reduced the GVSP packet size from {requested} to {effective}, below the \
167             {MIN_PACKET_SIZE}-byte minimum; it cannot stream on this link"
168        )));
169    }
170
171    warn!(
172        channel,
173        requested,
174        effective,
175        "camera clamped the GVSP packet size; the receive path will follow the camera. \
176         The requested size is what the host interface MTU allows, so the camera is the \
177         narrower end of this link"
178    );
179    Ok(effective)
180}
181
182/// Packet size at or below which probing has nothing to discover, and the
183/// control size the probe uses to decide whether the device answers at all.
184///
185/// 1500 is the Ethernet default: a path that cannot carry it is broken in a way
186/// no negotiation can rescue.
187const PROBE_FLOOR: u32 = 1500;
188
189/// How long to wait for a test packet before calling the size unusable.
190const PROBE_TIMEOUT: Duration = Duration::from_millis(250);
191
192/// Find the largest packet size the network path will actually deliver.
193///
194/// `GevSCPSPacketSize` reports what the *device* stored, which is not what the
195/// *link* will carry. On the Vieworks FS3200T in
196/// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112) the camera
197/// declares `Max=16366`, accepts and holds 16114, and streams nothing: the path
198/// tops out at a 9216-byte frame, so 9198 is the largest usable size. The
199/// reporter found that by bisecting by hand. The specification provides the
200/// mechanism to find it automatically — bit 31 asks the device for one test
201/// packet, bit 30 forbids fragmenting it — and we used neither.
202///
203/// **A device that does not implement test packets must not be punished for
204/// it.** Walking such a device down to 1500 would be a severe regression for
205/// every working jumbo link. So the probe first asks for a test packet at
206/// [`PROBE_FLOOR`], a size any functioning path carries: if *that* produces
207/// nothing, the device does not answer probes and the requested size is kept
208/// unchanged. Only a device that has demonstrably answered is allowed to talk
209/// us downwards.
210///
211/// The probe never increases the size, so an explicitly configured
212/// `--packet-size` is still a ceiling.
213///
214/// **Probing is destructive, so the answer has to be written back.** Asking for
215/// a test packet *is* a write to `GevSCPSPacketSize` — there is no separate
216/// register — so when the probe returns, the device holds the last size it was
217/// *asked about*, which is rarely the size that was chosen. Every path that
218/// probed therefore ends by configuring the negotiated value.
219///
220/// That rewrite also clears the do-not-fragment bit the probe set — except where
221/// the device already holds the negotiated size, since `configure_packet_size`
222/// skips a redundant write. The exception is the safe one: it is reachable only
223/// when a *DF-set* test packet of that size traversed the path a moment earlier,
224/// which is the evidence that leaving DF on cannot hurt.
225async fn probe_packet_size(
226    device: &mut GigeDevice,
227    channel: u32,
228    socket: &UdpSocket,
229    requested: u32,
230) -> Result<u32, GenicamError> {
231    if requested <= PROBE_FLOOR {
232        // Nothing was probed, so nothing was written and there is nothing to
233        // put back.
234        return Ok(requested);
235    }
236
237    let negotiated = probe_path_ceiling(device, channel, socket, requested).await?;
238
239    // The register now holds the last size the probe *tested*, not `negotiated`.
240    // Both mismatches are real and neither is visible from here: a device that
241    // never answered was left at `PROBE_FLOOR`, and a bisection whose final
242    // probe failed was left one byte above its own answer -- 9199 against a
243    // negotiated 9198 on the numbers in #112, which is exactly the size that
244    // reporter measured as the first failing one. Left uncorrected, the host
245    // strides at `negotiated` while the camera sends something else, which is
246    // the SR-02 failure mode this probe was built on top of.
247    configure_packet_size(device, channel, negotiated).await
248}
249
250/// The bisection itself: the largest size that arrives, or `requested` when the
251/// device does not answer probes at all.
252///
253/// Split out from [`probe_packet_size`] so that every early return still passes
254/// through the write-back, rather than each one having to remember it.
255async fn probe_path_ceiling(
256    device: &mut GigeDevice,
257    channel: u32,
258    socket: &UdpSocket,
259    requested: u32,
260) -> Result<u32, GenicamError> {
261    // Control probe. Establishes that this device answers at all before any
262    // negative result is allowed to mean anything.
263    if !test_packet_arrives(device, channel, socket, PROBE_FLOOR).await? {
264        debug!(
265            channel,
266            packet_size = requested,
267            "device did not answer a {PROBE_FLOOR}-byte test packet; it likely does not \
268             implement them, so the requested size stands"
269        );
270        return Ok(requested);
271    }
272
273    if test_packet_arrives(device, channel, socket, requested).await? {
274        debug!(
275            channel,
276            packet_size = requested,
277            "path carries the requested GVSP packet size"
278        );
279        return Ok(requested);
280    }
281
282    // Known good at `lo`, known bad at `hi`. Bisect to the byte: the boundary
283    // is a frame ceiling, not a round number, and landing one byte below it is
284    // the difference between a working jumbo link and 1500.
285    let (mut lo, mut hi) = (PROBE_FLOOR, requested);
286    while hi - lo > 1 {
287        let mid = lo + (hi - lo) / 2;
288        if test_packet_arrives(device, channel, socket, mid).await? {
289            lo = mid;
290        } else {
291            hi = mid;
292        }
293    }
294
295    warn!(
296        channel,
297        requested,
298        negotiated = lo,
299        "the network path did not carry a {requested}-byte GVSP test packet; negotiated down. \
300         Both the host interface and the camera accept the larger size, so the limit is the \
301         link between them"
302    );
303    Ok(lo)
304}
305
306/// Ask for one test packet of `size` and report whether it arrived.
307async fn test_packet_arrives(
308    device: &mut GigeDevice,
309    channel: u32,
310    socket: &UdpSocket,
311    size: u32,
312) -> Result<bool, GenicamError> {
313    // Discard anything already queued, so a previous probe's late arrival
314    // cannot be counted as this one's answer.
315    let mut scratch = [0u8; 2048];
316    while socket.try_recv_from(&mut scratch).is_ok() {}
317
318    device
319        .request_test_packet(channel, size)
320        .await
321        .map_err(|err| GenicamError::transport(err.to_string()))?;
322
323    let mut buf = vec![0u8; size as usize + 64];
324    match timeout(PROBE_TIMEOUT, socket.recv_from(&mut buf)).await {
325        Ok(Ok(_)) => Ok(true),
326        // A receive error here is the socket's problem, not the path's; treat
327        // it as "no answer" rather than failing the whole stream.
328        Ok(Err(_)) | Err(_) => Ok(false),
329    }
330}
331
332/// How long a stream may produce nothing before [`SilenceWatch`] speaks up.
333const SILENCE_GRACE: Duration = Duration::from_secs(3);
334
335/// How often the non-Windows receive loop wakes to re-check the watch.
336///
337/// The Windows reader thread gets the same effect free from its 100 ms socket
338/// read timeout.
339#[cfg(not(windows))]
340const SILENCE_POLL: Duration = Duration::from_millis(500);
341
342/// What a [`SilenceWatch`] has concluded so far.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344enum Silence {
345    /// Nothing to say: still inside the grace period, or already spoken, or the
346    /// stream is delivering frames.
347    Quiet,
348    /// Not one datagram has arrived.
349    NoPacket,
350    /// Datagrams are arriving and no frame has completed.
351    NoFrame,
352}
353
354/// Warns once when a stream produces nothing, and names what to check.
355///
356/// Backlog DX-09. A silent stream reports `frames=0 drops=0 resends=0`, and
357/// that line is identical for a firewall block, a packet size the path cannot
358/// carry, a control privilege held by another application, and a camera that is
359/// simply not triggering. [#70](https://github.com/VitalyVorobyev/viva-genicam/issues/70)'s
360/// reporter worked the third of those out unaided;
361/// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112)'s needed a
362/// custom instrumented build to find the second.
363///
364/// The distinction the watch adds over "no frames" is whether *datagrams* are
365/// arriving, which the receiver already knows and never reported. Nothing
366/// arriving is a path or privilege problem; datagrams arriving with no frame
367/// completing is a packet-size disagreement, which is exactly SR-02.
368struct SilenceWatch {
369    since: Instant,
370    packet_size: u32,
371    saw_packet: bool,
372    saw_frame: bool,
373    warned: bool,
374}
375
376impl SilenceWatch {
377    fn new(packet_size: u32) -> Self {
378        Self {
379            since: Instant::now(),
380            packet_size,
381            saw_packet: false,
382            saw_frame: false,
383            warned: false,
384        }
385    }
386
387    /// A datagram arrived — parsed or not. Malformed still means the path works.
388    fn record_packet(&mut self) {
389        self.saw_packet = true;
390    }
391
392    /// A frame completed, so there is nothing left to diagnose.
393    fn record_frame(&mut self) {
394        self.saw_frame = true;
395    }
396
397    /// Decide what to say at `elapsed`, marking the verdict as spoken.
398    ///
399    /// Split from [`SilenceWatch::tick`] so the decision is testable without a
400    /// clock.
401    fn assess(&mut self, elapsed: Duration) -> Silence {
402        if self.warned || self.saw_frame || elapsed < SILENCE_GRACE {
403            return Silence::Quiet;
404        }
405        self.warned = true;
406        if self.saw_packet {
407            Silence::NoFrame
408        } else {
409            Silence::NoPacket
410        }
411    }
412
413    /// Emit the warning if one is due. Cheap enough to call on every timeout.
414    fn tick(&mut self) {
415        let seconds = SILENCE_GRACE.as_secs();
416        match self.assess(self.since.elapsed()) {
417            Silence::Quiet => {}
418            Silence::NoPacket => warn!(
419                packet_size = self.packet_size,
420                seconds,
421                "no GVSP packet has arrived since the stream opened. Check, roughly in order of \
422                 how often each is the cause: a host firewall blocking inbound UDP on the stream \
423                 port; another application holding control privilege, so AcquisitionStart never \
424                 reached the camera; a camera waiting for a trigger; or a network path that \
425                 cannot carry packets this large — retry with an explicit packet size of 1500 to \
426                 rule that out"
427            ),
428            Silence::NoFrame => warn!(
429                packet_size = self.packet_size,
430                payload_stride = gvsp_payload_size(self.packet_size),
431                seconds,
432                "GVSP packets are arriving but no frame has completed. Reassembly places each \
433                 packet at a stride derived from the negotiated packet size, so the usual cause \
434                 is the two ends disagreeing about it — retry with an explicit packet size of \
435                 1500"
436            ),
437        }
438    }
439}
440
441/// How [`StreamBuilder`] chooses `GevSCPSPacketSize` (ADR-0021 / SR-14).
442///
443/// The variants differ only in where the *starting* size comes from. All three
444/// then hand it to the SR-13 path probe, which can lower it and never raise it,
445/// so no variant can end up above the size it began with.
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
447enum PacketSizeMode {
448    /// Start from the camera's current value; never write a larger one.
449    #[default]
450    Preserve,
451    /// Start from `best_packet_size(nic_mtu)`.
452    Auto,
453    /// Start from this size — a ceiling, never raised above.
454    Explicit(u32),
455}
456
457/// Builder for configuring a GVSP stream.
458pub struct StreamBuilder<'a> {
459    device: &'a mut GigeDevice,
460    iface: Option<Iface>,
461    dest: Option<StreamDest>,
462    rcvbuf_bytes: Option<usize>,
463    target_mtu: Option<u32>,
464    packet_size_mode: PacketSizeMode,
465    packet_delay: Option<u32>,
466    channel: u32,
467    dst_port: u16,
468    probe: bool,
469}
470
471impl<'a> StreamBuilder<'a> {
472    /// Create a new builder bound to an opened [`GigeDevice`].
473    ///
474    /// Default packet-size policy is **preserve**: the camera's current
475    /// `GevSCPSPacketSize` is used as-is. Call [`StreamBuilder::auto_packet_size`]
476    /// to set from the NIC MTU and path-probe, or [`StreamBuilder::packet_size`]
477    /// for an explicit ceiling (ADR-0021).
478    pub fn new(device: &'a mut GigeDevice) -> Self {
479        Self {
480            device,
481            iface: None,
482            dest: None,
483            rcvbuf_bytes: None,
484            target_mtu: None,
485            packet_size_mode: PacketSizeMode::Preserve,
486            packet_delay: None,
487            channel: 0,
488            dst_port: 0,
489            // On for every policy, Preserve included: the probe only lowers.
490            probe: true,
491        }
492    }
493
494    /// Whether to path-probe with a GVSP test packet before streaming
495    /// (default: on, under every packet-size policy).
496    ///
497    /// It applies under the default preserve policy too, which is not a
498    /// contradiction: the probe never *raises* a size, so it cannot override the
499    /// value an operator set. It can only decline to stream at a size the path
500    /// demonstrably drops — something no register read can discover, because
501    /// both endpoints accept it. Turning it off is what makes preserve literal.
502    ///
503    /// Turning it off restores the pre-0.4.2 behaviour: whatever
504    /// `GevSCPSPacketSize` holds is assumed to reach the host.
505    /// That is wrong on any path narrower than both endpoints
506    /// ([#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112)), so
507    /// the only good reason to disable it is a device that misbehaves when
508    /// asked for a test packet — in which case please open an issue, because
509    /// the probe is written to tolerate a device that simply ignores it.
510    pub fn probe(mut self, enable: bool) -> Self {
511        self.probe = enable;
512        self
513    }
514
515    /// Select the interface used for receiving GVSP packets.
516    pub fn iface(mut self, iface: Iface) -> Self {
517        self.iface = Some(iface);
518        self
519    }
520
521    /// Configure the stream destination.
522    pub fn dest(mut self, dest: StreamDest) -> Self {
523        self.dest = Some(dest);
524        self
525    }
526
527    /// Cap the MTU used when computing the GVSP packet size in
528    /// [`StreamBuilder::auto_packet_size`] mode.
529    ///
530    /// The interface's own MTU is still read; this only lowers the request.
531    pub fn target_mtu(mut self, mtu: u32) -> Self {
532        self.target_mtu = Some(mtu);
533        self
534    }
535
536    /// Set `GevSCPSPacketSize` from the host NIC MTU, then path-probe (SR-13).
537    ///
538    /// This is the explicit opt-in that replaces the old always-write-MTU
539    /// default (ADR-0021). It is **not** the pre-0.4 `--auto false ⇒ 1500`
540    /// behaviour.
541    pub fn auto_packet_size(mut self) -> Self {
542        self.packet_size_mode = PacketSizeMode::Auto;
543        self
544    }
545
546    /// Write this GVSP packet size (a ceiling for the path probe).
547    pub fn packet_size(mut self, size: u32) -> Self {
548        self.packet_size_mode = PacketSizeMode::Explicit(size);
549        self
550    }
551
552    /// Override the GVSP inter-packet delay.
553    pub fn packet_delay(mut self, delay: u32) -> Self {
554        self.packet_delay = Some(delay);
555        self
556    }
557
558    /// Configure the UDP port used for streaming (defaults to 0 => device chosen).
559    pub fn destination_port(mut self, port: u16) -> Self {
560        self.dst_port = port;
561        if let Some(dest) = &mut self.dest {
562            *dest = match *dest {
563                StreamDest::Unicast { dst_ip, .. } => StreamDest::Unicast {
564                    dst_ip,
565                    dst_port: port,
566                },
567                StreamDest::Multicast {
568                    group,
569                    loopback,
570                    ttl,
571                    ..
572                } => StreamDest::Multicast {
573                    group,
574                    port,
575                    loopback,
576                    ttl,
577                },
578            };
579        }
580        self
581    }
582
583    /// Configure multicast reception when the device is set to multicast mode.
584    pub fn multicast(mut self, group: Option<Ipv4Addr>) -> Self {
585        if let Some(group) = group {
586            self.dest = Some(StreamDest::Multicast {
587                group,
588                port: self.dst_port,
589                loopback: false,
590                ttl: 1,
591            });
592        } else {
593            self.dest = None;
594        }
595        self
596    }
597
598    /// Custom receive buffer size for the UDP socket.
599    pub fn rcvbuf_bytes(mut self, size: usize) -> Self {
600        self.rcvbuf_bytes = Some(size);
601        self
602    }
603
604    /// Select the GigE Vision stream channel to configure (defaults to 0).
605    pub fn channel(mut self, channel: u32) -> Self {
606        self.channel = channel;
607        self
608    }
609
610    /// Finalise the builder and return a configured [`Stream`].
611    pub async fn build(self) -> Result<Stream, GenicamError> {
612        let iface = self
613            .iface
614            .ok_or_else(|| GenicamError::transport("stream requires a network interface"))?;
615        let host_ip = iface
616            .ipv4()
617            .ok_or_else(|| GenicamError::transport("interface lacks IPv4 address"))?;
618        let default_port = if self.dst_port == 0 {
619            0x5FFF
620        } else {
621            self.dst_port
622        };
623        let mut dest = self.dest.unwrap_or(StreamDest::Unicast {
624            dst_ip: host_ip,
625            dst_port: default_port,
626        });
627        match &mut dest {
628            StreamDest::Unicast { dst_port, .. } => {
629                if *dst_port == 0 {
630                    *dst_port = default_port;
631                }
632            }
633            StreamDest::Multicast { port, .. } => {
634                if *port == 0 {
635                    *port = default_port;
636                }
637            }
638        }
639
640        let iface_mtu = nic::mtu(&iface).map_err(|err| GenicamError::transport(err.to_string()))?;
641        let mtu = self
642            .target_mtu
643            .map_or(iface_mtu, |limit| limit.min(iface_mtu));
644
645        // ADR-0021: default preserves the camera register. Auto writes the NIC
646        // MTU then SR-13-bisects; Explicit writes a caller ceiling. Preserve is
647        // never "fall back to 1500" (that was the pre-0.4 auto=false trap).
648        let (requested, write_camera) = match self.packet_size_mode {
649            PacketSizeMode::Preserve => {
650                let current = self
651                    .device
652                    .get_stream_packet_size(self.channel)
653                    .await
654                    .map_err(|err| GenicamError::transport(err.to_string()))?;
655                debug!(
656                    channel = self.channel,
657                    packet_size = current,
658                    "preserving camera GevSCPSPacketSize; pass auto_packet_size() \
659                     or packet_size(n) to negotiate"
660                );
661                (current, false)
662            }
663            PacketSizeMode::Auto => (nic::best_packet_size(mtu), true),
664            PacketSizeMode::Explicit(size) => (size, true),
665        };
666
667        if requested < MIN_PACKET_SIZE {
668            return Err(GenicamError::transport(format!(
669                "GVSP packet size {requested} is below the {MIN_PACKET_SIZE}-byte minimum; \
670                 raise the camera's GevSCPSPacketSize, or override with auto_packet_size() \
671                 or packet_size(n)"
672            )));
673        }
674        if requested > MAX_PACKET_SIZE {
675            return Err(GenicamError::transport(format!(
676                "GVSP packet size {requested} exceeds the {MAX_PACKET_SIZE}-byte maximum an \
677                 IPv4 datagram can carry, which is also the widest value GevSCPSPacketSize can \
678                 hold"
679            )));
680        }
681
682        match &dest {
683            StreamDest::Unicast { dst_ip, dst_port } => {
684                info!(%dst_ip, dst_port, channel = self.channel, "configuring unicast stream");
685                self.device
686                    .set_stream_destination(self.channel, *dst_ip, *dst_port)
687                    .await
688                    .map_err(|err| GenicamError::transport(err.to_string()))?;
689            }
690            StreamDest::Multicast { .. } => {
691                info!(
692                    channel = self.channel,
693                    port = dest.port(),
694                    addr = %dest.addr(),
695                    "configuring multicast stream parameters"
696                );
697            }
698        }
699
700        // Bind before configuring, so the socket is already listening when the
701        // probe below asks the camera to send to it.
702        let source = PacketSource::Udp(Self::bind_socket(&dest, &iface, self.rcvbuf_bytes).await?);
703
704        let packet_size = if write_camera {
705            configure_packet_size(self.device, self.channel, requested).await?
706        } else {
707            requested
708        };
709        // Preserve probes too. The probe only ever *lowers* a size, so it cannot
710        // override the value an operator chose -- it can only decline to send at
711        // a size the path demonstrably drops, which no register read can reveal
712        // (#112). Skipping it under Preserve would leave the reporter's own
713        // camera at the 16114 an earlier run wrote, streaming nothing, with the
714        // one mechanism that could rescue it turned off. `probe(false)` is the
715        // escape for a literal preserve.
716        let packet_size = if self.probe {
717            let socket = source
718                .as_udp_socket()
719                .expect("the UDP path always has a socket");
720            probe_packet_size(self.device, self.channel, socket, packet_size).await?
721        } else {
722            packet_size
723        };
724
725        // The delay compensates for a burst of many small packets, so it follows
726        // the size actually in force -- known only now, after a clamp or a probe.
727        // Keying it off the NIC MTU was equivalent while the size was *derived*
728        // from that MTU; under Preserve the two come apart, and a jumbo NIC in
729        // front of a camera holding 1500 is exactly the case that needs spacing.
730        let packet_delay = self.packet_delay.unwrap_or({
731            const DELAY_NS: u32 = 2_000;
732            if packet_size <= 1500 {
733                DELAY_NS / 80
734            } else {
735                0
736            }
737        });
738        self.device
739            .set_stream_packet_delay(self.channel, packet_delay)
740            .await
741            .map_err(|err| GenicamError::transport(err.to_string()))?;
742
743        let source_filter = if dest.is_multicast() {
744            None
745        } else {
746            Some(dest.addr())
747        };
748        let resend_enabled = !dest.is_multicast();
749
750        let params = StreamParams {
751            packet_size,
752            packet_delay,
753            mtu,
754            host: dest.addr(),
755            port: dest.port(),
756        };
757
758        let config = StreamConfig {
759            dest,
760            iface: iface.clone(),
761            packet_size: Some(packet_size),
762            packet_delay: Some(packet_delay),
763            source_filter,
764            resend_enabled,
765        };
766
767        let stats = StreamStatsAccumulator::new();
768        Ok(Stream {
769            source,
770            stats,
771            params,
772            config,
773        })
774    }
775
776    /// Bind a UDP socket for the given stream destination.
777    async fn bind_socket(
778        dest: &StreamDest,
779        iface: &Iface,
780        rcvbuf_bytes: Option<usize>,
781    ) -> Result<UdpSocket, GenicamError> {
782        match dest {
783            StreamDest::Unicast { dst_port, .. } => {
784                let bind_ip = IpAddr::V4(
785                    iface
786                        .ipv4()
787                        .ok_or_else(|| GenicamError::transport("interface lacks IPv4 address"))?,
788                );
789                nic::bind_udp(bind_ip, *dst_port, Some(iface.clone()), rcvbuf_bytes)
790                    .await
791                    .map_err(|err| GenicamError::transport(err.to_string()))
792            }
793            StreamDest::Multicast {
794                group,
795                port,
796                loopback,
797                ttl,
798            } => {
799                let opts = McOptions {
800                    loopback: *loopback,
801                    ttl: *ttl,
802                    rcvbuf_bytes: rcvbuf_bytes.unwrap_or(DEFAULT_RCVBUF_BYTES),
803                    ..McOptions::default()
804                };
805                nic::bind_multicast(iface, *group, *port, &opts)
806                    .await
807                    .map_err(|err| GenicamError::transport(err.to_string()))
808            }
809        }
810    }
811}
812
813/// Handle returned by [`StreamBuilder`] providing access to the configured
814/// packet source and statistics.
815pub struct Stream {
816    source: PacketSource,
817    stats: StreamStatsAccumulator,
818    params: StreamParams,
819    config: StreamConfig,
820}
821
822impl Stream {
823    /// Borrow the underlying UDP socket (returns `None` when using a custom transport).
824    pub fn socket(&self) -> Option<&UdpSocket> {
825        self.source.as_udp_socket()
826    }
827
828    /// Consume the stream and return its parts.
829    pub(crate) fn into_parts(
830        self,
831    ) -> (
832        PacketSource,
833        StreamStatsAccumulator,
834        StreamParams,
835        StreamConfig,
836    ) {
837        (self.source, self.stats, self.params, self.config)
838    }
839
840    /// Access the negotiated stream parameters.
841    pub fn params(&self) -> StreamParams {
842        self.params
843    }
844
845    /// Obtain a clone of the statistics accumulator handle for updates.
846    pub fn stats_handle(&self) -> StreamStatsAccumulator {
847        self.stats.clone()
848    }
849
850    /// Snapshot the collected statistics.
851    pub fn stats(&self) -> StreamStats {
852        self.stats.snapshot()
853    }
854
855    /// Immutable view of the stream configuration.
856    pub fn config(&self) -> &StreamConfig {
857        &self.config
858    }
859}
860
861impl<'a> From<&'a mut GigeDevice> for StreamBuilder<'a> {
862    fn from(device: &'a mut GigeDevice) -> Self {
863        StreamBuilder::new(device)
864    }
865}
866
867// ============================================================================
868// High-Level FrameStream API
869// ============================================================================
870
871/// Default timeout for frame assembly before declaring incomplete and moving on.
872const DEFAULT_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
873
874/// GVSP header size preceding payload data.
875const GVSP_HEADER_SIZE: usize = 8;
876/// IPv4 header size used by GigE Vision streams.
877const IPV4_HEADER_SIZE: usize = 20;
878/// UDP header size used by GigE Vision streams.
879const UDP_HEADER_SIZE: usize = 8;
880/// Bytes in a GVSP data packet that are not image payload.
881const GVSP_PACKET_OVERHEAD: usize = IPV4_HEADER_SIZE + UDP_HEADER_SIZE + GVSP_HEADER_SIZE;
882
883/// The image bytes one GVSP data packet can carry at `packet_size`.
884///
885/// Also the stride reassembly places packets at, which is why
886/// [`SilenceWatch`] reports it: a stream carrying packets that completes no
887/// frame is usually a disagreement about this number.
888fn gvsp_payload_size(packet_size: u32) -> usize {
889    (packet_size as usize).saturating_sub(GVSP_PACKET_OVERHEAD)
890}
891
892/// State for a frame being assembled from GVSP packets.
893///
894/// On Windows the runtime path uses [`WindowsFrameAssembly`] instead, so the
895/// fields only this type's completion step reads (`block_id`, `width`, `height`,
896/// `pixel_format`, `timestamp`) have no reader there — the completion step lives
897/// in `next_frame`, which is `cfg(not(windows))`. The type is still built under
898/// `test` so the reassembly unit tests below run on every platform. Unifying the
899/// two implementations is backlog API-01; the allow goes away with them.
900#[cfg(any(not(windows), test))]
901#[cfg_attr(windows, allow(dead_code))]
902#[derive(Debug)]
903struct FrameAssemblyState {
904    block_id: u64,
905    width: u32,
906    height: u32,
907    pixel_format: PixelFormat,
908    timestamp: u64,
909    expected_packets: Option<usize>,
910    bitmap: Option<PacketBitmap>,
911    received_packet_ids: HashSet<u32>,
912    payload: BytesMut,
913    packet_payload_size: usize,
914    started: Instant,
915}
916
917#[cfg(any(not(windows), test))]
918impl FrameAssemblyState {
919    fn new(
920        block_id: u64,
921        width: u32,
922        height: u32,
923        pixel_format: PixelFormat,
924        timestamp: u64,
925        packet_payload_size: usize,
926    ) -> Self {
927        Self {
928            block_id,
929            width,
930            height,
931            pixel_format,
932            timestamp,
933            expected_packets: None,
934            bitmap: None,
935            received_packet_ids: HashSet::new(),
936            payload: BytesMut::new(),
937            packet_payload_size,
938            started: Instant::now(),
939        }
940    }
941
942    /// Ingest a payload packet. Returns true if this is a new packet.
943    fn ingest(&mut self, packet_id: u32, data: &[u8]) -> bool {
944        // Packet ID 0 is the leader. GVSP payload packet IDs begin at 1.
945        if packet_id == 0 || !self.received_packet_ids.insert(packet_id) {
946            return false;
947        }
948
949        let pid = packet_id as usize;
950
951        // A resent packet can arrive after the trailer established the expected
952        // count, so keep the bitmap in sync in that case.
953        if let Some(ref mut bitmap) = self.bitmap
954            && !bitmap.set(pid.saturating_sub(1))
955        {
956            return false; // Duplicate packet.
957        }
958
959        // Write data at the correct offset for zero-copy reassembly.
960        let offset = pid.saturating_sub(1) * self.packet_payload_size;
961        let required = offset + data.len();
962        if self.payload.len() < required {
963            self.payload.resize(required, 0);
964        }
965        self.payload[offset..offset + data.len()].copy_from_slice(data);
966        true
967    }
968
969    /// Set expected payload packets from the GVSP trailer packet ID.
970    ///
971    /// Packet ID 0 belongs to the leader and the trailer immediately follows
972    /// the last payload packet, so a complete frame contains every payload ID
973    /// in the range `1..trailer_packet_id`.
974    fn set_trailer_packet_id(&mut self, trailer_packet_id: u32) {
975        if self.expected_packets.is_none() {
976            let expected_packets = trailer_packet_id.saturating_sub(1) as usize;
977            let mut bitmap = PacketBitmap::new(expected_packets);
978            for packet_id in &self.received_packet_ids {
979                if *packet_id < trailer_packet_id {
980                    bitmap.set(packet_id.saturating_sub(1) as usize);
981                }
982            }
983            self.expected_packets = Some(expected_packets);
984            self.bitmap = Some(bitmap);
985        }
986    }
987
988    /// Check if all packets have been received.
989    fn is_complete(&self) -> bool {
990        self.bitmap.as_ref().is_some_and(|b| b.is_complete())
991    }
992
993    /// Check if assembly has timed out.
994    fn is_expired(&self, timeout: Duration) -> bool {
995        self.started.elapsed() > timeout
996    }
997
998    /// Get missing packet ranges for resend requests.
999    #[allow(dead_code)]
1000    fn missing_ranges(&self) -> Vec<std::ops::RangeInclusive<u32>> {
1001        self.bitmap
1002            .as_ref()
1003            .map(|b| b.missing_ranges())
1004            .unwrap_or_default()
1005    }
1006}
1007
1008#[cfg(windows)]
1009struct WindowsFrameAssembly {
1010    block_id: u64,
1011    width: u32,
1012    height: u32,
1013    pixel_format: PixelFormat,
1014    timestamp: u64,
1015    next_packet_id: u32,
1016    payload: BytesMut,
1017    started: Instant,
1018}
1019
1020#[cfg(windows)]
1021struct WindowsReceiver {
1022    frames: tokio::sync::mpsc::Receiver<Result<Frame, GenicamError>>,
1023    stop: Arc<AtomicBool>,
1024    join: Option<thread::JoinHandle<()>>,
1025}
1026
1027#[cfg(windows)]
1028fn windows_frame_receiver(
1029    source: PacketSource,
1030    packet_size: u32,
1031    stats: StreamStatsAccumulator,
1032    frame_timeout_ns: Arc<AtomicU64>,
1033) -> WindowsReceiver {
1034    let (tx, rx) = tokio::sync::mpsc::channel(2);
1035    let stop = Arc::new(AtomicBool::new(false));
1036
1037    let PacketSource::Udp(socket) = source;
1038    let socket = match socket.into_std() {
1039        Ok(socket) => socket,
1040        Err(err) => {
1041            let _ = tx.try_send(Err(GenicamError::transport(format!(
1042                "socket conversion failed: {err}"
1043            ))));
1044            return WindowsReceiver {
1045                frames: rx,
1046                stop,
1047                join: None,
1048            };
1049        }
1050    };
1051    if let Err(err) = socket.set_nonblocking(false) {
1052        let _ = tx.try_send(Err(GenicamError::transport(format!(
1053            "set blocking mode failed: {err}"
1054        ))));
1055        return WindowsReceiver {
1056            frames: rx,
1057            stop,
1058            join: None,
1059        };
1060    }
1061    if let Err(err) = socket.set_read_timeout(Some(Duration::from_millis(100))) {
1062        let _ = tx.try_send(Err(GenicamError::transport(format!(
1063            "set stream socket read timeout failed: {err}"
1064        ))));
1065        return WindowsReceiver {
1066            frames: rx,
1067            stop,
1068            join: None,
1069        };
1070    }
1071
1072    let reader_stop = Arc::clone(&stop);
1073    let reader = thread::spawn(move || {
1074        let mut recv_buffer = vec![0u8; (packet_size as usize + 64).max(4096)];
1075        let mut active: Option<WindowsFrameAssembly> = None;
1076        let mut silence = SilenceWatch::new(packet_size);
1077
1078        while !reader_stop.load(Ordering::Acquire) {
1079            let (len, _) = match socket.recv_from(&mut recv_buffer) {
1080                Ok(result) => result,
1081                Err(err)
1082                    if matches!(
1083                        err.kind(),
1084                        ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted
1085                    ) =>
1086                {
1087                    if active.as_ref().is_some_and(|frame| {
1088                        frame.started.elapsed().as_nanos()
1089                            > frame_timeout_ns.load(Ordering::Relaxed) as u128
1090                    }) {
1091                        active = None;
1092                        stats.record_drop();
1093                    }
1094                    silence.tick();
1095                    continue;
1096                }
1097                Err(err) => {
1098                    let _ = tx.try_send(Err(GenicamError::transport(format!(
1099                        "socket receive failed: {err}"
1100                    ))));
1101                    break;
1102                }
1103            };
1104
1105            silence.record_packet();
1106
1107            let packet = match gvsp::parse_packet(&recv_buffer[..len]) {
1108                Ok(packet) => packet,
1109                Err(_) => continue,
1110            };
1111
1112            match packet {
1113                GvspPacket::Leader {
1114                    block_id,
1115                    width,
1116                    height,
1117                    pixel_format,
1118                    timestamp,
1119                    ..
1120                } => {
1121                    if active.take().is_some() {
1122                        stats.record_drop();
1123                    }
1124                    active = Some(WindowsFrameAssembly {
1125                        block_id,
1126                        width,
1127                        height,
1128                        pixel_format: PixelFormat::from_code(pixel_format),
1129                        timestamp,
1130                        next_packet_id: 1,
1131                        payload: BytesMut::new(),
1132                        started: Instant::now(),
1133                    });
1134                }
1135                GvspPacket::Payload {
1136                    block_id,
1137                    packet_id,
1138                    data,
1139                } => {
1140                    let Some(frame) = active.as_mut() else {
1141                        continue;
1142                    };
1143                    if frame.block_id != block_id || frame.next_packet_id != packet_id {
1144                        active = None;
1145                        stats.record_drop();
1146                        continue;
1147                    }
1148                    frame.payload.extend_from_slice(data.as_ref());
1149                    frame.next_packet_id += 1;
1150                    stats.record_packet();
1151                }
1152                GvspPacket::Trailer {
1153                    block_id,
1154                    packet_id,
1155                    status,
1156                    chunk_data,
1157                    ..
1158                } => {
1159                    let Some(mut frame) = active.take() else {
1160                        continue;
1161                    };
1162                    let expected_bytes = frame.width as usize
1163                        * frame.height as usize
1164                        * frame.pixel_format.bytes_per_pixel().unwrap_or(1);
1165                    if frame.block_id != block_id
1166                        || frame.next_packet_id != packet_id
1167                        || status != 0
1168                        || frame.payload.len() < expected_bytes
1169                    {
1170                        stats.record_drop();
1171                        continue;
1172                    }
1173                    frame.payload.truncate(expected_bytes);
1174                    let chunks = if chunk_data.is_empty() {
1175                        None
1176                    } else {
1177                        crate::chunks::parse_chunk_bytes(chunk_data.as_ref()).ok()
1178                    };
1179                    let completed = Frame {
1180                        payload: frame.payload.freeze(),
1181                        width: frame.width,
1182                        height: frame.height,
1183                        pixel_format: frame.pixel_format,
1184                        chunks,
1185                        ts_dev: Some(frame.timestamp),
1186                        ts_host: None,
1187                    };
1188                    stats.record_frame(completed.payload.len(), None);
1189                    silence.record_frame();
1190                    if tx.try_send(Ok(completed)).is_err() {
1191                        stats.record_backpressure_drop();
1192                    }
1193                }
1194            }
1195        }
1196    });
1197
1198    WindowsReceiver {
1199        frames: rx,
1200        stop,
1201        join: Some(reader),
1202    }
1203}
1204
1205/// High-level async iterator over reassembled GVSP frames.
1206///
1207/// Wraps a low-level [`Stream`] and handles packet parsing, reassembly,
1208/// and optional resend requests automatically.
1209///
1210/// # Example
1211///
1212/// ```rust,ignore
1213/// let raw_stream = StreamBuilder::new(&mut device)
1214///     .iface(iface)
1215///     .build()
1216///     .await?;
1217/// let mut frame_stream = FrameStream::new(raw_stream, None);
1218/// while let Some(frame) = frame_stream.next_frame().await? {
1219///     println!("Frame: {}x{}", frame.width, frame.height);
1220/// }
1221/// ```
1222pub struct FrameStream {
1223    #[cfg(not(windows))]
1224    source: PacketSource,
1225    stats: StreamStatsAccumulator,
1226    params: StreamParams,
1227    config: StreamConfig,
1228    #[cfg(not(windows))]
1229    recv_buffer: Vec<u8>,
1230    #[cfg(not(windows))]
1231    active: Option<FrameAssemblyState>,
1232    #[cfg(not(windows))]
1233    silence: SilenceWatch,
1234    frame_timeout: Duration,
1235    #[cfg(windows)]
1236    frame_timeout_ns: Arc<AtomicU64>,
1237    #[cfg(windows)]
1238    frame_rx: tokio::sync::mpsc::Receiver<Result<Frame, GenicamError>>,
1239    #[cfg(windows)]
1240    reader_stop: Arc<AtomicBool>,
1241    #[cfg(windows)]
1242    reader: Option<thread::JoinHandle<()>>,
1243    time_sync: Option<TimeSync>,
1244}
1245
1246impl FrameStream {
1247    /// Create a new frame stream from a configured [`Stream`].
1248    ///
1249    /// Optionally accepts a [`TimeSync`] for mapping device timestamps to host time.
1250    pub fn new(stream: Stream, time_sync: Option<TimeSync>) -> Self {
1251        let (source, stats, params, config) = stream.into_parts();
1252        let frame_timeout = DEFAULT_FRAME_TIMEOUT;
1253        #[cfg(not(windows))]
1254        let buffer_size = (params.packet_size as usize + 64).max(4096);
1255        #[cfg(windows)]
1256        let frame_timeout_ns = Arc::new(AtomicU64::new(
1257            frame_timeout.as_nanos().min(u64::MAX as u128) as u64,
1258        ));
1259        #[cfg(windows)]
1260        let WindowsReceiver {
1261            frames: frame_rx,
1262            stop: reader_stop,
1263            join: reader,
1264        } = windows_frame_receiver(
1265            source,
1266            params.packet_size,
1267            stats.clone(),
1268            Arc::clone(&frame_timeout_ns),
1269        );
1270
1271        Self {
1272            #[cfg(not(windows))]
1273            source,
1274            stats,
1275            params,
1276            config,
1277            #[cfg(not(windows))]
1278            recv_buffer: vec![0u8; buffer_size],
1279            #[cfg(not(windows))]
1280            active: None,
1281            #[cfg(not(windows))]
1282            silence: SilenceWatch::new(params.packet_size),
1283            frame_timeout,
1284            #[cfg(windows)]
1285            frame_timeout_ns,
1286            #[cfg(windows)]
1287            frame_rx,
1288            #[cfg(windows)]
1289            reader_stop,
1290            #[cfg(windows)]
1291            reader,
1292            time_sync,
1293        }
1294    }
1295
1296    /// Set the frame assembly timeout.
1297    ///
1298    /// If a frame is not complete within this duration, it will be dropped
1299    /// and assembly will move on to the next frame.
1300    pub fn set_frame_timeout(&mut self, timeout: Duration) {
1301        self.frame_timeout = timeout;
1302        #[cfg(windows)]
1303        self.frame_timeout_ns.store(
1304            timeout.as_nanos().min(u64::MAX as u128) as u64,
1305            Ordering::Relaxed,
1306        );
1307    }
1308
1309    /// Update or set the time synchronization model for timestamp mapping.
1310    pub fn set_time_sync(&mut self, time_sync: TimeSync) {
1311        self.time_sync = Some(time_sync);
1312    }
1313
1314    /// Obtain a clone of the statistics accumulator handle.
1315    pub fn stats_handle(&self) -> StreamStatsAccumulator {
1316        self.stats.clone()
1317    }
1318
1319    /// Snapshot the collected statistics.
1320    pub fn stats(&self) -> StreamStats {
1321        self.stats.snapshot()
1322    }
1323
1324    /// Access the negotiated stream parameters.
1325    pub fn params(&self) -> StreamParams {
1326        self.params
1327    }
1328
1329    /// Immutable view of the stream configuration.
1330    pub fn config(&self) -> &StreamConfig {
1331        &self.config
1332    }
1333
1334    /// Borrow the underlying UDP socket (returns `None` when using a custom transport).
1335    pub fn socket(&self) -> Option<&UdpSocket> {
1336        #[cfg(not(windows))]
1337        {
1338            self.source.as_udp_socket()
1339        }
1340        #[cfg(windows)]
1341        {
1342            None
1343        }
1344    }
1345
1346    /// Receive the next complete frame.
1347    ///
1348    /// This method handles packet reception, parsing, and reassembly internally.
1349    /// Returns `Ok(Some(frame))` when a complete frame is available, or
1350    /// `Ok(None)` if the stream has ended (socket closed).
1351    pub async fn next_frame(&mut self) -> Result<Option<Frame>, GenicamError> {
1352        #[cfg(windows)]
1353        {
1354            return match self.frame_rx.recv().await {
1355                Some(Ok(mut frame)) => {
1356                    if let Some(timestamp) = frame.ts_dev {
1357                        frame.ts_host = self
1358                            .time_sync
1359                            .as_ref()
1360                            .map(|time_sync| time_sync.to_host_time(timestamp));
1361                    }
1362                    Ok(Some(frame))
1363                }
1364                Some(Err(err)) => Err(err),
1365                None => Ok(None),
1366            };
1367        }
1368
1369        #[cfg(not(windows))]
1370        {
1371            loop {
1372                // Check for timeout on active frame assembly.
1373                if let Some(ref active) = self.active
1374                    && active.is_expired(self.frame_timeout)
1375                {
1376                    let block_id = active.block_id;
1377                    warn!(
1378                        block_id,
1379                        "frame assembly timeout, dropping incomplete frame"
1380                    );
1381                    self.stats.record_drop();
1382                    self.active = None;
1383                }
1384
1385                // Receive next packet. The poll deadline exists so the loop
1386                // keeps turning while nothing arrives: without it neither the
1387                // DX-09 watch below nor the frame-assembly timeout above can
1388                // fire on a stream that goes fully silent, because both are
1389                // only reached when a packet does. The Windows reader thread
1390                // has always had this, from its 100 ms socket read timeout.
1391                let raw = match timeout(SILENCE_POLL, self.source.recv(&mut self.recv_buffer)).await
1392                {
1393                    Ok(Ok(data)) if data.is_empty() => return Ok(None), // Stream closed.
1394                    Ok(Ok(data)) => data,
1395                    Ok(Err(e)) => return Err(e),
1396                    Err(_elapsed) => {
1397                        self.silence.tick();
1398                        continue;
1399                    }
1400                };
1401                self.silence.record_packet();
1402
1403                // Parse GVSP packet.
1404                let packet = match gvsp::parse_packet(&raw) {
1405                    Ok(p) => p,
1406                    Err(e) => {
1407                        trace!(error = %e, "discarding malformed GVSP packet");
1408                        continue;
1409                    }
1410                };
1411
1412                // Process packet based on type.
1413                match packet {
1414                    GvspPacket::Leader {
1415                        block_id,
1416                        width,
1417                        height,
1418                        pixel_format,
1419                        timestamp,
1420                        ..
1421                    } => {
1422                        // Start new frame assembly, dropping any incomplete previous frame.
1423                        if let Some(ref prev) = self.active
1424                            && prev.block_id != block_id
1425                        {
1426                            debug!(
1427                                old_block = prev.block_id,
1428                                new_block = block_id,
1429                                "new leader arrived, dropping incomplete frame"
1430                            );
1431                            self.stats.record_drop();
1432                        }
1433
1434                        let pixel_format = PixelFormat::from_code(pixel_format);
1435                        let packet_payload = gvsp_payload_size(self.params.packet_size);
1436
1437                        self.active = Some(FrameAssemblyState::new(
1438                            block_id,
1439                            width,
1440                            height,
1441                            pixel_format,
1442                            timestamp,
1443                            packet_payload,
1444                        ));
1445                        trace!(block_id, %pixel_format, width, height, "frame leader received");
1446                    }
1447
1448                    GvspPacket::Payload {
1449                        block_id,
1450                        packet_id,
1451                        data,
1452                    } => {
1453                        if let Some(ref mut active) = self.active
1454                            && active.block_id == block_id
1455                            && active.ingest(packet_id, data.as_ref())
1456                        {
1457                            self.stats.record_packet();
1458                        }
1459                    }
1460
1461                    GvspPacket::Trailer {
1462                        block_id,
1463                        packet_id,
1464                        status,
1465                        chunk_data,
1466                        ..
1467                    } => {
1468                        let Some(mut active) = self.active.take() else {
1469                            continue;
1470                        };
1471
1472                        if active.block_id != block_id {
1473                            // Mismatched trailer, drop and continue.
1474                            self.stats.record_drop();
1475                            continue;
1476                        }
1477
1478                        if status != 0 {
1479                            warn!(block_id, status, "trailer reported non-zero status");
1480                            self.stats.record_drop();
1481                            continue;
1482                        }
1483
1484                        active.set_trailer_packet_id(packet_id);
1485                        if !active.is_complete() {
1486                            warn!(
1487                                block_id,
1488                                trailer_packet_id = packet_id,
1489                                "dropping incomplete frame"
1490                            );
1491                            self.stats.record_drop();
1492                            continue;
1493                        }
1494
1495                        // Build the frame.
1496                        let ts_host = self
1497                            .time_sync
1498                            .as_ref()
1499                            .map(|ts| ts.to_host_time(active.timestamp));
1500
1501                        let chunks = if chunk_data.is_empty() {
1502                            None
1503                        } else {
1504                            match crate::chunks::parse_chunk_bytes(&chunk_data) {
1505                                Ok(map) => Some(map),
1506                                Err(e) => {
1507                                    debug!(error = %e, "failed to parse chunk data");
1508                                    None
1509                                }
1510                            }
1511                        };
1512
1513                        // Truncate payload to actual received size.
1514                        // The bitmap tells us what we received; we use the payload as-is.
1515                        let payload = active.payload.freeze();
1516
1517                        let frame = Frame {
1518                            payload,
1519                            width: active.width,
1520                            height: active.height,
1521                            pixel_format: active.pixel_format,
1522                            chunks,
1523                            ts_dev: Some(active.timestamp),
1524                            ts_host,
1525                        };
1526
1527                        // FrameStream is the sole owner of completed-frame
1528                        // accounting. Consumers may snapshot this accumulator
1529                        // through stats_handle(), but must not record the same
1530                        // frame again.
1531                        let latency = frame
1532                            .host_time()
1533                            .and_then(|ts| SystemTime::now().duration_since(ts).ok());
1534                        self.stats.record_frame(frame.payload.len(), latency);
1535                        self.silence.record_frame();
1536
1537                        debug!(
1538                            block_id,
1539                            width = frame.width,
1540                            height = frame.height,
1541                            bytes = frame.payload.len(),
1542                            "frame complete"
1543                        );
1544
1545                        return Ok(Some(frame));
1546                    }
1547                }
1548            }
1549        }
1550    }
1551}
1552
1553#[cfg(windows)]
1554impl Drop for FrameStream {
1555    fn drop(&mut self) {
1556        self.reader_stop.store(true, Ordering::Release);
1557        if let Some(reader) = self.reader.take() {
1558            let _ = reader.join();
1559        }
1560    }
1561}
1562
1563// ============================================================================
1564// USB3 Vision Frame Stream
1565// ============================================================================
1566
1567/// Async frame iterator wrapping blocking USB3 Vision bulk reads.
1568///
1569/// Internally spawns a blocking reader thread that calls
1570/// `U3vStream::next_frame()` in a loop and sends converted [`Frame`]
1571/// values through an mpsc channel. The async consumer reads from the
1572/// channel via [`next_frame()`](U3vFrameStream::next_frame).
1573///
1574/// # Example
1575///
1576/// ```rust,ignore
1577/// let u3v_stream = device.open_stream(payload_size)?;
1578/// let mut frames = U3vFrameStream::start(u3v_stream);
1579/// while let Some(frame) = frames.next_frame().await? {
1580///     println!("{}x{} frame", frame.width, frame.height);
1581/// }
1582/// frames.stop();
1583/// ```
1584#[cfg(feature = "u3v")]
1585#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
1586pub struct U3vFrameStream {
1587    rx: tokio::sync::mpsc::Receiver<Result<Frame, GenicamError>>,
1588    stop_tx: tokio::sync::watch::Sender<bool>,
1589    _reader: tokio::task::JoinHandle<()>,
1590}
1591
1592#[cfg(feature = "u3v")]
1593impl U3vFrameStream {
1594    /// Start the frame stream from a configured [`U3vStream`].
1595    ///
1596    /// The reader thread runs until [`stop()`](Self::stop) is called,
1597    /// the [`U3vStream`] errors, or the `U3vFrameStream` is dropped.
1598    ///
1599    /// [`U3vStream`]: crate::u3v::stream::U3vStream
1600    pub fn start<T: crate::u3v::usb::UsbTransfer + 'static>(
1601        mut stream: crate::u3v::stream::U3vStream<T>,
1602    ) -> Self {
1603        let (tx, rx) = tokio::sync::mpsc::channel(4);
1604        let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
1605
1606        let reader = tokio::task::spawn_blocking(move || {
1607            loop {
1608                if *stop_rx.borrow() {
1609                    break;
1610                }
1611                match stream.next_frame() {
1612                    Ok(raw) => {
1613                        let pixel_format = PixelFormat::from_code(raw.leader.pixel_format);
1614                        let frame = Frame {
1615                            payload: raw.payload,
1616                            width: raw.leader.width,
1617                            height: raw.leader.height,
1618                            pixel_format,
1619                            chunks: None,
1620                            ts_dev: Some(raw.leader.timestamp),
1621                            ts_host: None,
1622                        };
1623                        if tx.blocking_send(Ok(frame)).is_err() {
1624                            break; // Receiver dropped.
1625                        }
1626                    }
1627                    Err(e) => {
1628                        let _ = tx.blocking_send(Err(GenicamError::transport(e.to_string())));
1629                        break;
1630                    }
1631                }
1632            }
1633        });
1634
1635        Self {
1636            rx,
1637            stop_tx,
1638            _reader: reader,
1639        }
1640    }
1641
1642    /// Receive the next complete frame.
1643    ///
1644    /// Returns `Ok(None)` when the stream ends (reader stopped or errored).
1645    pub async fn next_frame(&mut self) -> Result<Option<Frame>, GenicamError> {
1646        match self.rx.recv().await {
1647            Some(Ok(frame)) => Ok(Some(frame)),
1648            Some(Err(e)) => Err(e),
1649            None => Ok(None),
1650        }
1651    }
1652
1653    /// Signal the reader thread to stop.
1654    pub fn stop(&self) {
1655        let _ = self.stop_tx.send(true);
1656    }
1657}
1658
1659// ============================================================================
1660// USB3 Vision Stream Builder
1661// ============================================================================
1662
1663/// Builder for configuring and starting a U3V frame stream.
1664///
1665/// Mirrors the GigE [`StreamBuilder`] pattern but for USB3 Vision.
1666/// Reads image dimensions from the camera features (or accepts explicit
1667/// overrides) and opens the underlying USB bulk stream.
1668///
1669/// # Example
1670///
1671/// ```rust,ignore
1672/// let mut camera = open_u3v_device(device)?;
1673/// let frames = U3vStreamBuilder::new(&mut camera)
1674///     .build()?;
1675/// ```
1676#[cfg(feature = "u3v")]
1677#[cfg_attr(docsrs, doc(cfg(feature = "u3v")))]
1678pub struct U3vStreamBuilder<'a, T: crate::u3v::usb::UsbTransfer + 'static> {
1679    camera: &'a mut crate::Camera<crate::U3vRegisterIo<T>>,
1680    payload_size: Option<u64>,
1681}
1682
1683#[cfg(feature = "u3v")]
1684impl<'a, T: crate::u3v::usb::UsbTransfer + 'static> U3vStreamBuilder<'a, T> {
1685    /// Create a new builder bound to a camera.
1686    pub fn new(camera: &'a mut crate::Camera<crate::U3vRegisterIo<T>>) -> Self {
1687        Self {
1688            camera,
1689            payload_size: None,
1690        }
1691    }
1692
1693    /// Override the payload size (bytes per frame).
1694    ///
1695    /// When not set, the builder computes it from Width, Height, and
1696    /// PixelFormat camera features.
1697    pub fn payload_size(mut self, size: u64) -> Self {
1698        self.payload_size = Some(size);
1699        self
1700    }
1701
1702    /// Finalise the builder: configure SIRM, start streaming, return
1703    /// an async [`U3vFrameStream`].
1704    pub fn build(self) -> Result<U3vFrameStream, GenicamError> {
1705        let payload_size = match self.payload_size {
1706            Some(s) => s,
1707            None => {
1708                let width: u64 = self
1709                    .camera
1710                    .get("Width")?
1711                    .parse()
1712                    .map_err(|e| GenicamError::parse(format!("Width: {e}")))?;
1713                let height: u64 = self
1714                    .camera
1715                    .get("Height")?
1716                    .parse()
1717                    .map_err(|e| GenicamError::parse(format!("Height: {e}")))?;
1718                let pf_str = self.camera.get("PixelFormat")?;
1719                let bpp = PixelFormat::from_name(&pf_str)
1720                    .bytes_per_pixel()
1721                    .unwrap_or(1) as u64;
1722                width * height * bpp
1723            }
1724        };
1725
1726        let mut device = self.camera.transport().lock_device()?;
1727        let stream = device
1728            .open_stream(payload_size)
1729            .map_err(|e| GenicamError::transport(e.to_string()))?;
1730
1731        Ok(U3vFrameStream::start(stream))
1732    }
1733}
1734
1735#[cfg(test)]
1736mod tests {
1737    use super::*;
1738
1739    #[test]
1740    fn frame_assembly_state_ingest_tracks_packets() {
1741        let mut state = FrameAssemblyState::new(1, 640, 480, PixelFormat::Mono8, 0, 1400);
1742
1743        // Ingest packets (packet_id 1 and 2 are payload, 0 is leader).
1744        assert!(state.ingest(1, &[1, 2, 3]));
1745        assert!(state.ingest(2, &[4, 5, 6]));
1746
1747        // Duplicate should return false.
1748        assert!(!state.ingest(1, &[1, 2, 3]));
1749
1750        state.set_trailer_packet_id(3);
1751        assert!(state.is_complete());
1752    }
1753
1754    #[test]
1755    fn gvsp_payload_size_excludes_ip_udp_and_gvsp_headers() {
1756        assert_eq!(gvsp_payload_size(1458), 1422);
1757        assert_eq!(gvsp_payload_size(9000), 8964);
1758    }
1759
1760    #[test]
1761    fn silence_watch_stays_quiet_inside_the_grace_period() {
1762        let mut watch = SilenceWatch::new(1500);
1763        assert_eq!(
1764            watch.assess(SILENCE_GRACE - Duration::from_millis(1)),
1765            Silence::Quiet
1766        );
1767        // Still unspoken, so the verdict is available once the grace expires.
1768        assert_eq!(watch.assess(SILENCE_GRACE), Silence::NoPacket);
1769    }
1770
1771    #[test]
1772    fn silence_watch_distinguishes_no_packet_from_no_frame() {
1773        let mut nothing = SilenceWatch::new(1500);
1774        assert_eq!(nothing.assess(SILENCE_GRACE), Silence::NoPacket);
1775
1776        // The distinction is the whole point of DX-09: nothing arriving is a
1777        // path or privilege problem, while packets arriving with no frame
1778        // completing is a packet-size disagreement (SR-02).
1779        let mut packets = SilenceWatch::new(1500);
1780        packets.record_packet();
1781        assert_eq!(packets.assess(SILENCE_GRACE), Silence::NoFrame);
1782    }
1783
1784    #[test]
1785    fn silence_watch_speaks_once_and_never_after_a_frame() {
1786        let mut watch = SilenceWatch::new(1500);
1787        assert_eq!(watch.assess(SILENCE_GRACE), Silence::NoPacket);
1788        // A stream that stays silent must not warn on every poll.
1789        assert_eq!(watch.assess(SILENCE_GRACE * 10), Silence::Quiet);
1790
1791        let mut healthy = SilenceWatch::new(1500);
1792        healthy.record_packet();
1793        healthy.record_frame();
1794        assert_eq!(healthy.assess(SILENCE_GRACE * 10), Silence::Quiet);
1795    }
1796
1797    #[test]
1798    fn frame_assembly_state_rejects_missing_payload_packets() {
1799        let mut state = FrameAssemblyState::new(1, 640, 480, PixelFormat::Mono8, 0, 1400);
1800        assert!(state.ingest(1, &[1, 2, 3]));
1801
1802        state.set_trailer_packet_id(3);
1803
1804        assert!(!state.is_complete());
1805    }
1806
1807    #[test]
1808    fn frame_assembly_state_accepts_out_of_order_payload_packets() {
1809        let mut state = FrameAssemblyState::new(1, 640, 480, PixelFormat::Mono8, 0, 1400);
1810        assert!(state.ingest(2, &[4, 5, 6]));
1811        assert!(state.ingest(1, &[1, 2, 3]));
1812
1813        state.set_trailer_packet_id(3);
1814
1815        assert!(state.is_complete());
1816    }
1817
1818    #[test]
1819    fn frame_assembly_state_timeout() {
1820        let state = FrameAssemblyState::new(1, 640, 480, PixelFormat::Mono8, 0, 1400);
1821        assert!(!state.is_expired(Duration::from_secs(10)));
1822        assert!(state.is_expired(Duration::ZERO));
1823    }
1824}