Skip to main content

pamoja_mavlink/
vehicle.rs

1//! A MAVLink vehicle modelled as a pamoja [`Device`].
2//!
3//! [`Vehicle`] wraps a [`Connection`] over any [`ByteLink`] and presents an autopilot through
4//! the core device traits, so a PX4 or ArduPilot vehicle drives like any other pamoja device
5//! from any language binding. It maps the three surfaces a ground station needs onto the
6//! device model:
7//!
8//! - [`Device`] - [`connect`](Device::connect) waits for the vehicle's first heartbeat (and
9//!   learns its system and component id), and the vehicle's stable [`id`](Device::id) is its
10//!   MAVLink address.
11//! - [`Telemetry`] - [`next_frame`](Telemetry::next_frame) yields the next decoded telemetry
12//!   [`Report`].
13//! - [`Actuator`] - [`apply`](Actuator::apply) streams an offboard [`Setpoint`].
14//!
15//! On top of those it offers the mission and command surfaces as async methods that drive the
16//! sans-IO [`protocol`](crate::protocol) machines over the link, applying the mission
17//! protocol's timeout-and-retransmit rules: [`upload_mission`](Vehicle::upload_mission) and
18//! [`download_mission`](Vehicle::download_mission) run the plan transfer, and
19//! [`send_command`](Vehicle::send_command) (with [`arm`](Vehicle::arm),
20//! [`set_mode`](Vehicle::set_mode), [`takeoff`](Vehicle::takeoff), and friends) run the command
21//! protocol.
22//!
23//! This layer is available with the default `std` feature; the wire core and the protocol
24//! machines below it are `no_std`.
25
26use std::time::Duration;
27
28use pamoja_core::{Actuator, Device, Error as CoreError, Result as CoreResult, Telemetry};
29use tokio::time::timeout;
30
31use crate::dialect::{
32    mav_autopilot, mav_cmd, mav_mission_result, mav_mission_type, mav_state, mav_type, Attitude,
33    BatteryStatus, CommandAck, CommandLong, GlobalPositionInt, GpsRawInt, Heartbeat, Message,
34    MissionAck, MissionCount, MissionItemInt, MissionRequest, MissionRequestInt,
35    SetPositionTargetGlobalInt, SetPositionTargetLocalNed, Statustext, SysStatus, VfrHud,
36};
37use crate::frame::Frame;
38use crate::link::{ByteLink, Connection};
39use crate::protocol::command::{AckOutcome, CommandProtocol};
40use crate::protocol::mission::{MissionReceiver, MissionSender, ReceiverAction};
41use crate::protocol::MAX_RETRIES;
42use crate::signing::{Signer, Verifier};
43use crate::MavlinkError;
44
45/// How long to wait for a response before retransmitting, as the mission and command protocols
46/// recommend for their request/response messages.
47const RESPONSE_TIMEOUT: Duration = Duration::from_millis(1500);
48
49/// How long [`connect`](Vehicle::connect) waits for the vehicle's first heartbeat.
50const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
51
52/// The conventional ground-station component id.
53pub const GCS_COMPONENT: u8 = 190;
54
55/// A decoded telemetry report from a [`Vehicle`].
56///
57/// The common messages a ground station displays are decoded into typed variants; anything
58/// else is carried as a [`Report::Other`] raw frame so no traffic is lost.
59#[derive(Clone, Debug)]
60#[non_exhaustive]
61pub enum Report {
62    /// The periodic heartbeat announcing the vehicle's type, autopilot, and status.
63    Heartbeat(Heartbeat),
64    /// Onboard sensor health, load, and battery state.
65    SysStatus(SysStatus),
66    /// The raw GPS fix.
67    GpsRawInt(GpsRawInt),
68    /// Orientation and angular rates.
69    Attitude(Attitude),
70    /// The fused global position, altitude, and velocity.
71    GlobalPositionInt(GlobalPositionInt),
72    /// The heads-up flight summary.
73    VfrHud(VfrHud),
74    /// Battery charge, current, and per-cell voltages.
75    BatteryStatus(BatteryStatus),
76    /// A human-readable status message.
77    Statustext(Statustext),
78    /// Any other message, carried as the raw frame. Boxed because a [`Frame`] is far larger
79    /// than a decoded message, so the common typed reports stay small to move around.
80    Other(Box<Frame>),
81}
82
83impl Report {
84    /// Decodes a frame into a typed report, falling back to [`Report::Other`].
85    ///
86    /// # Arguments
87    ///
88    /// * `frame` - the received frame.
89    ///
90    /// # Returns
91    ///
92    /// The decoded report.
93    fn from_frame(frame: &Frame) -> Report {
94        // A telemetry decode of a well-formed frame does not fail (a short payload is
95        // zero-extended), so a decode error falls back to the raw frame rather than dropping it.
96        // `unwrap_or_else` keeps the success path from boxing the frame it does not need.
97        let raw = || Report::Other(Box::new(*frame));
98        match frame.message_id() {
99            Heartbeat::ID => Heartbeat::decode(frame.payload())
100                .map(Report::Heartbeat)
101                .unwrap_or_else(|_| raw()),
102            SysStatus::ID => SysStatus::decode(frame.payload())
103                .map(Report::SysStatus)
104                .unwrap_or_else(|_| raw()),
105            GpsRawInt::ID => GpsRawInt::decode(frame.payload())
106                .map(Report::GpsRawInt)
107                .unwrap_or_else(|_| raw()),
108            Attitude::ID => Attitude::decode(frame.payload())
109                .map(Report::Attitude)
110                .unwrap_or_else(|_| raw()),
111            GlobalPositionInt::ID => GlobalPositionInt::decode(frame.payload())
112                .map(Report::GlobalPositionInt)
113                .unwrap_or_else(|_| raw()),
114            VfrHud::ID => VfrHud::decode(frame.payload())
115                .map(Report::VfrHud)
116                .unwrap_or_else(|_| raw()),
117            BatteryStatus::ID => BatteryStatus::decode(frame.payload())
118                .map(Report::BatteryStatus)
119                .unwrap_or_else(|_| raw()),
120            Statustext::ID => Statustext::decode(frame.payload())
121                .map(Report::Statustext)
122                .unwrap_or_else(|_| raw()),
123            _ => raw(),
124        }
125    }
126}
127
128/// An offboard control setpoint, in the local or global frame.
129///
130/// Build one with the constructors on [`SetPositionTargetLocalNed`] and
131/// [`SetPositionTargetGlobalInt`] (in [`protocol::offboard`](crate::protocol::offboard)), then
132/// stream it with [`Actuator::apply`].
133#[derive(Clone, Copy, Debug, PartialEq)]
134pub enum Setpoint {
135    /// A setpoint in the local NED frame.
136    Local(SetPositionTargetLocalNed),
137    /// A setpoint in the global frame.
138    Global(SetPositionTargetGlobalInt),
139}
140
141/// A MAVLink vehicle over a [`ByteLink`], exposed through the pamoja device model.
142///
143/// A vehicle sends as a ground station (its own system and component id) and addresses a target
144/// vehicle. The target is learned from the first heartbeat unless it is pinned with
145/// [`with_target`](Vehicle::with_target). Attach signing with
146/// [`with_signer`](Vehicle::with_signer) and [`with_verifier`](Vehicle::with_verifier).
147pub struct Vehicle<L> {
148    connection: Connection<L>,
149    id: String,
150    target_system: u8,
151    target_component: u8,
152    autodetect_target: bool,
153}
154
155impl<L: ByteLink> Vehicle<L> {
156    /// Creates a vehicle client sending as the given ground-station identity.
157    ///
158    /// The target vehicle defaults to system 1, component 1, and is updated from the first
159    /// heartbeat seen during [`connect`](Device::connect).
160    ///
161    /// # Arguments
162    ///
163    /// * `link` - the byte link to the vehicle.
164    /// * `system_id` - this ground station's system id.
165    /// * `component_id` - this ground station's component id.
166    ///
167    /// # Returns
168    ///
169    /// The vehicle client, with signing off.
170    pub fn new(link: L, system_id: u8, component_id: u8) -> Self {
171        Vehicle {
172            connection: Connection::new(link, system_id, component_id),
173            id: Self::format_id(1, 1),
174            target_system: 1,
175            target_component: 1,
176            autodetect_target: true,
177        }
178    }
179
180    /// Pins the target vehicle's system and component id instead of learning them.
181    ///
182    /// # Arguments
183    ///
184    /// * `system_id` - the target vehicle's system id.
185    /// * `component_id` - the target vehicle's component id.
186    ///
187    /// # Returns
188    ///
189    /// The vehicle, for chaining.
190    pub fn with_target(mut self, system_id: u8, component_id: u8) -> Self {
191        self.target_system = system_id;
192        self.target_component = component_id;
193        self.autodetect_target = false;
194        self.id = Self::format_id(system_id, component_id);
195        self
196    }
197
198    /// Signs every outgoing frame with `signer`.
199    ///
200    /// # Arguments
201    ///
202    /// * `signer` - the signer to stamp outgoing frames with.
203    ///
204    /// # Returns
205    ///
206    /// The vehicle, for chaining.
207    pub fn with_signer(mut self, signer: Signer) -> Self {
208        self.connection = self.connection.with_signer(signer);
209        self
210    }
211
212    /// Verifies signed incoming frames with `verifier`, and rejects unsigned ones.
213    ///
214    /// # Arguments
215    ///
216    /// * `verifier` - the verifier to check incoming signed frames with.
217    ///
218    /// # Returns
219    ///
220    /// The vehicle, for chaining.
221    pub fn with_verifier(mut self, verifier: Verifier) -> Self {
222        self.connection = self.connection.with_verifier(verifier);
223        self
224    }
225
226    /// Returns the target vehicle's system id.
227    ///
228    /// # Returns
229    ///
230    /// The target system id.
231    pub fn target_system(&self) -> u8 {
232        self.target_system
233    }
234
235    /// Returns the target vehicle's component id.
236    ///
237    /// # Returns
238    ///
239    /// The target component id.
240    pub fn target_component(&self) -> u8 {
241        self.target_component
242    }
243
244    /// Sends a ground-station heartbeat, which some autopilots require before they accept
245    /// commands.
246    ///
247    /// # Returns
248    ///
249    /// `Ok(())` once the heartbeat has been sent.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`Error::Transport`](pamoja_core::Error::Transport) if the frame cannot be sent.
254    pub async fn send_heartbeat(&mut self) -> CoreResult<()> {
255        let heartbeat = Heartbeat {
256            custom_mode: 0,
257            type_: mav_type::GCS,
258            autopilot: mav_autopilot::INVALID,
259            base_mode: 0,
260            system_status: mav_state::ACTIVE,
261            mavlink_version: 3,
262        };
263        self.tx(&heartbeat).await
264    }
265
266    /// Reads the next telemetry report from the vehicle.
267    ///
268    /// Unlike [`Telemetry::next_frame`], this treats a closed link as an error rather than an
269    /// end of stream.
270    ///
271    /// # Returns
272    ///
273    /// The next decoded [`Report`].
274    ///
275    /// # Errors
276    ///
277    /// Returns [`Error::Closed`](pamoja_core::Error::Closed) if the link ends, or
278    /// [`Error::Transport`](pamoja_core::Error::Transport) on a link fault.
279    pub async fn recv(&mut self) -> CoreResult<Report> {
280        let frame = self.rx().await?;
281        Ok(Report::from_frame(&frame))
282    }
283
284    /// Sends a command to the vehicle and awaits its result, retransmitting on timeout.
285    ///
286    /// The command is sent as a `COMMAND_LONG` and matched to its `COMMAND_ACK`. An
287    /// in-progress acknowledgement extends the wait; a missing acknowledgement resends the
288    /// command with an incremented confirmation, up to the retry budget.
289    ///
290    /// # Arguments
291    ///
292    /// * `command` - the [`MAV_CMD`](crate::dialect::mav_cmd) id.
293    /// * `params` - the seven command parameters.
294    ///
295    /// # Returns
296    ///
297    /// The [`MAV_RESULT`](crate::dialect::mav_result) the vehicle reported, including a
298    /// rejection such as [`DENIED`](crate::dialect::mav_result::DENIED).
299    ///
300    /// # Errors
301    ///
302    /// Returns [`Error::Transport`](pamoja_core::Error::Transport) if the command is not
303    /// acknowledged within the retry budget or the link faults.
304    pub async fn send_command(&mut self, command: u16, params: [f32; 7]) -> CoreResult<u8> {
305        let mut protocol = CommandProtocol::new(command, MAX_RETRIES);
306        loop {
307            let request = CommandLong {
308                param1: params[0],
309                param2: params[1],
310                param3: params[2],
311                param4: params[3],
312                param5: params[4],
313                param6: params[5],
314                param7: params[6],
315                command,
316                target_system: self.target_system,
317                target_component: self.target_component,
318                confirmation: protocol.confirmation(),
319            };
320            self.tx(&request).await?;
321
322            // Wait for the matching acknowledgement, ignoring unrelated traffic; on a timeout
323            // fall out to resend, and on an exhausted budget give up.
324            let resend = loop {
325                match timeout(RESPONSE_TIMEOUT, self.rx()).await {
326                    Err(_elapsed) => {
327                        if protocol.on_timeout().is_none() {
328                            return Err(CoreError::Transport(
329                                "command was not acknowledged".into(),
330                            ));
331                        }
332                        break true;
333                    }
334                    Ok(frame) => {
335                        let frame = frame?;
336                        if frame.message_id() == CommandAck::ID {
337                            let ack = CommandAck::decode(frame.payload()).map_err(map_mav)?;
338                            match protocol.on_ack(&ack) {
339                                AckOutcome::Final(result) => return Ok(result),
340                                AckOutcome::InProgress(_) | AckOutcome::Unrelated => continue,
341                            }
342                        }
343                    }
344                }
345            };
346            debug_assert!(resend);
347        }
348    }
349
350    /// Arms or disarms the vehicle.
351    ///
352    /// # Arguments
353    ///
354    /// * `arm` - `true` to arm, `false` to disarm.
355    ///
356    /// # Returns
357    ///
358    /// The [`MAV_RESULT`](crate::dialect::mav_result) of the arm command.
359    ///
360    /// # Errors
361    ///
362    /// As [`send_command`](Vehicle::send_command).
363    pub async fn arm(&mut self, arm: bool) -> CoreResult<u8> {
364        let flag = if arm { 1.0 } else { 0.0 };
365        self.send_command(
366            mav_cmd::COMPONENT_ARM_DISARM,
367            [flag, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
368        )
369        .await
370    }
371
372    /// Requests a mode change.
373    ///
374    /// # Arguments
375    ///
376    /// * `base_mode` - the [`MAV_MODE_FLAG`](crate::dialect::mav_mode_flag) base-mode bits.
377    /// * `custom_mode` - the autopilot-specific custom mode.
378    ///
379    /// # Returns
380    ///
381    /// The [`MAV_RESULT`](crate::dialect::mav_result) of the mode command.
382    ///
383    /// # Errors
384    ///
385    /// As [`send_command`](Vehicle::send_command).
386    pub async fn set_mode(&mut self, base_mode: u8, custom_mode: u32) -> CoreResult<u8> {
387        self.send_command(
388            mav_cmd::DO_SET_MODE,
389            [
390                base_mode as f32,
391                custom_mode as f32,
392                0.0,
393                0.0,
394                0.0,
395                0.0,
396                0.0,
397            ],
398        )
399        .await
400    }
401
402    /// Commands a takeoff to an altitude.
403    ///
404    /// # Arguments
405    ///
406    /// * `altitude` - the target altitude, in metres.
407    ///
408    /// # Returns
409    ///
410    /// The [`MAV_RESULT`](crate::dialect::mav_result) of the takeoff command.
411    ///
412    /// # Errors
413    ///
414    /// As [`send_command`](Vehicle::send_command).
415    pub async fn takeoff(&mut self, altitude: f32) -> CoreResult<u8> {
416        self.send_command(
417            mav_cmd::NAV_TAKEOFF,
418            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, altitude],
419        )
420        .await
421    }
422
423    /// Asks the vehicle to emit one message, by id.
424    ///
425    /// # Arguments
426    ///
427    /// * `message_id` - the id of the message to request.
428    ///
429    /// # Returns
430    ///
431    /// The [`MAV_RESULT`](crate::dialect::mav_result) of the request.
432    ///
433    /// # Errors
434    ///
435    /// As [`send_command`](Vehicle::send_command).
436    pub async fn request_message(&mut self, message_id: u32) -> CoreResult<u8> {
437        self.send_command(
438            mav_cmd::REQUEST_MESSAGE,
439            [message_id as f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
440        )
441        .await
442    }
443
444    /// Sets how often the vehicle streams a message.
445    ///
446    /// # Arguments
447    ///
448    /// * `message_id` - the id of the message.
449    /// * `interval_us` - the send interval, in microseconds, or `-1` to disable.
450    ///
451    /// # Returns
452    ///
453    /// The [`MAV_RESULT`](crate::dialect::mav_result) of the request.
454    ///
455    /// # Errors
456    ///
457    /// As [`send_command`](Vehicle::send_command).
458    pub async fn set_message_interval(
459        &mut self,
460        message_id: u32,
461        interval_us: i32,
462    ) -> CoreResult<u8> {
463        self.send_command(
464            mav_cmd::SET_MESSAGE_INTERVAL,
465            [
466                message_id as f32,
467                interval_us as f32,
468                0.0,
469                0.0,
470                0.0,
471                0.0,
472                0.0,
473            ],
474        )
475        .await
476    }
477
478    /// Uploads a mission plan to the vehicle.
479    ///
480    /// Runs the mission protocol's sender role: announces the count, answers each item request,
481    /// and completes on the vehicle's acknowledgement, retransmitting on timeout.
482    ///
483    /// # Arguments
484    ///
485    /// * `items` - the mission items, in sequence order; the target ids, sequence numbers, and
486    ///   mission type are stamped on for you.
487    ///
488    /// # Returns
489    ///
490    /// `Ok(())` once the vehicle accepts the plan.
491    ///
492    /// # Errors
493    ///
494    /// Returns [`Error::Transport`](pamoja_core::Error::Transport) if the transfer times out or
495    /// the vehicle rejects the plan.
496    pub async fn upload_mission(&mut self, items: &[MissionItemInt]) -> CoreResult<()> {
497        let sender = MissionSender::new(
498            items,
499            self.target_system,
500            self.target_component,
501            mav_mission_type::MISSION,
502        );
503        self.tx(&sender.count()).await?;
504
505        // `None` means the opening count is still unanswered and is what a timeout resends;
506        // `Some(seq)` is the last item sent, resent on a timeout.
507        let mut last_seq: Option<u16> = None;
508        let mut retries = MAX_RETRIES;
509        loop {
510            match timeout(RESPONSE_TIMEOUT, self.rx()).await {
511                Err(_elapsed) => {
512                    if retries == 0 {
513                        return Err(CoreError::Transport("mission upload timed out".into()));
514                    }
515                    retries -= 1;
516                    match last_seq {
517                        None => self.tx(&sender.count()).await?,
518                        Some(seq) => {
519                            if let Some(item) = sender.item(seq) {
520                                self.tx(&item).await?;
521                            }
522                        }
523                    }
524                }
525                Ok(frame) => {
526                    let frame = frame?;
527                    match frame.message_id() {
528                        MissionRequestInt::ID => {
529                            let request =
530                                MissionRequestInt::decode(frame.payload()).map_err(map_mav)?;
531                            self.answer_item(&sender, request.seq, &mut last_seq, &mut retries)
532                                .await?;
533                        }
534                        MissionRequest::ID => {
535                            let request =
536                                MissionRequest::decode(frame.payload()).map_err(map_mav)?;
537                            self.answer_item(&sender, request.seq, &mut last_seq, &mut retries)
538                                .await?;
539                        }
540                        MissionAck::ID => {
541                            let ack = MissionAck::decode(frame.payload()).map_err(map_mav)?;
542                            if ack.type_ == mav_mission_result::ACCEPTED {
543                                return Ok(());
544                            }
545                            return Err(CoreError::Transport(format!(
546                                "vehicle rejected the mission: result {}",
547                                ack.type_
548                            )));
549                        }
550                        _ => {}
551                    }
552                }
553            }
554        }
555    }
556
557    /// Downloads the vehicle's mission plan.
558    ///
559    /// Runs the mission protocol's receiver role: requests the count, requests each item in
560    /// order, re-requests an out-of-order item, and acknowledges completion, retransmitting on
561    /// timeout.
562    ///
563    /// # Returns
564    ///
565    /// The mission items, in sequence order.
566    ///
567    /// # Errors
568    ///
569    /// Returns [`Error::Transport`](pamoja_core::Error::Transport) if the transfer times out or
570    /// the link faults.
571    pub async fn download_mission(&mut self) -> CoreResult<Vec<MissionItemInt>> {
572        let mut receiver = MissionReceiver::new(
573            self.target_system,
574            self.target_component,
575            mav_mission_type::MISSION,
576        );
577        self.tx(&receiver.request_list()).await?;
578
579        let mut items: Vec<MissionItemInt> = Vec::new();
580        let mut last_request: Option<MissionRequestInt> = None;
581        let mut got_count = false;
582        let mut retries = MAX_RETRIES;
583        loop {
584            match timeout(RESPONSE_TIMEOUT, self.rx()).await {
585                Err(_elapsed) => {
586                    if retries == 0 {
587                        return Err(CoreError::Transport("mission download timed out".into()));
588                    }
589                    retries -= 1;
590                    match &last_request {
591                        Some(request) => self.tx(request).await?,
592                        None => self.tx(&receiver.request_list()).await?,
593                    }
594                }
595                Ok(frame) => {
596                    let frame = frame?;
597                    match frame.message_id() {
598                        MissionCount::ID if !got_count => {
599                            let count = MissionCount::decode(frame.payload())
600                                .map_err(map_mav)?
601                                .count;
602                            got_count = true;
603                            items.reserve(count as usize);
604                            match receiver.on_count(count) {
605                                ReceiverAction::Request(request) => {
606                                    self.tx(&request).await?;
607                                    last_request = Some(request);
608                                    retries = MAX_RETRIES;
609                                }
610                                ReceiverAction::Ack(ack) => {
611                                    self.tx(&ack).await?;
612                                    return Ok(items);
613                                }
614                            }
615                        }
616                        MissionItemInt::ID => {
617                            let item = MissionItemInt::decode(frame.payload()).map_err(map_mav)?;
618                            let (accepted, action) = receiver.on_item(&item);
619                            if let Some(item) = accepted {
620                                items.push(item);
621                            }
622                            match action {
623                                ReceiverAction::Request(request) => {
624                                    self.tx(&request).await?;
625                                    last_request = Some(request);
626                                    retries = MAX_RETRIES;
627                                }
628                                ReceiverAction::Ack(ack) => {
629                                    self.tx(&ack).await?;
630                                    return Ok(items);
631                                }
632                            }
633                        }
634                        _ => {}
635                    }
636                }
637            }
638        }
639    }
640
641    // Answers a mission item request during an upload, recording it as the item to resend on a
642    // timeout and refilling the retry budget.
643    async fn answer_item(
644        &mut self,
645        sender: &MissionSender<'_>,
646        seq: u16,
647        last_seq: &mut Option<u16>,
648        retries: &mut u8,
649    ) -> CoreResult<()> {
650        if let Some(item) = sender.item(seq) {
651            self.tx(&item).await?;
652            *last_seq = Some(seq);
653            *retries = MAX_RETRIES;
654        }
655        Ok(())
656    }
657
658    // Waits for the first heartbeat, learning the target ids when auto-detecting.
659    async fn wait_for_heartbeat(&mut self) -> CoreResult<()> {
660        loop {
661            let frame = timeout(HEARTBEAT_TIMEOUT, self.rx())
662                .await
663                .map_err(|_| CoreError::Transport("no heartbeat from the vehicle".into()))??;
664            if frame.message_id() == Heartbeat::ID {
665                if self.autodetect_target {
666                    self.target_system = frame.system_id();
667                    self.target_component = frame.component_id();
668                    self.id = Self::format_id(self.target_system, self.target_component);
669                }
670                return Ok(());
671            }
672        }
673    }
674
675    async fn tx<M: Message>(&mut self, message: &M) -> CoreResult<()> {
676        self.connection.send(message).await.map_err(map_mav)
677    }
678
679    async fn rx(&mut self) -> CoreResult<Frame> {
680        self.connection.recv().await.map_err(map_mav)
681    }
682
683    fn format_id(system_id: u8, component_id: u8) -> String {
684        format!("mavlink:{system_id}.{component_id}")
685    }
686}
687
688impl<L: ByteLink> Device for Vehicle<L> {
689    fn id(&self) -> &str {
690        &self.id
691    }
692
693    async fn connect(&mut self) -> CoreResult<()> {
694        self.wait_for_heartbeat().await
695    }
696
697    async fn disconnect(&mut self) -> CoreResult<()> {
698        // The link is released when the vehicle is dropped; there is no teardown handshake.
699        Ok(())
700    }
701}
702
703impl<L: ByteLink> Telemetry for Vehicle<L> {
704    type Frame = Report;
705
706    async fn next_frame(&mut self) -> CoreResult<Option<Report>> {
707        match self.connection.recv().await {
708            Ok(frame) => Ok(Some(Report::from_frame(&frame))),
709            Err(MavlinkError::Closed) => Ok(None),
710            Err(err) => Err(map_mav(err)),
711        }
712    }
713}
714
715impl<L: ByteLink> Actuator for Vehicle<L> {
716    type Command = Setpoint;
717
718    async fn apply(&mut self, command: Setpoint) -> CoreResult<()> {
719        match command {
720            Setpoint::Local(setpoint) => self.tx(&setpoint).await,
721            Setpoint::Global(setpoint) => self.tx(&setpoint).await,
722        }
723    }
724}
725
726// Maps a wire-layer fault onto the shared error model: a closed link is `Closed`, a bad payload
727// is a codec fault, a signing failure is an auth fault, and the rest are transport faults.
728fn map_mav(err: MavlinkError) -> CoreError {
729    match err {
730        MavlinkError::Closed => CoreError::Closed,
731        MavlinkError::BadPayload => CoreError::Codec("malformed MAVLink payload".into()),
732        MavlinkError::Unsigned | MavlinkError::BadSignature | MavlinkError::ReplayedTimestamp => {
733            CoreError::Auth(err.to_string())
734        }
735        other => CoreError::Transport(other.to_string()),
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use crate::dialect::{mav_frame, mav_result};
743    use crate::link::{MemoryLink, SitlAutopilot};
744
745    fn waypoint(seq: u16, lat: i32, lon: i32, alt: f32) -> MissionItemInt {
746        MissionItemInt {
747            param1: 0.0,
748            param2: 0.0,
749            param3: 0.0,
750            param4: 0.0,
751            x: lat,
752            y: lon,
753            z: alt,
754            seq,
755            command: mav_cmd::NAV_WAYPOINT,
756            target_system: 0,
757            target_component: 0,
758            frame: mav_frame::GLOBAL_RELATIVE_ALT_INT,
759            current: (seq == 0) as u8,
760            autocontinue: 1,
761            mission_type: mav_mission_type::MISSION,
762        }
763    }
764
765    // Spawns a SITL autopilot that serves whatever the vehicle sends until the test drops it.
766    fn spawn_autopilot(link: MemoryLink) -> tokio::task::JoinHandle<()> {
767        tokio::spawn(async move {
768            let mut autopilot = SitlAutopilot::new(link, 1, 1);
769            let _ = autopilot.emit_heartbeat().await;
770            loop {
771                if autopilot.serve_once().await.is_err() {
772                    break;
773                }
774            }
775        })
776    }
777
778    #[tokio::test]
779    async fn connect_learns_the_target_from_the_heartbeat() {
780        let (gcs, vehicle) = MemoryLink::pair();
781        let handle = spawn_autopilot(vehicle);
782        let mut client = Vehicle::new(gcs, 255, GCS_COMPONENT);
783        client.connect().await.unwrap();
784        assert_eq!(client.target_system(), 1);
785        assert_eq!(client.id(), "mavlink:1.1");
786        handle.abort();
787    }
788
789    #[tokio::test]
790    async fn a_command_is_acknowledged() {
791        let (gcs, vehicle) = MemoryLink::pair();
792        let handle = spawn_autopilot(vehicle);
793        let mut client = Vehicle::new(gcs, 255, GCS_COMPONENT);
794        client.connect().await.unwrap();
795        let result = client.arm(true).await.unwrap();
796        assert_eq!(result, mav_result::ACCEPTED);
797        handle.abort();
798    }
799
800    #[tokio::test]
801    async fn a_mission_uploads_and_downloads_unchanged() {
802        let (gcs, vehicle) = MemoryLink::pair();
803        let handle = spawn_autopilot(vehicle);
804        let mut client = Vehicle::new(gcs, 255, GCS_COMPONENT);
805        client.connect().await.unwrap();
806
807        let plan = [
808            waypoint(0, 473_977_418, 85_455_939, 10.0),
809            waypoint(1, 473_977_500, 85_456_000, 20.0),
810            waypoint(2, 473_977_600, 85_456_100, 15.0),
811        ];
812        client.upload_mission(&plan).await.unwrap();
813        let downloaded = client.download_mission().await.unwrap();
814
815        assert_eq!(downloaded.len(), 3);
816        for (sent, got) in plan.iter().zip(downloaded.iter()) {
817            assert_eq!(sent.x, got.x);
818            assert_eq!(sent.y, got.y);
819            assert_eq!(sent.z, got.z);
820            assert_eq!(sent.command, got.command);
821        }
822        handle.abort();
823    }
824
825    #[tokio::test]
826    async fn an_offboard_setpoint_is_accepted_by_the_actuator() {
827        let (gcs, vehicle) = MemoryLink::pair();
828        let handle = spawn_autopilot(vehicle);
829        let mut client = Vehicle::new(gcs, 255, GCS_COMPONENT);
830        client.connect().await.unwrap();
831        let setpoint = Setpoint::Local(SetPositionTargetLocalNed::velocity(
832            0,
833            mav_frame::LOCAL_NED,
834            client.target_system(),
835            client.target_component(),
836            0.5,
837            0.0,
838            -0.2,
839        ));
840        client.apply(setpoint).await.unwrap();
841        handle.abort();
842    }
843}