This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Rust embedded project for the Raspberry Pi Pico (RP2040) that implements a PDU (Power Distribution Unit) controller with the following features:
- Bootloader (
bootloader/): Usesembassy-boot-rpto manage firmware updates with flash partitioning - Application (
application/): HTTP server with web-based GPIO control and OTA firmware updates
The bootloader enables safe over-the-air firmware updates by maintaining separate ACTIVE and DFU (Device Firmware Update) partitions. The application provides a complete web interface for GPIO control and supports firmware uploads via HTTP.
# Build everything (bootloader + application)
cargo xtask build
# Build individual components
cargo xtask build --bootloader # Build bootloader only
cargo xtask build --application # Build main PDU controller application
# Produce combined UF2 (build + combine)
cargo xtask dist
# Flash to device
cargo xtask flash # Flash combined UF2 via BOOTSEL drag-and-drop
cargo xtask flash --bootloader # Flash bootloader only
cargo xtask flash --application # Flash application only
cargo xtask flash --probe # Flash via probe-rs with RTT logging (recommended for dev)
cargo xtask flash --ota 192.168.1.100 # OTA upload to running device
# Utility commands
cargo xtask clean # Clean all build artifacts
cargo xtask check-tools # Verify all required tools are installedAll build outputs are placed in the build/ directory:
combined.uf2- Recommended - Single file with bootloader + application (~280KB)bootloader.uf2- Bootloader firmware (16KB)application.uf2- Main PDU controller application (168KB)
Option 1: Combined Binary (Recommended for initial provisioning)
- Hold BOOTSEL button while connecting Pico to USB
- Drag
build/combined.uf2to the RPI-RP2 drive - Device will boot automatically with both bootloader and application
Option 2: Separate Binaries
- Hold BOOTSEL button while connecting Pico to USB
- Drag
build/bootloader.uf2to the RPI-RP2 drive - Wait for device to reboot
- Hold BOOTSEL button again
- Drag
build/application.uf2to the RPI-RP2 drive
- Rust nightly toolchain (2026-02-01)
thumbv6m-none-eabitargetelf2uf2-rs(UF2 converter)flip-link(linker for stack overflow protection)- Optional:
probe-rs(for probe-based flashing and RTT logging)
- MISO: GPIO 16
- MOSI: GPIO 19
- CLK: GPIO 18
- CS: GPIO 17
- INT: GPIO 21
- RST: GPIO 20
- SPI Frequency: 50 MHz
- GPIO 0: Relay/Output 0
- GPIO 1: Relay/Output 1
- GPIO 2: Relay/Output 2
- GPIO 3: Relay/Output 3
- GPIO 4: Relay/Output 4
- GPIO 5: Relay/Output 5
- GPIO 6: Relay/Output 6
- GPIO 7: Relay/Output 7
- GPIO 25: Built-in LED (indicates network status)
The application (application/src/main.rs) provides:
- HTTP Server on port 80 (picoserve 0.18) with single-page web interface
- Web Interface: Browser-based GPIO control with toggle buttons, admin panel
- REST API: JSON endpoints for GPIO control, sensors, user management, OTA
- HTTP Basic Auth: Multi-user, admin + regular users with per-port ACL
- ekv Storage: Persistent config (passwords, port names, user ACLs) in CONFIG flash region
- Firmware Updates: OTA updates via HTTP with A/B partition management
- Sensors: RP2040 internal temperature ADC, current/voltage stubs
- Factory Reset: GPIO 26 (hold low at boot) or
POST /api/admin/reset - DHCP client for automatic IP configuration
- Watchdog Timer: 8-second timeout with periodic feeding
- Status LED: GPIO 25 blinks on boot, stays on when ready
- Check serial console for DHCP-assigned IP address
- Open browser to
http://<device-ip>/ - Log in with
admin/admin(default; change on first login) - Click relay toggle buttons to control outputs
- Use the Admin panel for user management, port renaming, OTA upload
# Get device status
curl -u admin:admin http://192.168.1.100/api/status
# Get GPIO state
curl -u admin:admin http://192.168.1.100/api/gpio/0
# Toggle GPIO 0
curl -u admin:admin -X POST http://192.168.1.100/api/gpio/0/toggle
# Get sensor readings
curl -u admin:admin http://192.168.1.100/api/sensors
# Upload firmware OTA (triggers reboot)
cargo xtask flash --ota 192.168.1.100
# or manually:
curl -u admin:admin -X POST -H "Content-Type: application/octet-stream" \
--data-binary @build/application.uf2 \
http://192.168.1.100/api/updateThe flash is partitioned as follows (see memory.x files):
- BOOT2:
0x10000000-0x100(256 bytes) - RP2040 second-stage bootloader - Bootloader Flash:
0x10000100- 24KB - Bootloader code - BOOTLOADER_STATE:
0x10006000- 4KB - Bootloader state/metadata - ACTIVE:
0x10007000- 320KB - Currently running application - DFU:
0x10057000- 324KB - Staged firmware update (must be ≥ ACTIVE + 4KB per embassy-boot swap algorithm) - CONFIG:
0x100A8000- 256KB - ekv key-value database
The bootloader (bootloader/src/main.rs) performs these operations on boot:
- Initializes flash with watchdog timer (8 second timeout)
- Reads configuration from linker-defined memory regions
- Checks BOOTLOADER_STATE for pending updates
- If update marked, copies DFU partition to ACTIVE
- Jumps to ACTIVE partition to execute application
Hard faults trigger system reset to retry boot.
The application uses Embassy async runtime with multiple concurrent tasks:
- ethernet_task: Manages W5500 hardware and link layer
- net_task: Runs the embassy-net network stack (TCP/IP, DHCP)
- gpio_task: Handles GPIO control commands via Signal primitive (8 pins)
- sensor_task: Reads RP2040 internal ADC temperature every 5s
- web_task (×4): picoserve HTTP handlers, each serving one TCP connection
Modules: config, storage, auth, gpio, sensors, web/{mod,status,gpio,sensors,admin,update}
The application uses picoserve 0.18 (Embassy-integrated async HTTP framework):
AppWithStateBuilderpattern for router constructionFromRequestPartsextractors for HTTP Basic AuthJson<T>responses via serde + serde_json_core- Static HTML served from embedded
index.html - OTA body read into buffer, written to DFU in ERASE_SIZE chunks
When firmware is uploaded via /api/update:
- HTTP body is streamed to avoid large memory buffers
- Data written to DFU partition in 4KB chunks using
FirmwareUpdater - DFU partition marked as ready via
mark_updated() - System reset triggered via
cortex_m::peripheral::SCB::sys_reset() - Bootloader detects update on next boot and swaps partitions
This is a Cargo workspace with three members:
bootloader: Bootloader binary (uses minimal dependencies)application: Main PDU controller applicationxtask: Host-only build helper (cargo xtask ...)
The archive/ directory contains the original PIC18-based firmware tooling (Makefile, MPLAB project) for reference.
Application dependencies:
embassy-executor: Async task executorembassy-net: Network stack with DHCP supportembassy-net-wiznet: Driver for W5500 Ethernet chipembassy-rp: RP2040 HAL with flash supportembassy-boot-rp: Bootloader and firmware updaterembassy-embedded-hal: Async adapters for blocking peripheralsembassy-sync: Synchronization primitives (Signal, Mutex)embedded-hal-busv0.1: Async SPI device supportstatic_cell: Static memory allocationportable-atomic: Atomic operations with critical-section supportheapless: No-std collections (String, Vec)
Dependencies use patched Embassy framework from git revision 3651d8ef249....
-
Initial Setup: Flash bootloader (only once)
cargo xtask flash --bootloader
-
Flash Application: Via USB or OTA
# Option A: Flash via USB cargo xtask flash --application # Option B: Upload via HTTP (after first flash) cargo xtask flash --ota <device-ip>
-
Monitor: Connect to serial console
screen /dev/ttyACM0 115200 # Watch for DHCP IP assignment -
Control: Open web browser or use REST API
# Web interface open http://<device-ip>/ # Or use curl curl -X POST http://<device-ip>/api/gpio/0/toggle
- Nightly Rust channel:
nightly-2026-02-01 - Target:
thumbv6m-none-eabi(Cortex-M0+ architecture) - Required components:
rust-src,rustfmt,llvm-tools,miri - Release profile: optimized for size (
opt-level = "s"), LTO enabled
The .cargo/config.toml files configure:
- Linker:
flip-link(stack overflow protection) - Runner:
elf2uf2-rs(default) orprobe-rs(commented out) DEFMT_LOG=debugenvironment variable for logging
To use probe-rs instead of UF2 flashing, uncomment the probe-rs runner line in .cargo/config.toml.
The application uses picoserve 0.18 (Embassy-integrated async HTTP framework):
AppWithStateBuilderpattern for router construction withState = ()FromRequestPartsextractors for HTTP Basic AuthJson<T>responses via serde + serde_json_core- Static HTML served from embedded
index.html - OTA body read into buffer, written to DFU in ERASE_SIZE chunks
Key implementation details:
- DB pointer stored in
portable_atomic::AtomicUsize(avoids HRTB complexity) - All auth extractors call
crate::web::db()static accessor #![recursion_limit = "256"]required for picoserve task pool layout
RP2040 has 264KB RAM total. Memory allocation strategy:
- HTTP buffers per web_task: 4KB RX + 4KB TX + 2KB parsing = 10KB × 4 tasks = 40KB
- Firmware write buffer: 4KB (ERASE_SIZE)
- Network stack resources: ~8KB
- GPIO and sensor tasks: minimal
- Total: ~52KB, well within 264KB limits
Future additions could include:
- Security: Firmware signature verification, OTA rolling-back on failed boot
- Monitoring: Real-time GPIO state WebSocket updates
- Protocols: MQTT integration for IoT platforms
- Features: Input monitoring, PWM support, actual current/voltage sensor integration