Rants, Ideas, Stuff

A blog about coding, coffee, and stuff

How to talk to your QMK-based keyboard using Rust

Recently I’ve played around a lot with mechanical keyboards. I really like them and I love the programmability of the ones that are supported by the QMK firmware. In this post I’ll try to show how to talk to your QMK based keyboard with the Rust programming language.

I’ll post another article about my keyboard journey later.

It didn’t take long for me to get deeper into QMK and I had the idea to let monitoring alerts turn on LEDs on my keyboard.

First I built a hand-wired keyboard from scratch (the SixKeyTester, more on that in a later post) and added a red LED to it.

The idea is to have a command that turns on an LED in my keyboard when there are monitoring alerts, that I can use like any-alerts && kbdctl led on || kbdctl led off.

I decided to implement kbdctl in Rust.

We’ll keep the “protocol” absolutely simple. We’ll just send [0, 0] to turn the LED off and [1, 0] to turn it on.

Let’s get started!

QMK part

Let’s first start with the QMK firmware stuff. We’re using a feature called Raw HID which allows bidirectional communication. We only use it in one direction for GPIO Control.

We enable Raw HID in rules.mk by adding the following line:

# [...]
RAW_ENABLE = yes
# [...]

We also need to implement the raw_hid_receive function in keymap.c. This requires the raw_hid.h header file.

For my use case setting a GPIO pin to high or low is sufficient. You might want to blink, change the keymap, or us the backlight LEDs. For bidirectional communication you can send data back via raw_hid_send. This is what I did:

#include "raw_hid.h"

// [...]
void raw_hid_receive(uint8_t *data, uint8_t length) {
    setPinOutput(B6);
    uint8_t *command_id = &(data[0]);
    switch (*command_id) {
        case 0:
          writePinLow(B6);
          break;
        case 1:
          writePinHigh(B6);
          break;
    }
}

The last file we’ll take a look at in QMK is config.h. We don’t need to change anything in here, we’ll just note the VENDOR_ID and PRODUCT_ID because we’ll need them in the Rust part:

// [...]
/* USB Device descriptor parameter */
#define VENDOR_ID 0xFEED
#define PRODUCT_ID 0x0000
#define RAW_USAGE_PAGE 0xFF60
#define RAW_USAGE_ID 0x61
// [...]

Now we can build and flash our firmware:

qmk make SixKeyTester:default

Rust part

The Rust part is pretty straight forward. I’ll leave out the argument parsing I did with structopt.

We’ll be using the hidapi crate, so we’ll add it to our Cargo.toml:

[dependencies]
hidapi = "1.2.5"

And also use import it:

use hidapi::{DeviceInfo, HidApi};

Next we’ll define constants with the IDs from our firmware’s config.h:

// set values from config.h
const VENDOR_ID: u16 = 0xfeed;
const PRODUCT_ID: u16 = 0;
const USAGE_PAGE: u16 = 0xff60;

For readability we’ll add a function that checks if a device is our wanted device:

fn is_my_device(device: &DeviceInfo) -> bool {
    device.vendor_id() == VENDOR_ID
        && device.product_id() == PRODUCT_ID
        && device.usage_page() == USAGE_PAGE
}

In our main function we’ll first instatiate a HidApi:

let api = HidApi::new().unwrap_or_else(|e| {
    eprintln!("Error: {}", e);
    std::process::exit(1);
});

And then try to find our device and open it

let device = api
    .device_list()
    .find(|device| is_my_device(device))
    .unwrap_or_else(|| {
      eprintln!("Could not find keyboard");
      std::process::exit(1);
    })
    .open_device(&api)
    .unwrap_or_else(|| {
      eprintln!("Could not open HID device");
      std::process::exit(1);
    });

The last step is to write our command to the device:

let _ = device.write(&[command, 0]);
std::process::exit(0);

And we’re done!

Here’s the full main.rs:

use hidapi::{DeviceInfo, HidApi};

// set values from config.h
const VENDOR_ID: u16 = 0xfeed;
const PRODUCT_ID: u16 = 0;
const USAGE_PAGE: u16 = 0xff60;

fn is_my_device(device: &DeviceInfo) -> bool {
    device.vendor_id() == VENDOR_ID
        && device.product_id() == PRODUCT_ID
        && device.usage_page() == USAGE_PAGE
}

fn main() {
    // Do argument parsing to detect what to do
    // [...]
    let api = HidApi::new().unwrap_or_else(|e| {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    });

    let device = api
        .device_list()
        .find(|device| is_my_device(device))
        .unwrap_or_else(|| {
          eprintln!("Could not find keyboard");
          std::process::exit(1);
        })
        .open_device(&api)
        .unwrap_or_else(|| {
          eprintln!("Could not open HID device");
          std::process::exit(1);
        });

    let _ = device.write(&[command, 0]);
    std::process::exit(0);
}