Skip to content

Instantly share code, notes, and snippets.

@lovyan03
Last active July 3, 2024 02:21
Show Gist options
  • Save lovyan03/d68efb62fefdb74e31f965b1387c29fe to your computer and use it in GitHub Desktop.
Save lovyan03/d68efb62fefdb74e31f965b1387c29fe to your computer and use it in GitHub Desktop.
M5Stack/M5StickCでボタンの短押し・長押し・二回押し・同時押し・コマンド入力などの状態取得するサンプル
#if defined( ARDUINO_M5Stack_Core_ESP32 ) || defined( ARDUINO_M5STACK_FIRE ) // M5Stack
#include <M5Stack.h>
#elif defined( ARDUINO_M5Stick_C ) // M5Stick C
#include <M5StickC.h>
#endif
enum
{ btn_a
, btn_b
, btn_c
, BTN_MAX
};
// ボタン状態のビット列を保持する型(ボタン数を9個以上に増やす場合は必要に応じてuint16_tやuint32_tに変更する)
typedef std::uint8_t bits_btn_t;
enum : bits_btn_t
{ bit_btn_a = 1<<btn_a
, bit_btn_b = 1<<btn_b
, bit_btn_c = 1<<btn_c
};
struct command_holder
{
const std::uint32_t value; // ボタンコマンドヒット時の識別値
const std::uint_fast8_t len; // ボタンコマンド長
const bits_btn_t* inputs; // ボタンコマンド配列
};
static constexpr std::size_t BTN_STACK_MAX = 8; // ボタン状態の入力履歴保持数 (コマンド列以上の値にすること)
// 判定対象となるコマンド列の作成
// 例: 0, bit_btn_b, 0, bit_btn_a, 0
// この例にヒットする入力は、 無入力 -> ボタンA -> 無入力 -> ボタンB -> 無入力。 (ボタンBが離された時点でヒット)
// 配列の順序は時系列と逆順になる点に注意
static constexpr bits_btn_t btn_a_single[] = { 0, 0, bit_btn_a, 0, 0 };
static constexpr bits_btn_t btn_b_single[] = { 0, 0, bit_btn_b, 0, 0 };
static constexpr bits_btn_t btn_c_single[] = { 0, 0, bit_btn_c, 0, 0 };
static constexpr bits_btn_t btn_a_hold[] = { bit_btn_a, bit_btn_a, 0 };
static constexpr bits_btn_t btn_b_hold[] = { bit_btn_b, bit_btn_b, 0 };
static constexpr bits_btn_t btn_c_hold[] = { bit_btn_c, bit_btn_c, 0 };
static constexpr bits_btn_t btn_a_double[] = { 0, bit_btn_a, 0, bit_btn_a, 0 };
static constexpr bits_btn_t btn_b_double[] = { 0, bit_btn_b, 0, bit_btn_b, 0 };
static constexpr bits_btn_t btn_c_double[] = { 0, bit_btn_c, 0, bit_btn_c, 0 };
static constexpr bits_btn_t btn_ab_hold[] = { bit_btn_a|bit_btn_b, bit_btn_a|bit_btn_b };
static constexpr bits_btn_t btn_ac_hold[] = { bit_btn_a|bit_btn_c, bit_btn_a|bit_btn_c };
static constexpr bits_btn_t btn_bc_hold[] = { bit_btn_b|bit_btn_c, bit_btn_b|bit_btn_c };
static constexpr bits_btn_t btn_abc_hold[] = { bit_btn_a|bit_btn_b|bit_btn_c, bit_btn_a|bit_btn_b|bit_btn_c };
static constexpr bits_btn_t btn_a_b_c[] = { 0, bit_btn_c, 0, bit_btn_b, 0, bit_btn_a, 0 };
// 上記で作成したコマンド列を使い、コマンドリストを作成(第2引数の数字は第3引数の配列長と同じにすること)
static constexpr command_holder commands[] =
{ { 101, 5, btn_a_single } // A 短押し
, { 102, 5, btn_b_single } // B 短押し
, { 103, 5, btn_c_single } // C 短押し
, { 111, 3, btn_a_hold } // A 長押し
, { 112, 3, btn_b_hold } // B 長押し
, { 113, 3, btn_c_hold } // C 長押し
, { 121, 5, btn_a_double } // A 二回押し
, { 122, 5, btn_b_double } // B 二回押し
, { 123, 5, btn_c_double } // C 二回押し
, { 201, 2, btn_ab_hold } // AB 同時押し
, { 202, 2, btn_ac_hold } // AC 同時押し
, { 203, 2, btn_bc_hold } // BC 同時押し
, { 204, 2, btn_abc_hold } // ABC 同時押し
, { 301, 7, btn_a_b_c } // ABC 順番押し
};
class btn_manager
{
public:
std::uint32_t msec_debounce = 10; // デバウンス処理の待ち時間
std::uint32_t msec_hold = 250; // 長押し判定の待ち時間
void setup(void)
{
#if defined( ARDUINO_M5Stack_Core_ESP32 ) || defined( ARDUINO_M5STACK_FIRE ) // M5Stack
_gpio[btn_a] = 39;
_gpio[btn_b] = 38;
_gpio[btn_c] = 37;
#elif defined( ARDUINO_M5Stick_C ) // M5Stick C
_gpio[btn_a] = 37;
_gpio[btn_b] = 39;
_gpio[btn_c] = -1;
#endif
for (auto pin : _gpio) { pinMode(pin, INPUT); }
}
std::uint32_t hitcheck(void)
{
std::uint32_t hitres = 0;
std::uint_fast8_t hitlen = 0;
// 登録されているコマンドと一致するものを探すループ
for (const auto& cmd : commands)
{
// 既にヒットしたものより短いものは除外
if (hitlen >= cmd.len) continue;
// 履歴と一致しないものは除外
if (memcmp(_btns_stack, cmd.inputs, sizeof(bits_btn_t) * cmd.len)) continue;
// ヒットしたものを記録しておく
hitres = cmd.value;
hitlen = cmd.len;
}
return hitres;
}
// ボタン状態更新処理
// 戻り値:true=状態変化あり / false=状態変化なし
bool loop(void)
{
// 現在のミリ秒時間を取得
std::uint32_t msec = millis();
// ボタンのGPIOの状態をbtnsにビット列として取得
std::uint32_t btns = 0;
for (int i = 0; i < BTN_MAX; ++i) { if (_gpio[i] >= 0 && !digitalRead(_gpio[i])) btns |= 1 << i; }
// 記録しているボタンの状態と比較
if (_btns_before_db != btns)
{
// デバウンス処理前の入力状態を更新
_btns_before_db = btns;
_msec_changed = msec;
return false;
}
// 入力変化後、デバウンス処理時間が経過していることを確認
if (_btns_after_db != btns && msec - _msec_changed > msec_debounce)
{
// デバウンス処理後の入力状態を更新
_btns_after_db = btns;
// _msec_after_db = msec;
// 履歴を一つ追加
memmove(&_btns_stack[1], _btns_stack, (BTN_STACK_MAX - 1) * sizeof(bits_btn_t));
_btns_stack[0] = btns;
// 履歴とコマンドの比較処理を行う
_hitvalue = hitcheck();
return true;
}
// 入力無変化での長押し時間経過を確認(無操作での時間経過時も含む)
if (_btns_stack[1] != btns && msec - _msec_changed > msec_hold)
{
// 履歴を一つ追加(長押し時間が経過した場合、履歴の今回値と前回値が同じになる)
memmove(&_btns_stack[1], _btns_stack, (BTN_STACK_MAX - 1) * sizeof(bits_btn_t));
// 履歴とコマンドの比較処理を行う
_hitvalue = hitcheck();
return true;
}
// 無入力状態が長押し時間2回分経過した場合は履歴をクリア
if (!btns && msec - _msec_changed > (msec_hold<<1))
{
memset(_btns_stack, 0, BTN_STACK_MAX * sizeof(bits_btn_t));
_msec_changed = msec;
_hitvalue = 0;
return true;
}
return false;
}
// コマンド判定後の値を取得する
std::uint32_t getValue(void) const { return _hitvalue; }
// ボタン入力履歴の配列を取得する
const bits_btn_t* getStack(void) const { return _btns_stack; }
private:
std::uint32_t _hitvalue; // コマンド判定でヒットした値
std::int32_t _gpio[BTN_MAX]; // ボタンのGPIOリスト
std::uint32_t _msec_changed = 0; // 入力変化時点の時間記録
bits_btn_t _btns_before_db = 0; // デバウンス処理前のボタン状態記録
bits_btn_t _btns_after_db = 0; // デバウンス処理後のボタン状態記録
bits_btn_t _btns_stack[BTN_STACK_MAX]; // ボタン状態の履歴
};
static btn_manager bm;
void println(const char* src)
{
M5.Lcd.fillRect(0, 0, M5.Lcd.width(), 30, TFT_BLACK);
M5.Lcd.drawString(src, 0, 0);
Serial.println(src);
}
void setup(void)
{
M5.begin();
M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
#if defined( ARDUINO_M5Stack_Core_ESP32 ) || defined( ARDUINO_M5STACK_FIRE ) // M5Stack
M5.Lcd.setTextSize(3);
#elif defined( ARDUINO_M5Stick_C ) // M5Stick C
M5.Lcd.setTextSize(2);
M5.Lcd.setRotation(3);
#endif
bm.setup();
}
void loop(void)
{
delay(1);
// 入力状態の更新(変化がなければ終了)
if (!bm.loop()) return;
// ボタン入力履歴を画面に描画
auto stack = bm.getStack();
for (std::size_t idx = 0; idx < BTN_STACK_MAX; ++idx)
{
M5.Lcd.fillRect(idx * 10, 30, 9, 9, (stack[idx] & bit_btn_a) ? TFT_RED : TFT_DARKGREY);
M5.Lcd.fillRect(idx * 10, 40, 9, 9, (stack[idx] & bit_btn_b) ? TFT_GREEN : TFT_DARKGREY);
M5.Lcd.fillRect(idx * 10, 50, 9, 9, (stack[idx] & bit_btn_c) ? TFT_BLUE : TFT_DARKGREY);
}
// コマンド判定結果を取得
static std::uint32_t hitvalue = ~0U;
std::uint32_t prev = hitvalue;
hitvalue = bm.getValue();
// 以前の値と同じ場合は除外
if (prev == hitvalue) return;
// 入力内容を画面とシリアルに出力
switch (hitvalue)
{
default: println("--"); break;
case 101: println("A click"); break;
case 102: println("B click"); break;
case 103: println("C click"); break;
case 111: println("A hold"); break;
case 112: println("B hold"); break;
case 113: println("C hold"); break;
case 121: println("A double click"); break;
case 122: println("B double click"); break;
case 123: println("C double click"); break;
case 201: println("AB hold"); break;
case 202: println("AC hold"); break;
case 203: println("BC hold"); break;
case 204: println("ABC hold"); break;
case 301: println("A->B->C"); break;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment