Skip to main content

viva_camctl/
cmd_set_ip.rs

1use std::net::Ipv4Addr;
2use std::time::Duration;
3
4use anyhow::{Context, Result, anyhow};
5use tracing::info;
6
7use viva_gige::nic::IfaceSelector;
8
9use crate::common;
10
11/// Parse a MAC address string like "DE:AD:BE:EF:CA:FE" into a 6-byte array.
12fn parse_mac(s: &str) -> Result<[u8; 6]> {
13    let parts: Vec<&str> = s.split(':').collect();
14    if parts.len() != 6 {
15        return Err(anyhow!(
16            "invalid MAC address '{s}': expected 6 colon-separated hex bytes"
17        ));
18    }
19    let mut mac = [0u8; 6];
20    for (i, part) in parts.iter().enumerate() {
21        mac[i] = u8::from_str_radix(part, 16)
22            .with_context(|| format!("invalid hex byte '{part}' in MAC address"))?;
23    }
24    Ok(mac)
25}
26
27pub async fn run(
28    mac: &str,
29    ip: Ipv4Addr,
30    subnet: Ipv4Addr,
31    gateway: Ipv4Addr,
32    force: bool,
33    iface: Option<IfaceSelector>,
34) -> Result<()> {
35    let mac = parse_mac(mac)?;
36
37    if force {
38        // FORCEIP: broadcast temporary IP assignment.
39        let iface_obj = common::resolve_iface(iface.as_ref())?;
40        viva_gige::force_ip(mac, ip, subnet, gateway, iface_obj.as_ref())
41            .await
42            .context("FORCEIP command failed")?;
43        println!(
44            "FORCEIP sent: {} -> {} (subnet {}, gateway {})",
45            common::format_mac(&mac),
46            ip,
47            subnet,
48            gateway,
49        );
50    } else {
51        // Persistent IP: discover device by MAC, then write registers.
52        let timeout = Duration::from_millis(common::DEFAULT_DISCOVERY_TIMEOUT_MS);
53        let devices = common::discover_devices(timeout, iface.as_ref()).await?;
54        let device = devices.iter().find(|d| d.mac == mac).ok_or_else(|| {
55            anyhow!(
56                "no device with MAC {} found (use --force for offline assignment)",
57                common::format_mac(&mac),
58            )
59        })?;
60
61        info!(ip = %device.ip, "found device, opening control connection");
62        let mut control = common::open_control(device).await?;
63        control.claim_control().await.context("claim CCP")?;
64
65        control
66            .write_persistent_ip(ip, subnet, gateway)
67            .await
68            .context("write persistent IP registers")?;
69        control
70            .enable_persistent_ip()
71            .await
72            .context("enable persistent IP mode")?;
73
74        control.release_control().await.context("release CCP")?;
75        println!(
76            "Persistent IP configured: {} -> {} (subnet {}, gateway {})",
77            common::format_mac(&mac),
78            ip,
79            subnet,
80            gateway,
81        );
82        println!("Power-cycle the device to apply the new IP address.");
83    }
84
85    Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn parse_mac_valid() {
94        let mac = parse_mac("DE:AD:BE:EF:CA:FE").unwrap();
95        assert_eq!(mac, [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]);
96    }
97
98    #[test]
99    fn parse_mac_lowercase() {
100        let mac = parse_mac("de:ad:be:ef:ca:fe").unwrap();
101        assert_eq!(mac, [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]);
102    }
103
104    #[test]
105    fn parse_mac_invalid_format() {
106        assert!(parse_mac("DEADBEEFCAFE").is_err());
107        assert!(parse_mac("DE:AD:BE:EF:CA").is_err());
108        assert!(parse_mac("DE:AD:BE:EF:CA:FE:00").is_err());
109    }
110
111    #[test]
112    fn parse_mac_invalid_hex() {
113        assert!(parse_mac("GG:AD:BE:EF:CA:FE").is_err());
114    }
115}