
目次
はじめに
本記事では、AI駆動のコードエディタ「Cursor」と組み合わせて、M5Stack Basic(物理ボタンが3つある古い機種)を使用したBLEリモートスイッチ付き目覚ましアラーム時計の開発プロセスを紹介します。BLEスイッチはダイソーにて販売されていたスマートフォンのシャッター用リモコンです。
プロジェクトのきっかけ
このプロジェクトを始めたきっかけは、小学5年の娘が朝が弱く、なかなか自分で起きられないことでした。大音量目覚まし時計を使っても、止めて二度寝してしまうという問題がありました。そこで、時計を枕元に置き、アラーム停止のリモコンスイッチを部屋の入口付近に配置することで、アラームを止めるにはベッドから出て部屋の入口まで行かなければならない仕組みを作りました。これにより、少なくともベッドからは出られるようになるはずです。
開発方針
開発にあたっては、私自身は一切ソースコードを編集することなく、全て生成AIにソースコードを作成してもらいました。当初はバイブコーディングのように完全に生成AIに任せてコーディングしてもらいました。その後、仕様駆動型開発ツール「Spec Driven Codex」も導入し、その使用感を確認しました。
本記事では、特に開発環境の自動構築から実装、そして途中から導入した仕様駆動型開発ツール「Spec Driven Codex」による開発プロセスの改善まで、実際の開発経緯を解説します。(このブログもベースは生成AIが作成しています)
デモ動画
小さな音でアラームが鳴り始め、10秒ごとにボリュームがあがります。リモコンスイッチを押すまで鳴動します。
プロジェクト概要
開発したデバイスは、M5Stack BasicをベースとしたWiFi対応のアラーム時計です。主な機能は以下の通りです:
- NTP時刻同期: WiFi経由でNTPサーバーから時刻を取得し、正確な時刻を表示
- アラーム機能: ボタン操作でアラーム時刻を設定し、設定時刻にアラームを鳴動
- BLE接続: Bluetooth Low Energyによるリモートボタン操作に対応
- 画面表示: 時刻、日付、アラーム設定、BLE接続状態などを表示
- 設定の永続化: EEPROMに設定を保存し、電源OFF後も設定を保持
→この処理はSpec Driven Codexを活用して構築しました。
開発環境の構築:Cursorに依頼して自動化
Cursorとは
Cursorは、AIを活用したコードエディタで、コード補完、エラー修正、コード生成など、開発を大幅に効率化できるツールです。本プロジェクトでは、Cursorのチャット機能を活用して、開発環境の構築から実装まで、AIアシスタントに依頼しながら進めました。
PlatformIO開発環境の自動構築
プロジェクト開始時、まずCursorに「M5Stack Basic用のPlatformIO開発環境を構築してほしい」と依頼しました。Cursorは以下の作業を自動的に実行してくれました:
platformio.iniの作成: M5Stack Basic用の設定ファイルを生成
- ESP32プラットフォームの設定
- 必要なライブラリ(M5Stack、NTPClient)の依存関係定義
- ビルドフラグやシリアル通信設定
ビルドスクリプトの作成: Windows環境用のバッチファイルとPowerShellスクリプトを生成
build.bat/build.ps1: ビルドのみ実行upload.bat/upload.ps1: ビルド+書き込みmonitor.bat/monitor.ps1: シリアルモニター起動build-and-upload.bat/build-and-upload.ps1: 一括実行
README.mdの作成: セットアップ手順や使用方法を記載したドキュメントを生成
基本的なプロジェクト構造の構築: src/main.cppのテンプレート作成
このように、Cursorに依頼するだけで、PlatformIO開発環境が一から構築されました。手動で設定ファイルを書いたり、ドキュメントを作成したりする必要がなく、開発をすぐに開始できました。
開発環境の特徴
構築された開発環境の主な特徴:
- クロスプラットフォーム対応: Windows用のバッチファイルとPowerShellスクリプトの両方を提供
- 簡単な操作: バッチファイルをダブルクリックするだけでビルド・書き込みが可能
- Cursor統合: Cursorのタスクランナーからも実行可能
- 詳細なドキュメント: READMEにセットアップ手順からトラブルシューティングまで記載
アラームデバイス開発の工程
フェーズ1: 基本機能の実装
最初のフェーズでは、基本的な機能を順次実装していきました。
1. WiFi接続とNTP時刻同期
まず、WiFi接続機能とNTPサーバーからの時刻取得機能を実装しました。Cursorに「WiFi接続してNTPサーバーから時刻を取得する機能を実装してほしい」と依頼し、必要なコードを生成してもらいました。
実装内容:
- WiFi接続処理(最大30回リトライ)
- NTPクライアントの初期化
- 日次補正(日付が変わった時に再接続)
2. 時刻・日付表示
次に、取得した時刻を画面に表示する機能を実装しました。
実装内容:
- 時刻表示(HH:MM:SS形式、1秒ごとに更新)
- 日付表示(YYYY-MM-DD形式)
- 曜日表示(日曜日は赤、土曜日は青、その他は白)
- 差分更新によるちらつき防止
3. アラーム機能
アラーム機能の実装では、以下の機能を段階的に追加しました。
実装内容:
- アラーム時刻の設定(ボタンA/Bで時・分を設定)
- アラームON/OFF切り替え(ボタンC長押し)
- アラーム発動判定(1秒ごとに時刻をチェック)
- アラーム音の再生(ビープ音、メロディー、チャイム音から選択)
- 音量制御(開始時は最小音量、10秒ごとに段階的に増加)
- 自動停止(最大3分間で自動停止)
4. ボタン操作
M5Stack Basicの3つのボタン(A, B, C)を使った操作を実装しました。
実装内容:
- ボタンA: アラーム設定状態への遷移、時の設定
- ボタンB: アラーム音選択状態への遷移、分の設定
- ボタンC: 設定の確定、アラームON/OFF切り替え
- 長押し判定(1秒以上)
- デバウンス処理(10ms)
5. BLE機能
BLEによるリモートボタン操作機能を実装しました。ESP32のマルチコア機能を活用し、BLE処理を別コア(Core 1)で実行することで、メイン処理(Core 0)の応答性を確保しました。
実装内容:
- HIDサービスを持つBLEデバイスの自動検出
- 自動接続処理
- リモートボタン操作によるアラーム停止
- 接続状態の表示
フェーズ2: 機能改善と最適化
基本機能の実装が完了した後、以下の改善を行いました。
1. 設定の永続化
アラーム設定をEEPROMに保存し、電源OFF後も設定を保持する機能を追加しました。
実装内容:
- Preferencesライブラリを使用したEEPROM保存
- 起動時の設定読み込み
- 設定変更時の自動保存
2. アラーム音のカスタマイズ
アラーム音を3種類(ビープ音、メロディー、チャイム音)から選択できる機能を追加しました。
実装内容:
- アラーム音選択UI(ボタンB長押しで選択状態へ)
- 音声再生処理の拡張
- 設定の保存
3. 保存タイミングの最適化
アラーム設定の保存タイミングを最適化し、EEPROM書き込み回数を約70-80%削減しました。
改善内容:
- ボタンA/Bによる設定変更時は保存しない
- SETボタン(ボタンC短押し)押下時のみ保存
- アラームON/OFF切り替え時は即座に保存
4. 画面自動OFF機能
省電力のため、1分間操作がない場合に画面を自動的にOFFにする機能を追加しました。
開発プロセスでのCursorの活用
各フェーズで、Cursorのチャット機能を活用して以下のような作業を効率化しました:
- コード生成: 機能の実装依頼をすると、必要なコードを生成
- エラー修正: ビルドエラーや実行時エラーを報告すると、修正案を提示
- リファクタリング: コードの改善提案を依頼すると、より良い実装を提案
- ドキュメント作成: 実装内容の説明を依頼すると、コメントやドキュメントを生成
ソースコード
初期版ソースコード
#include <M5Stack.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <time.h>
#include "BLEDevice.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include <Preferences.h>
// アラーム停止機能の有効/無効(ifdef対応)
//#define ENABLE_ALARM_STOP
// 画面自動OFF機能の有効/無効(ifdef対応)
#define ENABLE_SCREEN_AUTO_OFF
// 外部スピーカー(Grove - スピーカープラス)の使用
// コメントアウトすると内蔵スピーカーを使用
// 注意: Grove - Speaker Plusは容量の影響で高音が出せません(ベース音のみ)
// 高音の「ピピッ」音を出すには、内蔵スピーカーを使用してください
//#define USE_EXTERNAL_SPEAKER
#ifdef USE_EXTERNAL_SPEAKER
// Grove - スピーカープラス用の設定(PORT.B の GPIO26)
#define EXTERNAL_SPEAKER_PIN 26
#endif
// WiFi設定
const char* ssid = "SSID";
const char* password = "PASSWORD";
// NTP設定
const char* ntpServer = "ntp.nict.jp"; // 日本のNTPサーバー
const long gmtOffset_sec = 9 * 3600; // JST (UTC+9) のオフセット(秒)
const int daylightOffset_sec = 0; // サマータイムなし
// NTPクライアント
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, gmtOffset_sec);
// 状態定義
enum AppState {
STATE_NORMAL, // 通常状態(時計表示)
STATE_ALARM_SETTING, // アラーム設定状態
STATE_ALARM_RINGING, // アラーム鳴動中
STATE_SOUND_SELECTING // アラーム音選択状態
};
// アラーム音の種類
enum AlarmSoundType {
ALARM_SOUND_BEEP, // ビープ音(高音・低音交互)
ALARM_SOUND_MELODY, // メロディー
ALARM_SOUND_CHIME // チャイム音
};
// アプリケーション状態
AppState currentState = STATE_NORMAL;
// アラーム設定
int alarmHour = 6; // デフォルト6時
int alarmMinute = 15; // デフォルト15分
bool alarmEnabled = true; // アラームON/OFF(デフォルトON)
AlarmSoundType alarmSoundType = ALARM_SOUND_BEEP; // アラーム音の種類(デフォルト: ビープ音)
// EEPROM設定保存用
Preferences preferences;
const char* PREF_NAMESPACE = "alarm"; // 設定の名前空間
const char* PREF_KEY_HOUR = "hour"; // アラーム時刻(時)のキー
const char* PREF_KEY_MINUTE = "minute"; // アラーム時刻(分)のキー
const char* PREF_KEY_ENABLED = "enabled"; // アラームON/OFFのキー
const char* PREF_KEY_SOUND_TYPE = "soundType"; // アラーム音の種類のキー
bool alarmRinging = false; // アラームが鳴っているか(currentStateと同期)
unsigned long alarmStartTime = 0; // アラーム開始時刻(ミリ秒)
const unsigned long alarmMaxDuration = 3 * 60 * 1000; // アラーム最大継続時間(3分 = 180000ミリ秒)
int alarmVolume = 1; // 現在のアラーム音量(1~10)
const int alarmVolumeMin = 1; // アラーム音量の最小値
const int alarmVolumeMax = 10; // アラーム音量の最大値
const unsigned long alarmVolumeInterval = 10 * 1000; // 音量を上げる間隔(10秒 = 10000ミリ秒)
unsigned long lastVolumeUpdateTime = 0; // 最後に音量を更新した時刻
// 時刻表示用の変数
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 1000; // 1秒ごとに更新
// ボタンGPIOピン定義(M5Stack Basic)
#define BUTTON_A_PIN 39
#define BUTTON_B_PIN 38
#define BUTTON_C_PIN 37
// ボタン検出用の変数
volatile unsigned long buttonAPressTime = 0;
volatile unsigned long buttonCPressTime = 0;
volatile unsigned long buttonBPressTime = 0;
const unsigned long longPressDuration = 1000; // 長押し判定時間(ミリ秒)= 1秒
const unsigned long autoIncrementInterval = 200; // 長押し時の自動進み間隔(ミリ秒)
volatile bool buttonAPressed = false;
volatile bool buttonBPressed = false;
volatile bool buttonCPressed = false;
volatile bool buttonAInterruptFlag = false;
volatile bool buttonBInterruptFlag = false;
volatile bool buttonCInterruptFlag = false;
volatile bool buttonALongPressed = false;
volatile bool buttonBLongPressed = false;
volatile bool buttonCLongPressed = false;
volatile bool buttonALongPressHandled = false; // 長押し処理が実行されたかどうか
volatile bool buttonBLongPressHandled = false; // 長押し処理が実行されたかどうか
volatile bool buttonCLongPressHandled = false; // 長押し処理が実行されたかどうか
// 前回の時刻(13時自動ON判定用)
int lastHour = -1;
// NTP再接続用の変数
int lastNTPReconnectDate = -1; // 最後にNTP再接続した日付
// アラーム設定状態移行後のボタン入力無効化用
unsigned long alarmSettingEnterTime = 0; // アラーム設定状態に移行した時刻
const unsigned long alarmSettingInputDisableDuration = 1000; // ボタン入力無効化時間(ミリ秒)= 1秒
// 画面OFF/ON制御用の変数
unsigned long lastActivityTime = 0; // 最後の操作時刻
const unsigned long screenOffTimeout = 60000; // 1分 = 60000ミリ秒
bool screenOn = true; // 画面ON/OFF状態
const int screenBrightness = 100; // 画面の明るさ(0-255)
// 画面更新用の変数(ちらつき防止のため前回の値を保存)
char lastTimeString[20] = "";
char lastAlarmString[20] = "";
bool lastAlarmEnabled = false;
int lastBleStatus = -1; // -1:未初期化, 0:Connected, 1:Disconnected, 2:Scanning, 3:Connecting, 4:Not connected
bool lastAlarmRinging = false;
char lastDateString[30] = ""; // 日付と曜日の文字列
// 状態変化検出用のグローバル変数(displayNormalState()とdisplayAlarmSettingState()で共有)
AppState lastDisplayState = STATE_NORMAL;
// アラーム音用の変数
unsigned long lastBeepTime = 0;
const unsigned long beepInterval = 250; // ビープ音の間隔(ミリ秒)
const int beepFrequencyHigh = 2500; // ビープ音の高音周波数(Hz)- 「ピ」
const int beepFrequencyLow = 2200; // ビープ音の低音周波数(Hz)- 「ピ」
const int beepDuration = 150; // ビープ音の長さ(ミリ秒)
bool useHighFrequency = true; // 高音・低音を交互に切り替えるフラグ
// メロディー用の変数
const int melodyNotes[] = {523, 587, 659, 698, 784, 880, 988, 1047}; // C, D, E, F, G, A, B, C(ドレミファソラシド)
const int melodyNoteCount = 8;
int currentMelodyNote = 0; // 現在のメロディーの音符
unsigned long lastMelodyNoteTime = 0;
const unsigned long melodyNoteInterval = 200; // メロディーの音符間隔(ミリ秒)
const int melodyNoteDuration = 150; // メロディーの音符の長さ(ミリ秒)
// チャイム音用の変数
const int chimeNotes[] = {523, 659, 784}; // C, E, G(ドミソ)
const int chimeNoteCount = 3;
int currentChimeNote = 0; // 現在のチャイムの音符
unsigned long lastChimeTime = 0;
const unsigned long chimeInterval = 300; // チャイム音の間隔(ミリ秒)
const int chimeNoteDuration = 200; // チャイム音の長さ(ミリ秒)
// BLE設定
static uint16_t GATT_HID = 0x1812; // HIDサービスのUUID
static BLEAddress *pServerAddress = NULL;
static BLEClient *pClient = NULL;
static volatile bool bleConnected = false;
static volatile bool bleScanning = false;
static volatile bool bleFirstConnected = false; // 初回接続完了フラグ
// マルチコア処理用のセマフォ(共有変数の保護)
static SemaphoreHandle_t bleMutex = NULL;
// タスクハンドル
static TaskHandle_t bleTaskHandle = NULL;
// 関数の前方宣言
void updateButtonStates();
void handleButtonAShortPress();
void handleButtonALongPress();
void handleButtonBPress();
void handleButtonBLongPress();
void handleButtonCShortPress();
void handleButtonCLongPress();
void checkAlarm();
void displayNormalState();
void displayAlarmSettingState();
void displaySoundSelectingState();
void playAlarmSound();
void stopAlarm();
void playBeepSound();
void playMelodySound();
void playChimeSound();
void initBLE();
void handleBLE();
void initButtonInterrupts();
void processButtonInterrupts();
void bleTask(void *parameter); // BLE処理タスク
void wakeUpScreen(); // 画面をONにする
void updateScreenState(); // 画面OFF/ONの状態を更新
void loadAlarmSettings(); // アラーム設定を読み込む
void saveAlarmSettings(); // アラーム設定を保存する
// 割り込みハンドラ(IRAM_ATTRでRAMに配置)
// 注意: 割り込みハンドラ内では時間のかかる処理は避ける
void IRAM_ATTR buttonAISR() {
buttonAInterruptFlag = true;
}
void IRAM_ATTR buttonBISR() {
buttonBInterruptFlag = true;
}
void IRAM_ATTR buttonCISR() {
buttonCInterruptFlag = true;
}
void setup() {
// M5Stackの初期化(LCD、SD、シリアル、I2C)
M5.begin();
// 画面の明るさを設定(0-255)
M5.Lcd.setBrightness(100);
// 背景色を黒に設定
M5.Lcd.fillScreen(BLACK);
// テキストの設定
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 10);
M5.Lcd.println("Connecting WiFi...");
Serial.begin(115200);
Serial.println("M5Stack Alarm Clock");
Serial.println("Initializing...");
// アラーム設定を読み込む(EEPROMから)
loadAlarmSettings();
// WiFi接続
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int wifiAttempts = 0;
while (WiFi.status() != WL_CONNECTED && wifiAttempts < 30) {
delay(500);
Serial.print(".");
wifiAttempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextSize(2);
M5.Lcd.println("WiFi Connected!");
M5.Lcd.setCursor(10, 40);
M5.Lcd.setTextSize(1);
M5.Lcd.print("IP: ");
M5.Lcd.println(WiFi.localIP());
delay(1000);
// NTPクライアントの初期化
timeClient.begin();
timeClient.update();
// 初期化時の日付を記録
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
lastNTPReconnectDate = timeinfo->tm_mday;
Serial.println("NTP time synchronized");
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextSize(2);
M5.Lcd.println("Time Synced!");
delay(1000);
} else {
Serial.println("");
Serial.println("WiFi connection failed!");
M5.Lcd.fillScreen(RED);
M5.Lcd.setCursor(10, 100);
M5.Lcd.setTextSize(2);
M5.Lcd.println("WiFi Failed!");
while (1) {
delay(1000);
}
}
// 画面をクリアして時計表示の準備
M5.Lcd.fillScreen(BLACK);
lastUpdate = millis();
lastActivityTime = millis(); // 初期化時に操作時刻を設定
// スピーカーの初期化
#ifdef USE_EXTERNAL_SPEAKER
// Grove - スピーカープラス(PORT.B GPIO26)の初期化
pinMode(EXTERNAL_SPEAKER_PIN, OUTPUT);
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
Serial.println("External speaker (Grove) initialized on GPIO26");
#else
// M5Stack BasicのスピーカーはGPIO25(DAC出力)で制御
// M5.begin()で自動初期化されるため、特別な初期化は不要
// 音量を10に設定(0-255の範囲)
M5.Speaker.setVolume(10);
Serial.println("Internal speaker initialized with volume: 10");
#endif
// セマフォの作成(共有変数の保護)
bleMutex = xSemaphoreCreateMutex();
if (bleMutex == NULL) {
Serial.println("Failed to create mutex");
}
// BLE初期化
initBLE();
// ボタン割り込みの初期化
initButtonInterrupts();
// BLE処理を別コア(Core 1)で実行するタスクを作成
xTaskCreatePinnedToCore(
bleTask, // タスク関数
"BLETask", // タスク名
10000, // スタックサイズ(バイト)
NULL, // パラメータ
1, // 優先度(0-25、数字が大きいほど優先度が高い)
&bleTaskHandle, // タスクハンドル
1 // コア番号(0または1、1=Core 1)
);
Serial.println("BLE task created on Core 1");
}
// ボタン割り込みの初期化
void initButtonInterrupts() {
// ボタンピンを入力モードに設定(プルアップ)
pinMode(BUTTON_A_PIN, INPUT_PULLUP);
pinMode(BUTTON_B_PIN, INPUT_PULLUP);
pinMode(BUTTON_C_PIN, INPUT_PULLUP);
// 割り込みを設定(FALLING: ボタンが押された時、LOW: ボタンが押されている間)
attachInterrupt(digitalPinToInterrupt(BUTTON_A_PIN), buttonAISR, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_B_PIN), buttonBISR, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_C_PIN), buttonCISR, FALLING);
Serial.println("Button interrupts initialized");
}
// ボタン割り込みフラグを処理
void processButtonInterrupts() {
static unsigned long lastDebounceTimeA = 0;
static unsigned long lastDebounceTimeB = 0;
static unsigned long lastDebounceTimeC = 0;
const unsigned long debounceDelay = 10; // チャタリング対策(ミリ秒)
unsigned long currentTime = millis();
// ボタンAの処理
if (buttonAInterruptFlag) {
buttonAInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeA > debounceDelay) {
lastDebounceTimeA = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_A_PIN) == LOW) {
if (!buttonAPressed) {
buttonAPressed = true;
buttonAPressTime = millis();
buttonALongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンAが押されている間、長押し判定と自動進み処理
if (buttonAPressed) {
if (digitalRead(BUTTON_A_PIN) == HIGH) {
// ボタンが離された
buttonAPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonALongPressHandled) {
// 短押し
handleButtonAShortPress();
}
buttonALongPressed = false;
buttonALongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonAPressTime;
if (pressDuration >= longPressDuration && !buttonALongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonALongPressed = true;
buttonALongPressHandled = true;
handleButtonALongPress();
}
// アラーム設定状態で長押し中は自動的に時を進める
// 長押し処理が実行された後、かつアラーム設定状態の場合
if (buttonALongPressHandled && currentState == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過している場合のみ処理
// currentTimeは上で取得済み(ボタンAの最適化)
if (currentTime - alarmSettingEnterTime >= alarmSettingInputDisableDuration) {
static unsigned long lastAutoIncrementA = 0;
if (currentTime - lastAutoIncrementA >= autoIncrementInterval) {
wakeUpScreen(); // 画面をONにする
alarmHour = (alarmHour + 1) % 24;
lastAutoIncrementA = currentTime;
Serial.print("Alarm hour auto-increment: ");
Serial.println(alarmHour);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
}
}
}
}
// ボタンBの処理
if (buttonBInterruptFlag) {
buttonBInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeB > debounceDelay) {
lastDebounceTimeB = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_B_PIN) == LOW) {
if (!buttonBPressed) {
buttonBPressed = true;
buttonBPressTime = millis();
buttonBLongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンBが押されている間、長押し判定と自動進み処理
if (buttonBPressed) {
if (digitalRead(BUTTON_B_PIN) == HIGH) {
// ボタンが離された
buttonBPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonBLongPressHandled) {
// 短押し
handleButtonBPress();
}
buttonBLongPressed = false;
buttonBLongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonBPressTime;
if (pressDuration >= longPressDuration && !buttonBLongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonBLongPressed = true;
buttonBLongPressHandled = true;
handleButtonBLongPress();
}
// アラーム設定状態で長押し中は自動的に分を進める
// 長押し処理が実行された後、かつアラーム設定状態の場合
if (buttonBLongPressHandled && currentState == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過している場合のみ処理
// currentTimeは上で取得済み(ボタンBの最適化)
if (currentTime - alarmSettingEnterTime >= alarmSettingInputDisableDuration) {
static unsigned long lastAutoIncrementB = 0;
if (currentTime - lastAutoIncrementB >= autoIncrementInterval) {
wakeUpScreen(); // 画面をONにする
alarmMinute = (alarmMinute + 1) % 60;
lastAutoIncrementB = currentTime;
Serial.print("Alarm minute auto-increment: ");
Serial.println(alarmMinute);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
}
}
}
}
// ボタンCの処理
if (buttonCInterruptFlag) {
buttonCInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeC > debounceDelay) {
lastDebounceTimeC = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_C_PIN) == LOW) {
if (!buttonCPressed) {
buttonCPressed = true;
buttonCPressTime = millis();
buttonCLongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンCが押されている間、長押し判定
if (buttonCPressed) {
if (digitalRead(BUTTON_C_PIN) == HIGH) {
// ボタンが離された
buttonCPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonCLongPressHandled) {
// 短押し
handleButtonCShortPress();
}
buttonCLongPressed = false;
buttonCLongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonCPressTime;
if (pressDuration >= longPressDuration && !buttonCLongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonCLongPressed = true;
buttonCLongPressHandled = true;
handleButtonCLongPress();
}
}
}
}
// 画面をONにする
void wakeUpScreen() {
if (!screenOn) {
screenOn = true;
M5.Lcd.setBrightness(screenBrightness);
Serial.println("Screen ON");
}
lastActivityTime = millis(); // 操作時刻を更新
}
// 画面OFF/ONの状態を更新
void updateScreenState() {
// アラームが鳴っている場合は常に画面をONに保つ
if (currentState == STATE_ALARM_RINGING) {
wakeUpScreen();
return;
}
#ifdef ENABLE_SCREEN_AUTO_OFF
// 通常状態でのみ画面OFF処理を行う
if (currentState == STATE_NORMAL) {
unsigned long timeSinceActivity = millis() - lastActivityTime;
if (timeSinceActivity >= screenOffTimeout) {
// 1分間操作がなかったら画面をOFF
if (screenOn) {
screenOn = false;
M5.Lcd.setBrightness(0); // バックライトをOFF
Serial.println("Screen OFF");
}
} else {
// 操作があった場合は画面をONに保つ
if (!screenOn) {
wakeUpScreen();
}
}
} else {
// アラーム設定状態では常に画面をONに保つ
wakeUpScreen();
}
#else
// 画面自動OFF機能が無効の場合は常に画面をONに保つ
wakeUpScreen();
#endif
}
// ボタン状態を更新(割り込み方式に変更)
void updateButtonStates() {
// 割り込みフラグを処理
processButtonInterrupts();
// ボタンが押されたら画面をONにする(processButtonInterrupts内で検出された場合)
// 実際のボタン処理は各ハンドラで行われるため、ここでは画面ON処理のみ
}
// ボタンA短押し処理
void handleButtonAShortPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:時を進める
alarmHour = (alarmHour + 1) % 24;
Serial.print("Alarm hour set to: ");
Serial.println(alarmHour);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
} else if (currentState == STATE_SOUND_SELECTING) {
// アラーム音選択状態:次のアラーム音に切り替え
alarmSoundType = (AlarmSoundType)((alarmSoundType + 1) % 3); // 0, 1, 2をループ
Serial.print("Alarm sound type set to: ");
Serial.println(alarmSoundType);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displaySoundSelectingState();
}
}
// ボタンA長押し処理
void handleButtonALongPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_NORMAL) {
// 通常状態からアラーム設定状態へ遷移
currentState = STATE_ALARM_SETTING;
alarmSettingEnterTime = millis(); // 移行時刻を記録
Serial.println("Entering alarm setting mode");
// 即座に画面を更新
displayAlarmSettingState();
}
// アラーム鳴動中やアラーム設定状態では何もしない
}
// アラーム停止処理(共通関数)
void stopAlarm() {
if (currentState == STATE_ALARM_RINGING) {
alarmRinging = false;
currentState = STATE_NORMAL; // 通常状態に戻す
alarmStartTime = 0; // アラーム開始時刻をリセット
alarmVolume = alarmVolumeMin; // 音量を最小値にリセット
lastVolumeUpdateTime = 0; // 音量更新時刻をリセット
M5.Speaker.mute(); // 音を止める
Serial.println("Alarm stopped");
}
}
// ボタンB短押し処理
void handleButtonBPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:分を進める
alarmMinute = (alarmMinute + 1) % 60;
Serial.print("Alarm minute set to: ");
Serial.println(alarmMinute);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
#ifdef ENABLE_ALARM_STOP
else if (currentState == STATE_ALARM_RINGING) {
// アラーム停止
stopAlarm();
}
#endif
}
// ボタンB長押し処理
void handleButtonBLongPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_NORMAL) {
// 通常状態からアラーム音選択状態へ遷移
currentState = STATE_SOUND_SELECTING;
alarmSettingEnterTime = millis(); // 移行時刻を記録(入力無効化用)
Serial.println("Entering sound selecting mode");
// 即座に画面を更新
displaySoundSelectingState();
} else if (currentState == STATE_ALARM_SETTING) {
// アラーム設定状態での長押しは自動進み処理で既に処理される
Serial.println("Button B long press in alarm setting mode");
}
}
// ボタンC短押し処理
void handleButtonCShortPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:アラーム時刻を確定して通常状態へ
currentState = STATE_NORMAL;
Serial.print("Alarm set to: ");
Serial.print(alarmHour);
Serial.print(":");
Serial.println(alarmMinute);
// 設定を保存(SETボタン押下時)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
} else if (currentState == STATE_SOUND_SELECTING) {
// アラーム音選択状態:選択を確定して通常状態へ
currentState = STATE_NORMAL;
Serial.print("Alarm sound type set to: ");
Serial.println(alarmSoundType);
// 設定を保存(SETボタン押下時)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
}
}
// ボタンC長押し処理
void handleButtonCLongPress() {
wakeUpScreen(); // 画面をONにする
if (currentState == STATE_NORMAL) {
// 通常状態:アラームON/OFF切り替え
alarmEnabled = !alarmEnabled;
Serial.print("Alarm ");
Serial.println(alarmEnabled ? "ON" : "OFF");
// 設定を保存(即座に反映が必要なため)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
}
}
// アラームチェック
void checkAlarm() {
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// 毎日13時になったらアラームを自動的にON
if (hours == 13 && lastHour != 13) {
alarmEnabled = true;
Serial.println("Alarm automatically enabled at 13:00");
}
lastHour = hours;
// アラームがONで、設定時刻になったらアラームを鳴らす
if (alarmEnabled && currentState != STATE_ALARM_RINGING && hours == alarmHour && minutes == alarmMinute && seconds == 0) {
alarmRinging = true;
currentState = STATE_ALARM_RINGING; // アラーム鳴動状態に遷移
alarmStartTime = millis(); // アラーム開始時刻を記録
alarmVolume = alarmVolumeMin; // 音量を最小値(1)に設定
lastVolumeUpdateTime = alarmStartTime; // 音量更新時刻を初期化
lastBeepTime = 0; // ビープ音を即座に開始
wakeUpScreen(); // アラームが鳴ったら画面をONにする
// 音量を設定(内蔵スピーカーの場合)
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.setVolume(alarmVolume);
#endif
Serial.print("ALARM RINGING! Volume: ");
Serial.println(alarmVolume);
}
// アラームが鳴っている場合の処理
if (currentState == STATE_ALARM_RINGING && alarmStartTime > 0) {
unsigned long currentTime = millis();
// オーバーフロー対策: 現在時刻が開始時刻より小さい場合はオーバーフローを考慮
unsigned long elapsedTime;
if (currentTime >= alarmStartTime) {
elapsedTime = currentTime - alarmStartTime;
} else {
// オーバーフローが発生した場合(約49日後に発生)
elapsedTime = (ULONG_MAX - alarmStartTime) + currentTime;
}
// 3分間以上鳴っている場合は自動停止
if (elapsedTime >= alarmMaxDuration) {
Serial.println("Alarm automatically stopped after 3 minutes");
stopAlarm();
alarmStartTime = 0; // リセット
}
// 10秒ごとに音量を上げる(最大10まで)
else if (alarmVolume < alarmVolumeMax) {
// オーバーフロー対策: 現在時刻が最後の更新時刻より小さい場合はオーバーフローを考慮
unsigned long timeSinceLastVolumeUpdate;
if (currentTime >= lastVolumeUpdateTime) {
timeSinceLastVolumeUpdate = currentTime - lastVolumeUpdateTime;
} else {
// オーバーフローが発生した場合
timeSinceLastVolumeUpdate = (ULONG_MAX - lastVolumeUpdateTime) + currentTime;
}
if (timeSinceLastVolumeUpdate >= alarmVolumeInterval) {
alarmVolume++;
lastVolumeUpdateTime = currentTime;
// 音量を設定(内蔵スピーカーの場合)
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.setVolume(alarmVolume);
#endif
Serial.print("Alarm volume increased to: ");
Serial.println(alarmVolume);
}
}
}
}
// アラーム音を再生
void playAlarmSound() {
#ifdef USE_EXTERNAL_SPEAKER
static unsigned long externalBeepStartTime = 0;
static bool externalBeeping = false;
static int pulseCount = 0;
static int currentHalfPeriod = 0; // 現在の半周期(マイクロ秒)
const int pulsesPerBeep = 200; // 1回のビープで鳴らすパルス数
// digitalWrite()のオーバーヘッドを考慮した補正値(マイクロ秒)
const int digitalWriteOverhead = 2;
#endif
if (currentState == STATE_ALARM_RINGING) {
// 選択されたアラーム音の種類に応じて再生
switch (alarmSoundType) {
case ALARM_SOUND_BEEP:
playBeepSound();
break;
case ALARM_SOUND_MELODY:
playMelodySound();
break;
case ALARM_SOUND_CHIME:
playChimeSound();
break;
default:
playBeepSound(); // デフォルトはビープ音
break;
}
} else {
// アラームが停止したら音を止める
#ifdef USE_EXTERNAL_SPEAKER
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
externalBeeping = false;
pulseCount = 0;
currentHalfPeriod = 0;
#else
M5.Speaker.mute();
#endif
// 周波数フラグをリセット
useHighFrequency = true;
currentMelodyNote = 0;
currentChimeNote = 0;
lastBeepTime = 0;
lastMelodyNoteTime = 0;
lastChimeTime = 0;
}
}
// ビープ音を再生
void playBeepSound() {
#ifdef USE_EXTERNAL_SPEAKER
static unsigned long externalBeepStartTime = 0;
static bool externalBeeping = false;
static int pulseCount = 0;
static int currentHalfPeriod = 0; // 現在の半周期(マイクロ秒)
const int pulsesPerBeep = 200; // 1回のビープで鳴らすパルス数
// digitalWrite()のオーバーヘッドを考慮した補正値(マイクロ秒)
const int digitalWriteOverhead = 2;
// Grove - スピーカープラスで音を鳴らす(非ブロッキング方式)
unsigned long currentMillis = millis();
if (!externalBeeping) {
// 一定間隔ごとに音を開始
if (currentMillis - lastBeepTime >= beepInterval) {
// 現在使用する周波数を選択(高音・低音を交互に)
int currentFrequency = useHighFrequency ? beepFrequencyHigh : beepFrequencyLow;
// 半周期を計算(digitalWrite()のオーバーヘッドを考慮)
currentHalfPeriod = (1000000L / currentFrequency / 2) - digitalWriteOverhead;
if (currentHalfPeriod < 10) currentHalfPeriod = 10; // 最小値制限
externalBeeping = true;
externalBeepStartTime = currentMillis;
pulseCount = 0;
lastBeepTime = currentMillis;
}
}
if (externalBeeping) {
// beepDuration経過するまで、少しずつパルスを生成
if (currentMillis - externalBeepStartTime < beepDuration) {
// 1回のloop()呼び出しで20パルス生成
for (int i = 0; i < 20 && pulseCount < pulsesPerBeep; i++) {
digitalWrite(EXTERNAL_SPEAKER_PIN, HIGH);
delayMicroseconds(currentHalfPeriod);
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
delayMicroseconds(currentHalfPeriod);
pulseCount++;
}
} else {
// beepDuration経過後は音を停止し、次回は別の周波数を使用
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
externalBeeping = false;
pulseCount = 0;
useHighFrequency = !useHighFrequency; // 高音⇔低音を交互に切り替え
}
}
#else
// 内蔵スピーカーで音を鳴らす(高音・低音を交互に)
if (millis() - lastBeepTime >= beepInterval) {
int currentFrequency = useHighFrequency ? beepFrequencyHigh : beepFrequencyLow;
M5.Speaker.tone(currentFrequency, beepDuration);
useHighFrequency = !useHighFrequency; // 高音⇔低音を交互に切り替え
lastBeepTime = millis();
}
#endif
}
// メロディーを再生
void playMelodySound() {
unsigned long currentMillis = millis();
if (currentMillis - lastMelodyNoteTime >= melodyNoteInterval) {
// 次の音符を再生
int note = melodyNotes[currentMelodyNote];
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.tone(note, melodyNoteDuration);
#endif
currentMelodyNote = (currentMelodyNote + 1) % melodyNoteCount; // ループ
lastMelodyNoteTime = currentMillis;
}
}
// チャイム音を再生
void playChimeSound() {
unsigned long currentMillis = millis();
if (currentMillis - lastChimeTime >= chimeInterval) {
// 次のチャイム音を再生
int note = chimeNotes[currentChimeNote];
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.tone(note, chimeNoteDuration);
#endif
currentChimeNote = (currentChimeNote + 1) % chimeNoteCount; // ループ
lastChimeTime = currentMillis;
}
}
// 通常状態の画面表示(差分更新でちらつき防止)
void displayNormalState() {
// 画面がOFFの場合は表示しない
if (!screenOn) {
return;
}
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// Unixタイムスタンプから日付情報を取得
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
int year = timeinfo->tm_year + 1900; // tm_yearは1900年からの年数
int month = timeinfo->tm_mon + 1; // tm_monは0-11
int date = timeinfo->tm_mday; // tm_mdayは1-31
int day = timeinfo->tm_wday; // tm_wdayは0=日曜日、1=月曜日、...、6=土曜日
// 現在時刻を表示
char timeString[20];
sprintf(timeString, "%02d:%02d:%02d", hours, minutes, seconds);
// 日付と曜日の文字列を生成(英語表記)
const char* dayNames[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
char dateString[30];
char datePart[20]; // 日付部分のみ
sprintf(dateString, "%04d-%02d-%02d (%s)", year, month, date, dayNames[day]);
sprintf(datePart, "%04d-%02d-%02d (", year, month, date); // 日付と開き括弧
// 状態が変わった場合は画面をリセット
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
lastTimeString[0] = '\0';
lastAlarmString[0] = '\0';
lastAlarmEnabled = false;
lastBleStatus = -1;
lastAlarmRinging = false;
lastDateString[0] = '\0';
lastDisplayState = currentState;
}
// 初回表示の場合は画面全体をクリア
bool isFirstDisplay = (lastTimeString[0] == '\0');
if (isFirstDisplay) {
M5.Lcd.fillScreen(BLACK);
}
// アラームが鳴っている場合は背景を赤・黒で点滅
static unsigned long lastFlashTime = 0;
static bool flashState = false;
bool currentFlashState = false;
bool isAlarmRinging = (currentState == STATE_ALARM_RINGING);
if (isAlarmRinging) {
unsigned long currentTime = millis();
if (currentTime - lastFlashTime >= 500) { // 500msごとに点滅
flashState = !flashState;
lastFlashTime = currentTime;
}
currentFlashState = flashState;
if (currentFlashState) {
M5.Lcd.fillScreen(RED); // 赤背景
} else {
M5.Lcd.fillScreen(BLACK); // 黒背景
}
} else {
// アラームが停止した場合は黒背景に戻す
if (isAlarmRinging != lastAlarmRinging && lastAlarmRinging) {
M5.Lcd.fillScreen(BLACK);
}
currentFlashState = false;
}
// アラーム停止時はすべての情報を再描画
bool alarmJustStopped = (isAlarmRinging != lastAlarmRinging && lastAlarmRinging && !isAlarmRinging);
lastAlarmRinging = isAlarmRinging;
// 日付と曜日の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (strcmp(dateString, lastDateString) != 0 || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
// 前回の日付を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 15);
M5.Lcd.println(lastDateString);
// 新しい日付を表示
// 1. 日付部分と開き括弧を白で表示
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 15);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.print(datePart); // "2024-01-15 ("
// 2. 曜日部分を色付けして表示
uint16_t dayColor = WHITE;
if (!isAlarmRinging) {
if (day == 0) {
// 日曜日は赤
dayColor = RED;
} else if (day == 6) {
// 土曜日は青
dayColor = BLUE;
}
}
M5.Lcd.setTextColor(dayColor);
M5.Lcd.print(dayNames[day]); // "Sun"など
// 3. 閉じ括弧を白で表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.println(")");
strcpy(lastDateString, dateString);
}
// 現在時刻の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (strcmp(timeString, lastTimeString) != 0 || isAlarmRinging || alarmJustStopped) {
// 前回の時刻を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(30, 60);
M5.Lcd.println(lastTimeString);
// 新しい時刻を表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(30, 60);
M5.Lcd.println(timeString);
strcpy(lastTimeString, timeString);
}
// アラーム時刻の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
char alarmString[20];
sprintf(alarmString, "Alarm: %02d:%02d", alarmHour, alarmMinute);
if (strcmp(alarmString, lastAlarmString) != 0 || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
// 前回のアラーム時刻を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 130);
M5.Lcd.println(lastAlarmString);
// 新しいアラーム時刻を表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 130);
M5.Lcd.println(alarmString);
strcpy(lastAlarmString, alarmString);
}
// アラームON/OFF状態の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (alarmEnabled != lastAlarmEnabled || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
// 前回の状態を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 155);
if (lastAlarmEnabled) {
M5.Lcd.println("ALARM ON");
} else {
M5.Lcd.println("ALARM OFF");
}
// 新しい状態を表示(アラーム鳴動中は白に変更)
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 155);
if (alarmEnabled) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : GREEN);
M5.Lcd.println("ALARM ON");
} else {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("ALARM OFF");
}
lastAlarmEnabled = alarmEnabled;
}
// BLE接続状態を表示(セマフォで保護して共有変数にアクセス)
bool isBleConnected = false;
bool isBleScanning = false;
bool isBleFirstConnected = false;
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
isBleConnected = bleConnected;
isBleScanning = bleScanning;
isBleFirstConnected = bleFirstConnected;
xSemaphoreGive(bleMutex);
}
// BLE状態の判定
int currentBleStatus = -1;
if (isBleConnected) {
currentBleStatus = 0; // Connected
} else if (isBleFirstConnected) {
currentBleStatus = 1; // Disconnected
} else if (isBleScanning) {
currentBleStatus = 2; // Scanning
} else if (pServerAddress != NULL) {
currentBleStatus = 3; // Connecting
} else {
currentBleStatus = 4; // Not connected
}
// BLE状態の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (currentBleStatus != lastBleStatus || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
// 前回のBLE状態を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(10, 180);
M5.Lcd.fillRect(10, 180, 300, 20, bgColor); // 領域をクリア
// 新しいBLE状態を表示(アラーム鳴動中は白に変更)
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(10, 180);
if (currentBleStatus == 0) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : GREEN);
M5.Lcd.println("BLE: Connected");
} else if (currentBleStatus == 1) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("BLE: Disconnected");
} else if (currentBleStatus == 2) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : YELLOW);
M5.Lcd.println("BLE: Scanning...");
} else if (currentBleStatus == 3) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : YELLOW);
M5.Lcd.println("BLE: Pairing...");
} else {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("BLE: Not connected");
}
lastBleStatus = currentBleStatus;
}
}
// アラーム設定状態の画面表示(差分更新でちらつき防止)
void displayAlarmSettingState() {
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// 現在時刻を表示
char timeString[20];
sprintf(timeString, "%02d:%02d:%02d", hours, minutes, seconds);
// 設定中のアラーム時刻
char alarmString[20];
sprintf(alarmString, "%02d:%02d", alarmHour, alarmMinute);
// 状態が変わった場合は画面をリセット
// displayNormalState()と同じグローバル変数を使用して状態を追跡
static bool firstDisplay = true;
static char lastSettingTimeString[20] = "";
static char lastSettingAlarmString[20] = "";
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
firstDisplay = true;
lastDisplayState = currentState;
lastTimeString[0] = '\0';
lastAlarmString[0] = '\0';
lastSettingTimeString[0] = '\0';
lastSettingAlarmString[0] = '\0';
}
// 初回表示または状態変化後の再描画
if (firstDisplay) {
M5.Lcd.setTextColor(WHITE);
// 現在時刻(小さめ)
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 20);
M5.Lcd.print("Current: ");
M5.Lcd.println(timeString);
// アラーム時刻を表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(50, 80);
M5.Lcd.println(alarmString);
// 操作説明
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(10, 200);
M5.Lcd.println("A: Hour B: Minute C: Set");
// 表示変数を初期化
strcpy(lastSettingTimeString, timeString);
strcpy(lastSettingAlarmString, alarmString);
firstDisplay = false;
}
// 現在時刻の更新(変更があった場合のみ)
if (strcmp(timeString, lastSettingTimeString) != 0) {
// 前回の時刻を黒で塗りつぶし
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 20);
M5.Lcd.print("Current: ");
M5.Lcd.println(lastSettingTimeString);
// 新しい時刻を表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 20);
M5.Lcd.print("Current: ");
M5.Lcd.println(timeString);
strcpy(lastSettingTimeString, timeString);
}
// アラーム時刻の更新(変更があった場合のみ)
if (strcmp(alarmString, lastSettingAlarmString) != 0) {
// 前回のアラーム時刻を黒で塗りつぶし
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(50, 80);
M5.Lcd.println(lastSettingAlarmString);
// 新しいアラーム時刻を表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(50, 80);
M5.Lcd.println(alarmString);
strcpy(lastSettingAlarmString, alarmString);
}
}
// BLEデバイススキャンコールバック
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// 既にデバイスが見つかっている場合は処理しない
if (pServerAddress != NULL) {
return;
}
Serial.print("Found device: ");
Serial.println(advertisedDevice.toString().c_str());
// HIDサービスを持つデバイスを検索
if (advertisedDevice.haveServiceUUID()) {
BLEUUID serviceUUID = advertisedDevice.getServiceUUID();
Serial.print("Service UUID: ");
Serial.println(serviceUUID.toString().c_str());
// HIDサービスのUUID(0x1812)と一致するか確認
if (serviceUUID.equals(BLEUUID((uint16_t)0x1812))) {
Serial.println("HID service found!");
// スキャンを停止
BLEScan* pScan = advertisedDevice.getScan();
if (pScan != nullptr) {
pScan->stop();
Serial.println("BLE scan stopped");
}
// デバイスアドレスを保存(セマフォで保護)
if (bleMutex != NULL && xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
pServerAddress = new BLEAddress(advertisedDevice.getAddress());
Serial.print("BLE device found: ");
Serial.println(pServerAddress->toString().c_str());
// スキャン状態をfalseに設定
bleScanning = false;
xSemaphoreGive(bleMutex);
}
Serial.println("Ready to connect");
}
}
}
};
// BLE通知コールバック(Core 1で実行される可能性がある)
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
Serial.print("BLE notify received: length=");
Serial.print(length);
Serial.print(" data: ");
for (size_t i = 0; i < length; i++) {
Serial.printf("%02X ", pData[i]);
}
Serial.println();
// ボタンが押された場合(参考サイトでは0x02で検出)
if (length > 0 && (pData[0] == 0x01 || pData[0] == 0x02)) { // Volume Up または Volume Down
Serial.println("Button press detected!");
// currentStateは共有変数ではないが、念のため確認
AppState currentStateCheck = currentState;
// アラーム鳴動中でボタンが押された場合は停止
if (currentStateCheck == STATE_ALARM_RINGING) {
Serial.println("Stopping alarm via BLE button");
stopAlarm();
} else if (currentStateCheck == STATE_NORMAL) {
Serial.println("BLE button pressed in normal state");
} else if (currentStateCheck == STATE_ALARM_SETTING) {
Serial.println("BLE button pressed in alarm setting state");
}
}
}
// BLE初期化
void initBLE() {
Serial.println("Initializing BLE...");
BLEDevice::init("");
// BLEスキャンの設定
BLEScan *pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true); // アクティブスキャンを有効化
pBLEScan->setInterval(1349); // スキャン間隔(ミリ秒)
pBLEScan->setWindow(449); // スキャンウィンドウ(ミリ秒)
// スキャンを開始(0 = 無期限)
pBLEScan->start(0, false); // 無期限スキャン
bleScanning = true;
Serial.println("BLE scan started (scanning indefinitely)");
}
// BLE処理
void handleBLE() {
static unsigned long lastBLEAttempt = 0;
static unsigned long lastStatusPrint = 0;
const unsigned long bleRetryInterval = 2000; // 2秒ごとに再接続試行
const unsigned long statusPrintInterval = 5000; // 5秒ごとに状態を表示
// 定期的にBLE接続状態をシリアルモニターに出力
if (millis() - lastStatusPrint >= statusPrintInterval) {
lastStatusPrint = millis();
Serial.print("BLE Status: ");
if (bleConnected) {
Serial.print("Connected");
if (pServerAddress != NULL) {
Serial.print(" (");
Serial.print(pServerAddress->toString().c_str());
Serial.print(")");
}
} else if (bleScanning) {
Serial.print("Scanning...");
} else if (pServerAddress != NULL) {
Serial.print("Connecting... (");
Serial.print(pServerAddress->toString().c_str());
Serial.print(")");
} else {
Serial.print("Not connected");
}
Serial.println();
}
// セマフォで保護して共有変数にアクセス
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
// スキャン中で、まだデバイスが見つかっていない場合は何もしない
if (bleScanning && pServerAddress == NULL) {
xSemaphoreGive(bleMutex);
return;
}
// デバイスが見つかったらスキャンを停止
if (bleScanning && pServerAddress != NULL) {
BLEScan* pBLEScan = BLEDevice::getScan();
if (pBLEScan != nullptr) {
pBLEScan->stop();
Serial.println("Stopping BLE scan");
}
bleScanning = false;
}
xSemaphoreGive(bleMutex);
}
// デバイスが見つかっていて、まだ接続していない場合
bool shouldConnect = false;
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
shouldConnect = (pServerAddress != NULL && !bleConnected);
xSemaphoreGive(bleMutex);
}
if (shouldConnect) {
// リトライ間隔をチェック
if (millis() - lastBLEAttempt < bleRetryInterval) {
return;
}
lastBLEAttempt = millis();
Serial.println("Attempting to connect to BLE device...");
if (pClient == NULL) {
pClient = BLEDevice::createClient();
Serial.println("BLE client created");
}
// 接続を試行
if (pClient->connect(*pServerAddress)) {
Serial.println("BLE connected successfully!");
// セマフォで保護して共有変数を更新
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = true;
bleFirstConnected = true; // 初回接続完了
xSemaphoreGive(bleMutex);
}
// HIDサービスを取得
BLERemoteService *pRemoteService = pClient->getService(BLEUUID((uint16_t)0x1812));
if (pRemoteService == nullptr) {
Serial.println("Failed to find HID service");
pClient->disconnect();
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = false;
xSemaphoreGive(bleMutex);
}
return;
}
Serial.println("HID service found");
// すべての特性をループして通知を購読
std::map<uint16_t, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristicsByHandle();
if (mapCharacteristics == nullptr) {
Serial.println("Failed to get characteristics");
pClient->disconnect();
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = false;
xSemaphoreGive(bleMutex);
}
return;
}
Serial.print("Found ");
Serial.print(mapCharacteristics->size());
Serial.println(" characteristics");
bool notifyRegistered = false;
for (std::map<uint16_t, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
BLERemoteCharacteristic* pCharacteristic = i->second;
Serial.print("Characteristic UUID: ");
Serial.println(pCharacteristic->getUUID().toString().c_str());
if (pCharacteristic->canNotify()) {
Serial.println("Registering for notify...");
// registerForNotifyは戻り値がvoidなので、呼び出すだけ
pCharacteristic->registerForNotify(notifyCallback);
Serial.println("Notify registered successfully!");
notifyRegistered = true;
}
}
if (!notifyRegistered) {
Serial.println("Warning: No notify characteristics registered");
}
} else {
Serial.println("BLE connection failed, will retry");
}
}
// 接続が切れた場合の処理
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bool isConnected = bleConnected;
xSemaphoreGive(bleMutex);
if (isConnected && pClient != NULL) {
if (!pClient->isConnected()) {
Serial.println("BLE disconnected");
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = false;
xSemaphoreGive(bleMutex);
}
pClient->disconnect();
delete pClient;
pClient = NULL;
// 初回接続完了後は再接続しない(時計表示を継続)
}
}
}
}
// 接続待ち画面の表示
void displayWaitingForBLE() {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(30, 80);
M5.Lcd.println("Waiting for");
M5.Lcd.setCursor(30, 120);
M5.Lcd.println("BLE Button");
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(30, 180);
bool isBleScanning = false;
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
isBleScanning = bleScanning;
xSemaphoreGive(bleMutex);
}
if (isBleScanning) {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("Scanning...");
} else if (pServerAddress != NULL) {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("Connecting...");
} else {
M5.Lcd.setTextColor(RED);
M5.Lcd.println("Not found");
}
}
// BLE処理タスク(Core 1で実行)
void bleTask(void *parameter) {
Serial.print("BLE task started on core: ");
Serial.println(xPortGetCoreID());
while (true) {
// BLE処理を実行
handleBLE();
// タスクを少し待機(CPU使用率を下げる)
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void loop() {
// loop()はCore 0で実行される
// BLE処理は別タスク(Core 1)で実行されるため、ここでは呼び出さない
// 初回接続完了前は接続待ち画面を表示し、他の処理を停止
bool isBleFirstConnected = false;
bool isBleScanning = false;
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
isBleFirstConnected = bleFirstConnected;
isBleScanning = bleScanning;
xSemaphoreGive(bleMutex);
}
if (!isBleFirstConnected) {
displayWaitingForBLE();
delay(100);
return;
}
// 初回接続完了後は通常処理を継続
// ボタン状態を更新(割り込み方式)
updateButtonStates();
// 画面OFF/ONの状態を更新
updateScreenState();
// 1秒ごとに時刻を更新
if (millis() - lastUpdate >= updateInterval) {
lastUpdate = millis();
// 重い処理の前にボタン状態をチェック(応答性向上)
processButtonInterrupts();
// 現在の日付を取得
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
int currentDate = timeinfo->tm_mday; // 現在の日付(1-31)
// 1日1回NTPに再接続して時刻を補正
if (currentDate != lastNTPReconnectDate) {
Serial.println("Reconnecting to NTP server for daily time correction...");
// NTPクライアントを再初期化
timeClient.end();
delay(100);
timeClient.begin();
// NTPから時刻を取得(最大5回リトライ)
bool ntpUpdated = false;
for (int retry = 0; retry < 5; retry++) {
if (timeClient.update()) {
ntpUpdated = true;
break;
}
delay(1000);
}
if (ntpUpdated) {
lastNTPReconnectDate = currentDate;
Serial.println("NTP reconnected and time corrected");
Serial.print("Current time: ");
Serial.print(timeClient.getHours());
Serial.print(":");
Serial.print(timeClient.getMinutes());
Serial.print(":");
Serial.println(timeClient.getSeconds());
} else {
Serial.println("NTP reconnection failed, will retry tomorrow");
}
}
// アラームチェック
checkAlarm();
// 状態に応じて画面を表示(画面がONの場合のみ)
if (screenOn) {
if (currentState == STATE_NORMAL || currentState == STATE_ALARM_RINGING) {
displayNormalState();
} else if (currentState == STATE_ALARM_SETTING) {
displayAlarmSettingState();
} else if (currentState == STATE_SOUND_SELECTING) {
displaySoundSelectingState();
}
}
}
// アラーム音を再生(ループ内で常にチェック)
playAlarmSound();
// ボタン処理を再度実行(応答性向上のため)
// delay()の前に実行することで、ボタン長押し判定の遅延を最小化
processButtonInterrupts();
delay(10); // 高負荷防止
}
// アラーム設定を読み込む(EEPROMから)
void loadAlarmSettings() {
preferences.begin(PREF_NAMESPACE, true); // 読み取り専用モードで開く
// 設定を読み込む(存在しない場合はデフォルト値を使用)
alarmHour = preferences.getUChar(PREF_KEY_HOUR, 6); // デフォルト6時
alarmMinute = preferences.getUChar(PREF_KEY_MINUTE, 15); // デフォルト15分
alarmEnabled = preferences.getBool(PREF_KEY_ENABLED, true); // デフォルトON
alarmSoundType = (AlarmSoundType)preferences.getUChar(PREF_KEY_SOUND_TYPE, ALARM_SOUND_BEEP); // デフォルト: ビープ音
preferences.end();
Serial.println("Alarm settings loaded from EEPROM:");
Serial.print(" Hour: ");
Serial.println(alarmHour);
Serial.print(" Minute: ");
Serial.println(alarmMinute);
Serial.print(" Enabled: ");
Serial.println(alarmEnabled ? "ON" : "OFF");
Serial.print(" Sound Type: ");
Serial.println(alarmSoundType);
}
// アラーム設定を保存する(EEPROMへ)
void saveAlarmSettings() {
preferences.begin(PREF_NAMESPACE, false); // 読み書きモードで開く
// 設定を保存
preferences.putUChar(PREF_KEY_HOUR, alarmHour);
preferences.putUChar(PREF_KEY_MINUTE, alarmMinute);
preferences.putBool(PREF_KEY_ENABLED, alarmEnabled);
preferences.putUChar(PREF_KEY_SOUND_TYPE, (uint8_t)alarmSoundType);
preferences.end();
Serial.println("Alarm settings saved to EEPROM");
}
// アラーム音選択状態の画面表示
void displaySoundSelectingState() {
// 状態が変わった場合は画面をリセット
static bool firstDisplay = true;
static int lastSoundType = -1;
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
firstDisplay = true;
lastDisplayState = currentState;
lastSoundType = -1;
}
// 初回表示またはアラーム音の種類が変わった場合のみ再描画
if (firstDisplay || (int)alarmSoundType != lastSoundType) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
// タイトル
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 20);
M5.Lcd.println("Alarm Sound");
// 選択中のアラーム音を表示(大きく)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(4);
M5.Lcd.setCursor(50, 100);
const char* soundNames[] = {"BEEP", "MELODY", "CHIME"};
M5.Lcd.println(soundNames[alarmSoundType]);
// 操作説明
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(10, 200);
M5.Lcd.println("A: Change C: Set");
// 表示変数を更新
lastSoundType = (int)alarmSoundType;
firstDisplay = false;
}
}
完成版ソースコード
#include <M5Stack.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <time.h>
#include "BLEDevice.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include <Preferences.h>
#include "esp_system.h"
#include "credentials.h" // WiFi認証情報(Git管理外)
// アラーム停止機能の有効/無効(ifdef対応)
//#define ENABLE_ALARM_STOP
// 画面自動OFF機能の有効/無効(ifdef対応)
#define ENABLE_SCREEN_AUTO_OFF
// 外部スピーカー(Grove - スピーカープラス)の使用
// コメントアウトすると内蔵スピーカーを使用
// 注意: Grove - Speaker Plusは容量の影響で高音が出せません(ベース音のみ)
// 高音の「ピピッ」音を出すには、内蔵スピーカーを使用してください
//#define USE_EXTERNAL_SPEAKER
#ifdef USE_EXTERNAL_SPEAKER
// Grove - スピーカープラス用の設定(PORT.B の GPIO26)
#define EXTERNAL_SPEAKER_PIN 26
#endif
// WiFi設定(credentials.hから読み込み)
const char* ssid = WIFI_SSID;
const char* password = WIFI_PASSWORD;
// NTP設定(credentials.hから読み込み)
const char* ntpServer = NTP_SERVER;
const long gmtOffset_sec = GMT_OFFSET_SEC;
// 画面レイアウト定数
namespace Layout {
namespace Normal {
constexpr int DATE_X = 10;
constexpr int DATE_Y = 15;
constexpr int TIME_X = 30;
constexpr int TIME_Y = 60;
constexpr int ALARM1_X = 10;
constexpr int ALARM1_Y = 130;
constexpr int ALARM2_X = 10;
constexpr int ALARM2_Y = 155;
constexpr int BLE_STATUS_X = 10;
constexpr int BLE_STATUS_Y = 180;
constexpr int BLE_STATUS_WIDTH = 300;
constexpr int BLE_STATUS_HEIGHT = 20;
}
namespace Setting {
constexpr int CURRENT_TIME_X = 10;
constexpr int CURRENT_TIME_Y = 20;
constexpr int ALARM_LABEL_X = 10;
constexpr int ALARM_LABEL_Y = 50;
constexpr int ALARM_TIME_X = 50;
constexpr int ALARM_TIME_Y = 80;
constexpr int INSTRUCTION_X = 10;
constexpr int INSTRUCTION_Y = 200;
}
namespace Sound {
constexpr int TITLE_X = 10;
constexpr int TITLE_Y = 20;
constexpr int ALARM_LABEL_X = 10;
constexpr int ALARM_LABEL_Y = 50;
constexpr int SOUND_TYPE_X = 50;
constexpr int SOUND_TYPE_Y = 100;
constexpr int INSTRUCTION_X = 10;
constexpr int INSTRUCTION_Y = 200;
}
}
// 接続・再試行関連の定数
namespace Connection {
constexpr int WIFI_MAX_ATTEMPTS = 30; // WiFi接続最大試行回数
constexpr int WIFI_RETRY_DELAY_MS = 500; // WiFi接続リトライ間隔(ミリ秒)
constexpr int NTP_MAX_RETRIES = 5; // NTP同期最大リトライ回数
constexpr int NTP_RETRY_DELAY_MS = 1000; // NTPリトライ間隔(ミリ秒)
constexpr unsigned long NTP_UPDATE_INTERVAL_MS = 3600000; // NTP更新間隔(1時間)
}
// 画面表示関連の定数
namespace Display {
constexpr unsigned long FLASH_INTERVAL_MS = 500; // アラーム鳴動時の画面点滅間隔(ミリ秒)
constexpr int SPEAKER_VOLUME = 10; // スピーカー音量(0-255)
constexpr int SCREEN_BRIGHTNESS = 100; // 画面の明るさ(0-255)
}
// 外部スピーカー関連の定数
namespace ExternalSpeaker {
constexpr int PULSES_PER_LOOP = 20; // ループあたりのパルス数
}
// NTPクライアント
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, gmtOffset_sec);
// 状態定義
enum AppState {
STATE_NORMAL, // 通常状態(時計表示)
STATE_ALARM_SETTING, // アラーム設定状態
STATE_ALARM_RINGING, // アラーム鳴動中
STATE_SOUND_SELECTING // アラーム音選択状態
};
// アラーム音の種類
enum AlarmSoundType {
ALARM_SOUND_BEEP, // ビープ音(高音・低音交互)
ALARM_SOUND_MELODY, // メロディー
ALARM_SOUND_CHIME // チャイム音
};
// アラーム設定構造体
struct AlarmConfig {
int hour; // アラーム時刻(時)0-23
int minute; // アラーム時刻(分)0-59
bool enabled; // アラームON/OFF
AlarmSoundType soundType; // アラーム音の種類
};
// アプリケーション状態
AppState currentState = STATE_NORMAL;
// アラーム設定(2つのアラーム)
AlarmConfig alarms[2] = {
{6, 15, true, ALARM_SOUND_BEEP}, // アラーム1: 6時15分、ON、ビープ音
{7, 0, false, ALARM_SOUND_BEEP} // アラーム2: 7時00分、OFF、ビープ音
};
// 現在選択中のアラーム(0: アラーム1, 1: アラーム2)
int selectedAlarmIndex = 0; // デフォルトはアラーム1
// 現在鳴っているアラームのインデックス(-1: 鳴っていない, 0: アラーム1, 1: アラーム2)
int ringingAlarmIndex = -1;
// EEPROM設定保存用
Preferences preferences;
const char* PREF_NAMESPACE = "alarm"; // 設定の名前空間
// 既存のキー(互換性のため維持)
const char* PREF_KEY_HOUR = "hour"; // アラーム時刻(時)のキー
const char* PREF_KEY_MINUTE = "minute"; // アラーム時刻(分)のキー
const char* PREF_KEY_ENABLED = "enabled"; // アラームON/OFFのキー
const char* PREF_KEY_SOUND_TYPE = "soundType"; // アラーム音の種類のキー
// アラーム1用のキー
const char* PREF_KEY_ALARM1_HOUR = "alarm1_hour";
const char* PREF_KEY_ALARM1_MINUTE = "alarm1_minute";
const char* PREF_KEY_ALARM1_ENABLED = "alarm1_enabled";
const char* PREF_KEY_ALARM1_SOUND_TYPE = "alarm1_soundType";
// アラーム2用のキー
const char* PREF_KEY_ALARM2_HOUR = "alarm2_hour";
const char* PREF_KEY_ALARM2_MINUTE = "alarm2_minute";
const char* PREF_KEY_ALARM2_ENABLED = "alarm2_enabled";
const char* PREF_KEY_ALARM2_SOUND_TYPE = "alarm2_soundType";
// 再起動情報保存用のキー(NVSのキー名は15文字以内)
const char* PREF_KEY_RESET_COUNT = "rst_count"; // 再起動回数
const char* PREF_KEY_LAST_RESET_REASON = "rst_reason"; // 最後の再起動理由
const char* PREF_KEY_LAST_RESET_TIME = "rst_time"; // 最後の再起動時刻(Unix時間)
const char* PREF_KEY_RESET_HISTORY = "rst_history"; // 再起動履歴(簡易版)
unsigned long alarmStartTime = 0; // アラーム開始時刻(ミリ秒)
const unsigned long alarmMaxDuration = 3 * 60 * 1000; // アラーム最大継続時間(3分 = 180000ミリ秒)
int alarmVolume = 1; // 現在のアラーム音量(1~10)
const int alarmVolumeMin = 1; // アラーム音量の最小値
const int alarmVolumeMax = 10; // アラーム音量の最大値
const unsigned long alarmVolumeInterval = 10 * 1000; // 音量を上げる間隔(10秒 = 10000ミリ秒)
unsigned long lastVolumeUpdateTime = 0; // 最後に音量を更新した時刻
// 時刻表示用の変数
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 1000; // 1秒ごとに更新
// ボタンGPIOピン定義(M5Stack Basic)
#define BUTTON_A_PIN 39
#define BUTTON_B_PIN 38
#define BUTTON_C_PIN 37
// ボタン検出用の変数
volatile unsigned long buttonAPressTime = 0;
volatile unsigned long buttonCPressTime = 0;
volatile unsigned long buttonBPressTime = 0;
const unsigned long longPressDuration = 1000; // 長押し判定時間(ミリ秒)= 1秒
const unsigned long autoIncrementInterval = 200; // 長押し時の自動進み間隔(ミリ秒)
volatile bool buttonAPressed = false;
volatile bool buttonBPressed = false;
volatile bool buttonCPressed = false;
volatile bool buttonAInterruptFlag = false;
volatile bool buttonBInterruptFlag = false;
volatile bool buttonCInterruptFlag = false;
volatile bool buttonALongPressHandled = false; // 長押し処理が実行されたかどうか
volatile bool buttonBLongPressHandled = false; // 長押し処理が実行されたかどうか
volatile bool buttonCLongPressHandled = false; // 長押し処理が実行されたかどうか
// 前回の時刻(13時自動ON判定用)
int lastHour = -1;
// NTP再接続用の変数
int lastNTPReconnectDate = -1; // 最後にNTP再接続した日付
// アラーム設定状態移行後のボタン入力無効化用
unsigned long alarmSettingEnterTime = 0; // アラーム設定状態に移行した時刻
const unsigned long alarmSettingInputDisableDuration = 1000; // ボタン入力無効化時間(ミリ秒)= 1秒
// 画面OFF/ON制御用の変数
unsigned long lastActivityTime = 0; // 最後の操作時刻
const unsigned long screenOffTimeout = 60000; // 1分 = 60000ミリ秒
bool screenOn = true; // 画面ON/OFF状態
const int screenBrightness = 100; // 画面の明るさ(0-255)
// 画面更新用の変数(ちらつき防止のため前回の値を保存)
char lastTimeString[20] = "";
char lastAlarm1String[30] = ""; // アラーム1の表示文字列
char lastAlarm2String[30] = ""; // アラーム2の表示文字列
bool lastAlarm1Enabled = false; // アラーム1のON/OFF状態
bool lastAlarm2Enabled = false; // アラーム2のON/OFF状態
int lastBleStatus = -1; // -1:未初期化, 0:Connected, 1:Disconnected, 2:Scanning, 3:Connecting, 4:Not connected
bool lastAlarmRinging = false;
char lastDateString[30] = ""; // 日付と曜日の文字列
// 状態変化検出用のグローバル変数(displayNormalState()とdisplayAlarmSettingState()で共有)
AppState lastDisplayState = STATE_NORMAL;
// アラーム音用の変数
unsigned long lastBeepTime = 0;
const unsigned long beepInterval = 250; // ビープ音の間隔(ミリ秒)
const int beepFrequencyHigh = 2500; // ビープ音の高音周波数(Hz)- 「ピ」
const int beepFrequencyLow = 2200; // ビープ音の低音周波数(Hz)- 「ピ」
const int beepDuration = 150; // ビープ音の長さ(ミリ秒)
bool useHighFrequency = true; // 高音・低音を交互に切り替えるフラグ
// メロディー用の変数
const int melodyNotes[] = {523, 587, 659, 698, 784, 880, 988, 1047}; // C, D, E, F, G, A, B, C(ドレミファソラシド)
const int melodyNoteCount = 8;
int currentMelodyNote = 0; // 現在のメロディーの音符
unsigned long lastMelodyNoteTime = 0;
const unsigned long melodyNoteInterval = 200; // メロディーの音符間隔(ミリ秒)
const int melodyNoteDuration = 150; // メロディーの音符の長さ(ミリ秒)
// チャイム音用の変数
const int chimeNotes[] = {523, 659, 784}; // C, E, G(ドミソ)
const int chimeNoteCount = 3;
int currentChimeNote = 0; // 現在のチャイムの音符
unsigned long lastChimeTime = 0;
const unsigned long chimeInterval = 300; // チャイム音の間隔(ミリ秒)
const int chimeNoteDuration = 200; // チャイム音の長さ(ミリ秒)
// BLE設定
static BLEAddress *pServerAddress = NULL;
static BLEClient *pClient = NULL;
static volatile bool bleConnected = false;
static volatile bool bleScanning = false;
static volatile bool bleFirstConnected = false; // 初回接続完了フラグ
static volatile bool bleConnecting = false; // 接続試行中フラグ(重複接続防止)
static unsigned long bleConnectionStartTime = 0; // 接続開始時刻
// マルチコア処理用のセマフォ(共有変数の保護)
static SemaphoreHandle_t bleMutex = NULL;
static SemaphoreHandle_t stateMutex = NULL; // currentState保護用
// タスクハンドル
static TaskHandle_t bleTaskHandle = NULL;
// BLEイベント通知用キュー
static QueueHandle_t bleEventQueue = NULL;
typedef uint8_t BleEventType;
const BleEventType BLE_EVENT_STOP_ALARM = 1;
// 関数の前方宣言
void updateButtonStates();
void handleButtonAShortPress();
void handleButtonALongPress();
void handleButtonBPress();
void handleButtonBLongPress();
void handleButtonCShortPress();
void handleButtonCLongPress();
void checkAlarm();
void displayNormalState();
void displayAlarmSettingState();
void displaySoundSelectingState();
void playAlarmSound();
void stopAlarm();
void playBeepSound();
void playMelodySound();
void playChimeSound();
void initBLE();
void handleBLE();
void initButtonInterrupts();
void processButtonInterrupts();
void bleTask(void *parameter); // BLE処理タスク
void wakeUpScreen(); // 画面をONにする
void updateScreenState(); // 画面OFF/ONの状態を更新
void migrateOldAlarmSettings(); // 既存設定の移行処理
void loadAlarmSettings(); // アラーム設定を読み込む
void saveAlarmSettings(); // アラーム設定を保存する
AppState getAppState(); // 状態を安全に取得
void setAppState(AppState newState); // 状態を安全に設定
void cleanupBLEResources(); // BLEリソースのクリーンアップ
void displayDateTime(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped); // 日付と時刻の表示
void displayAlarmStatus(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped, bool selectedAlarmChanged); // アラーム状態の表示
void displayBLEStatus(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped); // BLE状態の表示
// 割り込みハンドラ(IRAM_ATTRでRAMに配置)
// 注意: 割り込みハンドラ内では時間のかかる処理は避ける
void IRAM_ATTR buttonAISR() {
buttonAInterruptFlag = true;
}
void IRAM_ATTR buttonBISR() {
buttonBInterruptFlag = true;
}
void IRAM_ATTR buttonCISR() {
buttonCInterruptFlag = true;
}
// 前方宣言
const char* getResetReasonString(esp_reset_reason_t reason);
void recordAndDisplayResetReason();
void displayResetHistory();
void printMemoryInfo();
void printTaskStackInfo();
void setup() {
// M5Stackの初期化(LCD、SD、シリアル、I2C)
M5.begin();
// 画面の明るさを設定(0-255)
M5.Lcd.setBrightness(Display::SCREEN_BRIGHTNESS);
// 背景色を黒に設定
M5.Lcd.fillScreen(BLACK);
// テキストの設定
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 10);
M5.Lcd.println("Connecting WiFi...");
Serial.begin(115200);
delay(100); // シリアルポートの初期化を待つ
Serial.println("M5Stack Alarm Clock");
Serial.println("Initializing...");
// 再起動原因を記録・表示(最初に実行)
recordAndDisplayResetReason();
// アラーム設定を読み込む(EEPROMから)
loadAlarmSettings();
// WiFi接続
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int wifiAttempts = 0;
while (WiFi.status() != WL_CONNECTED && wifiAttempts < Connection::WIFI_MAX_ATTEMPTS) {
delay(Connection::WIFI_RETRY_DELAY_MS);
Serial.print(".");
wifiAttempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextSize(2);
M5.Lcd.println("WiFi Connected!");
M5.Lcd.setCursor(10, 40);
M5.Lcd.setTextSize(1);
M5.Lcd.print("IP: ");
M5.Lcd.println(WiFi.localIP());
delay(1000);
// NTPクライアントの初期化
timeClient.begin();
// NTPから時刻を取得(最大リトライ)
bool ntpUpdated = false;
for (int retry = 0; retry < Connection::NTP_MAX_RETRIES; retry++) {
if (timeClient.update()) {
ntpUpdated = true;
break;
}
delay(Connection::NTP_RETRY_DELAY_MS);
}
if (ntpUpdated) {
// 初期化時の日付を記録
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
lastNTPReconnectDate = timeinfo->tm_mday;
Serial.println("NTP time synchronized");
} else {
Serial.println("NTP time synchronization failed, using default time");
}
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextSize(2);
M5.Lcd.println("Time Synced!");
delay(1000);
} else {
Serial.println("");
Serial.println("WiFi connection failed!");
M5.Lcd.fillScreen(RED);
M5.Lcd.setCursor(10, 100);
M5.Lcd.setTextSize(2);
M5.Lcd.println("WiFi Failed!");
while (1) {
delay(1000);
}
}
// 画面をクリアして時計表示の準備
M5.Lcd.fillScreen(BLACK);
lastUpdate = millis();
lastActivityTime = millis(); // 初期化時に操作時刻を設定
// スピーカーの初期化
#ifdef USE_EXTERNAL_SPEAKER
// Grove - スピーカープラス(PORT.B GPIO26)の初期化
pinMode(EXTERNAL_SPEAKER_PIN, OUTPUT);
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
Serial.println("External speaker (Grove) initialized on GPIO26");
#else
// M5Stack BasicのスピーカーはGPIO25(DAC出力)で制御
// M5.begin()で自動初期化されるため、特別な初期化は不要
// 音量を設定(0-255の範囲)
M5.Speaker.setVolume(Display::SPEAKER_VOLUME);
Serial.print("Internal speaker initialized with volume: ");
Serial.println(Display::SPEAKER_VOLUME);
#endif
// セマフォの作成(共有変数の保護)
bleMutex = xSemaphoreCreateMutex();
if (bleMutex == NULL) {
Serial.println("ERROR: Failed to create BLE mutex - BLE features disabled");
} else {
Serial.println("BLE mutex created");
// BLEイベントキューの作成
bleEventQueue = xQueueCreate(10, sizeof(BleEventType));
if (bleEventQueue == NULL) {
Serial.println("ERROR: Failed to create BLE event queue - notifications cannot be deferred");
}
}
// 状態管理用セマフォの作成
stateMutex = xSemaphoreCreateMutex();
if (stateMutex == NULL) {
Serial.println("ERROR: Failed to create state mutex - state management may be unsafe");
} else {
Serial.println("State mutex created");
}
// BLE初期化(セマフォが利用可能な場合のみ)
if (bleMutex != NULL) {
initBLE();
} else {
Serial.println("Skipping BLE initialization due to mutex creation failure");
}
// ボタン割り込みの初期化
initButtonInterrupts();
// BLE処理タスクの起動(セマフォが利用可能な場合のみ)
if (bleMutex != NULL) {
// BLE処理を別コア(Core 1)で実行するタスクを作成
// スタックサイズを増やしてスタックオーバーフローを防止(BLE処理は重いため)
// アラーム鳴動中のBLE再接続を確実にするため、優先度を2に上げる
xTaskCreatePinnedToCore(
bleTask, // タスク関数
"BLETask", // タスク名
16384, // スタックサイズ(バイト)- 16KBに増加(BLE処理が重いため)
NULL, // パラメータ
2, // 優先度(0-25、数字が大きいほど優先度が高い)- アラーム音より高く
&bleTaskHandle, // タスクハンドル
1 // コア番号(0または1、1=Core 1)
);
Serial.println("BLE task created on Core 1");
} else {
Serial.println("BLE task not created (BLE mutex unavailable)");
}
// 初期化完了のビープ音(短い「ピッ」音)
Serial.println("Initialization complete - playing beep");
#ifdef USE_EXTERNAL_SPEAKER
// 外部スピーカーの場合(GPIO26でパルスを生成)
for (int i = 0; i < 200; i++) { // 約100ms分のパルス
digitalWrite(EXTERNAL_SPEAKER_PIN, HIGH);
delayMicroseconds(250); // 2000Hz = 500μs周期、50%デューティ
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
delayMicroseconds(250);
}
#else
// 内蔵スピーカーの場合
M5.Speaker.tone(2000, 100); // 2000Hz、100msの短いビープ音
delay(150); // ビープ音が終わるまで待つ
#endif
// WiFi接続完了後、時計を表示(BLE接続は待たない)
Serial.println("About to call displayNormalState()...");
Serial.print("screenOn: ");
Serial.println(screenOn ? "true" : "false");
displayNormalState();
Serial.println("displayNormalState() called");
}
// ボタン割り込みの初期化
void initButtonInterrupts() {
// ボタンピンを入力モードに設定(プルアップ)
pinMode(BUTTON_A_PIN, INPUT_PULLUP);
pinMode(BUTTON_B_PIN, INPUT_PULLUP);
pinMode(BUTTON_C_PIN, INPUT_PULLUP);
// 割り込みを設定(FALLING: ボタンが押された時、LOW: ボタンが押されている間)
attachInterrupt(digitalPinToInterrupt(BUTTON_A_PIN), buttonAISR, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_B_PIN), buttonBISR, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_C_PIN), buttonCISR, FALLING);
Serial.println("Button interrupts initialized");
}
// ボタン割り込みフラグを処理
void processButtonInterrupts() {
static unsigned long lastDebounceTimeA = 0;
static unsigned long lastDebounceTimeB = 0;
static unsigned long lastDebounceTimeC = 0;
const unsigned long debounceDelay = 10; // チャタリング対策(ミリ秒)
unsigned long currentTime = millis();
// ボタンAの処理
if (buttonAInterruptFlag) {
buttonAInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeA > debounceDelay) {
lastDebounceTimeA = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_A_PIN) == LOW) {
if (!buttonAPressed) {
buttonAPressed = true;
buttonAPressTime = millis();
buttonALongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンAが押されている間、長押し判定と自動進み処理
if (buttonAPressed) {
if (digitalRead(BUTTON_A_PIN) == HIGH) {
// ボタンが離された
buttonAPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonALongPressHandled) {
// 短押し
handleButtonAShortPress();
}
buttonALongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonAPressTime;
if (pressDuration >= longPressDuration && !buttonALongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonALongPressHandled = true;
handleButtonALongPress();
}
// アラーム設定状態で長押し中は自動的に時を進める
// 長押し処理が実行された後、かつアラーム設定状態の場合
if (buttonALongPressHandled && getAppState() == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過している場合のみ処理
// currentTimeは上で取得済み(ボタンAの最適化)
if (currentTime - alarmSettingEnterTime >= alarmSettingInputDisableDuration) {
static unsigned long lastAutoIncrementA = 0;
if (currentTime - lastAutoIncrementA >= autoIncrementInterval) {
wakeUpScreen(); // 画面をONにする
alarms[selectedAlarmIndex].hour = (alarms[selectedAlarmIndex].hour + 1) % 24;
lastAutoIncrementA = currentTime;
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" hour auto-increment: ");
Serial.println(alarms[selectedAlarmIndex].hour);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
}
}
}
}
// ボタンBの処理
if (buttonBInterruptFlag) {
buttonBInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeB > debounceDelay) {
lastDebounceTimeB = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_B_PIN) == LOW) {
if (!buttonBPressed) {
buttonBPressed = true;
buttonBPressTime = millis();
buttonBLongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンBが押されている間、長押し判定と自動進み処理
if (buttonBPressed) {
if (digitalRead(BUTTON_B_PIN) == HIGH) {
// ボタンが離された
buttonBPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonBLongPressHandled) {
// 短押し
handleButtonBPress();
}
buttonBLongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonBPressTime;
if (pressDuration >= longPressDuration && !buttonBLongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonBLongPressHandled = true;
handleButtonBLongPress();
}
// アラーム設定状態で長押し中は自動的に分を進める
// 長押し処理が実行された後、かつアラーム設定状態の場合
if (buttonBLongPressHandled && getAppState() == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過している場合のみ処理
// currentTimeは上で取得済み(ボタンBの最適化)
if (currentTime - alarmSettingEnterTime >= alarmSettingInputDisableDuration) {
static unsigned long lastAutoIncrementB = 0;
if (currentTime - lastAutoIncrementB >= autoIncrementInterval) {
wakeUpScreen(); // 画面をONにする
alarms[selectedAlarmIndex].minute = (alarms[selectedAlarmIndex].minute + 1) % 60;
lastAutoIncrementB = currentTime;
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" minute auto-increment: ");
Serial.println(alarms[selectedAlarmIndex].minute);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
}
}
}
}
// ボタンCの処理
if (buttonCInterruptFlag) {
buttonCInterruptFlag = false;
// デバウンス処理
if (currentTime - lastDebounceTimeC > debounceDelay) {
lastDebounceTimeC = currentTime;
// ボタンがまだ押されているか確認
if (digitalRead(BUTTON_C_PIN) == LOW) {
if (!buttonCPressed) {
buttonCPressed = true;
buttonCPressTime = millis();
buttonCLongPressHandled = false; // 長押し処理フラグをリセット
}
}
}
}
// ボタンCが押されている間、長押し判定
if (buttonCPressed) {
if (digitalRead(BUTTON_C_PIN) == HIGH) {
// ボタンが離された
buttonCPressed = false;
// 長押し処理が既に実行されていれば短押し処理はスキップ
if (!buttonCLongPressHandled) {
// 短押し
handleButtonCShortPress();
}
buttonCLongPressHandled = false;
} else {
// ボタンが押されている間、長押し判定
// millis()を1回だけ呼び出して最適化
unsigned long currentTime = millis();
unsigned long pressDuration = currentTime - buttonCPressTime;
if (pressDuration >= longPressDuration && !buttonCLongPressHandled) {
// 1秒以上押されていたら即座に長押し処理を実行
buttonCLongPressHandled = true;
handleButtonCLongPress();
}
}
}
}
// 画面をONにする
void wakeUpScreen() {
if (!screenOn) {
screenOn = true;
M5.Lcd.setBrightness(screenBrightness);
Serial.println("Screen ON");
}
lastActivityTime = millis(); // 操作時刻を更新
}
// 画面OFF/ONの状態を更新
void updateScreenState() {
// アラームが鳴っている場合は常に画面をONに保つ
AppState state = getAppState();
if (state == STATE_ALARM_RINGING) {
wakeUpScreen();
return;
}
#ifdef ENABLE_SCREEN_AUTO_OFF
// 通常状態でのみ画面OFF処理を行う
if (state == STATE_NORMAL) {
unsigned long timeSinceActivity = millis() - lastActivityTime;
if (timeSinceActivity >= screenOffTimeout) {
// 1分間操作がなかったら画面をOFF
if (screenOn) {
screenOn = false;
M5.Lcd.setBrightness(0); // バックライトをOFF
Serial.println("Screen OFF");
}
} else {
// 操作があった場合は画面をONに保つ
if (!screenOn) {
wakeUpScreen();
}
}
} else {
// アラーム設定状態では常に画面をONに保つ
wakeUpScreen();
}
#else
// 画面自動OFF機能が無効の場合は常に画面をONに保つ
wakeUpScreen();
#endif
}
// ボタン状態を更新(割り込み方式に変更)
void updateButtonStates() {
// 割り込みフラグを処理
processButtonInterrupts();
// ボタンが押されたら画面をONにする(processButtonInterrupts内で検出された場合)
// 実際のボタン処理は各ハンドラで行われるため、ここでは画面ON処理のみ
}
// ボタンA短押し処理
void handleButtonAShortPress() {
wakeUpScreen(); // 画面をONにする
AppState state = getAppState();
if (state == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:選択中のアラームの時を進める
alarms[selectedAlarmIndex].hour = (alarms[selectedAlarmIndex].hour + 1) % 24;
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" hour set to: ");
Serial.println(alarms[selectedAlarmIndex].hour);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
} else if (state == STATE_SOUND_SELECTING) {
// アラーム音選択状態:選択中のアラームの音種を切り替え
alarms[selectedAlarmIndex].soundType = (AlarmSoundType)((alarms[selectedAlarmIndex].soundType + 1) % 3); // 0, 1, 2をループ
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" sound type set to: ");
Serial.println(alarms[selectedAlarmIndex].soundType);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displaySoundSelectingState();
}
}
// ボタンA長押し処理
void handleButtonALongPress() {
wakeUpScreen(); // 画面をONにする
if (getAppState() == STATE_NORMAL) {
// 通常状態からアラーム設定状態へ遷移
setAppState(STATE_ALARM_SETTING);
alarmSettingEnterTime = millis(); // 移行時刻を記録
Serial.println("Entering alarm setting mode");
// 即座に画面を更新
displayAlarmSettingState();
}
// アラーム鳴動中やアラーム設定状態では何もしない
}
// アラーム停止処理(共通関数)
void stopAlarm() {
AppState state = getAppState();
Serial.print("stopAlarm() called, currentState: ");
Serial.println(state);
if (state == STATE_ALARM_RINGING) {
Serial.println("Stopping alarm...");
// 音を即座に停止(最初に実行)
M5.Speaker.mute();
// 状態をリセット
setAppState(STATE_NORMAL); // 通常状態に戻す
ringingAlarmIndex = -1; // 鳴っているアラームのインデックスをリセット
alarmStartTime = 0; // アラーム開始時刻をリセット
alarmVolume = alarmVolumeMin; // 音量を最小値にリセット
lastVolumeUpdateTime = 0; // 音量更新時刻をリセット
lastBeepTime = 0; // ビープ音のタイミングをリセット
lastMelodyNoteTime = 0; // メロディーのタイミングをリセット
lastChimeTime = 0; // チャイム音のタイミングをリセット
// 画面を更新
displayNormalState();
Serial.println("Alarm stopped successfully");
} else {
Serial.println("stopAlarm() called but alarm is not ringing");
}
}
// ボタンB短押し処理
void handleButtonBPress() {
wakeUpScreen(); // 画面をONにする
AppState state = getAppState();
if (state == STATE_NORMAL) {
// 通常状態:アラーム選択を切り替え
selectedAlarmIndex = (selectedAlarmIndex + 1) % 2;
Serial.print("Selected alarm: ");
Serial.println(selectedAlarmIndex + 1);
displayNormalState();
} else if (state == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:分を進める(アラーム1,2の切り替えは無効)
alarms[selectedAlarmIndex].minute = (alarms[selectedAlarmIndex].minute + 1) % 60;
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" minute set to: ");
Serial.println(alarms[selectedAlarmIndex].minute);
// 設定はSETボタン押下時に保存される
// 即座に画面を更新
displayAlarmSettingState();
}
#ifdef ENABLE_ALARM_STOP
else if (state == STATE_ALARM_RINGING) {
// アラーム停止
stopAlarm();
}
#endif
}
// ボタンB長押し処理
void handleButtonBLongPress() {
wakeUpScreen(); // 画面をONにする
AppState state = getAppState();
if (state == STATE_NORMAL) {
// 通常状態からアラーム音選択状態へ遷移
setAppState(STATE_SOUND_SELECTING);
alarmSettingEnterTime = millis(); // 移行時刻を記録(入力無効化用)
Serial.println("Entering sound selecting mode");
// 即座に画面を更新
displaySoundSelectingState();
} else if (state == STATE_ALARM_SETTING) {
// アラーム設定状態:アラーム1,2の切り替えは無効(分の自動進みのみ)
// アラーム選択の切り替えは削除(ユーザー要望により無効化)
Serial.println("Alarm switching disabled during alarm setting");
}
}
// ボタンC短押し処理
void handleButtonCShortPress() {
wakeUpScreen(); // 画面をONにする
AppState state = getAppState();
if (state == STATE_ALARM_SETTING) {
// アラーム設定状態に移行してから1秒経過していない場合は処理をスキップ
if (millis() - alarmSettingEnterTime < alarmSettingInputDisableDuration) {
return;
}
// アラーム設定状態:選択中のアラームの設定を確定して通常状態へ
setAppState(STATE_NORMAL);
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" set to: ");
Serial.print(alarms[selectedAlarmIndex].hour);
Serial.print(":");
Serial.println(alarms[selectedAlarmIndex].minute);
// 設定を保存(SETボタン押下時)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
} else if (state == STATE_SOUND_SELECTING) {
// アラーム音選択状態:選択中のアラームの音種を確定して通常状態へ
setAppState(STATE_NORMAL);
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" sound type set to: ");
Serial.println(alarms[selectedAlarmIndex].soundType);
// 設定を保存(SETボタン押下時)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
}
}
// ボタンC長押し処理
void handleButtonCLongPress() {
wakeUpScreen(); // 画面をONにする
if (getAppState() == STATE_NORMAL) {
// 通常状態:選択中のアラームのON/OFF切り替え
alarms[selectedAlarmIndex].enabled = !alarms[selectedAlarmIndex].enabled;
Serial.print("Alarm ");
Serial.print(selectedAlarmIndex + 1);
Serial.print(" ");
Serial.println(alarms[selectedAlarmIndex].enabled ? "ON" : "OFF");
// 設定を保存(即座に反映が必要なため)
saveAlarmSettings();
// 即座に画面を更新
displayNormalState();
}
}
// アラームチェック
void checkAlarm() {
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// 既存の13時自動ON処理は削除(要件により)
lastHour = hours;
// アラームが既に鳴っている場合は既存の継続処理を実行してreturn
AppState state = getAppState();
if (state == STATE_ALARM_RINGING && alarmStartTime > 0) {
// 既存のアラーム継続処理(音量調整など)は後で処理
// ここではreturnしない(継続処理が必要なため)
} else {
// アラーム1とアラーム2をチェック
// 優先順位: アラーム1 > アラーム2
for (int i = 0; i < 2; i++) {
if (alarms[i].enabled &&
hours == alarms[i].hour &&
minutes == alarms[i].minute &&
seconds == 0) {
// アラームを鳴らす
setAppState(STATE_ALARM_RINGING);
ringingAlarmIndex = i;
alarmStartTime = millis();
alarmVolume = alarmVolumeMin;
lastVolumeUpdateTime = alarmStartTime;
lastBeepTime = 0;
wakeUpScreen();
// 鳴っているアラームの音種を使用(一時変数として使用)
// 注意: alarmSoundTypeは既存コードで使用されているため、一時的に設定
AlarmSoundType currentSoundType = alarms[i].soundType;
Serial.println("Alarm started - BLE reconnection will be attempted if disconnected");
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.setVolume(alarmVolume);
#endif
Serial.print("ALARM ");
Serial.print(i + 1);
Serial.print(" RINGING! Volume: ");
Serial.println(alarmVolume);
// 最初に見つかったアラームを優先(アラーム1が優先)
break;
}
}
}
// アラームが鳴っている場合の処理
state = getAppState();
if (state == STATE_ALARM_RINGING && alarmStartTime > 0) {
unsigned long currentTime = millis();
// オーバーフロー対策: 現在時刻が開始時刻より小さい場合はオーバーフローを考慮
unsigned long elapsedTime;
if (currentTime >= alarmStartTime) {
elapsedTime = currentTime - alarmStartTime;
} else {
// オーバーフローが発生した場合(約49日後に発生)
elapsedTime = (ULONG_MAX - alarmStartTime) + currentTime;
}
// 3分間以上鳴っている場合は自動停止
if (elapsedTime >= alarmMaxDuration) {
Serial.println("Alarm automatically stopped after 3 minutes");
stopAlarm();
alarmStartTime = 0; // リセット
}
// 10秒ごとに音量を上げる(最大10まで)
// 注意: 外部スピーカー使用時は音量変数は更新されるが、実際の音量制御は未実装
// (外部スピーカーはデジタル出力のため、音量制御にはPWMのデューティ比を変更する必要がある)
else if (alarmVolume < alarmVolumeMax) {
// オーバーフロー対策: 現在時刻が最後の更新時刻より小さい場合はオーバーフローを考慮
unsigned long timeSinceLastVolumeUpdate;
if (currentTime >= lastVolumeUpdateTime) {
timeSinceLastVolumeUpdate = currentTime - lastVolumeUpdateTime;
} else {
// オーバーフローが発生した場合
timeSinceLastVolumeUpdate = (ULONG_MAX - lastVolumeUpdateTime) + currentTime;
}
if (timeSinceLastVolumeUpdate >= alarmVolumeInterval) {
alarmVolume++;
lastVolumeUpdateTime = currentTime;
// 音量を設定(内蔵スピーカーの場合)
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.setVolume(alarmVolume);
#endif
Serial.print("Alarm volume increased to: ");
Serial.println(alarmVolume);
}
}
}
}
// アラーム音を再生
void playAlarmSound() {
AppState state = getAppState();
if (state == STATE_ALARM_RINGING && ringingAlarmIndex >= 0) {
// 鳴っているアラームの音種を使用
AlarmSoundType currentSoundType = alarms[ringingAlarmIndex].soundType;
// 選択されたアラーム音の種類に応じて再生
switch (currentSoundType) {
case ALARM_SOUND_BEEP:
playBeepSound();
break;
case ALARM_SOUND_MELODY:
playMelodySound();
break;
case ALARM_SOUND_CHIME:
playChimeSound();
break;
default:
playBeepSound(); // デフォルトはビープ音
break;
}
} else {
// アラームが停止したら音を止める
#ifdef USE_EXTERNAL_SPEAKER
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
externalBeeping = false;
pulseCount = 0;
currentHalfPeriod = 0;
#else
M5.Speaker.mute();
#endif
// 周波数フラグをリセット
useHighFrequency = true;
currentMelodyNote = 0;
currentChimeNote = 0;
lastBeepTime = 0;
lastMelodyNoteTime = 0;
lastChimeTime = 0;
}
}
// ビープ音を再生
void playBeepSound() {
#ifdef USE_EXTERNAL_SPEAKER
static unsigned long externalBeepStartTime = 0;
static bool externalBeeping = false;
static int pulseCount = 0;
static int currentHalfPeriod = 0; // 現在の半周期(マイクロ秒)
const int pulsesPerBeep = 200; // 1回のビープで鳴らすパルス数
// digitalWrite()のオーバーヘッドを考慮した補正値(マイクロ秒)
const int digitalWriteOverhead = 2;
// Grove - スピーカープラスで音を鳴らす(非ブロッキング方式)
unsigned long currentMillis = millis();
if (!externalBeeping) {
// 一定間隔ごとに音を開始
if (currentMillis - lastBeepTime >= beepInterval) {
// 現在使用する周波数を選択(高音・低音を交互に)
int currentFrequency = useHighFrequency ? beepFrequencyHigh : beepFrequencyLow;
// 半周期を計算(digitalWrite()のオーバーヘッドを考慮)
currentHalfPeriod = (1000000L / currentFrequency / 2) - digitalWriteOverhead;
if (currentHalfPeriod < 10) currentHalfPeriod = 10; // 最小値制限
externalBeeping = true;
externalBeepStartTime = currentMillis;
pulseCount = 0;
lastBeepTime = currentMillis;
}
}
if (externalBeeping) {
// beepDuration経過するまで、少しずつパルスを生成
if (currentMillis - externalBeepStartTime < beepDuration) {
// 1回のloop()呼び出しで複数パルス生成
for (int i = 0; i < ExternalSpeaker::PULSES_PER_LOOP && pulseCount < pulsesPerBeep; i++) {
digitalWrite(EXTERNAL_SPEAKER_PIN, HIGH);
delayMicroseconds(currentHalfPeriod);
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
delayMicroseconds(currentHalfPeriod);
pulseCount++;
}
} else {
// beepDuration経過後は音を停止し、次回は別の周波数を使用
digitalWrite(EXTERNAL_SPEAKER_PIN, LOW);
externalBeeping = false;
pulseCount = 0;
useHighFrequency = !useHighFrequency; // 高音⇔低音を交互に切り替え
}
}
#else
// 内蔵スピーカーで音を鳴らす(高音・低音を交互に)
if (millis() - lastBeepTime >= beepInterval) {
int currentFrequency = useHighFrequency ? beepFrequencyHigh : beepFrequencyLow;
M5.Speaker.tone(currentFrequency, beepDuration);
useHighFrequency = !useHighFrequency; // 高音⇔低音を交互に切り替え
lastBeepTime = millis();
}
#endif
}
// メロディーを再生
void playMelodySound() {
unsigned long currentMillis = millis();
if (currentMillis - lastMelodyNoteTime >= melodyNoteInterval) {
// 次の音符を再生
int note = melodyNotes[currentMelodyNote];
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.tone(note, melodyNoteDuration);
#endif
currentMelodyNote = (currentMelodyNote + 1) % melodyNoteCount; // ループ
lastMelodyNoteTime = currentMillis;
}
}
// チャイム音を再生
void playChimeSound() {
unsigned long currentMillis = millis();
if (currentMillis - lastChimeTime >= chimeInterval) {
// 次のチャイム音を再生
int note = chimeNotes[currentChimeNote];
#ifndef USE_EXTERNAL_SPEAKER
M5.Speaker.tone(note, chimeNoteDuration);
#endif
currentChimeNote = (currentChimeNote + 1) % chimeNoteCount; // ループ
lastChimeTime = currentMillis;
}
}
// 通常状態の画面表示(差分更新でちらつき防止)
void displayNormalState() {
// 画面がOFFの場合は表示しない
if (!screenOn) {
Serial.println("displayNormalState: screen is OFF, skipping display");
return;
}
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// デバッグ用:初回表示時にログを出力
static bool firstCall = true;
if (firstCall) {
Serial.println("displayNormalState: First call - displaying clock");
Serial.print("Time: ");
Serial.print(hours);
Serial.print(":");
Serial.print(minutes);
Serial.print(":");
Serial.println(seconds);
firstCall = false;
}
// Unixタイムスタンプから日付情報を取得
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
int year = timeinfo->tm_year + 1900; // tm_yearは1900年からの年数
int month = timeinfo->tm_mon + 1; // tm_monは0-11
int date = timeinfo->tm_mday; // tm_mdayは1-31
int day = timeinfo->tm_wday; // tm_wdayは0=日曜日、1=月曜日、...、6=土曜日
// 現在時刻を表示
char timeString[20];
sprintf(timeString, "%02d:%02d:%02d", hours, minutes, seconds);
// 日付と曜日の文字列を生成(英語表記)
const char* dayNames[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
char dateString[30];
char datePart[20]; // 日付部分のみ
sprintf(dateString, "%04d-%02d-%02d (%s)", year, month, date, dayNames[day]);
sprintf(datePart, "%04d-%02d-%02d (", year, month, date); // 日付と開き括弧
// 状態が変わった場合は画面をリセット
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
lastTimeString[0] = '\0';
lastAlarm1String[0] = '\0';
lastAlarm2String[0] = '\0';
lastAlarm1Enabled = false;
lastAlarm2Enabled = false;
lastBleStatus = -1;
lastAlarmRinging = false;
lastDateString[0] = '\0';
lastDisplayState = currentState;
}
// 初回表示の場合は画面全体をクリア
bool isFirstDisplay = (lastTimeString[0] == '\0');
if (isFirstDisplay) {
M5.Lcd.fillScreen(BLACK);
}
// アラームが鳴っている場合は背景を赤・黒で点滅
static unsigned long lastFlashTime = 0;
static bool flashState = false;
bool currentFlashState = false;
bool isAlarmRinging = (getAppState() == STATE_ALARM_RINGING);
if (isAlarmRinging) {
unsigned long currentTime = millis();
if (currentTime - lastFlashTime >= Display::FLASH_INTERVAL_MS) {
flashState = !flashState;
lastFlashTime = currentTime;
}
currentFlashState = flashState;
if (currentFlashState) {
M5.Lcd.fillScreen(RED); // 赤背景
} else {
M5.Lcd.fillScreen(BLACK); // 黒背景
}
} else {
// アラームが停止した場合は黒背景に戻す
if (isAlarmRinging != lastAlarmRinging && lastAlarmRinging) {
M5.Lcd.fillScreen(BLACK);
}
currentFlashState = false;
}
// アラーム停止時はすべての情報を再描画
bool alarmJustStopped = (isAlarmRinging != lastAlarmRinging && lastAlarmRinging && !isAlarmRinging);
lastAlarmRinging = isAlarmRinging;
// 選択状態が変わった場合も再描画
static int lastSelectedAlarmIndex_display = -1;
bool selectedAlarmChanged = (lastSelectedAlarmIndex_display != selectedAlarmIndex);
if (selectedAlarmChanged) {
lastSelectedAlarmIndex_display = selectedAlarmIndex;
}
// 日付と時刻を表示
displayDateTime(isAlarmRinging, currentFlashState, isFirstDisplay, alarmJustStopped);
// アラーム状態を表示
displayAlarmStatus(isAlarmRinging, currentFlashState, isFirstDisplay, alarmJustStopped, selectedAlarmChanged);
// BLE状態を表示
displayBLEStatus(isAlarmRinging, currentFlashState, isFirstDisplay, alarmJustStopped);
}
// アラーム状態の表示(displayNormalState()から分離)
void displayAlarmStatus(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped, bool selectedAlarmChanged) {
// アラーム1の文字列を生成
char alarm1String[30];
if (selectedAlarmIndex == 0) {
sprintf(alarm1String, "*Alarm1: %02d:%02d %s",
alarms[0].hour, alarms[0].minute,
alarms[0].enabled ? "ON" : "OFF");
} else {
sprintf(alarm1String, " Alarm1: %02d:%02d %s",
alarms[0].hour, alarms[0].minute,
alarms[0].enabled ? "ON" : "OFF");
}
// アラーム1の表示(変更があった場合のみ再描画)
if (strcmp(alarm1String, lastAlarm1String) != 0 || isFirstDisplay || isAlarmRinging || alarmJustStopped || selectedAlarmChanged) {
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::ALARM1_X, Layout::Normal::ALARM1_Y);
M5.Lcd.println(lastAlarm1String);
uint16_t textColor = WHITE;
if (!isAlarmRinging) {
textColor = alarms[0].enabled ? GREEN : RED;
}
M5.Lcd.setTextColor(textColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::ALARM1_X, Layout::Normal::ALARM1_Y);
M5.Lcd.println(alarm1String);
strcpy(lastAlarm1String, alarm1String);
}
// アラーム2の文字列を生成
char alarm2String[30];
if (selectedAlarmIndex == 1) {
sprintf(alarm2String, "*Alarm2: %02d:%02d %s",
alarms[1].hour, alarms[1].minute,
alarms[1].enabled ? "ON" : "OFF");
} else {
sprintf(alarm2String, " Alarm2: %02d:%02d %s",
alarms[1].hour, alarms[1].minute,
alarms[1].enabled ? "ON" : "OFF");
}
// アラーム2の表示(変更があった場合のみ再描画)
if (strcmp(alarm2String, lastAlarm2String) != 0 || isFirstDisplay || isAlarmRinging || alarmJustStopped || selectedAlarmChanged) {
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::ALARM2_X, Layout::Normal::ALARM2_Y);
M5.Lcd.println(lastAlarm2String);
uint16_t textColor = WHITE;
if (!isAlarmRinging) {
textColor = alarms[1].enabled ? GREEN : RED;
}
M5.Lcd.setTextColor(textColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::ALARM2_X, Layout::Normal::ALARM2_Y);
M5.Lcd.println(alarm2String);
strcpy(lastAlarm2String, alarm2String);
}
}
// BLE状態の表示(displayNormalState()から分離)
void displayBLEStatus(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped) {
// BLE接続状態を取得(セマフォで保護して共有変数にアクセス)
int currentBleStatus = -1;
if (bleMutex == NULL) {
currentBleStatus = 5; // Disabled
} else if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bool isBleConnected = bleConnected;
bool isBleScanning = bleScanning;
bool isBleFirstConnected = bleFirstConnected;
xSemaphoreGive(bleMutex);
if (isBleConnected) {
currentBleStatus = 0; // Connected
} else if (isBleFirstConnected) {
currentBleStatus = 1; // Disconnected
} else if (isBleScanning) {
currentBleStatus = 2; // Scanning
} else if (pServerAddress != NULL) {
currentBleStatus = 3; // Connecting
} else {
currentBleStatus = 4; // Not connected
}
} else {
currentBleStatus = lastBleStatus;
}
// BLE状態の表示(変更があった場合のみ再描画)
if (currentBleStatus != lastBleStatus || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(Layout::Normal::BLE_STATUS_X, Layout::Normal::BLE_STATUS_Y);
M5.Lcd.fillRect(Layout::Normal::BLE_STATUS_X, Layout::Normal::BLE_STATUS_Y,
Layout::Normal::BLE_STATUS_WIDTH, Layout::Normal::BLE_STATUS_HEIGHT, bgColor);
// 新しいBLE状態を表示(アラーム鳴動中は白に変更)
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(Layout::Normal::BLE_STATUS_X, Layout::Normal::BLE_STATUS_Y);
if (currentBleStatus == 0) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : GREEN);
M5.Lcd.println("BLE: Connected");
} else if (currentBleStatus == 1) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("BLE: Disconnected");
} else if (currentBleStatus == 2) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : YELLOW);
M5.Lcd.println("BLE: Scanning...");
} else if (currentBleStatus == 3) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : YELLOW);
M5.Lcd.println("BLE: Pairing...");
} else if (currentBleStatus == 5) {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("BLE: Disabled");
} else {
M5.Lcd.setTextColor(isAlarmRinging ? WHITE : RED);
M5.Lcd.println("BLE: Not connected");
}
lastBleStatus = currentBleStatus;
}
}
// アラーム設定状態の画面表示(差分更新でちらつき防止)
void displayAlarmSettingState() {
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// 現在時刻を表示
char timeString[20];
sprintf(timeString, "%02d:%02d:%02d", hours, minutes, seconds);
// 設定中のアラーム時刻
char alarmString[20];
sprintf(alarmString, "%02d:%02d", alarms[selectedAlarmIndex].hour, alarms[selectedAlarmIndex].minute);
// 設定中のアラームラベル
char alarmLabel[20];
sprintf(alarmLabel, "Setting: Alarm %d", selectedAlarmIndex + 1);
// 状態が変わった場合は画面をリセット
// displayNormalState()と同じグローバル変数を使用して状態を追跡
static bool firstDisplay = true;
static char lastSettingTimeString[20] = "";
static char lastSettingAlarmString[20] = "";
static char lastSettingAlarmLabel[20] = "";
static int lastSelectedAlarmIndex = -1;
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
firstDisplay = true;
lastDisplayState = currentState;
lastSettingTimeString[0] = '\0';
lastSettingAlarmString[0] = '\0';
lastSettingAlarmLabel[0] = '\0';
lastSelectedAlarmIndex = -1;
}
// 初回表示または状態変化後の再描画、またはアラーム選択が変わった場合
if (firstDisplay || lastSelectedAlarmIndex != selectedAlarmIndex) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
// 現在時刻(小さめ)
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::CURRENT_TIME_X, Layout::Setting::CURRENT_TIME_Y);
M5.Lcd.print("Current: ");
M5.Lcd.println(timeString);
// 設定中のアラームを表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::ALARM_LABEL_X, Layout::Setting::ALARM_LABEL_Y);
M5.Lcd.println(alarmLabel);
// アラーム時刻を表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(Layout::Setting::ALARM_TIME_X, Layout::Setting::ALARM_TIME_Y);
M5.Lcd.println(alarmString);
// 操作説明
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(Layout::Setting::INSTRUCTION_X, Layout::Setting::INSTRUCTION_Y);
M5.Lcd.println("A: Hour B: Minute C: Set");
// 表示変数を初期化
strcpy(lastSettingTimeString, timeString);
strcpy(lastSettingAlarmString, alarmString);
strcpy(lastSettingAlarmLabel, alarmLabel);
lastSelectedAlarmIndex = selectedAlarmIndex;
firstDisplay = false;
}
// 現在時刻の更新(変更があった場合のみ)
if (strcmp(timeString, lastSettingTimeString) != 0) {
// 前回の時刻を黒で塗りつぶし
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::CURRENT_TIME_X, Layout::Setting::CURRENT_TIME_Y);
M5.Lcd.print("Current: ");
M5.Lcd.println(lastSettingTimeString);
// 新しい時刻を表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::CURRENT_TIME_X, Layout::Setting::CURRENT_TIME_Y);
M5.Lcd.print("Current: ");
M5.Lcd.println(timeString);
strcpy(lastSettingTimeString, timeString);
}
// 設定中のアラームラベルの更新(アラーム選択が変わった場合のみ)
if (strcmp(alarmLabel, lastSettingAlarmLabel) != 0) {
// 前回のラベルを黒で塗りつぶし
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::ALARM_LABEL_X, Layout::Setting::ALARM_LABEL_Y);
M5.Lcd.println(lastSettingAlarmLabel);
// 新しいラベルを表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Setting::ALARM_LABEL_X, Layout::Setting::ALARM_LABEL_Y);
M5.Lcd.println(alarmLabel);
strcpy(lastSettingAlarmLabel, alarmLabel);
}
// アラーム時刻の更新(変更があった場合のみ)
if (strcmp(alarmString, lastSettingAlarmString) != 0) {
// 前回のアラーム時刻を黒で塗りつぶし
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(Layout::Setting::ALARM_TIME_X, Layout::Setting::ALARM_TIME_Y);
M5.Lcd.println(lastSettingAlarmString);
// 新しいアラーム時刻を表示(黄色)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(Layout::Setting::ALARM_TIME_X, Layout::Setting::ALARM_TIME_Y);
M5.Lcd.println(alarmString);
strcpy(lastSettingAlarmString, alarmString);
}
}
// BLEデバイススキャンコールバック
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// 既にデバイスが見つかっている場合は処理しない
if (pServerAddress != NULL) {
return;
}
// toString()はメモリ割り当てが発生するため、メモリ不足でクラッシュする可能性がある
// 代わりにアドレスのみを表示(メモリ安全)
Serial.print("Found device: Name: ");
Serial.print(advertisedDevice.getName().c_str());
Serial.print(", Address: ");
Serial.print(advertisedDevice.getAddress().toString().c_str());
Serial.print(", rssi: ");
Serial.println(advertisedDevice.getRSSI());
// HIDサービスを持つデバイスを検索
if (advertisedDevice.haveServiceUUID()) {
BLEUUID serviceUUID = advertisedDevice.getServiceUUID();
Serial.print("Service UUID: ");
// toString()は安全(UUIDは短い文字列)
Serial.println(serviceUUID.toString().c_str());
// HIDサービスのUUID(0x1812)と一致するか確認
if (serviceUUID.equals(BLEUUID((uint16_t)0x1812))) {
Serial.println("HID service found!");
// スキャンを停止
BLEScan* pScan = advertisedDevice.getScan();
if (pScan != nullptr) {
pScan->stop();
Serial.println("BLE scan stopped");
}
// デバイスアドレスを保存(セマフォで保護)
// 注意: コールバック内ではportMAX_DELAYではなく、タイムアウト付きでセマフォを取得
if (bleMutex != NULL && xSemaphoreTake(bleMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// 既存のアドレスを解放(メモリリーク防止)
if (pServerAddress != NULL) {
delete pServerAddress;
pServerAddress = NULL;
}
// メモリ割り当て(メモリ不足の場合はNULLが返る可能性がある)
pServerAddress = new BLEAddress(advertisedDevice.getAddress());
if (pServerAddress != NULL) {
Serial.print("BLE device found: ");
Serial.println(pServerAddress->toString().c_str());
// スキャン状態をfalseに設定
bleScanning = false;
} else {
Serial.println("ERROR: Failed to allocate memory for BLE address");
}
xSemaphoreGive(bleMutex);
} else {
Serial.println("WARNING: Failed to take BLE mutex in callback");
}
Serial.println("Ready to connect");
}
}
}
};
// BLE通知コールバック(Core 1で実行される可能性がある)
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
Serial.print("BLE notify received: length=");
Serial.print(length);
Serial.print(" data: ");
for (size_t i = 0; i < length; i++) {
Serial.printf("%02X ", pData[i]);
}
Serial.println();
// ボタンが押された場合(参考サイトでは0x02で検出)
// より広範囲の値を受け入れる(0x00-0xFFの範囲で、0以外の値はボタン押下とみなす)
if (length > 0 && pData[0] != 0x00) {
Serial.print("Button press detected! Value: 0x");
Serial.println(pData[0], HEX);
// currentStateを確認(アラーム鳴動中かどうか)
// セマフォで保護して安全にアクセス
AppState currentStateCheck = getAppState();
Serial.print("Current state: ");
Serial.println(currentStateCheck);
// アラーム鳴動中でボタンが押された場合は停止
if (currentStateCheck == STATE_ALARM_RINGING) {
Serial.println("Stopping alarm via BLE button (deferred to main loop)");
if (bleEventQueue != NULL) {
BleEventType event = BLE_EVENT_STOP_ALARM;
if (xQueueSend(bleEventQueue, &event, 0) != pdTRUE) {
Serial.println("WARNING: BLE event queue full - stop alarm request dropped");
}
} else {
Serial.println("WARNING: BLE event queue not available - unable to defer stop alarm");
}
} else if (currentStateCheck == STATE_NORMAL) {
Serial.println("BLE button pressed in normal state");
} else if (currentStateCheck == STATE_ALARM_SETTING) {
Serial.println("BLE button pressed in alarm setting state");
} else {
Serial.print("BLE button pressed in unknown state: ");
Serial.println(currentStateCheck);
}
} else {
Serial.println("No button press detected (data is 0x00 or empty)");
}
}
// BLE初期化
void initBLE() {
Serial.println("Initializing BLE...");
BLEDevice::init("");
// BLEスキャンの設定(スキャン開始はBLEタスク内で行う)
BLEScan *pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true); // アクティブスキャンを有効化
pBLEScan->setInterval(1349); // スキャン間隔(ミリ秒)
pBLEScan->setWindow(449); // スキャンウィンドウ(ミリ秒)
// スキャン開始はBLEタスク内で行う(setup()をブロックしないため)
Serial.println("BLE initialized (scan will start in BLE task)");
}
// BLE処理
void handleBLE() {
static unsigned long lastBLEAttempt = 0;
const unsigned long bleRetryInterval = 20000; // 20秒ごとに再接続試行(接続タイムアウト15秒 + 余裕5秒)
const unsigned long bleRetryIntervalAlarmRinging = 5000; // アラーム鳴動中は5秒ごとに再接続試行
const unsigned long bleConnectionTimeout = 15000; // 接続タイムアウト(15秒)
const unsigned long bleConnectionTimeoutAlarmRinging = 20000; // アラーム鳴動中は20秒に延長
if (bleMutex == NULL) {
// BLE機能が無効化されている場合は何もしない
return;
}
// アラーム鳴動中かどうかを確認
bool isAlarmRinging = (getAppState() == STATE_ALARM_RINGING);
// セマフォで保護して共有変数にアクセス
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
// スキャン中で、まだデバイスが見つかっていない場合は何もしない
if (bleScanning && pServerAddress == NULL) {
xSemaphoreGive(bleMutex);
return;
}
// デバイスが見つかったらスキャンを停止
if (bleScanning && pServerAddress != NULL) {
BLEScan* pBLEScan = BLEDevice::getScan();
if (pBLEScan != nullptr) {
pBLEScan->stop();
Serial.println("Stopping BLE scan");
}
bleScanning = false;
}
xSemaphoreGive(bleMutex);
}
// 接続試行中のタイムアウトチェック(アラーム鳴動中は長いタイムアウトを使用)
if (bleConnecting && bleConnectionStartTime > 0) {
unsigned long elapsed = millis() - bleConnectionStartTime;
unsigned long timeout = isAlarmRinging ? bleConnectionTimeoutAlarmRinging : bleConnectionTimeout;
if (elapsed > timeout) {
Serial.print("BLE connection timeout - cleaning up");
if (isAlarmRinging) {
Serial.print(" (alarm ringing - extended timeout used)");
}
Serial.println();
// タイムアウトした接続をクリーンアップ
if (pClient != NULL) {
if (pClient->isConnected()) {
pClient->disconnect();
}
delete pClient;
pClient = NULL;
}
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnecting = false;
xSemaphoreGive(bleMutex);
}
bleConnectionStartTime = 0;
// 次の接続試行までリトライ間隔分待つために、lastBLEAttemptを更新
lastBLEAttempt = millis();
Serial.println("Next retry scheduled after retry interval");
}
}
// デバイスが見つかっていて、まだ接続していない場合
// 初回接続、またはアラーム鳴動中のみ再接続を試みる(バッテリー節約のため)
bool shouldConnect = false;
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bool isFirstConnection = !bleFirstConnected;
// 接続中でない場合のみ接続を試みる(重複接続防止)
// 初回接続、またはアラーム鳴動中のみ接続を試みる
shouldConnect = (pServerAddress != NULL && !bleConnected && !bleConnecting &&
(isFirstConnection || isAlarmRinging));
xSemaphoreGive(bleMutex);
}
if (shouldConnect) {
// リトライ間隔をチェック(アラーム鳴動中は短い間隔を使用)
unsigned long retryInterval = isAlarmRinging ? bleRetryIntervalAlarmRinging : bleRetryInterval;
if (millis() - lastBLEAttempt < retryInterval) {
return;
}
lastBLEAttempt = millis();
Serial.print("Attempting to connect to BLE device");
if (isAlarmRinging) {
Serial.print(" (alarm ringing - using shorter retry interval)");
}
Serial.println("...");
// 接続中フラグを設定(重複接続防止)
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnecting = true;
xSemaphoreGive(bleMutex);
}
bleConnectionStartTime = millis();
// 既存のクライアントが残っている場合は削除
if (pClient != NULL) {
if (pClient->isConnected()) {
pClient->disconnect();
}
delete pClient;
pClient = NULL;
}
// 新しいクライアントを作成
pClient = BLEDevice::createClient();
if (pClient == NULL) {
Serial.println("ERROR: Failed to create BLE client");
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnecting = false;
xSemaphoreGive(bleMutex);
}
bleConnectionStartTime = 0;
return;
}
Serial.println("BLE client created");
// 接続を試行(タイムアウト付き)
// 注意: pClient->connect()は内部でタイムアウトを処理するが、
// アラーム鳴動中はCPUリソースが不足している可能性があるため、
// より長いタイムアウトを期待して接続を試行
Serial.print("Connecting to BLE device");
if (isAlarmRinging) {
Serial.print(" (alarm ringing - may take longer)");
}
Serial.println("...");
bool connected = pClient->connect(*pServerAddress);
// 接続中フラグをリセット
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnecting = false;
xSemaphoreGive(bleMutex);
}
bleConnectionStartTime = 0;
if (connected) {
Serial.println("BLE connected successfully!");
// セマフォで保護して共有変数を更新
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = true;
bleFirstConnected = true; // 初回接続完了
xSemaphoreGive(bleMutex);
}
// HIDサービスを取得
BLERemoteService *pRemoteService = pClient->getService(BLEUUID((uint16_t)0x1812));
if (pRemoteService == nullptr) {
Serial.println("Failed to find HID service");
// クリーンアップ処理を実行(リソースリーク防止)
cleanupBLEResources();
return;
}
Serial.println("HID service found");
// すべての特性をループして通知を購読
std::map<uint16_t, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristicsByHandle();
if (mapCharacteristics == nullptr) {
Serial.println("Failed to get characteristics");
// クリーンアップ処理を実行(リソースリーク防止)
cleanupBLEResources();
return;
}
Serial.print("Found ");
Serial.print(mapCharacteristics->size());
Serial.println(" characteristics");
bool notifyRegistered = false;
for (std::map<uint16_t, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
BLERemoteCharacteristic* pCharacteristic = i->second;
Serial.print("Characteristic UUID: ");
Serial.println(pCharacteristic->getUUID().toString().c_str());
if (pCharacteristic->canNotify()) {
Serial.println("Registering for notify...");
// registerForNotifyは戻り値がvoidなので、呼び出すだけ
pCharacteristic->registerForNotify(notifyCallback);
Serial.println("Notify registered successfully!");
notifyRegistered = true;
}
}
if (!notifyRegistered) {
Serial.println("Warning: No notify characteristics registered");
}
} else {
Serial.println("BLE connection failed, will retry after timeout");
// 接続失敗時はクライアントを削除
if (pClient != NULL) {
delete pClient;
pClient = NULL;
}
}
}
// 接続が切れた場合の処理(アラーム鳴動中のみ再接続準備)
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bool isConnected = bleConnected;
xSemaphoreGive(bleMutex);
if (isConnected && pClient != NULL) {
if (!pClient->isConnected()) {
Serial.print("BLE disconnected");
if (isAlarmRinging) {
Serial.println(" - will attempt to reconnect (alarm ringing)");
} else {
Serial.println(" - no reconnection (alarm not ringing)");
}
// 接続状態のみリセット(pServerAddressは保持)
if (xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
bleConnected = false;
xSemaphoreGive(bleMutex);
}
// pClientは削除(アラーム鳴動中なら次のループで再接続を試みる)
if (pClient != NULL) {
delete pClient;
pClient = NULL;
Serial.println("BLE client deleted");
}
// pServerAddressは保持(再接続に使用)
}
}
}
}
// 接続待ち画面の表示
void displayWaitingForBLE() {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(30, 80);
M5.Lcd.println("Waiting for");
M5.Lcd.setCursor(30, 120);
M5.Lcd.println("BLE Button");
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(30, 180);
bool isBleScanning = false;
bool bleStatusAvailable = false;
if (bleMutex != NULL && xSemaphoreTake(bleMutex, portMAX_DELAY) == pdTRUE) {
isBleScanning = bleScanning;
bleStatusAvailable = true;
xSemaphoreGive(bleMutex);
}
if (bleMutex == NULL) {
M5.Lcd.setTextColor(RED);
M5.Lcd.println("BLE Disabled");
} else if (bleStatusAvailable && isBleScanning) {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("Scanning...");
} else if (pServerAddress != NULL) {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("Connecting...");
} else {
M5.Lcd.setTextColor(RED);
M5.Lcd.println("Not found");
}
}
// BLE処理タスク(Core 1で実行)
void bleTask(void *parameter) {
Serial.print("BLE task started on core: ");
Serial.println(xPortGetCoreID());
// BLEスキャンを開始(setup()をブロックしないため、ここで開始)
// 少し待機してから開始(初期化が完了するまで待つ)
vTaskDelay(pdMS_TO_TICKS(100));
BLEScan *pBLEScan = BLEDevice::getScan();
if (pBLEScan != nullptr) {
pBLEScan->start(0, false); // 無期限スキャン
if (xSemaphoreTake(bleMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
bleScanning = true;
xSemaphoreGive(bleMutex);
Serial.println("BLE scan started (scanning indefinitely)");
} else {
Serial.println("ERROR: Failed to take BLE mutex when starting scan");
}
} else {
Serial.println("ERROR: Failed to get BLE scan object");
}
while (true) {
// BLE処理を実行
handleBLE();
// タスクを少し待機(CPU使用率を下げる)
// アラーム鳴動中はより頻繁に実行(BLE再接続のため)
AppState currentState = getAppState();
if (currentState == STATE_ALARM_RINGING) {
vTaskDelay(pdMS_TO_TICKS(5)); // アラーム鳴動中は5msごとに実行
} else {
vTaskDelay(pdMS_TO_TICKS(10)); // 通常時は10msごとに実行
}
}
}
void loop() {
// loop()はCore 0で実行される
// BLE処理は別タスク(Core 1)で実行されるため、ここでは呼び出さない
// BLE接続完了を待たずに通常処理を開始
// BLE処理は別コア(Core 1)で継続実行される
// ボタン状態を更新(割り込み方式)
updateButtonStates();
// BLEイベントの処理(メインループでUI更新とサウンド制御を行う)
if (bleEventQueue != NULL) {
BleEventType event;
while (xQueueReceive(bleEventQueue, &event, 0) == pdTRUE) {
switch (event) {
case BLE_EVENT_STOP_ALARM:
Serial.println("Processing BLE stop alarm event on main loop");
stopAlarm();
break;
default:
Serial.print("Unknown BLE event received: ");
Serial.println(event);
break;
}
}
}
// 画面OFF/ONの状態を更新
updateScreenState();
// 1秒ごとに時刻を更新
if (millis() - lastUpdate >= updateInterval) {
lastUpdate = millis();
// 重い処理の前にボタン状態をチェック(応答性向上)
processButtonInterrupts();
// 現在の日付を取得
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
int currentDate = timeinfo->tm_mday; // 現在の日付(1-31)
// 1日1回NTPに再接続して時刻を補正
if (currentDate != lastNTPReconnectDate) {
Serial.println("Reconnecting to NTP server for daily time correction...");
// NTPクライアントを再初期化
timeClient.end();
delay(100);
timeClient.begin();
// NTPから時刻を取得(最大リトライ)
bool ntpUpdated = false;
for (int retry = 0; retry < Connection::NTP_MAX_RETRIES; retry++) {
if (timeClient.update()) {
ntpUpdated = true;
break;
}
delay(Connection::NTP_RETRY_DELAY_MS);
}
if (ntpUpdated) {
lastNTPReconnectDate = currentDate;
Serial.println("NTP reconnected and time corrected");
Serial.print("Current time: ");
Serial.print(timeClient.getHours());
Serial.print(":");
Serial.print(timeClient.getMinutes());
Serial.print(":");
Serial.println(timeClient.getSeconds());
} else {
Serial.println("NTP reconnection failed, will retry tomorrow");
}
}
// NTPから時刻を更新(定期的)
static unsigned long lastNTPUpdate = 0;
if (millis() - lastNTPUpdate >= Connection::NTP_UPDATE_INTERVAL_MS) {
timeClient.update();
lastNTPUpdate = millis();
Serial.println("NTP time updated");
}
// アラームチェック
checkAlarm();
// 状態に応じて画面を表示(画面がONの場合のみ)
if (screenOn) {
AppState state = getAppState();
if (state == STATE_NORMAL || state == STATE_ALARM_RINGING) {
displayNormalState();
} else if (state == STATE_ALARM_SETTING) {
displayAlarmSettingState();
} else if (state == STATE_SOUND_SELECTING) {
displaySoundSelectingState();
}
}
}
// アラーム音を再生(ループ内で常にチェック)
playAlarmSound();
// ボタン処理を再度実行(応答性向上のため)
// vTaskDelay()の前に実行することで、ボタン長押し判定の遅延を最小化
processButtonInterrupts();
// FreeRTOSのvTaskDelay()を使用してタスクスケジューラーに制御を譲る
// これによりBLEタスク(Core 1)が確実に実行される
vTaskDelay(pdMS_TO_TICKS(10)); // 高負荷防止
}
// 既存設定の移行処理(互換性のため)
void migrateOldAlarmSettings() {
preferences.begin(PREF_NAMESPACE, true); // 読み取り専用モードで開く
// 既存の設定が存在するか確認
if (preferences.isKey(PREF_KEY_HOUR)) {
// 既存設定をアラーム1に移行
alarms[0].hour = preferences.getUChar(PREF_KEY_HOUR, 6);
alarms[0].minute = preferences.getUChar(PREF_KEY_MINUTE, 15);
alarms[0].enabled = preferences.getBool(PREF_KEY_ENABLED, true);
alarms[0].soundType = (AlarmSoundType)preferences.getUChar(PREF_KEY_SOUND_TYPE, ALARM_SOUND_BEEP);
Serial.println("Migrated old alarm settings to Alarm 1:");
Serial.print(" Hour: ");
Serial.println(alarms[0].hour);
Serial.print(" Minute: ");
Serial.println(alarms[0].minute);
Serial.print(" Enabled: ");
Serial.println(alarms[0].enabled ? "ON" : "OFF");
Serial.print(" Sound Type: ");
Serial.println(alarms[0].soundType);
}
preferences.end();
}
// アラーム設定を読み込む(EEPROMから)
void loadAlarmSettings() {
// 既存設定の移行処理
migrateOldAlarmSettings();
preferences.begin(PREF_NAMESPACE, true); // 読み取り専用モードで開く
// アラーム1の設定を読み込み
alarms[0].hour = preferences.getUChar(PREF_KEY_ALARM1_HOUR, 6);
alarms[0].minute = preferences.getUChar(PREF_KEY_ALARM1_MINUTE, 15);
alarms[0].enabled = preferences.getBool(PREF_KEY_ALARM1_ENABLED, true);
alarms[0].soundType = (AlarmSoundType)preferences.getUChar(PREF_KEY_ALARM1_SOUND_TYPE, ALARM_SOUND_BEEP);
// アラーム2の設定を読み込み
alarms[1].hour = preferences.getUChar(PREF_KEY_ALARM2_HOUR, 7);
alarms[1].minute = preferences.getUChar(PREF_KEY_ALARM2_MINUTE, 0);
alarms[1].enabled = preferences.getBool(PREF_KEY_ALARM2_ENABLED, false);
alarms[1].soundType = (AlarmSoundType)preferences.getUChar(PREF_KEY_ALARM2_SOUND_TYPE, ALARM_SOUND_BEEP);
preferences.end();
Serial.println("Alarm settings loaded from EEPROM:");
Serial.println("Alarm 1:");
Serial.print(" Hour: ");
Serial.println(alarms[0].hour);
Serial.print(" Minute: ");
Serial.println(alarms[0].minute);
Serial.print(" Enabled: ");
Serial.println(alarms[0].enabled ? "ON" : "OFF");
Serial.print(" Sound Type: ");
Serial.println(alarms[0].soundType);
Serial.println("Alarm 2:");
Serial.print(" Hour: ");
Serial.println(alarms[1].hour);
Serial.print(" Minute: ");
Serial.println(alarms[1].minute);
Serial.print(" Enabled: ");
Serial.println(alarms[1].enabled ? "ON" : "OFF");
Serial.print(" Sound Type: ");
Serial.println(alarms[1].soundType);
}
// アラーム設定を保存する(EEPROMへ)
void saveAlarmSettings() {
preferences.begin(PREF_NAMESPACE, false); // 読み書きモードで開く
// アラーム1の設定を保存
preferences.putUChar(PREF_KEY_ALARM1_HOUR, alarms[0].hour);
preferences.putUChar(PREF_KEY_ALARM1_MINUTE, alarms[0].minute);
preferences.putBool(PREF_KEY_ALARM1_ENABLED, alarms[0].enabled);
preferences.putUChar(PREF_KEY_ALARM1_SOUND_TYPE, (uint8_t)alarms[0].soundType);
// アラーム2の設定を保存
preferences.putUChar(PREF_KEY_ALARM2_HOUR, alarms[1].hour);
preferences.putUChar(PREF_KEY_ALARM2_MINUTE, alarms[1].minute);
preferences.putBool(PREF_KEY_ALARM2_ENABLED, alarms[1].enabled);
preferences.putUChar(PREF_KEY_ALARM2_SOUND_TYPE, (uint8_t)alarms[1].soundType);
preferences.end();
Serial.println("Alarm settings saved to EEPROM");
Serial.print("Alarm 1: ");
Serial.print(alarms[0].hour);
Serial.print(":");
Serial.print(alarms[0].minute);
Serial.print(" ");
Serial.println(alarms[0].enabled ? "ON" : "OFF");
Serial.print("Alarm 2: ");
Serial.print(alarms[1].hour);
Serial.print(":");
Serial.print(alarms[1].minute);
Serial.print(" ");
Serial.println(alarms[1].enabled ? "ON" : "OFF");
}
// 再起動原因を文字列に変換
const char* getResetReasonString(esp_reset_reason_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN: return "UNKNOWN";
case ESP_RST_POWERON: return "POWERON";
case ESP_RST_EXT: return "EXTERNAL_RESET";
case ESP_RST_SW: return "SOFTWARE_RESET";
case ESP_RST_PANIC: return "PANIC/EXCEPTION";
case ESP_RST_INT_WDT: return "INTERNAL_WATCHDOG";
case ESP_RST_TASK_WDT: return "TASK_WATCHDOG";
case ESP_RST_WDT: return "WATCHDOG";
case ESP_RST_DEEPSLEEP: return "DEEPSLEEP";
case ESP_RST_BROWNOUT: return "BROWNOUT";
case ESP_RST_SDIO: return "SDIO";
default: return "UNKNOWN";
}
}
// 再起動原因を記録・表示
void recordAndDisplayResetReason() {
// 再起動原因を取得
esp_reset_reason_t resetReason = esp_reset_reason();
// シリアルに出力
Serial.println("========================================");
Serial.println("Reset Information:");
Serial.print(" Reset Reason: ");
Serial.println(getResetReasonString(resetReason));
Serial.print(" Chip Revision: ");
Serial.println(ESP.getChipRevision());
Serial.print(" CPU Frequency: ");
Serial.print(ESP.getCpuFreqMHz());
Serial.println(" MHz");
Serial.print(" Free Heap: ");
Serial.print(ESP.getFreeHeap());
Serial.println(" bytes");
Serial.println("========================================");
// 通常の電源ON(POWERON)の場合は記録を除外(ただし過去の情報は表示)
if (resetReason == ESP_RST_POWERON) {
Serial.println("Normal power-on detected - reset information not recorded.");
Serial.println();
// EEPROMから過去のリセット情報を読み込んで表示
displayResetHistory();
return;
}
// EEPROMに保存(POWERON以外の場合のみ)
preferences.begin(PREF_NAMESPACE, false);
// 再起動回数を取得してインクリメント
uint32_t resetCount = preferences.getUInt(PREF_KEY_RESET_COUNT, 0);
resetCount++;
preferences.putUInt(PREF_KEY_RESET_COUNT, resetCount);
// 最後の再起動理由を保存
preferences.putUChar(PREF_KEY_LAST_RESET_REASON, (uint8_t)resetReason);
// 最後の再起動時刻を保存(Unix時間、現在時刻が取得できない場合は0)
time_t now = time(nullptr);
if (now > 0) {
preferences.putULong64(PREF_KEY_LAST_RESET_TIME, (uint64_t)now);
}
preferences.end();
// 保存した情報を表示
Serial.print("Reset count: ");
Serial.println(resetCount);
Serial.print("Last reset reason saved: ");
Serial.println(getResetReasonString(resetReason));
Serial.println();
// 異常な再起動の場合に警告を表示
if (resetReason == ESP_RST_PANIC ||
resetReason == ESP_RST_INT_WDT ||
resetReason == ESP_RST_TASK_WDT ||
resetReason == ESP_RST_BROWNOUT) {
Serial.println("!!! WARNING: Abnormal reset detected !!!");
Serial.print("Reason: ");
Serial.println(getResetReasonString(resetReason));
Serial.println("Please check the system for issues.");
Serial.println();
// PANICの場合、より詳細な情報を表示
if (resetReason == ESP_RST_PANIC) {
Serial.println("PANIC/EXCEPTION Details:");
Serial.println("Possible causes:");
Serial.println(" 1. Stack overflow (check task stack sizes)");
Serial.println(" 2. Memory allocation failure (check free heap)");
Serial.println(" 3. Null pointer access");
Serial.println(" 4. Array bounds violation");
Serial.println(" 5. BLE callback issues");
Serial.println();
printMemoryInfo();
printTaskStackInfo();
}
}
}
// 保存された再起動情報を読み込んで表示
void displayResetHistory() {
preferences.begin(PREF_NAMESPACE, true); // 読み取り専用
uint32_t resetCount = preferences.getUInt(PREF_KEY_RESET_COUNT, 0);
uint8_t lastResetReason = preferences.getUChar(PREF_KEY_LAST_RESET_REASON, ESP_RST_UNKNOWN);
uint64_t lastResetTime = preferences.getULong64(PREF_KEY_LAST_RESET_TIME, 0);
preferences.end();
Serial.println("========================================");
Serial.println("Reset History (from EEPROM):");
Serial.print(" Total reset count: ");
Serial.println(resetCount);
Serial.print(" Last reset reason: ");
Serial.println(getResetReasonString((esp_reset_reason_t)lastResetReason));
if (lastResetTime > 0) {
Serial.print(" Last reset time: ");
struct tm timeinfo;
localtime_r((time_t*)&lastResetTime, &timeinfo);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
Serial.println(timeStr);
}
Serial.println("========================================");
}
// スタック使用量を監視する関数
void printTaskStackInfo() {
TaskHandle_t handle = xTaskGetHandle("BLETask");
if (handle != NULL) {
UBaseType_t stackHighWaterMark = uxTaskGetStackHighWaterMark(handle);
Serial.print("BLE Task Stack High Water Mark: ");
Serial.print(stackHighWaterMark);
Serial.println(" bytes (remaining)");
if (stackHighWaterMark < 1024) {
Serial.println("!!! WARNING: BLE Task stack is running low !!!");
}
} else {
Serial.println("BLE Task handle not found");
}
}
// メモリ情報を表示する関数
void printMemoryInfo() {
Serial.println("========================================");
Serial.println("Memory Information:");
Serial.print(" Free Heap: ");
Serial.print(ESP.getFreeHeap());
Serial.println(" bytes");
Serial.print(" Largest Free Block: ");
Serial.print(ESP.getMaxAllocHeap());
Serial.println(" bytes");
Serial.print(" Min Free Heap (ever): ");
Serial.print(ESP.getMinFreeHeap());
Serial.println(" bytes");
Serial.println("========================================");
}
// アラーム音選択状態の画面表示
void displaySoundSelectingState() {
// 状態が変わった場合は画面をリセット
static bool firstDisplay = true;
static int lastSoundType = -1;
static int lastSelectedAlarmIndex = -1;
if (currentState != lastDisplayState) {
// 状態が変わった場合は全画面をクリア
M5.Lcd.fillScreen(BLACK);
// 表示変数をリセット
firstDisplay = true;
lastDisplayState = currentState;
lastSoundType = -1;
lastSelectedAlarmIndex = -1;
}
// 初回表示またはアラーム音の種類が変わった場合、またはアラーム選択が変わった場合のみ再描画
if (firstDisplay || (int)alarms[selectedAlarmIndex].soundType != lastSoundType || lastSelectedAlarmIndex != selectedAlarmIndex) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
// タイトル
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Sound::TITLE_X, Layout::Sound::TITLE_Y);
M5.Lcd.println("Alarm Sound");
// 選択中のアラームを表示
char alarmLabel[20];
sprintf(alarmLabel, "For: Alarm %d", selectedAlarmIndex + 1);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Sound::ALARM_LABEL_X, Layout::Sound::ALARM_LABEL_Y);
M5.Lcd.println(alarmLabel);
// 選択中のアラーム音を表示(大きく)
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(4);
M5.Lcd.setCursor(Layout::Sound::SOUND_TYPE_X, Layout::Sound::SOUND_TYPE_Y);
const char* soundNames[] = {"BEEP", "MELODY", "CHIME"};
M5.Lcd.println(soundNames[alarms[selectedAlarmIndex].soundType]);
// 操作説明
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(Layout::Sound::INSTRUCTION_X, Layout::Sound::INSTRUCTION_Y);
M5.Lcd.println("A: Change C: Set");
// 表示変数を更新
lastSoundType = (int)alarms[selectedAlarmIndex].soundType;
lastSelectedAlarmIndex = selectedAlarmIndex;
firstDisplay = false;
}
}
// 状態を安全に取得(セマフォで保護)
AppState getAppState() {
if (stateMutex == NULL) {
// セマフォが利用できない場合は直接アクセス(初期化前)
return currentState;
}
AppState state;
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
state = currentState;
xSemaphoreGive(stateMutex);
} else {
// タイムアウト時は現在の値を返す(デッドロック回避)
Serial.println("WARNING: Failed to take state mutex in getAppState");
state = currentState;
}
return state;
}
// 状態を安全に設定(セマフォで保護)
void setAppState(AppState newState) {
if (stateMutex == NULL) {
// セマフォが利用できない場合は直接アクセス(初期化前)
currentState = newState;
return;
}
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
currentState = newState;
xSemaphoreGive(stateMutex);
} else {
// タイムアウト時は警告を出して設定(デッドロック回避)
Serial.println("WARNING: Failed to take state mutex in setAppState");
currentState = newState;
}
}
// BLEリソースのクリーンアップ(エラー時や再初期化時に使用)
void cleanupBLEResources() {
Serial.println("Cleaning up BLE resources...");
// セマフォで保護してBLE状態をリセット
if (bleMutex != NULL && xSemaphoreTake(bleMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
bleConnected = false;
bleScanning = false;
bleConnecting = false; // 接続中フラグもリセット
xSemaphoreGive(bleMutex);
}
bleConnectionStartTime = 0; // 接続開始時刻もリセット
// BLEクライアントの切断と削除
if (pClient != NULL) {
if (pClient->isConnected()) {
Serial.println("Disconnecting BLE client...");
pClient->disconnect();
}
delete pClient;
pClient = NULL;
Serial.println("BLE client deleted");
}
// BLEサーバーアドレスの削除
if (pServerAddress != NULL) {
delete pServerAddress;
pServerAddress = NULL;
Serial.println("BLE server address deleted");
}
Serial.println("BLE resources cleaned up");
}
// 日付と時刻の表示(displayNormalState()から分離)
void displayDateTime(bool isAlarmRinging, bool currentFlashState, bool isFirstDisplay, bool alarmJustStopped) {
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
// 現在時刻を表示
char timeString[20];
sprintf(timeString, "%02d:%02d:%02d", hours, minutes, seconds);
// Unixタイムスタンプから日付情報を取得
time_t epochTime = timeClient.getEpochTime();
struct tm *timeinfo = localtime(&epochTime);
int year = timeinfo->tm_year + 1900;
int month = timeinfo->tm_mon + 1;
int date = timeinfo->tm_mday;
int day = timeinfo->tm_wday;
// 日付と曜日の文字列を生成(英語表記)
const char* dayNames[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
char dateString[30];
char datePart[20];
sprintf(dateString, "%04d-%02d-%02d (%s)", year, month, date, dayNames[day]);
sprintf(datePart, "%04d-%02d-%02d (", year, month, date);
// 日付と曜日の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (strcmp(dateString, lastDateString) != 0 || isFirstDisplay || isAlarmRinging || alarmJustStopped) {
// 前回の日付を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::DATE_X, Layout::Normal::DATE_Y);
M5.Lcd.println(lastDateString);
// 新しい日付を表示
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(Layout::Normal::DATE_X, Layout::Normal::DATE_Y);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.print(datePart);
// 曜日部分を色付けして表示
uint16_t dayColor = WHITE;
if (!isAlarmRinging) {
if (day == 0) {
dayColor = RED; // 日曜日は赤
} else if (day == 6) {
dayColor = BLUE; // 土曜日は青
}
}
M5.Lcd.setTextColor(dayColor);
M5.Lcd.print(dayNames[day]);
// 閉じ括弧を白で表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.println(")");
strcpy(lastDateString, dateString);
}
// 現在時刻の更新(変更があった場合のみ、またはアラーム鳴動中/停止時は常に再描画)
if (strcmp(timeString, lastTimeString) != 0 || isAlarmRinging || alarmJustStopped) {
// 前回の時刻を背景色で塗りつぶし
uint16_t bgColor = (isAlarmRinging && currentFlashState) ? RED : BLACK;
M5.Lcd.setTextColor(bgColor);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(Layout::Normal::TIME_X, Layout::Normal::TIME_Y);
M5.Lcd.println(lastTimeString);
// 新しい時刻を表示
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(6);
M5.Lcd.setCursor(Layout::Normal::TIME_X, Layout::Normal::TIME_Y);
M5.Lcd.println(timeString);
strcpy(lastTimeString, timeString);
}
}仕様駆動型開発ツール「Spec Driven Codex」の導入
導入の経緯
開発が進むにつれ、以下の課題が浮上してきました:
- 仕様の散在: 仕様がコードやコメントに散在し、全体像を把握しにくい
- 設計の記録不足: 設計判断の理由や背景が記録されていない
- タスク管理の不備: 実装すべき機能と完了した機能の管理が不十分
- 振り返りの不足: 実装内容や成果の記録が不足している
これらの課題を解決するため、仕様駆動型開発ツール「Spec Driven Codex」を導入しました。
Spec Driven Codexとは
Spec Driven Codexは、仕様駆動開発(Spec-Driven Development、SDD)を支援するツールで、プロジェクトの要件定義から設計、タスク分解、実装、検証までのプロセスを体系的に進めることができます。
主な機能:
- 要件定義:
/sdd-requirementsコマンドで要件を明文化 - 設計:
/sdd-designコマンドで設計を固める - タスク分解:
/sdd-tasksコマンドでタスクを分解 - 実装:
/sdd-implementコマンドで実装を進める - 振り返り:
/sdd-archiveコマンドで成果を記録
導入プロセス
自動的にコマンドが実行され、.sdd/ディレクトリが作成され、以下のファイルが生成されました:
description.md: プロジェクト概要steering/goals.md: プロジェクトのゴールsteering/background.md: プロジェクトの背景README.md: Spec Driven Codexの使用方法
Spec Driven Codex導入後の開発プロセス
Spec Driven Codex導入後、以下のような開発プロセスに変更しました:
1.プロジェクトの背景とゴールを整理 (/sdd-steering)
description.mdの内容をもとに、以下のファイルが生成されました
steering/goals.md: プロジェクトのゴールsteering/background.md: プロジェクトの背景
2. 要件定義(/sdd-requirements)
新機能を追加する際、まず要件を明文化しました。Cursorのチャットで/sdd-requirementsコマンドを実行すると、要件定義書が生成されます。
例:アラーム音のカスタマイズ機能を追加する場合
- 要件:アラーム音を3種類から選択できる
- 受け入れ基準:ボタン操作でアラーム音を選択し、設定を保存できる
3. 設計(/sdd-design)
要件が固まったら、設計を進めました。/sdd-designコマンドを実行すると、実装方針や技術的な設計が記録されます。
例:アラーム音のカスタマイズ機能の設計
- アラーム音の種類をenumで定義
- アラーム音選択UIの実装方針
- 音声再生処理の拡張方法
4. タスク分解(/sdd-tasks)
設計が固まったら、タスクを分解しました。/sdd-tasksコマンドを実行すると、実装すべきタスクが整理されます。
例:アラーム音のカスタマイズ機能のタスク
- アラーム音の種類を定義
- アラーム音選択UIの実装
- 音声再生処理の拡張
- 設定の保存機能の追加
5. 実装(/sdd-implement)
タスクが分解されたら、実装を進めました。/sdd-implementコマンドを実行すると、実装とテストが進められます。
6. 振り返り(/sdd-archive)
実装が完了したら、振り返りを記録しました。/sdd-archiveコマンドを実行すると、実装内容や成果が記録されます。
Spec Driven Codex導入の効果
Spec Driven Codex導入により、以下の効果が得られました:
- 仕様の一元管理: 仕様が
.sdd/ディレクトリに集約され、全体像を把握しやすくなった - 設計の記録: 設計判断の理由や背景が記録され、後から参照できるようになった
- タスク管理の改善: 実装すべき機能と完了した機能が明確になった
- 振り返りの記録: 実装内容や成果が記録され、今後の参考にできるようになった
- 開発プロセスの体系化: 要件定義→設計→タスク分解→実装→振り返りの流れが明確になった
実際の使用例
実際の開発では、以下のような流れでSpec Driven Codexを使用しました:
タスク1: アラーム設定の永続化
/sdd-requirements: アラーム設定をEEPROMに保存する要件を定義/sdd-design: EEPROM保存の実装方針を設計/sdd-tasks: タスクを分解(読み込み、保存、エラーハンドリング)/sdd-implement: 実装とテスト/sdd-archive: 振り返りと成果の記録
タスク2: アラーム音のカスタマイズ
/sdd-requirements: アラーム音を選択できる要件を定義/sdd-design: アラーム音選択UIの設計/sdd-tasks: タスクを分解(音の種類定義、UI実装、音声再生拡張)/sdd-implement: 実装とテスト/sdd-archive: 振り返りと成果の記録
タスク3: 保存タイミングの最適化
/sdd-requirements: EEPROM書き込み回数を削減する要件を定義/sdd-design: 保存タイミングの最適化方針を設計/sdd-tasks: タスクを分解(保存処理の見直し、テスト)/sdd-implement: 実装とテスト/sdd-archive: 振り返りと成果の記録(約70-80%の削減を達成)
Notionフォーマットの仕様書作成
Spec Driven Codexが自動生成するドキュメントでも十分管理しやすくなるのですが、一般的な仕様書のフォーマットに近づけようと思い、NotionにインポートできるMarkdownフォーマットで仕様書を作成してもらいました。
開発で得られた知見
Cursorの活用
- 開発環境の自動構築: プロジェクト開始時に、Cursorに依頼するだけで開発環境が構築できた
- コード生成の効率化: 機能の実装依頼をすると、必要なコードが生成され、開発速度が向上した
- エラー修正の迅速化: エラーを報告すると、修正案が提示され、デバッグ時間が短縮された
- リファクタリングの支援: コードの改善提案を依頼すると、より良い実装が提案された
Spec Driven Codexの活用
- 仕様の一元管理: 仕様が一元管理され、プロジェクトの全体像を把握しやすくなった
- 設計の記録: 設計判断の理由や背景が記録され、後から参照できるようになった
- 開発プロセスの体系化: 要件定義→設計→タスク分解→実装→振り返りの流れが明確になった
- 品質の向上: 要件定義と設計を事前に行うことで、実装時の迷いが減り、品質が向上した
組み合わせの効果
CursorとSpec Driven Codexを組み合わせることで、以下の効果が得られました:
- 開発速度の向上: Cursorによるコード生成とSpec Driven Codexによる開発プロセスの体系化により、開発速度が向上した
- 品質の向上: Spec Driven Codexによる仕様管理とCursorによるコード生成により、品質が向上した
- 保守性の向上: Spec Driven Codexによる仕様の一元管理とCursorによるコード改善により、保守性が向上した
まとめ
本記事では、CursorとPlatformIO、そしてSpec Driven Codexを組み合わせたM5Stackアラーム時計の開発プロセスを紹介しました。
Cursorの活用により、開発環境の構築から実装まで、AIアシスタントに依頼しながら効率的に開発を進めることができました。
Spec Driven Codexの導入により、仕様の一元管理、設計の記録、開発プロセスの体系化が実現し、開発の品質と効率が向上しました。
組み合わせの効果により、開発速度、品質、保守性が向上し、より良い開発体験を得ることができました。
AI駆動の開発ツールと仕様駆動型開発ツールを組み合わせることで、個人開発でも体系的で効率的な開発プロセスを実現できることを実証できました。


