1use serde::{Deserialize, Serialize};
10
11pub mod frame_header;
12pub use frame_header::{FRAME_MAGIC, FrameHeader, FrameHeaderError, HEADER_SIZE};
13
14pub const API_VERSION: u32 = 2;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DeviceAnnounce {
32 pub id: String,
33 pub name: String,
34 pub model: String,
35 pub serial: String,
36 #[serde(default)]
41 pub api_version: Option<u32>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DeviceStatus {
49 pub connected: bool,
50 pub error: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DeviceXmlResponse {
56 pub xml: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct NodeValueUpdate {
73 pub value: serde_json::Value,
74 pub access_mode: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub min: Option<f64>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub max: Option<f64>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub inc: Option<f64>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct NumericRange {
97 pub min: f64,
98 pub max: f64,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub inc: Option<f64>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct FeatureState {
126 pub value: serde_json::Value,
128 pub access_mode: String,
130 pub kind: String,
132 #[serde(default = "default_true")]
134 pub is_implemented: bool,
135 #[serde(default = "default_true")]
137 pub is_available: bool,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub numeric: Option<NumericRange>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub enum_available: Option<Vec<String>>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub unit: Option<String>,
147}
148
149fn default_true() -> bool {
150 true
151}
152
153impl FeatureState {
154 pub fn to_node_value_update(&self) -> NodeValueUpdate {
157 let (min, max, inc) = match &self.numeric {
158 Some(r) => (Some(r.min), Some(r.max), r.inc),
159 None => (None, None, None),
160 };
161 NodeValueUpdate {
162 value: self.value.clone(),
163 access_mode: self.access_mode.clone(),
164 min,
165 max,
166 inc,
167 }
168 }
169}
170
171impl From<&FeatureState> for NodeValueUpdate {
172 fn from(s: &FeatureState) -> Self {
173 s.to_node_value_update()
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CommandResult {
188 pub ok: bool,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub error: Option<String>,
191 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
194 pub affected_states: std::collections::HashMap<String, FeatureState>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct NodeSetRequest {
200 pub value: serde_json::Value,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct NodeOpResponse {
206 pub ok: bool,
207 pub error: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct BulkReadRequest {
217 pub names: Vec<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct BulkReadResponse {
226 pub values: std::collections::HashMap<String, NodeValueUpdate>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct AcquisitionControlRequest {
234 pub command: AcquisitionCommand,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum AcquisitionCommand {
240 Start,
241 Stop,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct AcquisitionStatus {
247 pub active: bool,
248 pub fps: Option<f32>,
249 pub dropped: u64,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub enum PixelFormat {
257 Mono8,
258 Mono10,
259 Mono12,
260 Mono16,
261 BayerRG8,
262 BayerGR8,
263 BayerBG8,
264 BayerGB8,
265 BayerRG10,
266 BayerGR10,
267 BayerBG10,
268 BayerGB10,
269 BayerRG12,
270 BayerGR12,
271 BayerBG12,
272 BayerGB12,
273 BayerRG16,
274 BayerGR16,
275 BayerBG16,
276 BayerGB16,
277 RGB8,
278 BGR8,
279 RGBa8,
280 YCbCr422_8,
281 YCbCr8,
282 #[serde(rename = "Coord3D_C16")]
283 Coord3dC16,
284 #[serde(other)]
285 Unknown,
286}
287
288impl PixelFormat {
289 pub fn bytes_per_pixel(&self) -> f32 {
291 match self {
292 Self::Mono8 | Self::BayerRG8 | Self::BayerGR8 | Self::BayerBG8 | Self::BayerGB8 => 1.0,
293 Self::Mono10
294 | Self::Mono12
295 | Self::Mono16
296 | Self::BayerRG10
297 | Self::BayerGR10
298 | Self::BayerBG10
299 | Self::BayerGB10
300 | Self::BayerRG12
301 | Self::BayerGR12
302 | Self::BayerBG12
303 | Self::BayerGB12
304 | Self::BayerRG16
305 | Self::BayerGR16
306 | Self::BayerBG16
307 | Self::BayerGB16
308 | Self::Coord3dC16 => 2.0,
309 Self::RGB8 | Self::BGR8 | Self::YCbCr8 => 3.0,
310 Self::RGBa8 => 4.0,
311 Self::YCbCr422_8 => 2.0,
312 Self::Unknown => 1.0,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ImageMeta {
320 pub pixel_format: PixelFormat,
321 pub width: u32,
322 pub height: u32,
323 pub payload_size: u64,
324}
325
326pub mod keys {
330 pub const ANNOUNCE_ALL: &str = "genicam/devices/*/announce";
332
333 pub fn announce(device_id: &str) -> String {
334 format!("genicam/devices/{device_id}/announce")
335 }
336
337 pub fn xml(device_id: &str) -> String {
338 format!("genicam/devices/{device_id}/xml")
339 }
340
341 pub fn status(device_id: &str) -> String {
342 format!("genicam/devices/{device_id}/status")
343 }
344
345 pub fn node_value(device_id: &str, node_name: &str) -> String {
346 format!("genicam/devices/{device_id}/nodes/{node_name}/value")
347 }
348
349 pub fn node_value_wildcard(device_id: &str) -> String {
350 format!("genicam/devices/{device_id}/nodes/*/value")
351 }
352
353 pub fn node_set(device_id: &str, node_name: &str) -> String {
354 format!("genicam/devices/{device_id}/nodes/{node_name}/set")
355 }
356
357 pub fn node_execute(device_id: &str, node_name: &str) -> String {
358 format!("genicam/devices/{device_id}/nodes/{node_name}/execute")
359 }
360
361 pub fn node_introspect(device_id: &str, node_name: &str) -> String {
365 format!("genicam/devices/{device_id}/nodes/{node_name}/state")
366 }
367
368 pub fn nodes_bulk_read(device_id: &str) -> String {
371 format!("genicam/devices/{device_id}/nodes/bulk/read")
372 }
373
374 pub fn nodes_bulk_state(device_id: &str) -> String {
378 format!("genicam/devices/{device_id}/nodes/bulk/state")
379 }
380
381 pub fn acquisition_control(device_id: &str) -> String {
382 format!("genicam/devices/{device_id}/acquisition/control")
383 }
384
385 pub fn acquisition_status(device_id: &str) -> String {
386 format!("genicam/devices/{device_id}/acquisition/status")
387 }
388
389 pub fn image(device_id: &str) -> String {
390 format!("genicam/devices/{device_id}/image")
391 }
392
393 pub fn image_meta(device_id: &str) -> String {
394 format!("genicam/devices/{device_id}/image/meta")
395 }
396
397 pub fn extract_node_name(key: &str) -> Option<&str> {
400 let parts: Vec<&str> = key.split('/').collect();
401 if parts.len() >= 6 && parts[parts.len() - 3] == "nodes" {
402 Some(parts[parts.len() - 2])
403 } else {
404 None
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_device_announce_deserializes_without_api_version() {
415 let legacy = r#"{"id":"cam0","name":"Test Cam","model":"M1","serial":"S1"}"#;
416 let a: DeviceAnnounce = serde_json::from_str(legacy).expect("should deserialize");
417 assert!(
418 a.api_version.is_none(),
419 "api_version should be None for legacy JSON"
420 );
421 }
422
423 #[test]
424 fn test_device_announce_deserializes_with_api_version() {
425 let json = r#"{"id":"cam0","name":"Test","model":"M","serial":"S","api_version":1}"#;
426 let a: DeviceAnnounce = serde_json::from_str(json).expect("should deserialize");
427 assert_eq!(a.api_version, Some(1));
428 }
429
430 #[test]
431 fn test_node_value_update_without_constraints() {
432 let u = NodeValueUpdate {
433 value: serde_json::json!(42),
434 access_mode: "RW".to_string(),
435 min: None,
436 max: None,
437 inc: None,
438 };
439 let s = serde_json::to_string(&u).expect("serialization failed");
440 assert!(!s.contains("\"min\""), "min should be absent: {s}");
441 assert!(!s.contains("\"max\""), "max should be absent: {s}");
442 assert!(!s.contains("\"inc\""), "inc should be absent: {s}");
443 assert!(s.contains("\"value\""));
444 assert!(s.contains("\"access_mode\""));
445 }
446
447 #[test]
448 fn test_node_value_update_with_constraints() {
449 let u = NodeValueUpdate {
450 value: serde_json::json!(1024),
451 access_mode: "RW".to_string(),
452 min: Some(1.0),
453 max: Some(4096.0),
454 inc: Some(1.0),
455 };
456 let s = serde_json::to_string(&u).expect("serialization failed");
457 let d: NodeValueUpdate = serde_json::from_str(&s).expect("deserialization failed");
458 assert_eq!(d.min, Some(1.0));
459 assert_eq!(d.max, Some(4096.0));
460 assert_eq!(d.inc, Some(1.0));
461 assert_eq!(d.access_mode, "RW");
462 }
463
464 #[test]
465 fn test_extract_node_name_value_key() {
466 assert_eq!(
467 keys::extract_node_name("genicam/devices/cam0/nodes/Width/value"),
468 Some("Width")
469 );
470 }
471
472 #[test]
473 fn test_extract_node_name_set_key() {
474 assert_eq!(
475 keys::extract_node_name("genicam/devices/cam0/nodes/Width/set"),
476 Some("Width")
477 );
478 }
479
480 #[test]
481 fn test_extract_node_name_execute_key() {
482 assert_eq!(
483 keys::extract_node_name("genicam/devices/cam0/nodes/AcquisitionStart/execute"),
484 Some("AcquisitionStart")
485 );
486 }
487
488 #[test]
489 fn test_extract_node_name_too_short() {
490 assert_eq!(keys::extract_node_name("genicam/devices/cam0"), None);
491 }
492
493 #[test]
494 fn test_extract_node_name_non_node_key() {
495 assert_eq!(
496 keys::extract_node_name("genicam/devices/cam0/acquisition/control/something"),
497 None
498 );
499 }
500
501 #[test]
502 fn test_node_value_update_deserializes_legacy() {
503 let legacy = r#"{"value": 1024, "access_mode": "RW"}"#;
504 let d: NodeValueUpdate = serde_json::from_str(legacy).expect("deserialization failed");
505 assert!(d.min.is_none());
506 assert!(d.max.is_none());
507 assert!(d.inc.is_none());
508 assert_eq!(d.access_mode, "RW");
509 }
510
511 #[test]
514 fn test_feature_state_minimal_roundtrip() {
515 let s = FeatureState {
516 value: serde_json::json!(1920),
517 access_mode: "RW".to_string(),
518 kind: "Integer".to_string(),
519 is_implemented: true,
520 is_available: true,
521 numeric: None,
522 enum_available: None,
523 unit: None,
524 };
525 let j = serde_json::to_string(&s).expect("serialize");
526 assert!(!j.contains("\"numeric\""), "numeric should be absent: {j}");
528 assert!(
529 !j.contains("\"enum_available\""),
530 "enum_available absent: {j}"
531 );
532 assert!(!j.contains("\"unit\""), "unit absent: {j}");
533 let d: FeatureState = serde_json::from_str(&j).expect("deserialize");
534 assert_eq!(d, s);
535 }
536
537 #[test]
538 fn test_feature_state_defaults_when_fields_missing() {
539 let legacy = r#"{"value":1920,"access_mode":"RW","kind":"Integer"}"#;
542 let d: FeatureState = serde_json::from_str(legacy).expect("deserialize");
543 assert!(d.is_implemented, "is_implemented defaults to true");
544 assert!(d.is_available, "is_available defaults to true");
545 }
546
547 #[test]
548 fn test_feature_state_integer_with_range() {
549 let s = FeatureState {
550 value: serde_json::json!(512),
551 access_mode: "RW".to_string(),
552 kind: "Integer".to_string(),
553 is_implemented: true,
554 is_available: true,
555 numeric: Some(NumericRange {
556 min: 16.0,
557 max: 4096.0,
558 inc: Some(8.0),
559 }),
560 enum_available: None,
561 unit: Some("px".to_string()),
562 };
563 let j = serde_json::to_string(&s).expect("serialize");
564 let d: FeatureState = serde_json::from_str(&j).expect("deserialize");
565 assert_eq!(d, s);
566 }
567
568 #[test]
569 fn test_feature_state_to_node_value_update() {
570 let s = FeatureState {
571 value: serde_json::json!(512),
572 access_mode: "RW".to_string(),
573 kind: "Integer".to_string(),
574 is_implemented: true,
575 is_available: true,
576 numeric: Some(NumericRange {
577 min: 16.0,
578 max: 4096.0,
579 inc: Some(8.0),
580 }),
581 enum_available: None,
582 unit: None,
583 };
584 let u: NodeValueUpdate = (&s).into();
585 assert_eq!(u.value, serde_json::json!(512));
586 assert_eq!(u.access_mode, "RW");
587 assert_eq!(u.min, Some(16.0));
588 assert_eq!(u.max, Some(4096.0));
589 assert_eq!(u.inc, Some(8.0));
590 }
591
592 #[test]
593 fn test_feature_state_enumeration() {
594 let s = FeatureState {
595 value: serde_json::json!("Once"),
596 access_mode: "RW".to_string(),
597 kind: "Enumeration".to_string(),
598 is_implemented: true,
599 is_available: true,
600 numeric: None,
601 enum_available: Some(vec!["Off".to_string(), "Once".to_string()]),
602 unit: None,
603 };
604 let j = serde_json::to_string(&s).expect("serialize");
605 assert!(j.contains("\"enum_available\""));
606 let d: FeatureState = serde_json::from_str(&j).expect("deserialize");
607 assert_eq!(
608 d.enum_available.as_deref(),
609 Some(&["Off".to_string(), "Once".to_string()][..])
610 );
611 }
612
613 #[test]
614 fn test_command_result_ok_empty() {
615 let r = CommandResult {
616 ok: true,
617 error: None,
618 affected_states: std::collections::HashMap::new(),
619 };
620 let j = serde_json::to_string(&r).expect("serialize");
621 assert!(
622 !j.contains("\"affected_states\""),
623 "empty affected_states should be skipped: {j}"
624 );
625 assert!(
626 !j.contains("\"error\""),
627 "none error should be skipped: {j}"
628 );
629 }
630
631 #[test]
632 fn test_command_result_with_affected_states() {
633 let mut affected = std::collections::HashMap::new();
634 affected.insert(
635 "AcquisitionStatus".to_string(),
636 FeatureState {
637 value: serde_json::json!(true),
638 access_mode: "RO".to_string(),
639 kind: "Boolean".to_string(),
640 is_implemented: true,
641 is_available: true,
642 numeric: None,
643 enum_available: None,
644 unit: None,
645 },
646 );
647 let r = CommandResult {
648 ok: true,
649 error: None,
650 affected_states: affected,
651 };
652 let j = serde_json::to_string(&r).expect("serialize");
653 let d: CommandResult = serde_json::from_str(&j).expect("deserialize");
654 assert!(d.ok);
655 assert_eq!(d.affected_states.len(), 1);
656 assert!(d.affected_states.contains_key("AcquisitionStatus"));
657 }
658}