Skip to main content

feagi_hal/hal/
usb_cdc.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! USB CDC (Communications Device Class) Hardware Abstraction Layer
5//!
6//! This module defines the platform-agnostic trait for USB CDC Serial functionality.
7//! Platform implementations (ESP32, nRF52, STM32, RP2040) must implement this trait
8//! to provide USB serial capabilities.
9//!
10//! ## What is USB CDC?
11//!
12//! USB CDC makes the device appear as a virtual serial port (e.g., `/dev/ttyACM0`, `COM3`).
13//! It's a standard USB protocol that works with any OS without custom drivers.
14//!
15//! ## Architecture
16//!
17//! ```text
18//! ┌──────────────────────────────────────────────┐
19//! │ Application (firmware)                       │
20//! └─────────────────┬────────────────────────────┘
21//!                   │ uses
22//! ┌─────────────────▼────────────────────────────┐
23//! │ UsbCdcProvider trait (THIS FILE)             │
24//! │ - init()                                     │
25//! │ - write() / read()                           │
26//! │ - is_connected()                             │
27//! └─────────────────┬────────────────────────────┘
28//!                   │ implements
29//! ┌─────────────────▼────────────────────────────┐
30//! │ Platform Implementation                      │
31//! │ - Nrf52UsbCdc (embassy-nrf USB)             │
32//! │ - Esp32UsbCdc (esp-idf-hal USB)             │
33//! │ - Stm32UsbCdc (stm32-usbd)                  │
34//! │ - Rp2040UsbCdc (rp2040-hal USB)             │
35//! └──────────────────────────────────────────────┘
36//! ```
37//!
38//! ## Comparison: USB CDC vs BLE
39//!
40//! | Feature | USB CDC | BLE |
41//! |---------|---------|-----|
42//! | **Speed** | 12 Mbps (USB 2.0 Full Speed) | 2 Mbps (BLE 5) |
43//! | **Latency** | <1ms | 7.5-30ms (connection interval) |
44//! | **Range** | 5m (cable length) | 10-100m (wireless) |
45//! | **Pairing** | None (plug & play) | Required |
46//! | **Power** | Higher (USB powered) | Lower (BLE optimized) |
47//! | **Use Case** | Development, high-speed data | Production, wireless |
48//!
49//! ## Usage
50//!
51//! ```rust,no_run
52//! use feagi_hal::hal::UsbCdcProvider;
53//! # use feagi_hal::platforms::Nrf52UsbCdc;
54//!
55//! // Platform layer provides the implementation
56//! let mut usb: Nrf52UsbCdc = /* platform init */;
57//!
58//! // Write data
59//! usb.write(b"Hello FEAGI\n").unwrap();
60//!
61//! // Read data
62//! let mut buf = [0u8; 64];
63//! if let Ok(len) = usb.read(&mut buf) {
64//!     // Process received data
65//! }
66//! ```
67
68/// USB CDC connection status
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[cfg_attr(feature = "defmt", derive(defmt::Format))]
71pub enum UsbConnectionStatus {
72    /// USB cable not connected or not enumerated
73    Disconnected,
74    /// USB enumerated, DTR/RTS signals indicate host is ready
75    Connected,
76    /// USB suspended (host sleep/power saving)
77    Suspended,
78}
79
80/// USB CDC Serial provider trait
81///
82/// This trait must be implemented by each platform to provide USB CDC serial capabilities.
83/// The trait follows UART-like semantics: `write()` sends data, `read()` receives data.
84///
85/// ## Design Principles
86///
87/// 1. **Simple API**: Mirrors standard UART/serial interfaces
88/// 2. **Non-blocking**: `read()` returns immediately if no data
89/// 3. **Buffered I/O**: Platform manages TX/RX buffers internally
90/// 4. **Connection-aware**: `is_connected()` checks if host is ready
91///
92/// ## Thread Safety
93///
94/// Implementations do NOT need to be `Send` or `Sync` - embedded USB
95/// typically runs in a single executor/thread.
96///
97/// ## Flow Control
98///
99/// USB CDC uses DTR (Data Terminal Ready) and RTS (Request To Send) signals
100/// to indicate when the host is ready. `is_connected()` should check these signals.
101pub trait UsbCdcProvider {
102    /// Platform-specific error type
103    type Error: core::fmt::Debug;
104
105    /// Initialize USB CDC serial
106    ///
107    /// This sets up the USB stack, configures endpoints, and begins enumeration.
108    /// After this call, the device should appear as a serial port on the host.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if:
113    /// - USB peripheral initialization fails
114    /// - USB descriptor configuration is invalid
115    /// - Platform USB driver is unavailable
116    ///
117    /// # Blocking Behavior
118    ///
119    /// This method MAY block briefly during USB enumeration (~100-500ms).
120    fn init(&mut self) -> Result<(), Self::Error>;
121
122    /// Check if USB is connected and host is ready
123    ///
124    /// Returns `true` if:
125    /// - USB cable is plugged in
126    /// - Device is enumerated by host
127    /// - DTR signal is asserted (host terminal is open)
128    ///
129    /// Returns `false` if:
130    /// - Cable unplugged
131    /// - Not enumerated
132    /// - Host terminal closed (DTR de-asserted)
133    fn is_connected(&self) -> bool;
134
135    /// Get current USB connection status
136    ///
137    /// Provides more detail than `is_connected()`.
138    fn connection_status(&self) -> UsbConnectionStatus;
139
140    /// Write data to USB CDC (send to host)
141    ///
142    /// Writes data to the TX buffer. Data will be sent to the host in the next
143    /// USB IN transfer (typically within 1-10ms).
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if:
148    /// - TX buffer is full (caller should retry later)
149    /// - USB is disconnected
150    /// - USB peripheral error
151    ///
152    /// # Blocking Behavior
153    ///
154    /// This method SHOULD NOT block. If the TX buffer is full, return an error
155    /// immediately. The caller can retry or use `flush()` to wait.
156    ///
157    /// # Returns
158    ///
159    /// - `Ok(n)` where `n` is the number of bytes written (may be less than `data.len()`)
160    /// - `Err(e)` if write failed
161    fn write(&mut self, data: &[u8]) -> Result<usize, Self::Error>;
162
163    /// Read data from USB CDC (receive from host)
164    ///
165    /// Reads available data from the RX buffer into the provided buffer.
166    ///
167    /// # Returns
168    ///
169    /// - `Ok(n)` where `n` is the number of bytes read (0 if no data available)
170    /// - `Err(e)` if read failed
171    ///
172    /// # Non-blocking
173    ///
174    /// This method MUST NOT block. If no data is available, return `Ok(0)` immediately.
175    fn read(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
176
177    /// Flush TX buffer (block until all data is sent)
178    ///
179    /// Waits until all pending TX data has been sent to the host.
180    ///
181    /// # Blocking Behavior
182    ///
183    /// This method MAY block until the TX buffer is empty (typically <10ms).
184    ///
185    /// # Use Cases
186    ///
187    /// - Before entering sleep mode
188    /// - Before disconnecting USB
189    /// - After writing critical data (e.g., error messages)
190    fn flush(&mut self) -> Result<(), Self::Error>;
191
192    /// Get number of bytes available to read (optional)
193    ///
194    /// Returns the number of bytes in the RX buffer.
195    /// Default implementation returns 0 (unknown).
196    fn available(&self) -> usize {
197        0 // Default: unknown
198    }
199
200    /// Get free space in TX buffer (optional)
201    ///
202    /// Returns the number of bytes that can be written without blocking.
203    /// Default implementation returns 0 (unknown).
204    fn write_capacity(&self) -> usize {
205        0 // Default: unknown
206    }
207}
208
209/// Helper trait for platforms that support async USB CDC operations
210///
211/// This is optional and only used by platforms with async runtimes (embassy, tokio, etc.).
212#[cfg(feature = "async")]
213pub trait AsyncUsbCdcProvider: UsbCdcProvider {
214    /// Async version of `write`
215    ///
216    /// Waits until data can be written (if TX buffer is full).
217    async fn write_async(&mut self, data: &[u8]) -> Result<usize, Self::Error>;
218
219    /// Async version of `read`
220    ///
221    /// Waits until data is available (blocks if RX buffer is empty).
222    async fn read_async(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
223
224    /// Async version of `flush`
225    async fn flush_async(&mut self) -> Result<(), Self::Error>;
226}
227
228/// Helper: Write a complete line with newline
229///
230/// This is a convenience method that ensures a newline is appended.
231pub fn writeln<T: UsbCdcProvider>(usb: &mut T, data: &[u8]) -> Result<(), T::Error> {
232    usb.write(data)?;
233    usb.write(b"\n")?;
234    Ok(())
235}
236
237/// Helper: Read until newline or buffer full
238///
239/// Returns the number of bytes read (including newline if present).
240pub fn read_line<T: UsbCdcProvider>(usb: &mut T, buffer: &mut [u8]) -> Result<usize, T::Error> {
241    let mut total = 0;
242    loop {
243        let n = usb.read(&mut buffer[total..])?;
244        if n == 0 {
245            break; // No more data available
246        }
247        total += n;
248
249        // Check for newline
250        if buffer[..total].contains(&b'\n') {
251            break;
252        }
253
254        // Buffer full
255        if total >= buffer.len() {
256            break;
257        }
258    }
259    Ok(total)
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    // Mock USB CDC implementation for testing
267    struct MockUsbCdc {
268        connected: bool,
269        tx_buffer: heapless::Vec<u8, 256>,
270        rx_buffer: heapless::Vec<u8, 256>,
271    }
272
273    impl MockUsbCdc {
274        fn new() -> Self {
275            Self {
276                connected: false,
277                tx_buffer: heapless::Vec::new(),
278                rx_buffer: heapless::Vec::new(),
279            }
280        }
281
282        // Helper: Simulate host sending data
283        fn _push_rx_data(&mut self, data: &[u8]) {
284            for &byte in data {
285                let _ = self.rx_buffer.push(byte);
286            }
287        }
288    }
289
290    impl UsbCdcProvider for MockUsbCdc {
291        type Error = &'static str;
292
293        fn init(&mut self) -> Result<(), Self::Error> {
294            self.connected = false; // Not connected until enumerated
295            Ok(())
296        }
297
298        fn is_connected(&self) -> bool {
299            self.connected
300        }
301
302        fn connection_status(&self) -> UsbConnectionStatus {
303            if self.connected {
304                UsbConnectionStatus::Connected
305            } else {
306                UsbConnectionStatus::Disconnected
307            }
308        }
309
310        fn write(&mut self, data: &[u8]) -> Result<usize, Self::Error> {
311            if !self.connected {
312                return Err("Not connected");
313            }
314
315            let mut written = 0;
316            for &byte in data {
317                if self.tx_buffer.push(byte).is_ok() {
318                    written += 1;
319                } else {
320                    break; // Buffer full
321                }
322            }
323            Ok(written)
324        }
325
326        fn read(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
327            let len = self.rx_buffer.len().min(buffer.len());
328            buffer[..len].copy_from_slice(&self.rx_buffer[..len]);
329
330            // Remove read bytes from RX buffer
331            for _ in 0..len {
332                self.rx_buffer.remove(0);
333            }
334
335            Ok(len)
336        }
337
338        fn flush(&mut self) -> Result<(), Self::Error> {
339            if !self.connected {
340                return Err("Not connected");
341            }
342            self.tx_buffer.clear(); // Simulate data sent
343            Ok(())
344        }
345
346        fn available(&self) -> usize {
347            self.rx_buffer.len()
348        }
349
350        fn write_capacity(&self) -> usize {
351            self.tx_buffer.capacity() - self.tx_buffer.len()
352        }
353    }
354
355    #[test]
356    fn test_mock_usb_init() {
357        let mut usb = MockUsbCdc::new();
358        assert!(usb.init().is_ok());
359        assert!(!usb.is_connected());
360    }
361
362    #[test]
363    fn test_mock_usb_write_not_connected() {
364        let mut usb = MockUsbCdc::new();
365        assert!(usb.write(b"test").is_err());
366    }
367
368    #[test]
369    fn test_mock_usb_write_connected() {
370        let mut usb = MockUsbCdc::new();
371        usb.connected = true;
372
373        let written = usb.write(b"Hello").unwrap();
374        assert_eq!(written, 5);
375        assert_eq!(usb.tx_buffer.as_slice(), b"Hello");
376    }
377
378    #[test]
379    fn test_mock_usb_read() {
380        let mut usb = MockUsbCdc::new();
381        usb._push_rx_data(b"Data from host");
382
383        let mut buf = [0u8; 64];
384        let len = usb.read(&mut buf).unwrap();
385        assert_eq!(len, 14);
386        assert_eq!(&buf[..len], b"Data from host");
387    }
388
389    #[test]
390    fn test_mock_usb_flush() {
391        let mut usb = MockUsbCdc::new();
392        usb.connected = true;
393        usb.write(b"test").unwrap();
394
395        assert!(!usb.tx_buffer.is_empty());
396        usb.flush().unwrap();
397        assert!(usb.tx_buffer.is_empty());
398    }
399
400    #[test]
401    fn test_writeln_helper() {
402        let mut usb = MockUsbCdc::new();
403        usb.connected = true;
404
405        writeln(&mut usb, b"Line 1").unwrap();
406        assert_eq!(usb.tx_buffer.as_slice(), b"Line 1\n");
407    }
408
409    #[test]
410    fn test_connection_status() {
411        let usb = MockUsbCdc::new();
412        assert_eq!(usb.connection_status(), UsbConnectionStatus::Disconnected);
413    }
414
415    #[test]
416    fn test_available_and_capacity() {
417        let mut usb = MockUsbCdc::new();
418        usb._push_rx_data(b"test");
419
420        assert_eq!(usb.available(), 4);
421        assert_eq!(usb.write_capacity(), 256); // Empty TX buffer
422    }
423}