M5Dialをトラックパッド化する

この記事はキーボード #1 Advent Calendar 2023の11日目の記事です。

本記事で扱うのはキーボードではなくトラックパッドですが、キーボードとは切っても切れない関係と思いますので、ご容赦ください。

tl;dr

  • M5 DialはM5Stackが発売する、大きなロータリエンコーダにタッチパネルが付いた開発モジュールだよ
  • 載っているESP32-S3(M5StampS3)は、USBデバイス機能があるので、ArduinoでUSB Mouse、Keyboardを作ることができるよ
  • M5Stackのタッチパネルセンサはチューニングされているみたいで、割と素直に開発できるよ
  • M5 Dialをハンドデバイス化したら、それだけで完成度があって使いやすいモジュールになったよ

M5 Dialとは

M5 DialとはM5Stackが発売するロータリーエンコーダー付きモジュールです。

M5Stack Dial ESP32S3 スマートロータリーノブwww.switch-science.com

大きなロータリーエンコーダを持っており、ぐりぐり回して操作することを想定したデバイスになっています。 加えて、中央部分にはタッチパネルが付いており、回す以外にもタッチして操作することが可能です。

私自身、当初はこのデバイスを使って、スマートホームを直感的に操作するようなデバイスが作りたいと思っていました。

ESP32-S3はUSBデバイス機能がある

このモジュールは、内部にESP32-S3を使った開発ボードM5StampS3が搭載されています。ESP32-S3には、ESP32にはなかったUSBデバイス機能が搭載されており、USBキーボードやマウスとして動作させることもできます。そしてこのデバイスのUSB-C端子はESP32-S3に直結されています。

さらには、ESP32のArduinoライブラリ自体にUSBキーボード、マウスのためのライブラリが含まれているため、こちらを呼び出せばUSBマウスとして動作させることができます。

実際のマウスに関するコードもこれだけでした。

#include "USB.h"
#include "USBHIDMouse.h"

USBHIDMouse Mouse;

void setup()
{
    Mouse.begin();
    USB.begin();
}

void loop()
{
    // 移動
    // dx、dyにマウスの移動量が入る
    Mouse.move(dx * 5, dy * 5, 0, 0);

    // ホイールの操作
    // dwにホイールの回転量が入る
    Mouse.move(0, 0, dw, 0);

    // クリック
    Mouse.click(MOUSE_LEFT);
}

マウスとして動作させるには、Arduino Board Configurationで、USB ModeをUSB-OTG(TinyUSB)に変更する必要があります。 さらにUSB-OTGに変更すると、UploadとしてUSB経由で行う際に、一度ケーブルを抜いて、M5StampS3のBTN0を押しながら差し込んで、書き込みを待ち受けるモードにする必要があります。ちょっとだけイテレーションがしにくいですが、許容範囲内だと思います。

タッチパネルの処理

M5Stackのタッチパネルはライブラリを使うと、非常にチューニングされた状態で利用することができます。

docs.m5stack.com

まず、初期化は以下のコードだけです。

void setup()
{
    auto cfg = M5.config();
    M5Dial.begin(cfg, true, false);
    Mouse.begin();
    USB.begin();
}

loopの中で、以下の2つの命令を呼ぶと、タッチパネルの状態を取得できます。

M5Dial.update();
auto t = M5Dial.Touch.getDetail();

t.state には「触れていない(none)」「タッチされている(touch)」「長押しされている(hold)」「フリックされている(flick)」などと、操作に応じて入ります。座標データt.x、t.yだけでなく、長押ししていることや、短くタッチした、短くタッチして移動した(フリック)などの操作の検出も行ってくれるため、簡単に実装することができます。

実際に、マウスの移動に関する処理は以下だけでした。

int prev_x = -1;
int prev_y = -1;
static m5::touch_state_t prev_state;

int alpha_count = 0;

bool touched = false;
bool first_move = false;

void loop()
{
    M5Dial.update();
    auto t = M5Dial.Touch.getDetail();

    if (prev_state != t.state)
    {
        prev_state = t.state;
        static constexpr const char *state_name[16] = {
            "none", "touch", "touch_end", "touch_begin",
            "___", "hold", "hold_end", "hold_begin",
            "___", "flick", "flick_end", "flick_begin",
            "___", "drag", "drag_end", "drag_begin"};
        Serial.println(state_name[t.state]);
        if (t.state == m5::touch_state_t::touch)
        {
            // タッチ開始
            touched = true;
            first_move = true;
            prev_x = t.x;
            prev_y = t.y;
            Serial.println("TOUCH X:" + String(t.x) + " / " + "Y:" + String(t.y));
        }
        if (t.state == m5::touch_state_t::touch_end)
        {
            // 移動せずに離した(タップ)
            Serial.println("LEFT CLICK!!!");
            Mouse.click(MOUSE_LEFT);
        }
        if (t.state == m5::touch_state_t::none)
        {
            // 指を離した
            touched = false;
            M5Dial.Display.fillRect(0, 0, 240, 240, BLACK);
        }
    }
    if (touched && (prev_x != t.x || prev_y != t.y))
    {
        // タッチ中である
        int8_t dx = (int16_t)t.x - (int16_t)prev_x;
        int8_t dy = (int16_t)t.y - (int16_t)prev_y;

        Serial.println("MOVE  X:" + String(t.x) + " / " + "Y:" + String(t.y) + " / " + "DX:" + String(dx) + " / " + "DY:" + String(dy));
        Mouse.move(dx * 5, dy * 5, 0, 0);
        prev_x = t.x;
        prev_y = t.y;
        M5Dial.Display.drawCircle(t.x, t.y, 5, RED);
    }
}

エンコーダーの処理

エンコーダーの処理は、M5Dialのライブラリから今のポジションの値が取れるので、それをホイールに変換するだけです。 エンコーダーを読み取るための制御部分は一切書かずに、ポジションの値だけをみればよく非常に簡単に実装できます。

行ったチューニング

サクッとできたデバイスですが、操作してみると、移動の最初だけびよっと飛ぶ現象がありました。おそらくM5Dialがタップかフリックか判定するためのデッドゾーンがあるのだと思います。

ちょっと操作感が良くなかったため、最初の移動量はサンプリングから外す様にしました。

できたソースコード

Arduinoで実装したコードはこちらで公開しています。

github.com

できあがったもの

当初の期待を超える、すぐに実用できるレベル操作感のデバイスができあがりました。この時の感動を、Xに動画としてあげました。

こちらには、自分のTwitter歴で最大の、1,000以上のイイネをいただくことができました。

これをさらに天キーvol.5で展示するために、段ボールでグリップを作りました。

こちらは段ボールでの試作だったのですが、割とこれだけでしっくりくる感じになりました。

天キーで実際に触っていただくと、操作感の良さを多くの方に体験いただけました(何度もさわりに来た方がいました😊)。

今までジョイスティックでチューニングして作っていたマウスの体験を、遙かに凌駕する応答性の良さで、ジョイスティックモジュールに対する情熱がちょっと失われつつあります。。。

さらにやってみたいこと

このハンドデバイスが思いのほか、操作感が良くなっていました。 おそらく、M5Dialのデバイスとしての完成されていることと、M5Dialのライブラリが扱いやすいことに起因していると思っています。

段ボール試作だけでも十分だったのですが、以下のようなことにも取り組みたいと思っています。

  • 3Dプリンタで完成されたハンドデバイスにする
  • スイッチを増やして、キーボードデバイスとしても動作できるようにして、TV横PC操作デバイスとして完璧なものにする(例えば、ESC、Ctrl-W、F(YouTubeの全画面表示切り替え)を搭載する)
  • WiFi経由でTVリモコンを操作して、TVの電源オンオフをさせたい
  • ケース付きキーボード似組み込みたい

ESP32-S3のUSBキーボードでは、スリープしたPCをwakeupさせることができないことがわかっています。 これには、CH9329を組み込んでUSBキーボード、マウスの操作はになわせることで、実現したいと考えています。 CH9329の利用については、技術書典15の新刊で解説しています。是非見てみてください。

74th.booth.pm

これをタッチパッド兼ロータリーエンコーダとしてキーボードに組み込みたいと考えています。その際、2つあるポートA/Bとキーボードを繋ぐと良いのですが、ポートA/Bから電源供給はできないのを確認しています(M5StackCoreではできてそう)。なので、最低でも、PortAと電源を接続する必要があります。