koktoh の雑記帳

気ままに書いていきます

【不定期連載】QMK 探検隊 -その4-

はじめに

これは、「C言語とか組み込みなんもわからん」男が QMK というジャングルを探検していく、ノンフィクションドキュメンタリーである……
なお、更新は亀の歩みである……

前回のおさらい

koktoh.hatenablog.com

quantum.h の include を順番に探検していたら、 keymap.h もいっぱい include されていたので、順番に見ていくことにした

探検開始

続きから探検していこう

  1. keycode.h
  2. quantum_keycodes.h
  3. keycode_config.h
  4. debug.h
  5. report.h
  6. host.h
  7. action_macro.h
  8. action.h

host.h

github.com

ソース全体

/*
Copyright 2011 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <stdint.h>
#include <stdbool.h>
#include "report.h"
#include "host_driver.h"
#include "led.h"

#define IS_LED_ON(leds, led_name) ((leds) & (1 << (led_name)))
#define IS_LED_OFF(leds, led_name) (~(leds) & (1 << (led_name)))

#define IS_HOST_LED_ON(led_name) IS_LED_ON(host_keyboard_leds(), led_name)
#define IS_HOST_LED_OFF(led_name) IS_LED_OFF(host_keyboard_leds(), led_name)

#ifdef __cplusplus
extern "C" {
#endif

extern uint8_t keyboard_idle;
extern uint8_t keyboard_protocol;

/* host driver */
void           host_set_driver(host_driver_t *driver);
host_driver_t *host_get_driver(void);

/* host driver interface */
uint8_t host_keyboard_leds(void);
led_t   host_keyboard_led_state(void);
void    host_keyboard_send(report_keyboard_t *report);
void    host_mouse_send(report_mouse_t *report);
void    host_system_send(uint16_t data);
void    host_consumer_send(uint16_t data);

uint16_t host_last_system_report(void);
uint16_t host_last_consumer_report(void);

#ifdef __cplusplus
}
#endif

ここでも新しく include されている

  • host_driver.h
  • led.h

順番に見ていこう

host_driver.h

github.com

ソース全体

/*
Copyright 2011 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <stdint.h>
#include "report.h"
#ifdef MIDI_ENABLE
#    include "midi.h"
#endif

typedef struct {
    uint8_t (*keyboard_leds)(void);
    void (*send_keyboard)(report_keyboard_t *);
    void (*send_mouse)(report_mouse_t *);
    void (*send_system)(uint16_t);
    void (*send_consumer)(uint16_t);
} host_driver_t;

関数のプロトタイプ宣言のみと言ってもいいだろう
host_driver_t は、関数のみが宣言された構造体、オブジェクト指向でいうところのインターフェイスといったところか
様々な HID ドライバを同様に使うための工夫だろう
LED の状態を取得する、キーボード、マウスなどの状態を送信する関数が定義されている

led.h

github.com

ソース全体

/*
Copyright 2011 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <stdint.h>
#include <stdbool.h>

/* FIXME: Add doxygen comments here. */

/* keyboard LEDs */
#define USB_LED_NUM_LOCK 0
#define USB_LED_CAPS_LOCK 1
#define USB_LED_SCROLL_LOCK 2
#define USB_LED_COMPOSE 3
#define USB_LED_KANA 4

#ifdef __cplusplus
extern "C" {
#endif

typedef union {
    uint8_t raw;
    struct {
        bool    num_lock : 1;
        bool    caps_lock : 1;
        bool    scroll_lock : 1;
        bool    compose : 1;
        bool    kana : 1;
        uint8_t reserved : 3;
    };
} led_t;

void led_set(uint8_t usb_led);

void led_init_ports(void);

#ifdef __cplusplus
}
#endif

インジケータとして実装されることが多い Num Lock などのステータスを保存するための共用体 led_t が定義されている
あとは、 LED のステータスを処理する関数などが定義されている

led.c

github.com

ソース全体

/* Copyright 2020 zvecr<git@zvecr.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "quantum.h"

#ifdef BACKLIGHT_ENABLE
#    include "backlight.h"
extern backlight_config_t backlight_config;
#else
// Cannot use BACKLIGHT_CAPS_LOCK without backlight being enabled
#    undef BACKLIGHT_CAPS_LOCK
#endif

#ifndef LED_PIN_ON_STATE
#    define LED_PIN_ON_STATE 1
#endif

#if defined(BACKLIGHT_CAPS_LOCK)
/** \brief Caps Lock indicator using backlight (for keyboards without dedicated LED)
 */
static void handle_backlight_caps_lock(led_t led_state) {
    // Use backlight as Caps Lock indicator
    uint8_t bl_toggle_lvl = 0;

    if (led_state.caps_lock && !backlight_config.enable) {
        // Turning Caps Lock ON and backlight is disabled in config
        // Toggling backlight to the brightest level
        bl_toggle_lvl = BACKLIGHT_LEVELS;
    } else if (!led_state.caps_lock && backlight_config.enable) {
        // Turning Caps Lock OFF and backlight is enabled in config
        // Toggling backlight and restoring config level
        bl_toggle_lvl = backlight_config.level;
    }

    // Set level without modify backlight_config to keep ability to restore state
    backlight_set(bl_toggle_lvl);
}
#endif

/** \brief Lock LED set callback - keymap/user level
 *
 * \deprecated Use led_update_user() instead.
 */
__attribute__((weak)) void led_set_user(uint8_t usb_led) {}

/** \brief Lock LED set callback - keyboard level
 *
 * \deprecated Use led_update_kb() instead.
 */
__attribute__((weak)) void led_set_kb(uint8_t usb_led) { led_set_user(usb_led); }

/** \brief Lock LED update callback - keymap/user level
 *
 * \return True if led_update_kb() should run its own code, false otherwise.
 */
__attribute__((weak)) bool led_update_user(led_t led_state) { return true; }

/** \brief Lock LED update callback - keyboard level
 *
 * \return Ignored for now.
 */
__attribute__((weak)) bool led_update_kb(led_t led_state) {
    bool res = led_update_user(led_state);
    if (res) {
#if defined(LED_NUM_LOCK_PIN) || defined(LED_CAPS_LOCK_PIN) || defined(LED_SCROLL_LOCK_PIN) || defined(LED_COMPOSE_PIN) || defined(LED_KANA_PIN)
#    if LED_PIN_ON_STATE == 0
        // invert the whole thing to avoid having to conditionally !led_state.x later
        led_state.raw = ~led_state.raw;
#    endif

#    ifdef LED_NUM_LOCK_PIN
        writePin(LED_NUM_LOCK_PIN, led_state.num_lock);
#    endif
#    ifdef LED_CAPS_LOCK_PIN
        writePin(LED_CAPS_LOCK_PIN, led_state.caps_lock);
#    endif
#    ifdef LED_SCROLL_LOCK_PIN
        writePin(LED_SCROLL_LOCK_PIN, led_state.scroll_lock);
#    endif
#    ifdef LED_COMPOSE_PIN
        writePin(LED_COMPOSE_PIN, led_state.compose);
#    endif
#    ifdef LED_KANA_PIN
        writePin(LED_KANA_PIN, led_state.kana);
#    endif
#endif
    }
    return res;
}

/** \brief Initialise any LED related hardware and/or state
 */
__attribute__((weak)) void led_init_ports(void) {
#ifdef LED_NUM_LOCK_PIN
    setPinOutput(LED_NUM_LOCK_PIN);
    writePin(LED_NUM_LOCK_PIN, !LED_PIN_ON_STATE);
#endif
#ifdef LED_CAPS_LOCK_PIN
    setPinOutput(LED_CAPS_LOCK_PIN);
    writePin(LED_CAPS_LOCK_PIN, !LED_PIN_ON_STATE);
#endif
#ifdef LED_SCROLL_LOCK_PIN
    setPinOutput(LED_SCROLL_LOCK_PIN);
    writePin(LED_SCROLL_LOCK_PIN, !LED_PIN_ON_STATE);
#endif
#ifdef LED_COMPOSE_PIN
    setPinOutput(LED_COMPOSE_PIN);
    writePin(LED_COMPOSE_PIN, !LED_PIN_ON_STATE);
#endif
#ifdef LED_KANA_PIN
    setPinOutput(LED_KANA_PIN);
    writePin(LED_KANA_PIN, !LED_PIN_ON_STATE);
#endif
}

/** \brief Entrypoint for protocol to LED binding
 */
__attribute__((weak)) void led_set(uint8_t usb_led) {
#ifdef BACKLIGHT_CAPS_LOCK
    handle_backlight_caps_lock((led_t)usb_led);
#endif

    led_set_kb(usb_led);
    led_update_kb((led_t)usb_led);
}

「Caps Lock されていたら、 Caps Lock のインジケータ LED が繋がっている GPIO ピンを HIGH にする」というような処理が書かれている
他は初期化処理などだ

host.c

github.com

ソース全体

/*
Copyright 2011,2012 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <stdint.h>
//#include <avr/interrupt.h>
#include "keycode.h"
#include "host.h"
#include "util.h"
#include "debug.h"

#ifdef NKRO_ENABLE
#    include "keycode_config.h"
extern keymap_config_t keymap_config;
#endif

static host_driver_t *driver;
static uint16_t       last_system_report   = 0;
static uint16_t       last_consumer_report = 0;

void host_set_driver(host_driver_t *d) { driver = d; }

host_driver_t *host_get_driver(void) { return driver; }

uint8_t host_keyboard_leds(void) {
    if (!driver) return 0;
    return (*driver->keyboard_leds)();
}

led_t host_keyboard_led_state(void) {
    if (!driver) return (led_t){0};
    return (led_t)((*driver->keyboard_leds)());
}

/* send report */
void host_keyboard_send(report_keyboard_t *report) {
    if (!driver) return;
#if defined(NKRO_ENABLE) && defined(NKRO_SHARED_EP)
    if (keyboard_protocol && keymap_config.nkro) {
        /* The callers of this function assume that report->mods is where mods go in.
         * But report->nkro.mods can be at a different offset if core keyboard does not have a report ID.
         */
        report->nkro.mods      = report->mods;
        report->nkro.report_id = REPORT_ID_NKRO;
    } else
#endif
    {
#ifdef KEYBOARD_SHARED_EP
        report->report_id = REPORT_ID_KEYBOARD;
#endif
    }
    (*driver->send_keyboard)(report);

    if (debug_keyboard) {
        dprint("keyboard_report: ");
        for (uint8_t i = 0; i < KEYBOARD_REPORT_SIZE; i++) {
            dprintf("%02X ", report->raw[i]);
        }
        dprint("\n");
    }
}

void host_mouse_send(report_mouse_t *report) {
    if (!driver) return;
#ifdef MOUSE_SHARED_EP
    report->report_id = REPORT_ID_MOUSE;
#endif
    (*driver->send_mouse)(report);
}

void host_system_send(uint16_t report) {
    if (report == last_system_report) return;
    last_system_report = report;

    if (!driver) return;
    (*driver->send_system)(report);
}

void host_consumer_send(uint16_t report) {
    if (report == last_consumer_report) return;
    last_consumer_report = report;

    if (!driver) return;
    (*driver->send_consumer)(report);
}

uint16_t host_last_system_report(void) { return last_system_report; }

uint16_t host_last_consumer_report(void) { return last_consumer_report; }

キーボードやマウスの状態を送る関数の実体である
HID について詳しいわけではないので、 host_system_send()host_consumer_send() が何を何のために送っているのかはよくわからない
キーボードやマウスの状態を送っている というのが理解できれば十分ではないだろうか

action_macro.h

github.com

ソース全体

/*
Copyright 2013 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <stdint.h>
#include "progmem.h"

typedef uint8_t macro_t;

#define MACRO_NONE (macro_t *)0
#define MACRO(...)                                          \
    ({                                                      \
        static const macro_t __m[] PROGMEM = {__VA_ARGS__}; \
        &__m[0];                                            \
    })
#define MACRO_GET(p) pgm_read_byte(p)

// Sends press when the macro key is pressed, release when release, or tap_macro when the key has been tapped
#define MACRO_TAP_HOLD(record, press, release, tap_macro) (((record)->event.pressed) ? (((record)->tap.count <= 0 || (record)->tap.interrupted) ? (press) : MACRO_NONE) : (((record)->tap.count > 0 && !((record)->tap.interrupted)) ? (tap_macro) : (release)))

// Holds down the modifier mod when the macro key is held, or sends macro instead when tapped
#define MACRO_TAP_HOLD_MOD(record, macro, mod) MACRO_TAP_HOLD(record, (MACRO(D(mod), END)), MACRO(U(mod), END), macro)

// Holds down the modifier mod when the macro key is held, or pressed a shifted key when tapped (eg: shift+3 for #)
#define MACRO_TAP_SHFT_KEY_HOLD_MOD(record, key, mod) MACRO_TAP_HOLD_MOD(record, (MACRO(I(10), D(LSFT), T(key), U(LSFT), END)), mod)

// Momentary switch layer when held, sends macro if tapped
#define MACRO_TAP_HOLD_LAYER(record, macro, layer)                                                         \
    (((record)->event.pressed) ? (((record)->tap.count <= 0 || (record)->tap.interrupted) ? ({             \
        layer_on((layer));                                                                                 \
        MACRO_NONE;                                                                                        \
    })                                                                                                     \
                                                                                          : MACRO_NONE)    \
                               : (((record)->tap.count > 0 && !((record)->tap.interrupted)) ? (macro) : ({ \
                                     layer_off((layer));                                                   \
                                     MACRO_NONE;                                                           \
                                 })))

// Momentary switch layer when held, presses a shifted key when tapped (eg: shift+3 for #)
#define MACRO_TAP_SHFT_KEY_HOLD_LAYER(record, key, layer) MACRO_TAP_HOLD_LAYER(record, MACRO(I(10), D(LSFT), T(key), U(LSFT), END), layer)

#ifndef NO_ACTION_MACRO
void action_macro_play(const macro_t *macro_p);
#else
#    define action_macro_play(macro)
#endif

/* Macro commands
 *   code(0x04-73)                      // key down(1byte)
 *   code(0x04-73) | 0x80               // key up(1byte)
 *   { KEY_DOWN, code(0x04-0xff) }      // key down(2bytes)
 *   { KEY_UP,   code(0x04-0xff) }      // key up(2bytes)
 *   WAIT                               // wait milli-seconds
 *   INTERVAL                           // set interval between macro commands
 *   END                                // stop macro execution
 *
 * Ideas(Not implemented):
 *   modifiers
 *   system usage
 *   consumer usage
 *   unicode usage
 *   function call
 *   conditionals
 *   loop
 */
enum macro_command_id {
    /* 0x00 - 0x03 */
    END = 0x00,
    KEY_DOWN,
    KEY_UP,

    /* 0x04 - 0x73 (reserved for keycode down) */

    /* 0x74 - 0x83 */
    WAIT = 0x74,
    INTERVAL,

    /* 0x84 - 0xf3 (reserved for keycode up) */

    /* 0xf4 - 0xff */
};

/* TODO: keycode:0x04-0x73 can be handled by 1byte command  else 2bytes are needed
 * if keycode between 0x04 and 0x73
 *      keycode / (keycode|0x80)
 * else
 *      {KEY_DOWN, keycode} / {KEY_UP, keycode}
 */
#define DOWN(key) KEY_DOWN, (key)
#define UP(key) KEY_UP, (key)
#define TYPE(key) DOWN(key), UP(key)
#define WAIT(ms) WAIT, (ms)
#define INTERVAL(ms) INTERVAL, (ms)

/* key down */
#define D(key) DOWN(KC_##key)
/* key up */
#define U(key) UP(KC_##key)
/* key type */
#define T(key) TYPE(KC_##key)
/* wait */
#define W(ms) WAIT(ms)
/* interval */
#define I(ms) INTERVAL(ms)

/* for backward comaptibility */
#define MD(key) DOWN(KC_##key)
#define MU(key) UP(KC_##key)

マクロの組み立てや、実行のスイッチ動作を簡単に記述するためのマクロが定義されているようだ

action_macro.c

github.com

ソース全体

/*
Copyright 2013 Jun Wako <wakojun@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "action.h"
#include "action_util.h"
#include "action_macro.h"
#include "wait.h"

#ifdef DEBUG_ACTION
#    include "debug.h"
#else
#    include "nodebug.h"
#endif

#ifndef NO_ACTION_MACRO

#    define MACRO_READ() (macro = MACRO_GET(macro_p++))
/** \brief Action Macro Play
 *
 * FIXME: Needs doc
 */
void action_macro_play(const macro_t *macro_p) {
    macro_t macro    = END;
    uint8_t interval = 0;

    if (!macro_p) return;
    while (true) {
        switch (MACRO_READ()) {
            case KEY_DOWN:
                MACRO_READ();
                dprintf("KEY_DOWN(%02X)\n", macro);
                if (IS_MOD(macro)) {
                    add_macro_mods(MOD_BIT(macro));
                    send_keyboard_report();
                } else {
                    register_code(macro);
                }
                break;
            case KEY_UP:
                MACRO_READ();
                dprintf("KEY_UP(%02X)\n", macro);
                if (IS_MOD(macro)) {
                    del_macro_mods(MOD_BIT(macro));
                    send_keyboard_report();
                } else {
                    unregister_code(macro);
                }
                break;
            case WAIT:
                MACRO_READ();
                dprintf("WAIT(%u)\n", macro);
                {
                    uint8_t ms = macro;
                    while (ms--) wait_ms(1);
                }
                break;
            case INTERVAL:
                interval = MACRO_READ();
                dprintf("INTERVAL(%u)\n", interval);
                break;
            case 0x04 ... 0x73:
                dprintf("DOWN(%02X)\n", macro);
                register_code(macro);
                break;
            case 0x84 ... 0xF3:
                dprintf("UP(%02X)\n", macro);
                unregister_code(macro & 0x7F);
                break;
            case END:
            default:
                return;
        }
        // interval
        {
            uint8_t ms = interval;
            while (ms--) wait_ms(1);
        }
    }
}
#endif

マクロ実行の実体である
自分で組み上げたマクロが、この通りに実行されるのだろう
QMK の標準機能として、関数にラップされていそうだ(QMK のドキュメントmacro などで検索してほしい)

まとめ

今回は、 host.haction_macro.h を探検した
コア部分にだいぶ近いのだろうか、見通しが悪く、あまり理解できない部分が多かった
今後探検を続けていくことで、理解が深まるかもしれないと淡い希望を抱きつつ、進めていこう

次回予告

次回は action.h を探検していく
少し覗いただけで広大なジャングルが広がっており目がくらみそうになるが、少しずつ探検していこうと思う