はじめに
ここ何か月か TWELITE で無線キーボード作れないかなってやってることを書きます。
なお、コードを書くだけ書いていますが、ビルドもしていなければ、デバッグもしていないので、失敗して爆散する可能性も大いにあります。
コード類は一部折りたたんでいるので、気になる人は適宜展開してください。
TWELITE で無線キーボード
TWELITE とは、モノワイヤレス株式会社様が販売している、無線マイコンモジュールです。
消費電力が小さい、通信距離が長い、 act (TWELITE のプログラム、 C++)を書くことで様々な動作が可能、技適が通っている、といった特徴があります。
このことから、 TWELITE を無線 IO エキスパンダとして、無線キーボードにできるのではないかと考えました。
TWEBridge
以上から、このプロジェクトを TWEBridge と名付け、開発してみることにしました。
なお、以降の話は、現在開発中のもので、多分に変更される可能性があります。
構成
キーボード動作はすべて QMK にまかせ、 TWELITE は入力の取得と結果の送信だけに徹するようにします。
構成要素は増えますが、 QMK の資産を使えること、 HID を自分で実装する必要がないことなどから、そのようにしました。
イメージとしてはこんな感じです。
PC | | 有線 (USB HID) | QMK | | 有線 (UART) | TWELITE 親機 : : 無線 : .. .. .. .. .. .. .. .. .. .. .. .. .. : : : : キーボード1 キーボード2 キーボード3 ... (子機) (子機) (子機)
TWELITE 子機
以下のようなハードウェアの変化検知と通知を担当します。
- マトリクススキャン
- エンコーダの変化検知
- バッテリーモニタ
変化があれば NWK_SIMPLE で親機に都度送信します。
一定時間変化がなければスリープし、なるべく消費電力を抑えるようにします。
バッテリーモニタは、最後の計測から一定時間(1時間とか)後のスリープに入る直前に、計測と送信を行います。
ADC を常に動かしておくのは無駄なので、そのタイミングだけ ADC をオンにして測定し、その後オフにする、という動作です。
TWELITE 親機
子機から受信したデータを、そのまま UART で QMK に横流しします。
親機は QMK (が動作しているマイコン)と有線接続しているため、常に電源が供給されており、常に受信待ちをしている想定です。
QMK
親機から UART で送られてきたデータを解釈し、マトリクス状態の更新や、エンコーダイベントの登録を行います。
データの解釈や更新は専用関数に閉じ込め、 QMK の *_user() / *_kb() や matrix_scan_custom() などで呼び出し適宜設定することで、 QMK のフローに乗せ、キーボードの動作をすべて QMK に任せます。
プロトコル
とりあえずこんな感じで考えています。
- デバイス (
twb_device_packet_t)- デバイスの情報をまとめたもの
- バッテリー (
twb_battery_packet_t)- バッテリー情報
- マトリクス (
twb_matrix_packet_t)- マトリクスの状態
- エンコーダ (
twb_encoder_packet_t)- エンコーダの状態
コード(クリックで展開)
#define TWB_MAX_PACKET_SIZE 64 // ---------------------------------------------------------------------------------------------------- // // Packet definitions // // ---------------------------------------------------------------------------------------------------- typedef uint8_t twb_packet_type_t; enum { TWB_PACKET_TYPE_DEVICE = 0, TWB_PACKET_TYPE_BATTERY = 1, TWB_PACKET_TYPE_MATRIX = 2, TWB_PACKET_TYPE_ENCODER = 3, }; // ---------------------------------------------------------------------------------------------------- // packet header // ---------------------------------------------------------------------------------------------------- #define TWB_PACKET_HEADER_SIZE 3 // version, packet_type, device_id typedef struct { uint8_t version; twb_packet_type_t packet_type; uint8_t device_id; } TWB_PACKED twb_packet_header_t; // ---------------------------------------------------------------------------------------------------- // device packet // ---------------------------------------------------------------------------------------------------- #define TWB_DEVICE_PACKET_SIZE (TWB_PACKET_HEADER_SIZE + 3) // header, row_count, col_count, encoder_count typedef struct { twb_packet_header_t header; uint8_t row_count; uint8_t col_count; uint8_t encoder_count; } TWB_PACKED twb_device_packet_t; // ---------------------------------------------------------------------------------------------------- // battery packet // ---------------------------------------------------------------------------------------------------- #define TWB_BATTERY_PACKET_SIZE (TWB_PACKET_HEADER_SIZE + 2) // header, voltage_mv (2 bytes) typedef struct { twb_packet_header_t header; int16_t voltage_mv; } TWB_PACKED twb_battery_packet_t; // ---------------------------------------------------------------------------------------------------- // matrix packet // ---------------------------------------------------------------------------------------------------- #define TWB_MATRIX_PACKET_HEADER_SIZE (TWB_PACKET_HEADER_SIZE + 1) // header, matrix_data_len #define TWB_MAX_MATRIX_DATA_SIZE (TWB_MAX_PACKET_SIZE - TWB_MATRIX_PACKET_HEADER_SIZE) typedef struct { twb_packet_header_t header; uint8_t matrix_data_len; uint8_t matrix_data[TWB_MAX_MATRIX_DATA_SIZE]; } TWB_PACKED twb_matrix_packet_t; // ---------------------------------------------------------------------------------------------------- // encoder packet // ---------------------------------------------------------------------------------------------------- #define TWB_ENCODER_PACKET_HEADER_SIZE (TWB_PACKET_HEADER_SIZE + 1) // header, encoder_data_len #define TWB_MAX_ENCODER_DATA_SIZE (TWB_MAX_PACKET_SIZE - TWB_ENCODER_PACKET_HEADER_SIZE) typedef struct { twb_packet_header_t header; uint8_t encoder_data_len; uint8_t encoder_data[TWB_MAX_ENCODER_DATA_SIZE]; } TWB_PACKED twb_encoder_packet_t; // ---------------------------------------------------------------------------------------------------- // packet union // ---------------------------------------------------------------------------------------------------- typedef union { uint8_t raw[TWB_MAX_PACKET_SIZE]; twb_packet_header_t header; twb_device_packet_t device; twb_battery_packet_t battery; twb_matrix_packet_t matrix; twb_encoder_packet_t encoder; } twb_packet_t;
このプロトコルを解釈して、 QMK のマトリクス状態の更新やエンコーダイベントの登録などを行います。
ジェネレータ
QMK を使う以上、キーボードの各設定値はビルド時に決定している必要があります。
キーマップやマトリクスの情報、エンコーダの有無などです。
つまり、単純に TWELITE の子機を増やしても、その子機の情報を QMK のキーマップなどに反映することができないのです。
そこで、 YAML で設定を包括的に記述し、 QMK や TWELITE 側で必要なファイルを生成するジェネレータを作ることにしました。
バリデーションを実行して、設定の矛盾などを事前に検知するようにしたいと思っています。
とりあえず scriban でテンプレートから作るようにしています。
生成対象
とりあえずこの辺が必要かなと思って作ろうとしているファイルリストです。
開発中なので、当然増減します。
- QMK
rules.mkconfig.hkeybard.h/ckeymap.c
- TWELITE
Config.hpp
config.yaml
とりあえずこんな感じのを書いといたら、各種ファイルができるといいね、って感じです。
YAML(クリックで展開)
UsbDescriptor: VendorId: 0xFEED ProductId: 0x0000 DeviceVer: 0x0001 Manufacturer: manufacturer Product: product Description: A custom keyboard Devices: - Name: left Id: 0x01 ScanDirection: COL2ROW RowPins: - PIN0 - PIN1 - PIN2 - PIN3 ColPins: - PIN4 - PIN5 - PIN6 - PIN7 - PIN8 - PIN9 - PIN10 Matrix: - [left_00, left_01, left_02, left_03, left_04, left_05, left_06] - [left_10, left_11, left_12, left_13, left_14, left_15, _______] - [left_20, left_21, left_22, left_23, left_24, left_25, left_26] - [_______, _______, _______, left_33, left_34, left_35, _______] - Name: right Id: 0x02 ScanDirection: COL2ROW RowPins: - PIN0 - PIN1 - PIN2 - PIN3 ColPins: - PIN4 - PIN5 - PIN6 - PIN7 - PIN8 - PIN9 - PIN10 Matrix: - [right_06, right_05, right_04, right_03, right_02, right_01, right_00] - [________, right_15, right_14, right_13, right_12, right_11, right_10] - [right_26, right_25, right_24, right_23, right_22, right_21, right_20] - [________, right_35, right_34, right_33, ________, ________, ________] - Name: num pad Id: 0x03 ScanDirection: ROW2COL RowPins: - PIN0 - PIN1 - PIN2 - PIN3 - PIN4 - PIN5 ColPins: - PIN6 - PIN7 - PIN8 - PIN9 Matrix: - [np_00, np_01, np_02, np_03] - [np_10, np_11, np_12, np_13] - [np_20, np_21, np_22, np_23] - [np_30, np_31, np_32, _____] - [np_40, np_41, np_42, np_43] - [np_50, np_51, np_52, _____] - Name: macro pad Id: 0x04 ScanDirection: COL2ROW RowPins: - PIN0 - PIN1 - PIN2 - PIN3 ColPins: - PIN4 - PIN5 - PIN6 - PIN7 Encoders: - [PIN8, PIN9] - [PIN10, PIN11] - [PIN12, PIN13] Matrix: - [macro_00, macro_01, macro_02, macro_03] - [macro_10, macro_11, macro_12, macro_13] - [macro_20, macro_21, macro_22, macro_23] - [macro_30, macro_31, macro_32, macro_33] - Name: foot switch Id: 0x05 ScanDirection: DIRECT DirectPins: - PIN0 - PIN1 - PIN2 - PIN3 Matrix: - [fs_00, fs_01, fs_02] Layout: |- left_00, left_01, left_02, left_03, left_04, left_05, left_06, right_06, right_05, right_04, right_03, right_02, right_01, right_00, np_00, np_01, np_03, macro_00, macro_01, macro_02, macro_03, left_10, left_11, left_12, left_13, left_14, left_15, right_15, right_14, right_13, right_12, right_11, right_10, np_10, np_11, np_13, macro_10, macro_11, macro_12, macro_13, left_20, left_21, left_22, left_23, left_24, left_25, left_26, right_26, right_25, right_24, right_23, right_22, right_21, right_20, np_20, np_21, np_23, macro_20, macro_21, macro_22, macro_23, left_33, left_34, left_35, right_35, right_34, right_33, np_30, np_31, macro_30, macro_31, macro_32, macro_33, np_40, np_41, np_43, np_50, np_51, fs_00, fs_01, fs_02
出力ファイル例
まだ実装中ですが、こんな感じのが出力されると便利かなって感じです。
実際に上記の YAML を食わせて生成されたものです。
config.h
config.yaml に含まれる全デバイスのマトリクスを上から順に繋げて、1つのキーボードとして設定します。
#pragma once #include "config_common.h" /* USB Device descriptor parameter */ #define VENDOR_ID 0xFEED #define PRODUCT_ID 0x0000 #define DEVICE_VER 0x0001 #define MANUFACTURER manufacturer #define PRODUCT product #define DESCRIPTION A custom keyboard /* key matrix size */ #define MATRIX_ROWS 19 #define MATRIX_COLS 7 #define MATRIX_ROW_PINS { NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN } #define MATRIX_COL_PINS { NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN, NO_PIN } #define NUM_ENCODERS 3
keyboard.h/c
config.yaml に含まれる全デバイスのマトリクスを上から順に繋げて、1つのマトリクスとして LAYOUT マクロを定義します。
#pragma once #include <quantum.h> #define LAYOUT( \ left_00, left_01, left_02, left_03, left_04, left_05, left_06, right_06, right_05, right_04, right_03, right_02, right_01, right_00, np_00, np_01, np_03, macro_00, macro_01, macro_02, macro_03, \ left_10, left_11, left_12, left_13, left_14, left_15, right_15, right_14, right_13, right_12, right_11, right_10, np_10, np_11, np_13, macro_10, macro_11, macro_12, macro_13, \ left_20, left_21, left_22, left_23, left_24, left_25, left_26, right_26, right_25, right_24, right_23, right_22, right_21, right_20, np_20, np_21, np_23, macro_20, macro_21, macro_22, macro_23, \ left_33, left_34, left_35, right_35, right_34, right_33, np_30, np_31, macro_30, macro_31, macro_32, macro_33, \ np_40, np_41, np_43, \ np_50, np_51, fs_00, fs_01, fs_02 \ ) \ { \ { left_00, left_01, left_02, left_03, left_04, left_05, left_06 }, \ { left_10, left_11, left_12, left_13, left_14, left_15, KC_NO }, \ { left_20, left_21, left_22, left_23, left_24, left_25, left_26 }, \ { KC_NO, KC_NO, KC_NO, left_33, left_34, left_35, KC_NO }, \ { right_06, right_05, right_04, right_03, right_02, right_01, right_00 }, \ { KC_NO, right_15, right_14, right_13, right_12, right_11, right_10 }, \ { right_26, right_25, right_24, right_23, right_22, right_21, right_20 }, \ { KC_NO, right_35, right_34, right_33, KC_NO, KC_NO, KC_NO }, \ { np_00, np_01, np_02, np_03, KC_NO, KC_NO, KC_NO }, \ { np_10, np_11, np_12, np_13, KC_NO, KC_NO, KC_NO }, \ { np_20, np_21, np_22, np_23, KC_NO, KC_NO, KC_NO }, \ { np_30, np_31, np_32, KC_NO, KC_NO, KC_NO, KC_NO }, \ { np_40, np_41, np_42, np_43, KC_NO, KC_NO, KC_NO }, \ { np_50, np_51, np_52, KC_NO, KC_NO, KC_NO, KC_NO }, \ { macro_00, macro_01, macro_02, macro_03, KC_NO, KC_NO, KC_NO }, \ { macro_10, macro_11, macro_12, macro_13, KC_NO, KC_NO, KC_NO }, \ { macro_20, macro_21, macro_22, macro_23, KC_NO, KC_NO, KC_NO }, \ { macro_30, macro_31, macro_32, macro_33, KC_NO, KC_NO, KC_NO }, \ { fs_00, fs_01, fs_02, KC_NO, KC_NO, KC_NO, KC_NO } \ }
twb_device.c
各デバイスの ID と行列数、 QMK での行/エンコーダオフセットを設定します。
これをもとに、各デバイスのマトリクスパケットやエンコーダパケットのデータを QMK 用に変換します。
#include "twb_device.h" const twb_device_t twb_devices[] = { { 0x01, "left", 4, 7, 0, 0, 0 }, { 0x02, "right", 4, 7, 0, 4, 0 }, { 0x03, "num pad", 6, 4, 0, 8, 0 }, { 0x04, "macro pad", 4, 4, 3, 14, 0 }, { 0x05, "foot switch", 1, 3, 0, 18, 3 } }; const size_t twb_device_count = 5; twb_device_status_t twb_device_status[] = { { 0 }, { 0 }, { 0 }, { 0 }, { 0 } };
Config.hpp
TWELITE の最上位設定です。
COL2ROW
#pragma once #include <stdint.h> #include "twb_keyboard_definitions.h" #include "PinAlias.hpp" #define TWB_CFG_DEVICE_ID 0x01 #define SCAN_DIRECTION TWB_SCAN_DIRECTION_COL2ROW #define ROW_PINS { PIN0, PIN1, PIN2, PIN3 } #define COL_PINS { PIN4, PIN5, PIN6, PIN7, PIN8, PIN9, PIN10 }
ROW2COL
#pragma once #include <stdint.h> #include "twb_keyboard_definitions.h" #include "PinAlias.hpp" #define TWB_CFG_DEVICE_ID 0x03 #define SCAN_DIRECTION TWB_SCAN_DIRECTION_ROW2COL #define ROW_PINS { PIN0, PIN1, PIN2, PIN3, PIN4, PIN5 } #define COL_PINS { PIN6, PIN7, PIN8, PIN9 }
エンコーダあり
#pragma once #include <stdint.h> #include "twb_keyboard_definitions.h" #include "PinAlias.hpp" #define TWB_CFG_DEVICE_ID 0x04 #define SCAN_DIRECTION TWB_SCAN_DIRECTION_COL2ROW #define ROW_PINS { PIN0, PIN1, PIN2, PIN3 } #define COL_PINS { PIN4, PIN5, PIN6, PIN7 } #define ENCODER_PINS { \ { PIN8, PIN9 }, \ { PIN10, PIN11 }, \ { PIN12, PIN13 }}
DIRECT
#pragma once #include <stdint.h> #include "twb_keyboard_definitions.h" #include "PinAlias.hpp" #define TWB_CFG_DEVICE_ID 0x05 #define SCAN_DIRECTION TWB_SCAN_DIRECTION_DIRECT #define DIRECT_PINS { PIN0, PIN1, PIN2, PIN3 }
ビルダー
TWELITE では、ビルドに含めるサブディレクトリが4つまでとなっています。
しかし、現在のプロジェクトはこんな感じになっています。
TWEBridge
├─ generator // ジェネレータ
├─ qmk // QMK 用コード
├─ twb_core // TWELITE / QMK 両方で使用するライブラリコード
│ ├─ twb_compiler.h
│ ├─ twb_encoder.h
│ ├─ twb_keyboard_definitions.h
│ ├─ twb_matrix.h
│ ├─ twb_queue.c
│ ├─ twb_queue.h
│ ├─ twb_uart_parser.c
│ └─ twb_uart_parser.h
├─ twb_protocol // TWELITE / QMK 両方で使用するライブラリコード
│ ├─ twb_packet.h
│ └─ twb_packet_v1.h
└─ twelite // TWELITE 用コード
├─ Child
│ ├─ TWEBridgeChild-handlers.cpp
│ ├─ TWEBridgeChild.cpp
│ └─ TWEBridgeChild.hpp
├─ Common
│ ├─ BatteryMonitor
│ │ └─ BatteryMonitor.hpp
│ ├─ Encoder
│ │ ├─ Encoder.hpp
│ │ ├─ EncoderReader.cpp
│ │ └─ EncoderReader.hpp
│ ├─ Matrix
│ │ ├─ Matrix.hpp
│ │ ├─ MatrixScanner.cpp
│ │ └─ MatrixScanner.hpp
│ ├─ PinAlias.hpp
│ └─ PinUtils.hpp
├─ Parent
│ ├─ TWEBridgeParent-handlers.cpp
│ ├─ TWEBridgeParent.cpp
│ └─ TWEBridgeParent.hpp
├─ AppConfig.hpp
├─ Config.hpp
├─ ConfigMacros.hpp
├─ DeviceConfig.hpp
└─ main.cpp
規模が大きく、サブディレクトリの数が4つを超えています。
将来的には、こんな感じで TWELITE の共通コードはまとめ、各デバイスの Config.hpp だけ置いておいて、それを参照してビルドするようにしたいところです。
TWEBridge
├─ generator // ジェネレータ
├─ qmk // QMK 用コード
├─ twb_core // TWELITE / QMK 両方で使用するライブラリコード
│ ├─ twb_compiler.h
│ ├─ twb_encoder.h
│ ├─ twb_keyboard_definitions.h
│ ├─ twb_matrix.h
│ ├─ twb_queue.c
│ ├─ twb_queue.h
│ ├─ twb_uart_parser.c
│ └─ twb_uart_parser.h
├─ twb_protocol // TWELITE / QMK 両方で使用するライブラリコード
│ ├─ twb_packet.h
│ └─ twb_packet_v1.h
└─ twelite // TWELITE 用コード
├─ src
│ ├─ Child
│ │ ├─ TWEBridgeChild-handlers.cpp
│ │ ├─ TWEBridgeChild.cpp
│ │ └─ TWEBridgeChild.hpp
│ ├─ Common
│ │ ├─ BatteryMonitor
│ │ │ └─ BatteryMonitor.hpp
│ │ ├─ Encoder
│ │ │ ├─ Encoder.hpp
│ │ │ ├─ EncoderReader.cpp
│ │ │ └─ EncoderReader.hpp
│ │ ├─ Matrix
│ │ │ ├─ Matrix.hpp
│ │ │ ├─ MatrixScanner.cpp
│ │ │ └─ MatrixScanner.hpp
│ │ ├─ PinAlias.hpp
│ │ └─ PinUtils.hpp
│ ├─ Parent
│ │ ├─ TWEBridgeParent-handlers.cpp
│ │ ├─ TWEBridgeParent.cpp
│ │ └─ TWEBridgeParent.hpp
│ ├─ AppConfig.hpp
│ ├─ ConfigMacros.hpp
│ ├─ DeviceConfig.hpp
│ └─ main.cpp
├─ parent
│ └─ Config.hpp
└─ device
├─ device1
│ └─ Config.hpp
├─ device2
│ └─ Config.hpp
...
...
いずれにしろ TWELITE の Makefile では難しそうなので、ビルドできるようにファイル構成を平滑化してビルドできるようにする必要があるかなと思っています。
こんな感じで build_space とか、適当なワークスペースに必要なファイルを丸ごとコピーしてビルドするとか考えてます。
TWEBridge
├─ build_space
│ └─ <name> // parent / device1 / etc... ビルド対象のデバイス名
│ ├─ build // TWELITE ビルドで必要なディレクトリ
│ │ └─ Makefile // TWELITE ビルドの Makefile
│ ├─ <name>.cpp // main.cpp をリネーム(ビルドの起点となる)
│ ├─ twb_compiler.h // 以下、必要なソースをコピー
│ ├─ twb_encoder.h
│ ├─ twb_keyboard_definitions.h
│ ├─ twb_matrix.h
│ ├─ twb_queue.c
│ ├─ twb_queue.h
│ ├─ twb_uart_parser.c
│ ├─ twb_uart_parser.h
│ ├─ twb_packet.h
│ ├─ twb_packet_v1.h
│ ├─ TWEBridgeParent-handlers.cpp
│ ├─ TWEBridgeParent.cpp
│ ├─ TWEBridgeParent.hpp
│ ├─ TWEBridgeChild-handlers.cpp
│ ├─ TWEBridgeChild.cpp
│ ├─ TWEBridgeChild.hpp
│ ├─ BatteryMonitor.hpp
│ ├─ Encoder.hpp
│ ├─ EncoderReader.cpp
│ ├─ EncoderReader.hpp
│ ├─ Matrix.hpp
│ ├─ MatrixScanner.cpp
│ ├─ MatrixScanner.hpp
│ ├─ PinAlias.hpp
│ ├─ PinUtils.hpp
│ ├─ AppConfig.hpp
│ ├─ ConfigMacros.hpp
│ ├─ DeviceConfig.hpp
│ └─ Config.hpp
├─ qmk
├─ twb_core
├─ twb_protocol
└─ twelite
├─ src
├─ parent
│ └─ Config.hpp
└─ device
├─ device1
│ └─ Config.hpp
├─ device2
│ └─ Config.hpp
...
...
なお、これでうまくいくかはなんも検証してません。
できたらいいな、という妄想です。
おわりに
とりあえず、今やってることを雑多に書きました。
コードは書いてますが、これがちゃんと動くかどうかは未知数です。
昔に、マトリクススキャンして、押されたキーの位置を送信し、シリアルに表示する、みたいなことはできた記憶があるので、たぶんできるんじゃないかとは思っています。
面倒になったら、未完成で github に上げるかもしれないので、その時は、誰か遺志を継いで挑戦してみてください。









