use super::{FloatRange, xml_escape, xml_str};
use crate::error::OnvifError;
use crate::soap::{SoapError, XmlNode};
#[derive(Debug, Clone, Default)]
pub struct ImagingSettings {
pub brightness: Option<f32>,
pub color_saturation: Option<f32>,
pub contrast: Option<f32>,
pub sharpness: Option<f32>,
pub ir_cut_filter: Option<String>,
pub white_balance_mode: Option<String>,
pub exposure_mode: Option<String>,
pub backlight_compensation: Option<String>,
pub focus_mode: Option<String>,
pub focus_default_speed: Option<f32>,
pub wide_dynamic_range_mode: Option<String>,
pub wide_dynamic_range_level: Option<f32>,
pub image_stabilization_mode: Option<String>,
pub tone_compensation_mode: Option<String>,
}
impl ImagingSettings {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let s = resp
.child("ImagingSettings")
.ok_or_else(|| SoapError::missing("ImagingSettings"))?;
let parse_f32 = |child: &str| s.child(child).and_then(|n| n.text().parse::<f32>().ok());
Ok(Self {
brightness: parse_f32("Brightness"),
color_saturation: parse_f32("ColorSaturation"),
contrast: parse_f32("Contrast"),
sharpness: parse_f32("Sharpness"),
ir_cut_filter: xml_str(s, "IrCutFilter").filter(|v| !v.is_empty()),
white_balance_mode: s
.path(&["WhiteBalance", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
exposure_mode: s
.path(&["Exposure", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
backlight_compensation: s
.path(&["BacklightCompensation", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
focus_mode: s
.path(&["Focus", "AutoFocusMode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
focus_default_speed: s
.path(&["Focus", "DefaultSpeed"])
.and_then(|n| n.text().parse().ok()),
wide_dynamic_range_mode: s
.path(&["WideDynamicRange", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
wide_dynamic_range_level: s
.path(&["WideDynamicRange", "Level"])
.and_then(|n| n.text().parse().ok()),
image_stabilization_mode: s
.path(&["ImageStabilization", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
tone_compensation_mode: s
.path(&["ToneCompensation", "Mode"])
.map(|n| n.text().to_string())
.filter(|v| !v.is_empty()),
})
}
pub(crate) fn to_xml_body(&self) -> String {
let mut out = String::from("<timg:ImagingSettings>");
if let Some(v) = self.brightness {
out.push_str(&format!("<tt:Brightness>{v}</tt:Brightness>"));
}
if let Some(v) = self.color_saturation {
out.push_str(&format!("<tt:ColorSaturation>{v}</tt:ColorSaturation>"));
}
if let Some(v) = self.contrast {
out.push_str(&format!("<tt:Contrast>{v}</tt:Contrast>"));
}
if let Some(v) = self.sharpness {
out.push_str(&format!("<tt:Sharpness>{v}</tt:Sharpness>"));
}
if let Some(ref v) = self.ir_cut_filter {
out.push_str(&format!(
"<tt:IrCutFilter>{}</tt:IrCutFilter>",
xml_escape(v)
));
}
if let Some(ref m) = self.white_balance_mode {
out.push_str(&format!(
"<tt:WhiteBalance><tt:Mode>{}</tt:Mode></tt:WhiteBalance>",
xml_escape(m)
));
}
if let Some(ref m) = self.exposure_mode {
out.push_str(&format!(
"<tt:Exposure><tt:Mode>{}</tt:Mode></tt:Exposure>",
xml_escape(m)
));
}
if let Some(ref m) = self.backlight_compensation {
out.push_str(&format!(
"<tt:BacklightCompensation><tt:Mode>{}</tt:Mode></tt:BacklightCompensation>",
xml_escape(m)
));
}
if self.focus_mode.is_some() || self.focus_default_speed.is_some() {
out.push_str("<tt:Focus>");
if let Some(ref m) = self.focus_mode {
out.push_str(&format!(
"<tt:AutoFocusMode>{}</tt:AutoFocusMode>",
xml_escape(m)
));
}
if let Some(v) = self.focus_default_speed {
out.push_str(&format!("<tt:DefaultSpeed>{v}</tt:DefaultSpeed>"));
}
out.push_str("</tt:Focus>");
}
if self.wide_dynamic_range_mode.is_some() || self.wide_dynamic_range_level.is_some() {
out.push_str("<tt:WideDynamicRange>");
if let Some(ref m) = self.wide_dynamic_range_mode {
out.push_str(&format!("<tt:Mode>{}</tt:Mode>", xml_escape(m)));
}
if let Some(v) = self.wide_dynamic_range_level {
out.push_str(&format!("<tt:Level>{v}</tt:Level>"));
}
out.push_str("</tt:WideDynamicRange>");
}
if let Some(ref m) = self.image_stabilization_mode {
out.push_str(&format!(
"<tt:ImageStabilization><tt:Mode>{}</tt:Mode></tt:ImageStabilization>",
xml_escape(m)
));
}
if let Some(ref m) = self.tone_compensation_mode {
out.push_str(&format!(
"<tt:ToneCompensation><tt:Mode>{}</tt:Mode></tt:ToneCompensation>",
xml_escape(m)
));
}
out.push_str("</timg:ImagingSettings>");
out
}
}
#[derive(Debug, Clone, Default)]
pub struct ImagingOptions {
pub brightness: Option<FloatRange>,
pub color_saturation: Option<FloatRange>,
pub contrast: Option<FloatRange>,
pub sharpness: Option<FloatRange>,
pub ir_cut_filter_modes: Vec<String>,
pub white_balance_modes: Vec<String>,
pub exposure_modes: Vec<String>,
pub exposure_time_range: Option<FloatRange>,
pub gain_range: Option<FloatRange>,
pub iris_range: Option<FloatRange>,
pub focus_af_modes: Vec<String>,
pub focus_speed_range: Option<FloatRange>,
pub wdr_level_range: Option<FloatRange>,
pub wdr_modes: Vec<String>,
pub backlight_compensation_modes: Vec<String>,
}
impl ImagingOptions {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let opts = resp
.child("ImagingOptions")
.ok_or_else(|| SoapError::missing("ImagingOptions"))?;
let parse_range = |child: &str| {
opts.child(child).map(|n| FloatRange {
min: n
.child("Min")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
max: n
.child("Max")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
})
};
let parse_nested_range = |parent: Option<&XmlNode>, child: &str| {
parent.and_then(|p| p.child(child)).map(|n| FloatRange {
min: n
.child("Min")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
max: n
.child("Max")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
})
};
let exposure = opts.child("Exposure");
let focus = opts.child("Focus");
let wdr = opts.child("WideDynamicRange");
let blc = opts.child("BacklightCompensation");
Ok(Self {
brightness: parse_range("Brightness"),
color_saturation: parse_range("ColorSaturation"),
contrast: parse_range("Contrast"),
sharpness: parse_range("Sharpness"),
ir_cut_filter_modes: opts
.children_named("IrCutFilterModes")
.map(|n| n.text().to_string())
.collect(),
white_balance_modes: opts
.child("WhiteBalance")
.map(|wb| {
wb.children_named("Mode")
.map(|n| n.text().to_string())
.collect()
})
.unwrap_or_default(),
exposure_modes: exposure
.map(|e| {
e.children_named("Mode")
.map(|n| n.text().to_string())
.collect()
})
.unwrap_or_default(),
exposure_time_range: parse_nested_range(exposure, "ExposureTime"),
gain_range: parse_nested_range(exposure, "Gain"),
iris_range: parse_nested_range(exposure, "Iris"),
focus_af_modes: focus
.map(|f| {
f.children_named("AutoFocusModes")
.map(|n| n.text().to_string())
.collect()
})
.unwrap_or_default(),
focus_speed_range: parse_nested_range(focus, "DefaultSpeed"),
wdr_level_range: parse_nested_range(wdr, "Level"),
wdr_modes: wdr
.map(|w| {
w.children_named("Mode")
.map(|n| n.text().to_string())
.collect()
})
.unwrap_or_default(),
backlight_compensation_modes: blc
.map(|b| {
b.children_named("Mode")
.map(|n| n.text().to_string())
.collect()
})
.unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ImagingStatus {
pub focus_position: Option<f32>,
pub focus_move_status: String,
}
impl ImagingStatus {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let status = resp
.child("Status")
.ok_or_else(|| SoapError::missing("Status"))?;
Ok(Self {
focus_position: status
.path(&["FocusStatus20", "Position"])
.and_then(|n| n.text().parse().ok()),
focus_move_status: status
.path(&["FocusStatus20", "MoveStatus"])
.map(|n| n.text().to_string())
.unwrap_or_else(|| "UNKNOWN".to_string()),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ImagingMoveOptions {
pub absolute_position_range: Option<FloatRange>,
pub absolute_speed_range: Option<FloatRange>,
pub relative_distance_range: Option<FloatRange>,
pub relative_speed_range: Option<FloatRange>,
pub continuous_speed_range: Option<FloatRange>,
}
impl ImagingMoveOptions {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let opts = resp
.child("MoveOptions")
.ok_or_else(|| SoapError::missing("MoveOptions"))?;
let range = |parent: &str, child: &str| {
opts.child(parent)
.and_then(|p| p.child(child))
.map(|n| FloatRange {
min: n
.child("Min")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
max: n
.child("Max")
.and_then(|m| m.text().parse().ok())
.unwrap_or(0.0),
})
};
Ok(Self {
absolute_position_range: range("Absolute", "PositionSpace"),
absolute_speed_range: range("Absolute", "SpeedSpace"),
relative_distance_range: range("Relative", "DistanceSpace"),
relative_speed_range: range("Relative", "SpeedSpace"),
continuous_speed_range: range("Continuous", "SpeedSpace"),
})
}
}
#[derive(Debug, Clone)]
pub enum FocusMove {
Absolute {
position: f32,
speed: Option<f32>,
},
Relative {
distance: f32,
speed: Option<f32>,
},
Continuous {
speed: f32,
},
}
impl FocusMove {
pub(crate) fn to_xml_body(&self) -> String {
match self {
Self::Absolute { position, speed } => {
let speed_el = speed
.map(|s| format!("<timg:Speed>{s}</timg:Speed>"))
.unwrap_or_default();
format!(
"<timg:Absolute>\
<timg:Position>{position}</timg:Position>\
{speed_el}\
</timg:Absolute>"
)
}
Self::Relative { distance, speed } => {
let speed_el = speed
.map(|s| format!("<timg:Speed>{s}</timg:Speed>"))
.unwrap_or_default();
format!(
"<timg:Relative>\
<timg:Distance>{distance}</timg:Distance>\
{speed_el}\
</timg:Relative>"
)
}
Self::Continuous { speed } => {
format!("<timg:Continuous><timg:Speed>{speed}</timg:Speed></timg:Continuous>")
}
}
}
}