Skip to main content

ados_sdk/client/
peripheral.rs

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