1use std::fs::File;
2use std::io::Write;
3use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::time::{Duration, SystemTime};
7
8use anyhow::{Context, Result, anyhow, bail};
9use serde::Serialize;
10use std::convert::TryInto;
11use time::OffsetDateTime;
12use time::format_description::well_known::Rfc3339;
13use tokio::runtime::Handle;
14use tokio::sync::Mutex;
15use viva_genapi_xml::{self, XmlError};
16use viva_genicam::genapi::NodeMap;
17use viva_genicam::{Camera, GigeRegisterIo};
18use viva_gige::DeviceInfo;
19use viva_gige::discover_on_interface;
20use viva_gige::gvcp::GigeDevice;
21use viva_gige::nic::{Iface, IfaceSelector};
22use viva_gige::{GVCP_PORT, discover};
23
24pub const DEFAULT_DISCOVERY_TIMEOUT_MS: u64 = 500;
25
26pub fn format_mac(mac: &[u8; 6]) -> String {
27 mac.iter()
28 .map(|b| format!("{b:02X}"))
29 .collect::<Vec<_>>()
30 .join(":")
31}
32
33pub async fn discover_devices(
34 timeout: Duration,
35 iface: Option<&IfaceSelector>,
36) -> Result<Vec<DeviceInfo>> {
37 let devices = if let Some(selector) = iface {
38 let iface = resolve_iface_selector(selector)?;
39 discover_on_interface(timeout, iface.name())
40 .await
41 .context("discover devices on interface")?
42 } else {
43 discover(timeout).await.context("broadcast discovery")?
44 };
45 Ok(devices)
46}
47
48pub async fn select_device(
49 ip: Option<Ipv4Addr>,
50 index: Option<usize>,
51 iface: Option<&IfaceSelector>,
52 timeout: Duration,
53) -> Result<DeviceInfo> {
54 match (ip, index) {
55 (Some(ip), None) => {
56 let mut devices = discover_devices(timeout, iface).await?;
57 if let Some(found) = devices.drain(..).find(|dev| dev.ip == ip) {
58 return Ok(found);
59 }
60 Ok(DeviceInfo::from_ip(ip))
61 }
62 (None, Some(idx)) => {
63 let devices = discover_devices(timeout, iface).await?;
64 let device = devices
65 .into_iter()
66 .nth(idx)
67 .ok_or_else(|| anyhow!("no device at index {idx}"))?;
68 Ok(device)
69 }
70 (Some(ip), Some(_)) => {
71 bail!("specify either --ip or --index, not both (using {ip})");
72 }
73 (None, None) => {
74 bail!("a camera must be selected via --ip or --index");
75 }
76 }
77}
78
79pub async fn fetch_xml(control: Arc<Mutex<GigeDevice>>) -> Result<String> {
85 viva_genapi_xml::fetch_and_load_xml({
86 move |address, length| {
87 let control = Arc::clone(&control);
88 async move {
89 let mut guard = control.lock().await;
90 guard
91 .read_mem(address, length)
92 .await
93 .map_err(|err| XmlError::Transport(err.to_string()))
94 }
95 }
96 })
97 .await
98 .context("fetch GenApi XML")
99}
100
101pub async fn open_camera(device: &DeviceInfo) -> Result<Camera<GigeRegisterIo>> {
102 let addr = SocketAddr::new(IpAddr::V4(device.ip), GVCP_PORT);
103 let control =
104 Arc::new(Mutex::new(GigeDevice::open(addr).await.with_context(
105 || format!("connect GVCP control channel at {}", device.ip),
106 )?));
107 let xml = fetch_xml(control.clone()).await?;
108 let model = viva_genapi_xml::parse(&xml).context("parse GenApi XML")?;
109 let nodemap = NodeMap::try_from_xml(model)?;
110 let handle = Handle::current();
111 let device = Arc::try_unwrap(control)
112 .map_err(|_| anyhow!("control connection still in use"))?
113 .into_inner();
114 let transport = GigeRegisterIo::new(handle, device);
115 Ok(Camera::new(transport, nodemap))
116}
117
118pub async fn open_control(device: &DeviceInfo) -> Result<GigeDevice> {
124 let addr = SocketAddr::new(IpAddr::V4(device.ip), GVCP_PORT);
125 GigeDevice::open(addr)
126 .await
127 .with_context(|| format!("connect GVCP control channel at {}", device.ip))
128}
129
130pub fn resolve_iface_selector(selector: &IfaceSelector) -> Result<Iface> {
138 selector
139 .resolve()
140 .with_context(|| format!("resolve host interface '{selector}'"))
141}
142
143pub fn resolve_receive_iface(iface: Option<&IfaceSelector>, camera_ip: Ipv4Addr) -> Result<Iface> {
163 match iface {
164 Some(selector) => resolve_iface_selector(selector),
165 None => Iface::from_remote_ipv4(camera_ip).with_context(|| {
166 format!(
167 "probe which local interface routes to {camera_ip} \
168 (pass --iface <HOST-IP|NAME> to choose one explicitly)"
169 )
170 }),
171 }
172}
173
174pub fn resolve_iface(iface: Option<&IfaceSelector>) -> Result<Option<Iface>> {
175 iface.map(resolve_iface_selector).transpose()
176}
177
178pub fn print_json<T: Serialize>(value: &T) -> Result<()> {
179 let text = serde_json::to_string_pretty(value).context("serialise JSON output")?;
180 println!("{text}");
181 Ok(())
182}
183
184pub fn format_system_time(ts: SystemTime) -> Result<String> {
185 let dt: OffsetDateTime = <SystemTime as std::convert::Into<OffsetDateTime>>::into(ts);
186 dt.format(&Rfc3339).context("format timestamp")
187}
188
189pub fn encode_pgm(width: u32, height: u32, data: &[u8]) -> Result<Vec<u8>> {
190 let w: usize = width.try_into().context("width doesn't fit in usize")?;
192 let h: usize = height.try_into().context("height doesn't fit in usize")?;
193
194 let expected = w.checked_mul(h).context("image area overflow")?;
196
197 if expected != data.len() {
198 bail!(
199 "PGM payload length mismatch: expected {expected}, got {}",
200 data.len()
201 );
202 }
203
204 let header = format!("P5\n{width} {height}\n255\n");
205 let mut buf = Vec::with_capacity(header.len() + data.len());
206 buf.extend_from_slice(header.as_bytes());
207 buf.extend_from_slice(data);
208 Ok(buf)
209}
210
211pub fn encode_ppm(width: u32, height: u32, data: &[u8]) -> Result<Vec<u8>> {
212 let w: usize = width.try_into().context("width doesn't fit in usize")?;
213 let h: usize = height.try_into().context("height doesn't fit in usize")?;
214
215 let expected = w
217 .checked_mul(h)
218 .and_then(|px| px.checked_mul(3))
219 .context("image area overflow")?;
220
221 if expected != data.len() {
222 bail!(
223 "PPM payload length mismatch: expected {expected}, got {}",
224 data.len()
225 );
226 }
227 let header = format!("P6\n{width} {height}\n255\n");
228 let mut buf = Vec::with_capacity(header.len() + data.len());
229 buf.extend_from_slice(header.as_bytes());
230 buf.extend_from_slice(data);
231 Ok(buf)
232}
233
234pub fn save_image(buffer: &[u8], path: &PathBuf) -> Result<()> {
235 let mut file = File::create(path).with_context(|| format!("create {}", path.display()))?;
236 file.write_all(buffer)
237 .with_context(|| format!("write {}", path.display()))?;
238 Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn pgm_header_is_correct() {
247 let data = vec![0u8; 4];
248 let encoded = encode_pgm(2, 2, &data).expect("encode");
249 assert!(encoded.starts_with(b"P5\n2 2\n255\n"));
250 assert_eq!(encoded.len(), 4 + "P5\n2 2\n255\n".len());
251 }
252
253 #[test]
254 fn ppm_header_is_correct() {
255 let data = vec![0u8; 12];
256 let encoded = encode_ppm(2, 2, &data).expect("encode");
257 assert!(encoded.starts_with(b"P6\n2 2\n255\n"));
258 assert_eq!(encoded.len(), 12 + "P6\n2 2\n255\n".len());
259 }
260}