M5StackCore2、M5Dialをトラックパッドデバイスにして、QMK Firmwareから利用する

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デバイス化していました。

74th.hateblo.jp

あまりにくM5Dialをトラックパッドしたときの操作感が良すぎて、これらをキーボードに組み込んだトラックパッドとして使ってみたいと思いました。

QMK Firmwareで独自ポインティングデバイスを実装する

ジョイスティックデバイスをPIMORONI Trackball互換I2Cデバイスとして実装し、QMK Firmwareで利用できるようにしたことを、既に記事に書きました。

74th.hateblo.jp

この時、QMK Firmwareを理解するために、QMK FirmwareのPIMORONIトラックボールソースコードを読み込んでいました。このソースコード自体はそんなに行数はありません。定期的に呼ばれるハンドラー関数があり、そこでI2Cで受け取ったデータを、マウスの捜査情報を示すreport_mouse_t構造体に展開していれているのが基本です。それについかして、クリック時にポインターがブレるのを抑制する処理や、値が小さいので固定値をかける処理などです。

要するに、ハンドラー関数で、report_mouse_t構造体に詰めるデータを作れば良いのです。

さらにQMK Firmwareには独自のポンターデバイスを実装するためのハンドラーが用意されています。

github.com

# 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キーボードとして以下のリポジトリに置いています。

github.com

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の以下のリポジトリで公開しています。

github.com

どうな感じになったか

M5Dialをチューニングして、このようになりました。

  • ポインタ移動: スライド
  • スクロール: ロータリーエンコーダ
  • 左クリック: タップ
  • 右クリック: ボタン
  • 左クリックドラッグ: タッチパネルに触れているときにボタンを押す(タッチ中はボタンが左クリックとして動作)

M5StackCore2をチューニングして、このようになりました。2本指操作が読み取りにくいという問題があったのですが、向きを変えることで少しマシになりました。

  • ポインタ移動: スライド
  • スクロール: 2本指スライド
  • 左クリック: タップ、もしくは左下の領域にふれる
  • 右クリック: 2本指タップ
  • ドラッグ: 左下の領域に触れた状態で、もう1本の指でスライド

これを組み込んだキーボードSparrowDialを作りました。早速使っていますが、ちょっと小さいトラックパッドですが、思いのほか十分に使えています。むしろ、Sparrow60Cキーボードからスペースの都合、キーを減らしたことによるキー配置の変化に四苦八苦しています。

キーケットで販売予定

3/2の自作キーボード即売会『キーケット』にて、この組み込んだキーボードSparrowDialを頒布予定です。現在入場券が販売されています。ご興味がありましたら是非お越し下さい。

keeb-market.jp

今回作成したコードはOSSとして公開中

今回作成したqmk firmwareGPL)と、M5StackCore2、M5Dialのコード(MIT)は、紹介したとおり公開リポジトリに置いています。なので、任意のキーボードに組み込もうと思えば組み込むことが可能です。是非トライしてみてください。

また、今回行った操作性へのチューニングについては、別途また記事にしようと思います。