ados_sdk/client/mavlink.rs
1//! MAVLink, MSP, telemetry and flight-setpoint calls.
2
3use super::*;
4
5impl PluginIpcClient {
6 /// Send a MAVLink frame, optionally from a registered component id.
7 /// Mirrors `mavlink_send`.
8 pub async fn mavlink_send(
9 &self,
10 msg_bytes: &[u8],
11 component_id: Option<i64>,
12 ) -> Result<Value, ClientError> {
13 let mut entries = vec![(Value::from("msg_bytes"), Value::Binary(msg_bytes.to_vec()))];
14 if let Some(comp) = component_id {
15 entries.push((Value::from("component_id"), Value::from(comp)));
16 }
17 Ok(self
18 .send_request("mavlink.send", "mavlink.write", Value::Map(entries))
19 .await?
20 .args)
21 }
22
23 /// Register a MAVLink component of the given kind (camera, gimbal, payload,
24 /// peripheral, vio). The required cap is `mavlink.component.<kind>`. Mirrors
25 /// `mavlink_register_component`.
26 pub async fn mavlink_register_component(
27 &self,
28 comp_id: i64,
29 kind: &str,
30 ) -> Result<Value, ClientError> {
31 let args = Value::Map(vec![
32 (Value::from("component_id"), Value::from(comp_id)),
33 (Value::from("kind"), Value::from(kind)),
34 ]);
35 let cap = format!("mavlink.component.{kind}");
36 Ok(self
37 .send_request("mavlink.register_component", &cap, args)
38 .await?
39 .args)
40 }
41
42 /// Subscribe to a MAVLink message name and register a callback. MAVLink
43 /// deliveries arrive as `mavlink.deliver` envelopes and route by
44 /// `msg_name`. Mirrors `mavlink_subscribe`.
45 pub async fn mavlink_subscribe(
46 &self,
47 msg_name: &str,
48 callback: EventCallback,
49 ) -> Result<(), ClientError> {
50 register_callback(&self.mavlink_callbacks, msg_name, callback);
51 let args = Value::Map(vec![(Value::from("msg_name"), Value::from(msg_name))]);
52 self.send_request("mavlink.subscribe", "mavlink.read", args)
53 .await?;
54 Ok(())
55 }
56
57 /// Write raw MSP bytes (already framed by `ados_protocol::msp`) to the FC.
58 /// Gated on `msp.write`. The reply is `{sent, len}` or a `not_available`
59 /// map when no MSP FC is wired.
60 pub async fn msp_send(&self, msg_bytes: &[u8]) -> Result<Value, ClientError> {
61 let args = Value::Map(vec![(
62 Value::from("msg_bytes"),
63 Value::Binary(msg_bytes.to_vec()),
64 )]);
65 Ok(self.send_request("msp.send", "msp.write", args).await?.args)
66 }
67
68 /// Subscribe to the raw FC->host MSP byte stream and register a callback.
69 /// Deliveries arrive as `msp.deliver` events carrying `{bytes, timestamp_ms}`;
70 /// MSP has no per-message topic, so the callback sees every chunk and its own
71 /// codec parses it. Gated on `msp.read`. Mirrors the button subscribe shape.
72 pub async fn msp_subscribe(&self, callback: EventCallback) -> Result<(), ClientError> {
73 register_callback(&self.msp_callbacks, VISION_ANY_CAMERA, callback);
74 self.send_request("msp.subscribe", "msp.read", Value::Map(vec![]))
75 .await?;
76 Ok(())
77 }
78
79 /// Extend the heartbeat schema with a new channel. Mirrors
80 /// `telemetry_extend`.
81 pub async fn telemetry_extend(
82 &self,
83 channel: &str,
84 payload: Value,
85 ) -> Result<Value, ClientError> {
86 let args = Value::Map(vec![
87 (Value::from("channel"), Value::from(channel)),
88 (Value::from("payload"), payload),
89 ]);
90 Ok(self
91 .send_request("telemetry.extend", "telemetry.extend", args)
92 .await?
93 .args)
94 }
95
96 /// Subscribe to the vehicle-state snapshot and register a callback. The
97 /// host pushes the state object as an event on [`TELEMETRY_STATE_TOPIC`],
98 /// at most [`TELEMETRY_STATE_MAX_HZ`](ados_protocol::plugin::TELEMETRY_STATE_MAX_HZ)
99 /// times a second and always the newest; the callback receives the event's
100 /// args map (`topic`, `payload`, `publisher`, `timestamp_ms`) like any event.
101 /// Gated on `telemetry.read`. A node with no flight controller never fires
102 /// it.
103 pub async fn telemetry_subscribe(&self, callback: EventCallback) -> Result<(), ClientError> {
104 register_callback(&self.event_callbacks, TELEMETRY_STATE_TOPIC, callback);
105 self.send_request("telemetry.subscribe", "telemetry.read", Value::Map(vec![]))
106 .await?;
107 Ok(())
108 }
109}
110
111impl PluginIpcClient {
112 /// Send one application payload over a MAVLink TUNNEL frame.
113 ///
114 /// `payload_type` must be a private type (> 32767); the host validates it
115 /// before anything is sent.
116 pub async fn mavlink_tunnel_send(
117 &self,
118 payload_type: u16,
119 payload: &[u8],
120 ) -> Result<Value, ClientError> {
121 let args = Value::Map(vec![
122 (
123 Value::from("payload_type"),
124 Value::Integer(payload_type.into()),
125 ),
126 (Value::from("payload"), Value::Binary(payload.to_vec())),
127 ]);
128 Ok(self
129 .send_request("mavlink.tunnel.send", "mavlink.tunnel", args)
130 .await?
131 .args)
132 }
133
134 /// Send one guided-mode setpoint through the scoped sender.
135 ///
136 /// `args` is the setpoint map the host validates (`kind`, `coordinate_frame`,
137 /// `type_mask` and the axis fields). Single-shot by design: the host owns no
138 /// flight mode and no schedule, so a caller holding a velocity re-sends above
139 /// the autopilot's setpoint timeout or the vehicle brakes.
140 pub async fn flight_guided_setpoint(&self, args: Value) -> Result<Value, ClientError> {
141 Ok(self
142 .send_request(
143 "flight.guided_setpoint.send",
144 "flight.guided_setpoint",
145 args,
146 )
147 .await?
148 .args)
149 }
150
151 /// Send one attitude/body-rate + thrust setpoint (SET_ATTITUDE_TARGET)
152 /// through the scoped sender.
153 ///
154 /// `args` is the setpoint map the host validates (`type_mask`, `qw`/`qx`/
155 /// `qy`/`qz`, `body_*_rate`, `thrust`, optional `target_system`/
156 /// `target_component`). Single-shot by design, like the guided sender: the
157 /// host owns no flight mode and no schedule, so a caller holding an attitude
158 /// re-sends above the autopilot's setpoint timeout or the vehicle reverts.
159 pub async fn flight_rate_setpoint(&self, args: Value) -> Result<Value, ClientError> {
160 Ok(self
161 .send_request("flight.rate_setpoint.send", "flight.rate_setpoint", args)
162 .await?
163 .args)
164 }
165}