valentine

Terminal control panel for the Focusrite Scarlett 18i20 — a from-scratch replacement for Focusrite Control.
Log | Files | Refs | README | LICENSE

packet.rs (7357B)


      1 //! The scarlett2 wire packet: a 16-byte little-endian header followed by an
      2 //! optional payload. Both requests and responses share this layout.
      3 //!
      4 //! ```text
      5 //!   offset  size  field
      6 //!   0       4     cmd    (u32)  opcode
      7 //!   4       2     size   (u16)  payload length in bytes
      8 //!   6       2     seq    (u16)  sequence number, +1 per request
      9 //!   8       4     error  (u32)  device error code (0 = ok)
     10 //!   12      4     pad    (u32)  reserved, always 0
     11 //!   16      ..    data   payload
     12 //! ```
     13 //!
     14 //! Validated against a real Scarlett 18i20 3rd Gen (see the Phase-0 spike): the
     15 //! device echoes `cmd`, and `seq` except during INIT where it replies `seq-1`
     16 //! (the kernel resets seq to 1 for INIT_1/INIT_2 and accepts a response seq of 0).
     17 
     18 /// Size of the fixed packet header in bytes.
     19 pub const HEADER_LEN: usize = 16;
     20 
     21 /// Error decoding a response packet.
     22 #[derive(Debug, Clone, PartialEq, Eq)]
     23 pub enum PacketError {
     24     /// Fewer than [`HEADER_LEN`] bytes were returned.
     25     TooShort { got: usize },
     26     /// The device reported a non-zero error code.
     27     Device { code: u32 },
     28     /// The response opcode did not match the request.
     29     CmdMismatch { sent: u32, got: u32 },
     30     /// The declared payload size exceeds the bytes actually received.
     31     SizeOverrun { declared: usize, available: usize },
     32 }
     33 
     34 impl core::fmt::Display for PacketError {
     35     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
     36         match self {
     37             PacketError::TooShort { got } => {
     38                 write!(f, "response too short: {got} bytes (need {HEADER_LEN})")
     39             }
     40             PacketError::Device { code } => write!(f, "device error code {code:#x}"),
     41             PacketError::CmdMismatch { sent, got } => {
     42                 write!(f, "response cmd {got:#x} != request {sent:#x}")
     43             }
     44             PacketError::SizeOverrun { declared, available } => write!(
     45                 f,
     46                 "declared payload {declared} > available {available}"
     47             ),
     48         }
     49     }
     50 }
     51 
     52 impl std::error::Error for PacketError {}
     53 
     54 /// Build a request packet: 16-byte header + `payload`.
     55 pub fn encode_request(cmd: u32, seq: u16, payload: &[u8]) -> Vec<u8> {
     56     let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
     57     out.extend_from_slice(&cmd.to_le_bytes());
     58     out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
     59     out.extend_from_slice(&seq.to_le_bytes());
     60     out.extend_from_slice(&0u32.to_le_bytes()); // error
     61     out.extend_from_slice(&0u32.to_le_bytes()); // pad
     62     out.extend_from_slice(payload);
     63     out
     64 }
     65 
     66 /// A decoded response header plus a view of its payload.
     67 #[derive(Debug, Clone, PartialEq, Eq)]
     68 pub struct Response {
     69     pub cmd: u32,
     70     pub size: u16,
     71     pub seq: u16,
     72     pub error: u32,
     73     pub payload: Vec<u8>,
     74 }
     75 
     76 /// Parse a raw response buffer, validating it against the request `cmd`.
     77 ///
     78 /// `seq` is intentionally NOT validated here: the device's seq accounting has an
     79 /// init-time off-by-one (confirmed on hardware), and callers that care can check
     80 /// `Response::seq` themselves. We DO enforce a zero error code and a matching cmd.
     81 pub fn decode_response(cmd: u32, buf: &[u8]) -> Result<Response, PacketError> {
     82     if buf.len() < HEADER_LEN {
     83         return Err(PacketError::TooShort { got: buf.len() });
     84     }
     85     let r_cmd = u32::from_le_bytes(buf[0..4].try_into().unwrap());
     86     let size = u16::from_le_bytes(buf[4..6].try_into().unwrap());
     87     let seq = u16::from_le_bytes(buf[6..8].try_into().unwrap());
     88     let error = u32::from_le_bytes(buf[8..12].try_into().unwrap());
     89 
     90     if error != 0 {
     91         return Err(PacketError::Device { code: error });
     92     }
     93     if r_cmd != cmd {
     94         return Err(PacketError::CmdMismatch { sent: cmd, got: r_cmd });
     95     }
     96 
     97     let available = buf.len() - HEADER_LEN;
     98     let declared = size as usize;
     99     if declared > available {
    100         return Err(PacketError::SizeOverrun { declared, available });
    101     }
    102     let payload = buf[HEADER_LEN..HEADER_LEN + declared].to_vec();
    103 
    104     Ok(Response { cmd: r_cmd, size, seq, error, payload })
    105 }
    106 
    107 #[cfg(test)]
    108 mod tests {
    109     use super::*;
    110 
    111     #[test]
    112     fn request_header_is_little_endian_and_16_bytes() {
    113         // GET_DATA with an 8-byte {offset:u32, size:u32} payload, seq 3.
    114         let mut payload = Vec::new();
    115         payload.extend_from_slice(&0u32.to_le_bytes()); // offset 0
    116         payload.extend_from_slice(&8u32.to_le_bytes()); // size 8
    117         let req = encode_request(0x0080_0000, 3, &payload);
    118 
    119         assert_eq!(req.len(), HEADER_LEN + 8);
    120         assert_eq!(&req[0..4], &[0x00, 0x00, 0x80, 0x00]); // cmd LE
    121         assert_eq!(&req[4..6], &[0x08, 0x00]); // size = 8
    122         assert_eq!(&req[6..8], &[0x03, 0x00]); // seq = 3
    123         assert_eq!(&req[8..12], &[0; 4]); // error
    124         assert_eq!(&req[12..16], &[0; 4]); // pad
    125     }
    126 
    127     #[test]
    128     fn empty_payload_request_is_just_the_header() {
    129         let req = encode_request(0x0000_0000, 1, &[]); // INIT_1
    130         assert_eq!(req.len(), HEADER_LEN);
    131         assert_eq!(&req[4..6], &[0, 0]); // size 0
    132     }
    133 
    134     fn raw_response(cmd: u32, seq: u16, error: u32, payload: &[u8]) -> Vec<u8> {
    135         let mut b = Vec::new();
    136         b.extend_from_slice(&cmd.to_le_bytes());
    137         b.extend_from_slice(&(payload.len() as u16).to_le_bytes());
    138         b.extend_from_slice(&seq.to_le_bytes());
    139         b.extend_from_slice(&error.to_le_bytes());
    140         b.extend_from_slice(&0u32.to_le_bytes());
    141         b.extend_from_slice(payload);
    142         b
    143     }
    144 
    145     #[test]
    146     fn decodes_real_get_sync_response() {
    147         // From the spike: GET_SYNC returned payload 01 00 00 00 (locked).
    148         let raw = raw_response(0x0000_6004, 1, 0, &[0x01, 0x00, 0x00, 0x00]);
    149         let resp = decode_response(0x0000_6004, &raw).unwrap();
    150         assert_eq!(resp.payload, vec![0x01, 0x00, 0x00, 0x00]);
    151         assert_eq!(u32::from_le_bytes(resp.payload[..4].try_into().unwrap()), 1);
    152     }
    153 
    154     #[test]
    155     fn decodes_real_get_data_response() {
    156         // From the spike: GET_DATA@0 returned ec 20 00 00 00 00 01 00.
    157         let p = [0xec, 0x20, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00];
    158         let raw = raw_response(0x0080_0000, 2, 0, &p);
    159         let resp = decode_response(0x0080_0000, &raw).unwrap();
    160         assert_eq!(resp.payload, p);
    161     }
    162 
    163     #[test]
    164     fn rejects_device_error_code() {
    165         let raw = raw_response(0x0080_0000, 5, 0xdead, &[]);
    166         assert_eq!(
    167             decode_response(0x0080_0000, &raw),
    168             Err(PacketError::Device { code: 0xdead })
    169         );
    170     }
    171 
    172     #[test]
    173     fn rejects_cmd_mismatch() {
    174         let raw = raw_response(0x0000_2001, 5, 0, &[]);
    175         assert_eq!(
    176             decode_response(0x0080_0000, &raw),
    177             Err(PacketError::CmdMismatch { sent: 0x0080_0000, got: 0x0000_2001 })
    178         );
    179     }
    180 
    181     #[test]
    182     fn rejects_short_buffer() {
    183         assert_eq!(
    184             decode_response(0, &[0u8; 8]),
    185             Err(PacketError::TooShort { got: 8 })
    186         );
    187     }
    188 
    189     #[test]
    190     fn rejects_size_overrun() {
    191         // header claims 16-byte payload but only 4 bytes follow
    192         let mut b = encode_request(0x10, 0, &[]);
    193         b[4] = 16; // size = 16
    194         b.extend_from_slice(&[0u8; 4]);
    195         assert_eq!(
    196             decode_response(0x10, &b),
    197             Err(PacketError::SizeOverrun { declared: 16, available: 4 })
    198         );
    199     }
    200 }