Refactor code and moved from install.sh to just

This commit is contained in:
Christoffer Martinsson 2025-09-19 13:40:55 +02:00
parent dd1f9cd918
commit 6dbec2425e
14 changed files with 644 additions and 2258 deletions

30
Justfile Normal file
View File

@ -0,0 +1,30 @@
set export := true
default: check
check:
cd rp2040 && cargo check --target thumbv6m-none-eabi
test:
cd rp2040 && cargo test --lib --target x86_64-unknown-linux-gnu --features std
build-uf2:
cd rp2040 && cargo build --release --target thumbv6m-none-eabi
cd rp2040 && cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-keyboard-42.bin
cd rp2040 && python3 uf2conv.py target/thumbv6m-none-eabi/release/cmdr-keyboard-42.bin --base 0x10000000 --family 0xe48bff56 --convert --output target/firmware.uf2
clean:
cargo clean --manifest-path rp2040/Cargo.toml
flash mount="" timeout="10":
@just build-uf2
MOUNT="{{mount}}" python3 tools/copy_uf2.py --source rp2040/target/firmware.uf2 --timeout {{timeout}}
flash-ssh target mount="/Volumes/RPI-RP2" key="" port="22":
@just build-uf2
target="{{target}}"
mount="{{mount}}"
key_arg=""
if [ -n "{{key}}" ]; then key_arg="-i {{key}}"; fi
ssh $key_arg -p {{port}} "$target" "mkdir -p \"$mount\""
scp $key_arg -P {{port}} rp2040/target/firmware.uf2 "$target:$mount/"

View File

@ -36,18 +36,20 @@ _This HW(PCB) was original made for use with the TeensyLC module and is reused b
- Function layer system with two Fn keys (40/43/44) enabling sticky lock, OS lock and deep modifier combos without ghosting.
- High-speed matrix scanning (250µs cadence) with scan-based debounce, per-key press spacing protection and idle/suspend rate throttling.
- Bootloader entry options including `Fn+Fn+LCtrl+LShift+RShift` runtime chord and power-on detection on button index 0.
- Status LED heartbeat derived from joystick firmware: startup heartbeat, activity/idle transitions, sticky-state colors, Caps Lock flash, suspend blanking and error signalling.
- Status LED heartbeat carried over from the earlier CMDR firmware: startup heartbeat, activity/idle transitions, sticky-state colors, Caps Lock flash, suspend blanking and error signalling.
- Power-aware USB handling that drops scan frequency 20× during suspend while honouring wake-on-input and immediate resume.
## Install script (`install.sh`)
## Development workflow
The repository includes a host automation helper for the RP2040 firmware.
Recurring tasks are defined in the root `Justfile`:
- Quick start: `./install.sh check` (compilation + clippy) or `./install.sh test` (full suite including host-side tests).
- Flashing: `./install.sh flash --local` for direct UF2 copy on Linux/macOS, or `./install.sh flash --ssh --target user@host --mount /path` for remote deployments.
- Cleaning: `./install.sh clean` removes cargo artefacts, generated binaries and UF2 images.
- Prerequisites are validated automatically (Rust toolchain, `thumbv6m-none-eabi`, `cargo-binutils`, `uf2conv.py`, optional SSH settings).
- Script output uses colour-coded sections for readability and fails fast on missing dependencies or connectivity issues.
- Build-check the embedded target: `just check`
- Run host-side tests: `just test`
- Remove build artifacts: `just clean`
- Build and copy a UF2 to a mounted RP2040: `just flash` (auto-detects `/Volumes/*`, `/media/*`, `/run/media/*`; override with `mount=/path` and optional `timeout=30` seconds)
- Build, then transfer a UF2 over SSH: `just flash-ssh target=user@host mount=/media/user/RPI-RP2`
The flash recipes rely on `python3`, the bundled `rp2040/uf2conv.py`, `tools/copy_uf2.py`, and `cargo objcopy` (install with `rustup component add llvm-tools-preview` and `cargo install cargo-binutils`). For SSH transfers you also need passwordless access or supply a key path, e.g. `just flash-ssh target=user@host mount=/media/user/RPI-RP2 key=~/.ssh/id_ed25519`.
## Build environment rp2040 Zero

View File

@ -1,567 +0,0 @@
#!/bin/bash
# CMtec CMDR Joystick 25 Install Script
# Supports building, testing, and deploying RP2040 firmware
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Show usage information
show_usage() {
echo -e "${GREEN}CMtec CMDR Joystick 25 Install Script${NC}"
echo "========================================"
echo
echo "Usage: $0 [flash|test|check|clean] [options]"
echo
echo "Commands:"
echo " flash Build and flash RP2040 firmware"
echo " test Run comprehensive test suite"
echo " check Quick compilation and lint checks"
echo " clean Clean build artifacts and temp files"
echo
echo "flash options:"
echo " --local Build + flash locally (RP2040 mass storage)"
echo " --ssh Build + transfer UF2 via SSH"
echo " --mount PATH Remote RP2040 mount (default /Volumes/RPI-RP2)"
echo
echo "SSH options (when using --ssh):"
echo " --target user@host Combined user and host"
echo " --user USER SSH username"
echo " --host HOST SSH hostname or IP"
echo " --port PORT SSH port (default 22)"
echo " --key PATH SSH private key path"
echo
echo "Examples:"
echo " $0 # Show this help"
echo " $0 test # Run all tests"
echo " $0 check # Quick compilation check"
echo " $0 flash --local # Build and flash firmware locally"
echo " $0 flash --ssh --target user@host --mount /Volumes/RPI-RP2"
echo " $0 clean # Clean build artifacts"
exit "${1:-0}"
}
# Parse command line arguments
COMMAND="${1:-}"
shift 1 || true
# If no command provided, show usage
if [ -z "$COMMAND" ]; then
show_usage 0
fi
case "$COMMAND" in
flash | test | check | clean) ;;
-h | --help | help)
show_usage 0
;;
*)
echo -e "${RED}Error: Unknown command '$COMMAND'${NC}"
echo
show_usage 1
;;
esac
echo -e "${GREEN}CMtec CMDR Keyboard Install Script${NC}"
echo "========================================"
echo -e "${BLUE}Mode: $COMMAND${NC}"
echo
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Ensure cargo-binutils is available for `cargo objcopy`
ensure_cargo_binutils() {
if ! command -v cargo-objcopy >/dev/null 2>&1; then
echo "Installing cargo-binutils and llvm-tools-preview (required for UF2 conversion)..."
rustup component add llvm-tools-preview || true
if ! cargo install cargo-binutils; then
echo -e "${RED}Error: Failed to install cargo-binutils${NC}"
echo "Install manually: rustup component add llvm-tools-preview && cargo install cargo-binutils"
exit 1
fi
fi
}
# Function to check prerequisites
check_prerequisites() {
echo "Checking prerequisites..."
if ! command -v cargo &>/dev/null; then
echo -e "${RED}Error: cargo (Rust toolchain) not found${NC}"
echo "Install Rust: https://rustup.rs/"
exit 1
fi
if ! command -v python3 &>/dev/null; then
echo -e "${RED}Error: python3 not found${NC}"
echo "Install Python 3"
exit 1
fi
# Check for required Rust target
if ! rustup target list --installed | grep -q "thumbv6m-none-eabi"; then
echo "Installing Rust target thumbv6m-none-eabi..."
rustup target add thumbv6m-none-eabi
fi
# Check for x86_64 target for testing
if ! rustup target list --installed | grep -q "x86_64-unknown-linux-gnu"; then
echo "Installing Rust target x86_64-unknown-linux-gnu (for testing)..."
rustup target add x86_64-unknown-linux-gnu
fi
echo -e "${GREEN}${NC} Prerequisites check complete"
echo
}
# Function to run comprehensive test suite
run_tests() {
echo "Running comprehensive test suite..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Test 1: Cargo check for embedded target
echo "Testing embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Embedded target compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Embedded target compilation check passed"
# Test 2: Host target tests (17 comprehensive tests)
echo "Running host target test suite..."
if ! cargo test --lib --target x86_64-unknown-linux-gnu --features std; then
echo -e "${RED}${NC} Host target tests failed"
exit 1
fi
echo -e "${GREEN}${NC} Host target tests passed (17 tests)"
# Test 3: Clippy for code quality (warnings allowed for now)
echo "Running clippy code quality checks..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy code quality check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy code quality check passed"
# Test 4: Release build test
echo "Testing release build..."
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Release build failed"
exit 1
fi
echo -e "${GREEN}${NC} Release build passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}All tests completed successfully!${NC}"
echo
echo "Test summary:"
echo -e "${GREEN}${NC} Embedded target compilation check"
echo -e "${GREEN}${NC} Host target test suite (17 tests: expo + storage modules)"
echo -e "${GREEN}${NC} Clippy code quality checks"
echo -e "${GREEN}${NC} Release build verification"
echo
echo -e "${GREEN}Codebase is ready for deployment.${NC}"
}
# Function to run quick checks
run_check() {
echo "Running quick compilation and lint checks..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Quick cargo check
echo "Checking embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Compilation check passed"
# Quick clippy check (warnings allowed for now)
echo "Running clippy..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy check passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Quick checks completed successfully!${NC}"
}
# Function to clean build artifacts
clean_build() {
echo "Cleaning build artifacts and temporary files..."
echo
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Clean cargo artifacts
echo "Cleaning cargo build artifacts..."
cargo clean
echo -e "${GREEN}${NC} Cargo artifacts cleaned"
# Remove UF2 and binary files
if [ -f "firmware.uf2" ]; then
rm -f firmware.uf2
echo -e "${GREEN}${NC} Removed firmware.uf2"
fi
if [ -f "target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin" ]; then
rm -f target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin
echo -e "${GREEN}${NC} Removed binary files"
fi
# Clean any temp files
rm -f /tmp/cmdr_*.tmp 2>/dev/null || true
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Cleanup completed!${NC}"
}
# Function to build firmware locally
build_firmware() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
check_prerequisites
# Build firmware
echo "Building RP2040 firmware..."
cd "$RP2040_DIR"
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}Error: Failed to build firmware${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Firmware build complete"
echo
# Create UF2 file
echo "Converting to UF2 format..."
ensure_cargo_binutils
if ! cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to create binary file${NC}"
exit 1
fi
# Check for uf2conv.py script
if [ ! -f "uf2conv.py" ]; then
echo "Downloading uf2conv.py..."
if ! curl -L -o uf2conv.py "https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2conv.py"; then
echo -e "${RED}Error: Failed to download uf2conv.py${NC}"
exit 1
fi
chmod +x uf2conv.py
fi
if ! python3 uf2conv.py -b 0x10000000 -f 0xe48bff56 -c -o firmware.uf2 target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to convert to UF2 format${NC}"
exit 1
fi
echo -e "${GREEN}${NC} UF2 conversion complete"
echo
}
# Function to build and transfer firmware via SSH
flash_firmware_ssh() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
# Parse CLI args for SSH
SSH_HOST=""
SSH_USER=""
SSH_PORT="22"
SSH_KEY=""
REMOTE_MOUNT="/Volumes/RPI-RP2"
while [ $# -gt 0 ]; do
case "$1" in
--target)
shift
TARGET="$1"
SSH_USER="${TARGET%@*}"
SSH_HOST="${TARGET#*@}"
;;
--host)
shift
SSH_HOST="$1"
;;
--user)
shift
SSH_USER="$1"
;;
--port)
shift
SSH_PORT="$1"
;;
--key)
shift
SSH_KEY="$1"
;;
--mount)
shift
REMOTE_MOUNT="$1"
;;
*) echo -e "${YELLOW}Warning: Unknown SSH option: $1${NC}" ;;
esac
shift || true
done
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ]; then
echo -e "${RED}Error: provide SSH credentials via --target user@host or --user/--host${NC}"
exit 1
fi
# Build firmware locally first
build_firmware
echo "Testing SSH connection to $SSH_USER@$SSH_HOST:$SSH_PORT..."
mkdir -p "${HOME}/.ssh"
SSH_CONTROL_PATH="${HOME}/.ssh/cmdr_%C"
local SSH_OPTS=(
-p "$SSH_PORT"
-o "StrictHostKeyChecking=accept-new"
-o "ControlMaster=auto"
-o "ControlPersist=60s"
-o "ControlPath=$SSH_CONTROL_PATH"
-o "PreferredAuthentications=publickey,password,keyboard-interactive"
)
if [ -n "$SSH_KEY" ]; then
SSH_OPTS+=(-i "$SSH_KEY")
fi
# Test SSH connection
if ! ssh "${SSH_OPTS[@]}" -o ConnectTimeout=10 -fN "$SSH_USER@$SSH_HOST" 2>/dev/null; then
echo -e "${RED}Error: Cannot connect to $SSH_USER@$SSH_HOST:$SSH_PORT${NC}"
echo "Tips:"
echo "- Verify network reachability and SSH port"
echo "- Ensure SSH keys are properly configured"
exit 1
fi
echo -e "${GREEN}${NC} SSH connection successful"
# Check if remote mount exists
echo "Checking remote RP2040 mount path: $REMOTE_MOUNT"
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$SSH_HOST" "test -d \"$REMOTE_MOUNT\""; then
echo -e "${RED}Remote mount not found: $REMOTE_MOUNT${NC}"
echo -e "${YELLOW}Ensure the RP2040 is in BOOTSEL mode and mounted on the remote host.${NC}"
echo "Common paths: /Volumes/RPI-RP2 (macOS), /media/\$USER/RPI-RP2 (Linux)"
exit 1
fi
echo "Transferring firmware.uf2 to remote host..."
local SCP_OPTS=(-P "$SSH_PORT" -o "StrictHostKeyChecking=accept-new" -o "ControlPath=$SSH_CONTROL_PATH")
if [ -n "$SSH_KEY" ]; then
SCP_OPTS+=(-i "$SSH_KEY")
fi
cd "$RP2040_DIR"
if ! scp "${SCP_OPTS[@]}" firmware.uf2 "$SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"; then
echo -e "${RED}${NC} Failed to transfer firmware to remote RP2040"
exit 1
fi
echo -e "${GREEN}${NC} Transferred firmware.uf2 to $SSH_USER@$SSH_HOST:$REMOTE_MOUNT"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}SSH firmware deployment completed!${NC}"
echo "Remote: $SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"
echo -e "${YELLOW}The remote RP2040 should now restart with new firmware.${NC}"
}
# Function to flash firmware locally
flash_firmware_local() {
build_firmware
RP2040_DIR="$SCRIPT_DIR/rp2040"
cd "$RP2040_DIR"
# Detect OS and set up mount detection
OS="$(uname -s)"
# Wait for and flash to RP2040
echo "Waiting for RP2040 in bootloader mode..."
echo -e "${YELLOW}Put your RP2040 into bootloader mode (hold BOOTSEL button while plugging in USB)${NC}"
case "$OS" in
Linux*)
# Wait for RPI-RP2 mount point with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/media/RPI-RP2" ] && [ ! -d "/media/$USER/RPI-RP2" ] && [ ! -d "/run/media/$USER/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount (RPI-RP2)${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
echo "Expected mount at one of: /media/RPI-RP2, /media/$USER/RPI-RP2, /run/media/$USER/RPI-RP2"
exit 1
fi
done
# Find the actual mount point
if [ -d "/media/RPI-RP2" ]; then
MOUNT_POINT="/media/RPI-RP2"
elif [ -d "/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/media/$USER/RPI-RP2"
elif [ -d "/run/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/run/media/$USER/RPI-RP2"
else
echo -e "${RED}Error: Could not find RPI-RP2 mount point${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Found RP2040 at $MOUNT_POINT"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "$MOUNT_POINT/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
Darwin*)
# Wait for RPI-RP2 volume with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/Volumes/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount at /Volumes/RPI-RP2${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
exit 1
fi
done
echo -e "${GREEN}${NC} Found RP2040 at /Volumes/RPI-RP2"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "/Volumes/RPI-RP2/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
*)
echo -e "${RED}Error: Unsupported operating system for local flashing${NC}"
echo "This script supports Linux and macOS for local flashing."
exit 1
;;
esac
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Firmware installation completed!${NC}"
echo
echo "Firmware file: $RP2040_DIR/firmware.uf2"
echo -e "${GREEN}RP2040 firmware deployed successfully!${NC}"
echo -e "${YELLOW}The device should now restart with new firmware.${NC}"
}
# Detect OS
case "$(uname -s)" in
Linux*)
OS="Linux"
;;
Darwin*)
OS="macOS"
;;
*)
echo -e "${RED}Error: Unsupported operating system${NC}"
echo "This script supports Linux and macOS only."
exit 1
;;
esac
echo "Detected OS: $OS"
if [ "$COMMAND" = "flash" ]; then
echo "RP2040 firmware directory: $SCRIPT_DIR/rp2040"
elif [ "$COMMAND" = "test" ]; then
echo "Testing Rust embedded firmware with comprehensive test suite"
fi
echo
# Execute command
case "$COMMAND" in
flash)
MODE=""
REM_ARGS=()
for arg in "$@"; do
case "$arg" in
--local) MODE="local" ;;
--ssh) MODE="ssh" ;;
*) REM_ARGS+=("$arg") ;;
esac
done
if [ "$MODE" = "local" ]; then
flash_firmware_local
elif [ "$MODE" = "ssh" ]; then
flash_firmware_ssh "${REM_ARGS[@]}"
else
echo -e "${RED}Error: specify --local or --ssh for flash${NC}"
show_usage 1
fi
;;
test)
run_tests
;;
check)
run_check
;;
clean)
clean_build
;;
esac

View File

@ -24,7 +24,6 @@ target = "thumbv6m-none-eabi"
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "no-vectorize-loops",
]

998
rp2040/Cargo.lock generated
View File

@ -2,75 +2,21 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ascii-canvas"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
dependencies = [
"term",
]
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bare-metal"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
dependencies = [
"rustc_version 0.2.3",
"rustc_version",
]
[[package]]
name = "bare-metal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitfield"
version = "0.13.2"
@ -83,18 +29,6 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bitvec"
version = "1.0.1"
@ -107,15 +41,6 @@ dependencies = [
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytemuck"
version = "1.22.0"
@ -128,40 +53,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cmdr-keyboard-42"
version = "0.2.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"cortex-m-rtic",
"critical-section",
"defmt",
"defmt-rtt",
"dht-sensor",
"embedded-alloc",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
"embedded-hal-async",
"embedded-hal-bus",
"embedded-io",
"fugit",
"futures",
"hd44780-driver",
"heapless 0.8.0",
"nb 1.1.0",
"packed_struct",
"panic-halt",
"panic-probe",
"pio 0.3.0",
"portable-atomic",
"rp-binary-info 0.1.2",
"rp2040-boot2",
"rp2040-hal",
"smart-leds",
@ -171,23 +73,13 @@ dependencies = [
"ws2812-pio",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "cortex-m"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [
"bare-metal 0.2.5",
"bare-metal",
"bitfield 0.13.2",
"embedded-hal 0.2.7",
"volatile-register",
@ -213,43 +105,6 @@ dependencies = [
"syn 2.0.99",
]
[[package]]
name = "cortex-m-rtic"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d696ae7390bdb9f7978f71ca7144256a2c4616240a6df9002da3c451f9fc8f02"
dependencies = [
"bare-metal 1.0.0",
"cortex-m",
"cortex-m-rtic-macros",
"heapless 0.7.17",
"rtic-core",
"rtic-monotonic",
"version_check",
]
[[package]]
name = "cortex-m-rtic-macros"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eefb40b1ca901c759d29526e5c8a0a1b246c20caaa5b4cc5d0f0b94debecd4c7"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"rtic-syntax",
"syn 1.0.109",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc-any"
version = "2.5.0"
@ -265,99 +120,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "debug-helper"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "defmt"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6"
dependencies = [
"defmt-parser",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "defmt-parser"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3"
dependencies = [
"thiserror",
]
[[package]]
name = "defmt-rtt"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab697b3dbbc1750b7c8b821aa6f6e7f2480b47a99bc057a2ed7b170ebef0c51"
dependencies = [
"critical-section",
"defmt",
]
[[package]]
name = "dht-sensor"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d643854f324f02daea7153ebbfec5cfcc2f395faf1c7011be65e543570de28"
dependencies = [
"embedded-hal 0.2.7",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embedded-alloc"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddae17915accbac2cfbc64ea0ae6e3b330e6ea124ba108dada63646fd3c6f815"
dependencies = [
"critical-section",
"linked_list_allocator",
]
[[package]]
name = "embedded-dma"
version = "0.2.0"
@ -382,9 +156,6 @@ name = "embedded-hal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
dependencies = [
"defmt",
]
[[package]]
name = "embedded-hal-async"
@ -392,23 +163,9 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
dependencies = [
"defmt",
"embedded-hal 1.0.0",
]
[[package]]
name = "embedded-hal-bus"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d3980bf28e8577db59fe2bdb3df868a419469d2cecb363644eea2b6f7797669"
dependencies = [
"critical-section",
"defmt",
"embedded-hal 1.0.0",
"embedded-hal-async",
"portable-atomic",
]
[[package]]
name = "embedded-hal-nb"
version = "1.0.0"
@ -425,27 +182,6 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "ena"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5"
dependencies = [
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "frunk"
version = "0.4.3"
@ -500,104 +236,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "gcd"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hash32"
version = "0.3.1"
@ -607,70 +251,16 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "hd44780-driver"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aab2b13fdeaed7dde9133a57c28b2cbde4a8fc8c3196b5631428aad114857d3a"
dependencies = [
"embedded-hal 0.2.7",
]
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32 0.2.1",
"rustc_version 0.4.1",
"spin",
"stable_deref_trait",
]
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32 0.3.1",
"hash32",
"stable_deref_trait",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -680,90 +270,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
]
[[package]]
name = "lalrpop"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501"
dependencies = [
"ascii-canvas",
"bit-set",
"ena",
"itertools 0.14.0",
"lalrpop-util",
"petgraph",
"pico-args",
"regex",
"regex-syntax",
"sha3",
"string_cache",
"term",
"unicode-xid",
"walkdir",
]
[[package]]
name = "lalrpop-util"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733"
dependencies = [
"regex-automata",
"rustversion",
]
[[package]]
name = "libc"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "linked_list_allocator"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nb"
version = "0.1.3"
@ -779,12 +285,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "num_enum"
version = "0.5.11"
@ -858,82 +358,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
[[package]]
name = "panic-probe"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0"
dependencies = [
"cortex-m",
"defmt",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap 2.7.1",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pio"
version = "0.2.1"
@ -945,114 +375,11 @@ dependencies = [
"paste",
]
[[package]]
name = "pio"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ba4153cee9585abc451271aa437d9e8defdea8b468d48ba6b8f098cbe03d7f"
dependencies = [
"pio-core",
"pio-proc",
]
[[package]]
name = "pio-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d90fddc3d67f21bbf93683bc461b05d6a29c708caf3ffb79947d7ff7095406"
dependencies = [
"arrayvec",
"num_enum 0.7.3",
"paste",
]
[[package]]
name = "pio-parser"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825266c1eaddf54f636d06eefa4bf3c99d774c14ec46a4a6c6e5128a0f10d205"
dependencies = [
"lalrpop",
"lalrpop-util",
"pio-core",
]
[[package]]
name = "pio-proc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed4a76571f5fe51af43cc80ac870fe0c79cc0cdd686b9002a6c4c84bfdd0176b"
dependencies = [
"codespan-reporting",
"lalrpop-util",
"pio-core",
"pio-parser",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
dependencies = [
"critical-section",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "proc-macro2"
@ -1084,44 +411,6 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "redox_syscall"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rgb"
version = "0.8.50"
@ -1137,11 +426,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ed2051a0bf2c726df01cfce378ed8a367be2a6e402fc183857f429a346d429"
[[package]]
name = "rp-binary-info"
version = "0.1.2"
source = "git+https://github.com/rp-rs/rp-hal/#01cdf556696a3286435dcff0d01b76ff4334eb06"
[[package]]
name = "rp-hal-common"
version = "0.1.0"
@ -1169,7 +453,6 @@ dependencies = [
"bitfield 0.14.0",
"cortex-m",
"critical-section",
"defmt",
"embedded-dma",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
@ -1178,12 +461,12 @@ dependencies = [
"embedded-io",
"frunk",
"fugit",
"itertools 0.10.5",
"itertools",
"nb 1.1.0",
"paste",
"pio 0.2.1",
"pio",
"rand_core",
"rp-binary-info 0.1.1",
"rp-binary-info",
"rp-hal-common",
"rp2040-hal-macros",
"rp2040-pac",
@ -1216,69 +499,15 @@ dependencies = [
"vcell",
]
[[package]]
name = "rtic-core"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9369355b04d06a3780ec0f51ea2d225624db777acbc60abd8ca4832da5c1a42"
[[package]]
name = "rtic-monotonic"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb8b0b822d1a366470b9cea83a1d4e788392db763539dc4ba022bcc787fece82"
[[package]]
name = "rtic-syntax"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5e215601dc467752c2bddc6284a622c6f3d2bab569d992adcd5ab7e4cb9478"
dependencies = [
"indexmap 1.9.3",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
"semver",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver 1.0.26",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "0.9.0"
@ -1288,40 +517,12 @@ dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "smart-leds"
version = "0.4.0"
@ -1349,15 +550,6 @@ dependencies = [
"rgb",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@ -1366,25 +558,13 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_cell"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89b0684884a883431282db1e4343f34afc2ff6996fe1f4a1664519b66e14c1e"
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
dependencies = [
"portable-atomic",
]
[[package]]
name = "string_cache"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "syn"
version = "1.0.109"
@ -1413,75 +593,19 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "term"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a43bddab41f8626c7bdaab872bbba75f8df5847b516d77c569c746e2ae5eb746"
dependencies = [
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "usb-device"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6"
dependencies = [
"heapless 0.8.0",
"heapless",
"portable-atomic",
]
@ -1493,7 +617,7 @@ checksum = "2d19fd35b28737afde3b854c0dc354df41fceada782f3add0a8115794eda2624"
dependencies = [
"frunk",
"fugit",
"heapless 0.8.0",
"heapless",
"num_enum 0.7.3",
"option-block",
"packed_struct",
@ -1506,12 +630,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "void"
version = "1.0.2"
@ -1527,98 +645,6 @@ dependencies = [
"vcell",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "ws2812-pio"
version = "0.9.0"
@ -1629,7 +655,7 @@ dependencies = [
"embedded-hal 0.2.7",
"fugit",
"nb 1.1.0",
"pio 0.2.1",
"pio",
"rp2040-hal",
"smart-leds-trait 0.2.1",
"smart-leds-trait 0.3.1",

View File

@ -4,38 +4,18 @@ version = "0.2.0"
edition = "2024"
[dependencies]
# rp2040_hal dependencies copied from v0.11
cortex-m = "0.7.2"
cortex-m-rt = "0.7"
cortex-m-rtic = "1.1.4"
critical-section = {version = "1.2.0"}
defmt = "0.3"
defmt-rtt = "0.4.0"
dht-sensor = "0.2.1"
embedded-alloc = "0.5.1"
embedded-hal = "1.0.0"
embedded-hal-async = "1.0.0"
embedded-hal-bus = {version = "0.2.0", features = ["defmt-03"]}
embedded-io = "0.6.1"
embedded_hal_0_2 = {package = "embedded-hal", version = "0.2.5", features = ["unproven"]}
fugit = "0.3.6"
futures = {version = "0.3.30", default-features = false, features = ["async-await"]}
hd44780-driver = "0.4.0"
nb = "1.0"
panic-halt = "0.2.0"
panic-probe = {version = "0.3.1", features = ["print-defmt"]}
pio = "0.3.0"
portable-atomic = {version = "1.7.0", features = ["critical-section"]}
rp2040-boot2 = "0.3.0"
rp2040-hal = {version = "0.11.0", features = ["binary-info", "critical-section-impl", "rt", "defmt"]}
rp-binary-info = {git = "https://github.com/rp-rs/rp-hal/", version ="0.1.2"}
static_cell = "2.1.0"
rp2040-hal = {version = "0.11.0", features = ["binary-info", "critical-section-impl", "rt"]}
# USB hid dependencies
usbd-human-interface-device = {version = "0.5.1"}
usb-device = "0.3"
packed_struct = { version = "0.10", default-features = false }
heapless = "0.8"
# ws2812-pio dependencies
ws2812-pio = "0.9.0"
@ -65,3 +45,7 @@ path = "src/main.rs"
[features]
default = []
std = []
[target.'cfg(target_arch = "arm")'.dependencies]
static_cell = "2.1.0"
portable-atomic = { version = "1.11.0", default-features = false, features = ["unsafe-assume-single-core"] }

137
rp2040/src/board.rs Normal file
View File

@ -0,0 +1,137 @@
use crate::{hardware, ButtonMatrix, MatrixPins, StatusLed};
use cortex_m::delay::Delay;
use cortex_m::interrupt;
use rp2040_hal::{
Clock,
clocks::init_clocks_and_plls,
gpio::Pins,
pac,
pio::PIOExt,
sio::Sio,
timer::Timer,
watchdog::Watchdog,
};
use usb_device::class_prelude::UsbBusAllocator;
pub type KeyboardMatrix = ButtonMatrix<
MatrixPins<{ hardware::KEY_ROWS }, { hardware::KEY_COLS }>,
{ hardware::KEY_ROWS },
{ hardware::KEY_COLS },
{ hardware::NUMBER_OF_KEYS },
>;
pub type KeyboardStatusLed = StatusLed<pac::PIO0, rp2040_hal::pio::SM0, hardware::StatusLedPin>;
pub struct Board {
pub button_matrix: KeyboardMatrix,
pub status_led: KeyboardStatusLed,
pub delay: Delay,
pub timer: Timer,
usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}
pub struct BoardParts {
pub button_matrix: KeyboardMatrix,
pub status_led: KeyboardStatusLed,
pub delay: Delay,
pub timer: Timer,
pub usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}
impl Board {
pub fn new() -> Self {
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = init_clocks_and_plls(
hardware::XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let sio = Sio::new(pac.SIO);
let pins = Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let (rows, cols, status_pin) = hardware::split_board_pins(pins);
let matrix_pins = MatrixPins::new(rows, cols);
let mut button_matrix = ButtonMatrix::new(
matrix_pins,
hardware::MATRIX_DEBOUNCE_SCANS_PRESS,
hardware::MATRIX_DEBOUNCE_SCANS_RELEASE,
hardware::MIN_PRESS_SPACING_SCANS,
);
button_matrix.init_pins();
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
let status_led_pin = status_pin;
let status_led = StatusLed::new(
status_led_pin,
&mut pio,
sm0,
clocks.peripheral_clock.freq(),
);
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
let usb_bus = usb_allocator(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
&mut pac.RESETS,
);
Self {
button_matrix,
status_led,
delay,
timer,
usb_bus,
}
}
pub fn into_parts(self) -> BoardParts {
BoardParts {
button_matrix: self.button_matrix,
status_led: self.status_led,
delay: self.delay,
timer: self.timer,
usb_bus: self.usb_bus,
}
}
}
fn usb_allocator(
usbctrl_regs: pac::USBCTRL_REGS,
usbctrl_dpram: pac::USBCTRL_DPRAM,
usb_clock: rp2040_hal::clocks::UsbClock,
resets: &mut pac::RESETS,
) -> &'static UsbBusAllocator<rp2040_hal::usb::UsbBus> {
static USB_BUS: static_cell::StaticCell<UsbBusAllocator<rp2040_hal::usb::UsbBus>> =
static_cell::StaticCell::new();
interrupt::free(|_| {
USB_BUS.init_with(|| {
UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
usbctrl_regs,
usbctrl_dpram,
usb_clock,
true,
resets,
))
})
})
}

View File

@ -1,6 +1,6 @@
//! Bootloader entry helpers shared between power-on checks and runtime combos.
use crate::{layout, status::StatusMode, StatusLed, NUMBER_OF_KEYS};
use crate::{NUMBER_OF_KEYS, StatusLed, layout, status::StatusMode};
use cortex_m::asm;
use rp2040_hal::{
gpio::AnyPin,

View File

@ -1,66 +1,115 @@
//! Button matrix scanner for CMDR Keyboard 42
//! Button matrix scanner for CMDR Keyboard 42.
//!
//! Ported from the joystick firmware for consistent behaviour and structure.
//! Scans a row/column matrix and produces a debounced boolean state for each
//! button using per-button counters and small inter-column delays.
//! The scanner owns a concrete set of matrix pins and produces a debounced
//! boolean state for each key.
use core::convert::Infallible;
use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
/// Row/column scanned button matrix driver with debounce counters.
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
pressed: [bool; N],
press_threshold: u8,
release_threshold: u8,
debounce_counter: [u8; N],
// Additional protection: minimum time between same-key presses
last_press_scan: [u32; N],
scan_counter: u32,
/// Abstraction over the physical row/column pins backing the button matrix.
pub trait MatrixPinAccess<const ROWS: usize, const COLS: usize> {
fn init_columns(&mut self);
fn set_column_low(&mut self, column: usize);
fn set_column_high(&mut self, column: usize);
fn read_row(&mut self, row: usize) -> bool;
}
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
pub fn new(
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
/// Concrete matrix pins built from RP2040 dynamic pins.
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
rows: [RowPin; ROWS],
cols: [ColPin; COLS],
}
impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
Self { rows, cols }
}
}
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
fn init_columns(&mut self) {
for column in self.cols.iter_mut() {
column.set_high().ok();
}
}
fn set_column_low(&mut self, column: usize) {
self.cols[column].set_low().ok();
}
fn set_column_high(&mut self, column: usize) {
self.cols[column].set_high().ok();
}
fn read_row(&mut self, row: usize) -> bool {
self.rows[row].is_low().unwrap_or(false)
}
}
/// Row/column scanned button matrix driver with debounce counters.
pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const KEYS: usize> {
pins: P,
pressed: [bool; KEYS],
press_threshold: u8,
release_threshold: u8,
debounce_counter: [u8; KEYS],
last_press_scan: [u32; KEYS],
scan_counter: u32,
min_press_gap_scans: u32,
}
impl<P, const ROWS: usize, const COLS: usize, const KEYS: usize> ButtonMatrix<P, ROWS, COLS, KEYS>
where
P: MatrixPinAccess<ROWS, COLS>,
{
pub fn new(
pins: P,
press_threshold: u8,
release_threshold: u8,
min_press_gap_scans: u32,
) -> Self {
debug_assert_eq!(KEYS, ROWS * COLS);
Self {
rows,
cols,
pressed: [false; N],
pins,
pressed: [false; KEYS],
press_threshold,
release_threshold,
debounce_counter: [0; N],
last_press_scan: [0; N],
debounce_counter: [0; KEYS],
last_press_scan: [0; KEYS],
scan_counter: 0,
min_press_gap_scans,
}
}
pub fn init_pins(&mut self) {
for col in self.cols.iter_mut() {
col.set_high().unwrap();
self.pins.init_columns();
}
pub fn prime(&mut self, delay: &mut Delay, passes: usize) {
for _ in 0..passes {
self.scan_matrix(delay);
}
}
pub fn scan_matrix(&mut self, delay: &mut Delay) {
self.scan_counter = self.scan_counter.wrapping_add(1);
for col_index in 0..self.cols.len() {
self.cols[col_index].set_low().unwrap();
for column in 0..COLS {
self.pins.set_column_low(column);
delay.delay_us(1);
self.process_column(col_index);
self.cols[col_index].set_high().unwrap();
self.process_column(column);
self.pins.set_column_high(column);
delay.delay_us(1);
}
}
fn process_column(&mut self, col_index: usize) {
for row_index in 0..self.rows.len() {
let button_index: usize = col_index + (row_index * C);
let current_state = self.rows[row_index].is_low().unwrap();
pub(crate) fn process_column(&mut self, column: usize) {
for row in 0..ROWS {
let button_index = column + (row * COLS);
let current_state = self.pins.read_row(row);
if current_state == self.pressed[button_index] {
self.debounce_counter[button_index] = 0;
@ -77,147 +126,124 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
};
if self.debounce_counter[button_index] >= threshold {
// Additional protection for press events: minimum 20 scans (5ms) between presses
if current_state { // Pressing
let scans_since_last = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]);
if scans_since_last >= 20 { // 5ms at 250μs scan rate
self.pressed[button_index] = current_state;
if current_state {
let elapsed = self
.scan_counter
.wrapping_sub(self.last_press_scan[button_index]);
if self.last_press_scan[button_index] == 0
|| elapsed >= self.min_press_gap_scans
{
self.pressed[button_index] = true;
self.last_press_scan[button_index] = self.scan_counter;
}
} else { // Releasing
self.pressed[button_index] = current_state;
} else {
self.pressed[button_index] = false;
}
self.debounce_counter[button_index] = 0;
}
}
}
pub fn buttons_pressed(&mut self) -> [bool; N] {
pub fn buttons_pressed(&self) -> [bool; KEYS] {
self.pressed
}
#[cfg(all(test, feature = "std"))]
pub(crate) fn set_scan_counter(&mut self, value: u32) {
self.scan_counter = value;
}
#[cfg(all(test, feature = "std"))]
pub(crate) fn bump_scan_counter(&mut self) {
self.scan_counter = self.scan_counter.wrapping_add(1);
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use core::cell::Cell;
use embedded_hal::digital::ErrorType;
use std::rc::Rc;
struct MockInputPin {
state: Rc<Cell<bool>>,
#[derive(Clone)]
struct MockMatrixPins {
row: Rc<Cell<bool>>,
column: Rc<Cell<bool>>,
}
impl MockInputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
impl MockMatrixPins {
fn new(row: Rc<Cell<bool>>, column: Rc<Cell<bool>>) -> Self {
Self { row, column }
}
}
impl ErrorType for MockInputPin {
type Error = Infallible;
impl MatrixPinAccess<1, 1> for MockMatrixPins {
fn init_columns(&mut self) {
self.column.set(true);
}
impl InputPin for MockInputPin {
fn is_high(&mut self) -> Result<bool, Self::Error> {
Ok(!self.state.get())
fn set_column_low(&mut self, _column: usize) {
self.column.set(false);
}
fn is_low(&mut self) -> Result<bool, Self::Error> {
Ok(self.state.get())
fn set_column_high(&mut self, _column: usize) {
self.column.set(true);
}
fn read_row(&mut self, _row: usize) -> bool {
self.row.get()
}
}
struct MockOutputPin {
state: Rc<Cell<bool>>,
}
impl MockOutputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
}
}
impl ErrorType for MockOutputPin {
type Error = Infallible;
}
impl OutputPin for MockOutputPin {
fn set_high(&mut self) -> Result<(), Self::Error> {
self.state.set(true);
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
self.state.set(false);
Ok(())
}
}
fn matrix_fixture() -> (
ButtonMatrix<'static, 1, 1, 1>,
fn fixture() -> (
ButtonMatrix<MockMatrixPins, 1, 1, 1>,
Rc<Cell<bool>>,
Rc<Cell<bool>>,
) {
let row_state = Rc::new(Cell::new(false));
let col_state = Rc::new(Cell::new(false));
let row_pin: &'static mut dyn InputPin<Error = Infallible> =
Box::leak(Box::new(MockInputPin::new(row_state.clone())));
let col_pin: &'static mut dyn OutputPin<Error = Infallible> =
Box::leak(Box::new(MockOutputPin::new(col_state.clone())));
let rows: &'static mut [&'static mut dyn InputPin<Error = Infallible>; 1] =
Box::leak(Box::new([row_pin]));
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
Box::leak(Box::new([col_pin]));
let matrix = ButtonMatrix::new(rows, cols, 5, 5);
(matrix, row_state, col_state)
let column_state = Rc::new(Cell::new(false));
let pins = MockMatrixPins::new(row_state.clone(), column_state.clone());
let matrix = ButtonMatrix::new(pins, 5, 5, 200);
(matrix, row_state, column_state)
}
#[test]
fn init_sets_columns_high() {
let (mut matrix, _row_state, col_state) = matrix_fixture();
assert!(!col_state.get());
let (mut matrix, _row, column) = fixture();
assert!(!column.get());
matrix.init_pins();
assert!(col_state.get());
assert!(column.get());
}
#[test]
fn debounce_respects_threshold() {
let (mut matrix, row_state, _col_state) = matrix_fixture();
let (mut matrix, row, _column) = fixture();
let mut states = matrix.buttons_pressed();
assert!(!states[0]);
// Set scan counter to start with enough history
matrix.scan_counter = 100;
row_state.set(true);
// Need 5 scans to register press
matrix.set_scan_counter(100);
row.set(true);
for _ in 0..4 {
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]); // Still not pressed
assert!(!states[0]);
}
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0); // 5th scan
states = matrix.buttons_pressed();
assert!(states[0]); // Now pressed
row_state.set(false);
// Need 5 scans to register release
for _ in 0..4 {
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(states[0]); // Still pressed
}
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0); // 5th scan
assert!(states[0]);
row.set(false);
for _ in 0..4 {
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]); // Now released
assert!(states[0]);
}
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]);
}
}

View File

@ -1,5 +1,8 @@
//! Hardware configuration and timing constants for the CMDR Keyboard 42.
use rp2040_hal::gpio::{self, DynPinId, FunctionPio0, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
use rp2040_hal::gpio::Pins;
/// External crystal frequency (Hz).
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
@ -9,6 +12,9 @@ pub const XTAL_FREQ_HZ: u32 = 12_000_000;
pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 5;
pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 5;
/// Minimum scans between two press events for the same key (50ms at 250μs scan cadence).
pub const MIN_PRESS_SPACING_SCANS: u32 = 200;
/// Initial scan iterations before trusting key state.
pub const INITIAL_SCAN_PASSES: usize = 50;
@ -57,77 +63,41 @@ pub mod pins {
pub const STATUS_LED: u8 = 16;
}
/// Convenience macro mirroring the joystick repository for fetching typed pins.
#[macro_export]
macro_rules! get_pin {
($pins:expr, button_row_0) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_0;
$pins.gpio0
}};
($pins:expr, button_row_1) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_1;
$pins.gpio1
}};
($pins:expr, button_row_2) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_2;
$pins.gpio29
}};
($pins:expr, button_row_3) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_3;
$pins.gpio28
}};
($pins:expr, button_col_0) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_0;
$pins.gpio12
}};
($pins:expr, button_col_1) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_1;
$pins.gpio13
}};
($pins:expr, button_col_2) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_2;
$pins.gpio14
}};
($pins:expr, button_col_3) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_3;
$pins.gpio15
}};
($pins:expr, button_col_4) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_4;
$pins.gpio26
}};
($pins:expr, button_col_5) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_5;
$pins.gpio27
}};
($pins:expr, button_col_6) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_6;
$pins.gpio7
}};
($pins:expr, button_col_7) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_7;
$pins.gpio8
}};
($pins:expr, button_col_8) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_8;
$pins.gpio6
}};
($pins:expr, button_col_9) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_9;
$pins.gpio9
}};
($pins:expr, button_col_10) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_10;
$pins.gpio10
}};
($pins:expr, button_col_11) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_11;
$pins.gpio11
}};
($pins:expr, status_led) => {{
const _: u8 = $crate::hardware::pins::STATUS_LED;
$pins.gpio16
}};
/// Dynamic pin types for matrix scanning and status LED control.
pub type MatrixRowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
pub type MatrixColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
pub type StatusLedPin = Pin<gpio::bank0::Gpio16, FunctionPio0, PullNone>;
/// Consume the machine GPIO pins and split out matrix and status LED pins.
pub fn split_board_pins(pins: Pins) -> ([MatrixRowPin; KEY_ROWS], [MatrixColPin; KEY_COLS], StatusLedPin) {
let rows = [
pins.gpio0.into_pull_up_input().into_dyn_pin(),
pins.gpio1.into_pull_up_input().into_dyn_pin(),
pins.gpio29.into_pull_up_input().into_dyn_pin(),
pins.gpio28.into_pull_up_input().into_dyn_pin(),
];
let cols = [
pins.gpio12.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio13.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio14.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio15.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio26.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio27.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio7.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio8.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio6.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio9.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio10.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
pins.gpio11.into_push_pull_output().into_pull_type::<PullNone>().into_dyn_pin(),
];
let status_led = pins
.gpio16
.into_function::<FunctionPio0>()
.into_pull_type::<PullNone>();
(rows, cols, status_led)
}
#[cfg(all(test, feature = "std"))]

View File

@ -2,17 +2,21 @@
//! CMDR Keyboard 42 firmware library for RP2040.
//!
//! This crate mirrors the structure of the joystick firmware to keep hardware
//! details, status handling, and keyboard processing modular.
pub mod button_matrix;
//! This crate mirrors the structure used by earlier CMDR firmware so the
//! keyboard shares the same modular layout for hardware details, status
//! handling, and key processing.
#[cfg(not(feature = "std"))]
pub mod board;
pub mod bootloader;
pub mod button_matrix;
pub mod hardware;
pub mod keyboard;
pub mod layout;
pub mod status;
pub use button_matrix::ButtonMatrix;
#[cfg(not(feature = "std"))]
pub use board::{Board, BoardParts, KeyboardMatrix, KeyboardStatusLed};
pub use button_matrix::{ButtonMatrix, MatrixPinAccess, MatrixPins};
pub use hardware::{
KEY_COLS, KEY_ROWS, MATRIX_DEBOUNCE_SCANS_PRESS, MATRIX_DEBOUNCE_SCANS_RELEASE, NUMBER_OF_KEYS,
XTAL_FREQ_HZ, pins, timers, usb,

View File

@ -8,25 +8,16 @@
#![no_main]
use cmdr_keyboard_42::hardware::{self, timers};
use cmdr_keyboard_42::{bootloader, ButtonMatrix, KeyboardState, StatusLed, StatusMode};
use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
use cmdr_keyboard_42::{Board, BoardParts, KeyboardState, StatusMode, bootloader};
use embedded_hal_0_2::timer::CountDown;
use fugit::ExtU32;
use panic_halt as _;
use rp2040_hal::{
Sio,
clocks::{Clock, init_clocks_and_plls},
gpio::Pins,
pac,
pio::PIOExt,
timer::Timer,
watchdog::Watchdog,
};
use usb_device::class_prelude::*;
use usb_device::prelude::*;
use usb_device::UsbError;
use usb_device::device::UsbDeviceState;
use usb_device::prelude::*;
use usbd_human_interface_device::device::keyboard::NKROBootKeyboardConfig;
use usbd_human_interface_device::page::Keyboard;
use usbd_human_interface_device::prelude::UsbHidError;
use usbd_human_interface_device::prelude::*;
#[unsafe(link_section = ".boot2")]
@ -36,119 +27,20 @@ pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
#[rp2040_hal::entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = init_clocks_and_plls(
hardware::XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let core = pac::CorePeripherals::take().unwrap();
let sio = Sio::new(pac.SIO);
let pins = Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let button_matrix_row_pins: &mut [&mut dyn InputPin<Error = core::convert::Infallible>;
hardware::KEY_ROWS] = &mut [
&mut cmdr_keyboard_42::get_pin!(pins, button_row_0).into_pull_up_input(),
&mut cmdr_keyboard_42::get_pin!(pins, button_row_1).into_pull_up_input(),
&mut cmdr_keyboard_42::get_pin!(pins, button_row_2).into_pull_up_input(),
&mut cmdr_keyboard_42::get_pin!(pins, button_row_3).into_pull_up_input(),
];
let button_matrix_col_pins: &mut [&mut dyn OutputPin<Error = core::convert::Infallible>;
hardware::KEY_COLS] = &mut [
&mut cmdr_keyboard_42::get_pin!(pins, button_col_0).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_1).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_2).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_3).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_4).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_5).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_6).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_7).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_8).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_9).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_10).into_push_pull_output(),
&mut cmdr_keyboard_42::get_pin!(pins, button_col_11).into_push_pull_output(),
];
let mut button_matrix: ButtonMatrix<
{ hardware::KEY_ROWS },
{ hardware::KEY_COLS },
{ hardware::NUMBER_OF_KEYS },
> = ButtonMatrix::new(
button_matrix_row_pins,
button_matrix_col_pins,
hardware::MATRIX_DEBOUNCE_SCANS_PRESS,
hardware::MATRIX_DEBOUNCE_SCANS_RELEASE,
);
let mut keyboard_state = KeyboardState::new();
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
let mut status_led = StatusLed::new(
cmdr_keyboard_42::get_pin!(pins, status_led).into_function(),
&mut pio,
sm0,
clocks.peripheral_clock.freq(),
);
status_led.update(StatusMode::Error);
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
let mut usb_tick_count_down = timer.count_down();
usb_tick_count_down.start(timers::USB_TICK_INTERVAL_US.micros());
let mut status_led_count_down = timer.count_down();
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
let mut status_time_ms: u32 = 0;
let mut usb_initialized = false;
let mut usb_suspended = false;
let mut wake_on_input = false;
let mut last_activity_ms: u32 = 0;
button_matrix.init_pins();
for _ in 0..hardware::INITIAL_SCAN_PASSES {
button_matrix.scan_matrix(&mut delay);
}
let initial_pressed = button_matrix.buttons_pressed();
if initial_pressed[0] {
bootloader::enter(&mut status_led);
}
let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
true,
&mut pac.RESETS,
));
let BoardParts {
mut button_matrix,
mut status_led,
mut delay,
timer,
usb_bus,
} = Board::new().into_parts();
let mut keyboard = UsbHidClassBuilder::new()
.add_device(
usbd_human_interface_device::device::keyboard::NKROBootKeyboardConfig::default(),
)
.build(&usb_bus);
.add_device(NKROBootKeyboardConfig::default())
.build(usb_bus);
let mut usb_dev =
UsbDeviceBuilder::new(&usb_bus, UsbVidPid(hardware::usb::VID, hardware::usb::PID))
UsbDeviceBuilder::new(usb_bus, UsbVidPid(hardware::usb::VID, hardware::usb::PID))
.strings(&[StringDescriptors::default()
.manufacturer(hardware::usb::MANUFACTURER)
.product(hardware::usb::PRODUCT)
@ -156,22 +48,45 @@ fn main() -> ! {
.unwrap()
.build();
let mut keyboard_state = KeyboardState::new();
status_led.update(StatusMode::Error);
button_matrix.prime(&mut delay, hardware::INITIAL_SCAN_PASSES);
let initial_pressed = button_matrix.buttons_pressed();
if initial_pressed[0] {
bootloader::enter(&mut status_led);
}
let mut usb_tick = timer.count_down();
usb_tick.start(timers::USB_TICK_INTERVAL_US.micros());
let mut status_tick = timer.count_down();
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
let mut status_time_ms: u32 = 0;
let mut usb_initialized = false;
let mut usb_suspended = false;
let mut wake_on_input = false;
let mut last_activity_ms: u32 = 0;
loop {
if status_led_count_down.wait().is_ok() {
if status_tick.wait().is_ok() {
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
let idle_elapsed = status_time_ms.saturating_sub(last_activity_ms);
let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS;
let usb_active = usb_initialized && !idle_mode;
status_led.apply_summary(
keyboard_state.status_summary(usb_initialized, usb_active, usb_suspended, idle_mode),
keyboard_state.status_summary(
usb_initialized,
usb_active,
usb_suspended,
idle_mode,
),
status_time_ms,
);
}
// Skip high-frequency scanning when suspended to save power
// Only scan periodically to detect wake-up inputs
let should_scan = if usb_suspended {
// When suspended, reduce scan frequency by factor of 20 (every ~5ms instead of 250μs)
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
unsafe {
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 20;
@ -181,7 +96,7 @@ fn main() -> ! {
true
};
if usb_tick_count_down.wait().is_ok() && should_scan {
if usb_tick.wait().is_ok() && should_scan {
button_matrix.scan_matrix(&mut delay);
let pressed_keys = button_matrix.buttons_pressed();
@ -205,20 +120,15 @@ fn main() -> ! {
bootloader::enter(&mut status_led);
}
// Check for input activity
if pressed_keys.iter().any(|pressed| *pressed) {
last_activity_ms = status_time_ms;
// Wake from USB suspend if input detected
if wake_on_input && usb_suspended {
// TODO: Implement remote wakeup if supported by host
wake_on_input = false;
}
}
let keyboard_report = keyboard_state.process_scan(pressed_keys);
// Only transmit USB reports when not suspended
if !usb_suspended {
match keyboard.device().write_report(keyboard_report) {
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
@ -229,7 +139,7 @@ fn main() -> ! {
keyboard_state.mark_stopped();
usb_initialized = false;
}
};
}
}
match keyboard.tick() {
@ -238,7 +148,7 @@ fn main() -> ! {
keyboard_state.mark_stopped();
usb_initialized = false;
}
};
}
}
if usb_dev.poll(&mut [&mut keyboard]) {
@ -257,21 +167,16 @@ fn main() -> ! {
}
}
// Check USB device state for suspend/resume handling
let usb_state = usb_dev.state();
let was_suspended = usb_suspended;
usb_suspended = usb_state == UsbDeviceState::Suspend;
// Handle USB resume transition
if was_suspended && !usb_suspended {
// Device was suspended and is now resumed
last_activity_ms = status_time_ms;
wake_on_input = false;
}
// Handle USB suspend transition
if !was_suspended && usb_suspended {
// Device has just been suspended - enter power saving mode
wake_on_input = true;
}
}

View File

@ -1,4 +1,4 @@
//! WS2812 status LED driver adapted from the joystick firmware.
//! Minimal status LED driver for the CMDR keyboard.
use rp2040_hal::{
gpio::AnyPin,
@ -13,37 +13,16 @@ const COLOR_BLUE: RGB8 = RGB8 { r: 10, g: 4, b: 10 };
const COLOR_ORANGE: RGB8 = RGB8 { r: 5, g: 10, b: 0 };
const COLOR_RED: RGB8 = RGB8 { r: 20, g: 0, b: 0 };
const COLOR_PURPLE: RGB8 = RGB8 { r: 0, g: 10, b: 10 };
const BREATH_PERIOD_MS: u32 = 3200;
#[allow(dead_code)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StatusMode {
Off = 0,
Normal = 1,
NormalFlash = 2,
Activity = 3,
ActivityFlash = 4,
Idle = 5,
Other = 6,
OtherFlash = 7,
Warning = 8,
Error = 9,
Bootloader = 10,
Power = 11,
Suspended = 12,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemState {
pub usb_active: bool,
pub usb_initialized: bool,
pub usb_suspended: bool,
pub idle_mode: bool,
pub calibration_active: bool,
pub throttle_hold_enable: bool,
pub vt_enable: bool,
pub caps_lock_active: bool,
pub sticky_armed: bool,
pub sticky_latched: bool,
Off,
Active,
Idle,
Suspended,
Error,
Bootloader,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -77,195 +56,6 @@ impl StatusSummary {
idle_mode,
}
}
pub fn to_system_state(self) -> SystemState {
SystemState {
usb_active: self.usb_active,
usb_initialized: self.usb_initialized,
usb_suspended: self.usb_suspended,
idle_mode: self.idle_mode,
calibration_active: false,
throttle_hold_enable: false,
vt_enable: false,
caps_lock_active: self.caps_lock_active,
sticky_armed: self.sticky_armed,
sticky_latched: self.sticky_latched,
}
}
}
#[derive(Copy, Clone)]
enum LedEffect {
Solid,
Blink { period_ms: u32 },
Heartbeat { period_ms: u32 },
}
#[derive(Copy, Clone)]
struct ModeDescriptor {
color: RGB8,
effect: LedEffect,
}
const HEARTBEAT_POWER_MS: u32 = 800;
const HEARTBEAT_IDLE_MS: u32 = 3200;
impl LedEffect {
fn update_interval_ms(self) -> u32 {
match self {
LedEffect::Solid => 0,
LedEffect::Blink { period_ms } => period_ms / 2,
LedEffect::Heartbeat { .. } => 10,
}
}
fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 {
match self {
LedEffect::Solid => base,
LedEffect::Blink { period_ms } => {
if period_ms == 0 {
return base;
}
let half = (period_ms / 2).max(1);
if elapsed_ms % period_ms < half {
base
} else {
COLOR_OFF
}
}
LedEffect::Heartbeat { period_ms } => {
let period = period_ms.max(1);
let phase = elapsed_ms % period;
let half = (period / 2).max(1);
let ramp = if phase < half {
((phase * 255) / half) as u8
} else {
(((period - phase) * 255) / half) as u8
};
scale_color(base, ramp)
}
}
}
}
const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescriptor {
match mode {
StatusMode::Off => ModeDescriptor {
color: COLOR_OFF,
effect: LedEffect::Solid,
},
StatusMode::Normal => ModeDescriptor {
color: COLOR_GREEN,
effect: LedEffect::Solid,
},
StatusMode::NormalFlash => ModeDescriptor {
color: COLOR_GREEN,
effect: LedEffect::Blink { period_ms: 1000 },
},
StatusMode::Activity => ModeDescriptor {
color: COLOR_BLUE,
effect: LedEffect::Solid,
},
StatusMode::ActivityFlash => ModeDescriptor {
color: COLOR_BLUE,
effect: LedEffect::Blink { period_ms: 600 },
},
StatusMode::Idle => match base_mode {
StatusMode::Activity | StatusMode::ActivityFlash => ModeDescriptor {
color: COLOR_BLUE,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_IDLE_MS,
},
},
StatusMode::Other | StatusMode::OtherFlash => ModeDescriptor {
color: COLOR_ORANGE,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_IDLE_MS,
},
},
StatusMode::NormalFlash | StatusMode::Normal => ModeDescriptor {
color: COLOR_GREEN,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_IDLE_MS,
},
},
StatusMode::Warning | StatusMode::Error => ModeDescriptor {
color: COLOR_RED,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_IDLE_MS,
},
},
_ => ModeDescriptor {
color: COLOR_GREEN,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_IDLE_MS,
},
},
},
StatusMode::Other => ModeDescriptor {
color: COLOR_ORANGE,
effect: LedEffect::Solid,
},
StatusMode::OtherFlash => ModeDescriptor {
color: COLOR_ORANGE,
effect: LedEffect::Blink { period_ms: 600 },
},
StatusMode::Warning => ModeDescriptor {
color: COLOR_RED,
effect: LedEffect::Blink { period_ms: 500 },
},
StatusMode::Error => ModeDescriptor {
color: COLOR_RED,
effect: LedEffect::Solid,
},
StatusMode::Bootloader => ModeDescriptor {
color: COLOR_PURPLE,
effect: LedEffect::Solid,
},
StatusMode::Power => ModeDescriptor {
color: COLOR_GREEN,
effect: LedEffect::Heartbeat {
period_ms: HEARTBEAT_POWER_MS,
},
},
StatusMode::Suspended => ModeDescriptor {
color: COLOR_OFF,
effect: LedEffect::Solid,
},
}
}
fn scale_color(base: RGB8, brightness: u8) -> RGB8 {
let scale = brightness as u16;
RGB8 {
r: ((base.r as u16 * scale) / 255) as u8,
g: ((base.g as u16 * scale) / 255) as u8,
b: ((base.b as u16 * scale) / 255) as u8,
}
}
fn determine_base_mode(system_state: SystemState) -> StatusMode {
if system_state.usb_suspended {
StatusMode::Suspended
} else if system_state.caps_lock_active {
StatusMode::Warning
} else if system_state.sticky_latched {
StatusMode::ActivityFlash
} else if system_state.sticky_armed {
StatusMode::Activity
} else if system_state.calibration_active {
StatusMode::ActivityFlash
} else if !system_state.usb_initialized {
StatusMode::Power
} else if !system_state.usb_active {
StatusMode::NormalFlash
} else if system_state.vt_enable {
StatusMode::Activity
} else if system_state.throttle_hold_enable {
StatusMode::Other
} else {
StatusMode::Normal
}
}
pub struct StatusLed<P, SM, I>
@ -277,7 +67,6 @@ where
ws2812_direct: Ws2812Direct<P, SM, I>,
current_mode: StatusMode,
mode_started_at: Option<u32>,
last_update_time: Option<u32>,
}
impl<P, SM, I> StatusLed<P, SM, I>
@ -292,142 +81,109 @@ where
sm: UninitStateMachine<(P, SM)>,
clock_freq: fugit::HertzU32,
) -> Self {
let ws2812_direct = Ws2812Direct::new(pin, pio, sm, clock_freq);
let mut status = Self {
ws2812_direct,
let mut led = Self {
ws2812_direct: Ws2812Direct::new(pin, pio, sm, clock_freq),
current_mode: StatusMode::Off,
mode_started_at: None,
last_update_time: None,
};
status.write_color(COLOR_OFF);
status
led.write_color(COLOR_OFF);
led
}
pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) {
let base_mode = determine_base_mode(system_state);
let desired_mode = if system_state.idle_mode
&& system_state.usb_initialized
&& base_mode != StatusMode::Off
&& base_mode != StatusMode::Power
{
(StatusMode::Idle, base_mode)
} else {
(base_mode, base_mode)
};
self.set_mode(desired_mode.0, desired_mode.1, current_time);
}
pub fn set_mode(&mut self, mode: StatusMode, base_mode: StatusMode, current_time: u32) {
let force_update = mode != self.current_mode;
pub fn update(&mut self, mode: StatusMode) {
self.current_mode = mode;
let descriptor = descriptor_for(mode, base_mode);
if force_update {
let start_time = match descriptor.effect {
LedEffect::Heartbeat { period_ms } => {
let half = (period_ms / 2).max(1);
current_time.saturating_sub(half)
}
_ => current_time,
};
self.mode_started_at = Some(start_time);
self.last_update_time = None;
}
self.update_display(current_time, force_update, base_mode);
}
pub fn update_display(&mut self, current_time: u32, force_update: bool, base_mode: StatusMode) {
let descriptor = descriptor_for(self.current_mode, base_mode);
let interval = descriptor.effect.update_interval_ms();
if !force_update {
if interval == 0 {
return;
}
if let Some(last) = self.last_update_time
&& current_time.saturating_sub(last) < interval
{
return;
}
}
let elapsed = self
.mode_started_at
.map(|start| current_time.saturating_sub(start))
.unwrap_or(0);
let color = descriptor.effect.color_for(descriptor.color, elapsed);
self.mode_started_at = None;
let color = mode_color(mode, 0);
self.write_color(color);
}
pub fn apply_summary(&mut self, summary: StatusSummary, current_time_ms: u32) {
let mode = summary_to_mode(summary);
let elapsed = if self.current_mode != mode {
self.current_mode = mode;
self.mode_started_at = Some(current_time_ms);
0
} else {
let start = self.mode_started_at.unwrap_or(current_time_ms);
current_time_ms.saturating_sub(start)
};
let base_color = mode_color(mode, elapsed);
let color = highlight_color(summary).unwrap_or(base_color);
self.write_color(color);
self.last_update_time = Some(current_time);
}
fn write_color(&mut self, color: RGB8) {
let _ = self.ws2812_direct.write([color].iter().copied());
}
}
pub fn apply_summary(&mut self, summary: StatusSummary, current_time: u32) {
self.update_from_system_state(summary.to_system_state(), current_time);
fn summary_to_mode(summary: StatusSummary) -> StatusMode {
if summary.usb_suspended {
StatusMode::Suspended
} else if !summary.usb_initialized {
StatusMode::Off
} else if summary.idle_mode {
StatusMode::Idle
} else if summary.usb_active {
StatusMode::Active
} else {
StatusMode::Off
}
}
impl<P, SM, I> StatusLed<P, SM, I>
where
I: AnyPin<Function = P::PinFunction>,
P: PIOExt,
SM: StateMachineIndex,
{
pub fn update(&mut self, mode: StatusMode) {
self.set_mode(mode, mode, 0);
fn highlight_color(summary: StatusSummary) -> Option<RGB8> {
if summary.sticky_latched {
Some(COLOR_PURPLE)
} else if summary.sticky_armed {
Some(COLOR_BLUE)
} else if summary.caps_lock_active {
Some(COLOR_ORANGE)
} else {
None
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn caps_lock_overrides_with_warning() {
let state = SystemState {
caps_lock_active: true,
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::Warning);
}
#[test]
fn sticky_states_map_to_activity_modes() {
let state = SystemState {
sticky_latched: true,
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::ActivityFlash);
let state = SystemState {
sticky_armed: true,
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::Activity);
}
#[test]
fn usb_not_active_flashes_normal() {
let state = SystemState {
usb_initialized: true,
usb_active: false,
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::NormalFlash);
}
#[test]
fn usb_suspend_takes_priority() {
let state = SystemState {
usb_suspended: true,
caps_lock_active: true, // Even with caps lock active
sticky_latched: true, // Even with sticky latched
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::Suspended);
fn mode_color(mode: StatusMode, elapsed_ms: u32) -> RGB8 {
match mode {
StatusMode::Off => COLOR_OFF,
StatusMode::Active => COLOR_GREEN,
StatusMode::Idle => breathe(COLOR_GREEN, elapsed_ms, BREATH_PERIOD_MS),
StatusMode::Suspended => blink(COLOR_BLUE, elapsed_ms, 2000),
StatusMode::Error => COLOR_RED,
StatusMode::Bootloader => COLOR_PURPLE,
}
}
fn blink(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 {
if period_ms == 0 {
return color;
}
let phase = (elapsed_ms / (period_ms / 2).max(1)) % 2;
if phase == 0 { color } else { COLOR_OFF }
}
fn breathe(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 {
if period_ms == 0 {
return color;
}
let period = period_ms.max(1);
let time_in_period = (elapsed_ms % period) as f32;
let period_f = period as f32;
let phase = time_in_period / period_f;
let brightness = if phase < 0.5 {
1.0 - (phase * 2.0)
} else {
(phase - 0.5) * 2.0
};
let clamped = brightness.max(0.0).min(1.0);
let ramp = (clamped * 255.0 + 0.5) as u8;
scale_color(color, ramp)
}
fn scale_color(color: RGB8, factor: u8) -> RGB8 {
RGB8 {
r: (u16::from(color.r) * u16::from(factor) / 255) as u8,
g: (u16::from(color.g) * u16::from(factor) / 255) as u8,
b: (u16::from(color.b) * u16::from(factor) / 255) as u8,
}
}

114
tools/copy_uf2.py Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Copy a UF2 artifact to a detected RP2040 mass-storage mount."""
from __future__ import annotations
import argparse
import os
import sys
import time
from pathlib import Path
import shutil
INFO_FILE = "INFO_UF2.TXT"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True, help="Path to the UF2 file to copy")
parser.add_argument("--timeout", type=float, default=10.0, help="Seconds to wait for the mount")
parser.add_argument(
"--mount",
type=str,
default=os.environ.get("MOUNT", ""),
help="Explicit mount point (default: auto-detect)",
)
return parser.parse_args()
def candidate_paths(explicit: str, user: str) -> list[Path]:
paths: list[Path] = []
if explicit:
paths.append(Path(explicit))
roots = [
Path("/Volumes"),
Path("/media"),
Path(f"/media/{user}"),
Path("/run/media"),
Path(f"/run/media/{user}"),
]
for root in roots:
if not root.exists() or not root.is_dir():
continue
# Many systems mount the UF2 volume directly as a child of the root directory.
for child in root.iterdir():
if child.is_dir():
paths.append(child)
return paths
def choose_mount(explicit: str, user: str) -> Path | None:
candidates = candidate_paths(explicit, user)
if explicit:
# For an explicit mount we only care whether it exists.
path = Path(explicit)
return path if path.exists() and path.is_dir() else None
# Prefer candidates containing INFO_UF2.TXT, fall back to first existing directory.
info_candidates = [path for path in candidates if (path / INFO_FILE).exists()]
if info_candidates:
return info_candidates[0]
for path in candidates:
if path.exists() and path.is_dir():
return path
return None
def main() -> int:
args = parse_args()
source = args.source
if not source.exists():
print(f"UF2 source file not found: {source}", file=sys.stderr)
return 1
explicit_mount = args.mount.strip()
user = os.environ.get("USER", "")
deadline = time.time() + float(args.timeout)
while time.time() <= deadline:
mount = choose_mount(explicit_mount, user)
if mount is not None:
if not mount.exists() or not mount.is_dir():
time.sleep(1)
continue
destination = mount / source.name
try:
shutil.copy2(source, destination)
try:
if hasattr(os, "sync"):
os.sync()
except Exception:
pass
# Give the device a moment to process the copied file before we exit.
time.sleep(0.5)
except Exception as exc: # noqa: BLE001
print(f"Failed to copy UF2 to {destination}: {exc}", file=sys.stderr)
return 1
print(f"Copied {source} to {destination}")
return 0
time.sleep(1)
if explicit_mount:
print(
f"Mount point '{explicit_mount}' not found within {args.timeout} seconds",
file=sys.stderr,
)
else:
print(
"Unable to detect RP2040 UF2 mount. Pass one via mount=/path",
file=sys.stderr,
)
return 1
if __name__ == "__main__": # pragma: no cover
sys.exit(main())