M5Unified入門 その4 ボタン操作

概要

ボード紹介の途中ですが、ボタンまわりの操作を調べたのでまとめてみたいと思います。

ボード別ボタン一覧

M5Unifiedにはボタンが5種類定義されており、ボードごとに割り振りが異なっています。

ボードBtnABtnBBtnCBtnPWRBtnEXT
BASIC
(GPIO39)
中央
(GPIO38)
右(GPIO37)
GRAY
(GPIO39)
中央
(GPIO38)
右(GPIO37)
M5GO
(GPIO39)
中央
(GPIO38)
右(GPIO37)
FIRE
(GPIO39)
中央
(GPIO38)
右(GPIO37)
CORE2
(タッチ)
中央
(タッチ)

(タッチ)
電源
(AXP192)
CORE2 for AWS
(タッチ)
中央
(タッチ)

(タッチ)
電源
(AXP192)
CORES3電源
(AXP2101)
M5StickC中央(GPIO37)右側面(GPIO39)電源(AXP192)
M5StickC PLUS中央
(GPIO37)
右側面
(GPIO39)
電源
(AXP192)
CORE.INK
(GPIO37)
押込
(GPIO38)

(GPIO39)
電源
(GPIO27)
上側面
(GPIO5)
M5PAPER
(GPIO37)
押込
(GPIO38)

(GPIO39)
ATOM LITE中央
(GPIO39)
ATOM MATRIX中央
(GPIO39)
ATOM ECHO中央
(GPIO39)
ATOM U中央
(GPIO39)
AtomS3中央
(GPIO41)
AtomS3 Lite中央
(GPIO41)
AtomS3 U中央
(GPIO41)
TOUGH電源
(AXP192)
STATION 485
(GPIO37)
中央
(GPIO38)

(GPIO39)
電源
(AXP192)
STATION BAT
(GPIO37)
中央
(GPIO38)

(GPIO39)
電源
(AXP192)
STAMP PICO中央
(GPIO39)
STAMP C3中央
(GPIO3)
STAMP C3U中央
(GPIO9)
StampS3中央
(GPIO0)

3ボタン(Core系)

Core系は物理ボタンがA、B、Cと横並びで並んでいます。Core2もタッチパネルになっていますが3ボタンと電源ボタンが取得できます。ただし、電源ボタンは長押しをすると電源オフになるので、クリックしかできません。

2ボタン(Stick系)

M5StickCとM5StickC PlusはメインボタンのAと右側面にB、そしてAXP192経由で電源ボタンがあります。電源ボタンは長押しできないので注意してください。

1ボタン(ATOM、Stamp系)

ATOM系は画面相当の場所を押し込むとA、Stamp系は中央にあるボタンがAになっています。

マルチファンクション(E-INK系)

CORE.INKとM5PAPERはマルチファンクションボタンを採用しており、上がA、押し込むとB、下がCとなっています。CORE.INKは電源ボタンも普通のGPIOなので長押しが可能で、さらに上側面に追加ボタンもあります。

ボタン無し(CoreS3、TOUGH)

CoreS3もTOUGHもタッチパネルを搭載しているのですが、ボタン用のエリアが確保されていません。そのためボタン系の処理は電源ボタンのみになります。ボタンのかわりに自前でタッチパネルで操作を実装する必要があります。

BtnAがありませんので他のボードと共通ロジックにする場合には注意が必要です。

ボタンクラスの関数概要

状態更新

M5.update();

上記の関数をボタンの処理を行う前に呼び出す必要があります。この関数の中で各ボタンの状態を取得して、ボタンクラスの内部状態を更新しています。

基本関数

関数概要
bool isPressed()今現在ボタンが押されているか
bool isReleased()今現在ボタンが押されていないか
bool isHolding()今現在ボタンを0.5秒以上押しているか
bool wasChangePressed()ボタンの状態が変わったか
bool wasPressed()ボタンを押したか
bool wasReleased()ボタンを押して離したか
bool wasClicked()ボタンを短時間クリックしたか
bool wasHold()ボタンを0.5秒以上押したか
bool wasReleasedAfterHold()ボタンを0.5秒以上押して離したか
bool wasSingleClicked()ボタンをクリックして0.5秒経過したか
bool wasDoubleClicked()ボタンを0.5秒以内に2回クリックして0.5秒経過したか
bool wasDecideClickCount()ボタンを何度かクリックして0.5秒経過したか
std::uint8_t getClickCount()ボタンを連続クリックした回数を取得

よく使う関数として、上記のような関数があります。

is系関数

今現在の状態を取得します。これは何度呼び出してもM5.update()を実行したときの状態が返却されます。

was系関数

ボタンの状態を一度のみ取得する関数です。次にM5.update()を実行することで、そのフラグは消えてしまいますので、一度だけ処理したい場合にはwas系関数を利用してください。

isPressed()はボタンを押している間は常にtrueになりますが、wasPressed()はボタンを押してから最初にM5.update()を実行した時だけtrueになり、次にM5.update()を実行するとボタンが押されていてもfalseになります。

get系関数

getClickCount()にも内部の状態を取得する関数などがあります。特殊な状態以外ではあまり利用しないと思います。

その他の関数

xxxFor(ms)関数は指定時間を経過したかを取得することができます。一定以上長く押している場合などの利用方法がありますがあまり使いません。

set系関数もあり、しきい値などを変更可能です。

ボタンの動作確認

#include <M5Unified.h>

void setup(void) {
  M5.begin();
}

void loop(void) {
  String printStr = "";
  static String oldStr = "";

  delay(1);
  M5.update();

  printStr += String(",isPressed:") + String(M5.BtnA.isPressed() ? "X" : " ");
  printStr += String(",isHolding:") + String(M5.BtnA.isHolding() ? "X" : " ");
  printStr += String(",isReleased:") + String(M5.BtnA.isReleased() ? "X" : " ");

  printStr += String(",wasPressed:") + String(M5.BtnA.wasPressed() ? "X" : " ");
  printStr += String(",wasHold:") + String(M5.BtnA.wasHold() ? "X" : " ");
  printStr += String(",wasReleased:") + String(M5.BtnA.wasReleased() ? "X" : " ");
  printStr += String(",wasClicked:") + String(M5.BtnA.wasClicked() ? "X" : " ");

  printStr += String(",wasSingleClicked:") + String(M5.BtnA.wasSingleClicked() ? "X" : " ");
  printStr += String(",wasDoubleClicked:") + String(M5.BtnA.wasDoubleClicked() ? "X" : " ");
  printStr += String(",wasDecideClickCount:") + String(M5.BtnA.wasDecideClickCount() ? "X" : " ");
  printStr += String(",wasChangePressed:") + String(M5.BtnA.wasChangePressed() ? "X" : " ");
  printStr += String(",wasReleasedAfterHold:") + String(M5.BtnA.wasReleasedAfterHold() ? "X" : " ");

  printStr += String(",getClickCount:") + String(M5.BtnA.getClickCount());

  if (oldStr != printStr) {
    Serial.println(printStr);
    oldStr = printStr;
  }
}

上記のスケッチで主要な関数の動きを確認することができると思います。

短いクリック時の動き

イベントisPressedisReleasedwasPressedwasReleasedwasClickedwasSingleClickedwasDecideClickCountwasChangePressedgetClickCount
ボタンを押したXXX0
2度目はwas系はfalseX0
ボタンを離したXXXX1
2度目はwas系はfalseX1
次のクリックがこないのでシングルクリック判定XXX1
getClickCountの初期化X0

変化があるものだけ抜き出しています。is系は常にその時の状態を表しています。isReleased()は基本的にほぼtrueになるのであまり使わないと思います。

ボタンを押すとwasPressed()がtrueになります。その後ボタンを離すとwasReleased()とwasClicked()がtrueになります。wasReleased()は常にボタンを離したときにtreueになりますが、0.5秒以上ボタンを押しているとクリック判定ではなくホールド判定になりwasClicked()がtrueにならない場合があります。

その後0.5秒間ボタン操作が無いと連続クリックした回数が確定しwasSingleClicked()がtrueになります。0.5秒以内に次のクリックをすると連続クリック数がカウントアップして、2回の場合にはwasDoubleClicked()がtreueになります。ただし、ボタンを離してから0.5秒間経過しないと連続ボタンのクリック数が確定しないため、リアルタイムに反応がほしいときにはテンポが悪くなるためあまりおすすめしません。

ホールド時の動き

イベントisPressedisHoldingisReleasedwasPressedwasHoldwasReleasedwasChangePressedwasReleasedAfterHold
ボタンを押したXXX
2度目はwas系はfalseX
0.5秒以上押していたのでホールドXX
isHoldingは2回以上ホールドの場合XX
ボタンを離したXXXX
2度目はwas系はfalseX

0.5秒以上ボタンを押していると長押しのホールド判定がされます。wasHold()とwasReleasedAfterHold()でホールドの開始と完了が判定できます。

状態別の判定方法

ボタンを押した瞬間

wasPressed()で判定すると、ボタンを押した瞬間のイベントを実行することができます。とにかく早めの動作を実現するときには押した瞬間で判定するのが好ましいです。

ボタンが押されているのかの判定

isPressed()で現在の状態を判定します。押している間に何らかの処理をする場合の判定となります。

ボタンの押し方で2種類の判定

シングルクリックとダブルクリックは0.5秒の判定時間があるのでリアルタイム性の制御では使わないほうがよいと思います。シングルクリックで次へ、ダブルクリックで前に戻るなどの若干遅延があっても許容できる制御では使いやすいと思います。

リアルタイム性がある場合にはクリックとホールドの使い分けがおすすめです。

wasClicked()が押している時間が0.5秒以内でのクリック。wasReleasedAfterHold()が押している時間が0.5秒以上のホールドからボタンを離した状態を判定できます。この2つであれば押している時間で使い分けができ、離した瞬間に確定できるので使いやすいです。また、通常はクリックで処理をしていて、長押しのホールドの場合には無視するなどの押し間違え時のキャンセルを許容する場合にはwasPressed()のかわりにwasClicked()で判定をすると長押し時にはボタンを押したのをキャンセルする動きができます。

同時押し判定

#include <M5Unified.h>

void setup(void) {
  M5.begin();
}

void loop(void) {
  static bool multiPress = false;

  delay(1);
  M5.update();

  if (M5.BtnA.wasPressed() || M5.BtnB.wasPressed() || M5.BtnC.wasPressed()) {
    if (M5.BtnA.isPressed() && M5.BtnB.isPressed() && M5.BtnC.isPressed()) {
      multiPress = true;
      Serial.println("Multi Press: BtnA + BtnB + BtnC");
    } else {
      if (M5.BtnA.isPressed() && M5.BtnB.isPressed()) {
        multiPress = true;
        Serial.println("Multi Press: BtnA + BtnB");
      }
      if (M5.BtnA.isPressed() && M5.BtnC.isPressed()) {
        multiPress = true;
        Serial.println("Multi Press: BtnA + BtnC");
      }
      if (M5.BtnB.isPressed() && M5.BtnC.isPressed()) {
        multiPress = true;
        Serial.println("Multi Press: BtnB + BtnC");
      }
    }
  }

  if (multiPress) {
    if (M5.BtnA.isReleased() && M5.BtnB.isReleased() && M5.BtnC.isReleased()) {
      // 全部のボタンを離したら同時押し判定をクリア
      multiPress = false;
    }
  } else {
    // 同時押しでない場合にのみ通常クリックを取得
    if (M5.BtnA.wasClicked()) {
      Serial.println("BtnA: Short Click");
    }
    if (M5.BtnA.wasReleasedAfterHold()) {
      Serial.println("BtnA: Long Press Click");
    }
    if (M5.BtnB.wasClicked()) {
      Serial.println("BtnB: Short Click");
    }
    if (M5.BtnB.wasReleasedAfterHold()) {
      Serial.println("BtnB: Long Press Click");
    }
    if (M5.BtnC.wasClicked()) {
      Serial.println("BtnC: Short Click");
    }
    if (M5.BtnC.wasReleasedAfterHold()) {
      Serial.println("BtnC: Long Press Click");
    }
  }
}

ボタンが押されたwasPressed()時に、他のボタンが押されているのかをisPressed()で判定をしています。3ボタン同時押しは、2ボタン同時押し判定後に3ボタン同時押しになるのであまり使わないほうがよいと思います。

そのままですとボタンを離したときに通常のイベントが発生してしまうので、同時押しが発生した場合にはフラグを立てて、すべてのボタンが離すまではwasClicked()の判定をしないようにガードする必要があると思います。

まとめ

基本はwasPressed()で押して離したことを判定するのが無難です。複雑なボタンの押し方は操作方法の表示が難しいのでなるべく使わないほうがよいと思います。

(追記)ハンズオン資料

GitHub - lovyan03/M5Unified_HandsOn: M5Unifiedハンズオンイベント用飼料
M5Unifiedハンズオンイベント用飼料. Contribute to lovyan03/M5Unified_HandsOn development by creating an account on GitHub.

上記にてM5Unified作者であるらびやんさんのハンズオンセミナーの資料が公開されています。そこにボタン系の制御も載っていました!

がんばって調べたのですが、この記事を書いたあとに資料を見ました。スライドとサンプルコードもあるので非常に参考になる資料です。

コメント