12 Commits

Author SHA1 Message Date
74e63c513f Remap buttons
Signed-off-by: Nicholas Mello <nick@nmello.dev>
2026-07-25 17:51:35 -05:00
78c77a6b2d Merge pull request #7 from nicholasmello/usb-keycodes
Map Usb Keycodes to Enum
2026-07-25 17:36:46 -05:00
59a3ab1596 Map Usb Keycodes to Enum 2026-07-25 16:55:14 -05:00
06660ff07a Merge pull request #6 from nicholasmello/fix-ci
Fix ci
2026-07-25 16:54:39 -05:00
4c14894758 ci: Update pages
Signed-off-by: Nicholas Mello <nick@nmello.dev>
2026-07-25 16:43:31 -05:00
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
f8ee319fbe Merge pull request #2 from nicholasmello/nmello/bulk-send
keyboard: Update reporting logic
2026-07-24 18:37:25 -05:00
4 changed files with 397 additions and 98 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@v4
- name: Build documentation
run: cargo doc --no-deps
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: target/doc/
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,17 +1,138 @@
use crate::usb::KeyboardEvent;
use alloc::vec::Vec;
use embassy_rp::gpio::Input;
use embassy_sync::blocking_mutex::raw::RawMutex;
use embassy_sync::channel::Sender;
use futures_util::stream::{FuturesUnordered, StreamExt};
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum UsbKeycodes {
A = 4,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Zero,
Enter,
Escape,
BackSpace,
Tab,
Space,
Hyphen,
Equal,
SquareBracketLeft,
SquareBracketRight,
BackSlash,
Semicolon = 51,
SingleQuote,
Backtick,
Comma,
Period,
ForwardSlash,
CapsLock,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
PrintScreen,
ScrollLock,
Pause,
Insert,
Home,
PageUp,
Delete,
End,
PageDown,
Right,
Left,
Down,
Up,
NumLock,
F13 = 104,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Execute,
Help,
Menu,
Select,
Again,
Undo,
Cut,
Copy,
Paste,
Find,
Mute,
VolumeUp,
VolumeDown,
ControlLeft = 224,
ShiftLeft,
AltLeft,
GUILeft,
ControlRight,
ShiftRight,
AltRight,
GUIRight,
}
pub struct Key<'a> {
button: Input<'a>,
value: char,
value: UsbKeycodes,
}
impl Key<'_> {
#[allow(unused)]
pub async fn set_value(mut self, value: char) {
pub async fn set_value(mut self, value: UsbKeycodes) {
self.value = value;
}
}
@@ -21,6 +142,7 @@ pub struct Keyboard<'d, const KEY_N: usize> {
num_keys: usize,
}
#[derive(Debug, defmt::Format)]
pub enum KeyboardError {
MaxKeys,
}
@@ -33,7 +155,7 @@ impl<'d, const KEY_N: usize> Keyboard<'d, KEY_N> {
}
}
pub fn add_key(&mut self, button: Input<'d>, value: char) -> Result<(), KeyboardError> {
pub fn add_key(&mut self, button: Input<'d>, value: UsbKeycodes) -> Result<(), KeyboardError> {
let key = self
.keys
.get_mut(self.num_keys)
@@ -45,8 +167,10 @@ impl<'d, const KEY_N: usize> Keyboard<'d, KEY_N> {
Ok(())
}
pub async fn process<'ch, M, const N: usize>(mut self, sender: Sender<'ch, M, KeyboardEvent, N>)
where
pub async fn process<'ch, M, const N: usize>(
mut self,
sender: Sender<'ch, M, Vec<UsbKeycodes>, N>,
) where
M: RawMutex,
{
loop {
@@ -56,14 +180,14 @@ impl<'d, const KEY_N: usize> Keyboard<'d, KEY_N> {
.collect::<FuturesUnordered<_>>()
.next()
.await;
let mut pressed: Vec<UsbKeycodes> = Vec::new();
for key in self.keys.iter_mut().filter_map(|opt| opt.as_mut()) {
sender
.send(KeyboardEvent {
key: key.value,
action: key.button.get_level().into(),
})
.await;
if key.button.is_low() {
pressed.push(key.value);
}
}
sender.send(pressed).await;
}
}
}

View File

@@ -5,8 +5,10 @@ mod heartbeat;
mod keyboard;
mod usb;
extern crate alloc;
use crate::heartbeat::heartbeat;
use crate::keyboard::Keyboard;
use crate::keyboard::{Keyboard, UsbKeycodes};
use alloc::vec::Vec;
use defmt::*;
use embassy_executor::Spawner;
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<UsbKeycodes>, 64> = Channel::new();
let p = embassy_rp::init(Default::default());
@@ -45,18 +47,86 @@ async fn main(_spawner: Spawner) {
let driver = Driver::new(p.USB, Irqs);
let mut usb_buf = usb::UsbKeyboardBuf::new();
let usbkey = usb::UsbKeyboard::create_usb(&mut usb_buf, driver);
let mut keyboard = Keyboard::<10>::new();
// Set up the signal pin that will be used to trigger the keyboard.
let mut signal_pin = Input::new(p.PIN_16, Pull::None);
// Enable the schmitt trigger to slightly debounce.
let mut signal_pin = Input::new(p.PIN_15, Pull::None);
signal_pin.set_schmitt(true);
let mut keyboard = Keyboard::<1>::new();
match keyboard.add_key(signal_pin, 'a') {
match keyboard.add_key(signal_pin, UsbKeycodes::X) {
Ok(()) => info!("Key 'a' registered"),
Err(_) => {
defmt::panic!("Failed to register key!");
Err(e) => {
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, UsbKeycodes::C) {
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, UsbKeycodes::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, UsbKeycodes::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, UsbKeycodes::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, UsbKeycodes::ShiftLeft) {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_14, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, UsbKeycodes::Up) {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_13, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, UsbKeycodes::Down) {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_12, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, UsbKeycodes::Right) {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}
let mut signal_pin = Input::new(p.PIN_11, Pull::None);
signal_pin.set_schmitt(true);
match keyboard.add_key(signal_pin, UsbKeycodes::Left) {
Ok(()) => info!("Key 'w' registered"),
Err(e) => {
defmt::panic!("Failed to register key! {:?}", e);
}
}

View File

@@ -1,8 +1,9 @@
use crate::keyboard::UsbKeycodes;
use alloc::vec::Vec;
use core::cell::OnceCell;
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use defmt::*;
use embassy_futures::join::join;
use embassy_rp::gpio::Level;
use embassy_rp::peripherals::USB;
use embassy_rp::usb::Driver;
use embassy_sync::blocking_mutex::raw::RawMutex;
@@ -18,27 +19,6 @@ use usbd_hid::descriptor::{KeyboardReport, SerializedDescriptor};
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> {
// Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors.
@@ -128,7 +108,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<UsbKeycodes>, N>)
where
M: RawMutex,
{
@@ -148,58 +128,31 @@ impl<'d> UsbKeyboard<'d> {
loop {
let event = receiver.receive().await;
match event {
KeyboardEvent {
key: 'a',
action: KeyboardAction::Press,
} => {
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8
{
match writer.write(&[0, 0, 4, 0, 0, 0, 0, 0]).await {
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
};
} else {
// Create a report with the A key pressed. (no shift modifier)
let report = KeyboardReport {
keycodes: [4, 0, 0, 0, 0, 0],
leds: 0,
modifier: 0,
reserved: 0,
};
// Send the report.
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),
};
}
}
KeyboardEvent {
key: 'a',
action: KeyboardAction::Depress,
} => {
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8
{
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!");
}
let mut boot_keys = [0, 0, 0, 0, 0, 0, 0, 0];
for (&key, idx) in event.iter().zip(2usize..) {
boot_keys[idx] = key as u8;
}
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 {
match writer.write(&boot_keys).await {
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
};
} else {
defmt::unimplemented!("Only supports boot mode keyboards!");
// // Create a report with the A key pressed. (no shift modifier)
// let report = KeyboardReport {
// keycodes: [4, 0, 0, 0, 0, 0],
// leds: 0,
// modifier: 0,
// reserved: 0,
// };
// // Send the report.
// match writer.write_serialize(&report).await {
// Ok(()) => {}
// Err(e) => warn!("Failed to send report: {:?}", e),
// };
}
}
};