pamoja_mavlink/protocol/frames.rs
1//! The service protocols driven by frames rather than decoded messages.
2//!
3//! Each machine in this module speaks in typed messages, which is the right level for the
4//! logic. A caller holding a link has frames, though, and turning one into the right call
5//! means decoding the payload, matching the message id, calling the machine, and encoding
6//! whatever it answers. That glue is the same every time, so it lives here once: a machine
7//! takes an incoming [`Frame`], and gives back the frame to send in reply and what happened.
8//!
9//! Like the machines themselves this has no IO, no timers, and no allocation; a frame is a
10//! fixed buffer of bytes.
11
12use crate::dialect::{
13 encode_message, mav_mission_result, CommandAck, Message, MissionAck, MissionCount,
14 MissionItemInt, MissionRequest, MissionRequestInt, MissionRequestList,
15};
16use crate::error::Result;
17use crate::frame::{Frame, Header};
18
19use super::{AckOutcome, CommandProtocol, MissionReceiver, MissionSender, ReceiverAction};
20
21/// What one incoming frame produced for a [`MissionReceiver`].
22#[derive(Clone, Copy, Debug)]
23pub struct ReceiverStep {
24 /// The item the frame carried, if it was the one expected next.
25 pub accepted: Option<MissionItemInt>,
26 /// The next thing to send, as the machine sees it.
27 pub action: ReceiverAction,
28 /// That same thing, ready for the link.
29 pub reply: Frame,
30}
31
32impl MissionReceiver {
33 /// Builds the frame that starts a download.
34 ///
35 /// # Arguments
36 ///
37 /// * `header` - the addressing fields to stamp on the frame.
38 ///
39 /// # Returns
40 ///
41 /// The `MISSION_REQUEST_LIST` frame.
42 ///
43 /// # Errors
44 ///
45 /// Returns [`MavlinkError::PayloadTooLong`](crate::MavlinkError::PayloadTooLong) if the
46 /// message does not fit a frame, which a request list never fails.
47 pub fn request_list_frame(&self, header: Header) -> Result<Frame> {
48 encode_message(header, &self.request_list())
49 }
50
51 /// Handles an incoming frame, if it is one this transfer is waiting for.
52 ///
53 /// A `MISSION_COUNT` opens the transfer and a `MISSION_ITEM_INT` advances it; any other
54 /// message is not this machine's to handle and is reported as such rather than refused,
55 /// so a caller can route one link's traffic through several machines.
56 ///
57 /// # Arguments
58 ///
59 /// * `frame` - the frame off the link.
60 /// * `header` - the addressing fields to stamp on the reply.
61 ///
62 /// # Returns
63 ///
64 /// The step taken, or [`None`] if the frame carries a message this transfer does not
65 /// handle.
66 ///
67 /// # Errors
68 ///
69 /// Returns an error only if the reply does not fit a frame, which no mission message can
70 /// cause; a short or long payload decodes the way the message layer defines.
71 ///
72 /// # Examples
73 ///
74 /// ```
75 /// use pamoja_mavlink::dialect::{encode_message, MissionCount, MissionItemInt};
76 /// use pamoja_mavlink::protocol::{MissionReceiver, ReceiverAction};
77 /// use pamoja_mavlink::Header;
78 ///
79 /// let vehicle = Header::new(1, 1, 0);
80 /// let station = Header::new(255, 190, 0);
81 /// let mut download = MissionReceiver::new(1, 1, 0);
82 ///
83 /// // The vehicle announces one item, and the receiver asks for it.
84 /// let count = MissionCount { count: 1, target_system: 255, target_component: 190, mission_type: 0, opaque_id: 0 };
85 /// let step = download
86 /// .on_frame(&encode_message(vehicle, &count)?, station)?
87 /// .expect("a count is handled");
88 /// assert!(matches!(step.action, ReceiverAction::Request(request) if request.seq == 0));
89 /// assert_eq!(step.reply.message_id(), 51); // MISSION_REQUEST_INT
90 ///
91 /// // A frame for some other machine is passed over rather than refused.
92 /// let heartbeat = pamoja_mavlink::dialect::Heartbeat { custom_mode: 0, type_: 2, autopilot: 3, base_mode: 0, system_status: 4, mavlink_version: 3 };
93 /// assert!(download.on_frame(&encode_message(vehicle, &heartbeat)?, station)?.is_none());
94 /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
95 /// ```
96 pub fn on_frame(&mut self, frame: &Frame, header: Header) -> Result<Option<ReceiverStep>> {
97 let id = frame.message_id();
98 if id == MissionCount::ID {
99 let count = MissionCount::decode(frame.payload())?;
100 let action = self.on_count(count.count);
101 Ok(Some(ReceiverStep {
102 accepted: None,
103 reply: reply_of(&action, header)?,
104 action,
105 }))
106 } else if id == MissionItemInt::ID {
107 let item = MissionItemInt::decode(frame.payload())?;
108 let (accepted, action) = self.on_item(&item);
109 Ok(Some(ReceiverStep {
110 accepted,
111 reply: reply_of(&action, header)?,
112 action,
113 }))
114 } else {
115 Ok(None)
116 }
117 }
118}
119
120fn reply_of(action: &ReceiverAction, header: Header) -> Result<Frame> {
121 match action {
122 ReceiverAction::Request(request) => encode_message(header, request),
123 ReceiverAction::Ack(ack) => encode_message(header, ack),
124 }
125}
126
127/// What one incoming frame produced for a [`MissionSender`].
128///
129/// A reply carries a whole frame buffer, which dwarfs the other variant; boxing it would
130/// need an allocator, and this layer has none.
131#[derive(Clone, Copy, Debug)]
132#[allow(clippy::large_enum_variant)]
133pub enum SenderStep {
134 /// Send this frame: the count that opens the transfer, a requested item, or an error.
135 Reply(Frame),
136 /// The receiver has acknowledged the transfer with this
137 /// [`MAV_MISSION_RESULT`](crate::dialect::mav_mission_result); nothing more to send.
138 Finished(u8),
139}
140
141impl MissionSender<'_> {
142 /// Builds the frame that opens an upload.
143 ///
144 /// # Arguments
145 ///
146 /// * `header` - the addressing fields to stamp on the frame.
147 ///
148 /// # Returns
149 ///
150 /// The `MISSION_COUNT` frame.
151 ///
152 /// # Errors
153 ///
154 /// Returns [`MavlinkError::PayloadTooLong`](crate::MavlinkError::PayloadTooLong) if the
155 /// message does not fit a frame, which a count never fails.
156 pub fn count_frame(&self, header: Header) -> Result<Frame> {
157 encode_message(header, &self.count())
158 }
159
160 /// Handles an incoming frame, if it is one this transfer answers.
161 ///
162 /// A `MISSION_REQUEST_LIST` is answered with the count, a `MISSION_REQUEST_INT` (or the
163 /// older `MISSION_REQUEST`) with the item asked for, and a request past the end of the
164 /// plan with a `MISSION_ACK` reporting an invalid sequence. A `MISSION_ACK` from the
165 /// receiver ends the transfer. Any other message is reported as not handled.
166 ///
167 /// # Arguments
168 ///
169 /// * `frame` - the frame off the link.
170 /// * `header` - the addressing fields to stamp on the reply.
171 ///
172 /// # Returns
173 ///
174 /// The step taken, or [`None`] if the frame carries a message this transfer does not
175 /// handle.
176 ///
177 /// # Errors
178 ///
179 /// Returns an error only if the reply does not fit a frame, which no mission message can
180 /// cause; a short or long payload decodes the way the message layer defines.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use pamoja_mavlink::dialect::{encode_message, Message, MissionItemInt, MissionRequestInt};
186 /// use pamoja_mavlink::protocol::{MissionSender, SenderStep};
187 /// use pamoja_mavlink::Header;
188 ///
189 /// let waypoint = MissionItemInt { command: 16, x: -338_567_800, y: 1_512_153_000, z: 50.0, ..MissionItemInt::zeroed() };
190 /// let plan = [waypoint];
191 /// let upload = MissionSender::new(&plan, 1, 1, 0);
192 ///
193 /// // The vehicle asks for item 0 and gets it back, stamped with its sequence number.
194 /// let request = MissionRequestInt { seq: 0, target_system: 255, target_component: 190, mission_type: 0 };
195 /// let step = upload
196 /// .on_frame(&encode_message(Header::new(1, 1, 0), &request)?, Header::new(255, 190, 0))?
197 /// .expect("a request is handled");
198 /// let SenderStep::Reply(reply) = step else { panic!("an item is sent") };
199 /// assert_eq!(MissionItemInt::decode(reply.payload())?.seq, 0);
200 /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
201 /// ```
202 pub fn on_frame(&self, frame: &Frame, header: Header) -> Result<Option<SenderStep>> {
203 let id = frame.message_id();
204 if id == MissionRequestList::ID {
205 MissionRequestList::decode(frame.payload())?;
206 encode_message(header, &self.count()).map(|frame| Some(SenderStep::Reply(frame)))
207 } else if id == MissionRequestInt::ID || id == MissionRequest::ID {
208 let seq = if id == MissionRequestInt::ID {
209 MissionRequestInt::decode(frame.payload())?.seq
210 } else {
211 MissionRequest::decode(frame.payload())?.seq
212 };
213 let reply = match self.item(seq) {
214 Some(item) => encode_message(header, &item)?,
215 None => encode_message(header, &self.refuse(mav_mission_result::INVALID_SEQUENCE))?,
216 };
217 Ok(Some(SenderStep::Reply(reply)))
218 } else if id == MissionAck::ID {
219 let ack = MissionAck::decode(frame.payload())?;
220 Ok(Some(SenderStep::Finished(ack.type_)))
221 } else {
222 Ok(None)
223 }
224 }
225
226 fn refuse(&self, result: u8) -> MissionAck {
227 let count = self.count();
228 MissionAck {
229 target_system: count.target_system,
230 target_component: count.target_component,
231 type_: result,
232 mission_type: count.mission_type,
233 ..MissionAck::zeroed()
234 }
235 }
236}
237
238impl CommandProtocol {
239 /// Classifies an incoming frame against the command in flight, if it is an acknowledgment.
240 ///
241 /// # Arguments
242 ///
243 /// * `frame` - the frame off the link.
244 ///
245 /// # Returns
246 ///
247 /// The outcome, or [`None`] if the frame is not a `COMMAND_ACK`.
248 ///
249 /// # Errors
250 ///
251 /// Never fails in practice; the `Result` mirrors the message layer, which zero-extends a
252 /// short payload and ignores the tail of a long one rather than refusing either.
253 ///
254 /// # Examples
255 ///
256 /// ```
257 /// use pamoja_mavlink::dialect::{encode_message, mav_cmd, mav_result, CommandAck};
258 /// use pamoja_mavlink::protocol::{AckOutcome, CommandProtocol};
259 /// use pamoja_mavlink::Header;
260 ///
261 /// let arm = CommandProtocol::new(mav_cmd::COMPONENT_ARM_DISARM, 3);
262 /// let ack = CommandAck { command: mav_cmd::COMPONENT_ARM_DISARM, result: mav_result::ACCEPTED, ..CommandAck::zeroed() };
263 /// let outcome = arm.on_frame(&encode_message(Header::new(1, 1, 0), &ack)?)?;
264 /// assert_eq!(outcome, Some(AckOutcome::Final(mav_result::ACCEPTED)));
265 /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
266 /// ```
267 pub fn on_frame(&self, frame: &Frame) -> Result<Option<AckOutcome>> {
268 if frame.message_id() == CommandAck::ID {
269 Ok(Some(self.on_ack(&CommandAck::decode(frame.payload())?)))
270 } else {
271 Ok(None)
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::dialect::{mav_cmd, mav_result};
280
281 const VEHICLE: Header = Header::new(1, 1, 0);
282 const STATION: Header = Header::new(255, 190, 0);
283
284 fn plan() -> [MissionItemInt; 2] {
285 [
286 MissionItemInt {
287 command: mav_cmd::NAV_TAKEOFF,
288 z: 20.0,
289 ..MissionItemInt::zeroed()
290 },
291 MissionItemInt {
292 command: mav_cmd::NAV_WAYPOINT,
293 x: -338_567_800,
294 y: 1_512_153_000,
295 z: 50.0,
296 ..MissionItemInt::zeroed()
297 },
298 ]
299 }
300
301 #[test]
302 fn a_whole_upload_runs_frame_to_frame() -> Result<()> {
303 let items = plan();
304 let upload = MissionSender::new(&items, 1, 1, 0);
305 let mut download = MissionReceiver::new(255, 190, 0);
306
307 // The station opens with a request list, and the vehicle answers with the count.
308 let opened = download.request_list_frame(STATION)?;
309 let Some(SenderStep::Reply(mut from_vehicle)) = upload.on_frame(&opened, VEHICLE)? else {
310 panic!("a request list is answered");
311 };
312 assert_eq!(from_vehicle.message_id(), MissionCount::ID);
313
314 // Each side answers the other until the receiver acknowledges.
315 let mut accepted = 0;
316 loop {
317 let step = download
318 .on_frame(&from_vehicle, STATION)?
319 .expect("the vehicle only sends what the receiver handles");
320 if step.accepted.is_some() {
321 accepted += 1;
322 }
323 match upload.on_frame(&step.reply, VEHICLE)? {
324 Some(SenderStep::Reply(next)) => from_vehicle = next,
325 Some(SenderStep::Finished(result)) => {
326 assert_eq!(result, mav_mission_result::ACCEPTED);
327 break;
328 }
329 None => panic!("the receiver only sends what the sender handles"),
330 }
331 }
332 assert_eq!(accepted, 2);
333 assert!(download.is_complete());
334 Ok(())
335 }
336
337 #[test]
338 fn a_request_past_the_plan_is_refused_with_the_published_result() -> Result<()> {
339 let items = plan();
340 let upload = MissionSender::new(&items, 1, 1, 0);
341 let request = MissionRequestInt {
342 seq: 7,
343 target_system: 255,
344 target_component: 190,
345 mission_type: 0,
346 };
347 let Some(SenderStep::Reply(reply)) =
348 upload.on_frame(&encode_message(VEHICLE, &request)?, STATION)?
349 else {
350 panic!("a request is answered");
351 };
352 assert_eq!(reply.message_id(), MissionAck::ID);
353 assert_eq!(
354 MissionAck::decode(reply.payload())?.type_,
355 mav_mission_result::INVALID_SEQUENCE
356 );
357 Ok(())
358 }
359
360 #[test]
361 fn the_older_request_message_is_answered_too() -> Result<()> {
362 let items = plan();
363 let upload = MissionSender::new(&items, 1, 1, 0);
364 let request = MissionRequest {
365 seq: 1,
366 target_system: 255,
367 target_component: 190,
368 mission_type: 0,
369 };
370 let Some(SenderStep::Reply(reply)) =
371 upload.on_frame(&encode_message(VEHICLE, &request)?, STATION)?
372 else {
373 panic!("a request is answered");
374 };
375 assert_eq!(MissionItemInt::decode(reply.payload())?.seq, 1);
376 Ok(())
377 }
378
379 #[test]
380 fn a_frame_for_another_machine_is_passed_over() -> Result<()> {
381 let items = plan();
382 let upload = MissionSender::new(&items, 1, 1, 0);
383 let mut download = MissionReceiver::new(255, 190, 0);
384 let arm = CommandProtocol::new(mav_cmd::COMPONENT_ARM_DISARM, 3);
385
386 let ack = CommandAck {
387 command: mav_cmd::COMPONENT_ARM_DISARM,
388 result: mav_result::IN_PROGRESS,
389 progress: 40,
390 ..CommandAck::zeroed()
391 };
392 let frame = encode_message(VEHICLE, &ack)?;
393
394 assert!(upload.on_frame(&frame, STATION)?.is_none());
395 assert!(download.on_frame(&frame, STATION)?.is_none());
396 assert_eq!(arm.on_frame(&frame)?, Some(AckOutcome::InProgress(40)));
397 Ok(())
398 }
399}