Skip to content

Instantly share code, notes, and snippets.

@Nuigurumi777
Last active February 15, 2024 03:16
Show Gist options
  • Save Nuigurumi777/47788978b556d1ce258d83f60578a26c to your computer and use it in GitHub Desktop.
Save Nuigurumi777/47788978b556d1ce258d83f60578a26c to your computer and use it in GitHub Desktop.
An example of sending layer changing commands from Windows host to Moonlander keyboard

Build the firmware according to this: https://beta.docs.qmk.fm/using-qmk/software-features/feature_rawhid Add the content of keymap.c to the end of your keymap.c

Install pyhidapi: https://pypi.org/project/hid/

Install hidapi. Download binary release: https://github.com/libusb/hidapi/releases Copy the appropriate hidapi.dll into Windows\system32, reboot

Run testhid.py, it will change layers back and forth several times (noticeable if they have different light color)

Run autolayer.py to automatically change layers depending on the active window's input language (Windows-specific, English and Russian hardcoded, unoptimized - should install a global hook on foreground window change instead of polling in an infinite loop...

UPD: ... or not? there seem to be no such hook in Windows, you're supposed to install one for WH_CALLWNDPROC and then check if the message was WM_ACTIVATE - as there should be a shitload of window messages sent every moment, this may end up wasting way more resources than an infinite loop. Would WH_SHELL with HSHELL_LANGUAGE work for this case?).

import ctypes
import hid, sys
import time
user32 = ctypes.windll.user32
def get_active_language():
h_wnd = user32.GetForegroundWindow()
thread_id = user32.GetWindowThreadProcessId(h_wnd, 0)
klid = user32.GetKeyboardLayout(thread_id)
lid = klid & (2**16 - 1)
return lid
vendor_id = 0x3297 # Found in config.h for the keyboard's main directory
product_id = 0x1969 # (one level above "keymaps")
usage_page = 0xFF60 # The defaults, can be redefined in the same config.h
usage = 0x61
path = None # Found with the code below
for d in hid.enumerate(vendor_id, product_id):
if d["usage_page"] == usage_page and d["usage"] == usage:
path = d["path"]
if path is None:
print("Couldn't connect")
sys.exit(0)
# English, Russian (http://atpad.sourceforge.net/languages-ids.txt)
lids = {0x409: b'\0\1', 0x419: b'\0\2'}
LID = None
while True:
lid = get_active_language()
if (lid != LID) and (lid in lids.keys()):
LID = lid
#print(lid)
with hid.Device(vendor_id, product_id, path = path) as device:
device.write(lids[lid])
time.sleep(0.5)
enum layer_cmds
{
// define our "commands", can be anything
LAYER_SET_1 = 1,
LAYER_SET_2,
};
// and example of the data receiving function for the firmware
void raw_hid_receive(uint8_t *data, uint8_t length)
{
if (length == 0) return;
uint8_t i = 0;
uint8_t cmd = data[i++]; // the number we received
switch(cmd)
{
// LAYER1 is on at all times, switch LAYER2
// on and off on top of that
case LAYER_SET_1:
if (IS_LAYER_ON(LAYER2)) layer_off(LAYER2);
break;
case LAYER_SET_2:
if (IS_LAYER_OFF(LAYER2)) layer_on(LAYER2);
break;
}
}
# This will try to connect to the Moonlander keyboard and
# send it numbers 1 and 2 several times with 0.5 sec interval.
# raw_hid_receive will interpret them as layer changing commands
# and blink (assuming layers 1 and 2 have different colors)
import hid
import sys
import time
# These are probably just for Moonlander,
# should be changed for other devices
vendor_id = 0x3297 # Found in config.h for the keyboard's main directory
product_id = 0x1969 # (one level above "keymaps")
usage_page = 0xFF60 # The defaults, can be redefined in the same config.h
usage = 0x61
path = None # Found with the code below
for d in hid.enumerate(vendor_id, product_id):
if d["usage_page"] == usage_page and d["usage"] == usage:
path = d["path"]
if path is None:
print("Couldn't connect")
sys.exit(0)
# Have to specify the path, because there are several "usage pages" and
# if we choose wrong one we get access denied error
with hid.Device(vendor_id, product_id, path = path) as device:
codes = [b'\0\1', b'\0\2']
for i in range(10):
device.write(codes[i % 2])
time.sleep(0.5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment