feat: defmt-over-RTT debug build + probe-rs flow

Add an opt-in `defmt` feature that swaps panic-reset for panic-probe,
links defmt-rtt, and enables hsmc's `trace-defmt` so chart transitions
print over RTT. Default builds are unchanged (still panic-reset, no
defmt overhead).

- Cargo.toml: `defmt` feature pulls defmt + defmt-rtt + panic-probe
  and forwards `trace-defmt` to hsmc; mirror-flag pattern matches
  the existing `embassy` feature.
- build.rs: link `defmt.x` only when the feature is on.
- src/main.rs: cfg-gate the panic handler swap, log boot, USB
  serial, kbd/mouse wake completion vs. deadline, and jiggle dx/dy.
- main.rs: watchdog.pause_on_debug(true) so probe-rs halts don't
  trip the 8 s reset.
- justfile: build-debug, debug, attach, probes recipes
  (release mode — debug builds overflow flash with defmt+panic-probe).
- mise.toml: add cargo:probe-rs-tools.
This commit is contained in:
2026-05-04 12:57:31 -06:00
parent d0d9c451aa
commit 8cb071b132
6 changed files with 197 additions and 7 deletions
+43 -3
View File
@@ -24,10 +24,13 @@ use embassy_usb::{
};
use hsmc::{Duration, statechart};
use libm::{cosf, powf, roundf, sinf};
#[cfg(not(feature = "defmt"))]
use panic_reset as _;
use smart_leds::RGB8;
use static_cell::StaticCell;
use usbd_hid::descriptor::{KeyboardReport, MouseReport, SerializedDescriptor};
#[cfg(feature = "defmt")]
use {defmt_rtt as _, panic_probe as _};
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => UsbInterruptHandler<USB>;
@@ -291,11 +294,23 @@ impl JigglyActions for JigglyActionContext<'_> {
}
async fn keyboard_wake(&mut self) {
let _ = embassy_futures::select::select(
let result = embassy_futures::select::select(
wake_with_keyboard(&mut self.kbd),
Timer::after(KBD_WAKE_DEADLINE),
)
.await;
#[cfg(feature = "defmt")]
match result {
embassy_futures::select::Either::First(_) => defmt::info!("kbd wake: completed"),
embassy_futures::select::Either::Second(_) => {
defmt::warn!(
"kbd wake: deadline hit ({} ms)",
KBD_WAKE_DEADLINE.as_millis()
)
}
}
#[cfg(not(feature = "defmt"))]
let _ = result;
// Belt-and-suspenders: always send an all-keys-released report,
// even if the deadline preempted the loop *between* a key-down
// and its key-up. Without this, a stuck modifier (Shift!) or key
@@ -310,11 +325,21 @@ impl JigglyActions for JigglyActionContext<'_> {
}
async fn mouse_wake(&mut self) {
let _ = embassy_futures::select::select(
let result = embassy_futures::select::select(
wake_with_mouse(&mut self.mouse),
Timer::after(MOUSE_WAKE_DEADLINE),
)
.await;
#[cfg(feature = "defmt")]
match result {
embassy_futures::select::Either::First(_) => defmt::info!("mouse wake: completed"),
embassy_futures::select::Either::Second(_) => defmt::warn!(
"mouse wake: deadline hit ({} ms)",
MOUSE_WAKE_DEADLINE.as_millis()
),
}
#[cfg(not(feature = "defmt"))]
let _ = result;
}
async fn jiggle_pair(&mut self) {
@@ -323,6 +348,8 @@ impl JigglyActions for JigglyActionContext<'_> {
} else {
(0, 1)
};
#[cfg(feature = "defmt")]
defmt::info!("jiggle: dx={} dy={}", dx, dy);
send_mouse(&mut self.mouse, dx, dy).await;
Timer::after(PIXEL_DWELL).await;
send_mouse(&mut self.mouse, -dx, -dy).await;
@@ -649,7 +676,14 @@ async fn watchdog_task(mut wd: Watchdog) -> ! {
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
#[cfg(feature = "defmt")]
defmt::info!("jiggly v{} boot", env!("CARGO_PKG_VERSION"));
let mut watchdog = Watchdog::new(p.WATCHDOG);
// Pause the countdown while a debugger has the core halted, so
// breakpoints and single-stepping under `probe-rs` don't trip the
// 8 s reset. Cheap and always-correct, so leave it on for release too.
watchdog.pause_on_debug(true);
watchdog.start(WATCHDOG_TIMEOUT);
// NeoPixel: GPIO11 powers it, GPIO12 is the WS2812 data line driven from
@@ -679,7 +713,10 @@ async fn main(spawner: Spawner) {
let mut config = UsbConfig::new(0x1209, 0xb0b0);
config.manufacturer = Some("swaits.com");
config.product = Some("jiggly");
config.serial_number = Some(make_serial(p.FLASH));
let serial = make_serial(p.FLASH);
#[cfg(feature = "defmt")]
defmt::info!("usb serial: {}", serial);
config.serial_number = Some(serial);
config.device_release = 0x0200; // matches firmware version 0.2.0
config.max_power = 100;
config.max_packet_size_0 = 64;
@@ -731,6 +768,9 @@ async fn main(spawner: Spawner) {
spawner.spawn(usb_task(usb).unwrap());
spawner.spawn(watchdog_task(watchdog).unwrap());
#[cfg(feature = "defmt")]
defmt::info!("usb + watchdog tasks spawned, starting statechart");
static EVENT_CHAN: Channel<CriticalSectionRawMutex, Ev, 8> = Channel::new();
let ctx = Ctx {