1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

今回は、M5Stack CoreS3 のタッチパネルを使って、PC のマウス入力の代わりになるデバイスを作ってみました

本来の狙いは、マウスを握ったり細かい指の動きを必要とする入力が難しい人に代替手段を提供することです。特に、手の動きが制限された方にとっては、小型・軽量・持ち運びやすい入力デバイスとして、M5Stack のような機器が役立つ可能性があります。

この実験では、単純に「M5Stack をマウスにする」かどうかという話ではなく、「入力の形に応じてマウスカーソルを操作できるか」 という技術検証をしています。私は本来、IMU センサーを使って体の動きや傾きで操作したいと考えていましたが、思ったような挙動にならなかったため、まずは確実に動くタッチ入力で検証を進めました。

結論としては、タッチパネルの「スワイプで移動」「タップでクリック」「長押しでドラッグ」 という、比較的標準的な入力を BLE マウスとして実装できることが確認できました。これが 1 つの技術的な証明になれば、あとから IMU やその他のセンサーを組み合わせるときの足場になります。

今回のゴール

今回の実験でやりたかったことは、次の 3 点です。

  • M5Stack S3 のタッチパネルを、Windows / macOS のマウス入力として扱えるようにする
  • 「タップ」「スワイプ」「長押し」をマウス操作に変換する
  • 小さな入力機器で代替入力の可能性を探る

実際の最終形は、物理的な動きやセンサー入力に応じてマウスカーソルが動く、というものが理想です。今回のコードはその第一歩で、タッチベースの確実な検証としては十分に成立しています。

背景:なぜ M5Stack なのか

M5Stack は小型で軽く、バッテリーも持て、Bluetooth も使えます。特に CoreS3 は、画面とタッチパネルがついていて、入力デバイスとして使いやすいです。

今後の展望としては、次のような入力が考えられます。

  • IMU による傾き入力
  • 圧力センサーやスイッチ入力
  • ヘッドや首の動きに連動する入力
  • 体の振動や動作に応じたマウス操作

一番理想的なのは、何かしらの物理的な動きに対して、マウスカーソルが思ったように動くことです。そこまでいけば、マウスを握るという前提を捨てた代替入力の設計としてかなり有効になります。

ただし、最初の検証としては 手軽に再現できるタッチ入力が最も適切でした。IMU で「すべてが思い通りに動く」とは限らないので、まずはタッチで制御信号を確実に生成し、プログラムの考え方を確認するのが合理的です。

実装のポイント

今回のコードは、BleMouse ライブラリを使って Bluetooth HID のマウスとして PC に認識させることを前提にしています。

  • M5.touch押し始め
  • 押したままの移動量
  • 一定時間の長押し
  • 離した瞬間のクリック判定

を使って、マウスの move() / click() / press() / release() を呼び出しています。

実装コード

#include <Arduino.h>
#include <M5Unified.h>
#include <BleMouse.h>

constexpr float kTouchSensitivity = 1.8f;
constexpr int kTouchMovementThresholdPixels = 4;
constexpr int kMaxMouseDelta = 127;
constexpr uint32_t kClickThresholdMs = 220U;

static_assert(kTouchSensitivity > 0.0f);
static_assert(kTouchMovementThresholdPixels >= 0);

struct TouchpadState {
  bool active = false;
  bool moved = false;
  bool dragHeld = false;
};

TouchpadState touchpadState;
BleMouse bleMouse("M5Stack CoreS3 Touchpad");
uint32_t touchStartMs = 0;
bool statusDrawn = false;
bool displayedConnected = false;
bool displayedActive = false;
bool displayedDragHeld = false;

int clampMouseDelta(int value) {
  if (value < -kMaxMouseDelta) {
    return -kMaxMouseDelta;
  }
  if (value > kMaxMouseDelta) {
    return kMaxMouseDelta;
  }
  return value;
}

bool movedBeyondTapThreshold(const m5::touch_detail_t& touch) {
  return abs(touch.distanceX()) >= kTouchMovementThresholdPixels ||
         abs(touch.distanceY()) >= kTouchMovementThresholdPixels;
}

void moveMouseByTouch(const m5::touch_detail_t& touch) {
  const int deltaX = static_cast<int>(roundf(touch.deltaX() * kTouchSensitivity));
  const int deltaY = static_cast<int>(roundf(touch.deltaY() * kTouchSensitivity));

  if (bleMouse.isConnected() && (deltaX != 0 || deltaY != 0)) {
    bleMouse.move(clampMouseDelta(deltaX), clampMouseDelta(deltaY), 0, 0);
  }
}

void updateTouchpad() {
  M5.update();
  const auto touch = M5.Touch.getDetail();

  if (touch.wasPressed()) {
    touchStartMs = millis();
    touchpadState.active = true;
    touchpadState.moved = false;
    touchpadState.dragHeld = false;
  }

  if (touch.isPressed()) {
    if (movedBeyondTapThreshold(touch)) {
      touchpadState.moved = true;
    }

    if (touchpadState.moved) {
      moveMouseByTouch(touch);
    } else if (!touchpadState.dragHeld &&
               (millis() - touchStartMs) >= kClickThresholdMs) {
      bleMouse.press(MOUSE_LEFT);
      touchpadState.dragHeld = true;
    }
  }

  if (touch.wasReleased()) {
    if (touchpadState.dragHeld) {
      bleMouse.release(MOUSE_LEFT);
    } else if (!touchpadState.moved && bleMouse.isConnected()) {
      bleMouse.click(MOUSE_LEFT);
    }

    touchpadState.active = false;
    touchpadState.moved = false;
    touchpadState.dragHeld = false;
  }
}

void drawInstructions() {
  M5.Display.fillScreen(TFT_BLACK);
  M5.Display.setTextColor(TFT_GREEN, TFT_BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 10);
  M5.Display.printf("BLE Touchpad\n");
  M5.Display.setCursor(10, 120);
  M5.Display.printf("Tap: click\n");
  M5.Display.setCursor(10, 145);
  M5.Display.printf("Swipe: move\n");
  M5.Display.setCursor(10, 170);
  M5.Display.printf("Hold: drag\n");
}

void drawStatus() {
  const bool connected = bleMouse.isConnected();
  if (statusDrawn && connected == displayedConnected &&
      touchpadState.active == displayedActive &&
      touchpadState.dragHeld == displayedDragHeld) {
    return;
  }

  M5.Display.fillRect(10, 40, 300, 55, TFT_BLACK);
  M5.Display.setTextColor(TFT_GREEN, TFT_BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 40);
  M5.Display.printf("Conn: %s\n", connected ? "YES" : "NO");
  M5.Display.setCursor(10, 70);
  M5.Display.printf("Touch: %s\n", touchpadState.dragHeld
                                        ? "DRAG"
                                        : (touchpadState.active ? "ON" : "OFF"));

  statusDrawn = true;
  displayedConnected = connected;
  displayedActive = touchpadState.active;
  displayedDragHeld = touchpadState.dragHeld;
}

void setup() {
  M5.begin();

  M5.Display.setBrightness(90);
  bleMouse.begin();
  drawInstructions();
  drawStatus();
  delay(500);
}

void loop() {
  updateTouchpad();
  drawStatus();
  delay(10);
}

コードの要点と解説

1. BleMouse を使って PC に HID として認識させる

BleMouse bleMouse("M5Stack CoreS3 Touchpad");

これがこの実験の中心です。M5Stack が Bluetooth のマウス HID として PC から見えるようになります。そのため、PC 側では通常のマウスと同じようにテキスト選択やカーソル移動を受け取れます。

PC からすると「Bluetooth マウスが接続された」と見えるので、追加ドライバやアプリは不要です。これは非常に簡単で、かつ実用性が高いです。

2. マウス移動量を制限し、入力を安定させる

constexpr int kMaxMouseDelta = 127;

int clampMouseDelta(int value) {
  if (value < -kMaxMouseDelta) {
    return -kMaxMouseDelta;
  }
  if (value > kMaxMouseDelta) {
    return kMaxMouseDelta;
  }
  return value;
}

マウスの移動量は BleMouse.move() の引数にそのまま渡すと、大きすぎる値が原因でカーソルが飛びすぎることがあります。ここでは 127 に制限をかけ、過剰な移動量を抑えて安定化しています。

入力の感度は kTouchSensitivity で調整可能です。1.8f としているので、タッチの移動量を少しだけ増幅して、実際のマウスの移動に合うようにしています。

3. 「タップか、スワイプか」を判定する

bool movedBeyondTapThreshold(const m5::touch_detail_t& touch) {
  return abs(touch.distanceX()) >= kTouchMovementThresholdPixels ||
         abs(touch.distanceY()) >= kTouchMovementThresholdPixels;
}

ここが入力設計の大事な部分です。タッチが一切動いていないのか、少しだけ動いたのか、十分に動いたのかを判定しています。

  • distanceX / distanceY が小さい:タップと見なす
  • しきい値を超える:スワイプ / 移動と見なす
  • 長押しが続く:ドラッグと見なす

このしきい値を調整すると、画面を軽く触っただけで誤ってドラッグになったり、逆に動きが反応しなかったりする問題を避けられます。

4. タッチの移動をそのままマウス移動に変換する

void moveMouseByTouch(const m5::touch_detail_t& touch) {
  const int deltaX = static_cast<int>(roundf(touch.deltaX() * kTouchSensitivity));
  const int deltaY = static_cast<int>(roundf(touch.deltaY() * kTouchSensitivity));

  if (bleMouse.isConnected() && (deltaX != 0 || deltaY != 0)) {
    bleMouse.move(clampMouseDelta(deltaX), clampMouseDelta(deltaY), 0, 0);
  }
}

touch.deltaX() / touch.deltaY() は、タッチの移動差分です。これを感度係数で補正して、マウスの移動量に変換しています。

実用的には、この値を微調整する話が最も大事です。人が押す強さや指の動きやすさは一人ひとり違うので、感度としきい値は「ユーザーごとに調整できる」設計が必要になります。

5. 長押しでドラッグ、離すと確定する

if (touch.isPressed()) {
  if (movedBeyondTapThreshold(touch)) {
    touchpadState.moved = true;
  }

  if (touchpadState.moved) {
    moveMouseByTouch(touch);
  } else if (!touchpadState.dragHeld &&
             (millis() - touchStartMs) >= kClickThresholdMs) {
    bleMouse.press(MOUSE_LEFT);
    touchpadState.dragHeld = true;
  }
}

if (touch.wasReleased()) {
  if (touchpadState.dragHeld) {
    bleMouse.release(MOUSE_LEFT);
  } else if (!touchpadState.moved && bleMouse.isConnected()) {
    bleMouse.click(MOUSE_LEFT);
  }

  touchpadState.active = false;
  touchpadState.moved = false;
  touchpadState.dragHeld = false;
}

ここでの状態遷移は、入力デバイスとしてはかなり重要です。

  • touch.wasPressed() で開始
  • movedBeyondTapThreshold() で移動判定
  • touchpadState.movedtrue なら カーソル移動
  • それ以外で kClickThresholdMs(220ms)以上の長押しなら 左クリック押下
  • wasReleased()ドラッグ解除 or クリック確定

これにより、「軽く触る」→ クリック「指を動かす」→ カーソル移動「長押し」→ ドラッグ が自然に切り替わります。

ここが面白いところ

この実験の肝は、「マウスを握る」ことそのものが目的ではなく、マウス入力として扱える動作を別の身体動作に置き換えられるか にあります。

人によっては、マウスを手で握ること自体が難しい場合があります。そのようなケースでは、

  • 小さなボタンを押す
  • 頭や首の動きで傾きを検出する
  • 足や腕の微細な動きを検出する
  • IMU で姿勢変更をマウス移動に変える

といった入力方法が、そのまま代替入力へ広がっていきます。

今回のタッチパネルは、入力のメディアが違っても、最終的に HID マウスに変換する設計が重要だということを確認できた良い実験でした。

実際の利用イメージ

以下のような使い方が考えられます。

  • M5Stack を手のひらサイズの小型デバイスにする
  • ボタンの代わりに圧力センサーや IMU を使う
  • 動きそのものを、マウスの操作に変換する
  • アクセシビリティ用途の入出力デバイスとして使う

特に、障害のある方が「マウスを握る」「クリックを正確に打つ」ことが難しい場合、入力の形が違えば行えることはたくさんあるはずです。M5Stack のような小型機器は、入力デバイスを作るうえで非常に強い候補になります。

ハマりどころと注意点

M5Stack で BLE マウスを作るとき、PC 側がちゃんと Bluetooth のマウスとして認識しているか が最初の確認ポイントになります。接続ができていても、bleMouse.begin() の呼び出しやデバイス側の設定が足りないと、マウスとして認識されません。

  • BleMouse.begin() を呼んでいるか
  • 接続先の PC が Bluetooth を受け付けているか
  • M5.begin() の後の初期化順が正しいか
  • タッチのしきい値が大きすぎないか

このあたりは、ほぼすべての実装で最初にぶつかるポイントです。

また、タッチを使う場合、「指が動いたらマウスが動く」のように一瞬で反応しやすい設計は便利ですが、逆に「誤移動」が多くなることもあります。そこは感度と判定ロジックをユーザーごとに調整していくのが実用上重要です。

まとめ

今回の実験では、M5Stack S3 のタッチパネルを BLE マウスに変換して、タップ・スワイプ・長押しドラッグを実現できることを確認しました。

この先の発展としては、次のような可能性があります。

  • IMU を使って 傾き入力でカーソル移動
  • 物理センサーを使って 体の動きをマウスに変換
  • ボタンの数や配置を変えて 個別最適化した入力デバイス を作る

大事なのは、入力デバイスを「マウスそのもの」ではなく、マウス入力に変換できる入力装置として捉えることです。そう考えると、M5Stack のような小型デバイスは、ある意味で「マウスやキーボードを作るためのプラットフォーム」になり得ます。

今回の実験は、その第一歩としてはかなり価値がありました。今後は、IMU や他のセンサーを繋いで、より自然な身体動作に対応する仕組みを実装していきたいです。


このような実験は、単なる趣味の電子工作ではなく、入力デバイスの設計という観点でも大きな意味を持ちます。マウスを握れない人の代替方法を考えるとき、個人の体格・障害・入力のしやすさによって最適な入力方法は変わります。そうした前提を越えて、「動きそのものを入力にできる」 という発想は、アクセシビリティとハードウェアの両方に広がる可能性があります。

今回の試みがその未来の最初の一歩になれば嬉しいです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?