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
17const 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
25const SUB_COMMAND_ID_FC_FORCE_DISARM: u8 = 0x01;
27const SUB_COMMAND_ID_FC_SCALE_CHANNEL: u8 = 0x02;
28
29const SUB_COMMAND_ID_OSD_SEND_BUTTONS: u8 = 0x01;
31
32const 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
39const 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
47const SUB_COMMAND_ID_FLOW_CONTROL_SUBSCRIBE: u8 = 0x01;
49const SUB_COMMAND_ID_FLOW_CONTROL_UNSUBSCRIBE: u8 = 0x02;
50
51#[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#[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#[derive(Clone, Debug, PartialEq)]
74#[cfg_attr(feature = "defmt", derive(defmt::Format))]
75pub enum FcCommand {
76 ForceDisarm,
77 ScaleChannel,
78}
79
80#[derive(Clone, Debug, PartialEq)]
82#[cfg_attr(feature = "defmt", derive(defmt::Format))]
83pub enum OsdCommand {
84 SendButtons(u8),
85}
86
87#[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#[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#[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#[derive(Clone, Debug, PartialEq)]
129pub struct CommandAck {
130 pub command_id: u8,
131 pub sub_command_id: u8,
132 pub action: u8, information: Vec<u8, 48>, }
135
136impl CommandAck {
137 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 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 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 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 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), };
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 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 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, 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]; 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}