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}