rp_usb_console/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3#![warn(missing_docs)]
4
5//! USB CDC logging & command channel for RP2040 (embassy).
6//!
7//! This crate provides a zero-heap solution for bidirectional USB communication
8//! on RP2040 microcontrollers using the Embassy async framework. It enables both
9//! logging output and line-buffered command input (terminated by `\r`, `\n`, or
10//! `\r\n`) over a standard USB CDC ACM interface.
11//!
12//! # Quick Start
13//!
14//! ```rust,no_run
15//! use embassy_executor::Spawner;
16//! use embassy_sync::channel::Channel;
17//! use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
18//! use log::info;
19//! use rp_usb_console::USB_READ_BUFFER_SIZE;
20//!
21//! // Create a channel for receiving line-buffered commands from USB
22//! static COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4> = Channel::new();
23//!
24//! #[embassy_executor::main]
25//! async fn main(spawner: Spawner) {
26//!     let p = embassy_rp::init(Default::default());
27//!     
28//!     // Initialize USB logging with Info level
29//!     rp_usb_console::start(
30//!         spawner,
31//!         log::LevelFilter::Info,
32//!         p.USB,
33//!         Some(COMMAND_CHANNEL.sender()),
34//!     );
35//!     
36//!     // Now you can use standard log macros
37//!     info!("Hello over USB!");
38//!     
39//!     // Handle incoming commands in a separate task
40//!     spawner.spawn(command_handler()).unwrap();
41//! }
42//!
43//! #[embassy_executor::task]
44//! async fn command_handler() {
45//!     let receiver = COMMAND_CHANNEL.receiver();
46//!     loop {
47//!         let command = receiver.receive().await;
48//!         // Process received command data (trailing zeros may be present)
49//!         info!("Received command: {:?}", command);
50//!     }
51//! }
52//! ```
53//!
54//! # Features
55//!
56//! - **Zero-heap operation**: All buffers are fixed-size and statically allocated
57//! - **Non-blocking logging**: Messages are dropped rather than blocking when channels are full
58//! - **USB CDC ACM**: Standard USB serial interface compatible with most terminal programs
59//! - **Packet fragmentation**: Large messages are automatically split for reliable transmission
60//! - **Bidirectional**: Both log output and line-buffered command input over the same USB connection
61//! - **Configurable command sink**: Forward parsed commands to your own channel or disable command forwarding entirely
62//! - **Embassy integration**: Designed for Embassy's async executor on RP2040
63//!
64//! # Design Principles
65//!
66//! This crate prioritizes:
67//! - **Real-time behavior**: Never blocks the caller, even when USB is disconnected
68//! - **Memory efficiency**: Fixed buffers with no dynamic allocation
69//! - **Reliability**: Fragmented transmission reduces host-side latency issues
70//! - **Safety**: Single-core assumptions with proper synchronization primitives
71
72use core::cell::{RefCell, UnsafeCell};
73use core::cmp::min;
74use core::fmt::{Result as FmtResult, Write};
75use critical_section::Mutex as CsMutex;
76use embassy_executor::Spawner;
77use embassy_rp::peripherals::USB;
78use embassy_rp::usb::{Driver, InterruptHandler};
79use embassy_rp::{bind_interrupts, Peri};
80use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
81use embassy_sync::channel::{Channel, Sender};
82use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender as UsbSender, State};
83use embassy_usb::{Builder, UsbDevice};
84use log::{Level, LevelFilter, Log, Metadata, Record};
85use rp2040_hal::rom_data::reset_to_usb_boot;
86
87bind_interrupts!(struct Irqs {
88    USBCTRL_IRQ => InterruptHandler<USB>;
89});
90
91/// Size of each USB packet fragment.
92const PACKET_SIZE: usize = 64;
93const MODULE_FILTER_CAPACITY: usize = 255;
94/// Size (in bytes) of the receive buffer used to accumulate incoming USB command data before processing.
95///
96/// Applications that forward commands should allocate their channels using this
97/// buffer size to avoid truncation.
98pub const USB_READ_BUFFER_SIZE: usize = 255;
99
100#[derive(Copy, Clone)]
101struct LogModuleSettings {
102    module_filter: [u8; MODULE_FILTER_CAPACITY],
103    module_filter_len: usize,
104    module_level: LevelFilter,
105    other_level: LevelFilter,
106}
107
108impl LogModuleSettings {
109    fn new(module_name: &str, module_level: LevelFilter, other_level: LevelFilter) -> Self {
110        let mut buf = [0u8; MODULE_FILTER_CAPACITY];
111        let bytes = module_name.as_bytes();
112        let len = min(bytes.len(), MODULE_FILTER_CAPACITY);
113        buf[..len].copy_from_slice(&bytes[..len]);
114
115        Self {
116            module_filter: buf,
117            module_filter_len: len,
118            module_level,
119            other_level,
120        }
121    }
122
123    fn module_name(&self) -> &str {
124        core::str::from_utf8(&self.module_filter[..self.module_filter_len]).unwrap_or("")
125    }
126}
127
128/// Fixed-size (255 byte) log/command message buffer with USB packet fragmentation support.
129///
130/// This struct provides a fixed-size buffer for log messages that can be efficiently
131/// transmitted over USB by fragmenting into smaller packets. Messages longer than the
132/// buffer capacity are automatically truncated with an ellipsis indicator.
133///
134/// ```
135pub struct LogMessage {
136    len: usize,
137    buf: [u8; 255],
138}
139
140impl LogMessage {
141    /// Create an empty message buffer.
142    pub fn new() -> Self {
143        Self { len: 0, buf: [0; 255] }
144    }
145
146    /// Append a string (UTF-8 bytes) truncating if capacity exceeded.
147    ///
148    /// If the message would exceed the 255-byte capacity, it is truncated
149    /// and the last three bytes are replaced with dots to indicate truncation.
150    fn push_str(&mut self, s: &str) {
151        for &b in s.as_bytes() {
152            if self.len >= self.buf.len() {
153                self.buf[self.len - 1] = b'.'; // Indicate truncation with ellipsis
154                self.buf[self.len - 2] = b'.';
155                self.buf[self.len - 3] = b'.';
156                break;
157            }
158            self.buf[self.len] = b;
159            self.len += 1;
160        }
161    }
162
163    /// Number of 64-byte USB packets required to send this message.
164    ///
165    /// Returns the minimum number of USB packets needed to transmit the
166    /// entire message content.
167    pub fn packet_count(&self) -> usize {
168        self.len / PACKET_SIZE + if self.len % PACKET_SIZE == 0 { 0 } else { 1 }
169    }
170
171    /// Slice for a specific packet index (0-based) containing that chunk.
172    ///
173    /// Returns the bytes for the specified packet index. The last packet
174    /// may be shorter than `PACKET_SIZE` if the message doesn't divide evenly.
175    ///
176    pub fn as_packet_bytes(&self, packet_index: usize) -> &[u8] {
177        let start = core::cmp::min(packet_index * PACKET_SIZE, self.len);
178        let end = core::cmp::min(start + PACKET_SIZE, self.len);
179        &self.buf[start..end]
180    }
181}
182
183impl Write for LogMessage {
184    fn write_str(&mut self, s: &str) -> FmtResult {
185        self.push_str(s);
186        Ok(())
187    }
188}
189
190/// Channel type for sending log messages from the application to the USB sender task.
191type LogChannel = Channel<CriticalSectionRawMutex, LogMessage, 4>;
192static LOG_CHANNEL: LogChannel = Channel::new();
193
194static LOG_SETTINGS: CsMutex<RefCell<Option<LogModuleSettings>>> = CsMutex::new(RefCell::new(None));
195
196fn parse_level_filter(token: &str) -> Option<LevelFilter> {
197    let mut chars = token.chars();
198    let ch = chars.next()?.to_ascii_uppercase();
199    if chars.next().is_some() {
200        return None;
201    }
202    match ch {
203        'T' => Some(LevelFilter::Trace),
204        'D' => Some(LevelFilter::Debug),
205        'I' => Some(LevelFilter::Info),
206        'W' => Some(LevelFilter::Warn),
207        'E' => Some(LevelFilter::Error),
208        'O' => Some(LevelFilter::Off),
209        _ => None,
210    }
211}
212
213fn level_allowed(filter: LevelFilter, level: Level) -> bool {
214    match filter.to_level() {
215        Some(max) => level <= max,
216        None => false,
217    }
218}
219
220/// Internal logger implementation that forwards formatted lines to the USB channel.
221///
222/// This logger formats log records and sends them through the internal channel
223/// to be transmitted over USB. If the channel is full, messages are silently
224/// dropped to maintain non-blocking behavior.
225struct USBLogger;
226
227impl Log for USBLogger {
228    fn enabled(&self, _metadata: &Metadata) -> bool {
229        true
230    }
231
232    fn log(&self, record: &Record) {
233        if self.enabled(record.metadata()) {
234            let should_emit = critical_section::with(|cs| {
235                let guard = LOG_SETTINGS.borrow(cs);
236                let settings_ref = guard.borrow();
237                match &*settings_ref {
238                    Some(settings) => {
239                        let module_name = settings.module_name();
240                        let target_filter = match record.module_path() {
241                            Some(path) if !module_name.is_empty() && path.contains(module_name) => settings.module_level,
242                            _ => settings.other_level,
243                        };
244                        level_allowed(target_filter, record.level())
245                    }
246                    None => true,
247                }
248            });
249
250            if !should_emit {
251                return;
252            }
253
254            let mut message = LogMessage::new();
255            let path = if let Some(p) = record.module_path() { p } else { "" };
256            if write!(&mut message, "[{}] {}: {}\r\n", record.level(), path, record.args()).is_ok() {
257                // Non-blocking send. If the channel is full, the message is dropped.
258                let _ = LOG_CHANNEL.try_send(message);
259            }
260        }
261    }
262    fn flush(&self) {}
263}
264
265static LOGGER: USBLogger = USBLogger;
266
267/// USB receive task that handles incoming data from the host.
268///
269/// This task waits for USB connection, then continuously reads incoming packets
270/// and accumulates them into a single buffer until a line terminator (`\r`,
271/// `\n`, or `\r\n`) is received. Completed lines are dispatched to the optional
272/// command channel for application handling, while built-in control commands
273/// (such as `/BS` and `/LM`) are processed internally. If `command_sender` is
274/// `None`, application commands are ignored after internal processing.
275///
276/// The task automatically handles USB disconnection/reconnection cycles.
277#[embassy_executor::task]
278async fn usb_rx_task(
279    mut receiver: Receiver<'static, Driver<'static, USB>>,
280    command_sender: Option<Sender<'static, CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4>>,
281) {
282    let mut buf = [0u8; USB_READ_BUFFER_SIZE];
283    let mut buf_position: usize = 0;
284    loop {
285        receiver.wait_connection().await;
286        let mut read_buf = [0u8; USB_READ_BUFFER_SIZE];
287        loop {
288            match receiver.read_packet(&mut read_buf).await {
289                Ok(len) => {
290                    if len + buf_position > USB_READ_BUFFER_SIZE {
291                        buf_position = 0;
292                    }
293                    buf[buf_position..buf_position + len].copy_from_slice(&read_buf[..len]);
294                    buf_position += len;
295
296                    if !(buf.contains(&b'\n') || buf.contains(&b'\r')) {
297                        continue; // Wait for more data
298                    }
299
300                    let mut processed = false;
301                    if let Ok(command_str) = core::str::from_utf8(&buf[0..3]) {
302                        match command_str {
303                            "/BS" => {
304                                reset_to_usb_boot(0, 0);
305                            }
306                            "/RS" => {
307                                rp2040_hal::reset();
308                            }
309                            "/LT" => {
310                                processed = true;
311                                unsafe {
312                                    log::set_max_level_racy(LevelFilter::Trace);
313                                }
314                                log::info!("Log level set to Trace");
315                            }
316                            "/LD" => {
317                                processed = true;
318                                unsafe {
319                                    log::set_max_level_racy(LevelFilter::Debug);
320                                }
321                                log::info!("Log level set to Debug");
322                            }
323                            "/LI" => {
324                                processed = true;
325                                unsafe {
326                                    log::set_max_level_racy(LevelFilter::Info);
327                                }
328                                log::info!("Log level set to Info");
329                            }
330                            "/LW" => {
331                                processed = true;
332                                unsafe {
333                                    log::set_max_level_racy(LevelFilter::Warn);
334                                }
335                                log::warn!("Log level set to Warn");
336                            }
337                            "/LE" => {
338                                processed = true;
339                                unsafe {
340                                    log::set_max_level_racy(LevelFilter::Error);
341                                }
342                                log::error!("Log level set to Error");
343                            }
344                            "/LO" => {
345                                processed = true;
346                                unsafe {
347                                    log::set_max_level_racy(LevelFilter::Off);
348                                }
349                                // Cannot log here since logging is now off
350                            }
351                            "/LM" => {
352                                processed = true;
353                                if let Ok(param_string) = core::str::from_utf8(&buf[4..]) {
354                                    let param_string = param_string.trim_matches(char::from(0)).trim();
355
356                                    let mut parts = param_string.splitn(2, ',');
357                                    match (parts.next(), parts.next()) {
358                                        (Some(module_filter), Some(module_log_level)) => {
359                                            let module_filter = module_filter.trim();
360                                            let module_level_str = module_log_level.trim();
361
362                                            if module_filter.is_empty() {
363                                                log::error!("Invalid /LM parameters '{}'. Module name cannot be empty", param_string);
364                                                buf_position = 0; // Reset buffer for next command
365                                                buf = [0u8; USB_READ_BUFFER_SIZE];
366                                                continue;
367                                            }
368
369                                            if module_level_str == "-" {
370                                                log::info!("Module logging override cleared for '{}'", module_filter);
371                                                critical_section::with(|cs| {
372                                                    if let Some(x) = *LOG_SETTINGS.borrow(cs).borrow() {
373                                                        unsafe {
374                                                            log::set_max_level_racy(x.other_level);
375                                                        }
376                                                    }
377                                                    *LOG_SETTINGS.borrow(cs).borrow_mut() = None;
378                                                });
379                                                buf_position = 0; // Reset buffer for next command
380                                                buf = [0u8; USB_READ_BUFFER_SIZE];
381                                                continue;
382                                            }
383
384                                            let Some(module_level) = parse_level_filter(module_level_str) else {
385                                                log::error!("Invalid /LM module level '{}'. Use one of T,D,I,W,E,O", module_level_str);
386                                                buf_position = 0; // Reset buffer for next command
387                                                buf = [0u8; USB_READ_BUFFER_SIZE];
388                                                continue;
389                                            };
390                                            let other_level = log::max_level();
391
392                                            unsafe {
393                                                log::set_max_level_racy(module_level);
394                                            }
395
396                                            let settings = LogModuleSettings::new(module_filter, module_level, other_level);
397
398                                            critical_section::with(|cs| {
399                                                *LOG_SETTINGS.borrow(cs).borrow_mut() = Some(settings);
400                                            });
401
402                                            log::info!("Module logging override: module='{}' module_level={:?}", module_filter, module_level);
403                                        }
404                                        _ => {
405                                            log::error!(
406                                                "Invalid /LM parameters '{}'. Expected format: /LM <module_filter>,<module_log_level[T|D|I|W|E]>",
407                                                param_string
408                                            );
409                                        }
410                                    }
411                                }
412                            }
413
414                            _ => {}
415                        }
416                    }
417                    if !processed && buf_position > 1 {
418                        if let Some(sender) = &command_sender {
419                            // Null-terminate the command string
420                            if buf_position < USB_READ_BUFFER_SIZE {
421                                buf[buf_position] = 0;
422                            } else {
423                                buf[USB_READ_BUFFER_SIZE - 1] = 0;
424                            }
425                            let _ = sender.send(buf).await;
426                        }
427                    }
428                    buf_position = 0; // Reset buffer for next command
429                    buf = [0u8; USB_READ_BUFFER_SIZE];
430                }
431                Err(_) => break,
432            }
433        }
434    }
435}
436
437/// USB transmit task that sends log messages over USB.
438///
439/// This task drains the internal log channel and transmits each message by
440/// fragmenting it into 64-byte USB packets. Large messages are automatically
441/// split across multiple packets for reliable transmission.
442///
443/// The task handles USB disconnection gracefully and will resume transmission
444/// when the connection is restored.
445#[embassy_executor::task]
446async fn usb_tx_task(mut sender: UsbSender<'static, Driver<'static, USB>>) {
447    loop {
448        sender.wait_connection().await;
449        loop {
450            // Wait for a log message from the channel.
451            let message = LOG_CHANNEL.receive().await;
452            let mut problem = false;
453            for i in 0..message.packet_count() {
454                let packet = message.as_packet_bytes(i);
455
456                // Send the log message over USB. If sending fails, break to wait for reconnection.
457                if sender.write_packet(packet).await.is_err() {
458                    problem = true;
459                }
460            }
461            if problem {
462                break;
463            }
464        }
465    }
466}
467
468/// USB device task that runs the USB device state machine.
469///
470/// This task handles the low-level USB device protocol and must be spawned
471/// for USB communication to function. It manages device enumeration,
472/// configuration, and the USB control endpoint.
473#[embassy_executor::task]
474async fn usb_device_task(mut usb: UsbDevice<'static, Driver<'static, USB>>) {
475    usb.run().await;
476}
477
478/// Initialize USB CDC logging and command channel, spawning all necessary tasks.
479///
480/// This function sets up the USB CDC ACM interface for bidirectional communication
481/// and spawns three tasks to handle the USB device, transmission, and reception.
482/// It also initializes the global logger to forward log messages over USB.
483///
484/// # Arguments
485///
486/// * `spawner` - Embassy task spawner for creating the USB tasks
487/// * `level` - Maximum log level to transmit (e.g., `LevelFilter::Info`)
488/// * `usb_peripheral` - RP2040 USB peripheral instance
489/// * `command_sender` - Optional channel sender for receiving line-buffered commands from the host (`None` disables forwarding)
490///
491/// # Panics
492///
493/// Panics if called more than once, as it sets the global logger.
494///
495/// # Examples
496///
497/// ```rust,no_run
498/// use embassy_executor::Spawner;
499/// use embassy_sync::channel::Channel;
500/// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
501/// use log::LevelFilter;
502/// # use rp_usb_console;
503///
504/// static COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, [u8; rp_usb_console::USB_READ_BUFFER_SIZE], 4> = Channel::new();
505///
506/// # async fn example(spawner: Spawner, usb_peripheral: embassy_rp::peripherals::USB) {
507/// rp_usb_console::start(
508///     spawner,
509///     LevelFilter::Info,
510///     usb_peripheral,
511///     Some(COMMAND_CHANNEL.sender()),
512/// );
513/// # }
514/// ```
515pub fn start(
516    spawner: Spawner,
517    level: LevelFilter,
518    usb_peripheral: Peri<'static, USB>,
519    command_sender: Option<Sender<'static, CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4>>,
520) {
521    // Initialize the logger (use racy variants on targets without atomic ptr support, e.g. thumbv6m/RP2040)
522    unsafe {
523        log::set_logger_racy(&LOGGER).unwrap();
524        log::set_max_level_racy(level);
525    }
526
527    // Simple wrapper to mark our single-threaded statics as Sync. RP2040 + embassy executor: we ensure single-core access.
528    struct StaticCell<T>(UnsafeCell<T>);
529    unsafe impl<T> Sync for StaticCell<T> {}
530
531    static DEVICE_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
532    static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
533    static BOS_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
534    static CONTROL_BUF: StaticCell<[u8; 128]> = StaticCell(UnsafeCell::new([0; 128]));
535    static STATE: StaticCell<State> = StaticCell(UnsafeCell::new(State::new()));
536
537    let driver = Driver::new(usb_peripheral, Irqs);
538
539    let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
540    config.manufacturer = Some("Embassy");
541    config.product = Some("Modular USB-Serial");
542    config.serial_number = Some("12345678");
543    config.max_power = 100;
544
545    let mut builder = Builder::new(
546        driver,
547        config,
548        unsafe { &mut *DEVICE_DESC.0.get() },
549        unsafe { &mut *CONFIG_DESC.0.get() },
550        unsafe { &mut *BOS_DESC.0.get() },
551        unsafe { &mut *CONTROL_BUF.0.get() },
552    );
553
554    let class = CdcAcmClass::new(&mut builder, unsafe { &mut *STATE.0.get() }, 64);
555    let (sender, receiver) = class.split();
556    let usb = builder.build();
557
558    // Spawn all the necessary tasks.
559    spawner.spawn(usb_device_task(usb)).unwrap();
560    spawner.spawn(usb_tx_task(sender)).unwrap();
561    spawner.spawn(usb_rx_task(receiver, command_sender)).unwrap();
562}