vex_cdc/cdc/
mod.rs

1//! Simple CDC packets.
2
3use crate::{
4    decode::{Decode, DecodeError, DecodeErrorKind},
5    encode::{Encode, MessageEncoder},
6    varint::VarU16,
7    version::Version,
8};
9
10use super::{COMMAND_HEADER, REPLY_HEADER};
11
12/// CDC packet opcodes.
13///
14/// These are the byte values identifying the different CDC commands.
15/// This module is non-exhaustive.
16pub mod cmds {
17    pub const ACK: u8 = 0x33;
18    pub const QUERY_1: u8 = 0x21;
19    pub const USER_CDC: u8 = 0x56;
20    pub const CON_CDC: u8 = 0x58;
21    pub const SYSTEM_VERSION: u8 = 0xA4;
22    pub const EEPROM_ERASE: u8 = 0x31;
23    pub const USER_ENTER: u8 = 0x60;
24    pub const USER_CATALOG: u8 = 0x61;
25    pub const FLASH_ERASE: u8 = 0x63;
26    pub const FLASH_WRITE: u8 = 0x64;
27    pub const FLASH_READ: u8 = 0x65;
28    pub const USER_EXIT: u8 = 0x66;
29    pub const USER_PLAY: u8 = 0x67;
30    pub const USER_STOP: u8 = 0x68;
31    pub const COMPONENT_GET: u8 = 0x69;
32    pub const USER_SLOT_GET: u8 = 0x78;
33    pub const USER_SLOT_SET: u8 = 0x79;
34    pub const BRAIN_NAME_GET: u8 = 0x44;
35}
36
37use bitflags::bitflags;
38use cmds::{QUERY_1, SYSTEM_VERSION};
39
40/// CDC (Simple) command packet.
41///
42/// A device-bound message containing a command identifier and an encoded payload.
43/// Each packet begins with a 4-byte [`COMMAND_HEADER`], followed by the opcode,
44/// and then an optional length-prefixed payload.
45///
46/// The payload type `P` must implement [`Encode`].
47///
48/// # Encoding
49///
50/// | Field     | Size   | Description |
51/// |-----------|--------|-------------|
52/// | `header`  | 4      | Must be [`COMMAND_HEADER`]. |
53/// | `cmd`     | 1      | A [CDC command opcode](crate::cdc::cmds) indicating the type of packet. |
54/// | `size`    | 1–2    | Size of `payload` encoded as a [`VarU16`]. |
55/// | `payload` | n      | Encoded payload. |
56#[derive(Clone, Copy, Eq, PartialEq)]
57pub struct CdcCommandPacket<const CMD: u8, P: Encode> {
58    payload: P,
59}
60
61impl<const CMD: u8, P: Encode> CdcCommandPacket<CMD, P> {
62    /// Header used for device-bound VEX CDC packets.
63    pub const HEADER: [u8; 4] = COMMAND_HEADER;
64
65    /// Creates a new device-bound packet with a given generic payload type.
66    pub fn new(payload: P) -> Self {
67        Self { payload }
68    }
69}
70
71impl<const CMD: u8, P: Encode> Encode for CdcCommandPacket<CMD, P> {
72    fn size(&self) -> usize {
73        let payload_size = self.payload.size();
74
75        5 + if payload_size == 0 {
76            0
77        } else if payload_size > (u8::MAX >> 1) as _ {
78            2
79        } else {
80            1
81        } + payload_size
82    }
83
84    fn encode(&self, data: &mut [u8]) {
85        Self::HEADER.encode(data);
86        data[4] = CMD;
87
88        let payload_size = self.payload.size();
89
90        // We only encode the payload size if there is a payload
91        if payload_size > 0 {
92            let mut enc = MessageEncoder::new_with_position(data, 5);
93
94            enc.write(&VarU16::new(payload_size as u16));
95            enc.write(&self.payload);
96        }
97    }
98}
99
100/// CDC (Simple) reply packet.
101///
102/// A host-bound packet sent in response to a [`CdcCommandPacket`].  
103/// Each reply consists of a 2-byte [`REPLY_HEADER`], the echoed command ID,
104/// a variable-width length field, and the decoded payload.
105///
106/// The payload type `P` must implement [`Decode`].
107///
108/// # Encoding
109///
110/// | Field     | Size   | Description |
111/// |-----------|--------|-------------|
112/// | `header`  | 2      | Must be [`REPLY_HEADER`]. |
113/// | `cmd`     | 1      | A [CDC command opcode](crate::cdc::cmds) indicating the type of command being replied to. |
114/// | `size`    | 1–2    | Size of `payload` encoded as a [`VarU16`]. |
115/// | `payload` | n      | Encoded payload. |
116#[derive(Clone, Copy, Eq, PartialEq)]
117pub struct CdcReplyPacket<const CMD: u8, P: Decode> {
118    /// Packet Payload Size
119    pub size: u16,
120
121    /// Packet Payload
122    ///
123    /// Contains data for a given packet that be encoded and sent over serial to the host.
124    pub payload: P,
125}
126
127impl<const CMD: u8, P: Decode> CdcReplyPacket<CMD, P> {
128    /// Header used for host-bound VEX CDC packets.
129    pub const HEADER: [u8; 2] = REPLY_HEADER;
130}
131
132impl<const CMD: u8, P: Decode> Decode for CdcReplyPacket<CMD, P> {
133    fn decode(data: &mut &[u8]) -> Result<Self, DecodeError> {
134        if <[u8; 2]>::decode(data)? != Self::HEADER {
135            return Err(DecodeError::new::<Self>(DecodeErrorKind::InvalidHeader));
136        }
137
138        let cmd = u8::decode(data)?;
139        if cmd != CMD {
140            return Err(DecodeError::new::<Self>(DecodeErrorKind::UnexpectedByte {
141                name: "cmd",
142                value: cmd,
143                expected: &[CMD],
144            }));
145        }
146
147        let payload_size = VarU16::decode(data)?.into_inner();
148        let payload = P::decode(data)?;
149
150        Ok(Self {
151            size: payload_size,
152            payload,
153        })
154    }
155}
156
157pub type SystemVersionPacket = CdcCommandPacket<SYSTEM_VERSION, ()>;
158pub type SystemVersionReplyPacket = CdcReplyPacket<SYSTEM_VERSION, SystemVersionReplyPayload>;
159
160#[derive(Debug, Clone, Copy, Eq, PartialEq)]
161pub struct SystemVersionReplyPayload {
162    pub version: Version,
163    pub product_type: ProductType,
164    pub flags: ProductFlags,
165}
166impl Decode for SystemVersionReplyPayload {
167    fn decode(data: &mut &[u8]) -> Result<Self, DecodeError> {
168        let version = Version::decode(data)?;
169        let product_type = ProductType::decode(data)?;
170        let flags = ProductFlags::from_bits_truncate(u8::decode(data)?);
171
172        Ok(Self {
173            version,
174            product_type,
175            flags,
176        })
177    }
178}
179
180pub type Query1Packet = CdcCommandPacket<QUERY_1, ()>;
181pub type Query1ReplyPacket = CdcReplyPacket<QUERY_1, Query1ReplyPayload>;
182
183#[derive(Debug, Clone, Copy, Eq, PartialEq)]
184pub struct Query1ReplyPayload {
185    pub version_1: u32,
186    pub version_2: u32,
187
188    /// 0xFF = QSPI, 0 = NOT sdcard, other = sdcard (returns devcfg.MULTIBOOT_ADDR)
189    pub boot_source: u8,
190
191    /// Number of times this packet has been replied to.
192    pub count: u8,
193}
194
195impl Decode for Query1ReplyPayload {
196    fn decode(data: &mut &[u8]) -> Result<Self, DecodeError> {
197        let version_1 = u32::decode(data)?;
198        let version_2 = u32::decode(data)?;
199        let boot_source = u8::decode(data)?;
200        let count = u8::decode(data)?;
201
202        Ok(Self {
203            version_1,
204            version_2,
205            boot_source,
206            count,
207        })
208    }
209}
210
211#[derive(Debug, Clone, Copy, Eq, PartialEq)]
212#[repr(u16)]
213pub enum ProductType {
214    Brain = 0x10,
215    Controller = 0x11,
216}
217impl Decode for ProductType {
218    fn decode(data: &mut &[u8]) -> Result<Self, DecodeError> {
219        let data = <[u8; 2]>::decode(data)?;
220
221        match data[1] {
222            0x10 => Ok(Self::Brain),
223            0x11 => Ok(Self::Controller),
224            v => Err(DecodeError::new::<Self>(DecodeErrorKind::UnexpectedByte {
225                name: "ProductType",
226                value: v,
227                expected: &[0x10, 0x11],
228            })),
229        }
230    }
231}
232
233bitflags! {
234    #[derive(Debug, Clone, Copy, Eq, PartialEq)]
235    pub struct ProductFlags: u8 {
236        /// Bit 1 is set when the controller is connected over a cable to the V5 Brain
237        const CONNECTED_CABLE = 1 << 0; // From testing, this appears to be how it works.
238
239        /// Bit 2 is set when the controller is connected over VEXLink to the V5 Brain.
240        const CONNECTED_WIRELESS = 1 << 1;
241    }
242}