Skip to main content

ssh_stamp_esp32/
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 (TWAI) implementation for ESP32 family.
6//!
7//! Provides [`BufferedCan`] — a software-buffered, async CAN interface
8//! satisfying [`ssh_stamp::can::BufferedCan`]. The bridge can pump the
9//! same TWAI peripheral from two futures (TX and RX) concurrently because
10//! both sides take `&self`. All framing (slcan / GVRET auto-detection)
11//! lives in the platform-agnostic [`ssh_stamp::can`] layer; this file only
12//! moves bytes and frames.
13
14use core::future::Future;
15
16use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe};
17use embassy_time::{Duration, with_timeout};
18use esp_hal::gpio::AnyPin;
19use esp_hal::peripherals::TWAI0;
20use esp_hal::twai::{self, EspTwaiFrame, ExtendedId, StandardId, TwaiMode};
21use log::warn;
22use portable_atomic::{AtomicBool, AtomicUsize, Ordering};
23use ssh_stamp::can::{CanAction, CanId, CanParser, ENCODED_FRAME_MAX, encode_frame};
24use static_cell::StaticCell;
25
26const INWARD_BUF_SZ: usize = 512;
27const OUTWARD_BUF_SZ: usize = 256;
28
29/// Bus bitrate in bit/s. Keep in sync with the `BaudRate` passed to the
30/// TWAI driver in [`can_task`]; also reported to GVRET clients.
31const CAN_BITRATE: u32 = 500_000;
32
33/// TWAI operating mode.
34///
35/// `Normal` is what a real bus needs: the controller takes part in
36/// acknowledgement and retransmits a frame until some node acknowledges it.
37/// On a bench with no acknowledging peer (only a scope/analyzer attached)
38/// that same rule makes a single write repeat on the wire, so the
39/// `can-no-ack` feature switches to `SelfTest` mode, which drops the
40/// acknowledgement requirement: one slcan line, exactly one frame.
41#[cfg(feature = "can-no-ack")]
42const TWAI_MODE: TwaiMode = TwaiMode::SelfTest;
43#[cfg(not(feature = "can-no-ack"))]
44const TWAI_MODE: TwaiMode = TwaiMode::Normal;
45
46/// Safety net for transmissions stuck retrying (shorted/unwired bus, or a
47/// frame nothing acknowledges in `Normal` mode). Dropping the transmit
48/// future on timeout issues a TWAI TX-abort, cancelling the pending
49/// retransmissions instead of retrying forever. In `Normal` mode the
50/// timeout is generous so arbitration on a busy but healthy bus never
51/// drops frames; with `can-no-ack` any wait at all means a bus fault.
52#[cfg(feature = "can-no-ack")]
53const TX_TIMEOUT: Duration = Duration::from_millis(1);
54#[cfg(not(feature = "can-no-ack"))]
55const TX_TIMEOUT: Duration = Duration::from_millis(10);
56
57/// Bidirectional pipe buffer between the TWAI peripheral and the SSH
58/// `can` subsystem bridge. Traffic in both pipes is framed by the codec
59/// layer (slcan lines or GVRET binary messages).
60pub struct BufferedCan {
61    outward: Pipe<CriticalSectionRawMutex, OUTWARD_BUF_SZ>,
62    inward: Pipe<CriticalSectionRawMutex, INWARD_BUF_SZ>,
63    dropped_rx_frames: AtomicUsize,
64    /// Bus→host framing: GVRET binary after the host sent `0xE7`,
65    /// slcan ASCII otherwise. Reset at the start of every session.
66    binary_mode: AtomicBool,
67    /// Set by [`BufferedCan::reset_protocol`]; makes the pump task drop
68    /// parser state left over from a previous session.
69    proto_reset: AtomicBool,
70}
71
72impl BufferedCan {
73    #[must_use]
74    pub fn new() -> Self {
75        BufferedCan {
76            outward: Pipe::new(),
77            inward: Pipe::new(),
78            dropped_rx_frames: AtomicUsize::from(0),
79            binary_mode: AtomicBool::new(false),
80            proto_reset: AtomicBool::new(false),
81        }
82    }
83
84    /// Transfer frames between the TWAI hardware and internal buffers.
85    ///
86    /// This should be awaited from an Embassy task run in an `InterruptExecutor`
87    /// for lower latency.
88    ///
89    /// Both directions write into `inward` (encoded bus frames and GVRET
90    /// replies). That is safe from interleaving because they run in this
91    /// single task and only issue a `write_all` after checking the whole
92    /// message fits, so the write never yields midway.
93    pub async fn run(&self, twai: twai::Twai<'static, esp_hal::Async>) {
94        let (mut twai_rx, mut twai_tx) = twai.split();
95
96        loop {
97            use embassy_futures::select::select;
98
99            let rd_from = async {
100                let mut frame_buf = [0u8; ENCODED_FRAME_MAX];
101                loop {
102                    let frame = match twai_rx.receive_async().await {
103                        Ok(frame) => frame,
104                        Err(e) => {
105                            warn!("TWAI RX error: {e:?}");
106                            continue;
107                        }
108                    };
109                    let binary = self.binary_mode.load(Ordering::Relaxed);
110                    let n = encode_frame(&frame, binary, &mut frame_buf);
111                    self.send_to_ssh(&frame_buf[..n]).await;
112                }
113            };
114
115            let rd_to = async {
116                let mut parser = CanParser::new(CAN_BITRATE);
117                let mut chunk = [0u8; 64];
118                loop {
119                    let n = self.outward.read(&mut chunk).await;
120                    if self.proto_reset.swap(false, Ordering::Relaxed) {
121                        parser.reset();
122                    }
123                    for &byte in &chunk[..n] {
124                        match parser.feed(byte) {
125                            None => {}
126                            Some(CanAction::EnableBinary) => {
127                                self.binary_mode.store(true, Ordering::Relaxed);
128                            }
129                            Some(CanAction::Reply(bytes)) => {
130                                self.send_to_ssh(&bytes).await;
131                            }
132                            Some(CanAction::Transmit(frame)) => {
133                                let id: Option<twai::Id> = match frame.id {
134                                    CanId::Standard(id) => StandardId::new(id).map(twai::Id::from),
135                                    CanId::Extended(id) => ExtendedId::new(id).map(twai::Id::from),
136                                };
137                                let Some(esp_frame) =
138                                    id.and_then(|id| EspTwaiFrame::new(id, &frame.data))
139                                else {
140                                    continue;
141                                };
142                                match with_timeout(TX_TIMEOUT, twai_tx.transmit_async(&esp_frame))
143                                    .await
144                                {
145                                    Ok(Ok(())) => {}
146                                    Ok(Err(e)) => warn!("TWAI TX error: {e:?}"),
147                                    Err(_) => warn!(
148                                        "TWAI TX stuck (bus fault or missing ACK), aborting retransmission"
149                                    ),
150                                }
151                            }
152                        }
153                    }
154                }
155            };
156
157            select(rd_from, rd_to).await;
158        }
159    }
160
161    /// Queue one whole encoded message for the SSH side, or drop it (and
162    /// count the drop) when the session isn't keeping up: a partial slcan
163    /// line or GVRET message would corrupt the stream.
164    async fn send_to_ssh(&self, msg: &[u8]) {
165        if self.inward.free_capacity() < msg.len() {
166            let _ =
167                self.dropped_rx_frames
168                    .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |d| {
169                        Some(d.saturating_add(1))
170                    });
171        } else {
172            self.inward.write_all(msg).await;
173        }
174    }
175
176    pub async fn read(&self, buf: &mut [u8]) -> usize {
177        self.inward.read(buf).await
178    }
179
180    pub async fn write(&self, buf: &[u8]) {
181        self.outward.write_all(buf).await;
182    }
183
184    /// Number of frames the RX side dropped since the last call. Resets the counter.
185    pub fn check_dropped_frames(&self) -> usize {
186        self.dropped_rx_frames.swap(0, Ordering::Relaxed)
187    }
188
189    /// Start-of-session reset: back to slcan framing, drop half-parsed
190    /// protocol state and discard bus traffic buffered while no session
191    /// was attached.
192    pub fn reset_protocol(&self) {
193        self.binary_mode.store(false, Ordering::Relaxed);
194        self.proto_reset.store(true, Ordering::Relaxed);
195        let mut sink = [0u8; 32];
196        while self.inward.try_read(&mut sink).is_ok() {}
197    }
198}
199
200impl Default for BufferedCan {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206impl ssh_stamp::can::BufferedCan for BufferedCan {
207    fn read(&self, buf: &mut [u8]) -> impl Future<Output = usize> {
208        BufferedCan::read(self, buf)
209    }
210
211    fn write(&self, buf: &[u8]) -> impl Future<Output = ()> {
212        BufferedCan::write(self, buf)
213    }
214
215    fn check_dropped_frames(&self) -> usize {
216        BufferedCan::check_dropped_frames(self)
217    }
218
219    fn reset_protocol(&self) {
220        BufferedCan::reset_protocol(self);
221    }
222}
223
224/// CAN pins configuration.
225///
226/// The pin numbers inside are target-specific and come from the board's
227/// TOML in the `ssh-stamp-esp32-boards` crate.
228pub struct EspCanPins<'a> {
229    pub tx: AnyPin<'a>,
230    pub rx: AnyPin<'a>,
231}
232
233/// Static storage for the buffered CAN singleton.
234pub static CAN_BUF: StaticCell<BufferedCan> = StaticCell::new();
235
236/// Embassy task that owns the hardware TWAI peripheral and pumps it
237/// through [`BufferedCan::run`]. Spawn from a higher-priority
238/// `InterruptExecutor` for lower latency.
239#[embassy_executor::task]
240pub async fn can_task(
241    can_buf: &'static BufferedCan,
242    twai0: TWAI0<'static>,
243    pins: EspCanPins<'static>,
244) {
245    let twai_config =
246        twai::TwaiConfiguration::new(twai0, pins.rx, pins.tx, twai::BaudRate::B500K, TWAI_MODE);
247
248    let twai = twai_config.into_async().start();
249    can_buf.run(twai).await;
250}