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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
//! Peripheral drivers, cameras, buttons, GPIO, display and radio aux-stream calls.
use ados_protocol::camera_control::{
CameraControlRequest, CameraControlResult, CAMERA_CONTROL_CAPABILITY, CAMERA_CONTROL_METHOD,
ENGINE_WAIT_MS,
};
use super::*;
/// How long a camera control call waits: the engine's own wait
/// ([`ENGINE_WAIT_MS`]) plus a second for the two socket hops, so the
/// engine's answer arrives first.
const CAMERA_CONTROL_TIMEOUT: Duration = Duration::from_millis(ENGINE_WAIT_MS + 1_000);
impl PluginIpcClient {
/// Register a driver of the given kind, referenced by an opaque id the host
/// records. The required cap comes from [`driver_kind_capability`]
/// (camera, lidar, or the payload cap for gimbal/gps/esc/payload-actuator);
/// an unknown kind is refused here and nothing is sent. Mirrors
/// `peripheral_register_driver`.
pub async fn peripheral_register_driver(
&self,
kind: &str,
driver_ref: &str,
) -> Result<Value, ClientError> {
let cap = driver_kind_capability(kind)
.ok_or_else(|| ClientError::Rpc(format!("unknown driver kind: {kind}")))?;
let args = Value::Map(vec![
(Value::from("kind"), Value::from(kind)),
(Value::from("driver_ref"), Value::from(driver_ref)),
]);
Ok(self
.send_request("peripheral.register_driver", cap, args)
.await?
.args)
}
/// Release a previously-registered driver by handle. Mirrors
/// `peripheral_unregister_driver`.
pub async fn peripheral_unregister_driver(
&self,
handle_id: &str,
) -> Result<Value, ClientError> {
let args = Value::Map(vec![(Value::from("handle_id"), Value::from(handle_id))]);
Ok(self
.send_request("peripheral.unregister_driver", "", args)
.await?
.args)
}
/// Claim a `/dev/videoN` path, optionally exclusive. Mirrors `camera_claim`.
pub async fn camera_claim(
&self,
device_path: &str,
exclusive: bool,
) -> Result<Value, ClientError> {
let args = Value::Map(vec![
(Value::from("device_path"), Value::from(device_path)),
(Value::from("exclusive"), Value::Boolean(exclusive)),
]);
Ok(self
.send_request("camera.claim", "sensor.camera.register", args)
.await?
.args)
}
/// Release a previously-claimed camera path. Mirrors `camera_release`.
pub async fn camera_release(&self, device_path: &str) -> Result<Value, ClientError> {
let args = Value::Map(vec![(Value::from("device_path"), Value::from(device_path))]);
Ok(self
.send_request("camera.release", "sensor.camera.register", args)
.await?
.args)
}
/// Pull the latest captured frame from a claimed camera path. Mirrors
/// `camera_get_frame`.
pub async fn camera_get_frame(
&self,
device_path: &str,
format: &str,
timeout_ms: i64,
) -> Result<Value, ClientError> {
let args = Value::Map(vec![
(Value::from("device_path"), Value::from(device_path)),
(Value::from("format"), Value::from(format)),
(Value::from("timeout_ms"), Value::from(timeout_ms)),
]);
Ok(self
.send_request("camera.get_frame", "sensor.camera.register", args)
.await?
.args)
}
/// Set exposure, gain (a multiplier, 1.0 is unity) or auto exposure on a
/// `capture: true` camera the vision engine owns and return what the camera
/// reads back. Gated on `camera.control.write`
/// ([`ClientError::CapabilityDenied`] without it). `applied` is true only
/// when every requested control was set and reads back as asked; any other
/// camera, a simulated or control-less one, or a gain control with no known
/// unit gain answers `applied: false` with a
/// [`reason`](ados_protocol::camera_control::reason) code.
pub async fn camera_set_controls(
&self,
req: &CameraControlRequest,
) -> Result<CameraControlResult, ClientError> {
let reply = self
.send_request_within(
CAMERA_CONTROL_METHOD,
CAMERA_CONTROL_CAPABILITY,
req.to_args(),
CAMERA_CONTROL_TIMEOUT,
)
.await?;
decode_typed_reply(reply.args, CAMERA_CONTROL_METHOD)
}
}
impl PluginIpcClient {
/// Register a callback for `button.deliver` pushes.
pub fn register_button_callback(&self, callback: EventCallback) {
register_callback(&self.button_callbacks, VISION_ANY_CAMERA, callback);
}
}
impl PluginIpcClient {
/// Arm the host's front-panel button push stream for this connection.
pub async fn button_subscribe(&self) -> Result<Value, ClientError> {
Ok(self
.send_request(
ados_protocol::buttons::SUBSCRIBE,
ados_protocol::buttons::SUBSCRIBE,
Value::Map(vec![]),
)
.await?
.args)
}
/// Drive a host GPIO output line high or low.
pub async fn gpio_output_set(
&self,
chip: i64,
pin: i64,
high: bool,
) -> Result<Value, ClientError> {
let args = Value::Map(vec![
(Value::from("chip"), Value::Integer(chip.into())),
(Value::from("pin"), Value::Integer(pin.into())),
(
Value::from("level"),
Value::from(if high { "high" } else { "low" }),
),
]);
Ok(self
.send_request("gpio.output.set", "hardware.gpio_out", args)
.await?
.args)
}
/// Play a bounded beep pattern on a host GPIO line.
///
/// The gpio service is the single owner of the safe bounds and clamps the
/// pattern itself, so this sends the requested values verbatim rather than
/// pre-clamping them to a second, drifting opinion of what is safe.
pub async fn gpio_buzzer_beep(
&self,
chip: i64,
pin: i64,
on_ms: i64,
cycles: i64,
off_ms: Option<i64>,
freq_hz: Option<i64>,
) -> Result<Value, ClientError> {
let mut entries = vec![
(Value::from("chip"), Value::Integer(chip.into())),
(Value::from("pin"), Value::Integer(pin.into())),
(Value::from("on_ms"), Value::Integer(on_ms.into())),
(Value::from("cycles"), Value::Integer(cycles.into())),
];
if let Some(v) = off_ms {
entries.push((Value::from("off_ms"), Value::Integer(v.into())));
}
if let Some(v) = freq_hz {
entries.push((Value::from("freq_hz"), Value::Integer(v.into())));
}
Ok(self
.send_request("gpio.buzzer.beep", "hardware.gpio_out", Value::Map(entries))
.await?
.args)
}
/// Set the content of the host's reserved data-driven display page.
///
/// `rows` are `(label, value)` pairs and `zones` are
/// `(x, y, w, h, key, label)` touch rectangles in page-local content
/// coordinates. The host writes the page sidecar the display service renders.
pub async fn display_page_set(
&self,
title: &str,
rows: &[(String, String)],
zones: &[(i64, i64, i64, i64, String, String)],
) -> Result<Value, ClientError> {
let rows_v: Vec<Value> = rows
.iter()
.map(|(label, value)| {
Value::Map(vec![
(Value::from("label"), Value::from(label.as_str())),
(Value::from("value"), Value::from(value.as_str())),
])
})
.collect();
let zones_v: Vec<Value> = zones
.iter()
.map(|(x, y, w, h, key, label)| {
Value::Map(vec![
(Value::from("x"), Value::Integer((*x).into())),
(Value::from("y"), Value::Integer((*y).into())),
(Value::from("w"), Value::Integer((*w).into())),
(Value::from("h"), Value::Integer((*h).into())),
(Value::from("key"), Value::from(key.as_str())),
(Value::from("label"), Value::from(label.as_str())),
])
})
.collect();
let args = Value::Map(vec![
(Value::from("title"), Value::from(title)),
(Value::from("rows"), Value::Array(rows_v)),
(Value::from("zones"), Value::Array(zones_v)),
]);
Ok(self
.send_request("display.page.set", "display.oled.page", args)
.await?
.args)
}
/// Open the additive auxiliary application stream on the radio link.
pub async fn radio_aux_stream_open(&self) -> Result<Value, ClientError> {
Ok(self
.send_request(
"radio.aux_stream.open",
"radio.aux_stream",
Value::Map(vec![]),
)
.await?
.args)
}
/// Close the auxiliary application stream.
pub async fn radio_aux_stream_close(&self) -> Result<Value, ClientError> {
Ok(self
.send_request(
"radio.aux_stream.close",
"radio.aux_stream",
Value::Map(vec![]),
)
.await?
.args)
}
/// Send one application datagram on the open auxiliary stream.
///
/// `channel` is the reserved aux application channel: `AppStream` (8, the
/// stream direction) or `AppCommand` (9, the command direction). The payload
/// is the application bytes, opaque to the host; a payload over
/// [`ados_protocol::aux_mux::AUX_MAX_PAYLOAD`] is refused by the host before
/// it is sent. Gated on `radio.aux_stream`.
pub async fn radio_aux_stream_send(
&self,
channel: u8,
payload: &[u8],
) -> Result<Value, ClientError> {
let args = Value::Map(vec![
(Value::from("channel"), Value::Integer(channel.into())),
(Value::from("payload"), Value::Binary(payload.to_vec())),
]);
Ok(self
.send_request("radio.aux_stream.send", "radio.aux_stream", args)
.await?
.args)
}
/// Subscribe to application datagrams received on the open auxiliary stream.
///
/// The host forwards each received `(channel, payload)` as a
/// `radio.aux_stream.deliver` event to every registered `aux` callback
/// (the plugin's codec routes by channel). Register a callback first with
/// [`Self::register_aux_callback`] so no early frame is missed. Gated on
/// `radio.aux_stream`. Mirrors the button subscribe shape.
pub async fn radio_aux_stream_subscribe(&self) -> Result<Value, ClientError> {
Ok(self
.send_request(
"radio.aux_stream.subscribe",
"radio.aux_stream",
Value::Map(vec![]),
)
.await?
.args)
}
/// Register a callback for `radio.aux_stream.deliver` pushes. Mirrors the
/// button callback shape: every subscriber receives every application
/// datagram.
pub fn register_aux_callback(&self, callback: EventCallback) {
register_callback(&self.aux_callbacks, VISION_ANY_CAMERA, callback);
}
}