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.
191///
192/// Each `LogMessage` contains a 255-byte buffer, so the total memory usage of this channel is
193/// approximately `CAPACITY × 255` bytes plus channel bookkeeping. A capacity of 32 was chosen
194/// as a balance between buffering bursty logs and conserving RAM on RP2040-class MCUs with
195/// limited memory.
196///
197/// Earlier revisions of this module experimented with higher capacities (for example, 100
198/// messages), but this significantly increased static RAM usage without a proportional benefit
199/// on typical RP2040 workloads. The current capacity of 32 is therefore an intentional
200/// compromise. Increase this value only if profiling shows frequent log drops and your
201/// application can afford the additional static RAM usage; conversely, you may reduce it further
202/// on extremely memory-constrained systems at the cost of more aggressive log dropping.
203type LogChannel = Channel<CriticalSectionRawMutex, LogMessage, 32>;
204static LOG_CHANNEL: LogChannel = Channel::new();
205
206// Log settings protected by critical section for dual-core safety.
207// RP2040's critical sections use hardware spinlocks to synchronize between cores.
208static LOG_SETTINGS: CsMutex<RefCell<Option<LogModuleSettings>>> = CsMutex::new(RefCell::new(None));
209
210/// Read current log settings (inside critical section).
211#[inline]
212fn get_log_settings() -> Option<LogModuleSettings> {
213    critical_section::with(|cs| *LOG_SETTINGS.borrow(cs).borrow())
214}
215
216/// Update log settings (inside critical section).
217#[inline]
218fn set_log_settings(settings: Option<LogModuleSettings>) {
219    critical_section::with(|cs| {
220        *LOG_SETTINGS.borrow(cs).borrow_mut() = settings;
221    });
222}
223
224fn parse_level_filter(token: &str) -> Option<LevelFilter> {
225    let mut chars = token.chars();
226    let ch = chars.next()?.to_ascii_uppercase();
227    if chars.next().is_some() {
228        return None;
229    }
230    match ch {
231        'T' => Some(LevelFilter::Trace),
232        'D' => Some(LevelFilter::Debug),
233        'I' => Some(LevelFilter::Info),
234        'W' => Some(LevelFilter::Warn),
235        'E' => Some(LevelFilter::Error),
236        'O' => Some(LevelFilter::Off),
237        _ => None,
238    }
239}
240
241fn level_allowed(filter: LevelFilter, level: Level) -> bool {
242    match filter.to_level() {
243        Some(max) => level <= max,
244        None => false,
245    }
246}
247
248/// Internal logger implementation that forwards formatted lines to the USB channel.
249///
250/// This logger formats log records and sends them through the internal channel
251/// to be transmitted over USB. If the channel is full, messages are silently
252/// dropped to maintain non-blocking behavior.
253struct USBLogger;
254
255impl Log for USBLogger {
256    fn enabled(&self, _metadata: &Metadata) -> bool {
257        true
258    }
259
260    fn log(&self, record: &Record) {
261        if self.enabled(record.metadata()) {
262            let should_emit = match get_log_settings() {
263                Some(settings) => {
264                    let module_name = settings.module_name();
265                    let target_filter = match record.module_path() {
266                        Some(path) if !module_name.is_empty() && path.contains(module_name) => settings.module_level,
267                        _ => settings.other_level,
268                    };
269                    level_allowed(target_filter, record.level())
270                }
271                None => true,
272            };
273
274            if !should_emit {
275                return;
276            }
277
278            let mut message = LogMessage::new();
279            let path = if let Some(p) = record.module_path() { p } else { "" };
280            if write!(&mut message, "[{}] {}: {}\r\n", record.level(), path, record.args()).is_ok() {
281                // Non-blocking send. If the channel is full, the message is dropped.
282                let _ = LOG_CHANNEL.try_send(message);
283            }
284        }
285    }
286    fn flush(&self) {}
287}
288
289static LOGGER: USBLogger = USBLogger;
290
291/// USB receive task that handles incoming data from the host.
292///
293/// This task waits for USB connection, then continuously reads incoming packets
294/// and accumulates them into a single buffer until a line terminator (`\r`,
295/// `\n`, or `\r\n`) is received. Completed lines are dispatched to the optional
296/// command channel for application handling, while built-in control commands
297/// (such as `/BS` and `/LM`) are processed internally. If `command_sender` is
298/// `None`, application commands are ignored after internal processing.
299///
300/// The task automatically handles USB disconnection/reconnection cycles.
301#[embassy_executor::task]
302async fn usb_rx_task(
303    mut receiver: Receiver<'static, Driver<'static, USB>>,
304    command_sender: Option<Sender<'static, CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4>>,
305) {
306    let mut buf = [0u8; USB_READ_BUFFER_SIZE];
307    let mut buf_position: usize = 0;
308    loop {
309        receiver.wait_connection().await;
310        let mut read_buf = [0u8; USB_READ_BUFFER_SIZE];
311        loop {
312            match receiver.read_packet(&mut read_buf).await {
313                Ok(len) => {
314                    if len + buf_position > USB_READ_BUFFER_SIZE {
315                        buf_position = 0;
316                    }
317                    buf[buf_position..buf_position + len].copy_from_slice(&read_buf[..len]);
318                    buf_position += len;
319
320                    if !(buf.contains(&b'\n') || buf.contains(&b'\r')) {
321                        continue; // Wait for more data
322                    }
323
324                    let mut processed = false;
325                    if let Ok(command_str) = core::str::from_utf8(&buf[0..3]) {
326                        match command_str {
327                            "/BS" => {
328                                reset_to_usb_boot(0, 0);
329                            }
330                            "/RS" => {
331                                rp2040_hal::reset();
332                            }
333                            "/LT" => {
334                                processed = true;
335                                unsafe {
336                                    log::set_max_level_racy(LevelFilter::Trace);
337                                }
338                                log::info!("Log level set to Trace");
339                            }
340                            "/LD" => {
341                                processed = true;
342                                unsafe {
343                                    log::set_max_level_racy(LevelFilter::Debug);
344                                }
345                                log::info!("Log level set to Debug");
346                            }
347                            "/LI" => {
348                                processed = true;
349                                unsafe {
350                                    log::set_max_level_racy(LevelFilter::Info);
351                                }
352                                log::info!("Log level set to Info");
353                            }
354                            "/LW" => {
355                                processed = true;
356                                unsafe {
357                                    log::set_max_level_racy(LevelFilter::Warn);
358                                }
359                                log::warn!("Log level set to Warn");
360                            }
361                            "/LE" => {
362                                processed = true;
363                                unsafe {
364                                    log::set_max_level_racy(LevelFilter::Error);
365                                }
366                                log::error!("Log level set to Error");
367                            }
368                            "/LO" => {
369                                processed = true;
370                                unsafe {
371                                    log::set_max_level_racy(LevelFilter::Off);
372                                }
373                                // Cannot log here since logging is now off
374                            }
375                            "/LM" => {
376                                processed = true;
377                                if let Ok(param_string) = core::str::from_utf8(&buf[4..]) {
378                                    let param_string = param_string.trim_matches(char::from(0)).trim();
379
380                                    let mut parts = param_string.splitn(2, ',');
381                                    match (parts.next(), parts.next()) {
382                                        (Some(module_filter), Some(module_log_level)) => {
383                                            let module_filter = module_filter.trim();
384                                            let module_level_str = module_log_level.trim();
385
386                                            if module_filter.is_empty() {
387                                                log::error!("Invalid /LM parameters '{}'. Module name cannot be empty", param_string);
388                                                buf_position = 0; // Reset buffer for next command
389                                                buf = [0u8; USB_READ_BUFFER_SIZE];
390                                                continue;
391                                            }
392
393                                            if module_level_str == "-" {
394                                                log::info!("Module logging override cleared for '{}'", module_filter);
395                                                if let Some(settings) = get_log_settings() {
396                                                    unsafe {
397                                                        log::set_max_level_racy(settings.other_level);
398                                                    }
399                                                }
400                                                set_log_settings(None);
401                                                buf_position = 0; // Reset buffer for next command
402                                                buf = [0u8; USB_READ_BUFFER_SIZE];
403                                                continue;
404                                            }
405
406                                            let Some(module_level) = parse_level_filter(module_level_str) else {
407                                                log::error!("Invalid /LM module level '{}'. Use one of T,D,I,W,E,O", module_level_str);
408                                                buf_position = 0; // Reset buffer for next command
409                                                buf = [0u8; USB_READ_BUFFER_SIZE];
410                                                continue;
411                                            };
412                                            let other_level = log::max_level();
413
414                                            unsafe {
415                                                log::set_max_level_racy(module_level);
416                                            }
417
418                                            let settings = LogModuleSettings::new(module_filter, module_level, other_level);
419                                            set_log_settings(Some(settings));
420
421                                            log::info!("Module logging override: module='{}' module_level={:?}", module_filter, module_level);
422                                        }
423                                        _ => {
424                                            log::error!(
425                                                "Invalid /LM parameters '{}'. Expected format: /LM <module_filter>,<module_log_level[T|D|I|W|E]>",
426                                                param_string
427                                            );
428                                        }
429                                    }
430                                }
431                            }
432
433                            _ => {}
434                        }
435                    }
436                    if !processed && buf_position > 1 {
437                        if let Some(sender) = &command_sender {
438                            // Null-terminate the command string
439                            if buf_position < USB_READ_BUFFER_SIZE {
440                                buf[buf_position] = 0;
441                            } else {
442                                buf[USB_READ_BUFFER_SIZE - 1] = 0;
443                            }
444                            // Use try_send to avoid blocking if channel is full
445                            let _ = sender.try_send(buf);
446                        }
447                    }
448                    buf_position = 0; // Reset buffer for next command
449                    buf = [0u8; USB_READ_BUFFER_SIZE];
450                }
451                Err(_) => break,
452            }
453        }
454    }
455}
456
457/// USB transmit task that sends log messages over USB.
458///
459/// This task drains the internal log channel and transmits each message by
460/// fragmenting it into 64-byte USB packets. Large messages are automatically
461/// split across multiple packets for reliable transmission.
462///
463/// The task handles USB disconnection gracefully and will resume transmission
464/// when the connection is restored.
465#[embassy_executor::task]
466async fn usb_tx_task(mut sender: UsbSender<'static, Driver<'static, USB>>) {
467    loop {
468        sender.wait_connection().await;
469        loop {
470            // Wait for a log message from the channel.
471            let message = LOG_CHANNEL.receive().await;
472            let mut problem = false;
473            for i in 0..message.packet_count() {
474                let packet = message.as_packet_bytes(i);
475
476                // Send the log message over USB. If sending fails, break to wait for reconnection.
477                if sender.write_packet(packet).await.is_err() {
478                    problem = true;
479                }
480            }
481            if problem {
482                break;
483            }
484        }
485    }
486}
487
488/// USB device task that runs the USB device state machine.
489///
490/// This task handles the low-level USB device protocol and must be spawned
491/// for USB communication to function. It manages device enumeration,
492/// configuration, and the USB control endpoint.
493#[embassy_executor::task]
494async fn usb_device_task(mut usb: UsbDevice<'static, Driver<'static, USB>>) {
495    usb.run().await;
496}
497
498/// Initialize USB CDC logging and command channel, spawning all necessary tasks.
499///
500/// This function sets up the USB CDC ACM interface for bidirectional communication
501/// and spawns three tasks to handle the USB device, transmission, and reception.
502/// It also initializes the global logger to forward log messages over USB.
503///
504/// # Arguments
505///
506/// * `spawner` - Embassy task spawner for creating the USB tasks
507/// * `level` - Maximum log level to transmit (e.g., `LevelFilter::Info`)
508/// * `usb_peripheral` - RP2040 USB peripheral instance
509/// * `command_sender` - Optional channel sender for receiving line-buffered commands from the host (`None` disables forwarding)
510///
511/// # Panics
512///
513/// Panics if called more than once, as it sets the global logger.
514///
515/// # Examples
516///
517/// ```rust,no_run
518/// use embassy_executor::Spawner;
519/// use embassy_sync::channel::Channel;
520/// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
521/// use log::LevelFilter;
522/// # use rp_usb_console;
523///
524/// static COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, [u8; rp_usb_console::USB_READ_BUFFER_SIZE], 4> = Channel::new();
525///
526/// # async fn example(spawner: Spawner, usb_peripheral: embassy_rp::peripherals::USB) {
527/// rp_usb_console::start(
528///     spawner,
529///     LevelFilter::Info,
530///     usb_peripheral,
531///     Some(COMMAND_CHANNEL.sender()),
532/// );
533/// # }
534/// ```
535pub fn start(
536    spawner: Spawner,
537    level: LevelFilter,
538    usb_peripheral: Peri<'static, USB>,
539    command_sender: Option<Sender<'static, CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4>>,
540) {
541    // Initialize the logger (use racy variants on targets without atomic ptr support, e.g. thumbv6m/RP2040)
542    unsafe {
543        log::set_logger_racy(&LOGGER).unwrap();
544        log::set_max_level_racy(level);
545    }
546
547    // Simple wrapper to mark our single-threaded statics as Sync. RP2040 + embassy executor: we ensure single-core access.
548    struct StaticCell<T>(UnsafeCell<T>);
549    unsafe impl<T> Sync for StaticCell<T> {}
550
551    static DEVICE_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
552    static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
553    static BOS_DESC: StaticCell<[u8; 256]> = StaticCell(UnsafeCell::new([0; 256]));
554    static CONTROL_BUF: StaticCell<[u8; 128]> = StaticCell(UnsafeCell::new([0; 128]));
555    static STATE: StaticCell<State> = StaticCell(UnsafeCell::new(State::new()));
556
557    let driver = Driver::new(usb_peripheral, Irqs);
558
559    let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
560    config.manufacturer = Some("Embassy");
561    config.product = Some("Modular USB-Serial");
562    config.serial_number = Some("12345678");
563    config.max_power = 100;
564
565    let mut builder = Builder::new(
566        driver,
567        config,
568        unsafe { &mut *DEVICE_DESC.0.get() },
569        unsafe { &mut *CONFIG_DESC.0.get() },
570        unsafe { &mut *BOS_DESC.0.get() },
571        unsafe { &mut *CONTROL_BUF.0.get() },
572    );
573
574    let class = CdcAcmClass::new(&mut builder, unsafe { &mut *STATE.0.get() }, 64);
575    let (sender, receiver) = class.split();
576    let usb = builder.build();
577
578    // Spawn all the necessary tasks.
579    spawner.spawn(usb_device_task(usb)).unwrap();
580    spawner.spawn(usb_tx_task(sender)).unwrap();
581    spawner.spawn(usb_rx_task(receiver, command_sender)).unwrap();
582}