This project was a custom keyboard that I put together with bluetooth and RGB lights. It features a GH60 PCB and an Adafruit bluefruit board for the bluetooth and lighting effects. The switches are Kailh brown switches, and they're quite clicky and bottom out when typing. I used the NeoPixel LEDs built into the Adafruit board for RGB backlighting, and bit-banged UART to communicate from the atmega32 microcontroller built into the GH60 board. The keyboard firmware itself is running QMK with a customized keymap.c file.
This is a video of me testing out the keyboard in a mode that changes the colour of the lights every time a key is pressed. There are various different lighting modes, and you can easily modify it from the Python code.
Describing the entire build is a bit beyond the scope of this, but you can buy a GH60 board here and follow any tutorial to choose your preferred switches and solder everything together and flash the firmware. I also used lubricant on the stabilizers that I got for my switches which helps with noise and makes them feel more fluid.
Here is the code for the adafruit chip in circuitpython (Adafruit's fork of micropython). It works by reading keycodes over UART, as well as special bytes that signify which mode it should be in (bluetooth or regular mode) and RGB lighting modes.
import board
import busio
import digitalio
import adafruit_hid as hid
import adafruit_ble
from adafruit_ble.advertising import Advertisement
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
from adafruit_ble.services.standard.device_info import DeviceInfoService
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
from adafruit_circuitplayground import cp
from time import sleep
from random import randint
def wheel(pos):
# Input a value 0 to 255 to get a color value.
# The colours are a transition r - g - b - back to r.
if pos < 0 or pos > 255:
r = g = b = 0
elif pos < 85:
r = int(pos * 3)
g = int(255 - pos * 3)
b = 0
elif pos < 170:
pos -= 85
r = int(255 - pos * 3)
g = 0
b = int(pos * 3)
else:
pos -= 170
r = 0
g = int(pos * 3)
b = int(255 - pos * 3)
return (r, g, b)
# defined in keymap.c
Keycode.ENABLE_RGB = 0x5da6
Keycode.BRIGHTNESS_UP = 0x5da7
Keycode.BRIGHTNESS_DOWN = 0x5da8
Keycode.SWITCH_RGB_MODE = 0x5da9
# Custom codes
FUNCTION_KEY_ON = b'\xf2'
FUNCTION_KEY_OFF = b'\xf1'
BT_ON = b'\xff'
BT_OFF = b'\xfe'
special_codes = {
BT_ON,
BT_OFF,
FUNCTION_KEY_OFF,
FUNCTION_KEY_ON
}
class RGB:
def __init__(self):
self.colour_mode = {
"bluetooth" : False,
"function" : False,
"caps_lock" : False,
"rgb_mode" : False
}
self.rainbow_mode_pos = 0
self.rgb_mode = 0
self.brightness_delta = 0.05
self.pixels = cp.pixels
self.snake_index = 0
self.current_accel = 0
self.rgb_mode_callbacks = [
self.rainbow_next,
self.rainbow_next_random,
self.rainbow_next_snake,
self.rainbow_next_shake,
self.rainbow_next_brightness
]
def rainbow_next(self):
self.rainbow_mode_pos = (self.rainbow_mode_pos + 1) % 255
self.pixels.fill(wheel(self.rainbow_mode_pos))
def rainbow_next_shake(self):
x, y, z = cp.acceleration
self.rainbow_mode_pos = (self.rainbow_mode_pos + abs(z)) % 255
print(f"x = {x}, y = {y}, z = {z}")
self.pixels.fill((0, 0, 0))
print(abs(x) - abs(self.current_accel))
if abs(abs(x) - abs(self.current_accel)) > 0.3:
self.snake_index = (self.snake_index + 1) % 10
self.pixels[self.snake_index] = wheel(self.rainbow_mode_pos)
self.pixels.brightness = 1.0
self.current_accel = abs(x)
def rainbow_next_snake(self):
self.rainbow_mode_pos = (self.rainbow_mode_pos + 1) % 255
self.pixels.fill((0, 0, 0))
new_colour = wheel(self.rainbow_mode_pos)
self.pixels[self.snake_index] = new_colour
self.pixels[(self.snake_index+1) % 10] = new_colour
self.pixels[(self.snake_index-1) % 10] = new_colour
self.snake_index = (self.snake_index + 1) % 10
self.pixels.brightness = 1.0
def rainbow_next_random(self):
self.rainbow_mode_pos = (self.rainbow_mode_pos + randint(1, 255)) % 255
self.pixels.fill(wheel(self.rainbow_mode_pos))
def rainbow_next_brightness(self):
if (self.pixels.brightness <= abs(self.brightness_delta) or
self.pixels.brightness >= (1 - (abs(self.brightness_delta)) + 0.01)):
self.brightness_delta = -self.brightness_delta
self.change_brightness(self.brightness_delta)
self.rainbow_next()
def change_brightness(self, value):
new_brightness = self.pixels.brightness + value
if new_brightness <= 1.0 and new_brightness >= 0:
self.pixels.brightness += value
def transition(self, data, modifiers): # either actual keycode or custom code
if data == Keycode.BRIGHTNESS_UP:
print("Brightness up")
self.change_brightness(0.1)
elif data == Keycode.BRIGHTNESS_DOWN:
print("Brightness down")
self.change_brightness(-0.1)
elif data == Keycode.CAPS_LOCK:
self.colour_mode["caps_lock"] = not self.colour_mode["caps_lock"]
elif data == Keycode.ENABLE_RGB:
self.colour_mode["rgb_mode"] = not self.colour_mode["rgb_mode"]
elif data == Keycode.SWITCH_RGB_MODE:
if self.colour_mode["rgb_mode"]:
self.rgb_mode = (self.rgb_mode + 1) % len(self.rgb_mode_callbacks)
elif data == BT_ON:
modifiers.mode = "bt"
self.colour_mode["bluetooth"] = True
self.colour_mode["caps_lock"] = modifiers[Keycode.CAPS_LOCK]
self.colour_mode["rgb_mode"] = modifiers[Keycode.ENABLE_RGB]
modifiers.sync()
self.show_colour()
print("bt_on, caps_lock = ", modifiers[Keycode.CAPS_LOCK])
elif data == BT_OFF:
modifiers.mode = "kb"
self.colour_mode["bluetooth"] = False
self.colour_mode["caps_lock"] = modifiers[Keycode.CAPS_LOCK]
self.colour_mode["rgb_mode"] = modifiers[Keycode.ENABLE_RGB]
modifiers.sync()
self.show_colour()
elif data == FUNCTION_KEY_OFF:
self.colour_mode["function"] = False
elif data == FUNCTION_KEY_ON:
self.colour_mode["function"] = True
else:
pass
def show_colour(self):
if self.colour_mode["caps_lock"]:
self.pixels.fill((255, 0, 0))
elif self.colour_mode["function"]:
self.pixels.fill((0, 255, 0))
elif self.colour_mode["bluetooth"]:
self.pixels.fill((0, 0, 255))
elif self.colour_mode["rgb_mode"]:
self.rgb_mode_callbacks[self.rgb_mode]()
else:
self.pixels.fill((0, 0, 0))
self.pixels.show()
rgb = RGB()
# Bluetooth
hid = HIDService()
device_info = DeviceInfoService(software_revision=adafruit_ble.__version__,
manufacturer="Wesley Kerfoot")
advertisement = ProvideServicesAdvertisement(hid)
advertisement.appearance = 961
scan_response = Advertisement()
scan_response.complete_name = "Sheogorath"
ble = adafruit_ble.BLERadio()
if not ble.connected:
print("advertising")
ble.start_advertising(advertisement, scan_response)
else:
print("already connected")
print(ble.connections)
k = Keyboard(hid.devices)
kl = KeyboardLayoutUS(k)
# UART code
uart = busio.UART(tx=None, rx=board.RX, baudrate=9600)
print("Keyboard is started")
class Modifiers:
def __init__(self, mode="kb"):
self.bt_modifiers = self.empty_modifiers()
self.kb_modifiers = self.empty_modifiers()
self.mode_modifiers = {
"bt" : self.bt_modifiers,
"kb" : self.kb_modifiers
}
self.mode = mode
def sync(self):
mods = self.mode_modifiers[self.mode]
for kc, v in mods.items():
if not v:
#print(f"releasing {kc}")
k.release(kc)
else:
k.press(kc)
def __setitem__(self, k, v):
mods = self.mode_modifiers[self.mode]
mods[k] = v
def __getitem__(self, k):
return self.mode_modifiers[self.mode][k]
def keys(self):
if self.mode == "bt":
return self.bt_modifiers.keys()
else:
return self.kb_modifiers.keys()
def empty_modifiers(self):
return {
Keycode.LEFT_SHIFT : False,
Keycode.RIGHT_SHIFT : False,
Keycode.LEFT_CONTROL : False,
Keycode.RIGHT_CONTROL : False,
Keycode.LEFT_ALT : False,
Keycode.RIGHT_ALT : False,
Keycode.CAPS_LOCK : False,
Keycode.ENABLE_RGB : False,
Keycode.SWITCH_RGB_MODE : False,
Keycode.BRIGHTNESS_UP : False,
Keycode.BRIGHTNESS_DOWN : False
}
modifiers = Modifiers()
def reset_keyboard():
for kc in modifiers.keys():
modifiers[kc] = False
k.release_all()
current_keycode = []
while True:
assert len(current_keycode) <= 3
data = uart.read(1)
if data != b'' and (not data in special_codes):
if len(current_keycode) == 3:
state_down = bool(current_keycode[2]) # whether it was pressed or released
kc = int.from_bytes(bytes(current_keycode[0:2]), 'little')
if kc < 255 or kc in modifiers.keys(): # Adafruit HID doesn't like keycodes greater than a single byte
# Key was pressed and is a modifier
print(kc)
if state_down and (kc in modifiers.keys()):
if ble.connected and kc < 255:
k.press(kc)
if kc != Keycode.CAPS_LOCK:
modifiers[kc] = True
else:
# caps lock needs to be toggled, since we don't do that on "down" events
modifiers[kc] = not modifiers[kc]
rgb.transition(kc, modifiers)
else:
# Key was pressed and is not a modifier
if state_down:
# Only send keys if bluetooth is connected
if ble.connected and kc < 255:
k.press(kc)
k.release(kc)
else:
# Key was released and is any key
# Only release keys if bluetooth is connected
if ble.connected and kc < 255:
k.release(kc)
# set state to released for all keys except caps lock
if kc in modifiers.keys() and kc != Keycode.CAPS_LOCK:
modifiers[kc] = False
current_keycode = []
current_keycode += data
rgb.transition(data, modifiers)
# Every iteration, update the colour state
rgb.show_colour()
And here is the custom keymap.c for QMK. It uses a bit-banged UART protocol since there were no extra pins available on the board that would allow me to use the built in UART. It does surprisingly well even though it is a very simple implementation that simply waits a certain number of microseconds after transmitting each bit.
#include QMK_KEYBOARD_H
#include "print.h"
#include <stdint.h>
/* The following defines the configuration for the UART driver. */
/*!
* UART BAUD rate. This is the speed that we transmit at. For the 1MHz
* RC clock, 4800 is the maximum speed (out of the "standard" ones) that
* will work. If you're running at 8MHz or with an external crystal, you
* may go higher.
*/
#define UART_BAUD (9600)
#define UART_TX_PORT PORTF /*!< GPIO port register for Tx */
#define UART_TX_DDR DDRF /*!< GPIO direction register for Tx */
#define UART_TX_BIT DDF7 /*!< GPIO pin for Tx */
// Constants for signaling the adafruit chip
#define BT_ON 0xff
#define BT_OFF 0xfe
#define FUNC_ON 0xf2
#define FUNC_OFF 0xf1
static inline void uart_init(void);
static inline void uart_tx(uint8_t);
static inline void uart_tx_str(const char*);
static inline void uart_tx_16(uint16_t);
#define TX_BIT (1 << UART_TX_BIT)
#define BIT_US (1000000/UART_BAUD) /*!< Bit period in microseconds */
enum custom_keycodes {
RGB_MODE = SAFE_RANGE,
BRT_UP,
BRT_DOWN,
SWITCH_RGB_MODE
};
static inline void
uart_init(void) {
UART_TX_DDR |= TX_BIT;
UART_TX_PORT |= TX_BIT;
}
/*!
* Send a raw byte to the UART.
* It is best to keep this function as short as possible
*/
static inline void
uart_tx(uint8_t byte) {
uint8_t mask = 1;
/* Start bit */
// Set the bit for our gpio pin, to the opposite of the current value
// it should actually be 0 though, since 1 will mean it's idle
UART_TX_PORT &= ~TX_BIT; _delay_us(BIT_US);
while(mask) {
if (mask & byte)
// Set it to 1, TX_BIT is always 1 for that bit
UART_TX_PORT |= TX_BIT;
else
// Set it to 0, ~TX_BIT is always 0 for that bit
UART_TX_PORT &= ~TX_BIT;
_delay_us(BIT_US);
// Shift left by 1 bit (i.e. up another power of two)
mask <<= 1;
}
/* Stop bit */
UART_TX_PORT |= TX_BIT; _delay_us(BIT_US);
}
/*!
* Send a text string (in pgmspace) to the UART.
* String shall be null-terminated!
*/
static inline void
uart_tx_str(const char* str) {
char c = pgm_read_byte(str);
while (c) {
str++;
uart_tx(c);
c = pgm_read_byte(str);
}
}
static inline void
uart_tx_key(uint16_t code, char state) {
unsigned char l = code & 0x00ff;
unsigned char h = code >> 8;
//uprintf("l = %u\n", l);
//uprintf("h = %u\n", h);
cli();
uart_tx(l);
uart_tx(h);
uart_tx(state);
sei();
}
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[0] = LAYOUT_all( /* 0: qwerty */
KC_ESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_GRV,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_NO, KC_ENT,
KC_LSFT, TG(2), KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, MO(1),
KC_LCTL, KC_LALT, KC_LALT, KC_SPC, TG(1), KC_RGUI, KC_APP, KC_DELETE
),
[1] = LAYOUT_all( /* 1: fn */
KC_GRV, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, _______, _______,
_______, _______, KC_UP, _______, RGB_MODE, SWITCH_RGB_MODE, _______, _______, _______, KC_PGDN, KC_PGUP, BRT_DOWN, BRT_UP, _______,
_______, KC_LEFT, KC_DOWN, KC_RGHT, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______
),
[2] = LAYOUT_all( /* 2: arrows */
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC_UP, _______,
_______, _______, _______, _______, _______, KC_LEFT, KC_DOWN, KC_RGHT
),
};
void
matrix_scan_user(void) {
uint32_t layer = layer_state;
if (layer & (1<<1)) {
}
if (layer & (1<<2)) {
gh60_poker_leds_on();
gh60_esc_led_on();
} else {
gh60_poker_leds_off();
gh60_esc_led_off();
}
};
void
keyboard_post_init_user(void) {
// Customise these values to desired behaviour
uart_init();
debug_enable=true;
//debug_matrix=true;
debug_keyboard=true;
cli();
uart_tx(0xff);
sei();
}
layer_state_t
layer_state_set_user(layer_state_t state) {
uprintf("code = %u\n", get_highest_layer(state));
switch (get_highest_layer(state)) {
case 0:
print("function keys inactive");
uart_tx(FUNC_OFF); // normal
break;
case 1:
print("function keys active");
uart_tx(FUNC_ON); // function key active
break;
}
return state;
}
bool
process_record_user(uint16_t keycode, keyrecord_t *record) {
static int ctrl_down = 0;
static int shift_down = 0;
static int caps_down = 0;
static int bluetooth_enabled = 0;
static int lights_enabled = 0;
// TODO, use custom keycodes that only exist in layer state 1 for rgb lighting
switch (keycode) {
// Always return true for the function keys, so that the other function can process them
case TG(1):
return true;
// For these modifier keys, we want to keep track of them
case KC_LCTRL:
if (record->event.pressed) {
ctrl_down = record->event.pressed;
}
else {
ctrl_down = !ctrl_down;
}
break;
case KC_LSHIFT:
if (record->event.pressed) {
shift_down = record->event.pressed;
}
else {
shift_down = !shift_down;
}
break;
// Caps lock is a special snowflake so we toggle it instead
case KC_CAPS:
if (record->event.pressed) {
caps_down = !caps_down;
}
break;
// Controls bluetooth
case KC_B:
if (record->event.pressed) {
if (ctrl_down && shift_down) {
bluetooth_enabled = !bluetooth_enabled;
if (bluetooth_enabled) {
uart_tx(BT_ON);
}
else {
uart_tx(BT_OFF);
}
// Ignore no matter wbat state it's in
// release all keys here
clear_keyboard();
return false;
}
}
default:
break;
}
if (keycode == SWITCH_RGB_MODE) {
}
// Send certain keys even if bt is disabled or not.
if (keycode == KC_CAPS ||
keycode == KC_LSHIFT ||
keycode == KC_LCTRL ||
keycode == RGB_MODE ||
keycode == SWITCH_RGB_MODE ||
keycode == BRT_UP ||
keycode == BRT_DOWN) {
if (record->event.pressed) {
uprintf("keycode down = %u\n", keycode);
uart_tx_key(keycode, 1);
}
else {
uart_tx_key(keycode, 0);
}
return !bluetooth_enabled;
}
else if (bluetooth_enabled || lights_enabled) {
if (record->event.pressed) {
uart_tx_key(keycode, 1);
}
else {
uart_tx_key(keycode, 0);
}
return false;
}
else {
// send them anyway but don't return false (meaning it will actually get registered)
if (record->event.pressed) {
uart_tx_key(keycode, 1);
}
else {
uart_tx_key(keycode, 0);
}
}
return true;
}