+++
title = "Keyboard Lighting"
date = 2024-04-10
updated = 2024-07-30
+++
{% note() %}
**I am NOT a keyboard nerd**. Full respect to any of you out there, but I am not one, nor do I plan to become one anytime soon.
Don't take this as a "definitive guide" to anything, it was just me messing around.
{% end %}

# Introduction
Yesterday, I started using a new keyboard.  
Well, it was yesterday as of when I started writing this - it certainly isn't yesterday anymore.

Previously, I'd been using a super mushy-feeling Incredibly Generic Office Supply Keyboard from Dell, which honestly sucked.
After a few years of heavy use it had gotten kinda nasty, and was basically uncleanable.

Considering how much time I spend typing, I figured it was worth getting an upgrade, so I ordered a
[Keychron C1 Pro](https://www.keychron.com/products/keychron-c1-pro-qmk-via-wired-mechanical-keyboard?variant=40615805583449)
(the variant with hot-swappable switches, and therefore also RGB lighting).
I'm not going to say it's the best keyboard ever and that you should go out and buy it immediately,
or conversely that it's the *worst* keyboard and you should avoid it at all costs.
It's fine for my purposes, and is a huge improvement over my old keyboard, but that's beside the point.

{% note() %}
...though I have since sold off the C1 Pro and replaced it with a Rainy 75. It's sooo much better, and I'm not going back.
{% end %}

I didn't buy it for the RGB effects.  
To be honest, I don't even *like* RGB lighting most of the time. I find it kind of distracting,
and I usually try to avoid "gamer-branded" products stuffed to the brim with multicolored lights like the plague.
But, being me, and being told "yeah this keyboard has an ARM chip inside it" (a `STM32F402RC` to be precise) I had to see if I could customize it.

Aaaand so began an afternoon-long dive down a keyboard rabbit-hole.

# VIA
{% important() %}
I started writing this before [Keychron Launcher](https://launcher.keychron.com) came out.  
I haven't tried to use it, and don't really know what it's capable of, but it's possible that
if it had existed at the time I wouldn't have gone down this rabbit hole at all.
{% end %}

From poking around on Keychron's website it looked like the simplest route to customize the lighting was to use [VIA](https://www.caniusevia.com/).
Firefox doesn't support [webhid](https://caniuse.com/?search=webhid), but Chromium-based browsers do, so I opened up Chromium...
and what's this? An error? Already? I haven't even *done* anything yet.

{{ image(src="/assets/keyboard/keyboard-via-error.webp", alt="Screenshot of VIA displaying an error", caption="Extremely useful error message") }}

Ooookay?

Apparently you need to dig up a keyboard-specific config file (in my case [this json](/assets/keyboard/c1_pro_v2_ansi_rgb.json)) and load it.
Don't really know how I missed that.

In any case, it seemed to be working, and I could cycle through the various RGB modes, as well as customize the static color.
But, come on, that's *so dreadfully boring*. I wasn't interested in customizing keymaps or anything like that, and I felt... unsatisfied... with the lighting options.
Apparently their definition of "customizable" lighting doesn't quite align with mine.
There's a little computer inside there, there's bound to be *something* more I can do with it...

# Screwing around in a VM
After googling around for a while, I stumbled upon [this reddit post](https://www.reddit.com/r/Keychron/comments/lbk2fk/a_very_odd_workaround_for_the_rgb_software/)
advertising a supposed way to program the lighting using somewhat sketchy-seeming third-party software.

Soo... (not knowing any better), I guess it's Windows VM time!

{{ image(src="/assets/keyboard/keyboard-windont.webp", alt="Windows installation screen inside of QEMU", caption="Ah shit, here we go again") }}

Maybe it had something to do with the keyboard itself being unsupported, maybe it was the software,
maybe it was USB device passthrough being weird, but the app always quit with a "no gaming device found" popup and no other useful information.
I also tried VIA again and some other software, and had no real success with any of it.

Solid waste of an hour.

It felt like I was digging too deep into solutions for *my specific keyboard*, but how do people do this more generally?

# QMK Firmware
> Your computer keyboard has a processor inside of it, similar to the one inside your computer.
> This processor runs software that is responsible for detecting button presses and informing the computer when keys are pressed.
> QMK Firmware fills the role of that software, detecting button presses and passing that information on to the host computer. [...]
> QMK tries to put a lot of power into your hands by making easy things easy, and hard things possible.
>
> \- [QMK documentation](https://docs.qmk.fm/newbs)

Well, this certainly sounds more promising.
I'm kind of upset I didn't find this earlier, to be honest.

Apparently putting custom firmware on a keyboard is a perfectly normal thing? Having not looked into any of this before, it felt a little daunting.

> Open your `keymap.c` file in your text editor.

Wait what?  
I need C?  
I don't... I don't do C...  
What am I even doing at this point?  
How did I get here?  
Help??? Anyone???


At least the [QMK Documentation](https://docs.qmk.fm/newbs) is fairly comprehensive, and flashing custom firmware isn't *that* much of a pain, all things considered.
They have it set up to be fairly friendly toward clueless people like me who have no idea what they're doing.

I wasn't interested in a custom keymap, I'm just here for the [RGB matrix effects](https://docs.qmk.fm/features/rgb_matrix#custom-rgb-matrix-effects).

The basic examples were pretty easy to follow, and it wasn't that hard to display a static pattern of lights.
Doing something dynamic sounded cool, but also like a project for The Distant Future(tm).

## HID Communication
Somehow, the VIA app was communicating with the keyboard to set the static color without flashing new firmware,
so I wanted to see if I could imitate that myself.

Turns out QMK makes it quite easy to [send data back and forth](https://docs.qmk.fm/features/rawhid)
between the computer and the keyboard, and after a little fiddling around, I had something promising.

## Final Code
```c
RGB_MATRIX_EFFECT(custom_colors)
RGB_MATRIX_EFFECT(pride_flag)

#ifdef RGB_MATRIX_CUSTOM_EFFECT_IMPLS

// yes I *know* these could be packed but no
static uint8_t color_r = 0xFF;
static uint8_t color_g = 0xFF;
static uint8_t color_b = 0xFF;

// I'm almost positive you're not *supposed* to have this in your rgb effect definitions,
// but uhh... it works... so, I'm not complaining
#include "raw_hid.h"
void raw_hid_receive(uint8_t* data, uint8_t length) {
    // this probably isn't proper idiomatic C, but whatever
    color_r = data[0];
    color_g = data[1];
    color_b = data[2];
}

static bool custom_colors(effect_params_t* params) {
    RGB_MATRIX_USE_LIMITS(led_min, led_max)
    for (uint8_t i = led_min; i < led_max; i++) {
        rgb_matrix_set_color(i, 
            color_r, 
            color_g, 
            color_b
        );
    }
    return rgb_matrix_check_finished_leds(led_max);
}


// simple static effect, just for fun
static bool pride_flag(effect_params_t* params) {
    RGB_MATRIX_USE_LIMITS(led_min, led_max);
    for (uint8_t i = led_min; i < led_max; i++) {
        int16_t y = g_led_config.point[i].y;
        // hardcoded y values found through trial and error, 
        // and are probably specific to my particular keyboard model
        // there's almost certainly a better way, but this Works(tm)
        //
        // tbe colors are more or less completely eyeballed since the
        // color accuracy of this keyboard is garbage
        if (y <= 13) rgb_matrix_set_color(i, 0x00, 0x00, 0x00);
        else if (y <= 26) rgb_matrix_set_color(i, 0x2B, 0x9E, 0xFA);
        else if (y <= 38) rgb_matrix_set_color(i, 0xF0, 0x19, 0x18);
        else if (y <= 51) rgb_matrix_set_color(i, 0xFF, 0xFF, 0xFF);
        else if (y <= 63) rgb_matrix_set_color(i, 0xF5, 0x19, 0x18);
        else rgb_matrix_set_color(i, 0x2B, 0x9E, 0xFA);
    }
    return rgb_matrix_check_finished_leds(led_max);
}

#endif
```

I then wrote this little command-line thing to let me change the keyboard color on the fly.

```rs
use hidapi::HidApi;
use std::env;

fn main() {
    let color = u32::from_str_radix(&env::args().nth(1).expect("specify a color"), 16)
        .expect("failed to parse color");
    
    let api = HidApi::new().unwrap();
    let device = api
        .device_list()
        .find(|device|
        // this is... less than great
        device.vendor_id() == 0x3434 &&
            device.product_id() == 0x0510 &&
            device.usage_page() == 0xFF60 &&
            device.usage() == 0x61
        ).expect("failed to find keyboard")
        .open_device(&api)
        .expect("failed to open device");
    
    let mut data = [0u8; 32]; // must be 32 bytes
    data[1..4].copy_from_slice(&color.to_be_bytes()[1..4]);
    device.write(&data).expect("failed to set color");
}
```

It works! Yay!

I'm actually surprised it was that straightforward; when the QMK docs first mentioned using C and flashing
custom firmware on the keyboard I expected it to be a *lot* worse than it actually ended up being.

# Conclusion
This was a bit of an adventure.
I only barely scratched the surface when it comes to QMK, but it's still super cool to see something like this actually *work*.

There's clearly a lot more you can do with custom firmware, especially considering HID communication.
I can imagine someone doing all sorts of cool things with different keys lighting up to signal different things.
Maybe a push-to-talk key (for e.g. Discord) with a colorful status indicator?

I'm curious what kind of crazy stuff people have come up with.

