QMK Oled — Displaying currently playing media data

Retro
5 min readFeb 12, 2023

--

My Corne

QMK is a very powerful tool for making your keyboard your own, and here I’ll be describing how to hack into the oled on keyboards such as Corne to show some useful data.

Clock, currently playing media (title and artist) and current layer

Firstly, we will be using the Windows.Media.Control library for fetching the currently playing media data. Next, we will define some functions in our keymap.cfile on the QMK side to enable communication between the keyboard and our application running on the computer.

Computer

I have running on my computer an instance of KeybKontroller, an electron application I have made to send data to the keyboard over HID protocol. You can find this application here.

In KeybKontroller we make a connection with our keyboard so that we can start sending data to it. KeybKontroller also runs a nodeJS server so that different programs can also send data to it.

updateKeyboard method sends data to the connected keyboard. You can send 3 different kinds of messages.
1. Trigger a function
2. Trigger a function with single int value
3. Trigger a function with array of int values

The first index being sent is not read over on qmk side, so it does not matter.
10 designates a message from KeybKontroller.
value is the function that we wish to trigger. value corresponds to various cases inside a switch statement inside our keymap.
extraValues can be an int or array of ints, depending on usecase

exports.updateKeyboard = (value, extraValues=0) => {
try {
if (!keyboard) {
connectKeyboard();
}
if (extraValues == null) {
extraValues = 0
}
if (typeof extraValues === "object") {
let data_sent = [1, 10, value].concat(extraValues);
keyboard.write(data_sent);
} else {
keyboard.write([1, 10, value, extraValues]);
}
return 1;
} catch (ex) {
logger.error("Error in update keyboard \n" + ex.toString());
this.resetKeyboard();
return 0;
}
}

We run an instance of a C# program (chosen due to ease of access to windows library, I couldn’t get node-gyp to work) which fetches information such as the title of the focused window (useful in determining the OS when running KVM software) and currently playing media. This is automatically ran with KeybKontroller as a shell.

Media data is sent to nodeJS server in the /updateCurrentMedia where it is formatted in the formatMediaInfo function and then sent to the keyboard.

app.post("/updateCurrentMedia", (req, res) => {
const currentMediaTitle = req.body.currentMediaTitle.toUpperCase().replace(/[^A-Z ]/g, "");
const currentArtist = req.body.currentArtist.toUpperCase().replace(/[^A-Z ]/g, "");
// console.log(currentArtist);
const [mediaTitleArray, mediaArtistArray] = formatMediaInfo(
currentMediaTitle,
currentArtist
);
keyboardQmk.updateKeyboard(6, mediaTitleArray);
keyboardQmk.updateKeyboard(7, mediaArtistArray);
res.send("received");
});

const formatMediaInfo = (currentMediaTitle, currentArtist) => {
// remove artist name from title
currentMediaTitle = currentMediaTitle.replace(currentArtist, "");
currentMediaTitle = currentMediaTitle.trim();
let mediaTitleArray = [...currentMediaTitle].map(i => constants.CHAR_TO_CODE_MAPPING[i]);
mediaTitleArray = mediaTitleArray.slice(0, 21);

let mediaArtistArray = [...currentArtist].map((i) => constants.CHAR_TO_CODE_MAPPING[i]);
mediaArtistArray = mediaArtistArray.slice(0, 21);

return [mediaTitleArray, mediaArtistArray]
}

We also have formatDateTime function to format current date and time and send over to the board. This method is called almost every second using a timer defined in.

const formatDateTime = () => {
let now = new Date();
let hours = now.getHours() > 9 ? now.getHours() : "0" + now.getHours();
let minutes = now.getMinutes() > 9 ? now.getMinutes() : "0" + now.getMinutes();
let seconds = now.getSeconds() > 9 ? now.getSeconds() : "0" + now.getSeconds();
let formattedDate = now.toShortFormat() + ` ${hours}:${minutes}:${seconds}`;
let formattedDateArray = [...formattedDate].map(i => constants.CHAR_TO_CODE_MAPPING[i]);
return formattedDateArray;
}

Keyboard

We implement the raw_hid_receive function in our keymap along with setting RAW_ENABLE = yes in rules.mk file.

Case 6 and 7 correspond to media data, where current_title_code and current_artist_code arrays are filled with relevant codes according to code_to_name array.

int current_title_code[21];
int current_artist_code[21];
int time_code[21];

const char code_to_name[60] = {
' ', ' ', ' ', ' ', 'A', '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',
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'R', 'E', 'B', 'T', '_', '-', '=', '[', ']', '\\',
'#', ';', '\'', '`', ',', '.', '/', ':', ' ', ' '
};

void raw_hid_receive(uint8_t *data, uint8_t length) {
// printf("getting raw hid data %d %d %d\n", data[0], data[1], data[2]);
// print("raw hid\n");
int i = 0;
switch(*data) {
case 1:
// test_rgb_value(255, 0, 0);
break;
case 10:
switch(data[1]) {
case 1:
// update_encoder_state();
break;
case 2:
send_keyboard_state();
break;
case 3:
// set_cpu_usage_rgb(da );
break;
case 4:
// update_os_state(data[2]);
break;
case 5:
// test_rgb_value(data[2], data[3], data[4]);
break;
case 6:
for (i = 2; i < length; i++) {
if (data[i] == 0) {
current_title_code[i - 2] = 0;
break;
}
current_title_code[i - 2] = data[i];
}
media_updated = true;
break;
case 7:
for (i = 2; i < length; i++) {
if (data[i] == 0) {
current_artist_code[i - 2] = 0;
break;
}
current_artist_code[i - 2] = data[i];
}
media_updated = true;
break;
case 8:
for (i = 2; i < length; i++) {
if (data[i] == 0) {
time_code[i - 2] = 0;
break;
}
time_code[i - 2] = data[i];
}
clock_updated = true;
break;
}
break;
}
}

We also define 3 oled functions, one for each kind of oled data — layer, clock and media.

enum oled_states {
OLED_CLOCK,
OLED_MEDIA,
OLED_LAYER,
_OLED_STATE_RANGE
};

void oled_render_layer_state(void) {
switch (biton32(layer_state)) {
case _BASE:
oled_write_ln_P(PSTR("Layer: BASE"), false);
break;
case _SYMB:
oled_write_ln_P(PSTR("Layer: SYMB"), false);
break;
case _NUMP:
oled_write_ln_P(PSTR("Layer: NUMP"), false);
break;
}
}

void oled_render_clock(void) {
bool skip = false;
skip = false;
for (int i = 0; i < 19; i++) {
if (time_code[i] == 0 || skip) {
skip = true;
oled_write_char(code_to_name[0], false);
} else {
oled_write_char(code_to_name[time_code[i]], false);
}
}
}

void oled_render_media(void) {
bool skip = false;
for (int i = 0; i < 21; i++) {
if (current_title_code[i] == 0 || skip) {
skip = true;
oled_write_char(code_to_name[0], false);
} else {
oled_write_char(code_to_name[current_title_code[i]], false);
}
}

oled_advance_page(false);

skip = false;
for (int i = 0; i < 21; i++) {
if (current_artist_code[i] == 0 || skip) {
skip = true;
oled_write_char(code_to_name[0], false);
} else {
oled_write_char(code_to_name[current_artist_code[i]], false);
}
}
}

// Used to draw on to the oled screen
bool oled_task_user(void) {
if (!is_keyboard_master()) {
//
} else {
if (reset_oled) {
oled_clear();
reset_oled = false;
}
switch (oled_state) {
case OLED_CLOCK:
oled_render_clock();
break;
case OLED_MEDIA:
oled_render_media();
break;
case OLED_LAYER:
oled_render_layer_state();
break;
default:
oled_render_clock();
}
}
return false;
}

The oled_task_user method decides which oled data should be shown based on oled_state variable, which is cycled in the process_record_user method.

enum custom_keycodes {
KC_OLED = SAFE_RANGE // add this keycode to your keymap, this will change oled state
};

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case KC_OLED:
if (record->event.pressed) {
if (oled_state == _OLED_STATE_RANGE - 1) {
oled_state = 0;
} else {
oled_state = oled_state + 1 % _OLED_STATE_RANGE;
}
reset_oled = true;
return false;
} else {
return true;
}
default:
return true;
}
}

And that’s it, this is all you need to run to get some cool stuff on your keyboard. Feel free to reach me in on discord, retrogeek46#4047.

--

--

Retro

Video games are one of humanities greatest forms of artistic expression.