uf_crsf/packets/
commands.rs

1use crate::packets::{CrsfPacket, PacketType};
2use crate::CrsfParsingError;
3use crc::Crc;
4use heapless::Vec;
5
6pub const COMMAND_CRC_ALGO: Crc<u8> = Crc::<u8>::new(&crc::Algorithm {
7    width: 8,
8    poly: 0xBA,
9    init: 0x00,
10    refin: false,
11    refout: false,
12    xorout: 0x00,
13    check: 0x00,
14    residue: 0x00,
15});
16
17// Command IDs
18const COMMAND_ID_FC: u8 = 0x01;
19const COMMAND_ID_OSD: u8 = 0x05;
20const COMMAND_ID_VTX: u8 = 0x08;
21const COMMAND_ID_CROSSFIRE: u8 = 0x10;
22const COMMAND_ID_FLOW_CONTROL: u8 = 0x20;
23const COMMAND_ID_ACK: u8 = 0xFF;
24
25// FC Sub-command IDs
26const SUB_COMMAND_ID_FC_FORCE_DISARM: u8 = 0x01;
27const SUB_COMMAND_ID_FC_SCALE_CHANNEL: u8 = 0x02;
28
29// OSD Sub-command IDs
30const SUB_COMMAND_ID_OSD_SEND_BUTTONS: u8 = 0x01;
31
32// VTX Sub-command IDs
33const SUB_COMMAND_ID_VTX_SET_FREQUENCY: u8 = 0x02;
34const SUB_COMMAND_ID_VTX_ENABLE_PIT_MODE_ON_POWER_UP: u8 = 0x04;
35const SUB_COMMAND_ID_VTX_POWER_UP_FROM_PIT_MODE: u8 = 0x05;
36const SUB_COMMAND_ID_VTX_SET_DYNAMIC_POWER: u8 = 0x06;
37const SUB_COMMAND_ID_VTX_SET_POWER: u8 = 0x08;
38
39// Crossfire Sub-command IDs
40const SUB_COMMAND_ID_CROSSFIRE_SET_RECEIVER_IN_BIND_MODE: u8 = 0x01;
41const SUB_COMMAND_ID_CROSSFIRE_CANCEL_BIND_MODE: u8 = 0x02;
42const SUB_COMMAND_ID_CROSSFIRE_SET_BIND_ID: u8 = 0x03;
43const SUB_COMMAND_ID_CROSSFIRE_MODEL_SELECTION: u8 = 0x05;
44const SUB_COMMAND_ID_CROSSFIRE_CURRENT_MODEL_SELECTION: u8 = 0x06;
45const SUB_COMMAND_ID_CROSSFIRE_REPLY_CURRENT_MODEL_SELECTION: u8 = 0x07;
46
47// Flow Control Sub-command IDs
48const SUB_COMMAND_ID_FLOW_CONTROL_SUBSCRIBE: u8 = 0x01;
49const SUB_COMMAND_ID_FLOW_CONTROL_UNSUBSCRIBE: u8 = 0x02;
50
51/// Represents a Direct Commands packet (frame type 0x32).
52#[derive(Clone, Debug, PartialEq)]
53#[cfg_attr(feature = "defmt", derive(defmt::Format))]
54pub struct DirectCommands {
55    pub dst_addr: u8,
56    pub src_addr: u8,
57    pub payload: CommandPayload,
58}
59
60/// Enum for the different payloads of a DirectCommands packet.
61#[derive(Clone, Debug, PartialEq)]
62#[cfg_attr(feature = "defmt", derive(defmt::Format))]
63pub enum CommandPayload {
64    Fc(FcCommand),
65    Osd(OsdCommand),
66    Vtx(VtxCommand),
67    Crossfire(CrossfireCommand),
68    FlowControl(FlowControlCommand),
69    Ack(CommandAck),
70}
71
72/// FC Commands (command ID 0x01)
73#[derive(Clone, Debug, PartialEq)]
74#[cfg_attr(feature = "defmt", derive(defmt::Format))]
75pub enum FcCommand {
76    ForceDisarm,
77    ScaleChannel,
78}
79
80/// OSD Commands (command ID 0x05)
81#[derive(Clone, Debug, PartialEq)]
82#[cfg_attr(feature = "defmt", derive(defmt::Format))]
83pub enum OsdCommand {
84    SendButtons(u8),
85}
86
87/// VTX Commands (command ID 0x08)
88#[derive(Clone, Debug, PartialEq)]
89#[cfg_attr(feature = "defmt", derive(defmt::Format))]
90pub enum VtxCommand {
91    SetFrequency(u16),
92    EnablePitModeOnPowerUp {
93        pit_mode: bool,
94        pit_mode_control: u8,
95        pit_mode_switch: u8,
96    },
97    PowerUpFromPitMode,
98    SetDynamicPower(u8),
99    SetPower(u8),
100}
101
102/// Crossfire Commands (command ID 0x10)
103#[derive(Clone, Debug, PartialEq)]
104#[cfg_attr(feature = "defmt", derive(defmt::Format))]
105pub enum CrossfireCommand {
106    SetReceiverInBindMode,
107    CancelBindMode,
108    SetBindId,
109    ModelSelection(u8),
110    CurrentModelSelection,
111    ReplyCurrentModelSelection(u8),
112}
113
114/// Flow Control Commands (command ID 0x20)
115#[derive(Clone, Debug, PartialEq)]
116#[cfg_attr(feature = "defmt", derive(defmt::Format))]
117pub enum FlowControlCommand {
118    Subscribe {
119        frame_type: u8,
120        max_interval_time: u16,
121    },
122    Unsubscribe {
123        frame_type: u8,
124    },
125}
126
127/// Command ACK (command ID 0xFF)
128#[derive(Clone, Debug, PartialEq)]
129pub struct CommandAck {
130    pub command_id: u8,
131    pub sub_command_id: u8,
132    pub action: u8,           // 0 = rejected, 1 = accepted
133    information: Vec<u8, 48>, // Variable length string
134}
135
136impl CommandAck {
137    /// Creates a new CommandAck.
138    pub fn new(
139        command_id: u8,
140        sub_command_id: u8,
141        action: u8,
142        information: &[u8],
143    ) -> Result<Self, CrsfParsingError> {
144        if information.len() > 48 {
145            return Err(CrsfParsingError::InvalidPayloadLength);
146        }
147        let mut info = Vec::new();
148        info.extend_from_slice(information)
149            .map_err(|_| CrsfParsingError::InvalidPayloadLength)?;
150        Ok(Self {
151            command_id,
152            sub_command_id,
153            action,
154            information: info,
155        })
156    }
157
158    /// Returns the information payload as a slice.
159    pub fn information(&self) -> &[u8] {
160        &self.information
161    }
162}
163
164#[cfg(feature = "defmt")]
165impl defmt::Format for CommandAck {
166    fn format(&self, fmt: defmt::Formatter) {
167        defmt::write!(
168            fmt,
169            "CommandAck {{ command_id: {}, sub_command_id: {}, action: {}, information: {} }}",
170            self.command_id,
171            self.sub_command_id,
172            self.action,
173            self.information(),
174        )
175    }
176}
177
178impl CrsfPacket for DirectCommands {
179    const PACKET_TYPE: PacketType = PacketType::Command;
180    // dst, src, cmd_id, crc
181    const MIN_PAYLOAD_SIZE: usize = 4;
182
183    fn from_bytes(data: &[u8]) -> Result<Self, CrsfParsingError> {
184        if data.len() < Self::MIN_PAYLOAD_SIZE {
185            return Err(CrsfParsingError::InvalidPayloadLength);
186        }
187
188        // data = [dst, src, cmd_id, payload..., crc]
189        let crc_byte_index = data.len() - 1;
190        let received_crc = data[crc_byte_index];
191        let payload_with_headers = &data[..crc_byte_index];
192
193        // CRC is calculated over [type, dst, src, cmd_id, payload...]
194        let mut digest = COMMAND_CRC_ALGO.digest();
195        digest.update(&[Self::PACKET_TYPE as u8]);
196        digest.update(payload_with_headers);
197        let calculated_crc = digest.finalize();
198
199        if received_crc != calculated_crc {
200            return Err(CrsfParsingError::InvalidPayload);
201        }
202
203        let dst_addr = data[0];
204        let src_addr = data[1];
205        let command_id = data[2];
206        let command_payload_data = &data[3..crc_byte_index];
207
208        let payload = match command_id {
209            COMMAND_ID_FC => CommandPayload::Fc(FcCommand::try_from(command_payload_data)?),
210            COMMAND_ID_OSD => CommandPayload::Osd(OsdCommand::try_from(command_payload_data)?),
211            COMMAND_ID_VTX => CommandPayload::Vtx(VtxCommand::try_from(command_payload_data)?),
212            COMMAND_ID_CROSSFIRE => {
213                CommandPayload::Crossfire(CrossfireCommand::try_from(command_payload_data)?)
214            }
215            COMMAND_ID_FLOW_CONTROL => {
216                CommandPayload::FlowControl(FlowControlCommand::try_from(command_payload_data)?)
217            }
218            COMMAND_ID_ACK => CommandPayload::Ack(CommandAck::try_from(command_payload_data)?),
219            _ => return Err(CrsfParsingError::InvalidPayload), // Unknown command
220        };
221
222        Ok(Self {
223            dst_addr,
224            src_addr,
225            payload,
226        })
227    }
228
229    fn to_bytes(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
230        if buffer.len() < 3 {
231            return Err(CrsfParsingError::BufferOverflow);
232        }
233        buffer[0] = self.dst_addr;
234        buffer[1] = self.src_addr;
235        buffer[2] = self.payload.command_id();
236
237        let payload_len = self.payload.write_to(&mut buffer[3..])?;
238        let total_len = 3 + payload_len;
239
240        // Calculate and append CRC
241        // CRC is over [type, dst, src, cmd_id, payload...]
242        let mut digest = COMMAND_CRC_ALGO.digest();
243        digest.update(&[Self::PACKET_TYPE as u8]);
244        digest.update(&buffer[..total_len]);
245        let crc = digest.finalize();
246
247        if buffer.len() < total_len + 1 {
248            return Err(CrsfParsingError::BufferOverflow);
249        }
250        buffer[total_len] = crc;
251        Ok(total_len + 1)
252    }
253}
254
255impl CommandPayload {
256    fn command_id(&self) -> u8 {
257        match self {
258            CommandPayload::Fc(_) => COMMAND_ID_FC,
259            CommandPayload::Osd(_) => COMMAND_ID_OSD,
260            CommandPayload::Vtx(_) => COMMAND_ID_VTX,
261            CommandPayload::Crossfire(_) => COMMAND_ID_CROSSFIRE,
262            CommandPayload::FlowControl(_) => COMMAND_ID_FLOW_CONTROL,
263            CommandPayload::Ack(_) => COMMAND_ID_ACK,
264        }
265    }
266
267    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
268        match self {
269            CommandPayload::Fc(cmd) => cmd.write_to(buffer),
270            CommandPayload::Osd(cmd) => cmd.write_to(buffer),
271            CommandPayload::Vtx(cmd) => cmd.write_to(buffer),
272            CommandPayload::Crossfire(cmd) => cmd.write_to(buffer),
273            CommandPayload::FlowControl(cmd) => cmd.write_to(buffer),
274            CommandPayload::Ack(cmd) => cmd.write_to(buffer),
275        }
276    }
277}
278
279impl FcCommand {
280    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
281        if buffer.is_empty() {
282            return Err(CrsfParsingError::BufferOverflow);
283        }
284        buffer[0] = match self {
285            FcCommand::ForceDisarm => SUB_COMMAND_ID_FC_FORCE_DISARM,
286            FcCommand::ScaleChannel => SUB_COMMAND_ID_FC_SCALE_CHANNEL,
287        };
288        Ok(1)
289    }
290}
291
292impl<'a> TryFrom<&'a [u8]> for FcCommand {
293    type Error = CrsfParsingError;
294
295    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
296        if data.is_empty() {
297            return Err(CrsfParsingError::InvalidPayloadLength);
298        }
299        let sub_command_id = data[0];
300        match sub_command_id {
301            SUB_COMMAND_ID_FC_FORCE_DISARM => Ok(FcCommand::ForceDisarm),
302            SUB_COMMAND_ID_FC_SCALE_CHANNEL => Ok(FcCommand::ScaleChannel),
303            _ => Err(CrsfParsingError::InvalidPayload),
304        }
305    }
306}
307
308impl OsdCommand {
309    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
310        match self {
311            OsdCommand::SendButtons(buttons) => {
312                if buffer.len() < 2 {
313                    return Err(CrsfParsingError::BufferOverflow);
314                }
315                buffer[0] = SUB_COMMAND_ID_OSD_SEND_BUTTONS;
316                buffer[1] = *buttons;
317                Ok(2)
318            }
319        }
320    }
321}
322
323impl<'a> TryFrom<&'a [u8]> for OsdCommand {
324    type Error = CrsfParsingError;
325
326    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
327        if data.is_empty() {
328            return Err(CrsfParsingError::InvalidPayloadLength);
329        }
330        let sub_command_id = data[0];
331        match sub_command_id {
332            SUB_COMMAND_ID_OSD_SEND_BUTTONS => {
333                if data.len() < 2 {
334                    return Err(CrsfParsingError::InvalidPayloadLength);
335                }
336                Ok(OsdCommand::SendButtons(data[1]))
337            }
338            _ => Err(CrsfParsingError::InvalidPayload),
339        }
340    }
341}
342
343impl VtxCommand {
344    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
345        match self {
346            VtxCommand::SetFrequency(freq) => {
347                if buffer.len() < 3 {
348                    return Err(CrsfParsingError::BufferOverflow);
349                }
350                buffer[0] = SUB_COMMAND_ID_VTX_SET_FREQUENCY;
351                buffer[1..3].copy_from_slice(&freq.to_be_bytes());
352                Ok(3)
353            }
354            VtxCommand::EnablePitModeOnPowerUp {
355                pit_mode,
356                pit_mode_control,
357                pit_mode_switch,
358            } => {
359                if buffer.len() < 2 {
360                    return Err(CrsfParsingError::BufferOverflow);
361                }
362                buffer[0] = SUB_COMMAND_ID_VTX_ENABLE_PIT_MODE_ON_POWER_UP;
363                buffer[1] = (*pit_mode as u8) | (pit_mode_control << 1) | (pit_mode_switch << 3);
364                Ok(2)
365            }
366            VtxCommand::PowerUpFromPitMode => {
367                if buffer.is_empty() {
368                    return Err(CrsfParsingError::BufferOverflow);
369                }
370                buffer[0] = SUB_COMMAND_ID_VTX_POWER_UP_FROM_PIT_MODE;
371                Ok(1)
372            }
373            VtxCommand::SetDynamicPower(power) => {
374                if buffer.len() < 2 {
375                    return Err(CrsfParsingError::BufferOverflow);
376                }
377                buffer[0] = SUB_COMMAND_ID_VTX_SET_DYNAMIC_POWER;
378                buffer[1] = *power;
379                Ok(2)
380            }
381            VtxCommand::SetPower(power) => {
382                if buffer.len() < 2 {
383                    return Err(CrsfParsingError::BufferOverflow);
384                }
385                buffer[0] = SUB_COMMAND_ID_VTX_SET_POWER;
386                buffer[1] = *power;
387                Ok(2)
388            }
389        }
390    }
391}
392
393impl<'a> TryFrom<&'a [u8]> for VtxCommand {
394    type Error = CrsfParsingError;
395
396    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
397        if data.is_empty() {
398            return Err(CrsfParsingError::InvalidPayloadLength);
399        }
400        let sub_command_id = data[0];
401        let payload = &data[1..];
402        match sub_command_id {
403            SUB_COMMAND_ID_VTX_SET_FREQUENCY => {
404                if payload.len() < 2 {
405                    return Err(CrsfParsingError::InvalidPayloadLength);
406                }
407                let freq_bytes: [u8; 2] = payload[0..2]
408                    .try_into()
409                    .map_err(|_| CrsfParsingError::InvalidPayloadLength)?;
410                Ok(VtxCommand::SetFrequency(u16::from_be_bytes(freq_bytes)))
411            }
412            SUB_COMMAND_ID_VTX_ENABLE_PIT_MODE_ON_POWER_UP => {
413                if payload.is_empty() {
414                    return Err(CrsfParsingError::InvalidPayloadLength);
415                }
416                let byte = payload[0];
417                Ok(VtxCommand::EnablePitModeOnPowerUp {
418                    pit_mode: (byte & 0b1) != 0,
419                    pit_mode_control: (byte >> 1) & 0b11,
420                    pit_mode_switch: (byte >> 3) & 0b1111,
421                })
422            }
423            SUB_COMMAND_ID_VTX_POWER_UP_FROM_PIT_MODE => Ok(VtxCommand::PowerUpFromPitMode),
424            SUB_COMMAND_ID_VTX_SET_DYNAMIC_POWER => {
425                if payload.is_empty() {
426                    return Err(CrsfParsingError::InvalidPayloadLength);
427                }
428                Ok(VtxCommand::SetDynamicPower(payload[0]))
429            }
430            SUB_COMMAND_ID_VTX_SET_POWER => {
431                if payload.is_empty() {
432                    return Err(CrsfParsingError::InvalidPayloadLength);
433                }
434                Ok(VtxCommand::SetPower(payload[0]))
435            }
436            _ => Err(CrsfParsingError::InvalidPayload),
437        }
438    }
439}
440
441impl CrossfireCommand {
442    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
443        if buffer.is_empty() {
444            return Err(CrsfParsingError::BufferOverflow);
445        }
446        match self {
447            CrossfireCommand::SetReceiverInBindMode => {
448                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_SET_RECEIVER_IN_BIND_MODE;
449                Ok(1)
450            }
451            CrossfireCommand::CancelBindMode => {
452                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_CANCEL_BIND_MODE;
453                Ok(1)
454            }
455            CrossfireCommand::SetBindId => {
456                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_SET_BIND_ID;
457                Ok(1)
458            }
459            CrossfireCommand::ModelSelection(model) => {
460                if buffer.len() < 2 {
461                    return Err(CrsfParsingError::BufferOverflow);
462                }
463                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_MODEL_SELECTION;
464                buffer[1] = *model;
465                Ok(2)
466            }
467            CrossfireCommand::CurrentModelSelection => {
468                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_CURRENT_MODEL_SELECTION;
469                Ok(1)
470            }
471            CrossfireCommand::ReplyCurrentModelSelection(model) => {
472                if buffer.len() < 2 {
473                    return Err(CrsfParsingError::BufferOverflow);
474                }
475                buffer[0] = SUB_COMMAND_ID_CROSSFIRE_REPLY_CURRENT_MODEL_SELECTION;
476                buffer[1] = *model;
477                Ok(2)
478            }
479        }
480    }
481}
482
483impl<'a> TryFrom<&'a [u8]> for CrossfireCommand {
484    type Error = CrsfParsingError;
485
486    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
487        if data.is_empty() {
488            return Err(CrsfParsingError::InvalidPayloadLength);
489        }
490        let sub_command_id = data[0];
491        let payload = &data[1..];
492        match sub_command_id {
493            SUB_COMMAND_ID_CROSSFIRE_SET_RECEIVER_IN_BIND_MODE => {
494                Ok(CrossfireCommand::SetReceiverInBindMode)
495            }
496            SUB_COMMAND_ID_CROSSFIRE_CANCEL_BIND_MODE => Ok(CrossfireCommand::CancelBindMode),
497            SUB_COMMAND_ID_CROSSFIRE_SET_BIND_ID => Ok(CrossfireCommand::SetBindId),
498            SUB_COMMAND_ID_CROSSFIRE_MODEL_SELECTION => {
499                if payload.is_empty() {
500                    return Err(CrsfParsingError::InvalidPayloadLength);
501                }
502                Ok(CrossfireCommand::ModelSelection(payload[0]))
503            }
504            SUB_COMMAND_ID_CROSSFIRE_CURRENT_MODEL_SELECTION => {
505                Ok(CrossfireCommand::CurrentModelSelection)
506            }
507            SUB_COMMAND_ID_CROSSFIRE_REPLY_CURRENT_MODEL_SELECTION => {
508                if payload.is_empty() {
509                    return Err(CrsfParsingError::InvalidPayloadLength);
510                }
511                Ok(CrossfireCommand::ReplyCurrentModelSelection(payload[0]))
512            }
513            _ => Err(CrsfParsingError::InvalidPayload),
514        }
515    }
516}
517
518impl FlowControlCommand {
519    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
520        match self {
521            FlowControlCommand::Subscribe {
522                frame_type,
523                max_interval_time,
524            } => {
525                if buffer.len() < 4 {
526                    return Err(CrsfParsingError::BufferOverflow);
527                }
528                buffer[0] = SUB_COMMAND_ID_FLOW_CONTROL_SUBSCRIBE;
529                buffer[1] = *frame_type;
530                buffer[2..4].copy_from_slice(&max_interval_time.to_be_bytes());
531                Ok(4)
532            }
533            FlowControlCommand::Unsubscribe { frame_type } => {
534                if buffer.len() < 2 {
535                    return Err(CrsfParsingError::BufferOverflow);
536                }
537                buffer[0] = SUB_COMMAND_ID_FLOW_CONTROL_UNSUBSCRIBE;
538                buffer[1] = *frame_type;
539                Ok(2)
540            }
541        }
542    }
543}
544
545impl<'a> TryFrom<&'a [u8]> for FlowControlCommand {
546    type Error = CrsfParsingError;
547
548    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
549        if data.is_empty() {
550            return Err(CrsfParsingError::InvalidPayloadLength);
551        }
552        let sub_command_id = data[0];
553        let payload = &data[1..];
554        match sub_command_id {
555            SUB_COMMAND_ID_FLOW_CONTROL_SUBSCRIBE => {
556                if payload.len() < 3 {
557                    return Err(CrsfParsingError::InvalidPayloadLength);
558                }
559                let max_interval_time_bytes: [u8; 2] = payload[1..3]
560                    .try_into()
561                    .map_err(|_| CrsfParsingError::InvalidPayloadLength)?;
562                Ok(FlowControlCommand::Subscribe {
563                    frame_type: payload[0],
564                    max_interval_time: u16::from_be_bytes(max_interval_time_bytes),
565                })
566            }
567            SUB_COMMAND_ID_FLOW_CONTROL_UNSUBSCRIBE => {
568                if payload.is_empty() {
569                    return Err(CrsfParsingError::InvalidPayloadLength);
570                }
571                Ok(FlowControlCommand::Unsubscribe {
572                    frame_type: payload[0],
573                })
574            }
575            _ => Err(CrsfParsingError::InvalidPayload),
576        }
577    }
578}
579
580impl CommandAck {
581    fn write_to(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
582        let required_len = 3 + self.information.len();
583        if buffer.len() < required_len {
584            return Err(CrsfParsingError::BufferOverflow);
585        }
586        buffer[0] = self.command_id;
587        buffer[1] = self.sub_command_id;
588        buffer[2] = self.action;
589        buffer[3..required_len].copy_from_slice(&self.information);
590        Ok(required_len)
591    }
592}
593
594impl<'a> TryFrom<&'a [u8]> for CommandAck {
595    type Error = CrsfParsingError;
596
597    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
598        if data.len() < 3 {
599            return Err(CrsfParsingError::InvalidPayloadLength);
600        }
601        let mut information = Vec::new();
602        information
603            .extend_from_slice(&data[3..])
604            .map_err(|_| CrsfParsingError::BufferOverflow)?;
605        Ok(CommandAck {
606            command_id: data[0],
607            sub_command_id: data[1],
608            action: data[2],
609            information,
610        })
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    fn test_round_trip(packet: &DirectCommands) {
619        let mut buffer = [0u8; 64];
620        let len = packet.to_bytes(&mut buffer).unwrap();
621        let round_trip = DirectCommands::from_bytes(&buffer[..len]).unwrap();
622        assert_eq!(packet, &round_trip);
623        // make sure buffer overflow handled as proper error
624        let mut small_buffer = [0u8; 3];
625        let result = packet.to_bytes(&mut small_buffer);
626        assert!(matches!(result, Err(CrsfParsingError::BufferOverflow)));
627    }
628
629    #[test]
630    fn test_fc_command_force_disarm() {
631        test_round_trip(&DirectCommands {
632            dst_addr: 0xC8,
633            src_addr: 0xEA,
634            payload: CommandPayload::Fc(FcCommand::ForceDisarm),
635        });
636    }
637
638    #[test]
639    fn test_osd_send_buttons() {
640        test_round_trip(&DirectCommands {
641            dst_addr: 0x80,
642            src_addr: 0xEA,
643            payload: CommandPayload::Osd(OsdCommand::SendButtons(0b10101000)),
644        });
645    }
646
647    #[test]
648    fn test_vtx_set_frequency() {
649        test_round_trip(&DirectCommands {
650            dst_addr: 0xCE,
651            src_addr: 0xEA,
652            payload: CommandPayload::Vtx(VtxCommand::SetFrequency(5800)),
653        });
654    }
655
656    #[test]
657    fn test_crossfire_model_selection() {
658        test_round_trip(&DirectCommands {
659            dst_addr: 0xEE,
660            src_addr: 0xEA,
661            payload: CommandPayload::Crossfire(CrossfireCommand::ModelSelection(5)),
662        });
663    }
664
665    #[test]
666    fn test_flow_control_subscribe() {
667        test_round_trip(&DirectCommands {
668            dst_addr: 0xC8,
669            src_addr: 0xEA,
670            payload: CommandPayload::FlowControl(FlowControlCommand::Subscribe {
671                frame_type: 0x14, // Link Statistics
672                max_interval_time: 1000,
673            }),
674        });
675    }
676
677    #[test]
678    fn test_command_ack() {
679        test_round_trip(&DirectCommands {
680            dst_addr: 0xEA,
681            src_addr: 0xEE,
682            payload: CommandPayload::Ack(CommandAck::new(0x10, 0x01, 1, b"OK").unwrap()),
683        });
684    }
685
686    #[test]
687    fn test_from_bytes_invalid_crc() {
688        let data: [u8; 5] = [0xC8, 0xEA, 0x01, 0x01, 0x00]; // wrong crc
689        let result = DirectCommands::from_bytes(&data);
690        assert!(matches!(result, Err(CrsfParsingError::InvalidPayload)));
691    }
692
693    #[test]
694    fn test_small_buffer() {
695        let packet = DirectCommands {
696            dst_addr: 0xC8,
697            src_addr: 0xEA,
698            payload: CommandPayload::Fc(FcCommand::ForceDisarm),
699        };
700
701        let mut buffer = [0u8; 2];
702        let result = packet.to_bytes(&mut buffer);
703        assert!(matches!(result, Err(CrsfParsingError::BufferOverflow)));
704    }
705}