1use std::collections::HashMap;
4use std::io::Cursor;
5use std::net::{IpAddr, Ipv4Addr, SocketAddr};
6use std::time::Duration;
7
8use bytes::{Buf, BufMut, Bytes, BytesMut};
9use fastrand::Rng;
10use if_addrs::{IfAddr, get_if_addrs};
11use thiserror::Error;
12use tokio::net::UdpSocket;
13use tokio::task::JoinSet;
14use tokio::time;
15use tracing::{debug, info, trace, warn};
16use viva_gencp::{AckHeader, CommandFlags, GenCpAck, OpCode, StatusCode, decode_ack};
17
18use crate::nic::{self, Iface};
19
20pub mod consts {
22 use std::time::Duration;
23
24 pub const PORT: u16 = 3956;
26 pub const DISCOVERY_COMMAND: u16 = 0x0002;
28 pub const DISCOVERY_ACK: u16 = 0x0003;
30 pub const FORCEIP_COMMAND: u16 = 0x0004;
32 pub const FORCEIP_ACK: u16 = 0x0005;
34 pub const PACKET_RESEND_COMMAND: u16 = 0x0040;
36 pub const PACKET_RESEND_ACK: u16 = 0x0041;
38 pub const PENDING_ACK: u16 = 0x0089;
45
46 pub const EVENT_COMMAND: u16 = 0x00C0;
59 pub const EVENT_ACK: u16 = 0x00C1;
61 pub const EVENTDATA_COMMAND: u16 = 0x00C2;
63 pub const EVENTDATA_ACK: u16 = 0x00C3;
65 pub const ACTION_COMMAND: u16 = 0x0100;
67 pub const ACTION_ACK: u16 = 0x0101;
69
70 pub const EVENT_ENTRY: usize = 16;
72 pub const EVENT_ENTRY_EXTENDED: usize = 24;
77
78 pub const CURRENT_IP_CONFIG: u64 = 0x0014;
82
83 pub const PERSISTENT_IP_ADDRESS: u64 = 0x064C;
85 pub const PERSISTENT_SUBNET_MASK: u64 = 0x065C;
87 pub const PERSISTENT_DEFAULT_GATEWAY: u64 = 0x066C;
89
90 pub const CONTROL_CHANNEL_PRIVILEGE: u64 = 0x0a00;
95 pub const CCP_CONTROL: u32 = 1 << 1;
97 pub const CCP_EXCLUSIVE: u32 = 1 << 0;
99 pub const CCP_CONTROLLER_BITS: u32 = CCP_CONTROL | CCP_EXCLUSIVE;
101
102 pub const HEARTBEAT_TIMEOUT: u64 = 0x0938;
109
110 pub const NUMBER_OF_MESSAGE_CHANNELS: u64 = 0x0000_0900;
124 pub const MESSAGE_DESTINATION_PORT: u64 = 0x0000_0B00;
128 pub const MESSAGE_DESTINATION_ADDRESS: u64 = 0x0000_0B10;
130 pub const MESSAGE_CHANNEL_TIMEOUT: u64 = 0x0000_0B14;
132 pub const MESSAGE_CHANNEL_RETRY_COUNT: u64 = 0x0000_0B18;
134
135 pub const GENCP_MAX_BLOCK: usize = 512;
137 pub const GENCP_WRITE_OVERHEAD: usize = 8;
139
140 pub const CONTROL_TIMEOUT: Duration = Duration::from_millis(500);
142 pub const MAX_RETRIES: usize = 4;
144 pub const MAX_PENDING_ACKS: usize = 100;
149 pub const MAX_PENDING_ACK_WAIT: Duration = Duration::from_secs(10);
155 pub const RETRY_BASE_DELAY: Duration = Duration::from_millis(20);
157 pub const RETRY_JITTER: Duration = Duration::from_millis(10);
159
160 pub const DISCOVERY_BUFFER: usize = 2048;
162
163 pub const STREAM_CHANNEL_BASE: u64 = 0x0d00;
170 pub const STREAM_CHANNEL_STRIDE: u64 = 0x40;
172 pub const STREAM_DESTINATION_PORT: u64 = 0x00;
174 pub const STREAM_PACKET_SIZE: u64 = 0x04;
176 pub const STREAM_PACKET_DELAY: u64 = 0x08;
178 pub const STREAM_DESTINATION_ADDRESS: u64 = 0x18;
180}
181
182pub use consts::PORT as GVCP_PORT;
184
185pub const STREAM_PACKET_SIZE_MASK: u32 = 0xFFFF;
192
193pub const SCPS_FIRE_TEST_PACKET: u32 = 0x8000_0000;
200
201pub const SCPS_DO_NOT_FRAGMENT: u32 = 0x4000_0000;
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct GvcpRequestHeader {
212 pub flags: CommandFlags,
214 pub command: u16,
216 pub length: u16,
218 pub request_id: u16,
220}
221
222const GVCP_CMD_KEY: u8 = 0x42;
224
225impl GvcpRequestHeader {
226 pub fn encode(self, payload: &[u8]) -> Bytes {
231 let mut buf = BytesMut::with_capacity(viva_gencp::HEADER_SIZE + payload.len());
232 buf.put_u8(GVCP_CMD_KEY);
234 buf.put_u8(self.gvcp_flags_byte());
235 buf.put_u16(self.command);
236 buf.put_u16(self.length);
237 buf.put_u16(self.request_id);
238 buf.extend_from_slice(payload);
239 buf.freeze()
240 }
241
242 fn gvcp_flags_byte(&self) -> u8 {
250 let mut byte = 0u8;
251 if self.flags.contains(CommandFlags::ACK_REQUIRED) {
252 byte |= 0x01;
253 }
254 if self.flags.contains(CommandFlags::BROADCAST) {
255 byte |= 0x10;
256 }
257 if self.flags.contains(CommandFlags::SCHEDULED_ACTION) {
258 byte |= 0x80;
259 }
260 byte
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct GvcpAckHeader {
267 pub status: StatusCode,
269 pub command: u16,
271 pub length: u16,
273 pub request_id: u16,
275}
276
277impl From<AckHeader> for GvcpAckHeader {
278 fn from(value: AckHeader) -> Self {
279 Self {
280 status: value.status,
281 command: value.opcode.ack_code(),
282 length: value.length,
283 request_id: value.request_id,
284 }
285 }
286}
287
288#[derive(Debug, Error)]
290pub enum GigeError {
291 #[error("io: {0}")]
292 Io(#[from] std::io::Error),
293 #[error("protocol: {0}")]
294 Protocol(String),
295 #[error("timeout waiting for acknowledgement")]
296 Timeout,
297 #[error("GenCP: {0}")]
298 GenCp(#[from] viva_gencp::GenCpError),
299 #[error("device reported status {0}")]
300 Status(StatusCode),
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct DeviceInfo {
306 pub ip: Ipv4Addr,
307 pub mac: [u8; 6],
308 pub model: Option<String>,
309 pub manufacturer: Option<String>,
310 pub version: Option<String>,
312 pub serial: Option<String>,
314 pub user_name: Option<String>,
316}
317
318impl DeviceInfo {
319 pub fn from_ip(ip: Ipv4Addr) -> Self {
324 Self {
325 ip,
326 mac: [0; 6],
327 model: None,
328 manufacturer: None,
329 version: None,
330 serial: None,
331 user_name: None,
332 }
333 }
334
335 pub fn mac_string(&self) -> String {
337 self.mac
338 .iter()
339 .map(|byte| format!("{byte:02X}"))
340 .collect::<Vec<_>>()
341 .join(":")
342 }
343}
344
345pub async fn discover(timeout: Duration) -> Result<Vec<DeviceInfo>, GigeError> {
347 discover_impl(timeout, None, false).await
348}
349
350pub async fn discover_on_interface(
357 timeout: Duration,
358 interface: &str,
359) -> Result<Vec<DeviceInfo>, GigeError> {
360 discover_impl(timeout, Some(interface), true).await
361}
362
363pub async fn discover_all(timeout: Duration) -> Result<Vec<DeviceInfo>, GigeError> {
368 discover_impl(timeout, None, true).await
369}
370
371pub async fn force_ip(
390 mac: [u8; 6],
391 ip: Ipv4Addr,
392 subnet: Ipv4Addr,
393 gateway: Ipv4Addr,
394 iface: Option<&Iface>,
395) -> Result<(), GigeError> {
396 let payload = encode_forceip_payload(mac, ip, subnet, gateway);
398
399 let local_ip = match iface {
400 Some(iface) => iface
401 .ipv4()
402 .ok_or_else(|| GigeError::Protocol("interface lacks IPv4 address".into()))?,
403 None => Ipv4Addr::UNSPECIFIED,
404 };
405
406 let socket = UdpSocket::bind(SocketAddr::new(IpAddr::V4(local_ip), 0)).await?;
407 socket.set_broadcast(true)?;
408 let dest = SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), consts::PORT);
409
410 let header = GvcpRequestHeader {
411 flags: CommandFlags::ACK_REQUIRED | CommandFlags::BROADCAST,
412 command: consts::FORCEIP_COMMAND,
413 length: payload.len() as u16,
414 request_id: 1,
415 };
416 let packet = header.encode(&payload);
417 info!(mac = ?mac, %ip, %subnet, %gateway, "sending FORCEIP command");
418 socket.send_to(&packet, dest).await?;
419
420 let mut buf = vec![0u8; consts::DISCOVERY_BUFFER];
422 match time::timeout(consts::CONTROL_TIMEOUT, socket.recv_from(&mut buf)).await {
423 Ok(Ok((len, _src))) => {
424 if len < viva_gencp::HEADER_SIZE {
425 return Err(GigeError::Protocol("FORCEIP ack too short".into()));
426 }
427 let mut cursor = &buf[..];
428 let status = cursor.get_u16();
429 let command = cursor.get_u16();
430 if command != consts::FORCEIP_ACK {
431 return Err(GigeError::Protocol(format!(
432 "unexpected FORCEIP ack opcode {command:#06x}"
433 )));
434 }
435 if status != 0 {
436 return Err(GigeError::Protocol(format!(
437 "FORCEIP returned status {status:#06x}"
438 )));
439 }
440 info!(%ip, "FORCEIP accepted");
441 Ok(())
442 }
443 Ok(Err(e)) => Err(e.into()),
444 Err(_) => Err(GigeError::Timeout),
445 }
446}
447
448fn encode_forceip_payload(
450 mac: [u8; 6],
451 ip: Ipv4Addr,
452 subnet: Ipv4Addr,
453 gateway: Ipv4Addr,
454) -> Vec<u8> {
455 let mut buf = vec![0u8; 56];
456 buf[2..8].copy_from_slice(&mac);
459 buf[20..24].copy_from_slice(&ip.octets());
462 buf[36..40].copy_from_slice(&subnet.octets());
465 buf[52..56].copy_from_slice(&gateway.octets());
468 buf
469}
470
471async fn discover_impl(
472 timeout: Duration,
473 iface_filter: Option<&str>,
474 include_loopback: bool,
475) -> Result<Vec<DeviceInfo>, GigeError> {
476 let mut interfaces = Vec::new();
477 for iface in get_if_addrs()? {
478 let IfAddr::V4(v4) = iface.addr else {
479 continue;
480 };
481 if !include_loopback && v4.ip.is_loopback() {
482 continue;
483 }
484 if let Some(filter) = iface_filter
485 && iface.name != filter
486 {
487 continue;
488 }
489 interfaces.push((iface.name, v4));
490 }
491
492 if interfaces.is_empty() {
493 return Ok(Vec::new());
494 }
495
496 let mut join_set = JoinSet::new();
497 for (idx, (name, v4)) in interfaces.into_iter().enumerate() {
498 let request_id = 0x0100u16.wrapping_add(idx as u16);
499 let interface_name = name.clone();
500 join_set.spawn(async move {
501 let local_addr = SocketAddr::new(IpAddr::V4(v4.ip), 0);
502 let socket = match UdpSocket::bind(local_addr).await {
503 Ok(socket) => socket,
504 Err(err) => {
505 warn!(%interface_name, local = %v4.ip, error = %err,
506 "skipping interface: bind failed");
507 return Vec::new();
508 }
509 };
510 let destination = if v4.ip.is_loopback() {
513 SocketAddr::new(IpAddr::V4(v4.ip), consts::PORT)
514 } else {
515 if let Err(err) = socket.set_broadcast(true) {
516 warn!(%interface_name, local = %v4.ip, error = %err,
517 "skipping interface: SO_BROADCAST failed");
518 return Vec::new();
519 }
520 let broadcast = directed_broadcast(v4.ip, v4.netmask);
521 SocketAddr::new(IpAddr::V4(broadcast), consts::PORT)
522 };
523
524 let header = GvcpRequestHeader {
525 flags: CommandFlags::ACK_REQUIRED | CommandFlags::BROADCAST,
526 command: consts::DISCOVERY_COMMAND,
527 length: 0,
528 request_id,
529 };
530 let packet = header.encode(&[]);
531 info!(%interface_name, local = %v4.ip, dest = %destination, "sending GVCP discovery");
532 trace!(%interface_name, bytes = packet.len(), "GVCP discovery payload size");
533 if let Err(err) = socket.send_to(&packet, destination).await {
534 warn!(%interface_name, dest = %destination, error = %err,
535 "skipping interface: discovery send failed");
536 return Vec::new();
537 }
538
539 let mut responses = Vec::new();
540 let mut buffer = vec![0u8; consts::DISCOVERY_BUFFER];
541 let timer = time::sleep(timeout);
542 tokio::pin!(timer);
543 loop {
544 tokio::select! {
545 _ = &mut timer => break,
546 recv = socket.recv_from(&mut buffer) => {
547 let (len, src) = match recv {
552 Ok(v) => v,
553 Err(err) => {
554 debug!(%interface_name, error = %err,
555 "discovery receive failed; keeping results so far");
556 break;
557 }
558 };
559 info!(%interface_name, %src, "received GVCP response");
560 trace!(%interface_name, bytes = len, "GVCP response length");
561 if let Some(info) = parse_discovery_ack(&buffer[..len], request_id) {
562 trace!(ip = %info.ip, mac = %info.mac_string(), "parsed discovery ack");
563 responses.push(info);
564 }
565 }
566 }
567 }
568 responses
569 });
570 }
571
572 let mut seen = HashMap::new();
576 while let Some(res) = join_set.join_next().await {
577 match res {
578 Ok(devices) => {
579 for dev in devices {
580 seen.entry((dev.ip, dev.mac)).or_insert(dev);
581 }
582 }
583 Err(err) => warn!(error = %err, "discovery task failed"),
584 }
585 }
586
587 let mut devices: Vec<_> = seen.into_values().collect();
588 devices.sort_by_key(|d| d.ip);
589 Ok(devices)
590}
591
592fn directed_broadcast(ip: Ipv4Addr, netmask: Ipv4Addr) -> Ipv4Addr {
601 Ipv4Addr::from(u32::from(ip) | !u32::from(netmask))
602}
603
604fn parse_discovery_ack(buf: &[u8], expected_request: u16) -> Option<DeviceInfo> {
611 if buf.len() < viva_gencp::HEADER_SIZE {
612 trace!(len = buf.len(), "ignoring short GVCP datagram");
613 return None;
614 }
615 let mut header = buf;
616 let status = header.get_u16();
617 let command = header.get_u16();
618 let length = header.get_u16() as usize;
619 let request_id = header.get_u16();
620 if request_id != expected_request {
621 return None;
622 }
623 if command != consts::DISCOVERY_ACK {
624 debug!(
625 opcode = format_args!("{command:#06x}"),
626 "ignoring non-discovery GVCP ack"
627 );
628 return None;
629 }
630 if status != 0 {
631 debug!(
632 status = format_args!("{status:#06x}"),
633 "discovery ack reported a non-zero status"
634 );
635 return None;
636 }
637 if buf.len() < viva_gencp::HEADER_SIZE + length {
638 debug!(
639 declared = length,
640 actual = buf.len() - viva_gencp::HEADER_SIZE,
641 "ignoring truncated discovery payload"
642 );
643 return None;
644 }
645 let payload = &buf[viva_gencp::HEADER_SIZE..viva_gencp::HEADER_SIZE + length];
646 match parse_discovery_payload(payload) {
647 Ok(info) => Some(info),
648 Err(err) => {
649 debug!(error = %err, "ignoring unparsable discovery payload");
650 None
651 }
652 }
653}
654
655fn parse_discovery_payload(payload: &[u8]) -> Result<DeviceInfo, GigeError> {
687 if payload.len() < 40 {
691 return Err(GigeError::Protocol("discovery payload too small".into()));
692 }
693 let mut cursor = Cursor::new(payload);
694 let _spec_major = cursor.get_u16(); let _spec_minor = cursor.get_u16(); let _device_mode = cursor.get_u32(); cursor.advance(2); let mut mac = [0u8; 6];
703 cursor.copy_to_slice(&mut mac); let _supported_ip_config = cursor.get_u32(); let _current_ip_config = cursor.get_u32(); cursor.advance(12); let ip = Ipv4Addr::from(cursor.get_u32()); skip(&mut cursor, 12 + 4); skip(&mut cursor, 12 + 4); let manufacturer = read_fixed_string(&mut cursor, 32); let model = read_fixed_string(&mut cursor, 32); let version = read_fixed_string(&mut cursor, 32); skip(&mut cursor, 48); let serial = read_fixed_string(&mut cursor, 16); let user_name = read_fixed_string(&mut cursor, 16); Ok(DeviceInfo {
727 ip,
728 mac,
729 manufacturer,
730 model,
731 version,
732 serial,
733 user_name,
734 })
735}
736
737fn read_fixed_string(cursor: &mut Cursor<&[u8]>, len: usize) -> Option<String> {
739 if cursor.remaining() < len {
740 return None;
741 }
742 let mut buf = vec![0u8; len];
743 cursor.copy_to_slice(&mut buf);
744 parse_string(&buf)
745}
746
747fn skip(cursor: &mut Cursor<&[u8]>, len: usize) {
749 let n = len.min(cursor.remaining());
750 cursor.advance(n);
751}
752
753fn parse_string(bytes: &[u8]) -> Option<String> {
754 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
755 let slice = &bytes[..end];
756 let s = String::from_utf8_lossy(slice).trim().to_string();
757 if s.is_empty() { None } else { Some(s) }
758}
759
760enum AckRecv {
766 Received(usize),
767 Io(std::io::Error),
768 TimedOut,
769}
770
771fn parse_pending_ack(buf: &[u8]) -> Option<(u16, Duration)> {
789 if buf.len() < viva_gencp::HEADER_SIZE {
790 return None;
791 }
792 if u16::from_be_bytes([buf[2], buf[3]]) != consts::PENDING_ACK {
793 return None;
794 }
795 let request_id = u16::from_be_bytes([buf[6], buf[7]]);
796 let payload = &buf[viva_gencp::HEADER_SIZE..];
797 let millis = match payload {
800 [_, _, hi, lo, ..] => u16::from_be_bytes([*hi, *lo]),
801 _ => return Some((request_id, consts::CONTROL_TIMEOUT)),
802 };
803 Some((request_id, Duration::from_millis(u64::from(millis))))
804}
805
806pub struct GigeDevice {
808 socket: UdpSocket,
809 remote: SocketAddr,
810 request_id: u16,
811 rng: Rng,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
816pub struct StreamParams {
817 pub packet_size: u32,
819 pub packet_delay: u32,
821 pub mtu: u32,
823 pub host: Ipv4Addr,
825 pub port: u16,
827}
828
829impl GigeDevice {
830 pub async fn open(addr: SocketAddr) -> Result<Self, GigeError> {
836 let local_ip = match addr.ip() {
837 IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
838 IpAddr::V6(_) => {
839 return Err(GigeError::Protocol("IPv6 GVCP is not supported".into()));
840 }
841 };
842 let socket = UdpSocket::bind(SocketAddr::new(local_ip, 0)).await?;
843 socket.connect(addr).await?;
844 Ok(Self {
845 socket,
846 remote: addr,
847 request_id: 1,
848 rng: Rng::new(),
849 })
850 }
851
852 pub async fn claim_control(&mut self) -> Result<(), GigeError> {
857 self.write_register(
858 consts::CONTROL_CHANNEL_PRIVILEGE as u32,
859 consts::CCP_CONTROL,
860 )
861 .await?;
862 debug!(addr = %self.remote, "claimed control channel privilege");
863 Ok(())
864 }
865
866 pub async fn release_control(&mut self) -> Result<(), GigeError> {
868 self.write_register(consts::CONTROL_CHANNEL_PRIVILEGE as u32, 0)
869 .await
870 }
871
872 pub async fn heartbeat_timeout_ms(&mut self) -> Result<u32, GigeError> {
877 self.read_register(consts::HEARTBEAT_TIMEOUT as u32).await
878 }
879
880 pub async fn ping_control_channel(&mut self) -> Result<bool, GigeError> {
894 let privilege = self
895 .read_register(consts::CONTROL_CHANNEL_PRIVILEGE as u32)
896 .await?;
897 Ok(privilege & consts::CCP_CONTROLLER_BITS != 0)
898 }
899
900 pub fn remote_addr(&self) -> SocketAddr {
902 self.remote
903 }
904
905 fn next_request_id(&mut self) -> u16 {
906 let id = self.request_id;
907 self.request_id = self.request_id.wrapping_add(1);
908 if self.request_id == 0 {
909 self.request_id = 1;
910 }
911 id
912 }
913
914 async fn transact_with_retry(
915 &mut self,
916 opcode: OpCode,
917 payload: BytesMut,
918 ) -> Result<GenCpAck, GigeError> {
919 let request_id = self.next_request_id();
924 let payload_bytes = payload.freeze();
925 let header = GvcpRequestHeader {
926 flags: CommandFlags::ACK_REQUIRED,
927 command: opcode.command_code(),
928 length: payload_bytes.len() as u16,
929 request_id,
930 };
931 let encoded = header.encode(&payload_bytes);
932 let mut attempt = 0usize;
933 'retry: loop {
934 attempt += 1;
935 trace!(request_id, opcode = ?opcode, bytes = encoded.len(), attempt, "sending GVCP command");
936 if let Err(err) = self.socket.send(&encoded).await {
937 if attempt >= consts::MAX_RETRIES {
938 return Err(err.into());
939 }
940 warn!(request_id, ?opcode, attempt, "send failed, retrying");
941 self.backoff(attempt).await;
942 continue 'retry;
943 }
944
945 let mut buf = vec![
946 0u8;
947 viva_gencp::HEADER_SIZE
948 + consts::GENCP_MAX_BLOCK
949 + consts::GENCP_WRITE_OVERHEAD
950 ];
951 let mut mismatched_acks = 0usize;
952 loop {
953 match self.recv_absorbing_pending(&mut buf, request_id).await {
954 AckRecv::Received(len) => {
955 trace!(request_id, bytes = len, attempt, "received GenCP ack");
956 let ack = decode_ack(&buf[..len])?;
957 if ack.header.request_id != request_id {
958 debug!(
959 request_id,
960 got = ack.header.request_id,
961 attempt,
962 "acknowledgement id mismatch"
963 );
964 mismatched_acks += 1;
970 if mismatched_acks < consts::MAX_RETRIES {
971 continue;
972 }
973 if attempt >= consts::MAX_RETRIES {
974 return Err(GigeError::Protocol(
975 "acknowledgement id mismatch".into(),
976 ));
977 }
978 self.backoff(attempt).await;
979 continue 'retry;
980 }
981 if ack.header.opcode != opcode {
982 return Err(GigeError::Protocol(
983 "unexpected opcode in acknowledgement".into(),
984 ));
985 }
986 match ack.header.status {
987 StatusCode::Success => return Ok(ack),
988 status if status.is_retryable() && attempt < consts::MAX_RETRIES => {
995 warn!(request_id, attempt, %status, "device busy, retrying");
996 self.backoff(attempt).await;
997 continue 'retry;
998 }
999 other => return Err(GigeError::Status(other)),
1000 }
1001 }
1002 AckRecv::Io(err) => {
1003 if attempt >= consts::MAX_RETRIES {
1004 return Err(err.into());
1005 }
1006 warn!(request_id, ?opcode, attempt, "receive error, retrying");
1007 self.backoff(attempt).await;
1008 continue 'retry;
1009 }
1010 AckRecv::TimedOut => {
1011 if attempt >= consts::MAX_RETRIES {
1012 return Err(GigeError::Timeout);
1013 }
1014 warn!(request_id, ?opcode, attempt, "command timeout, retrying");
1015 self.backoff(attempt).await;
1016 continue 'retry;
1017 }
1018 }
1019 }
1020 }
1021 }
1022
1023 async fn recv_absorbing_pending(&mut self, buf: &mut [u8], request_id: u16) -> AckRecv {
1033 let mut wait = consts::CONTROL_TIMEOUT;
1034 let mut pending_seen = 0usize;
1035 loop {
1036 match time::timeout(wait, self.socket.recv(buf)).await {
1037 Ok(Ok(len)) => {
1038 let Some((pending_id, requested)) = parse_pending_ack(&buf[..len]) else {
1039 return AckRecv::Received(len);
1040 };
1041 if pending_id != request_id {
1042 debug!(
1043 request_id,
1044 got = pending_id,
1045 "ignoring PENDING_ACK for another request"
1046 );
1047 continue;
1048 }
1049 pending_seen += 1;
1050 if pending_seen > consts::MAX_PENDING_ACKS {
1051 warn!(
1052 request_id,
1053 pending_seen, "device kept requesting more time, giving up"
1054 );
1055 return AckRecv::TimedOut;
1056 }
1057 wait = requested.clamp(consts::CONTROL_TIMEOUT, consts::MAX_PENDING_ACK_WAIT);
1058 debug!(
1059 request_id,
1060 requested_ms = requested.as_millis() as u64,
1061 waiting_ms = wait.as_millis() as u64,
1062 pending_seen,
1063 "device requested more time (PENDING_ACK)"
1064 );
1065 }
1066 Ok(Err(err)) => return AckRecv::Io(err),
1067 Err(_) => return AckRecv::TimedOut,
1068 }
1069 }
1070 }
1071
1072 async fn backoff(&mut self, attempt: usize) {
1073 let multiplier = 1u32 << (attempt.saturating_sub(1)).min(3);
1074 let base_ms = consts::RETRY_BASE_DELAY.as_millis() as u64;
1075 let base = Duration::from_millis(base_ms.saturating_mul(multiplier as u64).max(base_ms));
1076 let jitter_ms = self.rng.u64(..=consts::RETRY_JITTER.as_millis() as u64);
1077 let jitter = Duration::from_millis(jitter_ms);
1078 let delay = base + jitter;
1079 debug!(attempt, delay = ?delay, "gvcp retry backoff");
1080 time::sleep(delay).await;
1081 }
1082
1083 pub async fn read_register(&mut self, addr: u32) -> Result<u32, GigeError> {
1088 let mut payload = BytesMut::with_capacity(4);
1089 payload.put_u32(addr);
1090 let ack = self
1091 .transact_with_retry(OpCode::ReadRegister, payload)
1092 .await?;
1093 if ack.payload.len() != 4 {
1094 return Err(GigeError::Protocol(format!(
1095 "expected 4-byte register ack but device returned {} bytes",
1096 ack.payload.len()
1097 )));
1098 }
1099 let mut cursor = &ack.payload[..];
1100 Ok(cursor.get_u32())
1101 }
1102
1103 pub async fn write_register(&mut self, addr: u32, value: u32) -> Result<(), GigeError> {
1108 let mut payload = BytesMut::with_capacity(8);
1109 payload.put_u32(addr);
1110 payload.put_u32(value);
1111 let ack = self
1112 .transact_with_retry(OpCode::WriteRegister, payload)
1113 .await?;
1114 if ack.payload.len() != 4 {
1115 return Err(GigeError::Protocol(format!(
1116 "expected 4-byte register write ack but device returned {} bytes",
1117 ack.payload.len()
1118 )));
1119 }
1120 Ok(())
1121 }
1122
1123 pub async fn read_mem(&mut self, addr: u64, len: usize) -> Result<Vec<u8>, GigeError> {
1128 let mut remaining = len;
1129 let mut offset = 0usize;
1130 let mut data = Vec::with_capacity(len);
1131 while remaining > 0 {
1132 let chunk = remaining.min(consts::GENCP_MAX_BLOCK);
1133 let request = chunk.next_multiple_of(4);
1139 let mut payload = BytesMut::with_capacity(8);
1140 payload.put_u32((addr + offset as u64) as u32);
1141 payload.put_u16(0); payload.put_u16(request as u16);
1143 let ack = self.transact_with_retry(OpCode::ReadMem, payload).await?;
1144 let ack_data = if ack.payload.len() >= 4 + request {
1146 &ack.payload[4..4 + request]
1147 } else if ack.payload.len() == request {
1148 &ack.payload[..request]
1150 } else {
1151 return Err(GigeError::Protocol(format!(
1152 "expected {} bytes but device returned {}",
1153 request,
1154 ack.payload.len()
1155 )));
1156 };
1157 data.extend_from_slice(&ack_data[..chunk]);
1158 remaining -= chunk;
1159 offset += chunk;
1160 }
1161 Ok(data)
1162 }
1163
1164 pub async fn write_mem(&mut self, addr: u64, data: &[u8]) -> Result<(), GigeError> {
1169 const GVCP_WRITE_OVERHEAD: usize = 4;
1171
1172 let mut offset = 0usize;
1173 while offset < data.len() {
1174 let chunk = (data.len() - offset).min(consts::GENCP_MAX_BLOCK - GVCP_WRITE_OVERHEAD);
1175 if chunk == 0 {
1176 return Err(GigeError::Protocol("write chunk size is zero".into()));
1177 }
1178 let mut payload = BytesMut::with_capacity(GVCP_WRITE_OVERHEAD + chunk);
1179 payload.put_u32((addr + offset as u64) as u32);
1180 payload.extend_from_slice(&data[offset..offset + chunk]);
1181 let ack = self.transact_with_retry(OpCode::WriteMem, payload).await?;
1182 if ack.payload.len() > 4 {
1184 return Err(GigeError::Protocol(
1185 "write acknowledgement carried unexpected payload".into(),
1186 ));
1187 }
1188 offset += chunk;
1189 }
1190 Ok(())
1191 }
1192
1193 pub async fn set_message_destination(
1195 &mut self,
1196 ip: Ipv4Addr,
1197 port: u16,
1198 ) -> Result<(), GigeError> {
1199 info!(%ip, port, "configuring message channel destination");
1200 self.write_mem(consts::MESSAGE_DESTINATION_ADDRESS, &ip.octets())
1201 .await?;
1202 self.write_mem(
1205 consts::MESSAGE_DESTINATION_PORT,
1206 &u32::from(port).to_be_bytes(),
1207 )
1208 .await?;
1209 Ok(())
1210 }
1211
1212 fn stream_reg(channel: u32, offset: u64) -> u64 {
1213 consts::STREAM_CHANNEL_BASE + channel as u64 * consts::STREAM_CHANNEL_STRIDE + offset
1214 }
1215
1216 pub async fn set_stream_destination(
1218 &mut self,
1219 channel: u32,
1220 ip: Ipv4Addr,
1221 port: u16,
1222 ) -> Result<(), GigeError> {
1223 info!(channel, %ip, port, "configuring stream destination");
1224 let addr = Self::stream_reg(channel, consts::STREAM_DESTINATION_ADDRESS);
1225 self.write_mem(addr, &ip.octets()).await?;
1226 let addr = Self::stream_reg(channel, consts::STREAM_DESTINATION_PORT);
1227 self.write_mem(addr, &(port as u32).to_be_bytes()).await?;
1228 Ok(())
1229 }
1230
1231 pub async fn set_stream_packet_size(
1239 &mut self,
1240 channel: u32,
1241 packet_size: u32,
1242 ) -> Result<(), GigeError> {
1243 if packet_size > STREAM_PACKET_SIZE_MASK {
1244 return Err(GigeError::Protocol(format!(
1245 "GVSP packet size {packet_size} does not fit GevSCPSPacketSize: the size field is \
1246 bits 0-15, so the device would receive {}",
1247 packet_size & STREAM_PACKET_SIZE_MASK
1248 )));
1249 }
1250 info!(channel, packet_size, "configuring stream packet size");
1251 let addr = Self::stream_reg(channel, consts::STREAM_PACKET_SIZE);
1252 self.write_mem(addr, &packet_size.to_be_bytes()).await
1253 }
1254
1255 pub async fn request_test_packet(
1265 &mut self,
1266 channel: u32,
1267 packet_size: u32,
1268 ) -> Result<(), GigeError> {
1269 if packet_size > STREAM_PACKET_SIZE_MASK {
1270 return Err(GigeError::Protocol(format!(
1271 "GVSP packet size {packet_size} does not fit GevSCPSPacketSize"
1272 )));
1273 }
1274 let addr = Self::stream_reg(channel, consts::STREAM_PACKET_SIZE);
1275 let word = packet_size | SCPS_FIRE_TEST_PACKET | SCPS_DO_NOT_FRAGMENT;
1276 debug!(channel, packet_size, "requesting GVSP test packet");
1277 self.write_mem(addr, &word.to_be_bytes()).await
1278 }
1279
1280 pub async fn get_stream_packet_size(&mut self, channel: u32) -> Result<u32, GigeError> {
1293 let addr = Self::stream_reg(channel, consts::STREAM_PACKET_SIZE) as u32;
1294 let raw = self.read_register(addr).await?;
1295 Ok(raw & STREAM_PACKET_SIZE_MASK)
1296 }
1297
1298 pub async fn set_stream_packet_delay(
1300 &mut self,
1301 channel: u32,
1302 packet_delay: u32,
1303 ) -> Result<(), GigeError> {
1304 debug!(channel, packet_delay, "configuring stream packet delay");
1305 let addr = Self::stream_reg(channel, consts::STREAM_PACKET_DELAY);
1306 self.write_mem(addr, &packet_delay.to_be_bytes()).await
1307 }
1308
1309 pub async fn negotiate_stream(
1311 &mut self,
1312 channel: u32,
1313 iface: &Iface,
1314 port: u16,
1315 target_mtu: Option<u32>,
1316 ) -> Result<StreamParams, GigeError> {
1317 let host_ip = iface
1318 .ipv4()
1319 .ok_or_else(|| GigeError::Protocol("interface lacks IPv4 address".into()))?;
1320 let iface_mtu = nic::mtu(iface)?;
1321 let mtu = target_mtu.map_or(iface_mtu, |limit| limit.min(iface_mtu));
1322 let packet_size = nic::best_packet_size(mtu);
1323 let packet_delay = if mtu <= 1500 {
1324 const DELAY_NS: u32 = 2_000; DELAY_NS / 80
1329 } else {
1330 0
1331 };
1332
1333 self.set_stream_destination(channel, host_ip, port).await?;
1334 self.set_stream_packet_size(channel, packet_size).await?;
1335 self.set_stream_packet_delay(channel, packet_delay).await?;
1336
1337 Ok(StreamParams {
1338 packet_size,
1339 packet_delay,
1340 mtu,
1341 host: host_ip,
1342 port,
1343 })
1344 }
1345 pub async fn read_persistent_ip(
1349 &mut self,
1350 ) -> Result<(Ipv4Addr, Ipv4Addr, Ipv4Addr), GigeError> {
1351 let ip = Ipv4Addr::from(
1352 self.read_register(consts::PERSISTENT_IP_ADDRESS as u32)
1353 .await?,
1354 );
1355 let subnet = Ipv4Addr::from(
1356 self.read_register(consts::PERSISTENT_SUBNET_MASK as u32)
1357 .await?,
1358 );
1359 let gateway = Ipv4Addr::from(
1360 self.read_register(consts::PERSISTENT_DEFAULT_GATEWAY as u32)
1361 .await?,
1362 );
1363 Ok((ip, subnet, gateway))
1364 }
1365
1366 pub async fn write_persistent_ip(
1368 &mut self,
1369 ip: Ipv4Addr,
1370 subnet: Ipv4Addr,
1371 gateway: Ipv4Addr,
1372 ) -> Result<(), GigeError> {
1373 self.write_register(consts::PERSISTENT_IP_ADDRESS as u32, u32::from(ip))
1374 .await?;
1375 self.write_register(consts::PERSISTENT_SUBNET_MASK as u32, u32::from(subnet))
1376 .await?;
1377 self.write_register(
1378 consts::PERSISTENT_DEFAULT_GATEWAY as u32,
1379 u32::from(gateway),
1380 )
1381 .await?;
1382 info!(%ip, %subnet, %gateway, "wrote persistent IP configuration");
1383 Ok(())
1384 }
1385
1386 pub async fn enable_persistent_ip(&mut self) -> Result<(), GigeError> {
1390 let current = self.read_register(consts::CURRENT_IP_CONFIG as u32).await?;
1391 let updated = current | 0x02; self.write_register(consts::CURRENT_IP_CONFIG as u32, updated)
1393 .await?;
1394 info!(config = format!("0x{updated:08x}"), "enabled persistent IP");
1395 Ok(())
1396 }
1397
1398 pub async fn request_resend(
1400 &mut self,
1401 block_id: u16,
1402 first_packet: u16,
1403 last_packet: u16,
1404 ) -> Result<(), GigeError> {
1405 let mut payload = BytesMut::with_capacity(8);
1406 payload.put_u16(block_id);
1407 payload.put_u16(0); payload.put_u16(first_packet);
1409 payload.put_u16(last_packet);
1410
1411 let request_id = self.next_request_id();
1412 let header = GvcpRequestHeader {
1413 flags: CommandFlags::ACK_REQUIRED,
1414 command: consts::PACKET_RESEND_COMMAND,
1415 length: payload.len() as u16,
1416 request_id,
1417 };
1418 let packet = header.encode(&payload);
1419 trace!(
1420 block_id,
1421 first_packet, last_packet, request_id, "sending packet resend request"
1422 );
1423 self.socket.send(&packet).await?;
1424 let mut buf = [0u8; viva_gencp::HEADER_SIZE];
1425 match time::timeout(consts::CONTROL_TIMEOUT, self.socket.recv(&mut buf)).await {
1426 Ok(Ok(len)) => {
1427 if len != viva_gencp::HEADER_SIZE {
1428 return Err(GigeError::Protocol("resend ack length mismatch".into()));
1429 }
1430 let mut cursor = &buf[..];
1431 let status = StatusCode::from_raw(cursor.get_u16());
1432 let command = cursor.get_u16();
1433 let length = cursor.get_u16();
1434 let ack_request_id = cursor.get_u16();
1435 if command == consts::PENDING_ACK {
1436 return Err(GigeError::Protocol(
1441 "device requested more time for a packet resend".into(),
1442 ));
1443 }
1444 if command != consts::PACKET_RESEND_ACK {
1445 return Err(GigeError::Protocol("unexpected resend ack opcode".into()));
1446 }
1447 if length != 0 {
1448 return Err(GigeError::Protocol("resend ack carried payload".into()));
1449 }
1450 if ack_request_id != request_id {
1451 return Err(GigeError::Protocol("resend ack request id mismatch".into()));
1452 }
1453 if status != StatusCode::Success {
1454 return Err(GigeError::Status(status));
1455 }
1456 Ok(())
1457 }
1458 Ok(Err(err)) => Err(err.into()),
1459 Err(_) => Err(GigeError::Timeout),
1460 }
1461 }
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466 use super::*;
1467
1468 fn golden_discovery_payload() -> Vec<u8> {
1479 let mut p = vec![0u8; 248];
1480 p[0..2].copy_from_slice(&2u16.to_be_bytes()); p[2..4].copy_from_slice(&1u16.to_be_bytes()); p[4..8].copy_from_slice(&0u32.to_be_bytes()); p[10..16].copy_from_slice(&[0x00, 0x0C, 0xDF, 0x06, 0x5B, 0x2F]); p[16..20].copy_from_slice(&7u32.to_be_bytes()); p[20..24].copy_from_slice(&5u32.to_be_bytes()); p[36..40].copy_from_slice(&[169, 254, 78, 62]); p[52..56].copy_from_slice(&[255, 255, 0, 0]); p[68..72].copy_from_slice(&[0, 0, 0, 0]); let put =
1491 |p: &mut [u8], at: usize, s: &str| p[at..at + s.len()].copy_from_slice(s.as_bytes());
1492 put(&mut p, 72, "JAI Corporation"); put(&mut p, 104, "FS-3200T-10GE-NNC"); put(&mut p, 136, "1.2.3"); put(&mut p, 168, "mfr-specific"); put(&mut p, 216, "SN-12345"); put(&mut p, 232, "left-camera"); p
1499 }
1500
1501 #[test]
1502 fn discovery_payload_matches_spec_offsets() {
1503 let info = parse_discovery_payload(&golden_discovery_payload()).expect("parse");
1504
1505 assert_eq!(info.mac, [0x00, 0x0C, 0xDF, 0x06, 0x5B, 0x2F]);
1509 assert_eq!(info.mac_string(), "00:0C:DF:06:5B:2F");
1510 assert_eq!(info.ip, Ipv4Addr::new(169, 254, 78, 62));
1511 assert_eq!(info.manufacturer.as_deref(), Some("JAI Corporation"));
1512 assert_eq!(info.model.as_deref(), Some("FS-3200T-10GE-NNC"));
1513 assert_eq!(info.version.as_deref(), Some("1.2.3"));
1514 assert_eq!(info.serial.as_deref(), Some("SN-12345"));
1515 assert_eq!(info.user_name.as_deref(), Some("left-camera"));
1516 }
1517
1518 #[test]
1519 fn discovery_payload_tolerates_truncation() {
1520 let short = golden_discovery_payload()[..40].to_vec();
1523 let info = parse_discovery_payload(&short).expect("short payload should parse");
1524 assert_eq!(info.ip, Ipv4Addr::new(169, 254, 78, 62));
1525 assert_eq!(info.mac, [0x00, 0x0C, 0xDF, 0x06, 0x5B, 0x2F]);
1526 assert_eq!(info.manufacturer, None);
1527 assert_eq!(info.serial, None);
1528
1529 assert!(parse_discovery_payload(&[0u8; 12]).is_err());
1531 }
1532
1533 #[test]
1534 fn discovery_ack_ignores_foreign_traffic() {
1535 let payload = golden_discovery_payload();
1536 let ack = |status: u16, command: u16, request_id: u16| {
1537 let mut buf = Vec::new();
1538 buf.extend_from_slice(&status.to_be_bytes());
1539 buf.extend_from_slice(&command.to_be_bytes());
1540 buf.extend_from_slice(&(payload.len() as u16).to_be_bytes());
1541 buf.extend_from_slice(&request_id.to_be_bytes());
1542 buf.extend_from_slice(&payload);
1543 buf
1544 };
1545
1546 assert!(parse_discovery_ack(&ack(0, consts::DISCOVERY_ACK, 0x0100), 0x0100).is_some());
1548 assert!(parse_discovery_ack(&ack(0, consts::DISCOVERY_ACK, 0x0999), 0x0100).is_none());
1552 assert!(parse_discovery_ack(&ack(0, 0x0081, 0x0100), 0x0100).is_none());
1553 assert!(parse_discovery_ack(&ack(0x8002, consts::DISCOVERY_ACK, 0x0100), 0x0100).is_none());
1554 assert!(parse_discovery_ack(&[0u8; 4], 0x0100).is_none());
1555 }
1556
1557 #[test]
1558 fn directed_broadcast_uses_netmask_for_link_local_address() {
1559 assert_eq!(
1564 directed_broadcast(
1565 Ipv4Addr::new(169, 254, 1, 10),
1566 Ipv4Addr::new(255, 255, 0, 0),
1567 ),
1568 Ipv4Addr::new(169, 254, 255, 255)
1569 );
1570 }
1571
1572 fn golden_pending_ack(request_id: u16, millis: u16) -> Vec<u8> {
1576 let mut buf = Vec::new();
1577 buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&consts::PENDING_ACK.to_be_bytes()); buf.extend_from_slice(&4u16.to_be_bytes()); buf.extend_from_slice(&request_id.to_be_bytes());
1581 buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&millis.to_be_bytes()); buf
1584 }
1585
1586 #[test]
1587 fn pending_ack_matches_spec_offsets() {
1588 let (id, wait) = parse_pending_ack(&golden_pending_ack(0x1234, 750)).expect("pending ack");
1589 assert_eq!(id, 0x1234);
1590 assert_eq!(wait, Duration::from_millis(750));
1591
1592 let mut junk = golden_pending_ack(0x1234, 750);
1597 junk[8..10].copy_from_slice(&0xDEADu16.to_be_bytes());
1598 assert_eq!(
1599 parse_pending_ack(&junk).expect("pending ack").1.as_millis(),
1600 750
1601 );
1602 }
1603
1604 #[test]
1605 fn pending_ack_is_distinguished_from_real_acks() {
1606 let mut readreg = Vec::new();
1609 readreg.extend_from_slice(&0u16.to_be_bytes());
1610 readreg.extend_from_slice(&0x0081u16.to_be_bytes());
1611 readreg.extend_from_slice(&4u16.to_be_bytes());
1612 readreg.extend_from_slice(&0x1234u16.to_be_bytes());
1613 readreg.extend_from_slice(&0u32.to_be_bytes());
1614 assert!(parse_pending_ack(&readreg).is_none());
1615 assert!(parse_pending_ack(&[0u8; 4]).is_none());
1616
1617 let truncated = &golden_pending_ack(0x1234, 750)[..viva_gencp::HEADER_SIZE];
1619 let (id, wait) = parse_pending_ack(truncated).expect("truncated pending ack");
1620 assert_eq!(id, 0x1234);
1621 assert_eq!(wait, consts::CONTROL_TIMEOUT);
1622 }
1623
1624 async fn pending_ack_device(
1630 pending: usize,
1631 wait_ms: u16,
1632 stale_ack: bool,
1633 ) -> (SocketAddr, tokio::task::JoinHandle<usize>) {
1634 let sock = UdpSocket::bind("127.0.0.1:0").await.expect("bind");
1635 let addr = sock.local_addr().expect("addr");
1636 let handle = tokio::spawn(async move {
1637 let mut buf = [0u8; 2048];
1638 let mut commands = 0usize;
1639 let (len, peer) = sock.recv_from(&mut buf).await.expect("recv");
1640 commands += 1;
1641 let request_id = u16::from_be_bytes([buf[6], buf[7]]);
1642 debug_assert!(len >= viva_gencp::HEADER_SIZE);
1643 for _ in 0..pending {
1644 let ack = golden_pending_ack(request_id, wait_ms);
1645 sock.send_to(&ack, peer).await.expect("send pending");
1646 }
1647 if stale_ack {
1648 let mut ack = Vec::new();
1649 ack.extend_from_slice(&0u16.to_be_bytes());
1650 ack.extend_from_slice(&0x0081u16.to_be_bytes());
1651 ack.extend_from_slice(&4u16.to_be_bytes());
1652 ack.extend_from_slice(&request_id.wrapping_add(1).to_be_bytes());
1653 ack.extend_from_slice(&0xDEADBEEFu32.to_be_bytes());
1654 sock.send_to(&ack, peer).await.expect("send stale ack");
1655 }
1656 let mut ack = Vec::new();
1658 ack.extend_from_slice(&0u16.to_be_bytes());
1659 ack.extend_from_slice(&0x0081u16.to_be_bytes());
1660 ack.extend_from_slice(&4u16.to_be_bytes());
1661 ack.extend_from_slice(&request_id.to_be_bytes());
1662 ack.extend_from_slice(&0xCAFEBABEu32.to_be_bytes());
1663 sock.send_to(&ack, peer).await.expect("send ack");
1664 let drain = time::timeout(Duration::from_millis(200), sock.recv_from(&mut buf)).await;
1667 if drain.is_ok() {
1668 commands += 1;
1669 }
1670 commands
1671 });
1672 (addr, handle)
1673 }
1674
1675 #[tokio::test]
1676 async fn pending_ack_extends_the_deadline_without_resending() {
1677 let (addr, server) = pending_ack_device(1, 300, false).await;
1678 let mut device = GigeDevice::open(addr).await.expect("open");
1679 let value = device.read_register(0x0a00).await.expect("read register");
1680 assert_eq!(value, 0xCAFEBABE);
1681 assert_eq!(server.await.expect("join"), 1, "command must not be resent");
1682 }
1683
1684 #[tokio::test]
1685 async fn repeated_pending_acks_are_all_honoured() {
1686 let (addr, server) = pending_ack_device(3, 200, false).await;
1689 let mut device = GigeDevice::open(addr).await.expect("open");
1690 let value = device.read_register(0x0a00).await.expect("read register");
1691 assert_eq!(value, 0xCAFEBABE);
1692 assert_eq!(server.await.expect("join"), 1, "command must not be resent");
1693 }
1694
1695 #[tokio::test]
1696 async fn stale_acknowledgement_is_ignored_without_resending() {
1697 let (addr, server) = pending_ack_device(0, 0, true).await;
1698 let mut device = GigeDevice::open(addr).await.expect("open");
1699 let value = device.read_register(0x0a00).await.expect("read register");
1700 assert_eq!(value, 0xCAFEBABE);
1701 assert_eq!(server.await.expect("join"), 1, "command must not be resent");
1702 }
1703
1704 async fn status_device(
1708 status_raw: u16,
1709 error_replies: usize,
1710 ) -> (SocketAddr, tokio::task::JoinHandle<usize>) {
1711 let sock = UdpSocket::bind("127.0.0.1:0").await.expect("bind");
1712 let addr = sock.local_addr().expect("addr");
1713 let handle = tokio::spawn(async move {
1714 let mut buf = [0u8; 2048];
1715 let mut commands = 0usize;
1716 loop {
1717 let recv =
1718 time::timeout(Duration::from_millis(600), sock.recv_from(&mut buf)).await;
1719 let Ok(Ok((_len, peer))) = recv else { break };
1720 commands += 1;
1721 let request_id = u16::from_be_bytes([buf[6], buf[7]]);
1722 let mut ack = Vec::new();
1723 if commands <= error_replies {
1724 ack.extend_from_slice(&status_raw.to_be_bytes());
1725 ack.extend_from_slice(&0x0081u16.to_be_bytes());
1726 ack.extend_from_slice(&0u16.to_be_bytes());
1727 ack.extend_from_slice(&request_id.to_be_bytes());
1728 } else {
1729 ack.extend_from_slice(&0u16.to_be_bytes());
1730 ack.extend_from_slice(&0x0081u16.to_be_bytes());
1731 ack.extend_from_slice(&4u16.to_be_bytes());
1732 ack.extend_from_slice(&request_id.to_be_bytes());
1733 ack.extend_from_slice(&0xCAFEBABEu32.to_be_bytes());
1734 }
1735 sock.send_to(&ack, peer).await.expect("send ack");
1736 }
1737 commands
1738 });
1739 (addr, handle)
1740 }
1741
1742 #[tokio::test]
1743 async fn busy_is_retried() {
1744 let (addr, server) = status_device(0x8007, 1).await;
1746 let mut device = GigeDevice::open(addr).await.expect("open");
1747 let value = device.read_register(0x0a00).await.expect("read register");
1748 assert_eq!(value, 0xCAFEBABE);
1749 assert_eq!(
1750 server.await.expect("join"),
1751 2,
1752 "BUSY must be retried, so the device sees a second command"
1753 );
1754 }
1755
1756 #[tokio::test]
1757 async fn write_protect_is_reported_not_retried() {
1758 let (addr, server) = status_device(0x8004, usize::MAX).await;
1762 let mut device = GigeDevice::open(addr).await.expect("open");
1763 let err = device
1764 .read_register(0x0a00)
1765 .await
1766 .expect_err("write protect must surface");
1767 assert!(
1768 matches!(err, GigeError::Status(StatusCode::WriteProtect)),
1769 "expected WRITE_PROTECT, got {err:?}"
1770 );
1771 assert_eq!(
1772 server.await.expect("join"),
1773 1,
1774 "a permanent refusal must not be retried"
1775 );
1776 }
1777
1778 #[tokio::test]
1779 async fn access_denied_names_itself_in_the_error() {
1780 let (addr, server) = status_device(0x8006, usize::MAX).await;
1782 let mut device = GigeDevice::open(addr).await.expect("open");
1783 let err = device
1784 .read_register(0x0a00)
1785 .await
1786 .expect_err("access denied must surface");
1787 assert_eq!(
1788 err.to_string(),
1789 "device reported status ACCESS_DENIED (0x8006)"
1790 );
1791 assert_eq!(server.await.expect("join"), 1);
1792 }
1793
1794 #[test]
1795 fn request_header_roundtrip() {
1796 let header = GvcpRequestHeader {
1797 flags: CommandFlags::ACK_REQUIRED,
1798 command: 0x1234,
1799 length: 4,
1800 request_id: 0xBEEF,
1801 };
1802 let payload = [1u8, 2, 3, 4];
1803 let encoded = header.encode(&payload);
1804 assert_eq!(encoded.len(), viva_gencp::HEADER_SIZE + payload.len());
1805 assert_eq!(encoded[0], GVCP_CMD_KEY);
1807 assert_eq!(encoded[1], 0x01); assert_eq!(&encoded[2..4], &header.command.to_be_bytes());
1809 assert_eq!(&encoded[4..6], &header.length.to_be_bytes());
1810 assert_eq!(&encoded[6..8], &header.request_id.to_be_bytes());
1811 assert_eq!(&encoded[8..], &payload);
1812 }
1813
1814 #[test]
1815 fn forceip_payload_encoding() {
1816 let mac = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE];
1817 let ip = Ipv4Addr::new(192, 168, 1, 100);
1818 let subnet = Ipv4Addr::new(255, 255, 255, 0);
1819 let gateway = Ipv4Addr::new(192, 168, 1, 1);
1820 let payload = encode_forceip_payload(mac, ip, subnet, gateway);
1821 assert_eq!(payload.len(), 56);
1822 assert_eq!(&payload[2..8], &mac);
1824 assert_eq!(&payload[20..24], &ip.octets());
1826 assert_eq!(&payload[36..40], &subnet.octets());
1828 assert_eq!(&payload[52..56], &gateway.octets());
1830 assert_eq!(&payload[0..2], &[0, 0]);
1832 assert_eq!(&payload[8..20], &[0u8; 12]);
1833 assert_eq!(&payload[24..36], &[0u8; 12]);
1834 assert_eq!(&payload[40..52], &[0u8; 12]);
1835 }
1836
1837 #[test]
1838 fn ack_header_conversion() {
1839 let ack = AckHeader {
1840 status: StatusCode::Busy,
1841 opcode: OpCode::ReadMem,
1842 length: 12,
1843 request_id: 0x44,
1844 };
1845 let converted = GvcpAckHeader::from(ack);
1846 assert_eq!(converted.status, StatusCode::Busy);
1847 assert_eq!(converted.command, OpCode::ReadMem.ack_code());
1848 assert_eq!(converted.length, 12);
1849 assert_eq!(converted.request_id, 0x44);
1850 }
1851}