Skip to content

Instantly share code, notes, and snippets.

@DanielGibson
Last active November 27, 2023 17:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielGibson/cd92c5faa6b2e0426351dc44193c6182 to your computer and use it in GitHub Desktop.
Save DanielGibson/cd92c5faa6b2e0426351dc44193c6182 to your computer and use it in GitHub Desktop.
Hacky Linux tool to explore Pulsar PCMK TKL Keyboard LED control (incl. documentation of that protocol)
/* Based on https://github.com/torvalds/linux/blob/master/samples/hidraw/hid-example.c
*
* This is for "0416:b23c Winbond Electronics Corp. PCMK TKL"
* USB VID 0x0416, PID 0xb23c, using a Winbond/Nuvoton Chip (NUC121SC2AE),
* sometimes also identified as Winbond "Gaming Keyboard",
* My actual device is a Pulsar PCMK TKL Barebone in ISO layout, but reportedly
* there are other devices with the same USB ID, like "KT108" or some from "WIANXP"
* that *might* use the same protocol, see also https://usb-ids.gowdy.us/read/UD/0416/b23c
*
* -------------------------
*
* Hidraw Userspace Example
*
* Copyright (c) 2010 Alan Ott <alan@signal11.us>
* Copyright (c) 2010 Signal 11 Software
*
* The code may be used by anyone for any purpose,
* and can serve as a starting point for developing
* applications using hidraw.
*/
/* Linux */
#include <linux/types.h>
#include <linux/input.h>
#include <linux/hidraw.h>
/*
* Ugly hack to work around failing compilation on systems that don't
* yet populate new version of hidraw.h to userspace.
*/
#ifndef HIDIOCSFEATURE
#warning Please have your distro update the userspace kernel headers
#define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len)
#define HIDIOCGFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x07, len)
#endif
/* Unix */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
/* C */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
const char *bus_str(int bus);
static void setmode(int devFD, bool logo, char effect, char brightness, char speed, char direction, bool fullColor,
unsigned char fgR, unsigned char fgG, unsigned char fgB,
unsigned char bgR, unsigned char bgG, unsigned char bgB)
{
// RI: Report ID (1)
// C: Command (7 to set effects or static color for keys, 8 for logo effects)
// EF: Effect number (0: static, 1: breathe, 2: wave, 3: neon, 4: logo-only! neon, 5: snake,
// 6: reactive, 7: aurora, 8: ripple, 10: custom (see setKeyColors()), 100: musical rhythm)
// => what about 4? "twinkle"? what about "flashing", "reactice", "speed replond"? 9-11?
// TODO: document which effects support which directions
// BR: Brightness (0-4)
// SP: Speed (0-4)
// RT, GT, BT: Red/Green/Blue TOP/foreground color
// RB, GB, BB: Red/Green/Blue BOTTOM/background color
// DR: Effect Direction (0: right, 1: left, 2: up, 3: down, 4: to outside, 5: to inside,
// 6: clockwise, 7: counter-clockwise)
// FC: "Full Color" (seems to use all rainbow colors instead of just TOP/BG color, or something like that)
// RI C ?? EF BR SP RT GT BT RB GB BB DR FC
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 01 07 00 00 00 0e 00 03 03 00 ff 00 00 00 00 00 00 // static green, brightness 3/4
// 01 07 00 00 00 0e 03 02 01 ff 00 00 00 00 00 00 01 // neon, bright 2, speed 1, red/black, full color
// 01 07 00 00 00 0e 03 02 04 ff 00 00 00 00 ff 00 00 // neon, bright 2, speed 4, red/blue, full color?
// 01 07 00 00 00 0e 01 03 02 ff 00 00 ff ff 00 00 01 // breathe, bright 3, speed 3?, red/yellow, full color
// 01 07 00 00 00 0e 01 03 02 ff 00 00 ff ff 00 00 00 // breathe, bright 3, speed 3?, red/yellow, NOT full color
// 01 07 00 00 00 0e 02 04 02 ff 00 00 00 00 00 01 01 // wave full color, top red bottom black, bright 4, speed 2, dir left
// 01 07 00 00 00 0e 02 04 02 ff 00 00 00 00 00 00 01 // wave full color, top red bottom black, bright 4, speed 2, dir right
// 01 07 00 00 00 0e 02 04 02 ff 00 00 00 00 00 02 00 // wave, NOT full color, bright 4 speed 2, top red, bottom black, dir up
// 01 07 00 00 00 0e 05 03 02 ff 00 00 00 00 00 06 01 // snake, full color, top red, bottom black, bright 3 speed 2, clockwise
// 01 07 00 00 00 0e 07 03 04 ff 00 00 00 00 00 04 01 // aurora, bright 3 speed 4 full color, red/black dir inner to outer
// 01 07 00 00 00 0e 08 01 03 00 00 ff 00 00 00 04 01 // ripple, full color, blue/black, bright 1 speed 3
// 01 07 00 00 00 0e 08 04 01 00 00 ff ff ff 00 04 01 // ripple, full color, blue/yellow, bright 4 speed 1 (always yellow, ripples when pressed => bg color used)
// 01 07 00 00 00 0e 06 02 03 ff 00 00 00 00 ff 00 01 // reactive, bright 2 speed 3 full color, red/blue
// 01 07 00 00 00 0e 64 03 03 ff 00 00 00 00 00 02 01 // musical rythm, full color, red/black, bright 3 (didn't seem to work? or maybe requires the host to send data about the music?)
// 01 07 00 00 00 0e 0a 03 03 00 00 ff 00 00 00 00 01 // this is sent after all the "set individual key colors" messages (report ID 9)
// RI C ?? EF BR SP RT GT BT RB GB BB DR FC
// 01 08 00 00 00 0d 00 00 03 00 00 ff 00 00 00 00 00 // logo light static blue bright 0
// 01 08 00 00 00 0d 00 04 03 ff ff 00 00 00 00 00 00 // logo light static yellow bright 4
// 01 08 00 00 00 0d 00 01 03 00 ff 00 00 00 00 00 00 // (logo) static green bright 1
// 01 08 00 00 00 0d 04 04 03 ff ff ff 00 00 00 06 01 // logo light neon, full color, white/black, bright 4 speed 3
// 01 08 00 00 00 0d 04 01 04 00 00 ff ff ff 00 06 00 // logo light neon, NO full color, blue/yellow, bright 1 speed 4
// 01 08 00 00 00 0d 01 04 03 ff 00 00 00 00 00 00 01 // breathe full color red/black, bright 4 speed 3
// 01 08 00 00 00 0d 02 03 04 ff 00 00 00 00 00 00 01 // wave full color red/black bright 3 speed 4
// TODO: make sure brightness, speed etc have valid values, esp. for current mode?
// NOTE: all HID messages in this protocol have 64 bytes (Report ID + 63 bytes for this custom protocol)
// for these "set effect mode" messages, all bytes after byte 16 (FC) are 0
unsigned char buf[64] = {
1, logo ? 8 : 7, // byte 0: ReportID; byte 1: command
0, 0, 0, // bytes 2,3,4 are always 0 here
logo ? 0x0d : 0x0e, // byte 5, whatever this is, for setting the logo it's 0x0d, for normal keys 0x0e
effect, brightness, speed, // bytes 6,7,8
fgR, fgG, fgB, // bytes 9,10,11 are foreground red/green/blue
bgR, bgG, bgB, // bytes 12,13,14 are background red/green/blue
direction, // byte 15
fullColor ? 0x01 : 0x00, // byte 16
0 // the remaining bytes in buf are implicitly zero
};
int res = write(devFD, buf, 64);
if (res < 0) {
printf("Error: %d\n", errno);
perror("write");
} else {
printf("write() wrote %d bytes\n", res);
}
}
static void setKeyColors(int fd, int messageIndex, int keyIndex)
{
// this consists of multiple HID messages, each 64 bytes.
// 8 messages that start with "01 09" and, at the end, one that starts with "01 07"
// furthermore, for some reason, color bytes only go up to 0xc1, instead of 0xff.
// the first 7 messages look like this:
// 0 1 2 3 4 5 6
// 01 09 00 00 NN 36 <RGB-Data for 0x36/3 = 54/3 = 18 keys, 3 bytes each>...
// 01 is the endpoint, like in setmode() it's always 1, 09 is the command, 9 in this case.
// NN is 00 in the first message, 01 in the second, 03 in the third, etc
// and byte 5 is the number of following bytes with RGB data (the message is still always 64 bytes)
// message 7 is very similar:
// 01 09 00 00 07 12 <RGB-Data for 0x12/3 = 18/3 = 6 keys>
// except, as indicated by byte 5 (0x12), only for the remaining 6 keys
for(int i=0; i<8; ++i) {
unsigned char msg[64] = {
1, 9, 0, 0, i, (i==7) ? 0x12 : 0x36,
0 // the remaining byte will be automatically set to 0
};
// TODO: could memsset the remaining message to 0xC1 so everything is white by default (no everything is off)
if(i == messageIndex) {
int redIdx = keyIndex * 3;
if(redIdx >= msg[5]) { // 0x36 or 0x12
printf("Invalid keyIndex!\n");
continue;
}
redIdx += 6; // start at byte 6
msg[redIdx+0] = 0; // R
msg[redIdx+1] = 0; // G
msg[redIdx+2] = 0xC1; // B
}
int res = write(fd, msg, 64);
if (res < 0) {
printf("Error while writing message %d: %d\n", i, errno);
perror("write");
} else {
printf("write() wrote %d bytes\n", res);
}
}
// Message 0: 0 = Esc, ??, F1, F2, F3, F4, ??, F5, F6, F7, F8, F9, F10, F11, F12, 15 = Print, ScrollLock, 17 = Pause (=> voll)
// Message 1: 0-3 = ?? (could be multimedia keys above numblock, or status LEDs), 4 = ^, 1, 2, 3, 4, 5, 10 = 6, 7, 8, 9, 0, ß, 16 = ´, 17 = ??
// Message 2: 0 = Backspace, Ins, Pos1, 3 = PgUp, 4-7 = ?? (NumLock, Num/, Num*, Num- ?), 8 = Tab, Q, W, E, R, T, ..., 16 = I, 17 = O
// Message 3: 0 = P, Ü, +, ??, ??, 5 = Del, End, 7 = PgDown, 8-11 = ?? (Num7, Num8, Num9, Num+?), 12 = CapsLock, ??, 14 = A, S, D, F
// Message 4: 0 = G, H, J, K, L, Ö, Ä, 7 = #, 8 = Return, 9 - 15 = ?? (maybe 12-15 are Num4,5,6 and Num+?), 16 = Shift, 17 = <
// Message 5: 0 = Y, X, C, V, B, N, M, ',', . , 9 = -, ??, ??, 12 = Shift, ??, 14 = Up, 15-17 ?? (maybe 16 and 17 are Num1, Num2, and in next msg Num3, NumEnter?)
// Message 6: ??, ??, 2 = Ctrl, Win, 4 = Alt, .. ?? .., 8 = Space, .. ?? .., 12 = AltGr, Fn, Menu, 15 = Ctrl, ??, 17 = Left
// Message 7: 0 = Down, 1 = Right, 2-5 = ?? (maybe 2 or 3 = Num0, 4 = NumDecimal, 5 = NumEnter ?)
// the last message looks like this:
// RI C ?? EF BR SP RT GT BT RB GB BB DR FC
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 01 07 00 00 00 0e 0a 04 03 80 00 ff 00 00 00 00 01
// => it's basically setmode() with effect=0x0a and fullColor=true (no idea if fullColor is really needed)
// (not sure if the values of bytes 9-11 have any meaning)
char brightness = 3;
setmode(fd, false, 0x0a, brightness, 3, 0, true, 0x80, 0, 0xff, 0, 0, 0);
}
int main(int argc, char **argv)
{
int fd;
int i, res, desc_size = 0;
char buf[256];
struct hidraw_report_descriptor rpt_desc;
struct hidraw_devinfo info;
char *device = "/dev/hidraw0";
if (argc > 1)
device = argv[1];
/* Open the Device with non-blocking reads. In real life,
don't use a hard coded path; use libudev instead. */
fd = open(device, O_RDWR|O_NONBLOCK);
if (fd < 0) {
perror("Unable to open device");
return 1;
}
memset(&rpt_desc, 0x0, sizeof(rpt_desc));
memset(&info, 0x0, sizeof(info));
memset(buf, 0x0, sizeof(buf));
/* Get Report Descriptor Size */
res = ioctl(fd, HIDIOCGRDESCSIZE, &desc_size);
if (res < 0)
perror("HIDIOCGRDESCSIZE");
else
printf("Report Descriptor Size: %d\n", desc_size);
/* Get Report Descriptor */
rpt_desc.size = desc_size;
res = ioctl(fd, HIDIOCGRDESC, &rpt_desc);
if (res < 0) {
perror("HIDIOCGRDESC");
} else {
printf("Report Descriptor:\n");
for (i = 0; i < rpt_desc.size; i++)
printf("%.2hhx ", rpt_desc.value[i]);
puts("\n");
}
/* Get Raw Name */
res = ioctl(fd, HIDIOCGRAWNAME(256), buf);
if (res < 0)
perror("HIDIOCGRAWNAME");
else
printf("Raw Name: %s\n", buf);
/* Get Physical Location */
res = ioctl(fd, HIDIOCGRAWPHYS(256), buf);
if (res < 0)
perror("HIDIOCGRAWPHYS");
else
printf("Raw Phys: %s\n", buf);
/* Get Raw Info */
res = ioctl(fd, HIDIOCGRAWINFO, &info);
if (res < 0) {
perror("HIDIOCGRAWINFO");
} else {
printf("Raw Info:\n");
printf("\tbustype: %d (%s)\n",
info.bustype, bus_str(info.bustype));
printf("\tvendor: 0x%04hx\n", info.vendor);
printf("\tproduct: 0x%04hx\n", info.product);
}
memset(buf, 0x0, sizeof(buf));
/* Send a Report to the Device */
bool logo = false; // false: set key LEDs, true: set logo LEDs
char effect = 0; // 0: static, 1: breathe, 2: wave, 3: neon, 4: logo-only! neon, 5: snake, 6: reactive, 7: aurora, 8: ripple, 10: custom per-key, 100: musical rhythm
char brightness = 2; // 0-4
char speed = 3; // 0 - 4
char direction = 2; // 0: right, 1: left, 2: up, 3: down, 4: to outside, 5: to inside, 6: clockwise, 7: counter-clockwise
bool fullColor = true; // seems to use all rainbow colors instead of just TOP/BG color, or something like that
//setmode(fd, logo, effect, brightness, speed, direction, fullColor, 255, 255, 255, 0, 0, 255);
int msgIdx = 0;
int keyIdx = 0;
if(argc > 2)
msgIdx = atoi(argv[2]);
if(argc > 3)
keyIdx = atoi(argv[3]);
printf("Will try to set key %d in message %d\n", keyIdx, msgIdx);
setKeyColors(fd, msgIdx, keyIdx);
//usleep(300000);
/* Get a report from the device */
res = read(fd, buf, 128);
if (res < 0) {
perror("read");
} else {
printf("read() read %d bytes:\n\t", res);
for (i = 0; i < res; i++)
printf("%hhx ", buf[i]);
puts("\n");
}
#if 0
/* Set Feature */
buf[0] = 0x9; /* Report Number */
buf[1] = 0xff;
buf[2] = 0xff;
buf[3] = 0xff;
res = ioctl(fd, HIDIOCSFEATURE(4), buf);
if (res < 0)
perror("HIDIOCSFEATURE");
else
printf("ioctl HIDIOCSFEATURE returned: %d\n", res);
/* Get Feature */
buf[0] = 0x9; /* Report Number */
res = ioctl(fd, HIDIOCGFEATURE(256), buf);
if (res < 0) {
perror("HIDIOCGFEATURE");
} else {
printf("ioctl HIDIOCGFEATURE returned: %d\n", res);
printf("Report data:\n\t");
for (i = 0; i < res; i++)
printf("%hhx ", buf[i]);
puts("\n");
}
/* Send a Report to the Device */
buf[0] = 0x1; /* Report Number */
buf[1] = 0x77;
res = write(fd, buf, 2);
if (res < 0) {
printf("Error: %d\n", errno);
perror("write");
} else {
printf("write() wrote %d bytes\n", res);
}
/* Get a report from the device */
res = read(fd, buf, 16);
if (res < 0) {
perror("read");
} else {
printf("read() read %d bytes:\n\t", res);
for (i = 0; i < res; i++)
printf("%hhx ", buf[i]);
puts("\n");
}
#endif
close(fd);
return 0;
}
const char *
bus_str(int bus)
{
switch (bus) {
case BUS_USB:
return "USB";
break;
case BUS_HIL:
return "HIL";
break;
case BUS_BLUETOOTH:
return "Bluetooth";
break;
case BUS_VIRTUAL:
return "Virtual";
break;
default:
return "Other";
break;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment