Skip to main content

ssh_stamp/
can.rs

1// SPDX-FileCopyrightText: 2026 Roman Valls Guimera <brainstorm@nopcode.org>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! CAN frame encoding/decoding for SSH tunnelling.
6//!
7//! Provides a trait-based framing layer so the encapsulation format can be
8//! swapped without touching the SSH bridge. Two encapsulations are
9//! implemented, multiplexed over the same SSH `can` subsystem byte stream
10//! by [`CanParser`] — the same auto-detection real GVRET hardware performs
11//! on its serial port:
12//!
13//! * **slcan** (ASCII, the default): one `t`/`T` frame per `\r`-terminated
14//!   line. For interactive `ssh -s ... can` use and slcan-speaking tools.
15//! * **GVRET** (binary): the `SavvyCAN` / ESP32RET wire protocol. `0xF1`
16//!   starts a command (send frame, time sync, device info, keepalive, ...)
17//!   and is answered with binary replies; `0xE7` (sent twice by `SavvyCAN` on
18//!   connect) switches bus→host frame output to GVRET framing.
19//!
20//! `embedded-can` traits are used as the frame abstraction.
21
22use core::fmt::Write as _;
23use core::future::Future;
24
25use embassy_futures::select::select;
26use embassy_time::Instant;
27use embedded_can::{ExtendedId, Frame, Id, StandardId};
28use embedded_io_async::{Read, Write};
29use log::{debug, warn};
30
31/// Longest slcan line: `T` + 8 ID chars + 1 DLC char + 16 data chars.
32const SLCAN_LINE_SZ: usize = 32;
33
34/// Upper bound for one encoded frame or protocol reply in either
35/// encapsulation (slcan frame: 27 bytes, GVRET frame: 20 bytes, largest
36/// GVRET reply: 17 bytes).
37pub const ENCODED_FRAME_MAX: usize = 32;
38
39/// Encodes a CAN frame into a byte buffer for transmission over SSH.
40pub trait CanEncoder {
41    /// Encode `frame` into `buf`. Returns the number of bytes written.
42    fn encode(&self, frame: &impl Frame, buf: &mut [u8]) -> usize;
43}
44
45/// Decodes a byte buffer into a CAN frame.
46pub trait CanDecoder {
47    /// Try to decode a CAN frame from `buf`. Returns `Some(frame)` on success,
48    /// `None` if the buffer does not contain a complete frame.
49    fn decode(&self, buf: &[u8]) -> Option<CanFrame>;
50}
51
52/// Platform-agnostic buffered CAN bridge.
53///
54/// The CAN bridge pumps encoded frames between the SSH channel and
55/// the target CAN peripheral. Every platform provides a concrete type
56/// implementing this trait (ESP32: `ssh_stamp_esp32::BufferedCan`).
57pub trait BufferedCan: Sync {
58    fn read(&self, buf: &mut [u8]) -> impl Future<Output = usize>;
59    fn write(&self, buf: &[u8]) -> impl Future<Output = ()>;
60    fn check_dropped_frames(&self) -> usize;
61
62    /// Start-of-session hook: revert bus→host framing to slcan (ASCII),
63    /// reset any half-parsed protocol state left by a previous session and
64    /// discard bus traffic buffered while no session was attached.
65    fn reset_protocol(&self);
66}
67
68/// A simple owned CAN frame for use in the platform-agnostic layer.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct CanFrame {
71    pub id: CanId,
72    pub data: heapless::Vec<u8, 8>,
73}
74
75/// CAN identifier, mirroring `embedded_can::Id`.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum CanId {
78    Standard(u16),
79    Extended(u32),
80}
81
82impl From<Id> for CanId {
83    fn from(id: Id) -> Self {
84        match id {
85            Id::Standard(s) => CanId::Standard(s.as_raw()),
86            Id::Extended(e) => CanId::Extended(e.as_raw()),
87        }
88    }
89}
90
91impl From<CanId> for Id {
92    fn from(id: CanId) -> Self {
93        match id {
94            CanId::Standard(v) => Id::Standard(StandardId::new(v).unwrap_or(StandardId::ZERO)),
95            CanId::Extended(v) => Id::Extended(ExtendedId::new(v).unwrap_or(ExtendedId::ZERO)),
96        }
97    }
98}
99
100impl Frame for CanFrame {
101    fn new(id: impl Into<Id>, data: &[u8]) -> Option<Self> {
102        Some(CanFrame {
103            id: id.into().into(),
104            data: heapless::Vec::from_slice(data).ok()?,
105        })
106    }
107
108    // Remote (RTR) frames are not tunnelled.
109    fn new_remote(_id: impl Into<Id>, _dlc: usize) -> Option<Self> {
110        None
111    }
112
113    fn is_extended(&self) -> bool {
114        matches!(self.id, CanId::Extended(_))
115    }
116
117    fn is_remote_frame(&self) -> bool {
118        false
119    }
120
121    fn id(&self) -> Id {
122        self.id.into()
123    }
124
125    fn dlc(&self) -> usize {
126        self.data.len()
127    }
128
129    fn data(&self) -> &[u8] {
130        &self.data
131    }
132}
133
134/// slcan (ASCII) encoder/decoder.
135///
136/// Encodes frames as `tIIILDD...\r` (standard) or `TIIIIIIIILDD...\r`
137/// (extended), where I = hex ID, L = data length, D = hex data bytes.
138pub struct Slcan;
139
140impl CanEncoder for Slcan {
141    fn encode(&self, frame: &impl Frame, buf: &mut [u8]) -> usize {
142        let mut s = heapless::String::<64>::new();
143        match frame.id() {
144            Id::Standard(id) => {
145                let _ = write!(s, "t{:03X}{:1X}", id.as_raw(), frame.dlc());
146            }
147            Id::Extended(id) => {
148                let _ = write!(s, "T{:08X}{:1X}", id.as_raw(), frame.dlc());
149            }
150        }
151        let data = frame.data();
152        for &b in data {
153            let _ = write!(s, "{b:02X}");
154        }
155        let _ = s.push('\r');
156        let len = s.len().min(buf.len());
157        buf[..len].copy_from_slice(s.as_bytes());
158        len
159    }
160}
161
162impl CanDecoder for Slcan {
163    fn decode(&self, buf: &[u8]) -> Option<CanFrame> {
164        let s = core::str::from_utf8(buf).ok()?;
165        // Byte-index slicing below is only safe on ASCII input.
166        if !s.is_ascii() {
167            return None;
168        }
169        let s = s.trim_end_matches('\r');
170        let bytes = s.as_bytes();
171        match bytes.first()? {
172            b't' => {
173                if s.len() < 5 {
174                    return None;
175                }
176                let id = u16::from_str_radix(&s[1..4], 16).ok()?;
177                let dlc = (bytes[4] as char).to_digit(16)? as usize;
178                if dlc > 8 {
179                    return None;
180                }
181                let mut data = heapless::Vec::new();
182                let hex_data = &s[5..];
183                if hex_data.len() < dlc * 2 {
184                    return None;
185                }
186                for i in 0..dlc {
187                    let b = u8::from_str_radix(&hex_data[i * 2..i * 2 + 2], 16).ok()?;
188                    let _ = data.push(b);
189                }
190                Some(CanFrame {
191                    id: CanId::Standard(id),
192                    data,
193                })
194            }
195            b'T' => {
196                if s.len() < 10 {
197                    return None;
198                }
199                let id = u32::from_str_radix(&s[1..9], 16).ok()?;
200                let dlc = (bytes[9] as char).to_digit(16)? as usize;
201                if dlc > 8 {
202                    return None;
203                }
204                let mut data = heapless::Vec::new();
205                let hex_data = &s[10..];
206                if hex_data.len() < dlc * 2 {
207                    return None;
208                }
209                for i in 0..dlc {
210                    let b = u8::from_str_radix(&hex_data[i * 2..i * 2 + 2], 16).ok()?;
211                    let _ = data.push(b);
212                }
213                Some(CanFrame {
214                    id: CanId::Extended(id),
215                    data,
216                })
217            }
218            _ => None,
219        }
220    }
221}
222
223/// GVRET (binary) encoder for bus→host frames.
224///
225/// Message layout, as produced by ESP32RET and parsed by `SavvyCAN`:
226/// `F1 00 <timestamp us, u32 LE> <id, u32 LE, bit31 = extended>`
227/// `<dlc | bus << 4> <data...> 00`.
228pub struct Gvret;
229
230impl CanEncoder for Gvret {
231    fn encode(&self, frame: &impl Frame, buf: &mut [u8]) -> usize {
232        let data = frame.data();
233        let n = 12 + data.len();
234        let Ok(dlc) = u8::try_from(data.len()) else {
235            return 0;
236        };
237        if dlc > 8 || buf.len() < n {
238            return 0;
239        }
240        let raw_id = match frame.id() {
241            Id::Standard(id) => u32::from(id.as_raw()),
242            Id::Extended(id) => id.as_raw() | 0x8000_0000,
243        };
244        buf[0] = 0xF1;
245        buf[1] = gvret::BUILD_CAN_FRAME;
246        buf[2..6].copy_from_slice(&timestamp_us());
247        buf[6..10].copy_from_slice(&raw_id.to_le_bytes());
248        buf[10] = dlc; // bus 0 in the high nibble
249        buf[11..11 + data.len()].copy_from_slice(data);
250        buf[11 + data.len()] = 0; // checksum placeholder, ignored by SavvyCAN
251        n
252    }
253}
254
255/// Microsecond timestamp in GVRET's format: a `u32` that wraps by design
256/// (every ~71 minutes), little endian.
257fn timestamp_us() -> [u8; 4] {
258    #[allow(clippy::cast_possible_truncation)]
259    let t = Instant::now().as_micros() as u32;
260    t.to_le_bytes()
261}
262
263/// Encode a bus→host frame in whichever framing the session negotiated:
264/// GVRET binary after the host sent `0xE7`, slcan ASCII otherwise.
265pub fn encode_frame(frame: &impl Frame, binary: bool, buf: &mut [u8]) -> usize {
266    if binary {
267        Gvret.encode(frame, buf)
268    } else {
269        Slcan.encode(frame, buf)
270    }
271}
272
273/// GVRET command bytes (the byte following an `0xF1` marker).
274mod gvret {
275    pub const BUILD_CAN_FRAME: u8 = 0x00;
276    pub const TIME_SYNC: u8 = 0x01;
277    pub const GET_DIG_INPUTS: u8 = 0x02;
278    pub const GET_ANALOG_INPUTS: u8 = 0x03;
279    pub const SET_DIG_OUT: u8 = 0x04;
280    pub const SETUP_CANBUS: u8 = 0x05;
281    pub const GET_CANBUS_PARAMS: u8 = 0x06;
282    pub const GET_DEVICE_INFO: u8 = 0x07;
283    pub const SET_SINGLEWIRE_MODE: u8 = 0x08;
284    pub const KEEPALIVE: u8 = 0x09;
285    pub const SET_SYSTYPE: u8 = 0x0A;
286    pub const ECHO_CAN_FRAME: u8 = 0x0B;
287    pub const GET_NUM_BUSES: u8 = 0x0C;
288    pub const GET_EXT_BUSES: u8 = 0x0D;
289    pub const SET_EXT_BUSES: u8 = 0x0E;
290}
291
292/// What the platform pump should do with the bytes just parsed.
293pub enum CanAction {
294    /// A complete host→bus frame was decoded; transmit it.
295    Transmit(CanFrame),
296    /// A GVRET command produced a reply; send it back to the host.
297    Reply(heapless::Vec<u8, ENCODED_FRAME_MAX>),
298    /// The host sent `0xE7`: switch bus→host framing to GVRET binary.
299    EnableBinary,
300}
301
302/// In-progress GVRET command, after the `0xF1` marker.
303#[derive(Clone, Copy)]
304enum GvretState {
305    /// Waiting for the command byte.
306    Command,
307    /// Collecting a host→bus frame: id(4) bus(1) dlc(1) data(dlc) chk(1).
308    /// `echo` replies the frame to the host instead of transmitting it.
309    Frame {
310        echo: bool,
311        buf: [u8; 15],
312        got: usize,
313    },
314    /// Swallow N payload bytes of a command we accept but ignore.
315    Consume(usize),
316}
317
318/// Byte-stream front end for the SSH `can` subsystem.
319///
320/// Feeds one byte at a time; slcan lines are accumulated and decoded here
321/// (SSH reads arrive fragmented), GVRET commands are parsed by a state
322/// machine mirroring the ESP32RET firmware. `0xF1`/`0xE7` outside a GVRET
323/// command abort any partial slcan line — they cannot occur in valid slcan.
324pub struct CanParser {
325    /// Bus bitrate reported to GVRET clients (bit/s).
326    bitrate: u32,
327    line: heapless::Vec<u8, SLCAN_LINE_SZ>,
328    gvret: Option<GvretState>,
329}
330
331impl CanParser {
332    #[must_use]
333    pub fn new(bitrate: u32) -> Self {
334        CanParser {
335            bitrate,
336            line: heapless::Vec::new(),
337            gvret: None,
338        }
339    }
340
341    /// Drop any half-parsed slcan line or GVRET command.
342    pub fn reset(&mut self) {
343        self.line.clear();
344        self.gvret = None;
345    }
346
347    /// Consume one host byte, returning an action once one is complete.
348    pub fn feed(&mut self, byte: u8) -> Option<CanAction> {
349        if let Some(state) = self.gvret.take() {
350            return self.feed_gvret(state, byte);
351        }
352        match byte {
353            0xF1 => {
354                self.line.clear();
355                self.gvret = Some(GvretState::Command);
356                None
357            }
358            0xE7 => {
359                self.line.clear();
360                Some(CanAction::EnableBinary)
361            }
362            b'\r' | b'\n' => {
363                let frame = Slcan.decode(&self.line);
364                self.line.clear();
365                frame.map(CanAction::Transmit)
366            }
367            b => {
368                // Oversized/garbage lines are discarded until the next
369                // terminator resyncs the stream.
370                if self.line.push(b).is_err() {
371                    self.line.clear();
372                }
373                None
374            }
375        }
376    }
377
378    fn feed_gvret(&mut self, state: GvretState, byte: u8) -> Option<CanAction> {
379        match state {
380            GvretState::Command => self.gvret_command(byte),
381            GvretState::Consume(n) => {
382                if n > 1 {
383                    self.gvret = Some(GvretState::Consume(n - 1));
384                }
385                None
386            }
387            GvretState::Frame { echo, mut buf, got } => {
388                buf[got] = byte;
389                let got = got + 1;
390                if got >= 6 {
391                    let dlc = usize::from(buf[5] & 0x0F).min(8);
392                    // Header (6) + data + one trailing checksum byte
393                    // (transmitted but never verified, as in ESP32RET).
394                    if got == 6 + dlc + 1 {
395                        return Self::finish_frame(echo, &buf, dlc);
396                    }
397                }
398                self.gvret = Some(GvretState::Frame { echo, buf, got });
399                None
400            }
401        }
402    }
403
404    fn gvret_command(&mut self, cmd: u8) -> Option<CanAction> {
405        match cmd {
406            gvret::BUILD_CAN_FRAME | gvret::ECHO_CAN_FRAME => {
407                self.gvret = Some(GvretState::Frame {
408                    echo: cmd == gvret::ECHO_CAN_FRAME,
409                    buf: [0u8; 15],
410                    got: 0,
411                });
412                None
413            }
414            gvret::TIME_SYNC => {
415                let mut b = [0u8; 6];
416                b[0] = 0xF1;
417                b[1] = gvret::TIME_SYNC;
418                b[2..6].copy_from_slice(&timestamp_us());
419                reply(&b)
420            }
421            // No digital/analog IO is exposed; report zeroed inputs.
422            gvret::GET_DIG_INPUTS => reply(&[0xF1, gvret::GET_DIG_INPUTS, 0, 0]),
423            gvret::GET_ANALOG_INPUTS => {
424                reply(&[0xF1, gvret::GET_ANALOG_INPUTS, 0, 0, 0, 0, 0, 0, 0, 0, 0])
425            }
426            gvret::GET_CANBUS_PARAMS => {
427                // Bus 0 enabled at the fixed hardware bitrate, bus 1 absent.
428                let mut b = [0u8; 12];
429                b[0] = 0xF1;
430                b[1] = gvret::GET_CANBUS_PARAMS;
431                b[2] = 1;
432                b[3..7].copy_from_slice(&self.bitrate.to_le_bytes());
433                b[8..12].copy_from_slice(&self.bitrate.to_le_bytes());
434                reply(&b)
435            }
436            gvret::GET_DEVICE_INFO => {
437                // Build number 618 (0x026A) — SavvyCAN only displays it.
438                reply(&[0xF1, gvret::GET_DEVICE_INFO, 0x6A, 0x02, 0, 0, 0, 0])
439            }
440            gvret::KEEPALIVE => reply(&[0xF1, gvret::KEEPALIVE, 0xDE, 0xAD]),
441            gvret::GET_NUM_BUSES => reply(&[0xF1, gvret::GET_NUM_BUSES, 1]),
442            gvret::GET_EXT_BUSES => {
443                // No SWCAN/LIN buses: 15 zeroed payload bytes.
444                let mut b = [0u8; 17];
445                b[0] = 0xF1;
446                b[1] = gvret::GET_EXT_BUSES;
447                reply(&b)
448            }
449            // Accepted but ignored: the bus runs at a fixed bitrate and has
450            // no digital outputs / single-wire / system-type settings.
451            gvret::SET_DIG_OUT | gvret::SET_SINGLEWIRE_MODE | gvret::SET_SYSTYPE => {
452                self.gvret = Some(GvretState::Consume(1));
453                None
454            }
455            gvret::SETUP_CANBUS => {
456                self.gvret = Some(GvretState::Consume(8));
457                None
458            }
459            gvret::SET_EXT_BUSES => {
460                self.gvret = Some(GvretState::Consume(10));
461                None
462            }
463            _ => None,
464        }
465    }
466
467    fn finish_frame(echo: bool, buf: &[u8; 15], dlc: usize) -> Option<CanAction> {
468        let raw_id = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
469        let id = if raw_id & 0x8000_0000 != 0 {
470            CanId::Extended(raw_id & 0x1FFF_FFFF)
471        } else {
472            CanId::Standard((raw_id & 0x7FF) as u16)
473        };
474        let frame = CanFrame {
475            id,
476            data: heapless::Vec::from_slice(&buf[6..6 + dlc]).ok()?,
477        };
478        if echo {
479            let mut b = [0u8; ENCODED_FRAME_MAX];
480            let n = Gvret.encode(&frame, &mut b);
481            reply(&b[..n])
482        } else {
483            Some(CanAction::Transmit(frame))
484        }
485    }
486}
487
488fn reply(bytes: &[u8]) -> Option<CanAction> {
489    heapless::Vec::from_slice(bytes).ok().map(CanAction::Reply)
490}
491
492/// Forwards an incoming SSH CAN channel to/from the local CAN bus, until
493/// the connection drops.
494///
495/// # Errors
496/// Returns an error if the SSH connection fails.
497pub async fn can_bridge<C: BufferedCan + ?Sized>(
498    chan_read: impl Read<Error = sunset::Error>,
499    chan_write: impl Write<Error = sunset::Error>,
500    can: &C,
501) -> Result<(), sunset::Error> {
502    debug!("Starting CAN <--> SSH bridge");
503    can.reset_protocol();
504    select(can_to_ssh(can, chan_write), ssh_to_can(chan_read, can)).await;
505    debug!("Stopping CAN <--> SSH bridge");
506    Ok(())
507}
508
509async fn can_to_ssh<C: BufferedCan + ?Sized>(
510    can_buf: &C,
511    mut chan_write: impl Write<Error = sunset::Error>,
512) -> Result<(), sunset::Error> {
513    let mut ssh_tx_buf = [0u8; 128];
514    loop {
515        let dropped = can_buf.check_dropped_frames();
516        if dropped > 0 {
517            warn!("CAN RX dropped {dropped} frames");
518        }
519        let n = can_buf.read(&mut ssh_tx_buf).await;
520        chan_write.write_all(&ssh_tx_buf[..n]).await?;
521    }
522}
523
524async fn ssh_to_can<C: BufferedCan + ?Sized>(
525    mut chan_read: impl Read<Error = sunset::Error>,
526    can_buf: &C,
527) -> Result<(), sunset::Error> {
528    let mut can_tx_buf = [0u8; 64];
529    loop {
530        let n = chan_read.read(&mut can_tx_buf).await?;
531        if n == 0 {
532            return Err(sunset::Error::ChannelEOF);
533        }
534        can_buf.write(&can_tx_buf[..n]).await;
535    }
536}