M5Stack Tab5で押しボタンカウンターを作った

M5Stack Tab5で押しボタンカウンターを作りました。
レジンキャスト複製時にレジンを混合してから時間を十分に置かずに脱型を試みて、成型品をダメにしてしまう事は良くあります。
それを避けるためには、レジンの混合や注型などのタイミングからの時間管理が重要なので、スイッチを押したり、真空脱泡機からの信号を受けた時刻を記録したり、経過時間を表示する為に作った物です。
真空脱泡機とM5Stack Basic

「自作真空脱泡機の改修終了」の記事でも紹介している、自作した真空脱泡機はM5Stack Basicで制御していて、真空脱泡が問題なく終了した時間を記録して、経過時間を表示する機能が有ります。

M5Stack Basicの液晶は2インチで320 x 240の解像度なので、見やすさを考慮して表示は最大5件となっています。

停電やM5Stack Basicに不具合が起きても真空脱泡した時間が確認出来る様に、サーマルプリンターに真空脱泡が完了した時間を印字して記録しているので、5件以上でも記録を確認する事は可能なのですが、一目で確認したいので、真空脱泡終了時に信号を受け取り、その時刻と、経過時間を表示する機器の構想は以前からありました。
M5Stack Tab5を入手してから暫くはプログラミングできなかった。

M5Stackから1280×720の解像度の5インチ液晶画面を備えた、M5Stack Tab5が発売されるのを知り、5月の発売開始時にスイッチサイエンスのネット通販で購入しました。

M5StackからTab5のArduino IDEを使える様にするライブラリーの提供は少し遅れた様で、購入直後は、汎用のArduino IDEライブラリーを使って文字を表示する程度の事しか私にはでず、私がArduino IDEを使いTab5のプログラミングが自由に出来る様になったのは9月に入ってからでした。
押しボタンスイッチボックス
Arduino IDE環境でTab5のプログラミングが自由に出来る様になるまでの間は、Tab5に最初からインストールされているデモ画面のGPIO出力機能を使って押しボタンスイッチボックスのリレーをテストしていました。

押しボタンスイッチボックスはM5Stack Basicの箱を利用して作りました。
外部からの電気信号でスイッチのON/OFFが出来る様にリレーモジュールを内蔵しています。

スイッチボックスはTab5のGrove端子に接続していて、Grove端子用コネクターは、市販のケーブルを分解してスイッチ動作に必要なGPIO G54とGNDにだけに配線し直しました。
Tab5のArduinoIDE環境が整ってからのプログラミング
9月に入ってからTab5はM5UnifiedとU5GFXのライブラリを使えばArduinoIDE環境でプログラミングできるのを知り数日で実用に耐えるプログラムが完成しました。
上の動画はプログラムが完成した際の動作確認の様子です。
上の動画は更に改良を加えた物のテスト動画。
動画では押しボタンスイッチボックスに内蔵したリレーモジュールにより、3.3Vや5Vの電気信号でスイッチを動作させる様子も収録しています。
プログラムの動作時の見た目の違いはあまりありませんが、にはWifiで現在時刻の取得が出来た際はその旨を表示する様にしました。
内部的にはWifiで現在時刻が取得できた時は内蔵時計(RTC)を上書きして時間補正を行い、Wifiで現在時刻の取得の可否に関係なく、時刻表示は内蔵時計を利用しています。
ChatGPTにプログラムさせてみた。
今まで使っていたArduino IDE環境でも、M5UnifiedとU5GFXのライブラリを使うTab5とM5Stackライブラリを使うM5Stack Basicとでは勝手が違う為、自分がM5UifiedとM5GFXを学ぶためのTab5用サンプルスケッチを書く様にChatGPTに指示してみた所、一発でコンパイルが通って、挙動も指示通りのスケッチが提供されました。
これに気を良くして、どこまでChatGPTへの指示で作れるのかテストしたのですが、3回前くらい前の指示は忘れてしまうらしく、場合によってはTab5では動作すらしないスケッチを書くので、今まで行った指示を毎回行う必要が有りました。
また、ChatGPTでは条件文で扱う変数に上限がある様な感じで、どんなに指示を工夫しても私が希望するボタン操作を実装する事は出来ませんでした。
最終的にはChatGPTが書いたスケッチを私が読み下した後、必要な挙動を私が自力で組み込んでスケッチを完成させました。
私が書いたスケッチだと、一発でコンパイルできる事は稀なので、ChatGPTにスケッチをチェックして貰い、構文ミスや、誤字脱字を修正しました。
AIがプログラムを生成する為に適切に指示を出すには、プログラミングに対する理解が必要で、必然的にプログラミングの経験も必要となるので、プログラミング未経験者がAIに指示を出すだけでプログラミングするのは、原理的に難しい気がします。
現在時刻の取得 プログラミング
当初、現在時刻の取得はWifiでのみ行っていました。
M5Stack Basicは時計機能(RTC)を搭載していなかったのでWifiで時間を取得していました。
Tab5でもM5Stack Basicと同様にWifiから時間取得を行うつもりで、ChatGPTに指示してスケッチを書かせた所、時間取得の部分は私がM5Stack Basic用に書いたスケッチと同様の内容でした。
現在時刻の正確さから、時計機能を搭載していてもWifiで時間を取得するのは一般的だったらしく、Tab5で内蔵時計(ArtronShop RX8130CE RTC)の利用方法を紹介する記事「M5Stack TAB5 ArduinoIDE M5GFX RTC 環境でニキシー管時計もどきを表示してみた」では、Wifi機能が使えたらそうした旨の記述がありました。
RTCの利用方法が分かってからは、Wifiで時間が取得できた場合はRTCに時刻を書き込み、Wifiでの時間取得の可否に関わらず、RTCの時間を現在時刻に用いる様に改良しました。
文字表示 プログラミング
文字表示は当初、既存の文字を背景色で塗り潰してから文字を表示させる、M5ライブラリでは一般的方法を行っていました。

この方法だ背景色の塗り潰しで画面がどうしてもチラ付いてしまうのですが、塗りつぶしをしないと上の画像や動画の様な事になります。
表示方法を思案していた所、XでSprite機能の存在を教えて貰い、チラつきが解決しました。
Sprite機能はCanvasと呼ばれる仮想スクリーンに描画した後、一枚絵として液晶画面に表示する機能です。
背景色で書き換えたい文字を塗り潰す様子が実際の画面には表示されないのと、一枚絵の書き換えはかなり早いみたいで、体感できるチラ付きは有りませんでした。
終わりに サンプルスケッチ
最後に、最新のスケッチを掲載しておきます。
スケッチはArduinoIDE 2.3.6で作りました。
スケッチやライブラリはArduinoのバージョンに依存しない様なので、どのArduino環境でも利用できると思います。
Wifi環境で現在時刻を取得する場合は、ssidとパスワードをスケッチに書き込んでください。
WifiはRTCに時間を書き込んで補正する事にしか使っていないので、ssidとパスワードを設定せずにWifiが使えなくても利用出来ます。
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <vector>
#include <M5GFX.h>
#include <ArtronShop_RX8130CE.h>
#include <Wire.h>
#include <time.h>
M5GFX tft;
M5Canvas canvas(&tft); // ★ Sprite用キャンバスを追加
// Wi-Fi情報
const char* ssid = "ssid";
const char* password = "password";
const int pinG54 = 54;// GPIOピン(Tab5用)
const int displayrefresh = 0;// 画面更新周期
// ボタンを離した後のリセット画面表示時間
const unsigned long ResetTime = 1000;
unsigned long resetStartMillis = 0;
unsigned long resetLimitStart = 0;
// ボタン関連
const unsigned long buttonReleaseDelay = 500; // ボタン離してから再検知までの待機時間
const unsigned long longPress = 0; // ログ用の長押し判定(0秒以上)
const unsigned long ResetLimit = 2000; // 3連打判定時間(最初の押下から2秒以内)
const unsigned long ResetLongPress = 2000; // リセット用長押し判定(2秒以上)
const unsigned long ResetShortPress = 500; // リセット用短押し判定(0.5秒以下)
unsigned long buttonPressStart = 0;
unsigned long buttonRelease = 0;
int LogSyokai = 0;
int ResetCount = 0;
int ResetCount2 = 0;
const int ResetCountLimit = 2;
// 表示設定
const int textSize = 5;
const int flipDisplay = 2; // 上(2) 下(0) 左(3) 右(1) 方向切替 ケーブル位置基準
struct LogEntry {
int number;
unsigned long pressMillis;
time_t pressTime;
};
std::vector<LogEntry> logs;
int counter = 1;
// 現在時刻
time_t currentJSTTime = 0;
// RTC インスタンス
ArtronShop_RX8130CE rtc(&Wire);
// JST取得
time_t getJSTTime() {
if (WiFi.status() != WL_CONNECTED) return 0;
HTTPClient http;
http.begin("http://worldtimeapi.org/api/timezone/Asia/Tokyo");
int httpCode = http.GET();
time_t t = 0;
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
const char* datetime = doc["datetime"];
struct tm tm_struct;
sscanf(datetime, "%4d-%2d-%2dT%2d:%2d:%2d",
&tm_struct.tm_year, &tm_struct.tm_mon, &tm_struct.tm_mday,
&tm_struct.tm_hour, &tm_struct.tm_min, &tm_struct.tm_sec);
tm_struct.tm_year -= 1900;
tm_struct.tm_mon -= 1;
t = mktime(&tm_struct);
}
http.end();
return t;
}
String formatElapsed(unsigned long elapsedMs) {
unsigned long elapsedSec = elapsedMs / 1000;
int h = elapsedSec / 3600;
int m = (elapsedSec % 3600) / 60;
int s = elapsedSec % 60;
char buf[16];
sprintf(buf, "%02d:%02d:%02d", h, m, s);
return String(buf);
}
String formatCurrentTime(time_t t) {
struct tm* tm_info = localtime(&t);
char buf[16];
strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
return String(buf);
}
void setup() {
tft.begin();
tft.setRotation(flipDisplay);
// ★ Sprite初期化
canvas.createSprite(tft.width(), tft.height());
pinMode(pinG54, INPUT_PULLUP);
Wire.begin(31,32);
while (!rtc.begin()) {
canvas.fillScreen(BLACK);
canvas.setCursor(0, 0);
canvas.setTextSize(textSize);
canvas.setTextColor(WHITE);
canvas.println("RTC Init Fail...");
canvas.pushSprite(0, 0);
delay(1000);
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
canvas.fillScreen(BLACK);
canvas.setCursor(0, 0);
canvas.setTextSize(textSize);
canvas.setTextColor(WHITE);
canvas.println("Connecting WiFi...");
canvas.pushSprite(0, 0); // ★ LCDに反映
delay(displayrefresh);
}
// Wi-Fiで時刻取得
while (true) {
time_t t = getJSTTime();
if (t != 0) {
currentJSTTime = t;
// ★ RTCへ書き込み(Wi-Fi取得成功時のみ)
struct tm tm_struct;
localtime_r(¤tJSTTime, &tm_struct);
tm_struct.tm_isdst = 0; // 夏時間無効
rtc.setTime(tm_struct);
// ★ Wi-Fi取得成功メッセージを表示(2秒間)
canvas.fillScreen(BLACK);
canvas.setCursor(0, 0);
canvas.setTextSize(textSize);
canvas.println("Wi-Fi Time Sync Success!");
canvas.pushSprite(0, 0);
delay(2000);
break;
}
M5.update();
if (digitalRead(pinG54) == LOW) break;
delay(displayrefresh);
}
}
void ResetDisplay() {
canvas.fillScreen(BLACK);
canvas.setCursor(0, 0);
canvas.setTextSize(textSize);
canvas.setTextColor(TFT_CYAN);
canvas.println("Now: " + formatCurrentTime(currentJSTTime));
canvas.setTextColor(WHITE);
canvas.println("Reset");
canvas.pushSprite(0, 0); // ★ LCDに反映
delay(displayrefresh);
}
void Reset() {
while(digitalRead(pinG54) == LOW){ResetDisplay();}
resetStartMillis = millis();
while((millis() - resetStartMillis) < ResetTime ){ResetDisplay();}
logs.clear();
buttonPressStart = 0 ;
buttonRelease = millis() ;
LogSyokai = 0;
ResetCount = 0;
ResetCount2 = 0;
counter = 1;
resetLimitStart = 0;
}
void loop() {
M5.update();
static unsigned long lastTimeUpdate = 0;
static unsigned long lastDisplayUpdate = 0;
unsigned long nowMillis = millis();
// --- ここまでボタン処理(変更なし) ---
if (digitalRead(pinG54) == LOW && buttonPressStart == 0){ buttonPressStart = millis();}
if ( digitalRead(pinG54) == LOW && (millis() - buttonPressStart) >= longPress && LogSyokai == 0 && (millis() - buttonRelease > buttonReleaseDelay ) && buttonPressStart > 0 ) {
LogEntry entry; entry.number = counter++; entry.pressMillis = millis(); entry.pressTime = currentJSTTime;
logs.push_back(entry); LogSyokai = 1 ;
}
if ( digitalRead(pinG54) == LOW && ResetCount2 == 0 && ResetCountLimit > ResetCount && ( millis()-resetLimitStart) > ResetLimit || resetLimitStart == 0) {resetLimitStart = millis() ; ResetCount = 0; }
if (digitalRead(pinG54) == LOW && (millis()-resetLimitStart) < ResetLimit && ResetCountLimit <= ResetCount) { ResetCount2 = 1;}
if (digitalRead(pinG54) == LOW && (millis()-resetLimitStart) > ResetLimit){
if ( ResetCount2 == 0 ) {resetLimitStart = 0;ResetCount = 0;}
else {if ( ResetCount2 == 1 && (millis() - buttonPressStart) > ResetLongPress && ResetCountLimit <= ResetCount ){Reset();}}
}
if (digitalRead(pinG54) == HIGH && (millis() - buttonPressStart) <= ResetShortPress && buttonPressStart > 0 ){ResetCount++ ; buttonPressStart = 0 ; buttonRelease = millis() ; LogSyokai = 0;}
if (digitalRead(pinG54) == HIGH && (millis() - buttonPressStart) > ResetShortPress && buttonPressStart > 0 ){ buttonPressStart = 0 ; buttonRelease = millis() ; LogSyokai = 0;}
if (digitalRead(pinG54) == HIGH && (millis()-resetLimitStart) > ResetLimit ){ ResetCount = 0; ResetCount2 = 0 ; resetLimitStart = 0 ; }
// 時刻更新(RTCから取得)
if (nowMillis - lastTimeUpdate >= 1000) {
struct tm tm_struct;
if (rtc.getTime(&tm_struct)) {
tm_struct.tm_isdst = 0; // 夏時間無効
currentJSTTime = mktime(&tm_struct);
}
lastTimeUpdate = nowMillis;
}
// 表示更新
if (nowMillis - lastDisplayUpdate >= displayrefresh) {
canvas.fillScreen(BLACK);
// 現在時刻
canvas.setCursor(0, 0);
canvas.setTextSize(textSize);
canvas.setTextColor(TFT_CYAN);
canvas.println("Now: " + formatCurrentTime(currentJSTTime));
int y = 60;
if (logs.empty()) {
canvas.setCursor(0, y);
canvas.setTextSize(textSize);
canvas.setTextColor(WHITE);
canvas.println("Press button");
}
else {
for (int i = logs.size() - 1; i >= 0; i--) {
LogEntry log = logs[i];
unsigned long elapsed = nowMillis - log.pressMillis;
String elapsedStr = formatElapsed(elapsed);
String timeStr = formatCurrentTime(log.pressTime);
canvas.setCursor(0, y);
canvas.setTextSize(textSize);
canvas.setTextColor(WHITE);
canvas.printf("%d: %s - %s\n", log.number, elapsedStr.c_str(), timeStr.c_str());
y += 60;
}
}
canvas.pushSprite(0, 0); // ★ LCDに反映
lastDisplayUpdate = nowMillis;
}
}




