Compare commits

...

6 Commits

Author SHA1 Message Date
189a8c463f Merge pull request #5 from nicholasmello/multikey
Some checks are pending
Rust CI / Check (push) Waiting to run
Rust CI / Clippy (push) Waiting to run
Rust CI / Format (push) Waiting to run
Rust CI / Build (push) Blocked by required conditions
Rust CI / Documentation (push) Waiting to run
Rust CI / Deploy Docs (push) Blocked by required conditions
Multikey support
2026-07-25 16:23:34 -05:00
ea01fe2071 Multikey support
Signed-off-by: Nicholas Mello <nick@nmello.dev>
2026-07-25 16:20:51 -05:00
2b99a56a4a Merge pull request #4 from nicholasmello/multiple-buttons
Multiple buttons part 1
2026-07-25 15:17:09 -05:00
681c93f558 Multiple buttons part 1
Add and hook up all 6 buttons

Signed-off-by: Nicholas Mello <nick@nmello.dev>
2026-07-25 15:13:28 -05:00
0dc8e721b2 Merge pull request #3 from nicholasmello/add-ci
ci: Add rust workflow
2026-07-25 14:10:35 -05:00
e49e5b1311 ci: Add rust workflow
Signed-off-by: Nicholas Mello <nick@nmello.dev>
2026-07-25 14:08:16 -05:00
4 changed files with 243 additions and 92 deletions

152
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,152 @@
# .github/workflows/ci.yml
name: Rust CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
schedule:
- cron: '0 0 * * 1' # Weekly Sunday midnight UTC
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Dwarnings"
CARGO_TERM_COLOR: always
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.17.0
with:
target: thumbv6m-none-eabi
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: "."
- name: Run cargo check
run: cargo check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.17.0
with:
components: clippy
target: thumbv6m-none-eabi
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: "."
- name: Run clippy
run: cargo clippy -- -D warnings
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.17.0
with:
components: rustfmt
target: thumbv6m-none-eabi
- name: Check formatting
run: cargo fmt --all -- --check
build:
name: Build
runs-on: ubuntu-latest
needs: [check, clippy, fmt]
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.17.0
with:
target: thumbv6m-none-eabi
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: "."
- name: Build debug
run: cargo build --verbose
- name: Build release
run: cargo build --release --verbose
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: target/
retention-days: 7
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.17.0
with:
components: rust-docs
target: thumbv6m-none-eabi
- name: Build documentation
run: cargo doc --no-deps --document-private-items
- name: Upload documentation
uses: actions/upload-artifact@v4
with:
name: documentation
path: target/thumbv6m-none-eabi/doc/
retention-days: 7
pages:
name: Deploy Docs
runs-on: ubuntu-latest
needs: [docs]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Build documentation
run: cargo doc --no-deps
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: target/doc/
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View File

@@ -1,4 +1,4 @@
use crate::usb::KeyboardEvent; use alloc::vec::Vec;
use embassy_rp::gpio::Input; use embassy_rp::gpio::Input;
use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::blocking_mutex::raw::RawMutex;
use embassy_sync::channel::Sender; use embassy_sync::channel::Sender;
@@ -21,6 +21,7 @@ pub struct Keyboard<'d, const KEY_N: usize> {
num_keys: usize, num_keys: usize,
} }
#[derive(Debug, defmt::Format)]
pub enum KeyboardError { pub enum KeyboardError {
MaxKeys, MaxKeys,
} }
@@ -45,7 +46,7 @@ impl<'d, const KEY_N: usize> Keyboard<'d, KEY_N> {
Ok(()) Ok(())
} }
pub async fn process<'ch, M, const N: usize>(mut self, sender: Sender<'ch, M, KeyboardEvent, N>) pub async fn process<'ch, M, const N: usize>(mut self, sender: Sender<'ch, M, Vec<char>, N>)
where where
M: RawMutex, M: RawMutex,
{ {
@@ -56,14 +57,14 @@ impl<'d, const KEY_N: usize> Keyboard<'d, KEY_N> {
.collect::<FuturesUnordered<_>>() .collect::<FuturesUnordered<_>>()
.next() .next()
.await; .await;
let mut pressed: Vec<char> = Vec::new();
for key in self.keys.iter_mut().filter_map(|opt| opt.as_mut()) { for key in self.keys.iter_mut().filter_map(|opt| opt.as_mut()) {
sender if key.button.is_low() {
.send(KeyboardEvent { pressed.push(key.value);
key: key.value, }
action: key.button.get_level().into(),
})
.await;
} }
sender.send(pressed).await;
} }
} }
} }

View File

@@ -5,8 +5,10 @@ mod heartbeat;
mod keyboard; mod keyboard;
mod usb; mod usb;
extern crate alloc;
use crate::heartbeat::heartbeat; use crate::heartbeat::heartbeat;
use crate::keyboard::Keyboard; use crate::keyboard::Keyboard;
use alloc::vec::Vec;
use defmt::*; use defmt::*;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_futures::join::join; use embassy_futures::join::join;
@@ -37,7 +39,7 @@ async fn main(_spawner: Spawner) {
); );
} }
let keyboard_events: Channel<ThreadModeRawMutex, usb::KeyboardEvent, 64> = Channel::new(); let keyboard_events: Channel<ThreadModeRawMutex, Vec<char>, 64> = Channel::new();
let p = embassy_rp::init(Default::default()); let p = embassy_rp::init(Default::default());
@@ -45,18 +47,54 @@ async fn main(_spawner: Spawner) {
let driver = Driver::new(p.USB, Irqs); let driver = Driver::new(p.USB, Irqs);
let mut usb_buf = usb::UsbKeyboardBuf::new(); let mut usb_buf = usb::UsbKeyboardBuf::new();
let usbkey = usb::UsbKeyboard::create_usb(&mut usb_buf, driver); let usbkey = usb::UsbKeyboard::create_usb(&mut usb_buf, driver);
let mut keyboard = Keyboard::<6>::new();
// Set up the signal pin that will be used to trigger the keyboard. let mut signal_pin = Input::new(p.PIN_15, Pull::None);
let mut signal_pin = Input::new(p.PIN_16, Pull::None);
// Enable the schmitt trigger to slightly debounce.
signal_pin.set_schmitt(true); signal_pin.set_schmitt(true);
let mut keyboard = Keyboard::<1>::new();
match keyboard.add_key(signal_pin, 'a') { match keyboard.add_key(signal_pin, 'a') {
Ok(()) => info!("Key 'a' registered"), Ok(()) => info!("Key 'a' registered"),
Err(_) => { Err(e) => {
defmt::panic!("Failed to register key!"); defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_16, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, 's') {
Ok(()) => info!("Key 's' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_17, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, 'x') {
Ok(()) => info!("Key 'x' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_18, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, 'd') {
Ok(()) => info!("Key 'd' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_19, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, 'c') {
Ok(()) => info!("Key 'c' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_20, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, 'w') {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
} }
} }

View File

@@ -1,8 +1,8 @@
use alloc::vec::Vec;
use core::cell::OnceCell; use core::cell::OnceCell;
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use defmt::*; use defmt::*;
use embassy_futures::join::join; use embassy_futures::join::join;
use embassy_rp::gpio::Level;
use embassy_rp::peripherals::USB; use embassy_rp::peripherals::USB;
use embassy_rp::usb::Driver; use embassy_rp::usb::Driver;
use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::blocking_mutex::raw::RawMutex;
@@ -18,27 +18,6 @@ use usbd_hid::descriptor::{KeyboardReport, SerializedDescriptor};
pub static HID_PROTOCOL_MODE: AtomicU8 = AtomicU8::new(HidProtocolMode::Boot as u8); pub static HID_PROTOCOL_MODE: AtomicU8 = AtomicU8::new(HidProtocolMode::Boot as u8);
#[derive(Clone, Copy)]
pub enum KeyboardAction {
Press,
Depress,
}
impl From<Level> for KeyboardAction {
fn from(value: Level) -> Self {
match value {
Level::Low => KeyboardAction::Depress,
Level::High => KeyboardAction::Press,
}
}
}
#[derive(Clone, Copy)]
pub struct KeyboardEvent {
pub key: char,
pub action: KeyboardAction,
}
pub struct UsbKeyboardBuf<'d> { pub struct UsbKeyboardBuf<'d> {
// Create embassy-usb DeviceBuilder using the driver and config. // Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors. // It needs some buffers for building the descriptors.
@@ -128,7 +107,7 @@ impl<'d> UsbKeyboard<'d> {
} }
} }
pub async fn run<'ch, M, const N: usize>(self, receiver: Receiver<'ch, M, KeyboardEvent, N>) pub async fn run<'ch, M, const N: usize>(self, receiver: Receiver<'ch, M, Vec<char>, N>)
where where
M: RawMutex, M: RawMutex,
{ {
@@ -148,58 +127,39 @@ impl<'d> UsbKeyboard<'d> {
loop { loop {
let event = receiver.receive().await; let event = receiver.receive().await;
match event { let mut boot_keys = [0, 0, 0, 0, 0, 0, 0, 0];
KeyboardEvent {
key: 'a', for (&key, idx) in event.iter().zip(2usize..) {
action: KeyboardAction::Press, let boot_key = match key {
} => { 'd' => 79,
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 'a' => 80,
{ 's' => 81,
match writer.write(&[0, 0, 4, 0, 0, 0, 0, 0]).await { 'w' => 82,
Ok(()) => {} ('a'..='z') => (key as u8) - b'a' + 4,
Err(e) => warn!("Failed to send boot report: {:?}", e), _ => defmt::panic!("Not supported key: {}", key),
}; };
} else { boot_keys[idx] = boot_key;
// Create a report with the A key pressed. (no shift modifier) }
let report = KeyboardReport {
keycodes: [4, 0, 0, 0, 0, 0], if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 {
leds: 0, match writer.write(&boot_keys).await {
modifier: 0, Ok(()) => {}
reserved: 0, Err(e) => warn!("Failed to send boot report: {:?}", e),
}; };
// Send the report. } else {
match writer.write_serialize(&report).await { defmt::unimplemented!("Only supports boot mode keyboards!");
Ok(()) => {} // // Create a report with the A key pressed. (no shift modifier)
Err(e) => warn!("Failed to send report: {:?}", e), // let report = KeyboardReport {
}; // keycodes: [4, 0, 0, 0, 0, 0],
} // leds: 0,
} // modifier: 0,
KeyboardEvent { // reserved: 0,
key: 'a', // };
action: KeyboardAction::Depress, // // Send the report.
} => { // match writer.write_serialize(&report).await {
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 // Ok(()) => {}
{ // Err(e) => warn!("Failed to send report: {:?}", e),
match writer.write(&[0, 0, 0, 0, 0, 0, 0, 0]).await { // };
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
};
} else {
let report = KeyboardReport {
keycodes: [0, 0, 0, 0, 0, 0],
leds: 0,
modifier: 0,
reserved: 0,
};
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),
};
}
}
_ => {
defmt::panic!("Unexpected event!");
}
} }
} }
}; };