tl;dr
- M5StackCore2とM5Dialには、タッチパッドを備えているよ
- QMK Firmwareには、カスタムにポインターデバイスをくみこむためのハンドラーがあるよ
- M5StackCore2とM5DialのPortA端子には、I2Cが使えるようになっているよ。QMK FirmwareのRP2040から、M5StackCore2とM5DialをI2Cデバイスとして通信して、このハンドラーに処理を書くことで、ポインターデバイスとして動作させたよ
- この制作したSparrowDialキーボードはキーケットで頒布するよ!
M5StackCore2、M5Dialのタッチパッド
M5StackCore2は、M5Stackが販売する、WiFi、BLE機能を持ったマイコンESP32に、ディスプレイ、タッチセンサー、加速度センサ、スピーカなど、様々センサーと更にバッテリーを組み込んだデバイスです。IoT機器を作る電子工作には、非常に便利なものです。
M5Stack Core2 v1.1www.switch-science.com
※画像は公式より引用
M5Dialは、同じくM5Stackが販売する、ロータリーエンコーダー、ディスプレイ、タッチセンサーなどを組み込んだESP32を使ったデバイス です。
M5Stack Dial ESP32S3 スマートロータリーノブwww.switch-science.com
M5Dialを使ってトラックパッドデバイスを作ったことは既に記事にしました。この時は、マイコンESP32-S3のUSBデバイス機能を使い、M5Dial単体でUSBデバイス化していました。
あまりにくM5Dialをトラックパッドしたときの操作感が良すぎて、これらをキーボードに組み込んだトラックパッドとして使ってみたいと思いました。
QMK Firmwareで独自ポインティングデバイスを実装する
ジョイスティックデバイスをPIMORONI Trackball互換I2Cデバイスとして実装し、QMK Firmwareで利用できるようにしたことを、既に記事に書きました。
この時、QMK Firmwareを理解するために、QMK FirmwareのPIMORONIトラックボールのソースコードを読み込んでいました。このソースコード自体はそんなに行数はありません。定期的に呼ばれるハンドラー関数があり、そこでI2Cで受け取ったデータを、マウスの捜査情報を示すreport_mouse_t構造体に展開していれているのが基本です。それについかして、クリック時にポインターがブレるのを抑制する処理や、値が小さいので固定値をかける処理などです。
要するに、ハンドラー関数で、report_mouse_t構造体に詰めるデータを作れば良いのです。
さらにQMK Firmwareには独自のポンターデバイスを実装するためのハンドラーが用意されています。
# rules.mk POINTING_DEVICE_DRIVER = custom
void pointing_device_driver_init(void) {} report_mouse_t pointing_device_driver_get_report(report_mouse_t mouse_report) { return mouse_report; } uint16_t pointing_device_driver_get_cpi(void) { return 0; } void pointing_device_driver_set_cpi(uint16_t cpi) {}
このpointing_device_driver_get_report関数さえ実装すれば良いことがわかりました。
まず、プロトコルを実装する
ジョイスティックを使ったStickPointVでは、PIMORONI Trackball互換デバイスとしてそのプロトコルを実装しましたが、いかんせんトラックボールのプロトコルのため右クリックやスクロールなどの情報を伝えることができません。データは以下のような構造体です。
typedef struct { uint8_t left; uint8_t right; uint8_t up; uint8_t down; uint8_t click; } pimoroni_data_t;
そのため、独自プロトコルを実装することにしました。といっても受け取るデータを、report_mouse_t構造体にはめるためのデータに置き換えただけです。
typedef struct { uint8_t click; int8_t pointer_x; int8_t pointer_y; int8_t wheel_h; int8_t wheel_v; } simple_pointer_data_t;
これにより、QMK Firmware側の処理は簡単に書くことができます。どうせ、デバイス側でチューニングするので、QMK Firmware側の実装は単純にしました。
// sparrowdial.c #define SIMPLE_POINTER_ADDRESS 0x0B #define SIMPLE_POINTER_REG_POINTER 0x04 #define SIMPLE_POINTER_LEFT_CLICK 1 #define SIMPLE_POINTER_RIGHT_CLICK 1 << 1 #define SIMPLE_POINTER_MIDDLE_CLICK 1 << 2 report_mouse_t pointing_device_driver_get_report(report_mouse_t mouse_report) { // 受け取ったデータ simple_pointer_data_t data = {0}; // I2Cからデータ受信 i2c_status_t status = i2c_readReg(SIMPLE_POINTER_ADDRESS << 1, SIMPLE_POINTER_REG_POINTER, (uint8_t*)&data, sizeof(data), SIMPLE_POINTER_TIMEOUT); // 受信に成功したら、report_mouse_t構造体に入れる if (status == I2C_STATUS_SUCCESS) { mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, data.click & SIMPLE_POINTER_LEFT_CLICK, POINTING_DEVICE_BUTTON1); mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, data.click & SIMPLE_POINTER_RIGHT_CLICK, POINTING_DEVICE_BUTTON2); mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, data.click & SIMPLE_POINTER_MIDDLE_CLICK, POINTING_DEVICE_BUTTON3); mouse_report.x = data.pointer_x * SIMPLE_POINTER_SCALE; mouse_report.y = data.pointer_y * SIMPLE_POINTER_SCALE; mouse_report.v = data.wheel_v; mouse_report.h = data.wheel_h; } return mouse_report; }
これで、ほとんどの処理をトラックパッドデバイス側で制御できるようになります。
この実装は、SparrowDialキーボードとして以下のリポジトリに置いています。
M5StackCore2、M5DialをI2Cデバイスとして使う
あとは、M5StackCore2、M5DialをI2Cデバイス化すれば良いです。M5Stackの製品のPortAと呼ばれるGroveソケットのポートには、I2C機能のあるピンと接続されています。
ESP32では、I2C Maste(制御元側)rとI2C Slave(デバイス側)は同じピンでできるようになっています。Arduino FrameworkにもI2C Slaveの機能はあり、どちらのデバイスもその機能を呼び出すことでI2Cデバイスとして動作させることができました。
#include <Arduino.h> #include <M5Dial.h> #include <Wire.h> #define I2C_SLAVE_ADDRESS 0x0B void setup() { auto cfg = M5.config(); M5Dial.begin(cfg, true, false); // 引数にI2C Slaveアドレスがある呼び方をすると、I2C Slaveとして動く Wire.begin(I2C_SLAVE_ADDRESS, G13, G15, 400000); // I2C Masterから送信、受信要求されたときに割り込まれる関数を指定 Wire.onReceive(i2c_receive_event); Wire.onRequest(i2c_send_event); }
受信、送信する関数は以下のようにかけます。i2c_bufには、loop関数内でタッチパネルの操作を書き込みます。
// 受け渡しに使う変数 // loop関数内でここに値をセットする volatile simple_pointer_data_t i2c_buf = {0}; // 受信 void i2c_receive_event(int numBytes) { // 特に何も受信しないので、読み出して捨てる for (int i = 0; i < numBytes; i++) { Wire.read(); } } // 送信 void i2c_send_event() { int n = 0; int i; // 構造体をuint8_tのリストとして処理する uint8_t *raw_buf = (uint8_t *)&i2c_buf; // 1バイトずつ送る for (i = 0; i < 5; i++) { Wire.write(raw_buf[i]); raw_buf[i] = 0; } }
なお、このM5StackCore2、M5Dialで実装したトラックパッドモジュールはGitHubの以下のリポジトリで公開しています。
どうな感じになったか
M5Dialをチューニングして、このようになりました。
M5DialのTrackpad化のチューニングが良い感じになってきた。
— 74th (@74th) February 1, 2024
スライドでポインタ移動、タップで左クリック、ボタンで右クリック、ロータリーエンコーダーでホイール。さらき、タッチパネルに触っている間にボタンを押すと左クリックになってドラッグまでできるようにした。https://t.co/1uHZIyoo7S pic.twitter.com/rH0TacwnsX
- ポインタ移動: スライド
- スクロール: ロータリーエンコーダ
- 左クリック: タップ
- 右クリック: ボタン
- 左クリックドラッグ: タッチパネルに触れているときにボタンを押す(タッチ中はボタンが左クリックとして動作)
M5StackCore2をチューニングして、このようになりました。2本指操作が読み取りにくいという問題があったのですが、向きを変えることで少しマシになりました。
- ポインタ移動: スライド
- スクロール: 2本指スライド
- 左クリック: タップ、もしくは左下の領域にふれる
- 右クリック: 2本指タップ
- ドラッグ: 左下の領域に触れた状態で、もう1本の指でスライド
M5StackCore2 のTrackpad化のチューニング、2点タッチで横向きが認識しにくい問題を、ディスプレイの向きを変えることでわりと解決した。2本指タップで右クリック、2本指スライドでのホイール操作を追加。さらに左下を左クリックボタンにしてドラッグ操作を実現できた。https://t.co/XuddCrGJ1s pic.twitter.com/gtmHwaEEwu
— 74th (@74th) February 2, 2024
これを組み込んだキーボードSparrowDialを作りました。早速使っていますが、ちょっと小さいトラックパッドですが、思いのほか十分に使えています。むしろ、Sparrow60Cキーボードからスペースの都合、キーを減らしたことによるキー配置の変化に四苦八苦しています。
#キーケット に出展するSparrowDial、M5StackCore2用の次に、M5Dialでも組み立てた。
— 74th (@74th) February 4, 2024
汎用ケースにはなんとか収まっている。 pic.twitter.com/RQ9wgmxuCh
キーケットで販売予定
3/2の自作キーボード即売会『キーケット』にて、この組み込んだキーボードSparrowDialを頒布予定です。現在入場券が販売されています。ご興味がありましたら是非お越し下さい。
今回作成したコードはOSSとして公開中
今回作成したqmk firmware(GPL)と、M5StackCore2、M5Dialのコード(MIT)は、紹介したとおり公開リポジトリに置いています。なので、任意のキーボードに組み込もうと思えば組み込むことが可能です。是非トライしてみてください。
また、今回行った操作性へのチューニングについては、別途また記事にしようと思います。