Skip to main content

pamoja_mavlink/dialect/
common.rs

1//! The typed common-dialect messages.
2//!
3//! Each is declared with the `message!` macro from its name, id, official `CRC_EXTRA`, and
4//! base fields in wire order. The set spans what a ground station and an autopilot exchange
5//! to fly a mission: the heartbeat, system status, the command, parameter, and mission
6//! protocols, and the core position and attitude telemetry.
7
8use super::{Message, MessageDescriptor};
9
10/// Every typed message's shape, in message-id order.
11///
12/// This is what [`descriptor`](super::descriptor) and [`descriptor_by_name`](super::descriptor_by_name) search, and what a caller
13/// enumerates to discover which messages this build types.
14pub const DESCRIPTORS: &[&MessageDescriptor<'static>] = &[
15    <Heartbeat as Message>::DESCRIPTOR,
16    <SysStatus as Message>::DESCRIPTOR,
17    <SystemTime as Message>::DESCRIPTOR,
18    <Ping as Message>::DESCRIPTOR,
19    <SetMode as Message>::DESCRIPTOR,
20    <ParamRequestRead as Message>::DESCRIPTOR,
21    <ParamRequestList as Message>::DESCRIPTOR,
22    <ParamValue as Message>::DESCRIPTOR,
23    <ParamSet as Message>::DESCRIPTOR,
24    <GpsRawInt as Message>::DESCRIPTOR,
25    <Attitude as Message>::DESCRIPTOR,
26    <AttitudeQuaternion as Message>::DESCRIPTOR,
27    <LocalPositionNed as Message>::DESCRIPTOR,
28    <GlobalPositionInt as Message>::DESCRIPTOR,
29    <ServoOutputRaw as Message>::DESCRIPTOR,
30    <MissionRequest as Message>::DESCRIPTOR,
31    <MissionCurrent as Message>::DESCRIPTOR,
32    <MissionRequestList as Message>::DESCRIPTOR,
33    <MissionCount as Message>::DESCRIPTOR,
34    <MissionClearAll as Message>::DESCRIPTOR,
35    <MissionAck as Message>::DESCRIPTOR,
36    <MissionRequestInt as Message>::DESCRIPTOR,
37    <RcChannels as Message>::DESCRIPTOR,
38    <ManualControl as Message>::DESCRIPTOR,
39    <MissionItemInt as Message>::DESCRIPTOR,
40    <VfrHud as Message>::DESCRIPTOR,
41    <CommandInt as Message>::DESCRIPTOR,
42    <CommandLong as Message>::DESCRIPTOR,
43    <CommandAck as Message>::DESCRIPTOR,
44    <SetPositionTargetLocalNed as Message>::DESCRIPTOR,
45    <SetPositionTargetGlobalInt as Message>::DESCRIPTOR,
46    <BatteryStatus as Message>::DESCRIPTOR,
47    <AutopilotVersion as Message>::DESCRIPTOR,
48    <HomePosition as Message>::DESCRIPTOR,
49    <ExtendedSysState as Message>::DESCRIPTOR,
50    <Statustext as Message>::DESCRIPTOR,
51];
52
53message! {
54    /// `HEARTBEAT`: the periodic broadcast every MAVLink node sends to announce its type,
55    /// autopilot, and status, and the frame a peer waits for before deciding a link is up.
56    Heartbeat = 0, crc = 50, name = "HEARTBEAT";
57    custom_mode: u32,
58    type_: u8,
59    autopilot: u8,
60    base_mode: u8,
61    system_status: u8,
62    mavlink_version: u8,
63}
64
65message! {
66    /// `SYS_STATUS`: the vehicle's onboard sensor health, load, and battery state.
67    SysStatus = 1, crc = 124, name = "SYS_STATUS";
68    onboard_control_sensors_present: u32,
69    onboard_control_sensors_enabled: u32,
70    onboard_control_sensors_health: u32,
71    load: u16,
72    voltage_battery: u16,
73    current_battery: i16,
74    drop_rate_comm: u16,
75    errors_comm: u16,
76    errors_count1: u16,
77    errors_count2: u16,
78    errors_count3: u16,
79    errors_count4: u16,
80    battery_remaining: i8;
81    ext {
82        onboard_control_sensors_present_extended: u32,
83        onboard_control_sensors_enabled_extended: u32,
84        onboard_control_sensors_health_extended: u32,
85    }
86}
87
88message! {
89    /// `SYSTEM_TIME`: the vehicle's Unix and boot time, used to align clocks across a link.
90    SystemTime = 2, crc = 137, name = "SYSTEM_TIME";
91    time_unix_usec: u64,
92    time_boot_ms: u32,
93}
94
95message! {
96    /// `PING`: a round-trip timing and reachability probe.
97    Ping = 4, crc = 237, name = "PING";
98    time_usec: u64,
99    seq: u32,
100    target_system: u8,
101    target_component: u8,
102}
103
104message! {
105    /// `ATTITUDE`: the vehicle's orientation and angular rates in radians.
106    Attitude = 30, crc = 39, name = "ATTITUDE";
107    time_boot_ms: u32,
108    roll: f32,
109    pitch: f32,
110    yaw: f32,
111    rollspeed: f32,
112    pitchspeed: f32,
113    yawspeed: f32,
114}
115
116message! {
117    /// `GLOBAL_POSITION_INT`: the fused global position, altitude, and velocity.
118    GlobalPositionInt = 33, crc = 104, name = "GLOBAL_POSITION_INT";
119    time_boot_ms: u32,
120    lat: i32,
121    lon: i32,
122    alt: i32,
123    relative_alt: i32,
124    vx: i16,
125    vy: i16,
126    vz: i16,
127    hdg: u16,
128}
129
130message! {
131    /// `COMMAND_LONG`: a command for the vehicle to run, with up to seven float parameters.
132    CommandLong = 76, crc = 152, name = "COMMAND_LONG";
133    param1: f32,
134    param2: f32,
135    param3: f32,
136    param4: f32,
137    param5: f32,
138    param6: f32,
139    param7: f32,
140    command: u16,
141    target_system: u8,
142    target_component: u8,
143    confirmation: u8,
144}
145
146message! {
147    /// `COMMAND_ACK`: the vehicle's acknowledgment of a command, carrying the result.
148    ///
149    /// The extension fields report progress for a long-running command
150    /// ([`mav_result::IN_PROGRESS`](crate::dialect::mav_result::IN_PROGRESS)), a
151    /// command-specific second result value, and which system and component the
152    /// acknowledgment is addressed to.
153    CommandAck = 77, crc = 143, name = "COMMAND_ACK";
154    command: u16,
155    result: u8;
156    ext {
157        progress: u8,
158        result_param2: i32,
159        target_system: u8,
160        target_component: u8,
161    }
162}
163
164message! {
165    /// `COMMAND_INT`: a command in a coordinate frame, with integer-encoded position.
166    CommandInt = 75, crc = 158, name = "COMMAND_INT";
167    param1: f32,
168    param2: f32,
169    param3: f32,
170    param4: f32,
171    x: i32,
172    y: i32,
173    z: f32,
174    command: u16,
175    target_system: u8,
176    target_component: u8,
177    frame: u8,
178    current: u8,
179    autocontinue: u8,
180}
181
182message! {
183    /// `SET_MODE`: requests the vehicle switch base and custom flight modes.
184    SetMode = 11, crc = 89, name = "SET_MODE";
185    custom_mode: u32,
186    target_system: u8,
187    base_mode: u8,
188}
189
190message! {
191    /// `MANUAL_CONTROL`: a pilot's stick and button input, normalized to a fixed range.
192    ManualControl = 69, crc = 243, name = "MANUAL_CONTROL";
193    x: i16,
194    y: i16,
195    z: i16,
196    r: i16,
197    buttons: u16,
198    target: u8;
199    ext {
200        buttons2: u16,
201        enabled_extensions: u8,
202        s: i16,
203        t: i16,
204        aux1: i16,
205        aux2: i16,
206        aux3: i16,
207        aux4: i16,
208        aux5: i16,
209        aux6: i16,
210    }
211}
212
213message! {
214    /// `PARAM_REQUEST_READ`: asks the vehicle for one onboard parameter, by name or index.
215    ParamRequestRead = 20, crc = 214, name = "PARAM_REQUEST_READ";
216    param_index: i16,
217    target_system: u8,
218    target_component: u8,
219    param_id: [char; 16],
220}
221
222message! {
223    /// `PARAM_REQUEST_LIST`: asks the vehicle to stream all of its onboard parameters.
224    ParamRequestList = 21, crc = 159, name = "PARAM_REQUEST_LIST";
225    target_system: u8,
226    target_component: u8,
227}
228
229message! {
230    /// `PARAM_VALUE`: one onboard parameter's value, type, and position in the set.
231    ParamValue = 22, crc = 220, name = "PARAM_VALUE";
232    param_value: f32,
233    param_count: u16,
234    param_index: u16,
235    param_id: [char; 16],
236    param_type: u8,
237}
238
239message! {
240    /// `PARAM_SET`: sets one onboard parameter on the vehicle.
241    ParamSet = 23, crc = 168, name = "PARAM_SET";
242    param_value: f32,
243    target_system: u8,
244    target_component: u8,
245    param_id: [char; 16],
246    param_type: u8,
247}
248
249message! {
250    /// `GPS_RAW_INT`: the raw GPS fix: position, accuracy, speed, and satellite count.
251    GpsRawInt = 24, crc = 24, name = "GPS_RAW_INT";
252    time_usec: u64,
253    lat: i32,
254    lon: i32,
255    alt: i32,
256    eph: u16,
257    epv: u16,
258    vel: u16,
259    cog: u16,
260    fix_type: u8,
261    satellites_visible: u8;
262    ext { alt_ellipsoid: i32, h_acc: u32, v_acc: u32, vel_acc: u32, hdg_acc: u32, yaw: u16 }
263}
264
265message! {
266    /// `ATTITUDE_QUATERNION`: orientation as a quaternion, with angular rates.
267    AttitudeQuaternion = 31, crc = 246, name = "ATTITUDE_QUATERNION";
268    time_boot_ms: u32,
269    q1: f32,
270    q2: f32,
271    q3: f32,
272    q4: f32,
273    rollspeed: f32,
274    pitchspeed: f32,
275    yawspeed: f32;
276    ext { repr_offset_q: [f32; 4] }
277}
278
279message! {
280    /// `LOCAL_POSITION_NED`: the vehicle's local position and velocity in the NED frame.
281    LocalPositionNed = 32, crc = 185, name = "LOCAL_POSITION_NED";
282    time_boot_ms: u32,
283    x: f32,
284    y: f32,
285    z: f32,
286    vx: f32,
287    vy: f32,
288    vz: f32,
289}
290
291message! {
292    /// `SERVO_OUTPUT_RAW`: the raw PWM values driving the first eight servo outputs.
293    ServoOutputRaw = 36, crc = 222, name = "SERVO_OUTPUT_RAW";
294    time_usec: u32,
295    servo1_raw: u16,
296    servo2_raw: u16,
297    servo3_raw: u16,
298    servo4_raw: u16,
299    servo5_raw: u16,
300    servo6_raw: u16,
301    servo7_raw: u16,
302    servo8_raw: u16,
303    port: u8;
304    ext {
305        servo9_raw: u16,
306        servo10_raw: u16,
307        servo11_raw: u16,
308        servo12_raw: u16,
309        servo13_raw: u16,
310        servo14_raw: u16,
311        servo15_raw: u16,
312        servo16_raw: u16,
313    }
314}
315
316message! {
317    /// `RC_CHANNELS`: the raw values of up to eighteen RC input channels.
318    RcChannels = 65, crc = 118, name = "RC_CHANNELS";
319    time_boot_ms: u32,
320    chan1_raw: u16,
321    chan2_raw: u16,
322    chan3_raw: u16,
323    chan4_raw: u16,
324    chan5_raw: u16,
325    chan6_raw: u16,
326    chan7_raw: u16,
327    chan8_raw: u16,
328    chan9_raw: u16,
329    chan10_raw: u16,
330    chan11_raw: u16,
331    chan12_raw: u16,
332    chan13_raw: u16,
333    chan14_raw: u16,
334    chan15_raw: u16,
335    chan16_raw: u16,
336    chan17_raw: u16,
337    chan18_raw: u16,
338    chancount: u8,
339    rssi: u8,
340}
341
342message! {
343    /// `VFR_HUD`: the heads-up flight summary: speeds, heading, throttle, altitude, climb.
344    VfrHud = 74, crc = 20, name = "VFR_HUD";
345    airspeed: f32,
346    groundspeed: f32,
347    alt: f32,
348    climb: f32,
349    heading: i16,
350    throttle: u16,
351}
352
353message! {
354    /// `BATTERY_STATUS`: a battery's charge, current, temperature, and per-cell voltages.
355    BatteryStatus = 147, crc = 154, name = "BATTERY_STATUS";
356    current_consumed: i32,
357    energy_consumed: i32,
358    temperature: i16,
359    voltages: [u16; 10],
360    current_battery: i16,
361    id: u8,
362    battery_function: u8,
363    type_: u8,
364    battery_remaining: i8;
365    ext {
366        time_remaining: i32,
367        charge_state: u8,
368        voltages_ext: [u16; 4],
369        mode: u8,
370        fault_bitmask: u32,
371    }
372}
373
374message! {
375    /// `MISSION_REQUEST_LIST`: asks the vehicle to begin downloading a plan by reporting its
376    /// [`MissionCount`].
377    MissionRequestList = 43, crc = 132, name = "MISSION_REQUEST_LIST";
378    target_system: u8,
379    target_component: u8;
380    ext { mission_type: u8 }
381}
382
383message! {
384    /// `MISSION_COUNT`: announces how many items a mission transfer will contain, and which
385    /// plan (mission, geofence, or rally) the transfer carries.
386    MissionCount = 44, crc = 221, name = "MISSION_COUNT";
387    count: u16,
388    target_system: u8,
389    target_component: u8;
390    ext { mission_type: u8, opaque_id: u32 }
391}
392
393message! {
394    /// `MISSION_REQUEST_INT`: requests one mission item by sequence number, with position
395    /// returned integer-encoded.
396    MissionRequestInt = 51, crc = 196, name = "MISSION_REQUEST_INT";
397    seq: u16,
398    target_system: u8,
399    target_component: u8;
400    ext { mission_type: u8 }
401}
402
403message! {
404    /// `MISSION_REQUEST`: requests one mission item by sequence number, the legacy request
405    /// some autopilots still send in place of [`MissionRequestInt`].
406    MissionRequest = 40, crc = 230, name = "MISSION_REQUEST";
407    seq: u16,
408    target_system: u8,
409    target_component: u8;
410    ext { mission_type: u8 }
411}
412
413message! {
414    /// `MISSION_ITEM_INT`: one mission item, with integer-encoded position.
415    MissionItemInt = 73, crc = 38, name = "MISSION_ITEM_INT";
416    param1: f32,
417    param2: f32,
418    param3: f32,
419    param4: f32,
420    x: i32,
421    y: i32,
422    z: f32,
423    seq: u16,
424    command: u16,
425    target_system: u8,
426    target_component: u8,
427    frame: u8,
428    current: u8,
429    autocontinue: u8;
430    ext { mission_type: u8 }
431}
432
433message! {
434    /// `MISSION_CURRENT`: the sequence number of the mission item the vehicle is running.
435    MissionCurrent = 42, crc = 28, name = "MISSION_CURRENT";
436    seq: u16;
437    ext {
438        total: u16,
439        mission_state: u8,
440        mission_mode: u8,
441        mission_id: u32,
442        fence_id: u32,
443        rally_points_id: u32,
444    }
445}
446
447message! {
448    /// `MISSION_CLEAR_ALL`: asks the vehicle to erase a stored plan.
449    MissionClearAll = 45, crc = 232, name = "MISSION_CLEAR_ALL";
450    target_system: u8,
451    target_component: u8;
452    ext { mission_type: u8 }
453}
454
455message! {
456    /// `MISSION_ACK`: acknowledges the end of a mission transfer with a result.
457    MissionAck = 47, crc = 153, name = "MISSION_ACK";
458    target_system: u8,
459    target_component: u8,
460    type_: u8;
461    ext { mission_type: u8, opaque_id: u32 }
462}
463
464message! {
465    /// `SET_POSITION_TARGET_LOCAL_NED`: an offboard position, velocity, or thrust setpoint
466    /// in the local NED frame.
467    SetPositionTargetLocalNed = 84, crc = 143, name = "SET_POSITION_TARGET_LOCAL_NED";
468    time_boot_ms: u32,
469    x: f32,
470    y: f32,
471    z: f32,
472    vx: f32,
473    vy: f32,
474    vz: f32,
475    afx: f32,
476    afy: f32,
477    afz: f32,
478    yaw: f32,
479    yaw_rate: f32,
480    type_mask: u16,
481    target_system: u8,
482    target_component: u8,
483    coordinate_frame: u8,
484}
485
486message! {
487    /// `SET_POSITION_TARGET_GLOBAL_INT`: an offboard setpoint in the global frame, with
488    /// integer-encoded latitude and longitude.
489    SetPositionTargetGlobalInt = 86, crc = 5, name = "SET_POSITION_TARGET_GLOBAL_INT";
490    time_boot_ms: u32,
491    lat_int: i32,
492    lon_int: i32,
493    alt: f32,
494    vx: f32,
495    vy: f32,
496    vz: f32,
497    afx: f32,
498    afy: f32,
499    afz: f32,
500    yaw: f32,
501    yaw_rate: f32,
502    type_mask: u16,
503    target_system: u8,
504    target_component: u8,
505    coordinate_frame: u8,
506}
507
508message! {
509    /// `HOME_POSITION`: the vehicle's home location and the approach vector to it.
510    HomePosition = 242, crc = 104, name = "HOME_POSITION";
511    latitude: i32,
512    longitude: i32,
513    altitude: i32,
514    x: f32,
515    y: f32,
516    z: f32,
517    q: [f32; 4],
518    approach_x: f32,
519    approach_y: f32,
520    approach_z: f32;
521    ext { time_usec: u64 }
522}
523
524message! {
525    /// `AUTOPILOT_VERSION`: the autopilot's capability flags and firmware versions.
526    AutopilotVersion = 148, crc = 178, name = "AUTOPILOT_VERSION";
527    capabilities: u64,
528    uid: u64,
529    flight_sw_version: u32,
530    middleware_sw_version: u32,
531    os_sw_version: u32,
532    board_version: u32,
533    vendor_id: u16,
534    product_id: u16,
535    flight_custom_version: [u8; 8],
536    middleware_custom_version: [u8; 8],
537    os_custom_version: [u8; 8];
538    ext { uid2: [u8; 18] }
539}
540
541message! {
542    /// `EXTENDED_SYS_STATE`: the VTOL and landed state of the vehicle.
543    ExtendedSysState = 245, crc = 130, name = "EXTENDED_SYS_STATE";
544    vtol_state: u8,
545    landed_state: u8,
546}
547
548message! {
549    /// `STATUSTEXT`: a human-readable status message with a severity.
550    Statustext = 253, crc = 83, name = "STATUSTEXT";
551    severity: u8,
552    text: [char; 50];
553    ext { id: u16, chunk_seq: u8 }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::dialect::{crc_extra, Message};
560    use crate::message_crc_extra;
561
562    // Re-derives a message's CRC_EXTRA from its field definitions and checks it against the
563    // official dialect value the message declares and the registry returns. A wrong field
564    // type, name, or order changes the derived seed, so this catches it.
565    fn verify<M: Message>() {
566        assert_eq!(
567            message_crc_extra(M::NAME, M::BASE_FIELDS),
568            M::CRC_EXTRA,
569            "derived CRC_EXTRA disagrees with the dialect for {}",
570            M::NAME
571        );
572        assert_eq!(
573            crc_extra(M::ID),
574            Some(M::CRC_EXTRA),
575            "registry disagrees for {}",
576            M::NAME
577        );
578    }
579
580    #[test]
581    fn every_message_crc_extra_matches_the_dialect() {
582        verify::<Heartbeat>();
583        verify::<SysStatus>();
584        verify::<SystemTime>();
585        verify::<Ping>();
586        verify::<Attitude>();
587        verify::<GlobalPositionInt>();
588        verify::<CommandLong>();
589        verify::<CommandAck>();
590        verify::<CommandInt>();
591        verify::<SetMode>();
592        verify::<ManualControl>();
593        verify::<ParamRequestRead>();
594        verify::<ParamRequestList>();
595        verify::<ParamValue>();
596        verify::<ParamSet>();
597        verify::<GpsRawInt>();
598        verify::<AttitudeQuaternion>();
599        verify::<LocalPositionNed>();
600        verify::<ServoOutputRaw>();
601        verify::<RcChannels>();
602        verify::<VfrHud>();
603        verify::<BatteryStatus>();
604        verify::<MissionRequestList>();
605        verify::<MissionCount>();
606        verify::<MissionRequestInt>();
607        verify::<MissionRequest>();
608        verify::<MissionItemInt>();
609        verify::<MissionCurrent>();
610        verify::<MissionClearAll>();
611        verify::<MissionAck>();
612        verify::<SetPositionTargetLocalNed>();
613        verify::<SetPositionTargetGlobalInt>();
614        verify::<HomePosition>();
615        verify::<AutopilotVersion>();
616        verify::<ExtendedSysState>();
617        verify::<Statustext>();
618    }
619
620    #[test]
621    fn a_heartbeat_encodes_to_the_documented_frame() {
622        // A published worked example of a v1 HEARTBEAT on the wire (custom_mode 0, a
623        // quadrotor running ArduPilot, base_mode 0x51, status active). Encoding the same
624        // message must reproduce it byte for byte, checksum included, which anchors the
625        // whole v1 frame layout to an external reference rather than a round-trip.
626        let heartbeat = Heartbeat {
627            custom_mode: 0,
628            type_: 2,
629            autopilot: 3,
630            base_mode: 0x51,
631            system_status: 4,
632            mavlink_version: 3,
633        };
634        let mut payload = [0u8; 255];
635        let len = heartbeat.encode(&mut payload);
636        let frame = crate::Frame::encode_v1(
637            crate::Header::new(1, 1, 0x4E),
638            Heartbeat::ID,
639            &payload[..len],
640            Heartbeat::CRC_EXTRA,
641        )
642        .unwrap();
643        let expected = [
644            0xFE, 0x09, 0x4E, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x51, 0x04,
645            0x03, 0x1C, 0x7F,
646        ];
647        assert_eq!(frame.as_bytes(), &expected);
648    }
649
650    #[test]
651    fn a_heartbeat_round_trips_through_its_payload() {
652        let heartbeat = Heartbeat {
653            custom_mode: 0x0A0B0C0D,
654            type_: 2,
655            autopilot: 12,
656            base_mode: 0x81,
657            system_status: 4,
658            mavlink_version: 3,
659        };
660        let mut buf = [0u8; 255];
661        let len = heartbeat.encode(&mut buf);
662        assert_eq!(len, Heartbeat::WIRE_LEN);
663        assert_eq!(Heartbeat::decode(&buf[..len]).unwrap(), heartbeat);
664    }
665
666    #[test]
667    fn a_command_long_round_trips_through_field_reordering() {
668        // COMMAND_LONG mixes 4-, 2-, and 1-byte fields, so this exercises the wire
669        // reordering the declaration encodes (floats first, then the command, then the bytes).
670        let command = CommandLong {
671            param1: 1.0,
672            param2: 0.0,
673            param3: 0.0,
674            param4: 0.0,
675            param5: 12.34,
676            param6: 56.78,
677            param7: 100.0,
678            command: 400, // MAV_CMD_COMPONENT_ARM_DISARM
679            target_system: 1,
680            target_component: 1,
681            confirmation: 0,
682        };
683        let mut buf = [0u8; 255];
684        let len = command.encode(&mut buf);
685        assert_eq!(len, CommandLong::WIRE_LEN);
686        assert_eq!(CommandLong::decode(&buf[..len]).unwrap(), command);
687    }
688
689    #[test]
690    fn an_extension_field_round_trips_after_the_base_fields() {
691        // MISSION_COUNT gained the `mission_type` and `opaque_id` extension fields; a full
692        // encode carries them after the base fields and decodes them back.
693        let count = MissionCount {
694            count: 7,
695            target_system: 1,
696            target_component: 1,
697            mission_type: crate::dialect::mav_mission_type::FENCE,
698            opaque_id: 0xDEAD_BEEF,
699        };
700        let mut buf = [0u8; 255];
701        let len = count.encode(&mut buf);
702        assert_eq!(len, MissionCount::WIRE_LEN);
703        // Base fields (count, targets) occupy four bytes; the extensions follow.
704        assert_eq!(len, 4 + 1 + 4);
705        assert_eq!(MissionCount::decode(&buf[..len]).unwrap(), count);
706    }
707
708    #[test]
709    fn a_base_only_payload_decodes_extensions_as_zero() {
710        // A peer that sends only the base fields (a MAVLink 1 sender, or truncated MAVLink 2)
711        // still decodes; the extensions read as zero, which is what keeps a message that gains
712        // extensions compatible with one that predates them.
713        let base_only = [7u8, 0, 1, 1]; // count = 7, target_system = 1, target_component = 1
714        let decoded = MissionCount::decode(&base_only).unwrap();
715        assert_eq!(decoded.count, 7);
716        assert_eq!(decoded.mission_type, 0);
717        assert_eq!(decoded.opaque_id, 0);
718    }
719
720    #[test]
721    fn command_ack_keeps_its_seed_after_gaining_extensions() {
722        // The extensions are excluded from CRC_EXTRA, so COMMAND_ACK's seed is still the
723        // pre-extension value a peer that only knows the base fields computes.
724        assert_eq!(
725            message_crc_extra("COMMAND_ACK", CommandAck::BASE_FIELDS),
726            CommandAck::CRC_EXTRA
727        );
728        assert_eq!(CommandAck::CRC_EXTRA, 143);
729        // The base fields drive the seed; the extensions are not among them.
730        assert_eq!(CommandAck::BASE_FIELDS.len(), 2);
731    }
732
733    #[test]
734    fn decode_zero_extends_a_truncated_payload() {
735        // A HEARTBEAT whose trailing bytes were truncated to a single zero still decodes,
736        // with the missing fields read as zero.
737        let decoded = Heartbeat::decode(&[0]).unwrap();
738        assert_eq!(decoded, Heartbeat::default_zeroed());
739    }
740
741    impl Heartbeat {
742        fn default_zeroed() -> Self {
743            Heartbeat {
744                custom_mode: 0,
745                type_: 0,
746                autopilot: 0,
747                base_mode: 0,
748                system_status: 0,
749                mavlink_version: 0,
750            }
751        }
752    }
753}