#include <M5Unified.h>
#include <cmath>
// 向き
enum Orientation
{
ORIENT_NONE,
ORIENT_BOTTOM_DOWN, // ボタン側(底面)が下 -> 5分
ORIENT_TOP_DOWN, // 上端が下 -> 3分
ORIENT_RIGHT_DOWN // 右側面が下 -> 10秒
};
// タイマー用の変数
unsigned long startMillis = 0;
unsigned long elapsed = 0;
bool running = false;
unsigned long prevElapsed = 0;
// 向き検出関連
Orientation currentDetected = ORIENT_NONE;
Orientation lastStableDetected = ORIENT_NONE;
unsigned long orientDetectStart = 0;
const unsigned long ORIENT_HOLD_MS = 500; // 0.5秒ホールドで新規開始
const unsigned long ORIENT_RESET_MS = 2000; // 2秒ホールドでリセット&再度の開始
// 各タイマーの時間(秒)
const unsigned long DURATION_BOTTOM_SEC = 300; // 5分
const unsigned long DURATION_TOP_SEC = 180; // 3分
const unsigned long DURATION_RIGHT_SEC = 5; // 5秒
// 連続トリガー防止
Orientation lastTriggered = ORIENT_NONE;
// 加速度判定閾値(g単位)
const float G_THRESHOLD = 0.8f;
// プログレスバー関連の変数
int lastBarWidth = -1; // 前回描画したバーの幅を記憶
// センサ値を g に正規化(m/s^2 の場合は 9.80665 で割る)
void normalizeToG(float &ax, float &ay, float &az)
{
float maxAbs = fmaxf(fmaxf(fabsf(ax), fabsf(ay)), fabsf(az));
if (maxAbs > 5.0f)
{
const float g0 = 9.80665f;
ax /= g0;
ay /= g0;
az /= g0;
}
}
// 現在の向きを判定(M5.Imu.getAccelData を利用)
Orientation detectOrientation()
{
float ax = 0, ay = 0, az = 0;
if (!M5.Imu.getAccelData(&ax, &ay, &az))
{
return ORIENT_NONE;
}
normalizeToG(ax, ay, az);
float absx = fabsf(ax);
float absy = fabsf(ay);
float absz = fabsf(az);
// 重力成分が大きい軸で判定
float maxAxis = fmaxf(fmaxf(absx, absy), absz);
// Y 軸となった場合(上下判定)
if (absy == maxAxis && absy >= G_THRESHOLD)
{
if (ay >= G_THRESHOLD)
{
// ay が正(Y軸の正方向に重力) -> ボタン側(底面)が下
return ORIENT_BOTTOM_DOWN;
}
else if (ay <= -G_THRESHOLD)
{
// ay が負(Y軸の負方向に重力) -> 上端が下
return ORIENT_TOP_DOWN;
}
}
// X 軸となった場合(左右判定)
else if (absx == maxAxis && absx >= G_THRESHOLD)
{
if (ax <= -G_THRESHOLD)
{
// ax が負(X軸の負方向に重力) -> 右側面が下
return ORIENT_RIGHT_DOWN;
}
// 左側面が下の場合は今回は何もしない
}
return ORIENT_NONE;
}
// プログレスバーを描画
void drawProgressBar(unsigned long remaining, unsigned long total)
{
const int barY = 40; // プログレスバーのY座標の位置
const int barHeight = 30; // プログレスバーの高さ
const int barX = 0; // プログレスバーのX座標の開始位置
int screenWidth = M5.Lcd.width();
// 残り時間の割合を計算(0.0~1.0)
float progress = (total > 0) ? (float)remaining / (float)total : 0.0;
int barWidth = (int)(screenWidth * progress);
// バーの幅が変わっていない場合は再描画しない
if (barWidth == lastBarWidth && lastBarWidth >= 0)
{
return;
}
// バーの色を設定(タイマーの種類により変更)
uint16_t barColor;
if (lastTriggered == ORIENT_TOP_DOWN)
{
// 3分タイマー:明るい水色
barColor = M5.Lcd.color565(100, 220, 255);
}
else if (lastTriggered == ORIENT_BOTTOM_DOWN)
{
// 5分タイマー:明るい黄緑色
barColor = M5.Lcd.color565(200, 255, 100);
}
else if (lastTriggered == ORIENT_RIGHT_DOWN)
{
// 5秒タイマー:薄い紫
barColor = M5.Lcd.color565(200, 150, 255);
}
else
{
barColor = TFT_DARKGREY;
}
// 差分描画:前回より短くなった部分だけ黒で塗る
if (lastBarWidth > barWidth && lastBarWidth >= 0)
{
// 減った部分だけを黒で塗る
M5.Lcd.fillRect(barX + barWidth, barY, lastBarWidth - barWidth, barHeight, TFT_BLACK);
}
// 前回より長くなった場合(リセット時など)
else if (lastBarWidth < barWidth)
{
// 増えた部分だけを描画
if (lastBarWidth >= 0)
{
M5.Lcd.fillRect(barX + lastBarWidth, barY, barWidth - lastBarWidth, barHeight, barColor);
}
else
{
// 初回描画
M5.Lcd.fillRect(barX, barY, barWidth, barHeight, barColor);
}
}
// 枠線は初回のみ描画(バーと同じ色)
if (lastBarWidth < 0)
{
M5.Lcd.drawRect(barX, barY, screenWidth, barHeight, barColor);
}
lastBarWidth = barWidth;
}
// プログレスバーをリセット(画面切り替え時に呼ぶ)
void resetProgressBar()
{
lastBarWidth = -1;
}
void showHeader()
{
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(8, 8);
M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
M5.Lcd.print("M5 GraviTimer");
}
void showAccel(float ax, float ay, float az)
{
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(8, 220);
M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
char buf[80];
snprintf(buf, sizeof(buf), "ax:%.1f ay:%.1f az:%.1f ", ax, ay, az);
M5.Lcd.print(buf);
}
void showStatus(const char *s)
{
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(8, 80);
M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK);
M5.Lcd.print(" "); // clear area
M5.Lcd.setCursor(8, 80);
M5.Lcd.print(s);
}
void showTimeMs(unsigned long ms)
{
unsigned long totalSeconds = ms / 1000;
unsigned int minutes = totalSeconds / 60;
unsigned int seconds = totalSeconds % 60;
unsigned int msPart = (ms % 1000) / 10; // 10ms単位で表示
// 右側面が下の時は文字サイズを調整
if (lastTriggered == ORIENT_RIGHT_DOWN)
{
M5.Lcd.setTextSize(4); // 縦向きの時は少し小さくする
}
else
{
M5.Lcd.setTextSize(6);
}
// タイマーの種類によって色を変更
if (lastTriggered == ORIENT_TOP_DOWN)
{
M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); // 3分タイマー:水色
}
else if (lastTriggered == ORIENT_BOTTOM_DOWN)
{
M5.Lcd.setTextColor(TFT_GREENYELLOW, TFT_BLACK); // 5分タイマー:黄緑色
}
else if (lastTriggered == ORIENT_RIGHT_DOWN)
{
M5.Lcd.setTextColor(TFT_VIOLET, TFT_BLACK); // 5秒タイマー:紫
}
else
{
M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK); // デフォルト:白
}
M5.Lcd.setCursor(8, 130);
char buf[32];
snprintf(buf, sizeof(buf), "%dm%02d.%02ds ", minutes, seconds, msPart);
M5.Lcd.print(buf);
}
void startTimerSeconds(unsigned long seconds, Orientation src)
{
startMillis = millis();
elapsed = 0;
prevElapsed = 0;
running = true;
lastTriggered = src;
// 向きに応じて画面の回転を設定
if (src == ORIENT_TOP_DOWN)
{
M5.Lcd.setRotation(3); // 上端が下の時(180度回転)
}
else if (src == ORIENT_RIGHT_DOWN)
{
M5.Lcd.setRotation(0); // 右側面が下の時(270度回転)
}
else
{
M5.Lcd.setRotation(1); // 通常表示(ボタン側が下の時)
}
// 画面をクリアして再描画
M5.Lcd.fillScreen(TFT_BLACK);
showHeader();
// プログレスバーをリセットして初期描画
resetProgressBar();
drawProgressBar(seconds * 1000UL, seconds * 1000UL);
}
void stopTimerReset()
{
running = false;
elapsed = 0;
startMillis = 0;
prevElapsed = 0;
lastTriggered = ORIENT_NONE;
// 画面回転を通常に戻す
M5.Lcd.setRotation(1);
M5.Lcd.fillScreen(TFT_BLACK);
resetProgressBar(); // プログレスバーをリセット
showHeader();
showTimeMs(0);
}
void setup()
{
auto cfg = M5.config();
M5.begin(cfg);
M5.Lcd.setRotation(1); // 初期画面の回転設定(横向き、ボタンが下)
M5.Lcd.fillScreen(TFT_BLACK);
showHeader();
showTimeMs(0);
showStatus("Waiting...");
delay(200);
}
void loop()
{
M5.update();
// 手動操作
// A: 手動で 5 分開始(動作テスト用) / C: リセット
if (M5.BtnA.wasPressed())
{
if (!running)
{
startTimerSeconds(DURATION_BOTTOM_SEC, ORIENT_BOTTOM_DOWN);
showStatus("Manual start 5min");
}
}
if (M5.BtnC.wasPressed())
{
stopTimerReset();
showStatus("Reset");
}
// 向き検出
Orientation o = detectOrientation();
// 状態遷移
if (o != currentDetected)
{
currentDetected = o;
orientDetectStart = millis();
}
else
{
unsigned long holdTime = millis() - orientDetectStart;
if (o != ORIENT_NONE)
{
// タイマーが動いていない場合:0.5秒で開始
if (!running && holdTime >= ORIENT_HOLD_MS)
{
if (o != lastTriggered)
{
if (o == ORIENT_BOTTOM_DOWN)
{
startTimerSeconds(DURATION_BOTTOM_SEC, ORIENT_BOTTOM_DOWN);
showStatus("5min");
}
else if (o == ORIENT_TOP_DOWN)
{
startTimerSeconds(DURATION_TOP_SEC, ORIENT_TOP_DOWN);
showStatus("3min");
}
else if (o == ORIENT_RIGHT_DOWN)
{
startTimerSeconds(DURATION_RIGHT_SEC, ORIENT_RIGHT_DOWN);
showStatus("5sec");
}
}
}
// タイマーが動作中で反対の向きを検出:2秒でリセット&再開始
else if (running && o != lastTriggered && holdTime >= ORIENT_RESET_MS)
{
if (o == ORIENT_BOTTOM_DOWN)
{
stopTimerReset();
startTimerSeconds(DURATION_BOTTOM_SEC, ORIENT_BOTTOM_DOWN);
showStatus("5min");
}
else if (o == ORIENT_TOP_DOWN)
{
stopTimerReset();
startTimerSeconds(DURATION_TOP_SEC, ORIENT_TOP_DOWN);
showStatus("3min");
}
else if (o == ORIENT_RIGHT_DOWN)
{
stopTimerReset();
startTimerSeconds(DURATION_RIGHT_SEC, ORIENT_RIGHT_DOWN);
showStatus("5sec");
}
}
// 待機中の表示
else if (running && o != lastTriggered && holdTime < ORIENT_RESET_MS)
{
char buf[32];
snprintf(buf, sizeof(buf), "Wait %.1fs...", (ORIENT_RESET_MS - holdTime) / 1000.0);
showStatus(buf);
}
}
}
// タイマー更新(残り時間を表示)
if (running)
{
elapsed = millis() - startMillis;
unsigned long durationMs = 0;
if (lastTriggered == ORIENT_BOTTOM_DOWN)
durationMs = DURATION_BOTTOM_SEC * 1000UL;
else if (lastTriggered == ORIENT_TOP_DOWN)
durationMs = DURATION_TOP_SEC * 1000UL;
else if (lastTriggered == ORIENT_RIGHT_DOWN)
durationMs = DURATION_RIGHT_SEC * 1000UL;
if (elapsed >= durationMs && durationMs > 0)
{
running = false;
elapsed = durationMs;
showTimeMs(0); // 終了時は 0 を表示
drawProgressBar(0, durationMs); // プログレスバーを空にする
// 終了通知: 文字を5回点滅
for (int i = 0; i < 5; i++)
{
// 右側面の時は文字サイズ調整
if (lastTriggered == ORIENT_RIGHT_DOWN)
{
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(8, 130);
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
M5.Lcd.print("TIME UP!");
}
else
{
M5.Lcd.setTextSize(5);
M5.Lcd.setCursor(8, 130);
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
M5.Lcd.print("TIME UP! ");
}
delay(200);
// 点滅OFF
M5.Lcd.fillRect(8, 130, 320, 60, TFT_BLACK);
delay(200);
}
// 終了表示
if (lastTriggered == ORIENT_RIGHT_DOWN)
{
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(8, 130);
M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK);
M5.Lcd.print("Finished!");
}
else
{
M5.Lcd.setTextSize(5);
M5.Lcd.setCursor(8, 130);
M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK);
M5.Lcd.print("Finished! ");
}
}
else
{
// 残り時間表示
unsigned long remain = (durationMs > elapsed) ? (durationMs - elapsed) : 0;
showTimeMs(remain);
drawProgressBar(remain, durationMs); // プログレスバーを更新
}
}
else
{
// タイマー停止中はプログレスバーを非表示または初期状態
drawProgressBar(0, 1); // 空のバーを表示
}
// 加速度をデバッグ用に目立たない大きさで表示
float ax = 0, ay = 0, az = 0;
if (M5.Imu.getAccelData(&ax, &ay, &az))
{
normalizeToG(ax, ay, az);
showAccel(ax, ay, az);
}
delay(50);
}
Comments