編集履歴一覧に戻る
kasysのアイコン画像

kasys が 2025年01月31日22時52分45秒 に編集

コメント無し

本文の変更

# はじめに Spresense はSonyさんが発売しているマイコンボードで、昨年JAXAのSLIMに搭載されたロボットで使用されたことなどで話題になりましたが、自分は今まで触れたことがなかったので今回のSPRESENSE活用コンテストで初めてSpresenseを使用した制作に挑戦しました。GPSや高音質オーディオ再生ができるので、これらを組み合わせて目覚まし時計と音楽再生スピーカーを組み合わせたようなデバイスを作りました。 # 概要 この作品は時刻表示、温湿度や気圧などの環境情報表示、音楽再生(MP3 / Bluetooth)、アラームなど非常に多くの機能が盛り込まれた目覚まし時計です。快適な睡眠環境の構築をテーマに開発しました。目覚まし時計として最も重要な時刻はGPSから取得しています。また、音楽はSDカードに入ったMP3の音源やBluetoothで接続したデバイスの音源を再生できます。これらの音楽は寝るときに再生することを想定しているため、音楽を一定時間で停止するオフタイマー機能も搭載しています。また、睡眠環境の改善を行うためにガスセンサBME680で気温などの様々なデータを表示しています。 ![Bluetooth経由で音楽を再生する様子(再生中のタイトル名なども同期しています)](https://camo.elchika.com/9623e4cd4ae99928b4ee115680ad8ef467ce28c7/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f31393266353932382d353831342d343062612d613662342d306330363535396631633835/) # システム構成 ![簡易システム構成図](https://camo.elchika.com/f3b45fcd5da3a3e45bd185b94fa014d185e79326/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f38373739393132332d613761362d346435342d623639302d366330376433633338616534/) システム構成は図のようになっています。Spresense拡張ボードとディスプレイがSPIで接続されており、映像情報とタッチ情報を送受信しています。また、M5 Atom LiteをBluetoothモジュール代わりに搭載しており、SpresenseとUARTとI2Sで接続することでスマートフォンからのオーディオ情報を転送しています。このI2S接続はレベルシフタを噛ませて電圧を変換しています。音はSpresense拡張ボードについているステレオミニジャックから直接出力しています。アラーム音やMP3再生のデータはMicro SDカードに保存しています。 ![本体構造(表)](https://camo.elchika.com/8ce66f7ea7b2e2809f9a928476d662e82a98aabc/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f61363865616436382d386634612d343961372d616636612d323239373931646431643161/) 本体は分割式で、ディスプレイやマイコンボードの入ったコントロールボックスとスピーカーボックスに分かれており、写真のように重ねて配置したり、分割して配置したり出来ます。スピーカーボックス部分は秋月で買ったスピーカーを組み合わせただけの、普通のパッシブスピーカーです。ケースはマルチカラー3Dプリンタで印刷しました。主観ではありますが、結構音質が良いです。裏面は以下の図のような構造になっています。 ![本体構造(裏)](https://camo.elchika.com/9feb30f7e9e3d043cb345cfaee93ef4fe8ed81ef/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f65373738386433612d383163382d343964302d386137372d353238306532373963373834/) # 機能紹介 ![画面情報](https://camo.elchika.com/1ce94ed6b23712a3da2b7642806fcd1b882c04db/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f34356333666438332d333962612d343063392d393435322d326565336632616130356131/) このようにタッチパネル画面には様々な情報が表示されます。UIはLovyanGFXで作成しました。一番上にはGPSから取得した時刻を表示しています。また、現在電波を受信できているGPS衛星の数も表示されます。睡眠時には眩しいことが想定されるので、一定時間でバックライトが消灯するようにプログラムしてあります。 周辺環境センサ情報はガスセンサであるBME680から取得した情報を表示しています。表示項目は、温度、湿度、二酸化炭素濃度(eCO2)、気圧、不快指数、室内空気質です。文字の色が青っぽいと数値が低く、緑に近いと良好、赤くなるにつれて高いというように文字色で適切な環境かがわかるようになっています。通常の時計などに搭載されているようなデータより多くの種類のデータが表示されています。また、不快指数などの快適性に関するデータも用意しているため、睡眠環境の改善などに役立てることができます。 ![音楽再生エリア](https://camo.elchika.com/9d836d46cedba1f74d790802f5c7b0478dc81420/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f64353035353334312d383731392d343530612d623831392d666366303763656538633530/) その下には音楽再生エリアがあります。ここでは、SDカードに保存したMP3データの再生やBluetooth接続したスマホの音楽の再生ができます。MP3はシャッフル再生やリピート再生に対応しており、ファイル名も表示されます。Bluetoothの場合にはプレイヤー操作(再生、停止など)は出来ないものの、再生中の音楽の名前が表示されます。音量操作とオフタイマーはMP3/BT共に動作します。MP3モードとBluetoothモードの切り替えはボタンを押すだけで完了します。 ![Bluetooth接続画面](https://camo.elchika.com/946457d14222166acf1e235490c7df263f5dcb34/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f37373134666361362d663566652d343361312d613762622d613663356161643333393237/) Bluetoothの接続は非常に簡単でスマートフォンなどのBluetooth対応デバイスで“SpresenseAlarmClock”というデバイスに接続するだけです。 ![オフタイマー設定画面](https://camo.elchika.com/945cd90723c360bd74c0516ced6874cbe03b6329/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f63336462656631342d623533332d346362662d623462342d366330323638353633303836/) オフタイマーはこのような画面で設定します。分単位で設定を行います。設定時間経過後、自動的に音楽の再生が停止します。 ![アラーム設定画面](https://camo.elchika.com/0db5d35ab1d4f7eb7667dd2cc12e952376942fc3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f62343336663935392d323730612d346332302d623831382d363039386631303864643536/) アラームの設定画面です。時、分、秒をそれぞれ設定します。また、有効、無効の切り替えも行えます。 ![アラーム発動画面](https://camo.elchika.com/11be7ef3eedcc44e2d0342a3a1fa089d163dac3e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f34343863333566612d363766382d343732652d623735352d303036366461633836623533/) アラームが鳴るとこのような画面になります。画面をタッチするとアラームが止まります。

+

# 動作の様子(動画) ### 1. 音楽再生 @[youtube](https://youtu.be/f3q2q17Udkw) ※テスト用にフリーBGMを使用させていただきました。([甘茶の音楽工房](https://amachamusic.chagasi.com/index.html)様、[H/MIX GALLERY](http://www.hmix.net/)様) ### 2. アラーム @[youtube](https://youtu.be/dDIJiwSaPIQ) ※アラーム音には[OtoLogic](https://otologic.jp)様の音源を使用させていただきました。

# ケースの中身 ![開発中の様子](https://camo.elchika.com/f9b2382865bd5df0e45607019a249328baf374fd/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f37646639626366332d373366642d346265612d613932392d383936663132653233363930/) 今回はジャンパ線とブレッドボードベースで作成しました。このようにコントロールボックスにはブレッドボードとセンサ、マイコンなどが収められています。固定等はしていませんが、目覚まし時計自体があまり動くものでは無いので問題なしということにしています。ケーブル等は側面もしくは裏蓋に開けた切り欠きから通しています。 # 部品 |部品名|数|備考|参考リンク| |---|:---:|---|---| |Spresenseメインボード|1|モニタ提供品|https://developer.sony.com/ja/spresense/products/spresense-main-board| |Spresense拡張ボード|1|モニタ提供品|https://developer.sony.com/ja/spresense/products/spresense-extension-board| |SPRESENSE用ガスセンサ BME680基板|1|モニタ提供品|https://www.switch-science.com/products/6082?srsltid=AfmBOorCaOM1gBvFfZ1ZuJlojOA8mg73EJIZm5qJRUffKb3sO25r9a5X| |ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807|1|モニタ提供品|https://akizukidenshi.com/catalog/g/g116265/| |ATOM Lite|1||https://akizukidenshi.com/catalog/g/g117209/| |8ビット双方向ロジックレベル変換ブレークアウトモジュールキット|1||https://akizukidenshi.com/catalog/g/g117062/| |8Ω7Wフルレンジ(広帯域)スピーカーユニット LSE77-20R|2||https://akizukidenshi.com/catalog/g/g114614/| |ブレッドボード|1||| |ジャンパ線|適量||| |3Dプリントケース|適量|PLA樹脂で製造|| # 配線図 ![配線図](https://camo.elchika.com/5d8b0f9d38b6f0ff23afff6304fe5dfcc4a4b452/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f36626661363062622d363537352d343932362d613366392d653761383065366431363132/) この配線図に従って配線します。一部、配線が分岐する部分がありますがブレッドボードなどを使って対応します。 # 3Dモデルなど 下記のような3Dモデルを3DモデリングソフトのBlenderを使って設計しました。また、これらをBambuLab社のA1 Miniという3Dプリンタでプリントしました。 ![スピーカーボックス](https://camo.elchika.com/b7827c60d139fff07f9aae766bb80e6ff6293912/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f30643165616332322d303333352d346464642d626337352d393661313230383138333865/) ![コントロールボックス](https://camo.elchika.com/d55303f78ff4338d366016beec413c4d8bada830/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65353662643033332d313833612d343966662d396563322d6264356664336262346432382f37306234663932642d363437312d346565662d396436302d326238383630646664376566/) # ソースコード Spresense、M5 Atom Lite共にArduinoIDE環境でプログラムしました。注意点としてはSpresenseのコードの書き込み時の設定でMemoryを1152KBにする必要があります。また、必要なサードパーティライブラリは以下の通りです。 - Spresense 1. LovyanGFX 2. XPT2046_Touchscreen 3. BSEC-Arduino-library 4. ArduinoJson - Atom Lite 1. ESP32-A2DP 2. arduino-audio-tools ```Arduino:Spresense用コード // マクロ定義 ---------------------------------------- #define LGFX_USE_V1 // タイムゾーン設定 (例: JST = 9時間 = 32400秒) #define MY_TIMEZONE_IN_SECONDS (9 * 60 * 60) // JST // ライブラリ関係 ---------------------------------------- // サードパーティライブラリ #include <LovyanGFX.hpp> #include <XPT2046_Touchscreen.h> #include "bsec.h" #include <ArduinoJson.h> // その他 #include <RTC.h> #include <time.h> #include <GNSS.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <Arduino.h> #include <SDHCI.h> #include <File.h> #include <SDHCI.h> #include <Audio.h> #include <EEPROM.h> #include <audio/utilities/playlist.h> #include <stdio.h> #include <stdlib.h> // 関数のプロトタイプ宣言 ---------------------------------------- void setup(); void loop(); void setupLCD(); void setBrightness(int brightness); void checkIaqSensorStatus(void); void errLeds(void); // 定数定義 ---------------------------------------- // グローバル変数 ---------------------------------------- int screenState = 0; // 画面の状態 // 環境センサ関連 Bsec iaqSensor; String output; // SDカード関連 ---------------------------------------- SDClass SD; File myFile; String Artist; String Title; void setupSD() { if (!SD.begin()) { Serial.println("SD Card initialization failed!"); return; } Serial.println("SD Card initialization done."); SD.beginUsbMsc(); } class LocalStorage { private: const char *filePath; StaticJsonDocument<1024> jsonDoc; bool isLoaded; // ロード済みかどうかを追跡 void save() { // バグがあるので封印 return; if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return; } File file = SD.open(filePath, FILE_WRITE); if (!file) { Serial.println("[LocalStorage] Failed to open file for writing."); return; } serializeJson(jsonDoc, file); file.close(); printAll(); Serial.println("[LocalStorage] Data saved."); } public: LocalStorage(const char *path) : filePath(path), isLoaded(false) {} // 明示的にロードするメソッド void load() { File file = SD.open(filePath, FILE_READ); if (!file) { Serial.println("[LocalStorage] File not found. Creating a new one."); jsonDoc.clear(); // 空のJSONに初期化 isLoaded = true; return; } DeserializationError error = deserializeJson(jsonDoc, file); if (error) { Serial.print("[LocalStorage] Failed to load JSON: "); Serial.println(error.c_str()); jsonDoc.clear(); // エラー時は空のJSONに初期化 } file.close(); isLoaded = true; // ロード成功 } // デフォルト値なしバージョン String get(const String &key) { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return ""; } if (jsonDoc.containsKey(key)) { return jsonDoc[key].as<String>(); } return ""; // キーが存在しない場合は空文字列を返す } // デフォルト値ありバージョン String get(const String &key, const String &defaultValue) { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return defaultValue; } if (jsonDoc.containsKey(key)) { return jsonDoc[key].as<String>(); } return defaultValue; // キーが存在しない場合はデフォルト値を返す } void set(const String &key, const String &value) { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return; } jsonDoc[key] = value; save(); } void remove(const String &key) { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return; } if (jsonDoc.containsKey(key)) { jsonDoc.remove(key); save(); } } void clear() { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return; } jsonDoc.clear(); save(); } void printAll() { if (!isLoaded) { Serial.println("[LocalStorage] Error: Data not loaded. Call load() first."); return; } serializeJsonPretty(jsonDoc, Serial); Serial.println(); } }; LocalStorage storage("/localstorage.json"); // MP3関連 ---------------------------------------- void removeSubstring(char *str, const char *sub) { char *pos = strstr(str, sub); // subの位置を探す if (pos != NULL) { size_t len = strlen(sub); // subの長さを取得 // subの後ろの文字を前に詰める memmove(pos, pos + len, strlen(pos + len) + 1); // +1で終端の\0も移動 } } class AudioPlayer { private: SDClass &sd; AudioClass *audio; // AudioClass *i2sAudio; Playlist playlist; Track currentTrack; File currentFile; int volume; bool randomMode; bool repeatMode; bool isPlaying; bool isReady; bool isI2SPassthrough; bool threadRunning_; int tempVolume; bool ErrEnd; static void audioAttentionCallback(const ErrorAttentionParam *atprm) { puts("Attention!"); if (atprm->error_code >= AS_ATTENTION_CODE_WARNING) { // Handle fatal errors here if needed // this->setPlaying(false); // stop(); } } void setPlaying(bool playing) { isPlaying = playing; } bool setPlayer(Track *t) { static uint8_t s_codec = 0; static uint32_t s_fs = 0; static uint8_t s_bitlen = 0; static uint8_t s_channel = 0; static AsClkMode s_clkmode = (AsClkMode)-1; err_t err = AUDIOLIB_ECODE_OK; if ((s_codec != t->codec_type) || (s_fs != t->sampling_rate) || (s_bitlen != t->bit_length) || (s_channel != t->channel_number)) { AsClkMode clkmode = AS_CLKMODE_NORMAL; if (s_clkmode != clkmode) { if (s_clkmode != (AsClkMode)-1) { audio->setReadyMode(); } s_clkmode = clkmode; audio->setRenderingClockMode(clkmode); audio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); } err = audio->initPlayer(AudioClass::Player0, t->codec_type, "/mnt/sd0/BIN", t->sampling_rate, t->bit_length, t->channel_number); if (err != AUDIOLIB_ECODE_OK) { printf("Player initialization error\n"); return false; } s_codec = t->codec_type; s_fs = t->sampling_rate; s_bitlen = t->bit_length; s_channel = t->channel_number; } return true; } public: AudioPlayer(SDClass &sdInstance, AudioClass *audioInstance, const char *playlistPath) : sd(sdInstance), audio(audioInstance), playlist(playlistPath), ErrEnd(false), volume(-160), randomMode(false), repeatMode(false), isPlaying(false), isI2SPassthrough(false), thread_(0) {} bool begin() { if (!playlist.init("/mnt/sd0/PLAYLIST")) { printf("Playlist initialization failed\n"); return false; } audio->begin(audioAttentionCallback); audio->setVolume(volume); // i2sAudio = AudioClass::getInstance(); // i2sAudio->begin(audioAttentionCallback); // int err = i2sAudio->setThroughMode(AudioClass::I2sIn, AudioClass::None, true, 160, AS_SP_DRV_MODE_LINEOUT); // if (err != AUDIOLIB_ECODE_OK) // { // printf("Through initialize error\n"); // exit(1); // } // i2sAudio->setVolume(-1000); playlist.getNextTrack(&currentTrack); return true; } bool isPlayNow() { return isPlaying; } bool isPlaylistExists(const char *playlistPath) { if (!sd.exists(playlistPath)) { return false; } return true; } void generatePlaylistCSV(const char *folderPath, const char *outputFile) { if (!sd.exists(folderPath)) { printf("Folder not found: %s\nCreate Folder.", folderPath); sd.mkdir(folderPath); } File folder = sd.open(folderPath); if (!folder || !folder.isDirectory()) { printf("Invalid folder path: %s\n", folderPath); return; } File csvFile = sd.open(outputFile, FILE_WRITE); if (!csvFile) { printf("Failed to create playlist file: %s\n", outputFile); return; } File file = folder.openNextFile(); while (file) { if (!file.isDirectory()) { const char *fileName = file.name(); const char *extension = strrchr(fileName, '.'); if (extension && (strcmp(extension, ".mp3") == 0 || strcmp(extension, ".wav") == 0)) { // Placeholder metadata; replace with actual file parsing if needed const char *author = "Unknown"; const char *album = "Unknown"; int channels = 2; int bitLength = 16; int samplingRate = 44100; const char *format = (strcmp(extension, ".mp3") == 0) ? "mp3" : "wav"; csvFile.printf("%s,%s,%s,%d,%d,%d,%s\n", fileName, author, album, channels, bitLength, samplingRate, format); } } file = folder.openNextFile(); } folder.close(); csvFile.close(); printf("Playlist generated at %s\n", outputFile); } void setVolume(int newVolume) { // volumeは 0 ~ 100 の範囲で設定 volume = newVolume; // dB単位(-1020 ~ 120)に変換して設定 audio->setVolume(map(volume, 0, 100, -1020, 120)); } int getVolume() { return volume; } void volumeUp() { volume += 2; if (volume > 100) { volume = 100; } setVolume(volume); } void volumeDown() { volume -= 2; if (volume < 0) { volume = 0; } setVolume(volume); } void toggleRandomMode() { randomMode = !randomMode; playlist.setPlayMode(randomMode ? Playlist::PlayModeShuffle : Playlist::PlayModeNormal); } void toggleRepeatMode() { repeatMode = !repeatMode; playlist.setRepeatMode(repeatMode ? Playlist::RepeatModeOn : Playlist::RepeatModeOff); } bool play() { if (isI2SPassthrough) { printf("Cannot play in I2S passthrough mode\n"); return false; } if (!setPlayer(&currentTrack)) { return false; } char fullpath[64] = {0}; snprintf(fullpath, sizeof(fullpath), "%s", currentTrack.title); removeSubstring(fullpath, "/mnt/sd0/"); Serial.println(fullpath); currentFile.close(); currentFile = sd.open(fullpath); if (!currentFile) { printf("File open error\n"); return false; } err_t err = audio->writeFrames(AudioClass::Player0, currentFile); if ((err != AUDIOLIB_ECODE_OK) && (err != AUDIOLIB_ECODE_FILEEND)) { printf("File read error\n"); return false; } audio->startPlayer(AudioClass::Player0); isPlaying = true; // 再生中フラグをオン return true; } String getCurrentTrackTitle() { return currentTrack.title; } void startAlarm() { if (isPlaying) { stop(); } tempVolume = volume; setVolume(90); currentFile.close(); currentFile = sd.open("ALARM/Alarm.mp3"); if (!currentFile) { printf("File open error\n"); return false; } err_t err = audio->writeFrames(AudioClass::Player0, currentFile); if ((err != AUDIOLIB_ECODE_OK) && (err != AUDIOLIB_ECODE_FILEEND)) { printf("File read error\n"); return false; } audio->startPlayer(AudioClass::Player0); // リピートモードをオンにする repeatMode = true; isPlaying = true; // 再生中フラグをオン } void stopAlarm() { stop(); repeatMode = false; isPlaying = false; // 再生中フラグをオフ setVolume(tempVolume); } void stream() { if (isPlaying) { err_t err = audio->writeFrames(AudioClass::Player0, currentFile); if ((err != AUDIOLIB_ECODE_OK) && (err != AUDIOLIB_ECODE_FILEEND)) { printf("File read error\n"); return; } else if (err == AUDIOLIB_ECODE_FILEEND) { if (!currentFile) { Serial.println("File not found"); return; } audio->stopPlayer(AudioClass::Player0, AS_STOPPLAYER_ESEND); // 再生モードによって次の曲を再生するか判断 if (repeatMode || randomMode) { playlist.getNextTrack(&currentTrack); isReady = true; Serial.println("Ready to play next track"); // リピート、ランダムの状態を表示 if (repeatMode) { Serial.println("Repeat mode: ON"); } if (randomMode) { Serial.println("Random mode: ON"); } } } } if (isReady) { if (play()) { isReady = false; Serial.println("Playing next track"); } else { Serial.println("Failed to play next track"); } } } static void *streamThread(void *arg) { AudioPlayer *self = static_cast<AudioPlayer *>(arg); self->run(); pthread_exit(NULL); Serial.println("Thread exit"); return NULL; } void run() { while (1) { stream(); // 次の更新まで待機 (2秒) sleep(2); } } pthread_t thread_; ///< スレッドハンドル bool startBackgroundThread(size_t stackSize = 1024 * 4) { if (threadRunning_) { // すでにスレッドが実行中 printf("[AudioPlayer] Thread already running.\n"); return false; } threadRunning_ = true; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, stackSize); int ret = pthread_create(&thread_, &attr, AudioPlayer::streamThread, this); pthread_attr_destroy(&attr); if (ret != 0) { printf("[AudioPlayer] Failed to create thread. ret = %d\n", ret); threadRunning_ = false; return false; } return true; } void stop() { audio->stopPlayer(AudioClass::Player0, AS_STOPPLAYER_NORMAL); if (currentFile) { currentFile.close(); } isPlaying = false; // 再生中フラグをオフ } void pause() { audio->stopPlayer(AudioClass::Player0, AS_STOPPLAYER_NORMAL); isPlaying = false; // 再生中フラグをオフ } void resume() { audio->startPlayer(AudioClass::Player0); isPlaying = true; // 再生中フラグをオン } void playStop() { if (isPlaying) { stop(); } else { play(); } } void pauseResume() { if (currentFile.size() == 0) { play(); return; } if (isPlaying) { pause(); } else { resume(); } } bool nextTrack() { stop(); if (playlist.getNextTrack(&currentTrack)) { return play(); } return false; } bool previousTrack() { stop(); if (playlist.getPrevTrack(&currentTrack)) { return play(); } return false; } void listTracks() { Track t; playlist.restart(); while (playlist.getNextTrack(&t)) { printf("%s | %s | %s\n", t.author, t.album, t.title); } playlist.restart(); } bool isCurrentlyPlaying() const { return isPlaying; } bool isRandomMode() const { return randomMode; } bool isRepeatMode() const { return repeatMode; } bool isPlayPaththrough() const { return isI2SPassthrough; } bool toggleI2SPassthroughMode() { if (isI2SPassthrough) { // Disable passthrough mode audio->setReadyMode(); isI2SPassthrough = false; printf("I2S passthrough mode disabled\n"); // i2sAudio->setVolume(-1000); } else { if (isPlaying) { stop(); } audio->setReadyMode(); // i2sAudio->setVolume(-160); // Enable passthrough mode int err = audio->setThroughMode(AudioClass::I2sIn, AudioClass::None, true, 160, AS_SP_DRV_MODE_LINEOUT); if (err != AUDIOLIB_ECODE_OK) { printf("Failed to enable I2S passthrough mode\n"); return false; } isI2SPassthrough = true; printf("I2S passthrough mode enabled\n"); } return true; } }; AudioPlayer player(SD, AudioClass::getInstance(), "TRACK_DB.CSV"); bool isInitializeAudio = false; // オフタイマー---------------------------------------- class MusicOffTimer { private: unsigned long duration; // タイマーの継続時間 (ミリ秒単位) unsigned long startTime; // タイマーの開始時刻 unsigned long remainingTime; // 停止時の残り時間 (ミリ秒単位) bool timerRunning; // タイマーが動作中かどうか public: bool timerEnabled; // タイマーが有効かどうか // コンストラクタ MusicOffTimer() : duration(0), startTime(0), remainingTime(0), timerRunning(false), timerEnabled(false) {} // タイマーの設定 (秒単位で設定) void setTimer(int seconds) { duration = seconds * 1000; // ミリ秒に変換 remainingTime = duration; // 初期状態の残り時間を設定 timerRunning = false; // 新しく設定した場合は停止状態 } // タイマーの開始 void startTimer() { if (remainingTime > 0) { startTime = millis(); timerRunning = true; timerEnabled = true; } } // タイマーの停止 void stopTimer() { if (timerRunning) { unsigned long elapsedTime = millis() - startTime; if (elapsedTime < remainingTime) { remainingTime -= elapsedTime; } else { remainingTime = 0; // 時間が経ちすぎて残り時間がなくなる場合 } timerRunning = false; timerEnabled = false; } } // タイマーのリセット void resetTimer() { remainingTime = duration; startTime = millis(); } // タイマーの残り時間を取得 (秒単位) int getRemainingTime() const { if (!timerRunning) return remainingTime / 1000; unsigned long elapsedTime = millis() - startTime; if (elapsedTime >= remainingTime) { return 0; // タイマー終了 } return (remainingTime - elapsedTime) / 1000; // 残り時間を秒単位で返す } // タイマーの残り時間を取得 (分単位) int getRemainingTimeMinutes() const { if (!timerRunning) Serial.println(remainingTime / 60000); return remainingTime / 60000; unsigned long elapsedTime = millis() - startTime; if (elapsedTime >= remainingTime) { Serial.println(0); return 0; // タイマー終了 } Serial.println((remainingTime - elapsedTime) / 60000); return int((remainingTime - elapsedTime) / 60000); // 残り時間を分単位で返す } // タイマーの残り時間を取得 (hh:mm形式) String getRemainingTimeFormatted() const { unsigned long remainingMillis = remainingTime; if (timerEnabled && timerRunning) { unsigned long elapsedTime = millis() - startTime; remainingMillis = remainingTime - elapsedTime; } else { return "00:00"; // タイマー終了 } int hours = remainingMillis / 3600000; int minutes = (remainingMillis % 3600000) / 60000; char buffer[6]; snprintf(buffer, sizeof(buffer), "%02d:%02d", hours, minutes); // Serial.println(buffer); return String(buffer); } // タイマーが終了したかどうかをチェック bool isTimerFinished() { if (!timerEnabled) { return false; // タイマーが無効の場合は終了とみなさない } if (!timerRunning) { timerEnabled = false; return true; // タイマーが停止中の場合は終了とみなす } return millis() - startTime >= remainingTime; } // タイマーの動作状態を取得 bool isTimerRunning() const { return timerRunning; } // タイマーのインクリメント (1分) void increment1Minute() { remainingTime += 60000; // 1分 (60秒 * 1000ミリ秒) if (!timerRunning) duration = remainingTime; // 停止中の場合はdurationも更新 } // タイマーのデクリメント (1分) void decrement1Minute() { if (remainingTime >= 60000) { remainingTime -= 60000; } else { remainingTime = 0; // デクリメントしすぎないように } if (!timerRunning) duration = remainingTime; // 停止中の場合はdurationも更新 } // タイマーのインクリメント (10分) void increment10Minutes() { remainingTime += 600000; // 10分 (600秒 * 1000ミリ秒) if (!timerRunning) duration = remainingTime; // 停止中の場合はdurationも更新 } // タイマーのデクリメント (10分) void decrement10Minutes() { if (remainingTime >= 600000) { remainingTime -= 600000; } else { remainingTime = 0; // デクリメントしすぎないように } if (!timerRunning) duration = remainingTime; // 停止中の場合はdurationも更新 } }; MusicOffTimer musicTimer; // GPS時刻取得関連 ---------------------------------------- /** * @brief RtcTime 構造体の簡易表示関数 * @param rtc RtcTimeオブジェクト */ void printClock(RtcTime &rtc) { printf("%04d/%02d/%02d %02d:%02d:%02d\n", rtc.year(), rtc.month(), rtc.day(), rtc.hour(), rtc.minute(), rtc.second()); } // RtcTime構造体を文字列に変換 String getClockString(RtcTime &rtc) { char buf[20]; sprintf(buf, "%04d/%02d/%02d %02d:%02d:%02d", rtc.year(), rtc.month(), rtc.day(), rtc.hour(), rtc.minute(), rtc.second()); return String(buf); } String getTimeString(RtcTime &rtc) { char buf[20]; sprintf(buf, "%02d:%02d:%02d", rtc.hour(), rtc.minute(), rtc.second()); return String(buf); } String getHourMinuteString(RtcTime &rtc) { char buf[20]; sprintf(buf, "%02d:%02d", rtc.hour(), rtc.minute()); return String(buf); } String getDateString(RtcTime &rtc) { char buf[20]; sprintf(buf, "%04d/%02d/%02d", rtc.year(), rtc.month(), rtc.day()); return String(buf); } bool isTimeGetOnce = false; int numSatellites = 0; /** * @brief TimeSyncGNSS クラス * RTCとGNSSを管理し、並行処理で時刻を同期します。 */ class TimeSyncGNSS { public: /** * @param timezoneSec タイムゾーン (秒単位)。デフォルトは0。 */ TimeSyncGNSS(int timezoneSec = 0) : timezoneSec_(timezoneSec), thread_(0), threadRunning_(false) { } /** * @brief GNSSおよびRTCの初期化 * @return 初期化の成否 (true: 成功, false: 失敗) */ bool begin() { // RTC初期化 RTC.begin(); // GNSS初期化 int ret = gnss_.begin(); if (ret != 0) { printf("[TimeSyncGNSS] Gnss.begin() failed. ret = %d\n", ret); return false; } gnss_.select(GPS); gnss_.select(GLONASS); gnss_.select(QZ_L1CA); return true; } /** * @brief GNSS測位(データ取得)を開始 * @return 成否 (true: 成功, false: 失敗) */ bool startGnss() { int ret = gnss_.start(COLD_START); if (ret != 0) { printf("[TimeSyncGNSS] Gnss.start() failed. ret = %d\n", ret); return false; } return true; } /** * @brief GNSSのバックグラウンドスレッドを開始する * @param stackSize スレッドスタックサイズ (デフォルト: 4096バイト) * @return 成否 (true: 成功, false: 失敗) */ bool startBackgroundThread(size_t stackSize = 1024 * 4) { if (threadRunning_) { // すでにスレッドが実行中 printf("[TimeSyncGNSS] Thread already running.\n"); return false; } threadRunning_ = true; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, stackSize); int ret = pthread_create(&thread_, &attr, TimeSyncGNSS::threadFunc, this); pthread_attr_destroy(&attr); if (ret != 0) { printf("[TimeSyncGNSS] Failed to create thread. ret = %d\n", ret); threadRunning_ = false; return false; } return true; } /** * @brief GNSSバックグラウンドスレッドの停止要求 */ void stopBackgroundThread() { if (!threadRunning_) return; threadRunning_ = false; pthread_join(thread_, NULL); // スレッドの終了を待機 printf("[TimeSyncGNSS] Background thread stopped.\n"); } /** * @brief 現在のRTC時刻を取得する * @return RtcTime構造体 */ RtcTime getTime() { return RTC.getTime(); } /** * @brief RTC時間を表示 (デバッグ用途) */ void printCurrentTime() { RtcTime now = getTime(); printClock(now); } /** * @brief 現在のRTC時刻を文字列で取得 * @return 時刻文字列 */ String getCurrentTimeString() { RtcTime now = getTime(); return getTimeString(now); } String getCurrentHourMinuteString() { RtcTime now = getTime(); return getHourMinuteString(now); } String getCurrentDateString() { RtcTime now = getTime(); return getDateString(now); } String getCurrentDatetimeString() { RtcTime now = getTime(); return getClockString(now); } String getYearString() { RtcTime now = getTime(); char buf[5]; sprintf(buf, "%04d", now.year()); return String(buf); } String getMonthString() { RtcTime now = getTime(); char buf[3]; sprintf(buf, "%02d", now.month()); return String(buf); } String getDayString() { RtcTime now = getTime(); char buf[3]; sprintf(buf, "%02d", now.day()); return String(buf); } String getHourString() { RtcTime now = getTime(); char buf[3]; sprintf(buf, "%02d", now.hour()); return String(buf); } String getMinuteString() { RtcTime now = getTime(); char buf[3]; sprintf(buf, "%02d", now.minute()); return String(buf); } String getSecondString() { RtcTime now = getTime(); char buf[3]; sprintf(buf, "%02d", now.second()); return String(buf); } int getYearInt() { RtcTime now = getTime(); return now.year(); } int getMonthInt() { RtcTime now = getTime(); return now.month(); } int getDayInt() { RtcTime now = getTime(); return now.day(); } int getHourInt() { RtcTime now = getTime(); return now.hour(); } int getMinuteInt() { RtcTime now = getTime(); return now.minute(); } int getSecondInt() { RtcTime now = getTime(); return now.second(); } // 曜日を取得 String getWeekdayString() { RtcTime now = getTime(); static const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"}; struct tm *tm_info = localtime((const time_t *)&now); return wd[tm_info->tm_wday]; } private: /** * @brief スレッド処理関数 (静的メンバ) * @param arg this ポインタ * @return NULL */ static void *threadFunc(void *arg) { TimeSyncGNSS *self = static_cast<TimeSyncGNSS *>(arg); self->run(); pthread_exit(NULL); return NULL; } /** * @brief スレッド内で実行される処理 */ void run() { while (threadRunning_) { if (updateGnssTime()) { Serial.println("[TimeSyncGNSS] RTC updated"); isTimeGetOnce = true; } else { // Serial.println("[TimeSyncGNSS] RTC not updated"); } } } /** * @brief GNSS情報を取得してRTCを更新する */ bool updateGnssTime() { // GNSSからの時刻更新を待つ (タイムアウト: 1000ms) if (gnss_.waitUpdate(-1)) { SpNavData navData; gnss_.getNavData(&navData); SpGnssTime *time = &navData.time; // デバッグ表示 // printf("[TimeSyncGNSS] Sattelite num: %d\n", navData.numSatellites); numSatellites = navData.numSatellites; // 取得したUTC時刻が正しそうな場合 (year >= 2000) → RTCに反映 if (time->year >= 2000) { // GNSSの時刻をRtcTime型に変換 RtcTime gps(time->year, time->month, time->day, time->hour, time->minute, time->sec, time->usec * 1000); // マイクロ秒をナノ秒に変換 // タイムゾーン補正 gps += timezoneSec_; // 現在時刻との差を比較 RtcTime now = RTC.getTime(); int diff = now - gps; // 秒単位の差分 if (abs(diff) >= 1) { RTC.setTime(gps); // デバッグ表示 printf("[TimeSyncGNSS] RTC updated: "); printClock(gps); } else { printf("[TimeSyncGNSS] RTC not updated (diff = %d)\n", diff); return false; } } else { // printf("[TimeSyncGNSS] GNSS time invalid. time = %04d/%02d/%02d %02d:%02d:%02d\n", // time->year, time->month, time->day, time->hour, time->minute, time->sec); return false; } return true; } else { printf("[TimeSyncGNSS] GNSS update failed.\n"); return false; } } SpGnss gnss_; ///< GNSSオブジェクト int timezoneSec_; ///< タイムゾーン (秒単位) pthread_t thread_; ///< スレッドハンドル bool threadRunning_; ///< スレッド実行状態 }; // グローバルインスタンス生成 (例: JSTなら9時間 = 32400秒) TimeSyncGNSS timeSync(MY_TIMEZONE_IN_SECONDS); // GNSSとRTCの初期化処理 void setupTimeSyncGNSS() { // 1. 初期化 if (!timeSync.begin()) { Serial.println("TimeSyncGNSS.begin() failed"); while (1) { ; } // 初期化失敗時は停止 } // 2. GNSSを開始 if (!timeSync.startGnss()) { Serial.println("TimeSyncGNSS.startGnss() failed"); while (1) { ; } // GNSS開始失敗時は停止 } // 3. バックグラウンドで定期的にGNSS時刻を取得&RTC更新するスレッドを開始 if (!timeSync.startBackgroundThread()) { Serial.println("TimeSyncGNSS.startBackgroundThread() failed"); while (1) { ; } // スレッド開始失敗時は停止 } Serial.println("TimeSyncGNSS initialized"); } // アラーム関連 ---------------------------------------- class AlarmClock { private: int alarmHour; int alarmMinute; int alarmSecond; bool alarmEnabled; // 最後にアラームがなった時のUnix時刻 unsigned long lastAlarmTime; public: // コンストラクタ AlarmClock() : alarmHour(0), alarmMinute(0), alarmSecond(0), alarmEnabled(false) {} // アラーム時刻を設定 void setAlarm(int hour, int minute, int second) { alarmHour = hour; alarmMinute = minute; alarmSecond = second; alarmEnabled = true; } // アラームを無効化 void disableAlarm() { alarmEnabled = false; } // アラームの有効/無効を設定 void setAlarmStatus(bool status) { alarmEnabled = status; } // アラームトリガー void triggerAlarm() { // ここにアラーム時の処理を記述 Serial.println("Alarm triggered!"); player.startAlarm(); screenState = 1; } // アラームを有効化/無効化をトグル void toggleAlarm() { alarmEnabled = !alarmEnabled; } // アラームの状態を取得 bool isAlarmEnabled() const { return alarmEnabled; } // アラームチェック void checkAlarm(int currentHour, int currentMinute, int currentSecond) { if (!alarmEnabled) return; // 最後にアラームがなった時刻から10秒以上経過している場合のみアラームを再度設定 if (millis() - lastAlarmTime > 10000) { // アラーム時刻になったらトリガー if ((currentHour == alarmHour && currentMinute == alarmMinute) && ((currentSecond >= alarmSecond && currentSecond <= alarmSecond + 10) || (alarmSecond > 50 && currentSecond <= (alarmSecond + 10) % 60))) { if (player.isPlayPaththrough()) { player.toggleI2SPassthroughMode(); } triggerAlarm(); lastAlarmTime = millis(); } } } // アラーム時刻をインクリメント/デクリメント void incrementHour() { alarmHour = (alarmHour + 1) % 24; } void decrementHour() { alarmHour = (alarmHour - 1 + 24) % 24; } void incrementMinute() { alarmMinute = (alarmMinute + 1) % 60; } void decrementMinute() { alarmMinute = (alarmMinute - 1 + 60) % 60; } void incrementSecond() { alarmSecond = (alarmSecond + 1) % 60; } void decrementSecond() { alarmSecond = (alarmSecond - 1 + 60) % 60; } String getAlarmTimeString() { char buf[20]; sprintf(buf, "%02d:%02d", alarmHour, alarmMinute); return String(buf); } String getAlarmTimeString(bool getSecond) { if (getSecond) { char buf[20]; sprintf(buf, "%02d:%02d:%02d", alarmHour, alarmMinute, alarmSecond); return String(buf); } else { getAlarmTimeString(); } } int getAlarmHour() const { return alarmHour; } int getAlarmMinute() const { return alarmMinute; } int getAlarmSecond() const { return alarmSecond; } void setAlarmHour(int hour) { alarmHour = hour; } void setAlarmMinute(int minute) { alarmMinute = minute; } void setAlarmSecond(int second) { alarmSecond = second; } }; // インスタンス生成 AlarmClock alarmClock; // 環境センサ関連 ---------------------------------------- // センサデータ用の構造体 struct SensorData { float rawTemperature; // 生の温度データ(摂氏) float pressure; // 気圧(ヘクトパスカル) float rawHumidity; // 生の湿度データ(%) float gasResistance; // ガス抵抗値(オーム) float iaqIndex; // 室内空気質指数 int iaqAccuracy; // IAQ精度(0-3) float temperature; // 補正済み温度(摂氏) float humidity; // 補正済み湿度(%) float staticIaqIndex; // 静的IAQ指数 float co2Equivalent; // CO2等価濃度(ppm) float breathVocEquivalent; // VOC等価濃度(ppm) float discomfortIndex; // 不快指数 }; SensorData masterSensorData; // センサデータ // センサデータの種類 bsec_virtual_sensor_t sensorList[10] = { BSEC_OUTPUT_RAW_TEMPERATURE, BSEC_OUTPUT_RAW_PRESSURE, BSEC_OUTPUT_RAW_HUMIDITY, BSEC_OUTPUT_RAW_GAS, BSEC_OUTPUT_IAQ, BSEC_OUTPUT_STATIC_IAQ, BSEC_OUTPUT_CO2_EQUIVALENT, BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, }; // 環境センサの初期化 void setupIaqSensor(void) { Wire.begin(); iaqSensor.begin(BME680_I2C_ADDR_SECONDARY, Wire); checkIaqSensorStatus(); iaqSensor.updateSubscription(sensorList, 10, BSEC_SAMPLE_RATE_LP); checkIaqSensorStatus(); } // 環境センサの状態確認 void checkIaqSensorStatus(void) { if (iaqSensor.status != BSEC_OK) { if (iaqSensor.status < BSEC_OK) { output = "BSEC error code : " + String(iaqSensor.status); Serial.println(output); } else { output = "BSEC warning code : " + String(iaqSensor.status); Serial.println(output); } } if (iaqSensor.bme680Status != BME680_OK) { if (iaqSensor.bme680Status < BME680_OK) { output = "BME680 error code : " + String(iaqSensor.bme680Status); Serial.println(output); } else { output = "BME680 warning code : " + String(iaqSensor.bme680Status); Serial.println(output); } } } // 不快指数を小数点以下第2位まで計算する関数 float calcDiscomfortIndex(float temperature, float humidity) { float discomfortIndex = 0.81 * temperature + 0.01 * humidity * (0.99 * temperature - 14.3) + 46.3; return round(discomfortIndex * 100) / 100; } // センサーデータを取得する関数 void updateSensorData() { if (iaqSensor.run()) { // ざっくり校正する masterSensorData.rawTemperature = iaqSensor.rawTemperature; // 生の温度データ(摂氏) masterSensorData.pressure = iaqSensor.pressure + 10; // 気圧(ヘクトパスカル) masterSensorData.rawHumidity = iaqSensor.rawHumidity; // 生の湿度データ(%) masterSensorData.gasResistance = iaqSensor.gasResistance; // ガス抵抗(オーム) masterSensorData.iaqIndex = iaqSensor.iaq; // 室内空気質(IAQ)指数 masterSensorData.iaqAccuracy = iaqSensor.iaqAccuracy; // IAQ 精度(0-3の範囲) masterSensorData.temperature = iaqSensor.temperature - 5; // 校正パラメータで簡単な校正 // 補正済み温度(摂氏) masterSensorData.humidity = iaqSensor.humidity + 5; // 補正済み湿度(%) masterSensorData.staticIaqIndex = iaqSensor.staticIaq; // 静的 IAQ 指数 masterSensorData.co2Equivalent = iaqSensor.co2Equivalent; // CO2 等価値(ppm) masterSensorData.breathVocEquivalent = iaqSensor.breathVocEquivalent; // 呼気 VOC 等価値(ppm) masterSensorData.discomfortIndex = calcDiscomfortIndex(masterSensorData.temperature, masterSensorData.humidity); // 不快指数 } else { checkIaqSensorStatus(); } } // センサーデータをフォーマットして出力する関数 void printSensorData() { Serial.println("=== Sensor Data ==="); Serial.println("Raw Temperature (°C): " + String(masterSensorData.rawTemperature)); Serial.println("Pressure (hPa): " + String(masterSensorData.pressure)); Serial.println("Raw Humidity (%): " + String(masterSensorData.rawHumidity)); Serial.println("Gas Resistance (Ohms): " + String(masterSensorData.gasResistance)); Serial.println("IAQ Index: " + String(masterSensorData.iaqIndex)); Serial.println("IAQ Accuracy (0-3): " + String(masterSensorData.iaqAccuracy)); Serial.println("Compensated Temperature (°C): " + String(masterSensorData.temperature)); Serial.println("Compensated Humidity (%): " + String(masterSensorData.humidity)); Serial.println("Static IAQ Index: " + String(masterSensorData.staticIaqIndex)); Serial.println("CO2 Equivalent (ppm): " + String(masterSensorData.co2Equivalent)); Serial.println("Breath VOC Equivalent (ppm): " + String(masterSensorData.breathVocEquivalent)); Serial.println("===================="); } // タッチセンサ関連 ---------------------------------------- // XPT2046_Touchscreen tch(5, 7); XPT2046_Touchscreen tch(4, 7); void setupTouch() { tch.begin(); tch.setRotation(0); } // 画面タッチの不感時間 int touchCoolMillis = 250; unsigned long touchCoolTime = 0; // タッチ座標を取得 TS_Point getTouchPoint() { if (millis() - touchCoolTime < touchCoolMillis) { TS_Point p; p.x = -1; p.y = -1; return p; } else if (tch.touched()) { touchCoolTime = millis(); TS_Point p = tch.getPoint(); // 画面の向きに合わせて座標を変換 p.x = (int)(((float)p.x / 3600.0f) * 240.0f) - 15; p.y = (int)(((float)p.y / 3600.0f) * 320.0f) - 30; // 範囲に収める if (p.x < 0) { p.x = 0; } else if (p.x > 240) { p.x = 240; } if (p.y < 0) { p.y = 0; } else if (p.y > 320) { p.y = 320; } return p; } else { TS_Point p; p.x = -1; p.y = -1; return p; } } TS_Point touchPoint; bool isTouchArea(TS_Point p, int x, int y, int w, int h) { return (p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h); } // ディスプレイ関係 ---------------------------------------- class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ILI9341 _panel_instance; lgfx::Bus_SPI _bus_instance; public: LGFX(void) { { // バス制御の設定を行います。 auto cfg = _bus_instance.config(); // バス設定用の構造体を取得します。 cfg.spi_mode = 0; // SPI通信モードを設定 (0 ~ 3) cfg.freq_write = 30000000; // 送信時のSPIクロック cfg.freq_read = 16000000; // 受信時のSPIクロック cfg.pin_dc = 9; // SPIのD/Cピン番号を設定 (-1 = disable) cfg.spi_port = 4; // Arduino拡張ボードの場合は 4 _bus_instance.config(cfg); // 設定値をバスに反映します。 _panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。 } { // 表示パネル制御の設定を行います。 auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。 // cfg.pin_cs = -1; // CSが接続されているピン番号 (-1 = disable) HW CSピンの場合は-1を指定 cfg.pin_rst = 8; // RSTが接続されているピン番号 (-1 = disable) cfg.pin_busy = -1; // BUSYが接続されているピン番号 (-1 = disable) cfg.panel_width = 240; // 実際に表示可能な幅 cfg.panel_height = 320; // 実際に表示可能な高さ cfg.offset_x = 0; // パネルのX方向オフセット量 cfg.offset_y = 0; // パネルのY方向オフセット量 cfg.offset_rotation = 0; // 回転方向の値のオフセット 0~7 (4~7は上下反転) cfg.dummy_read_pixel = 8; // ピクセル読出し前のダミーリードのビット数 cfg.dummy_read_bits = 1; // ピクセル以外のデータ読出し前のダミーリードのビット数 cfg.readable = true; // データ読出しが可能な場合 trueに設定 cfg.invert = false; // パネルの明暗が反転してしまう場合 trueに設定 cfg.rgb_order = false; // パネルの赤と青が入れ替わってしまう場合 trueに設定 cfg.dlen_16bit = false; // データ長を16bit単位で送信するパネルの場合 trueに設定 cfg.bus_shared = true; // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います) _panel_instance.config(cfg); } setPanel(&_panel_instance); // 使用するパネルをセットします。 } }; LGFX lcd; // 明るさ減少までの待機時間 (ミリ秒) const unsigned long DIM_DELAY_1 = 60000; // 60秒で減光 // 現在の明るさを保持する変数 int currentBrightness = 100; // 最後に操作された時刻 unsigned long lastActiveTime = 0; // 画面の明るさを設定(0-100) void setBrightness(int brightness) { // 0-100の範囲に収める if (brightness < 0) { brightness = 0; } else if (brightness > 100) { brightness = 100; } int duty = 255 * (brightness / 100); analogWriteFreq(PIN_PWM_0, duty, 1000); } // 画面を点灯する関数 void turnOnDisplay() { // 明るさを最大に設定 if (currentBrightness != 100) { setBrightness(100); currentBrightness = 100; Serial.println("Turn on display"); } // 最後の操作時間を記録 lastActiveTime = millis(); } // 一定時間が経過すると消灯する関数 void dimDisplayOverTime() { unsigned long currentTime = millis(); // 一回目の減光: 50%にする if (currentBrightness == 100 && currentTime - lastActiveTime > DIM_DELAY_1) { setBrightness(0); currentBrightness = 0; Serial.println("Dimming display to 0%"); } } // LCD初期化処理 void setupLCD() { lcd.init(); lcd.setRotation(0); // 画面の回転方向を設定(0-3) lcd.fillScreen(lcd.color565(0, 0, 0)); // 画面全体を黒色で塗りつぶし // setBrightness(50); // 画面の明るさを設定(0-100) turnOnDisplay(); } // 起動時の画面表示 void showBootScreen() { lcd.fillScreen(lcd.color565(255, 255, 255)); // 画面全体を黒で塗りつぶし lcd.setTextColor(TFT_WHITE, TFT_BLACK); // テキスト色を設定 // 中央にSpresens\nAlarm Clockと中央揃えで表示 lcd.drawString("Alarm Clock", 10, 10, &fonts::DejaVu18); } void resetLcd() { lcd.fillScreen(lcd.color565(0, 0, 0)); // 画面全体を黒で塗りつぶし } // Google Material Icons are licensed under the Apache License, Version 2.0. // Copyright 2014 Google LLC. // You may obtain a copy of the License at: // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const uint16_t sateliteImgWidth = 24; const uint16_t sateliteImgHeight = 24; // RGB565 Dump(little endian) const unsigned short sateliteImg[576] PROGMEM = { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 0, 24 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, // row 1, 48 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, // row 2, 72 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, // row 3, 96 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 4, 120 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 5, 144 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 6, 168 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, // row 7, 192 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, // row 8, 216 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, // row 9, 240 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 10, 264 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 11, 288 pixels 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 12, 312 pixels 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 13, 336 pixels 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 14, 360 pixels 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 15, 384 pixels 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 16, 408 pixels 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 17, 432 pixels 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, // row 18, 456 pixels 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, // row 19, 480 pixels 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, // row 20, 504 pixels 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, // row 21, 528 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 22, 552 pixels 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // row 23, 576 pixels }; // efont // (c) Copyright 2000-2001 /efont/ The Electronic Font Open Laboratory. // All rights reserved. const uint8_t emojiFont_data[419] = { 0x49, 0x00, 0x04, 0x02, 0x05, 0x05, 0x05, 0x06, 0x06, 0x18, 0x18, 0x00, 0xFE, 0x11, 0xFE, 0x11, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x2B, 0x0E, 0x4A, 0x45, 0x92, 0x25, 0xA1, 0xA3, 0xC1, 0x0F, 0x44, 0x42, 0x47, 0x00, 0x2D, 0x08, 0x4A, 0x44, 0x94, 0x85, 0x3F, 0x10, 0x52, 0x20, 0xC9, 0xC9, 0x91, 0x85, 0x83, 0xC8, 0x60, 0xA0, 0x10, 0x0D, 0x54, 0x83, 0xD4, 0x20, 0x34, 0x38, 0x50, 0x0C, 0x06, 0x11, 0x8D, 0x44, 0x23, 0x11, 0x29, 0x44, 0x0A, 0xD5, 0x20, 0x25, 0x53, 0x1B, 0xC9, 0xC9, 0x91, 0x95, 0x81, 0x66, 0x30, 0x48, 0x88, 0x06, 0xAA, 0x41, 0xEE, 0x60, 0x34, 0xD8, 0x1D, 0xA4, 0x06, 0xA1, 0xC1, 0x81, 0x64, 0x30, 0x01, 0x7C, 0x09, 0xC2, 0xD6, 0x8F, 0x85, 0xFF, 0x3F, 0x10, 0x00, 0x00, 0x04, 0xFF, 0xFF, 0x21, 0x90, 0x0E, 0xCA, 0xC4, 0x93, 0x15, 0x9D, 0x6E, 0xF0, 0x03, 0x85, 0x52, 0x06, 0x21, 0x92, 0x0E, 0xCA, 0xC4, 0x93, 0x35, 0xA5, 0x62, 0xF0, 0x03, 0x9D, 0x09, 0x00, 0x21, 0xD2, 0x18, 0xEC, 0xC1, 0x91, 0xC5, 0x6C, 0x38, 0x1B, 0x19, 0x1C, 0x68, 0xC3, 0xD9, 0xD2, 0xC4, 0xE0, 0x40, 0x9A, 0x8D, 0x66, 0x33, 0x00, 0x25, 0xA0, 0x0B, 0x08, 0x49, 0x93, 0x85, 0xFF, 0xFF, 0x1F, 0x04, 0x25, 0xA1, 0x0F, 0xCC, 0xC2, 0x8F, 0x85, 0x0F, 0xA2, 0xFE, 0xFF, 0x4F, 0x07, 0x0F, 0x02, 0x25, 0xB2, 0x13, 0x08, 0x49, 0x93, 0x1D, 0x99, 0x6A, 0x10, 0x1A, 0x64, 0x06, 0x93, 0xC1, 0x62, 0xF0, 0x41, 0x00, 0x25, 0xB3, 0x24, 0x4C, 0xC2, 0x90, 0xAD, 0xAC, 0x54, 0x99, 0x48, 0x26, 0x92, 0x91, 0x60, 0x24, 0x97, 0xC9, 0x85, 0x62, 0xA1, 0x58, 0x28, 0x15, 0x0B, 0xC5, 0x42, 0xB1, 0x4C, 0x30, 0x12, 0x8C, 0x04, 0x13, 0x83, 0x07, 0x25, 0xB6, 0x27, 0x6A, 0xC6, 0x90, 0x85, 0xA4, 0x70, 0x37, 0x88, 0x0D, 0x54, 0x83, 0xD1, 0x60, 0x90, 0x19, 0x0C, 0x24, 0x83, 0x8B, 0xC1, 0x0F, 0x12, 0x83, 0x81, 0x64, 0x30, 0xC8, 0x0C, 0x46, 0x03, 0xD5, 0x20, 0xB6, 0x13, 0x26, 0x01, 0x25, 0xBC, 0x13, 0x08, 0x49, 0x93, 0x85, 0x1F, 0x24, 0x06, 0x93, 0xC1, 0x66, 0x10, 0x1A, 0xA4, 0x64, 0x1A, 0x00, 0x25, 0xBD, 0x23, 0x4C, 0xC2, 0x90, 0x85, 0x2F, 0x82, 0x91, 0x60, 0x24, 0x98, 0x89, 0x85, 0x62, 0xA1, 0x58, 0x2A, 0x14, 0x0B, 0xC5, 0x42, 0xB1, 0x4C, 0x30, 0x12, 0x8C, 0x04, 0x13, 0xC9, 0x44, 0xD4, 0x34, 0x06, 0x25, 0xC0, 0x26, 0x6A, 0xC6, 0x90, 0xCD, 0xA0, 0x6E, 0x36, 0x48, 0x0D, 0x44, 0x83, 0xCD, 0x60, 0x10, 0x19, 0x0C, 0x14, 0x83, 0x1F, 0x24, 0x06, 0x27, 0x83, 0x81, 0x66, 0x30, 0x08, 0x0D, 0x56, 0x03, 0xD9, 0x20, 0x37, 0x54, 0x06, 0x26, 0x6A, 0x1A, 0xC8, 0xC9, 0x91, 0x1D, 0xD9, 0x6A, 0x10, 0x1A, 0x84, 0x14, 0x1A, 0x85, 0x46, 0xA1, 0x91, 0x19, 0x0D, 0x32, 0x83, 0xD0, 0x20, 0xA5, 0x02, 0xFF, 0x5C, 0x09, 0x01, 0x6F, 0x0F, 0x87, 0xFF, 0x00, 0x00, 0x00}; extern const lgfx::U8g2font emojiFont = {emojiFont_data}; class TextScroller { public: // コンストラクタ TextScroller(LGFX_Sprite *targetSprite, const char *text, int x, int y, int width, int height, int speed, int interval_ms, const lgfx::IFont *font = nullptr) : sprite(targetSprite), text(text), x(x), y(y), width(width), height(height), speed(speed), interval_ms(interval_ms), font(font) { if (font) sprite->setFont(font); // フォント設定 textWidth = sprite->textWidth(text); offsetX = 0; lastUpdateTime = millis(); // 初期化時の時間を記録 } // フォントを変更する void setFont(const lgfx::IFont *newFont) { font = newFont; sprite->setFont(newFont); textWidth = sprite->textWidth(text); // フォント変更時に幅を再計算 } // **テキストを動的に変更** void setText(const char *newText) { text = newText; if (font) sprite->setFont(font); // フォント設定 textWidth = sprite->textWidth(text); // 幅を再計算 // 以前の文字列と比較して同じ場合は何もしない if (String(text) == String(newText)) { return; } offsetX = 0; // スクロール位置をリセット } // 文字列をスクロール描画(非ブロッキング) void update() { sprite->setTextWrap(false); // テキストの折り返しを無効に if (millis() - lastUpdateTime >= interval_ms) { // 一定時間経過後に更新 lastUpdateTime = millis(); if (font) sprite->setFont(font); // フォント設定 // スクロールする必要がない場合、通常表示 if (textWidth <= width) { sprite->setCursor(x, y - 2); if (font) sprite->setFont(&emojiFont); // フォント設定 sprite->print("♪"); if (font) sprite->setFont(font); // フォント設定 sprite->setCursor(x + 16, y); sprite->print(text); return; } // スクロール処理(2周目のテキストも描画) sprite->fillRect(x, y, width, height, TFT_BLACK); // 指定領域をクリア sprite->setCursor(x - offsetX, y); if (font) sprite->setFont(&emojiFont); // フォント設定 sprite->print("♪"); if (font) sprite->setFont(font); // フォント設定 sprite->print(text); // 2周目のテキストを追加描画(途切れをなくす) sprite->setCursor(x - offsetX + textWidth + 10, y); // 10px の間隔を空けて2周目を描画 if (font) sprite->setFont(&emojiFont); // フォント設定 sprite->print("♪"); if (font) sprite->setFont(font); // フォント設定 sprite->print(text); offsetX += speed; if (offsetX > textWidth + 10) { // 1周分スクロールしたらリセット offsetX = 0; } } } private: LGFX_Sprite *sprite; // 描画対象のスプライト const char *text; int x, y, width, height; int speed, interval_ms; int offsetX; int textWidth; unsigned long lastUpdateTime; // 最後に更新した時間 const lgfx::IFont *font; // フォント }; LGFX_Sprite sprite(&lcd); TextScroller scroller(&sprite, "再生中の音楽なし", 0, 165, 240, 20, 2, 50, &lgfxJapanGothicP_16); // ボタン描画関数 bool drawButton(int x, int y, int w, int h, TS_Point touchPoint, String text, int textSize, int textColor, int buttonColor) { bool isTouched = false; if (touchPoint.x >= x && touchPoint.x <= x + w && touchPoint.y >= y && touchPoint.y <= y + h) { isTouched = true; lcd.fillRect(x, y, w, h, buttonColor); } else { lcd.drawRect(x, y, w, h, buttonColor); } lcd.setTextColor(textColor, buttonColor); lcd.drawString(text, x + 5, y + 5, 2); return isTouched; } // DIに応じた色を取得する関数 uint16_t getDIColor(float DI) { uint8_t r = 0, g = 0, b = 0; if (DI <= 50) { // 寒くてたまらない → 濃い青 r = 0; g = 0; b = 128; } else if (DI <= 55) { // 寒い → 青 r = 0; g = 0; b = 255; } else if (DI <= 60) { // 肌寒い → 水色 r = 0; g = 128; b = 255; } else if (DI <= 65) { // 何も感じない → 薄緑 r = 128; g = 255; b = 128; } else if (DI <= 70) { // 快適 → 緑 r = 0; g = 255; b = 0; } else if (DI <= 75) { // 不快感を持つ人が出始める → 黄緑 r = 128; g = 255; b = 0; } else if (DI <= 80) { // 半数以上が不快 → 黄色 r = 255; g = 255; b = 0; } else if (DI <= 85) { // 全員が不快 → オレンジ r = 255; g = 165; b = 0; } else { // 暑くてたまらない → 赤 r = 255; g = 0; b = 0; } return lcd.color565(r, g, b); } // IAQに応じた色を取得する関数 uint16_t getIAQColor(float IAQ, float IAQ_max) { float ratio = IAQ / IAQ_max; // 0.0 (良好) ~ 1.0 (最悪) uint8_t r = (uint8_t)(ratio * 255); uint8_t g = 255 - r; return lcd.color565(r, g, 0); } // CO2濃度に応じた色を取得 uint16_t getCO2Color(int CO2) { uint8_t r = 0, g = 0, b = 0; if (CO2 <= 800) { r = 0; g = 255; b = 0; } // 緑 else if (CO2 <= 1000) { r = 128; g = 255; b = 0; } // 黄緑 else if (CO2 <= 1500) { r = 255; g = 255; b = 0; } // 黄色 else if (CO2 <= 2000) { r = 255; g = 165; b = 0; } // オレンジ else { r = 255; g = 0; b = 0; } // 赤 return lcd.color565(r, g, b); } // 温度に応じた色を取得 uint16_t getTemperatureColor(float temp) { uint8_t r = 0, g = 0, b = 0; if (temp <= 0) { r = 0; g = 0; b = 128; } // 濃い青 else if (temp <= 10) { r = 0; g = 0; b = 255; } // 青 else if (temp <= 20) { r = 0; g = 128; b = 255; } // 水色 else if (temp <= 25) { r = 0; g = 255; b = 0; } // 緑 else if (temp <= 30) { r = 128; g = 255; b = 0; } // 黄緑 else if (temp <= 35) { r = 255; g = 255; b = 0; } // 黄色 else { r = 255; g = 0; b = 0; } // 赤 return lcd.color565(r, g, b); } // 湿度に応じた色を取得 uint16_t getHumidityColor(float humidity) { uint8_t r = 0, g = 0, b = 0; if (humidity <= 30) { r = 0; g = 0; b = 255; } // 青 else if (humidity <= 50) { r = 0; g = 255; b = 0; } // 緑 else if (humidity <= 70) { r = 128; g = 255; b = 0; } // 黄緑 else if (humidity <= 85) { r = 255; g = 255; b = 0; } // 黄色 else { r = 255; g = 0; b = 0; } // 赤 return lcd.color565(r, g, b); } // 気圧に応じた色を取得 uint16_t getPressureColor(float pressure) { uint8_t r = 0, g = 0, b = 0; if (pressure <= 980) { r = 0; g = 0; b = 128; } // 濃い青 else if (pressure <= 1000) { r = 0; g = 0; b = 255; } // 青 else if (pressure <= 1020) { r = 0; g = 255; b = 0; } // 緑(快適) else if (pressure <= 1040) { r = 255; g = 255; b = 0; } // 黄色 else { r = 255; g = 0; b = 0; } // 赤(高気圧) return lcd.color565(r, g, b); } void drawScreen() { // 画面の初期化 sprite.startWrite(); sprite.clear(TFT_BLACK); // 画面の状態によって処理を分岐 if (screenState == 0) // 時計画面 { sprite.setTextColor(TFT_WHITE); sprite.setTextSize(1); sprite.setTextSize(1.05); sprite.drawString(timeSync.getCurrentTimeString(), 10, 10, &Font7); // カウント数表示 sprite.setTextSize(1.0); sprite.drawString(timeSync.getCurrentDateString() + " (" + timeSync.getWeekdayString() + ")", 10, 70, &Font4); // カウント数表示 sprite.setTextSize(1); sprite.drawLine(0, 100, 240, 100, TFT_WHITE); // センサーデータを表示 sprite.setTextColor(getTemperatureColor(masterSensorData.temperature)); sprite.drawString(String(masterSensorData.temperature) + " ℃", 5, 105, &lgfxJapanGothicP_16); sprite.drawLine(80, 100, 80, 130, TFT_WHITE); sprite.setTextColor(getHumidityColor(masterSensorData.humidity)); sprite.drawString(String(masterSensorData.humidity) + " %", 85, 105, &lgfxJapanGothicP_16); sprite.drawLine(160, 100, 160, 130, TFT_WHITE); sprite.setTextColor(getCO2Color(masterSensorData.co2Equivalent)); sprite.drawString(String(int(masterSensorData.co2Equivalent)) + " ppm", 165, 105, &lgfxJapanGothicP_16); sprite.drawLine(0, 130, 240, 130, TFT_WHITE); sprite.setTextColor(getPressureColor(int(masterSensorData.pressure / 100))); sprite.drawString(String(int(masterSensorData.pressure / 100)) + " hPa", 5, 135, &lgfxJapanGothicP_16); sprite.drawLine(80, 130, 80, 160, TFT_WHITE); sprite.setTextColor(getDIColor(masterSensorData.discomfortIndex)); sprite.drawString("DI: " + String(int(masterSensorData.discomfortIndex)), 85, 135, &lgfxJapanGothicP_16); sprite.drawLine(160, 130, 160, 160, TFT_WHITE); sprite.setTextColor(getIAQColor(masterSensorData.iaqIndex, 500)); sprite.drawString("IAQ: " + String(int(masterSensorData.iaqIndex)), 165, 135, &lgfxJapanGothicP_16); sprite.drawLine(0, 160, 240, 160, TFT_WHITE); sprite.setTextColor(TFT_WHITE); if (isTimeGetOnce == false) { sprite.drawString("GPS時刻取得中... (衛星数: " + String(numSatellites) + ")", 15, 180, &lgfxJapanGothicP_16); sprite.setTextSize(1.0); sprite.setTextColor(TFT_RED); sprite.drawString("Audioを有効化すると", 42, 220, &lgfxJapanGothicP_16); sprite.drawString("時刻合わせが遅れます", 40, 240, &lgfxJapanGothicP_16); sprite.setTextSize(1); sprite.setTextColor(TFT_WHITE); // 強制的にオーディオを有効化する sprite.fillRoundRect(10, 270, 220, 40, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("強制的にオーディオを有効化", 15, 280, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 10, 270, 220, 40)) { isTimeGetOnce = true; } } else { // 音楽情報 if (player.isPlayPaththrough()) { scroller.update(); } else if (player.isPlayNow()) { sprite.drawString("♪", 5, 162, &emojiFont); if (player.isPlayPaththrough()) { sprite.drawString(String(player.getCurrentTrackTitle()), 20, 165, &lgfxJapanGothicP_16); // scroller.setText(player.getCurrentTrackTitle().c_str()); // scroller.update(); } else { String path = String(player.getCurrentTrackTitle()); path.replace("/mnt/sd0/MUSIC/", ""); sprite.drawString(path, 20, 165, &lgfxJapanGothicP_16); // scroller.setText(player.getCurrentTrackTitle().c_str()); // scroller.update(); } } else { sprite.drawString("♪", 5, 162, &emojiFont); sprite.drawString("再生中の音楽無し", 20, 165, &lgfxJapanGothicP_16); // scroller.setText("再生中の音楽無し"); // scroller.update(); } sprite.setTextColor(TFT_BLACK); // 前の曲 sprite.fillRoundRect(10, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("|◀", 8, 196, &emojiFont); if (isTouchArea(touchPoint, 10, 195, 25, 25)) { if (player.isPlayPaththrough()) { Serial2.println("AUD_CTRL,PREV"); } else { player.previousTrack(); } } // 再生ボタン if (player.isCurrentlyPlaying()) { sprite.fillRoundRect(40, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("■", 47, 196, &emojiFont); } else { sprite.fillRoundRect(40, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("▶", 49, 196, &emojiFont); } if (isTouchArea(touchPoint, 40, 195, 25, 25)) { if (player.isPlayPaththrough()) { Serial2.println("AUD_CTRL,PLAY"); } else { player.playStop(); } } // 次の曲 sprite.fillRoundRect(70, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("▶|", 72, 196, &emojiFont); if (isTouchArea(touchPoint, 70, 195, 25, 25)) { if (player.isPlayPaththrough()) { Serial2.println("AUD_CTRL,NEXT"); } else { player.nextTrack(); } } // 音量 sprite.fillRoundRect(140, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("-", 147, 194, &emojiFont); sprite.setTextColor(TFT_WHITE); sprite.drawString(String(player.getVolume()), 167, 198, &lgfxJapanGothicP_16); // 音量表示 sprite.setTextColor(TFT_BLACK); sprite.fillRoundRect(200, 195, 25, 25, 5, TFT_WHITE); sprite.drawString("+", 207, 194, &emojiFont); if (isTouchArea(touchPoint, 140, 195, 25, 25)) { player.volumeDown(); } if (isTouchArea(touchPoint, 200, 195, 25, 25)) { player.volumeUp(); } // リピートボタン if (player.isPlayPaththrough()) { sprite.fillRoundRect(10, 230, 25, 25, 5, TFT_LIGHTGREY); sprite.drawString("R", 15, 231, &emojiFont); } else if (player.isRepeatMode()) { sprite.fillRoundRect(10, 230, 25, 25, 5, TFT_LIGHTGREY); sprite.drawString("R", 15, 231, &emojiFont); } else { sprite.fillRoundRect(10, 230, 25, 25, 5, TFT_WHITE); sprite.drawString("R", 15, 231, &emojiFont); } if (isTouchArea(touchPoint, 10, 230, 25, 25) && player.isPlayPaththrough() == false) { player.toggleRepeatMode(); } // シャッフルボタン if (player.isPlayPaththrough()) { sprite.fillRoundRect(40, 230, 25, 25, 5, TFT_LIGHTGREY); sprite.drawString("S", 45, 231, &emojiFont); } else if (player.isRandomMode()) { sprite.fillRoundRect(40, 230, 25, 25, 5, TFT_LIGHTGREY); sprite.drawString("S", 45, 231, &emojiFont); } else { sprite.fillRoundRect(40, 230, 25, 25, 5, TFT_WHITE); sprite.drawString("S", 45, 231, &emojiFont); } if (isTouchArea(touchPoint, 40, 230, 25, 25) && player.isPlayPaththrough() == false) { player.toggleRandomMode(); } // BTボタン if (player.isPlayPaththrough()) { sprite.fillRoundRect(70, 230, 25, 25, 5, TFT_BLUE); sprite.setTextColor(TFT_WHITE); sprite.drawString("BT", 72, 233, &lgfxJapanGothicP_16); sprite.setTextColor(TFT_BLACK); } else { sprite.fillRoundRect(70, 230, 25, 25, 5, TFT_LIGHTGREY); sprite.setTextColor(TFT_BLUE); sprite.drawString("BT", 72, 233, &lgfxJapanGothicP_16); sprite.setTextColor(TFT_BLACK); } if (isTouchArea(touchPoint, 70, 230, 25, 25)) { player.toggleI2SPassthroughMode(); } // オフタイマーボタン sprite.fillRoundRect(110, 230, 80, 25, 5, TFT_WHITE); sprite.drawString("Off Timer", 114, 233, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 110, 230, 80, 25)) { screenState = 3; musicTimer.stopTimer(); } // 残り時間 sprite.setTextSize(0.3); sprite.setTextColor(TFT_WHITE); sprite.drawString(musicTimer.getRemainingTimeFormatted(), 195, 235, &Font7); // カウント数表示 sprite.setTextSize(1); // アラーム時刻 sprite.drawLine(0, 260, 240, 260, TFT_WHITE); sprite.drawString("Alarm", 10, 265, &lgfxJapanGothicP_16); if (alarmClock.isAlarmEnabled()) { sprite.setTextSize(0.6); sprite.drawString(alarmClock.getAlarmTimeString(), 10, 285, &Font7); // カウント数表示 sprite.setTextSize(1); } else { sprite.setTextSize(1); sprite.drawString("設定なし", 10, 285, &lgfxJapanGothicP_16); // カウント数表示 } // この領域をタッチするとアラーム設定画面に遷移 if (isTouchArea(touchPoint, 0, 260, 240, 60)) { screenState = 2; } // プレイリストをアップデート sprite.drawLine(100, 260, 100, 320, TFT_WHITE); // sprite.fillRoundRect(110, 265, 120, 25, 5, TFT_WHITE); // sprite.setTextColor(TFT_BLACK); // sprite.drawString("Update Playlist", 115, 267, &lgfxJapanGothicP_16); // sprite.setTextSize(1.0); // sprite.setTextColor(TFT_WHITE); // if (isTouchArea(touchPoint, 110, 265, 120, 25)) // { // player.generatePlaylistCSV("MUSIC", "PLAYLIST/TRACK_DB.CSV"); // } } // 画像を表示 if (timeSync.getYearString() == "1970") { // GPS秒が奇数なら if (timeSync.getSecondInt() % 2 == 0) { // GPS待機中 sprite.setTextColor(TFT_WHITE); sprite.pushImage(210, 70, sateliteImgWidth, sateliteImgHeight, sateliteImg); sprite.setTextSize(0.2); sprite.drawString(String(numSatellites), 207, 69, &Font7); // カウント数表示 sprite.setTextSize(1.0); } } else { sprite.setTextColor(TFT_WHITE); sprite.pushImage(210, 70, sateliteImgWidth, sateliteImgHeight, sateliteImg); sprite.setTextSize(0.2); sprite.drawString(String(numSatellites), 207, 69, &Font7); // カウント数表示 sprite.setTextSize(1.0); } } else if (screenState == 1) { // アラーム画面 // 現在時刻を表示 sprite.setTextColor(TFT_WHITE); sprite.setTextSize(1); sprite.setTextSize(1.05); sprite.drawString(timeSync.getCurrentTimeString(), 10, 10, &Font7); // カウント数表示 sprite.setTextSize(1.0); sprite.drawString(timeSync.getCurrentDateString() + " (" + timeSync.getWeekdayString() + ")", 10, 70, &Font4); // カウント数表示 sprite.setTextSize(1); sprite.drawLine(0, 100, 240, 100, TFT_WHITE); sprite.pushImage(210, 70, sateliteImgWidth, sateliteImgHeight, sateliteImg); sprite.setTextSize(0.2); sprite.drawString(String(numSatellites), 207, 69, &Font7); // カウント数表示 sprite.setTextSize(1.0); sprite.fillCircle(120, 210, 90, TFT_WHITE); sprite.setTextSize(2); sprite.setTextColor(TFT_BLACK); sprite.drawString("STOP", 60, 190, &Font4); sprite.setTextColor(TFT_WHITE); sprite.setTextSize(1); // 全画面タッチ検出 if (isTouchArea(touchPoint, 0, 0, 240, 320)) { player.stopAlarm(); screenState = 0; } } else if (screenState == 2) { // アラーム設定画面 // 現在時刻を表示 sprite.setTextColor(TFT_WHITE); sprite.setTextSize(1); sprite.setTextSize(1.05); sprite.drawString(alarmClock.getAlarmTimeString(true), 10, 60, &Font7); sprite.setTextSize(1.0); // Hourの上下にボタンを表示 sprite.fillRoundRect(10, 10, 70, 25, 5, TFT_WHITE); sprite.fillRoundRect(10, 130, 70, 25, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); // 矢印 sprite.drawString("▲", 37, 12, &lgfxJapanGothicP_16); sprite.drawString("▼", 37, 132, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 10, 10, 70, 25)) { // Hourの上ボタンが押された alarmClock.incrementHour(); storage.set("alarmHour", String(alarmClock.getAlarmHour())); } if (isTouchArea(touchPoint, 10, 130, 70, 25)) { // Hourの下ボタンが押された alarmClock.decrementHour(); storage.set("alarmHour", String(alarmClock.getAlarmHour())); } // 分の上下にボタンを表示 sprite.fillRoundRect(85, 10, 70, 25, 5, TFT_WHITE); sprite.fillRoundRect(85, 130, 70, 25, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); // 矢印 sprite.drawString("▲", 112, 12, &lgfxJapanGothicP_16); sprite.drawString("▼", 112, 132, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 85, 10, 70, 25)) { // 分の上ボタンが押された alarmClock.incrementMinute(); storage.set("alarmMinute", String(alarmClock.getAlarmMinute())); } if (isTouchArea(touchPoint, 85, 130, 70, 25)) { // 分の下ボタンが押された alarmClock.decrementMinute(); storage.set("alarmMinute", String(alarmClock.getAlarmMinute())); } // 秒の上下にボタンを表示 sprite.fillRoundRect(160, 10, 70, 25, 5, TFT_WHITE); sprite.fillRoundRect(160, 130, 70, 25, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); // 矢印 sprite.drawString("▲", 187, 12, &lgfxJapanGothicP_16); sprite.drawString("▼", 187, 132, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 160, 10, 70, 25)) { // 秒の上ボタンが押された alarmClock.incrementSecond(); storage.set("alarmSecond", String(alarmClock.getAlarmSecond())); } if (isTouchArea(touchPoint, 160, 130, 70, 25)) { // 秒の下ボタンが押された alarmClock.decrementSecond(); storage.set("alarmSecond", String(alarmClock.getAlarmSecond())); } // 有効無効ボタン if (alarmClock.isAlarmEnabled()) { sprite.fillRoundRect(10, 170, 220, 40, 5, TFT_LIGHTGREY); sprite.setTextColor(TFT_BLACK); sprite.drawString("アラーム有効", 70, 180, &lgfxJapanGothicP_16); } else { sprite.fillRoundRect(10, 170, 220, 40, 5, TFT_WHITE); sprite.setTextColor(TFT_RED); sprite.drawString("アラーム無効", 70, 180, &lgfxJapanGothicP_16); } if (isTouchArea(touchPoint, 10, 170, 220, 40)) { // アラーム有効ボタンが押された alarmClock.toggleAlarm(); storage.set("alarmStatus", String(alarmClock.isAlarmEnabled())); } // 画面下部にSaveボタンを表示 sprite.fillRoundRect(10, 270, 220, 40, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("Save", 100, 280, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 10, 270, 220, 40)) { // Saveボタンが押された screenState = 0; } } else if (screenState == 3) { // off timer設定画面 // 残り時間をminで表示 sprite.setTextColor(TFT_WHITE); sprite.setTextSize(1.05); sprite.drawString(String(int(musicTimer.getRemainingTimeMinutes())), 10, 10, &Font7); // カウント数表示 // min 表示 sprite.setTextSize(2); sprite.drawString("min", 180, 20, &lgfxJapanGothicP_16); sprite.setTextSize(1.0); // 線を引く sprite.drawLine(0, 70, 240, 70, TFT_WHITE); // 4つのボタン横並びに表示 sprite.setTextSize(1); // -10分ボタン sprite.fillRoundRect(5, 80, 50, 30, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("-10", 15, 85, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 5, 80, 50, 30)) { // -10分ボタンが押された musicTimer.decrement10Minutes(); } // -1分ボタン sprite.fillRoundRect(65, 80, 50, 30, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("-1", 83, 85, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 65, 80, 50, 30)) { // -1分ボタンが押された musicTimer.decrement1Minute(); } // +1分ボタン sprite.fillRoundRect(125, 80, 50, 30, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("+1", 140, 85, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 125, 80, 50, 30)) { // +1分ボタンが押された musicTimer.increment1Minute(); } // +10分ボタン sprite.fillRoundRect(185, 80, 50, 30, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("+10", 195, 85, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 185, 80, 50, 30)) { // +10分ボタンが押された musicTimer.increment10Minutes(); } // リセットボタン sprite.fillRoundRect(5, 140, 230, 40, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("Reset", 95, 150, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 5, 140, 230, 40)) { // Resetボタンが押された musicTimer.setTimer(0); musicTimer.stopTimer(); musicTimer.resetTimer(); } // 画面下部にSaveボタンを表示 sprite.fillRoundRect(10, 270, 220, 40, 5, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("Save", 100, 280, &lgfxJapanGothicP_16); if (isTouchArea(touchPoint, 10, 270, 220, 40)) { // Saveボタンが押された musicTimer.startTimer(); screenState = 0; } } sprite.pushSprite(&lcd, 0, 0); sprite.endWrite(); } char inputBuffer[1024]; // 受信用バッファ int bufferIndex = 0; // バッファのインデックス // 初期化処理 ---------------------------------------- void setup() { // シリアル通信開始 Serial.begin(115200); while (!Serial) { }; // シリアルポートが接続されるまで待機 Serial2.begin(115200); while (!Serial2) { }; // GNSSとRTCの初期化 setupTimeSyncGNSS(); // LCD初期化 setupLCD(); // 起動時の画面表示 showBootScreen(); // センサー初期化 // pinMode(3, INPUT_PULLUP); // PIRセンサーを入力として設定 // Sprite初期化 sprite.createSprite(240, 320); // タッチセンサ初期化 setupTouch(); // SDカードの初期化 setupSD(); // 環境センサの初期化 setupIaqSensor(); // LocalStorageの初期化 storage.load(); alarmClock.setAlarmStatus(storage.get("alarmStatus", "0").toInt()); alarmClock.setAlarmHour(storage.get("alarmHour", "0").toInt()); alarmClock.setAlarmMinute(storage.get("alarmMinute", "0").toInt()); alarmClock.setAlarmSecond(storage.get("alarmSecond", "0").toInt()); // 1秒待つ delay(1000); // 起動時の画面を消す resetLcd(); // 画面の再点灯 turnOnDisplay(); // プレイリスト初期化 if (!player.isPlaylistExists("PLAYLIST/TRACK_DB.CSV")) { printf("Playlist does not exist. Generating...\n"); player.generatePlaylistCSV("MUSIC", "PLAYLIST/TRACK_DB.CSV"); } Serial.println("Setup done."); } // コマンドを解析する関数 void parseCommand(char *command) { char *cmd = strtok(command, ","); // 最初のトークン(コマンド名) if (cmd == NULL) return; // 無効なコマンドなら何もしない char *param1 = strtok(NULL, ","); // 1つ目のパラメータ char *param2 = strtok(NULL, ","); // 2つ目のパラメータ // コマンド処理 if (strcmp(cmd, "TRK_TITLE") == 0) { if (param1 != NULL) { Serial.println("Playing: " + String(param1)); Title = String(param1); scroller.setText(param1); } else { Title = ""; scroller.setText(""); } // String text = Artist + " - " + Title; // // 両方が空の場合は再生中の音楽無し // if (Artist == "" && Title == "") // { // text = "再生中の音楽無し"; // } } else if (strcmp(cmd, "TRK_ARTIST") == 0) { if (param1 != NULL) { Serial.println("Playing: " + String(param1)); Artist = String(param1); } else { Artist = ""; } // String text = Artist + " - " + Title; // // 両方が空の場合は再生中の音楽無し // if (Artist == "" && Title == "") // { // text = "再生中の音楽無し"; // } // scroller.setText(text.c_str()); } else if (strcmp(cmd, "ECHO") == 0) { if (param1 != NULL) { Serial.print("ECHO: "); Serial.println(param1); } } else { Serial.print("Unknown command: "); Serial.println(cmd); } } // メイン処理 ---------------------------------------- void loop() { touchPoint = getTouchPoint(); if (touchPoint.x != -1) { Serial.println("Touched: " + String(touchPoint.x) + ", " + String(touchPoint.y)); turnOnDisplay(); } else { // Serial.println("Not touched"); } // 測位後にオーディオ初期化 if (isInitializeAudio == false && isTimeGetOnce == true) { // MP3初期化 if (!player.begin()) { printf("Player initialization failed\n"); while (1) { } } // player.startBackgroundThread(); player.setVolume(80); // player.listTracks(); // player.toggleRandomMode(); player.toggleRepeatMode(); // player.play(); isInitializeAudio = true; } if (isTimeGetOnce == false) { turnOnDisplay(); } // シリアルデータを1バイト受信 while (Serial2.available()) { char c = Serial2.read(); // バッファに追加 if (c == '\n') { // 改行が来たらコマンド解析 inputBuffer[bufferIndex] = '\0'; // 文字列終端 parseCommand(inputBuffer); bufferIndex = 0; // バッファをリセット } else { if (bufferIndex < 1024 - 1) { inputBuffer[bufferIndex++] = c; } } } // オーディオ再生中なら if (player.isCurrentlyPlaying()) { turnOnDisplay(); } // Titleが空でない場合は再生中 if (player.isPlayPaththrough() && Title != "") { turnOnDisplay(); } // int sensorValue = digitalRead(3); // PIRセンサーの値を取得 // if (sensorValue == 1) // { // Serial.println("Motion detected!"); // }else{ // Serial.println("No motion detected."); // } // 一定時間が経過すると滑らかに減光する dimDisplayOverTime(); // センサーデータを取得 updateSensorData(); // アラームの状態を監視 alarmClock.checkAlarm(timeSync.getHourInt(), timeSync.getMinuteInt(), timeSync.getSecondInt()); // オフタイマーの状態を監視 if (musicTimer.isTimerFinished()) { Serial.println("Timer finished! Stopping music."); musicTimer.stopTimer(); // 音楽停止の処理をここに追加 if (player.isPlayPaththrough()) { player.toggleI2SPassthroughMode(); } else { player.stop(); } } // 画面描画 drawScreen(); // プレイヤーの状態を監視 player.stream(); } ``` ```Arduino:Atom lite用コード #include "AudioTools.h" #include "BluetoothA2DPSink.h" #include <Arduino.h> #include "ESP_I2S.h" #define BUFFER_SIZE 1024 // バッファサイズ // UART 通信ピン (例: TX=22, RX=19) static const int UART_TX_PIN = 23; static const int UART_RX_PIN = 33; static const uint32_t UART_BAUDRATE = 115200; const uint8_t I2S_SCK = 19; /* Audio data bit clock */ const uint8_t I2S_WS = 21; /* Audio data left and right clock */ const uint8_t I2S_SDOUT = 22; /* ESP32 audio data output (to speakers) */ // I2SStream i2s; I2SClass i2s; BluetoothA2DPSink a2dp_sink(i2s); uint8_t avrc_title_buffer[128]; uint8_t avrc_artist_buffer[128]; uint8_t avrc_album_buffer[128]; void avrc_metadata_callback(uint8_t data1, const uint8_t *data2) { // data1: attribute id // data2: attribute value // id 0x01: track title if (data1 == 0x01) { Serial.printf("Track title: %s\n", data2); avrc_title_buffer[0] = '\0'; strcpy((char *)avrc_title_buffer, (char *)data2); Serial2.printf("TRK_TITLE,%s\n", avrc_title_buffer); } // id 0x02: track artist else if (data1 == 0x02) { Serial.printf("Track artist: %s\n", data2); avrc_artist_buffer[0] = '\0'; strcpy((char *)avrc_artist_buffer, (char *)data2); // Serial2.printf("TRK_ARTIST,%s\n", avrc_artist_buffer); } // id 0x03: track album else if (data1 == 0x03) { Serial.printf("Track album: %s\n", data2); } else { Serial.printf("Unknown attribute id: 0x%x, %s\n", data1, data2); } } // コマンドを解析する関数 void parseCommand(char *command) { char *cmd = strtok(command, ","); // 最初のトークン(コマンド名) if (cmd == NULL) return; // 無効なコマンドなら何もしない char *param1 = strtok(NULL, ","); // 1つ目のパラメータ char *param2 = strtok(NULL, ","); // 2つ目のパラメータ // コマンド処理 if (strcmp(cmd, "AUD_CTRL") == 0) { if (a2dp_sink.get_audio_state() == ESP_A2D_AUDIO_STATE_STARTED) { Serial.println("AUD_CTRL"); if (strcmp(param2, "PLAY") == 0) { // a2dp_sink.play(); } else if (strcmp(param2, "PAUSE") == 0) { // a2dp_sink.pause(); } else if (strcmp(param2, "STOP") == 0) { a2dp_sink.stop(); // next } else if (strcmp(param2, "NEXT") == 0) { // a2dp_sink.next(); // previous } else if (strcmp(param2, "PREV") == 0) { // a2dp_sink.previous(); } // fast forward else if (strcmp(param2, "FF") == 0) { a2dp_sink.fast_forward(); } // rewind else if (strcmp(param2, "REW") == 0) { a2dp_sink.rewind(); } else { Serial.print("Unknown command: "); Serial.println(param2); } } } else if (strcmp(cmd, "ECHO") == 0) { if (param1 != NULL) { Serial.print("ECHO: "); Serial.println(param1); } } else { Serial.print("Unknown command: "); Serial.println(cmd); } } void setup() { Serial.begin(115200); // I2Sの初期化 // auto cfg = i2s.defaultConfig(); // cfg.pin_bck = I2S_SCK; // cfg.pin_ws = I2S_WS; // cfg.pin_data = I2S_SDOUT; // i2s.begin(cfg); i2s.setPins(I2S_SCK, I2S_WS, I2S_SDOUT); if (!i2s.begin(I2S_MODE_STD, 44100, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, I2S_STD_SLOT_BOTH)) { Serial.println("Failed to initialize I2S!"); while (1) ; // do nothing } a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback); a2dp_sink.start("SpresenseAlarmClock"); Serial.println("Bluetooth Sink started"); // UARTポート(例: Serial2) を初期化 Serial2.begin(UART_BAUDRATE, SERIAL_8N1, UART_RX_PIN, UART_TX_PIN); Serial.println("UART2 initialized."); } unsigned long lastSendTime = 0; const unsigned long sendInterval = 10000; // 送信間隔(ミリ秒) char inputBuffer[BUFFER_SIZE]; // 受信用バッファ int bufferIndex = 0; // バッファのインデックス void loop() { // シリアルデータを受信 while (Serial2.available()) { char c = Serial2.read(); // バッファに追加 if (c == '\n') { // 改行が来たらコマンド解析 inputBuffer[bufferIndex] = '\0'; // 文字列終端 Serial.println(inputBuffer); parseCommand(inputBuffer); bufferIndex = 0; // バッファをリセット } else { if (bufferIndex < BUFFER_SIZE - 1) { inputBuffer[bufferIndex++] = c; } } } if (millis() - lastSendTime >= sendInterval) { lastSendTime = millis(); // Serial2.println("TEST_COMMAND"); Serial2.printf("TRK_TITLE,%s\n", avrc_title_buffer); // Serial2.printf("TRK_ARTIST,%s\n", avrc_artist_buffer); Serial.printf("TRK_TITLE,%s\n", avrc_title_buffer); // Serial.printf("TRK_ARTIST,%s\n", avrc_artist_buffer); } } ``` # SDカードの中身 SDカードの中身は以下の構造に従う必要があります。 ```Bash:ファイル構造 |-- MicroSDカード(2GBくらいが推奨)/ |-- ALARM/ |-- Alarm.mp3(任意のファイルで良いが名前はAlarm.mp3の必要がある) |-- BIN/ - 公式ドキュメントに従ってDSPをインストールする必要がある |-- MP3DEC/ |-- MP3ENC/ |-- SRC/ |-- WAVDEC/ |-- MUSIC/ |-- ○○○.mp3 - 好きなmp3ファイルを入れる |-- PLAYLIST/ |-- TRACK_DB.CSV - 自動生成されるプレイリスト ``` # おわりに 今回、かなり多機能な目覚まし時計兼Bluetoothスピーカーを作ることが出来ました。特にGPS周りやオーディオ周りはあまり触れたことが無かったので、色々勉強になって楽しかったです。今後、何かを作るときにSpresenseを積極的に使いたいと思いました。今後の課題としては、現状GPSの感度が悪く時刻取得までかなり時間がかかるので、外部アンテナなどを検討したいです。また、今回は忙しくて間に合わなかったBluetoothオーディオ操作(AVRCPによる操作)の実装や、コントロールボックスへの物理インターフェース(ボタン)などの実装、照度センサーを使用した液晶のバックライトPWM制御などアイデアはたくさんあるので余力があれば今後も改善したいです。