1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
use super::xml_str;
use crate::error::OnvifError;
use crate::soap::{SoapError, XmlNode};
// ── PtzPreset ─────────────────────────────────────────────────────────────────
/// A named PTZ preset position returned by `GetPresets`.
#[derive(Debug, Clone)]
pub struct PtzPreset {
/// Opaque preset identifier; pass to `ptz_goto_preset`.
pub token: String,
/// Human-readable preset name.
pub name: String,
/// Stored pan (x) and tilt (y) position, range `[-1.0, 1.0]`.
/// `None` if the preset has no stored position.
pub pan_tilt: Option<(f32, f32)>,
/// Stored zoom position, range `[0.0, 1.0]`.
/// `None` if the preset has no stored zoom.
pub zoom: Option<f32>,
}
impl PtzPreset {
/// Parse all `<Preset>` children from a `GetPresetsResponse` node.
pub(crate) fn vec_from_xml(resp: &XmlNode) -> Result<Vec<Self>, OnvifError> {
resp.children_named("Preset")
.map(|p| {
let token = p
.attr("token")
.filter(|t| !t.is_empty())
.ok_or_else(|| SoapError::missing("Preset/@token"))?
.to_string();
Ok(Self {
token,
name: xml_str(p, "Name").unwrap_or_default(),
pan_tilt: p.path(&["PTZPosition", "PanTilt"]).and_then(|n| {
let x = n.attr("x")?.parse().ok()?;
let y = n.attr("y")?.parse().ok()?;
Some((x, y))
}),
zoom: p
.path(&["PTZPosition", "Zoom"])
.and_then(|n| n.attr("x")?.parse().ok()),
})
})
.collect()
}
}
// ── PtzStatus ─────────────────────────────────────────────────────────────────
/// Current PTZ position and movement state returned by `GetStatus`.
#[derive(Debug, Clone)]
pub struct PtzStatus {
/// Current pan position in the normalised range `[-1.0, 1.0]`.
/// `None` if the device did not report a position.
pub pan: Option<f32>,
/// Current tilt position in the normalised range `[-1.0, 1.0]`.
/// `None` if the device did not report a position.
pub tilt: Option<f32>,
/// Current zoom position in the normalised range `[0.0, 1.0]`.
/// `None` if the device did not report a position.
pub zoom: Option<f32>,
/// Pan/tilt movement state (e.g. `"IDLE"`, `"MOVING"`, `"UNKNOWN"`).
pub pan_tilt_status: String,
/// Zoom movement state (e.g. `"IDLE"`, `"MOVING"`, `"UNKNOWN"`).
pub zoom_status: String,
/// UTC timestamp of this status snapshot, if reported by the device.
pub utc_time: Option<String>,
/// Human-readable error description from `PTZStatus/Error`, if present.
pub error: Option<String>,
}
impl PtzStatus {
/// Parse from a `GetStatusResponse` node.
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let status = resp
.child("PTZStatus")
.ok_or_else(|| SoapError::missing("PTZStatus"))?;
let (pan, tilt) = status
.path(&["Position", "PanTilt"])
.and_then(|n| {
let x = n.attr("x")?.parse().ok()?;
let y = n.attr("y")?.parse().ok()?;
Some((Some(x), Some(y)))
})
.unwrap_or((None, None));
let zoom = status
.path(&["Position", "Zoom"])
.and_then(|n| n.attr("x")?.parse().ok());
Ok(Self {
pan,
tilt,
zoom,
pan_tilt_status: status
.path(&["MoveStatus", "PanTilt"])
.map(|n| n.text().to_string())
.unwrap_or_default(),
zoom_status: status
.path(&["MoveStatus", "Zoom"])
.map(|n| n.text().to_string())
.unwrap_or_default(),
utc_time: status.child("UtcTime").map(|n| n.text().to_string()),
error: status.child("Error").map(|n| n.text().to_string()),
})
}
}