arcnyxx.net.git

qmk.txt

espurr
designing custom backlighting for my keyboard
using @{https://github.com/qmk/qmk_firmware}{qmk} to write custom firmware for my @{https://ergodox-ez.com}{ergodox ez}
blog post about qmk firmware and keyboard backlighting
2024-02-19

i own an ergodox ez, which is a split, %{in terms of keyboards, having key columns arranged without a horizontal offset}{columnar} keyboard. it does an even better job precluding rsi in my wrists than my old single-board split microsoft keyboard did. ergodox ez keyboards also run open-source qmk firmware, meaning they are extensively configurable. i love my keyboard, but everybody laughs at how silly it looks. :(
the firmware i designed for my ergodox ez implements a flashy reactive backlighting & has a keymap & macros designed for @{https://en.wikipedia.org/wiki/C_(programming_language)}{c programming} with @{https://neovim.io}{neovim}.

 !key layout
my layout is basically qwerty but with thumb keys:
 #https://arcnyxx.net/layout.webp layout
escape & underscore are next to space as fairly important keys. escape is used to change %{modal text editors have different modes, one of which is text input, and switching between them efficiently is important}{modes} in neovim & i use @{https://en.wikipedia.org/wiki/Snake_case}{snake case} for variables & types in c. that key with @{https://en.wikipedia.org/wiki/Tux_(mascot)}{tux} on it is equivalent to the windows key.
my function keys use qmk's @{https://github.com/qmk/qmk_firmware/blob/master/docs/feature_tap_dance.md}{tap dance} functionality. in my case, pressing a function key twice or thrice quickly will output a different function key. that way, fewer keys are used for uncommon keycodes.
opposite the underscore key is a macro. c uses `\c->` for accessing structure members via pointers, which is a pretty common operation. when i press the macro key, the `\cMACRO` keycode is sent, and my function uses `\cSEND_STRING()` to type `\c->`.
 `\c	switch (keycode) {
 `	case KC_BSPC:
 `		if (last)
 `			unregister_code(KC_BSPC), register_code(KC_BSPC);
 `		break;
 `	case MACRO:
 `		SEND_STRING(SS_TAP(X_MINUS) SS_LSFT(SS_TAP(X_DOT)));
 `		break;
 `	case DISCO:
 `		disco = !disco, lkeys = rkeys = 0;
 `	}
 `	last = (keycode == MACRO);
this function looks for two other keys: backspace & `\cDISCO`. when backspace is pressed, it registers the key an extra time if `\cMACRO` was the last keycode sent, completely erasing the `\c->` generated by the macro. the `\cDISCO` keycode intuitively toggles disco mode.

 !disco mode
solid or breathing backlighting is pedestrian, so i wrote my own, more reactive backlighting. it works like this, with both halves of the keyboard work independently of the other:
 -every key press changes the backlight colour
 -when no more keys are pressed, the backlights fade out
 -backlight LEDs further from the key pressed are dimmer than those close to it
those first two points are easy to implement: just keep a counter for both sides that track how many keys are pressed. the last point is a bit more complicated. here is the function that is triggered after every key press:
 `\cvoid
 `post_process_record_user(uint16_t keycode, keyrecord_t *record)
 `{
 `	if (!disco)
 `		return;
 `
 `	if (!record->event.pressed) {
 `		if (record->event.key.row < 7 && lkeys > 0)
 `			--lkeys;
 `		else if (record->event.key.row >= 7 && rkeys > 0)
 `			--rkeys;
 `		return;
 `	}
 `
 `	if (record->event.key.row < 7) {
 `		lhue = rand() % 255, lval = 255, ++lkeys, lmid = 15;
 `		if (record->event.key.col != 5)
 `			lmid = 28 - 2 * record->event.key.row;
 `		for (uint8_t i = RGBLED_NUM / 2; i < RGBLED_NUM; ++i)
 `			sethsv(lhue, 255, BASE(lval, i, lmid), &led[i]);
 `	} else {
 `		rhue = rand() % 255, rval = 255, ++rkeys, rmid = 14;
 `		if (record->event.key.col != 5)
 `			rmid = 27 - 2 * record->event.key.row;
 `		for (uint8_t i = 0; i < RGBLED_NUM / 2; ++i)
 `			sethsv(rhue, 255, BASE(rval, i, rmid), &led[i]);
 `	}
 `	rgblight_set();
 `}
the first `\cif` block returns if disco mode is inactive so the backlights fade & turn off. the second `\cif` block decrements the key counter for either side when a key on that side is released by checking the row number.
the final `\cif` block has an `\celse` block that operates for the opposite side of the keyboard with different variables, but they both do the same thing. the hue is set to one of 256 colours randomly with `\clhue = rand() % 255`, the intensity is maxed with `\clval = 255`, the number of keys pressed is incremented, and the `\clmid` variable is set to a default value.
that default value represents the index of the LED closest to the middle of the keyboard. the thumb keys have weird column numbers, so an `\cif` block checks if the key pressed was not a thumb key. if so, it changes the index to that of an LED under the row of the key which was pressed.
finally, the function iterates through all the LEDs on each side, setting their values. another function, which has the same loops, runs every 20 milliseconds to fade out the backlights when no keys are pressed on a side.
but what about the last point? what does that `\cBASE()` macro do? well...
 ?cubic fade & distance scaling
the human perception of change in light intensity follows a @{https://en.wikipedia.org/wiki/Weber–Fechner_law}{logarithmic scale}. i approximate this with a cubic function (|<code>f(x) = x<sup>3</sup> ÷ 255<sup>2</sup></code>|) in the following macro:
 `\c#define BASE(val, ind, mid) ((uint32_t)(val) * (val) * (val) / 255 / 255) *   \
 `	5 / (((ind) > (mid) ? (ind) - (mid) : (mid) - (ind)) + 5)
the `\cval` argument of `\cBASE()` is the linearly decremented brightness. the maximum intensity is `\c255`, so i modify the invariant points of my cubic function by dividing |<code>x<sup>3</sup></code> by <code>255<sup>2</sup></code>|.
the second line of `\cBASE()` deals with the other two arguments: `\cind`, which is the index of the LED whose brightness is being calculated, and `\cmid`, which is the index of the LED closest to the key which was pressed. dividing the brightness by their difference dims LEDs the further they are from the key pressed.
the constant value `\c5` controls the magnitude of the decrease. a smaller number would allow greater changes because the difference of `\cind` & `\cmid` would have a greater impact on the fraction `a ÷ ax` (where `a` is the constant & `x` is the difference).
 !result
with some simple counters & some slightly involved maths, the backlights react to key presses on each side based on which key was pressed and fade nicely when all keys are released.
here is the firmware in action:
 ^https://arcnyxx.net/keyboard.mp4 typing with keyboard backlighting