use rmpv::Value;
use serde::{Deserialize, Serialize};
pub const CAMERA_CONTROL_METHOD: &str = "camera.control.set";
pub const CAMERA_CONTROL_CAPABILITY: &str = "camera.control.write";
pub const ENGINE_WAIT_MS: u64 = 1_500;
pub mod reason {
pub const NOT_V4L2: &str = "not_v4l2";
pub const CONTROL_UNSUPPORTED: &str = "control_unsupported";
pub const UNKNOWN_CAMERA: &str = "unknown_camera";
pub const NOT_CAPTURE_CAMERA: &str = "not_capture_camera";
pub const GAIN_UNIT_UNKNOWN: &str = "gain_unit_unknown";
pub const NOT_STREAMING: &str = "not_streaming";
pub const OUT_OF_RANGE: &str = "out_of_range";
pub const SET_FAILED: &str = "set_failed";
pub const READBACK_MISMATCH: &str = "readback_mismatch";
pub const CONFLICTING_REQUEST: &str = "conflicting_request";
pub const INVALID_VALUE: &str = "invalid_value";
pub const EMPTY_REQUEST: &str = "empty_request";
pub const BUSY: &str = "busy";
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CameraControlRequest {
pub camera_id: String,
#[serde(default)]
pub exposure_us: Option<u32>,
#[serde(default)]
pub gain: Option<f32>,
#[serde(default)]
pub auto_exposure: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CameraControlResult {
pub applied: bool,
#[serde(default)]
pub exposure_us: Option<u32>,
#[serde(default)]
pub gain: Option<f32>,
#[serde(default)]
pub reason: Option<String>,
}
impl CameraControlResult {
pub fn refused(reason: &str) -> Self {
Self {
applied: false,
exposure_us: None,
gain: None,
reason: Some(reason.to_string()),
}
}
}
fn decode<T: serde::de::DeserializeOwned>(value: &Value, what: &str) -> Result<T, String> {
rmpv::ext::from_value(value.clone()).map_err(|e| format!("{what}: {e}"))
}
fn map(entries: Vec<(&str, Option<Value>)>) -> Value {
Value::Map(
entries
.into_iter()
.filter_map(|(k, v)| v.map(|v| (Value::from(k), v)))
.collect(),
)
}
impl CameraControlRequest {
pub fn to_args(&self) -> Value {
map(vec![
("camera_id", Some(Value::from(self.camera_id.as_str()))),
("exposure_us", self.exposure_us.map(Value::from)),
("gain", self.gain.map(Value::from)),
("auto_exposure", self.auto_exposure.map(Value::from)),
])
}
pub fn from_args(args: &Value) -> Result<Self, String> {
decode(args, "camera.control.set arguments are invalid")
}
}
impl CameraControlResult {
pub fn to_value(&self) -> Value {
map(vec![
("applied", Some(Value::from(self.applied))),
("exposure_us", self.exposure_us.map(Value::from)),
("gain", self.gain.map(Value::from)),
("reason", self.reason.as_deref().map(Value::from)),
])
}
pub fn from_value(value: &Value) -> Result<Self, String> {
decode(value, "camera.control.set reply did not decode")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_request_round_trips_and_omits_absent_fields() {
let req = CameraControlRequest {
camera_id: "cam_fl".into(),
exposure_us: Some(2_000),
gain: Some(4.0),
auto_exposure: None,
};
let args = req.to_args();
let Value::Map(entries) = &args else {
panic!("a map")
};
assert!(!entries
.iter()
.any(|(k, _)| k.as_str() == Some("auto_exposure")));
assert_eq!(CameraControlRequest::from_args(&args).unwrap(), req);
}
#[test]
fn a_request_without_optional_fields_decodes() {
let args = Value::Map(vec![(Value::from("camera_id"), Value::from("c"))]);
let req = CameraControlRequest::from_args(&args).unwrap();
assert_eq!(req.camera_id, "c");
assert_eq!(
(req.exposure_us, req.gain, req.auto_exposure),
(None, None, None)
);
let args = Value::Map(vec![
(Value::from("camera_id"), Value::from("c")),
(Value::from("gain"), Value::from(2)),
]);
assert_eq!(
CameraControlRequest::from_args(&args).unwrap().gain,
Some(2.0)
);
assert!(CameraControlRequest::from_args(&Value::Map(vec![])).is_err());
}
#[test]
fn a_result_round_trips() {
let applied = CameraControlResult {
applied: true,
exposure_us: Some(2_000),
gain: Some(4.0),
reason: None,
};
assert_eq!(
CameraControlResult::from_value(&applied.to_value()).unwrap(),
applied
);
let refused = CameraControlResult::refused(reason::NOT_V4L2);
assert_eq!(
CameraControlResult::from_value(&refused.to_value()).unwrap(),
refused
);
}
}