Skip to main content

moteus_protocol/
diagnostic.rs

1// Copyright 2026 mjbots Robotic Systems, LLC.  info@mjbots.com
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Payload framing for the tunneled diagnostic stream.
16//!
17//! The diagnostic stream is a bidirectional byte stream tunneled in
18//! CAN frame payloads using the `0x40`-series multiplex subframes.
19//! This module encodes and decodes those payloads; arbitration ID
20//! handling and transport concerns live in the `moteus` crate.
21//!
22//! Two poll variants exist:
23//!
24//! - [`write_client_poll`] / [`parse_server_to_client`]: the original
25//!   protocol.  Polling is destructive — data is consumed from the
26//!   device as soon as it is sent, so a lost response loses data.
27//! - [`write_client_poll_flow`] / [`parse_server_to_client_flow`]:
28//!   flow-controlled variant for lossy transports such as UART.  Each
29//!   server response carries a packet number; the client acknowledges
30//!   the last packet number it received in each poll, allowing the
31//!   server to retransmit unacknowledged data.
32
33use crate::multiplex::{
34    read_varuint_slice, CLIENT_POLL_SERVER, CLIENT_POLL_SERVER_FLOW, CLIENT_TO_SERVER,
35    SERVER_TO_CLIENT, SERVER_TO_CLIENT_FLOW,
36};
37
38/// Maximum data bytes in a single client-to-server write subframe:
39/// a 64-byte CAN-FD payload minus the 3-byte header (action, channel,
40/// length).
41pub const MAX_WRITE: usize = 61;
42
43/// Writes a `CLIENT_TO_SERVER` subframe (action, channel, length,
44/// data) into `out`, returning the number of bytes written.
45///
46/// # Panics
47///
48/// Panics if `data` exceeds [`MAX_WRITE`] bytes.
49pub fn write_client_to_server(channel: u8, data: &[u8], out: &mut [u8; 64]) -> u8 {
50    assert!(data.len() <= MAX_WRITE);
51
52    out[0] = CLIENT_TO_SERVER;
53    out[1] = channel;
54    out[2] = data.len() as u8;
55    out[3..3 + data.len()].copy_from_slice(data);
56    (3 + data.len()) as u8
57}
58
59/// Writes a `CLIENT_POLL_SERVER` subframe (action, channel,
60/// max_length) into `out`, returning the number of bytes written.
61pub fn write_client_poll(channel: u8, max_length: u8, out: &mut [u8; 64]) -> u8 {
62    out[0] = CLIENT_POLL_SERVER;
63    out[1] = channel;
64    out[2] = max_length;
65    3
66}
67
68/// Writes a `CLIENT_POLL_SERVER_FLOW` subframe (action, channel,
69/// packet_number, max_length) into `out`, returning the number of
70/// bytes written.
71///
72/// `packet_number` acknowledges the last packet number received from
73/// the server on this channel.
74pub fn write_client_poll_flow(
75    channel: u8,
76    packet_number: u8,
77    max_length: u8,
78    out: &mut [u8; 64],
79) -> u8 {
80    out[0] = CLIENT_POLL_SERVER_FLOW;
81    out[1] = channel;
82    out[2] = packet_number;
83    out[3] = max_length;
84    4
85}
86
87/// Parses a `SERVER_TO_CLIENT` subframe, returning the tunneled data
88/// if the payload is a well-formed response for `channel`.
89pub fn parse_server_to_client(payload: &[u8], channel: u8) -> Option<&[u8]> {
90    let (action, rest) = payload.split_first()?;
91    if *action != SERVER_TO_CLIENT {
92        return None;
93    }
94    let (msg_channel, rest) = rest.split_first()?;
95    if *msg_channel != channel {
96        return None;
97    }
98    let (len, rest) = read_varuint_slice(rest)?;
99    rest.get(..len as usize)
100}
101
102/// A parsed flow-controlled diagnostic response.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct FlowData<'a> {
105    /// The packet number of this response, to be acknowledged in the
106    /// next poll.  Wraps from 255 to 0.
107    pub packet_number: u8,
108    /// The tunneled data (may be empty).
109    pub data: &'a [u8],
110}
111
112/// Parses a `SERVER_TO_CLIENT_FLOW` subframe (action, channel,
113/// packet_number, varuint length, data), returning the packet number
114/// and tunneled data if the payload is a well-formed flow response for
115/// `channel`.
116pub fn parse_server_to_client_flow(payload: &[u8], channel: u8) -> Option<FlowData<'_>> {
117    let (action, rest) = payload.split_first()?;
118    if *action != SERVER_TO_CLIENT_FLOW {
119        return None;
120    }
121    let (msg_channel, rest) = rest.split_first()?;
122    if *msg_channel != channel {
123        return None;
124    }
125    let (packet_number, rest) = rest.split_first()?;
126    let (len, rest) = read_varuint_slice(rest)?;
127    Some(FlowData {
128        packet_number: *packet_number,
129        data: rest.get(..len as usize)?,
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_write_client_to_server() {
139        let mut out = [0u8; 64];
140        let size = write_client_to_server(1, b"hello", &mut out);
141        assert_eq!(size, 8);
142        assert_eq!(&out[..8], &[0x40, 0x01, 0x05, b'h', b'e', b'l', b'l', b'o']);
143    }
144
145    #[test]
146    fn test_write_client_poll() {
147        let mut out = [0u8; 64];
148        let size = write_client_poll(1, 48, &mut out);
149        assert_eq!(size, 3);
150        assert_eq!(&out[..3], &[0x42, 0x01, 0x30]);
151    }
152
153    #[test]
154    fn test_write_client_poll_flow() {
155        // Matches C++ DiagnosticReadFlow::Make: action, channel,
156        // packet_number, max_length.
157        let mut out = [0u8; 64];
158        let size = write_client_poll_flow(1, 0xAB, 48, &mut out);
159        assert_eq!(size, 4);
160        assert_eq!(&out[..4], &[0x44, 0x01, 0xAB, 0x30]);
161    }
162
163    #[test]
164    fn test_parse_server_to_client() {
165        assert_eq!(
166            parse_server_to_client(&[0x41, 0x01, 0x03, b'a', b'b', b'c'], 1),
167            Some(b"abc".as_slice())
168        );
169
170        // Empty data is valid.
171        assert_eq!(
172            parse_server_to_client(&[0x41, 0x01, 0x00], 1),
173            Some(b"".as_slice())
174        );
175
176        // Wrong channel, wrong action, truncated data.
177        assert_eq!(parse_server_to_client(&[0x41, 0x02, 0x01, b'a'], 1), None);
178        assert_eq!(parse_server_to_client(&[0x43, 0x01, 0x01, b'a'], 1), None);
179        assert_eq!(parse_server_to_client(&[0x41, 0x01, 0x05, b'a'], 1), None);
180        assert_eq!(parse_server_to_client(&[], 1), None);
181    }
182
183    #[test]
184    fn test_parse_server_to_client_flow() {
185        let result =
186            parse_server_to_client_flow(&[0x43, 0x01, 0x07, 0x03, b'a', b'b', b'c'], 1).unwrap();
187        assert_eq!(result.packet_number, 0x07);
188        assert_eq!(result.data, b"abc");
189
190        // Empty data still carries a packet number.
191        let result = parse_server_to_client_flow(&[0x43, 0x01, 0xFF, 0x00], 1).unwrap();
192        assert_eq!(result.packet_number, 0xFF);
193        assert_eq!(result.data, b"");
194
195        // A plain response is not a flow response.
196        assert_eq!(
197            parse_server_to_client_flow(&[0x41, 0x01, 0x01, b'a'], 1),
198            None
199        );
200        // Wrong channel.
201        assert_eq!(
202            parse_server_to_client_flow(&[0x43, 0x02, 0x07, 0x00], 1),
203            None
204        );
205        // Truncated.
206        assert_eq!(parse_server_to_client_flow(&[0x43, 0x01, 0x07], 1), None);
207        assert_eq!(
208            parse_server_to_client_flow(&[0x43, 0x01, 0x07, 0x05, b'a'], 1),
209            None
210        );
211    }
212
213    #[test]
214    fn test_round_trip_via_write_combiner_sizes() {
215        // A poll followed by parse of a synthesized response.
216        let mut out = [0u8; 64];
217        let size = write_client_poll_flow(2, 5, 10, &mut out) as usize;
218        assert_eq!(&out[..size], &[0x44, 0x02, 0x05, 0x0A]);
219
220        let response = [0x43, 0x02, 0x06, 0x02, 0x11, 0x22];
221        let parsed = parse_server_to_client_flow(&response, 2).unwrap();
222        assert_eq!(parsed.packet_number, 6);
223        assert_eq!(parsed.data, &[0x11, 0x22]);
224    }
225
226    #[test]
227    fn test_read_varuint_multibyte() {
228        // Lengths >= 0x80 use a multi-byte varuint.
229        let (value, rest) = read_varuint_slice(&[0x80, 0x01, 0xAA]).unwrap();
230        assert_eq!(value, 128);
231        assert_eq!(rest, &[0xAA]);
232
233        // Unterminated varuint.
234        assert!(read_varuint_slice(&[0x80, 0x80, 0x80, 0x80, 0x80]).is_none());
235    }
236}