背景

客制化键盘玩法很多,有很多原厂高度的GMK配色键帽,但那都是不透光的。如果想要RGB,那可以整双皮奶键帽,全键RGB。更进一步,可以整亚克力外壳、透明轴、透明键帽…

前不久买一把我觉得是RGB终点的套件,方块男孩的RK680,Gasket,110颗 RGB灯,QMK VIA,南向轴座…

但这就是RGB终点了吗?!

看着一机箱的RGB灯,和这把无法和他们愉快同步的键盘,我幡然大悟!

灯效同步才是RGB的终点,我要给我这把键盘自己加灯效!

好在方块男孩键盘的开源程度很不错,轻松要到了工程文件,装好qmk工具就可以在wsl编译。但是因为这个键盘尼玛的reset键要拆了键盘才能按,一直懒得弄。开发RGB同步的念头就像咒语一样在脑海中徘徊了几个月。直到昨天不想上班休了一天假,在家才坐下来仔细研究了一下RGB同步能否实现。

先上下最后同步的效果:







QMK VIA

qmk是开源键盘驱动Quantum Mechanical Keyboard的缩写,支持非常多的客制化键盘,支持自定义键位、自定义层、自带RGB灯效等功能,如果在2022年你的键盘还没有用上qmk,那么你可以考虑换一把了。
via是什么就比较难懂了,直观来看,只要支持via的键盘,就可以通过via的电脑程序或者webapp获得实时修改键位、调整RGB光效的能力,不用再像QMK每次改键都要编译一个新的固件出来刷机了。从底层来看,via是附加在qmk上的一层自定义的协议,通过UsbHID和电脑通信,定义的协议中实现了一些功能就有修改键位、修改RGB光效。

\

开启VIA需要在rule.mk中设置VIA_ENABLED=yes,当VIA开启的时候,RAWHID会自动开启,可以不需要额外设置RAW_ENABLE=yes。编译QMK固件的过程不再在这里赘述,需要确认编译好的固件可以正常使用VIA软件,这代表电脑-usbhid-via这条路就已经通了。

SignalRGB

SignalRGB是现在一款统一RGB软件,可以统一控制华硕Aurora、技嘉fusion、海盗船、雷蛇等很多厂商的RGB,他采取了反向工程的方法,破解出厂商RGB通信的协议,然后再用自己实现的服务端和RGB设备通信,起到控制RGB灯效的作用。在统一RGB领域中还有一款是OpenRGB,全开源软件,但是我觉得UI比较简陋,还是更喜欢用SignalRGB。

华硕、技嘉、海盗船这些厂商对于RGB通信协议都是支支吾吾的,从来没有一份公开的文档或者SDK,现在开源的SDK都是无数玩家开发者贡献出来的反向工程代码,OpenRGB就是这些代码的集大成者,SignalRGB这个软件也极大程度受益于OpenRGB提供的开源协议。诸如内存灯光,显卡灯光,可能是厂家通过向内存/PCIE设备一个指定的地址,按照指定的格式写RGB信息实现的控制方法,都是大佬们通过反向工程抓包解析出来的。有了OpenRGB和SignalRGB这样的平台铺路,我们一般人想写个插件支持下新的设备,或者添加一个自定义的光效,就变得非常简单了。

https://signalrgb.com/devices/
这是SignalRGB目前支持的设备列表,主流厂商并不是尽数支持的,可以进去看看你的设备支持不支持。很巧的是,我用的华硕主板+海盗船内存+技嘉显卡,SignalRGB都是支持的,所以它就成了我得最佳选择,赶紧抛弃掉这三个厂商的垃圾RGB控制软件。

我的设备列表,技嘉显卡送去修了所以这里没有

它支持的RGB效果很多,你也可以自己开发效果

每一个RGB效果都有速度、颜色等参数可以自定义

RGB控制采取类似桌面布局的方式,可以把设备放到对应的位置,大小方向都可以调整

对主板的每个argb插针都可以单独控制

总之这是一款特别棒的RGB控制软件,经过一段时间使用之后也对基于它二次开发有了兴趣。哦对了,他们自己也卖RGB灯条,牌子叫whirlwindfx,小贵,并且只寄美国和加拿大,淘宝没有。

我的键盘

先说下我使用的这把键盘

这把键盘是方块男孩的FK680Pro套件,用的V2版本的PCB,正面有70颗LED灯,背面有40颗LED灯,使用QMK固件支持VIA,单模USB接口。
这里用的键帽是AKKO透明键帽,轴是幻想轴。
这是把不错的入门QMK的键盘,套件价格只要不到三百元,键盘手感不错,RGB灯很多,硬件软件全开源,群里有图纸和代码,工程文件可以找淘宝客服要,很适合二次开发RGB灯效。

基于SignalRGB的二次开发

SignalRGB的很多设备是通过Javascript插件来支持的,对于尚不支持的设备,开发者鼓励自己开发Javascript脚本支持。
官方对于如何从头开始制作一个脚本有详细的教程,特别解释了如何去抓USB HID包,因为大部分RGB都是通过HID来控制的。

从0开始反向工程一个USBHID协议还是一个挺难的工程,我配置了半天wireshark,都没看到一个有效的hid包。不过好在我们今天做的QMK VIA键盘不需要我们反向工程。QMK VIA代码是开源的,并且SignalRGB的Javascript脚本也是开源的,只要参考官方的脚本和代码,我们就可以将自己的QMK键盘也接入SignalRGB的控制。

细读修改QMK VIA的RGB Matrix代码与USBHID代码,加入SignalRGB支持

前言,SignalRGB改造的协议、handler都是SignalRGB的开发者写好的,只是SignalRGB尚未有详细的文档教人如何去自己改造QMK固件,官方只发布了三个QMK键盘的支持:https://signalrgb.developerhub.io/qmk/supported-keyboards

可以预见官方肯定会加强QMK的支持,但目前我只能根据这已经放出的三把键盘,改造一下我得QMK固件,自己编译一个支持SignalRGB的固件,同时在写一个电脑端调用脚本。好在我有上面链接里的三把键盘可以参考。不幸的是官方是放出了编译好的固件,所以qmk固件中的修改,还要去官方的qmk仓库中找:signalrgb/qmk_firmware

RGB

QMK控制RGB等效有两种接口,一个叫rgblight,FLAG是RGBLIGHT_ENABLE,这个比较旧了,效果也很简单;另一个是rgbmatrix,FLAG是RGB_MATRIX_ENABLE,如其名字是矩阵化的RGB等效,这两个的区别有点像12v4pin的RGB和5v3pin的ARGB的效果区别,matrix的灯效更精细,可以per键控制。FK680键盘默认使用rgbmatrix控制rgb。

rgb_matrix中需要使用的函数是rgb_matrix_set_color,可以设置一个灯泡的rgb值,灯泡从0开始顺序计数的,一般来说一行一行递增计数。rgb_matrix的动画效果存放在animation文件中,动画需要先在rgb_matrix_effects.inc中定义,然后在定义的头文件里实现RGB效果。

rgb_matrix支持使用RGB灯条控制器,FK680pro自带的控制器是WS2812。

为了实现SignalRGB接入,需要在animation创建一个自定义的动画:SIGNALRGB

// animations/signalrgb_anim.h
RGB_MATRIX_EFFECT(SIGNALRGB)
#ifdef RGB_MATRIX_CUSTOM_EFFECT_IMPLS

bool SIGNALRGB(effect_params_t* params)
{
    RGB_MATRIX_USE_LIMITS(led_min, led_max);

    return rgb_matrix_check_finished_leds(led_max);
}

#endif // RGB_MATRIX_CUSTOM_EFFECT_IMPLS

然后在头文件中加入SIGNALRGB的动画定义

// animations/rgb_matrix_effects.inc
// Add your new core rgb matrix effect here, order determines enum order
#include "signalrgb_anim.h"

这个动画模式的目的是给后面从电脑控制RGB预留一个空白的RGB模式,这样电脑传过来的RGB信号不会和固件发出的RGB冲突。

VIA

VIA实现在quantum/via.c中,VIA的核心就是在 raw_hid_receive 函数中实现了一系列的USBHID命令,包括实时修改键位、设置RGB灯等等。

VIA的USBHID协议包含的命令可以看via.h头文件。从0x010x13是via原生定义的指令,0x21开始则是signalRGB自己新增加的命令,SignalRGB的电脑端就是通过这些命令和VIA键盘通信的。包括获得qmk版本、via版本、signalrgb指令版本、开启关闭signalrgb,最重要的id_signalrgb_stream_leds才是设置LED颜色,控制RGB的函数。

enum via_command_id {
    id_get_protocol_version                 = 0x01, // always 0x01
    id_get_keyboard_value                   = 0x02,
    id_set_keyboard_value                   = 0x03,
    id_dynamic_keymap_get_keycode           = 0x04,
    id_dynamic_keymap_set_keycode           = 0x05,
    id_dynamic_keymap_reset                 = 0x06,
    id_lighting_set_value                   = 0x07,
    id_lighting_get_value                   = 0x08,
    id_lighting_save                        = 0x09,
    id_eeprom_reset                         = 0x0A,
    id_bootloader_jump                      = 0x0B,
    id_dynamic_keymap_macro_get_count       = 0x0C,
    id_dynamic_keymap_macro_get_buffer_size = 0x0D,
    id_dynamic_keymap_macro_get_buffer      = 0x0E,
    id_dynamic_keymap_macro_set_buffer      = 0x0F,
    id_dynamic_keymap_macro_reset           = 0x10,
    id_dynamic_keymap_get_layer_count       = 0x11,
    id_dynamic_keymap_get_buffer            = 0x12,
    id_dynamic_keymap_set_buffer            = 0x13,
    id_unhandled                            = 0xFF,
    id_signalrgb_qmk_version                = 0x21,
    id_signalrgb_protocol_version           = 0x22,
    id_signalrgb_unique_identifier          = 0x23,
    id_signalrgb_stream_leds                = 0x24,
    id_signalrgb_effect_enable              = 0x25,
    id_signalrgb_effect_disable             = 0x26,
    id_signalrgb_get_total_leds             = 0x27,
    id_signalrgb_get_firmware_type          = 0x28,
};

VIA的raw_hid_receive函数就是一个很简单的switch,传入的data是1字节uint8_t数组,第一个1字节data[0]是指令,之后的都是数据段,可以在各个指令里自己定义。这段SWITCH最后就是signalrgb相关的handler,signalrgb的开发者已经把他们实现好了,我们只需要拷贝一份。仓库:signalrgb/qmk_firmware

// VIA handles received HID messages first, and will route to
// raw_hid_receive_kb() for command IDs that are not handled here.
// This gives the keyboard code level the ability to handle the command
// specifically.
//
// raw_hid_send() is called at the end, with the same buffer, which was
// possibly modified with returned values.
void raw_hid_receive(uint8_t *data, uint8_t length) {
    uint8_t *command_id   = &(data[0]);
    uint8_t *command_data = &(data[1]);
    switch (*command_id) {
        case id_get_protocol_version: {
            command_data[0] = VIA_PROTOCOL_VERSION >> 8;
            command_data[1] = VIA_PROTOCOL_VERSION & 0xFF;
            break;
        }
        case id_get_keyboard_value: {
            switch (command_data[0]) {
                case id_uptime: {
                    uint32_t value  = timer_read32();
                    command_data[1] = (value >> 24) & 0xFF;
                    command_data[2] = (value >> 16) & 0xFF;
                    command_data[3] = (value >> 8) & 0xFF;
                    command_data[4] = value & 0xFF;
                    break;
                }
                case id_layout_options: {
                    uint32_t value  = via_get_layout_options();
                    command_data[1] = (value >> 24) & 0xFF;
                    command_data[2] = (value >> 16) & 0xFF;
                    command_data[3] = (value >> 8) & 0xFF;
                    command_data[4] = value & 0xFF;
                    break;
                }
                case id_switch_matrix_state: {
#if ((MATRIX_COLS / 8 + 1) * MATRIX_ROWS <= 28)
                    uint8_t i = 1;
                    for (uint8_t row = 0; row < MATRIX_ROWS; row++) {
                        matrix_row_t value = matrix_get_row(row);
#    if (MATRIX_COLS > 24)
                        command_data[i++] = (value >> 24) & 0xFF;
#    endif
#    if (MATRIX_COLS > 16)
                        command_data[i++] = (value >> 16) & 0xFF;
#    endif
#    if (MATRIX_COLS > 8)
                        command_data[i++] = (value >> 8) & 0xFF;
#    endif
                        command_data[i++] = value & 0xFF;
                    }
#endif
                    break;
                }
                default: {
                    raw_hid_receive_kb(data, length);
                    break;
                }
            }
            break;
        }
        case id_set_keyboard_value: {
            switch (command_data[0]) {
                case id_layout_options: {
                    uint32_t value = ((uint32_t)command_data[1] << 24) | ((uint32_t)command_data[2] << 16) | ((uint32_t)command_data[3] << 8) | (uint32_t)command_data[4];
                    via_set_layout_options(value);
                    break;
                }
                default: {
                    raw_hid_receive_kb(data, length);
                    break;
                }
            }
            break;
        }
        case id_dynamic_keymap_get_keycode: {
            uint16_t keycode = dynamic_keymap_get_keycode(command_data[0], command_data[1], command_data[2]);
            command_data[3]  = keycode >> 8;
            command_data[4]  = keycode & 0xFF;
            break;
        }
        case id_dynamic_keymap_set_keycode: {
            dynamic_keymap_set_keycode(command_data[0], command_data[1], command_data[2], (command_data[3] << 8) | command_data[4]);
            break;
        }
        case id_dynamic_keymap_reset: {
            dynamic_keymap_reset();
            break;
        }
        case id_lighting_set_value: {
#if defined(VIA_QMK_BACKLIGHT_ENABLE)
            via_qmk_backlight_set_value(command_data);
#endif
#if defined(VIA_QMK_RGBLIGHT_ENABLE)
            via_qmk_rgblight_set_value(command_data);
#endif
#if defined(VIA_QMK_RGB_MATRIX_ENABLE)
            via_qmk_rgb_matrix_set_value(command_data);
#endif
#if defined(VIA_CUSTOM_LIGHTING_ENABLE)
            raw_hid_receive_kb(data, length);
#endif
#if !defined(VIA_QMK_BACKLIGHT_ENABLE) && !defined(VIA_QMK_RGBLIGHT_ENABLE) && !defined(VIA_CUSTOM_LIGHTING_ENABLE) && !defined(VIA_QMK_RGB_MATRIX_ENABLE)
            // Return the unhandled state
            *command_id = id_unhandled;
#endif
            break;
        }
        case id_lighting_get_value: {
#if defined(VIA_QMK_BACKLIGHT_ENABLE)
            via_qmk_backlight_get_value(command_data);
#endif
#if defined(VIA_QMK_RGBLIGHT_ENABLE)
            via_qmk_rgblight_get_value(command_data);
#endif
#if defined(VIA_QMK_RGB_MATRIX_ENABLE)
            via_qmk_rgb_matrix_get_value(command_data);
#endif
#if defined(VIA_CUSTOM_LIGHTING_ENABLE)
            raw_hid_receive_kb(data, length);
#endif
#if !defined(VIA_QMK_BACKLIGHT_ENABLE) && !defined(VIA_QMK_RGBLIGHT_ENABLE) && !defined(VIA_CUSTOM_LIGHTING_ENABLE)  && !defined(VIA_QMK_RGB_MATRIX_ENABLE)
            // Return the unhandled state
            *command_id = id_unhandled;
#endif
            break;
        }
        case id_lighting_save: {
#if defined(VIA_QMK_BACKLIGHT_ENABLE)
            eeconfig_update_backlight_current();
#endif
#if defined(VIA_QMK_RGBLIGHT_ENABLE)
            eeconfig_update_rgblight_current();
#endif
#if defined(VIA_QMK_RGB_MATRIX_ENABLE)
            eeconfig_update_rgb_matrix();
#endif
#if defined(VIA_CUSTOM_LIGHTING_ENABLE)
            raw_hid_receive_kb(data, length);
#endif
#if !defined(VIA_QMK_BACKLIGHT_ENABLE) && !defined(VIA_QMK_RGBLIGHT_ENABLE) && !defined(VIA_CUSTOM_LIGHTING_ENABLE)  && !defined(VIA_QMK_RGB_MATRIX_ENABLE)
            // Return the unhandled state
            *command_id = id_unhandled;
#endif
            break;
        }
#ifdef VIA_EEPROM_ALLOW_RESET
        case id_eeprom_reset: {
            via_eeprom_set_valid(false);
            eeconfig_init_via();
            break;
        }
#endif
        case id_dynamic_keymap_macro_get_count: {
            command_data[0] = dynamic_keymap_macro_get_count();
            break;
        }
        case id_dynamic_keymap_macro_get_buffer_size: {
            uint16_t size   = dynamic_keymap_macro_get_buffer_size();
            command_data[0] = size >> 8;
            command_data[1] = size & 0xFF;
            break;
        }
        case id_dynamic_keymap_macro_get_buffer: {
            uint16_t offset = (command_data[0] << 8) | command_data[1];
            uint16_t size   = command_data[2]; // size <= 28
            dynamic_keymap_macro_get_buffer(offset, size, &command_data[3]);
            break;
        }
        case id_dynamic_keymap_macro_set_buffer: {
            uint16_t offset = (command_data[0] << 8) | command_data[1];
            uint16_t size   = command_data[2]; // size <= 28
            dynamic_keymap_macro_set_buffer(offset, size, &command_data[3]);
            break;
        }
        case id_dynamic_keymap_macro_reset: {
            dynamic_keymap_macro_reset();
            break;
        }
        case id_dynamic_keymap_get_layer_count: {
            command_data[0] = dynamic_keymap_get_layer_count();
            break;
        }
        case id_dynamic_keymap_get_buffer: {
            uint16_t offset = (command_data[0] << 8) | command_data[1];
            uint16_t size   = command_data[2]; // size <= 28
            dynamic_keymap_get_buffer(offset, size, &command_data[3]);
            break;
        }
        case id_dynamic_keymap_set_buffer: {
            uint16_t offset = (command_data[0] << 8) | command_data[1];
            uint16_t size   = command_data[2]; // size <= 28
            dynamic_keymap_set_buffer(offset, size, &command_data[3]);
            break;
        }
        case id_signalrgb_qmk_version:

        get_qmk_version();

        break;
        case id_signalrgb_protocol_version:

        get_signalrgb_protocol_version();

        break;
        case id_signalrgb_unique_identifier:

        get_unique_identifier();

        break;
        case id_signalrgb_stream_leds:

        led_streaming(data);

        break;

        case id_signalrgb_effect_enable:

        signalrgb_mode_enable();

        break;

        case id_signalrgb_effect_disable:

        signalrgb_mode_disable();

        break;

        case id_signalrgb_get_total_leds:

        get_total_leds();

        break;

        case id_signalrgb_get_firmware_type:

        get_firmware_type();

        break;

        default: {
            // The command ID is not known
            // Return the unhandled state
            *command_id = id_unhandled;
            break;
        }
    }

SignalRGB实现好了的handler。我们也可以直接拷贝,其中最重要的ledstream指令,传入的是一个led index + led num + rgb list的结构,data[1]代表设置的第一个灯泡index,data[2]是接下来数据里包含的灯泡数量,data[2]之后是一组组长度为24(3字节)的 (R,G,B)数据。

 uint8_t packet[32];

 void get_qmk_version(void) //Grab the QMK Version
{
        packet[0] = id_signalrgb_qmk_version;
        packet[1] = QMK_VERSION_BYTE_1;
        packet[2] = QMK_VERSION_BYTE_2;
        packet[3] = QMK_VERSION_BYTE_3;

        raw_hid_send(packet, 32);
}

void get_signalrgb_protocol_version(void) //Grab what version of the SignalRGB protocol a keyboard is running
{
        packet[0] = id_signalrgb_protocol_version;
        packet[1] = PROTOCOL_VERSION_BYTE_1;
        packet[2] = PROTOCOL_VERSION_BYTE_2;
        packet[3] = PROTOCOL_VERSION_BYTE_3;

        raw_hid_send(packet, 32);
}

void get_unique_identifier(void) //Grab the unique identifier for each specific model of keyboard.
{
        packet[0] = id_signalrgb_unique_identifier;
        packet[1] = DEVICE_UNIQUE_IDENTIFIER_BYTE_1;
        packet[2] = DEVICE_UNIQUE_IDENTIFIER_BYTE_2;
        packet[3] = DEVICE_UNIQUE_IDENTIFIER_BYTE_3;

        raw_hid_send(packet, 32);
}

void led_streaming(uint8_t *data) //Stream data from HID Packets to Keyboard.
{
    uint8_t index = data[1];
    uint8_t numberofleds = data[2]; 

    if(numberofleds >= 10)
    {
        packet[1] = DEVICE_ERROR_LEDS;
        raw_hid_send(packet,32);
        return; 
    } 

    for (uint8_t i = 0; i < numberofleds; i++)
    {
      uint8_t offset = (i * 3) + 3;
      uint8_t  r = data[offset];
      uint8_t  g = data[offset + 1];
      uint8_t  b = data[offset + 2];

      rgb_matrix_set_color(index + i, r, g, b);
     }
}

void signalrgb_mode_enable(void)
{
    rgb_matrix_mode_noeeprom(RGB_MATRIX_SIGNALRGB); //Set RGB Matrix to SignalRGB Compatible Mode
}

void signalrgb_mode_disable(void)
{
    rgb_matrix_reload_from_eeprom(); //Reloading last effect from eeprom
}

void get_total_leds(void)//Grab total number of leds that a board has.
{
    packet[0] = id_signalrgb_get_total_leds;
    packet[1] = DRIVER_LED_TOTAL;

    raw_hid_send(packet, 32);
}

void get_firmware_type(void) //Grab which fork of qmk a board is running.
{
    packet[0] = id_signalrgb_get_firmware_type;
    packet[1] = FIRMWARE_TYPE_BYTE;

    raw_hid_send(packet, 32);
}

在这里补充下FK680Pro键盘的LED INDEX,在SignalRGB中定义RGB矩阵时会用到:

细读修改SignalRGB的设备接入脚本

这里我们先参考一下SignalRGB官方放出的KDBFans KDB67的QMK VIA支持脚本

首先是头部,定义了一些脚本的基本信息,这些是提供给SignalRGB显示用的。

export function Name() { return "KBDFans KBD67"; }
export function VendorId() { return 0x4b42; }
export function ProductId() { return 0x1225; }
export function Publisher() { return "WhirlwindFX"; }
export function Size() { return [15, 5]; }
export function DefaultPosition(){return [10, 100]; }
export function DefaultScale(){return 8.0}
export function ControllableParameters() {
    return [
        {"property":"shutdownColor", "group":"lighting", "label":"Shutdown Color", "min":"0", "max":"360", "type":"color", "default":"009bde"},
        {"property":"LightingMode", "group":"lighting", "label":"Lighting Mode", "type":"combobox", "values":["Canvas", "Forced"], "default":"Canvas"},
        {"property":"forcedColor", "group":"lighting", "label":"Forced Color", "min":"0", "max":"360", "type":"color", "default":"009bde"},
    ];
}

接下来是正戏,在vKeys中定义的键位(应该说LED顺序),在vKeyNames中定义的每一个LED名字,在vKeyPositions里定义的每一个LED的抽象位置。

vKeys中的可以值可以对LED排序再做一次映射,比较绕,我就直接保持了从0开始递增到最后一个LED结束的规则,我有110颗LED,就是[0, … 109]

vKeyNames只是debug用的名称显示,自己取个名字就行,但是注意这三个数组长度必须一样。

vKeyPosition是每一个LED在SignalRGB的Layout中的抽象位置,键盘被划成5×15的区域(这个在第一段代码中第一),比如最左上的ESC位置就是[0,0],左下的Ctrl就是[0,4],这里比较坑的是:这里的顺序,ESC所在的LED灯的INDEX值是0。

const vKeys = 
[
    0,  1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 
    30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
    44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
    58, 59, 60, 61, 62, 63, 64, 65, 66
];
const vKeyNames = 
[
    "Esc","1","2", "3", "4", "5",  "6", "7", "8", "9", "0",  "-", "+",  "Backspace","Home", //15
    "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\","Page Up",     //15
    "CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'","Enter",  "Page Down",    //14
    "Left Shift","Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Right Shift",    "Up Arrow",   "End",   //14
    "Left Ctrl", "Left Win", "Left Alt", "Space", "Right Alt", "Fn", "Left Arrow",  "Down Arrow", "Right Arrow", //9

];

const vKeyPositions = 
[
[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0], [12,0], [13,0], [14,0], //15
[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1], [12,1], [13,1], [14,1], //15 
[0,2],[1,2],[2,2],[3,2],[4,2],[5,2],[6,2],[7,2],[8,2],[9,2],[10,2],[11,2],         [13,2], [14,2], //14                    
[0,3],[1,3],[2,3],[3,3],[4,3],[5,3],[6,3],[7,3],[8,3],[9,3],[10,3],[11,3],         [13,3], [14,3],  //14
[0,4],[1,4],[2,4],                  [6,4],                  [10,4],[11,4], [12,4], [13,4], [14,4]  //9
];

但实际上在我的键盘上,Left Ctrl的LED INDEX才是0,所以这里要做顺序调整,也是我花了一些时间才搞出来的。之外我得空格下有3颗LED,所以空格中间也需要再加两颗LED。最后这是67键盘的键位,我是68键盘,最下层再多一个键。最最后我还有背面40颗灯珠,还得给后买后面的灯珠设置逻辑位置。这里直接公布答案了,照抄就行:

const vKeys = 
[
    0,  1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11,                             // 12
    12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,                     // 14
    26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,                     // 14
    40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,                 // 15
    55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,                 // 15
    70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109 // 40
];
const vKeyNames = 
[
    "Left Ctrl", "Left Win", "Left Alt", "Space0", "Space1", "Space2", "Right Ctrl", "Fn", "Right Alt", "Left Arrow",  "Down Arrow", "Right Arrow", //12
    "Left Shift","Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Right Shift",    "Up Arrow",   "End",   //14
    "CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'","Enter",  "Page Down",    //14
    "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\","Page Up",     //15
    "Esc","1","2", "3", "4", "5",  "6", "7", "8", "9", "0",  "-", "+",  "Backspace","Home", //15
    "bk0", "bk1", "bk2", "bk3", "bk4", "bk5", "bk6", "bk7", "bk8", "bk9", "bk10", "bk11", "bk12", "bk13", "bk14", "bk15", "bk16", "bk17", "bk18", "bk19", "bk20", 
    "bk21", "bk22", "bk23", "bk24", "bk25", "bk26", "bk27", "bk28", "bk29", "bk30", "bk31", "bk32", "bk33", "bk34", "bk35", "bk36", "bk37", "bk38", "bk39"
];

const vKeyPositions = 
[
    [0,4],[1,4],[2,4],      [4,4],      [6,4],      [8,4],[9,4],[10,4],[11,4], [12,4], [13,4], [14,4], //12
    [0,3],[1,3],[2,3],[3,3],[4,3],[5,3],[6,3],[7,3],[8,3],[9,3],[10,3],[11,3],         [13,3], [14,3], //14
    [0,2],[1,2],[2,2],[3,2],[4,2],[5,2],[6,2],[7,2],[8,2],[9,2],[10,2],[11,2],         [13,2], [14,2], //14                    
    [0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1], [12,1], [13,1], [14,1], //15 
    [0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0], [12,0], [13,0], [14,0], //15
    [14,0],[14,1],[14,2],[14,3],[14,4],
    [14,4],[13,4],[12,4],[11,4],[10,4],[9,4],[8,4],[7,4],[6,4],[5,4],[4,4],[3,4],[2,4],[1,4],[0,4],
    [0,4],[0,3],[0,2],[0,1],[0,0],
    [0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],`-`
];

后面是从SignalRGB获取每一个逻辑位置RGB值得代码,和实际构造数据包,发送USBHID的代码,如果使用SignalRGB开发者设计好的协议的话这段不需要修改,当然你可以修改协议实现自己的RGB或者别的功能。

export function LedNames() 
{
    return vKeyNames;
}

export function LedPositions() 
{
    return vKeyPositions;
}

export function Initialize() 
{
    ClearReadBuffer();
    versionVIA();
    versionQMK();
    versionSignalRGBProtocol();
    uniqueIdentifier();
    effectEnable();
}

export function Render() 
{
    sendColors();
}

export function Shutdown() 
{
    effectDisable();
}

function ClearReadBuffer(timeout = 10){
    let count = 0;
    let readCounts = [];
    device.flush();

    while(device.getLastReadSize() > 0){
        device.read([0x00], 32, timeout);
        count++;
        readCounts.push(device.getLastReadSize());
    }
    //device.log(`Read Count {count}:{readCounts} Bytes`)
}

function versionVIA()
{
    var packet = [];
    packet[0] = 0x00;
    packet[1] = 0x01;

    device.write(packet, 32);
    packet = device.read(packet,32);
    let via_version = packet[3];
    device.log("Via Protocol Version: " + via_version);
}

function versionQMK() //Check the version of QMK Firmware that the keyboard is running
{
    var packet = [];
    packet[0] = 0x00;
    packet[1] = 0x21;

    device.write(packet, 32);
    packet = device.read(packet,32);
    let QMKVersionByte1 = packet[2];
    let QMKVersionByte2 = packet[3];
    let QMKVersionByte3 = packet[4];
    device.log("QMK Version: " + QMKVersionByte1 + "." + QMKVersionByte2 + "." + QMKVersionByte3);
    device.pause(30);
}

function versionSignalRGBProtocol() //Grab the version of the SignalRGB Protocol the keyboard is running
{
    var packet = [];
    packet[0] = 0x00;
    packet[1] = 0x22;

    device.write(packet, 32);
    packet = device.read(packet,32);
    let ProtocolVersionByte1 = packet[2];
    let ProtocolVersionByte2 = packet[3];
    let ProtocolVersionByte3 = packet[4];
    device.log("SignalRGB Protocol Version: " + ProtocolVersionByte1 + "." + ProtocolVersionByte2 + "." + ProtocolVersionByte3);
    device.pause(30);
}

function uniqueIdentifier() //Grab the unique identifier for this keyboard model
{
    var packet = [];
    packet[0] = 0x00;
    packet[1] = 0x23;

    device.write(packet, 32);
    packet = device.read(packet,32);
    let UniqueIdentifierByte1 = packet[2];
    let UniqueIdentifierByte2 = packet[3];
    let UniqueIdentifierByte3 = packet[4];
    device.log("Unique Device Identifier: " + UniqueIdentifierByte1 + UniqueIdentifierByte2 + UniqueIdentifierByte3);
    device.pause(30);
}

function effectEnable() //Enable the SignalRGB Effect Mode
{
    let packet = [];
    packet[0] = 0x00;
    packet[1] = 0x25;

    device.write(packet,32);
    device.pause(30);
}

function effectDisable() //Revert to Hardware Mode
{
    let packet = [];
    packet[0] = 0x00;
    packet[1] = 0x26;

    device.write(packet,32);  
}

function grabColors(shutdown = false) 
{
    let rgbdata = [];

    for(let iIdx = 0; iIdx < vKeys.length; iIdx++)
    {
        let iPxX = vKeyPositions[iIdx][0];
        let iPxY = vKeyPositions[iIdx][1];
        let color;

        if(shutdown)
        {
            color = hexToRgb(shutdownColor);
        }
        else if (LightingMode === "Forced")
        {
            color = hexToRgb(forcedColor);
        }
        else
        {
            color = device.color(iPxX, iPxY);
        }

        let iLedIdx = vKeys[iIdx] * 3;
        rgbdata[iLedIdx] = color[0];
        rgbdata[iLedIdx+1] = color[1];
        rgbdata[iLedIdx+2] = color[2];
    }

    return rgbdata;
}

function sendColors()
{
    let rgbdata = grabColors();

    for(var index = 0; index < 8; index++) //This will need rounded up to closest value for your board.
    {
    let packet = [];
    let offset = index * 9;
    packet[0] = 0x00;
    packet[1] = 0x24;
    packet[2] = offset;
    packet[3] = 0x09;
    packet = packet.concat(rgbdata.splice(0, 27));
    device.write(packet, 33);

    }

}

function hexToRgb(hex) 
{
    let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    let colors = [];
    colors[0] = parseInt(result[1], 16);
    colors[1] = parseInt(result[2], 16);
    colors[2] = parseInt(result[3], 16);

    return colors;
}

export function Validate(endpoint) 
{
    return endpoint.interface === 1;
}

export function Image() 
{
    return "";
}

注意脚本中打印的device.log需要在SignalRGB的设备管理里头看:

开发好的自定义脚本可以加上Image,放到C:\%USER%\Documents\WhirlwindFX\Plugins\


到此,只要刷上新的固件,在SignalRGB中导入你的脚本,就可以愉快的用SignalRGB控制你的键盘了


扩展:添加模式切换,大小写,饱和度亮度调整等

FK680键盘原来有个CapsLock灯的效果,这个是在qmk_firmware\keyboards\FK680ProV2\keymaps\via\keymap.c中由键盘开发者实现的,代码逻辑如下:

这段代码同时实现了两个功能:如果上/下键盘RGB等效关闭则关灯;如果大写锁定打开,则打开index=26灯。从键盘LED INDEX可以看出26就是大小写键。

void rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {

    if (user_config.top_rgb_change)
    {
        for (size_t i = 0; i < 70; i++)
        {
            RGB_MATRIX_INDICATOR_SET_COLOR(i, 0, 0, 0);
        }
    }

    if (host_keyboard_led_state().caps_lock) {
        RGB_MATRIX_INDICATOR_SET_COLOR(26, 0, 255, 255); // assuming caps lock is at led #5
    }

    if (user_config.bottom_rgb_change)
    {
        for (size_t i = 70; i < 110; i++)
        {
            RGB_MATRIX_INDICATOR_SET_COLOR(i, 0, 0, 0);
        }
    }
}

在支持SignalRGB之后,SignalRGB会覆盖大小写灯,还原原来的功能只需要加一行continue:

void led_streaming(uint8_t *data) //Stream data from HID Packets to Keyboard.
{
    uint8_t index = data[1];
    uint8_t numberofleds = data[2];

    if(numberofleds >= 10)
    {
        packet[1] = DEVICE_ERROR_LEDS;
        raw_hid_send(packet,32);
        return;
    }

    for (uint8_t i = 0; i < numberofleds; i++)
    {
      uint8_t offset = (i * 3) + 3;
      uint8_t  r = data[offset];
      uint8_t  g = data[offset + 1];
      uint8_t  b = data[offset + 2];
      if (index + i == 26 && host_keyboard_led_state().caps_lock) continue;

     rgb_matrix_set_color(index + i, r, g, b);
     }
}

饱和度和亮度调整是在计算RGB是使用HSV格式,修改 S(atuation) 和 V(alue) 实现的,同时 H(ue) 也是可以修改的,不过 Hue对于有RGB颜色循环的等效无效。使用SignalRGB之后,键盘直接显示电脑端传过来的RGB值,这样很直接。但是我们也可以加上原来的饱和度S,亮度S调节。通过将RGB值先用rgb_to_hsv转换成HSV值,在对S和V进行微调,然后再用hsv_to_rgb转换回RGB格式,调用rgb_matrix_set_color显示。这么做是可行的,可以通过键盘对电脑同步RGB灯效再做加强/减弱饱和度/亮度的调整。但是我也遇到了问题,rgb->hsv->rgb这样的转换造成了一些色彩的丢失,特别表现在亮度上,原来变化自然的明暗变得非常生硬,几乎没有了亮度变化,不知道是转换中不可避免的损失,还是我哪里写错了。

void led_streaming(uint8_t *data) //Stream data from HID Packets to Keyboard.
{
    uint8_t index = data[1];
    uint8_t numberofleds = data[2];

    if(numberofleds >= 10)
    {
        packet[1] = DEVICE_ERROR_LEDS;
        raw_hid_send(packet,32);
        return;
    }

    for (uint8_t i = 0; i < numberofleds; i++)
    {
      uint8_t offset = (i * 3) + 3;
      uint8_t  r = data[offset];
      uint8_t  g = data[offset + 1];
      uint8_t  b = data[offset + 2];
      if (index + i == 26 && host_keyboard_led_state().caps_lock) continue;
      if (rgb_matrix_get_mode() != RGB_MATRIX_SIGNALRGB) {
        if (user_config.top_rgb_signal == true && index + i < 70) {
            rgb_matrix_set_color(index + i, r, g, b);
            continue;
        }
        else if (user_config.bottom_rgb_signal == true && index + i >= 70) {
            rgb_matrix_set_color(index + i, r, g, b);
            continue;
        }
        else continue;
      };

    //   rgb_matrix_set_color(index + i, r, g, b);
      RGB rgb;
      rgb.r = r;
      rgb.g = g;
      rgb.b = b;
      HSV hsv = rgb_to_hsv(rgb);

      hsv.s = hsv.s + 128 - rgb_matrix_config.hsv.s;
      hsv.s = hsv.s > 255 ? 255 : hsv.s;
      hsv.s = hsv.s < 0 ? 0 : hsv.s;

      hsv.v = hsv.v + 128 - rgb_matrix_config.hsv.v;
      hsv.v = hsv.v > 255 ? 255 : hsv.v;
      hsv.v = hsv.v < 0 ? 0 : hsv.v;

     rgb = hsv_to_rgb(hsv);

      rgb_matrix_set_color(index + i, rgb.r, rgb.g, rgb.b);
     }
}

这里用到的rgb_to_hsv不是qmk代码中自带的,是我从stackoverflow随便复制的一个函数:

HSV rgb_to_hsv(RGB rgb)
{
    HSV hsv;
    uint8_t rgbMin, rgbMax;

    rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b);
    rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b);

    hsv.v = rgbMax;
    if (hsv.v == 0)
    {
        hsv.h = 0;
        hsv.s = 0;
        return hsv;
    }

    long delta = rgbMax - rgbMin;
    hsv.s = 255 * delta / hsv.v;
    if (hsv.s == 0)
    {
        hsv.h = 0;
        return hsv;
    }

    if (rgbMax == rgb.r)
        hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin);
    else if (rgbMax == rgb.g)
        hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin);
    else
        hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin);

    return hsv;
}

扩展:自定义动画

最后一个bonus,我想实现的上层字母区LED和下层背面LED实现不同的动画:

在SignalRGB的led_streaming函数中添加如下开关:
user_config.top_rgb_signalTrue时上层RGB(前70个灯)一定输出;
user_config.bottom_rgb_signalTrue时下层RGB(后40个灯)一定输出;

void led_streaming(uint8_t *data) //Stream data from HID Packets to Keyboard.
{
    uint8_t index = data[1];
    uint8_t numberofleds = data[2];

    if(numberofleds >= 10)
    {
        packet[1] = DEVICE_ERROR_LEDS;
        raw_hid_send(packet,32);
        return;
    }

    for (uint8_t i = 0; i < numberofleds; i++)
    {
      uint8_t offset = (i * 3) + 3;
      uint8_t  r = data[offset];
      uint8_t  g = data[offset + 1];
      uint8_t  b = data[offset + 2];
      if (index + i == 26 && host_keyboard_led_state().caps_lock) continue;
      if (rgb_matrix_get_mode() != RGB_MATRIX_SIGNALRGB) {
        if (user_config.top_rgb_signal == true && index + i < 70) {
            rgb_matrix_set_color(index + i, r, g, b);
            continue;
        }
        else if (user_config.bottom_rgb_signal == true && index + i >= 70) {
            rgb_matrix_set_color(index + i, r, g, b);
            continue;
        }
        else continue;
      };

      rgb_matrix_set_color(index + i, r, g, b);
     }
}

user_config定义如下,原来是在keymap.c中定义的,挪到via.c

typedef union {
  uint32_t raw;
  struct {
    bool top_rgb_change :1;
    bool bottom_rgb_change :1;
    bool top_rgb_signal :1;
    bool bottom_rgb_signal :1;
  };
} user_config_t;

user_config_t user_config;

最后为了SignalRGB显示在上层、原版动画显示在下层的效果,需要在开关打开时,原版的RGB不写入,防止冲突。在animation中定义自定义函数rgb_matrix_set_color_user_config,让原版的animation设置RGB时都调用这个函数。

#pragma once

typedef HSV (*dx_dy_dist_f)(HSV hsv, int16_t dx, int16_t dy, uint8_t dist, uint8_t time);

void rgb_matrix_set_color_user_config(int index, uint8_t r, uint8_t g, uint8_t b) {
    if (user_config.top_rgb_signal == false && index < 70) {
        rgb_matrix_set_color(index, r, g, b);
        return;
    }
    else if (user_config.bottom_rgb_signal == false && index >= 70) {
        rgb_matrix_set_color(index, r, g, b);
        return;
    }
    else return;
}

bool effect_runner_dx_dy_dist(effect_params_t* params, dx_dy_dist_f effect_func) {
    RGB_MATRIX_USE_LIMITS(led_min, led_max);

    uint8_t time = scale16by8(g_rgb_timer, rgb_matrix_config.speed / 2);
    for (uint8_t i = led_min; i < led_max; i++) {
        RGB_MATRIX_TEST_LED_FLAGS();
        int16_t dx   = g_led_config.point[i].x - k_rgb_matrix_center.x;
        int16_t dy   = g_led_config.point[i].y - k_rgb_matrix_center.y;
        uint8_t dist = sqrt16(dx * dx + dy * dy);
        RGB     rgb  = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, dx, dy, dist, time));
        rgb_matrix_set_color_user_config(i, rgb.r, rgb.g, rgb.b);
    }
    return rgb_matrix_check_finished_leds(led_max);
}

效果:

代码库:
https://github.com/azuse/qmk_firmware

18 thoughts to “任何QMK VIA键盘接入SignalRGB实现电脑端RGB控制

  • Huy

    Thank for your post.
    But can you share source code of Blockboy FK640RGB? (It same your keyboard but 64 key and 88 led)
    I can’t find because I don’t know Chinese and don’t have QQ account 🙁
    I just comment but it not show, sorry if it duplicate

    回复
    • azuse

      FK640 doesn’t have source code uploaded in QQ chat group, I tried to reach out to the seller on Taobao but they won’t give me because I didn’t purchase one.😣 They want a order number, IDK if you have.

      回复
  • Ulrich

    大佬你好(*´▽`)ノノ,我有一个和你相同的设备,正在尝试烧录与你相同的固件,用于s rgb灯光管理,但是我存在一个非常愚蠢的问题是FK680pro这个键盘该如何进入固件烧录模式?期待你的解答

    回复
    • azuse

      背面有个reset键,对角线短接两下。其实可以问客服(。PS,很多键盘pcb都在背面有reset键

      回复
      • Ulrich

        感谢大佬的解答,最后确实是问了客服解决了这个问题,另外还想问下最后一段bonus中上下层分别控制在操作中如何实现,需要再改动一下固件吗

        回复
  • rainkill

    大佬我拉了你的库但是编译不了好像是缺少了 目录下的platforms/chibios/boards/STM32_F103_STM32DUINO/ld 路径里的uf2 id文件以及
    ib/ChibiOS-Contrib/os/hal/boards/目录下的 STM相关文件。你能发我一份你本机能运行的完整源码到我的邮箱吗?客服和群里已经不提供源码了。拜托了你是我唯一的希望 哭哭

    回复
  • koishi

    大佬可以分享FK750Pro固件或者有哪些地方需要修改吗

    回复
  • koishi

    为什么我在msys编译固件时提示没有规则可制作目标“lib/chibios-contrib/os/hal/boards/GENER
    IC_STM32_F103/board.mk(fk680和fk750都是这样,其他键盘没问题)

    回复
    • azuse

      你从github clone下的chibios-contrib库里没有这个芯片的编译文件,https://github.com/azuse/qmk_firmware/releases/download/1.0/qmk_firmware.zip 从这下一下

      回复
      • koishi

        用这个下的提示”git describe –abbrev=6 –dirty –always –tags”报128。。。。

        回复
      • koishi

        刚刚把mcu_selection.mk里的GENERIC_STM32_F103改成STM32_F103_STM32DUINO解决了

        回复
    • GG GG

      您好 您这里有k750pro的源文件吗 我也是在群里也问不到了

      回复
      • azuse

        没有诶 我只买了fk680,找淘宝客服要会给吗?以前挺喜欢方块的因为他们家开源很友好

        回复
  • hannnn

    ⚠ “git describe –abbrev=6 –dirty –always –tags” returned error code 128
    ⚠ “git describe –abbrev=6 –dirty –always –tags” returned error code 128
    ⚠ “git describe –abbrev=6 –dirty –always –tags” returned error code 128
    builddefs/build_keyboard.mk:49: *** 多个目标匹配。 停止。
    不知道什么情况

    回复
  • aaa

    问下那个Image()函数返回的是图片的base64编码?是jpg还是png转换的,有没有什么需要特别注意的地方?

    回复
    • azuse

      jpg和png应该都行,html里的

      回复
  • eniru

    你好,请问如果是分体键盘的话应该怎么让signalrgb控制副键盘呢?我这边试过很多写法,都只能控制usb连着的主键盘

    回复

Leave a comment

您的电子邮箱地址不会被公开。