Hack: saving QMK firmware source to the keyboard

2020-01-18

I wrote a script to save the C source code of my keyboard layout as a macro in the firmware itself, so that if I hit a special key combination, it’ll type a base64 string that can be decoded into the original source.

It’s super quick and dirty - you have to edit a keymap_base.c file in your QMK layout directory, and you have to run a script called typeself.py before building your firmware.

Unfortunately, this probably doesn’t scale very well because it bloats the firmware size significantly. A better solution might be to compress the keymap source code and write it to the microcontroller as data, but that would require understanding QMK firmware and the Arduino Pro Micro much better than I currently do, so I didn’t try it. (If you do, please tell me!)

Also included in this example is a sequence of keys that will curlbash code from the Internet, because I realized I could do it pretty easily. Sort of like what a Rubber Ducky can do, although the Rubber Ducky has more advanced features and can be programmed to send the keys on a timer rather than at the press of a key on a keyboard

Writing the base64 of the keymap source works on any OS. The other hacks and special keys I have defined below are Mac specific, but the concepts could be extended to Windows, Linux, or any other OS that allows control via keyboard.

The keyboard and firmware

Here’s an example keymap_base.c for the SpaceCat Design Launch Pad v2 that I just put together.

My Launch Pad v2

It’s a tiny little 8 key macropad, and it has this key layout:

,-------.
| F | S |
|---+---|
| V | H |
|---+---|
| C | X |
|---+---|
| K | T |
`-------'

However, if you hold the T key, you can get into a function layer, which has these functions on each key:

  • F: open the force quick dialog box
  • V: lock the screen
  • C: type an important phrase I use constantly
  • K: type out the base64 encoded version of the source code
  • S: volume up
  • H: volume down
  • X: mute
  • T: does nothing because you have to be holding it to be in this layer

You can see in the code listing below that there are two placeholder strings, $___keymap and $___typeself. These are Python template strings, which the script replaces with the contents of the keymap_base.c and typeself.py files.

Decoding base64

Once you have hit the proper keys to get the macropad to type out the base64 strings, you have to run that typed base64 back through a base64 decoder, such as Python’s base64.b64decode(); I typically do something like

import base64
keymap_base = "the base64 string"
print(base64.b64decode(keymap_base).decode())

keymap_base.c

#include QMK_KEYBOARD_H

enum {
    LAY_BASE,
    LAY_FUNC,
};

enum mrl_custom_keycodes {
    CKC_NICECAPS = SAFE_RANGE,
    CKC_TYPESELF,
};

// Custom keys
#define MACLOCK LGUI(LCTL(KC_Q))
#define FQUIT LGUI(LALT(KC_ESC))

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [LAY_BASE] = LAYOUT(
        KC_F,         KC_S,
        KC_V,         KC_H,
        KC_C,         KC_X,
        KC_K,         LT(LAY_FUNC, KC_T)
    ),
    [LAY_FUNC] = LAYOUT(
        FQUIT,        KC__VOLD,
        MACLOCK,      KC__VOLD,
        CKC_NICECAPS, KC__MUTE,
        CKC_TYPESELF, KC_TRANSPARENT
    ),
};

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case CKC_NICECAPS:
            if (record->event.pressed) SEND_STRING("Nice keycaps, wanna fvck?");
            return false;
        case CKC_TYPSELF:
            if (record->event.pressed) {
                SEND_STRING("___keymap");
                tap_code(KC_ENTER);
                SEND_STRING("___typeself");
                tap_code(KC_ENTER);
            }
                return false;
        default:
            return true; // Process all other keycodes normally
    }
}

typeself.py

#!/usr/bin/env python3

import base64
import os
import string

thisfile = os.path.realpath(__file__)
thisdir = os.path.dirname(thisfile)
kmbase = os.path.join(thisdir, 'keymap_base.c')
keymap = os.path.join(thisdir, 'keymap.c')

with open(thisfile) as tf:
    thisfile_b64 = base64.b64encode(tf.read().encode())
with open(kmbase) as kmbf:
    kmcontents = kmbf.read()
    kmbase_b64 = base64.b64encode(kmcontents.encode())

templ = string.Template(kmcontents)
keymap_content = templ.safe_substitute({'___keymap': kmbase_b64, '___typeself': thisfile_b64})
with open(keymap, 'w') as kmf:
    kmf.write(keymap_content)