diff --git a/firmware/NeoPixelController.cpp b/firmware/NeoPixelController.cpp index 5624ebb..4e77f5f 100644 --- a/firmware/NeoPixelController.cpp +++ b/firmware/NeoPixelController.cpp @@ -3,7 +3,9 @@ #include namespace { +// 今回は1個のNeoPixelを使います。 constexpr uint16_t kCount = 1; +// 音楽再生中に順番に表示する色です。値はRGBの順です。 constexpr uint32_t kColors[] = { 0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x8F00FF @@ -14,6 +16,7 @@ Adafruit_NeoPixel pixels(kCount, 0, NEO_GRB + NEO_KHZ800); } void NeoPixelController::begin(uint8_t pin) { + // LEDの接続先と明るさを設定し、停止中の色を表示します。 pixels.setPin(pin); pixels.begin(); pixels.setBrightness(255); @@ -25,17 +28,20 @@ void NeoPixelController::setPlaying(bool playing) { isPlaying_ = playing; if (isPlaying_) { + // 再生開始時刻を記録します。ここから色の変化を始めます。 transitionStart_ = millis(); lastUpdate_ = transitionStart_; return; } + // 停止中は、決めておいた色に戻します。 colorIndex_ = kStoppedColorIndex; pixels.setPixelColor(0, kColors[colorIndex_]); pixels.show(); } void NeoPixelController::update() { + // 再生中でない、または更新間隔が短すぎるときは何もしません。 if (!isPlaying_ || millis() - lastUpdate_ < kUpdateIntervalMs) { return; } @@ -44,6 +50,7 @@ void NeoPixelController::update() { lastUpdate_ = now; unsigned long transitionElapsed = now - transitionStart_; if (transitionElapsed >= kTransitionMs) { + // 色の切り替えが終わったら、次の色へ進みます。 colorIndex_ = (colorIndex_ + transitionElapsed / kTransitionMs) % kColorCount; transitionStart_ = now; transitionElapsed = 0; @@ -52,6 +59,7 @@ void NeoPixelController::update() { uint8_t nextColorIndex = (colorIndex_ + 1) % kColorCount; uint32_t currentColor = kColors[colorIndex_]; uint32_t nextColor = kColors[nextColorIndex]; + // 現在の色と次の色を混ぜることで、急に変わらず滑らかに変化させます。 uint8_t red = ((currentColor >> 16) * (kTransitionMs - transitionElapsed) + (nextColor >> 16) * transitionElapsed) / kTransitionMs; uint8_t green = (((currentColor >> 8) & 0xFF) * (kTransitionMs - transitionElapsed) diff --git a/firmware/NeoPixelController.h b/firmware/NeoPixelController.h index f091f81..29b4b20 100644 --- a/firmware/NeoPixelController.h +++ b/firmware/NeoPixelController.h @@ -4,15 +4,22 @@ class NeoPixelController { public: + // NeoPixelを使い始める準備をします。 void begin(uint8_t pin); + // 音楽再生中かどうかを設定します。 void setPlaying(bool playing); + // 時間の経過に合わせてLEDを更新します。 void update(); private: + // 音楽停止中に表示する色(配列の4番目、緑)です。 static constexpr uint8_t kStoppedColorIndex = 3; + // 色を計算する間隔です。短いほどなめらかになります。 static constexpr unsigned long kUpdateIntervalMs = 30; + // 1色から次の色へ変わる時間です。 static constexpr unsigned long kTransitionMs = 3000; + // 現在のLEDの状態を保存します。 bool isPlaying_ = false; uint8_t colorIndex_ = 0; unsigned long lastUpdate_ = 0; diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index c6958b3..ee9f35d 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -9,6 +9,8 @@ #include namespace { +// BLEサービスと、その中の2本の通信路を識別する番号です。 +// UUIDは、Bluetooth上で機能を区別するための長い名前です。 constexpr char kNusServiceUuid[] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusRxUuid[] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusTxUuid[] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; @@ -16,21 +18,25 @@ constexpr char kBleDeviceNamePrefix[] = "EnvLog"; constexpr int64_t kInitialYear = 1700; constexpr int64_t kSecondsPerDay = 86400; +// センサー本体と、センサーが使える状態かどうかを保存します。 Adafruit_AHTX0 aht20; BH1750 bh1750; bool aht20Ready = false; bool bh1750Ready = false; bool isLeapYear(int year) { + // 4年ごと。ただし100年ごとは除き、400年ごとはうるう年です。 return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } } +// BLEの受信側の処理です。アプリがデータを書き込むと呼ばれます。 class SensorLoggerCallbacks : public BLECharacteristicCallbacks { public: explicit SensorLoggerCallbacks(SensorLogger* logger) : logger_(logger) {} void onWrite(BLECharacteristic* characteristic) override { + // 受信した文字をSensorLoggerへ渡します。 logger_->receiveBluetoothData(characteristic->getValue().c_str()); } @@ -38,15 +44,18 @@ private: SensorLogger* logger_; }; +// BLEの接続と切断を知らせる処理です。 class SensorLoggerServerCallbacks : public BLEServerCallbacks { public: explicit SensorLoggerServerCallbacks(SensorLogger* logger) : logger_(logger) {} void onConnect(BLEServer*) override { + // 接続中は、画面に接続中であることを表示できます。 logger_->bluetoothConnected_ = true; } void onDisconnect(BLEServer* server) override { + // 切断されたら、次の接続を待つために広告を再開します。 logger_->bluetoothConnected_ = false; logger_->commandBuffer_ = ""; logger_->lastAdvertise_ = millis(); @@ -58,33 +67,41 @@ private: }; void SensorLogger::begin(uint8_t soilPin) { + // センサーとBLE通信を使い始めるための初期設定です。 soilPin_ = soilPin; pinMode(soilPin_, INPUT); analogReadResolution(12); + // センサーの初期化に成功したかを保存します。 aht20Ready = aht20.begin(); bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); + // ESP32固有の番号を名前に加え、同じ名前の機器と区別します。 uint64_t mac = ESP.getEfuseMac(); char name[14]; snprintf(name, sizeof(name), "%s-%05llX", kBleDeviceNamePrefix, mac & 0xFFFFF); bluetoothName_ = name; BLEDevice::init(bluetoothName_.c_str()); + // BLEサーバーと、センサー通信用のサービスを作ります。 server_ = BLEDevice::createServer(); server_->setCallbacks(new SensorLoggerServerCallbacks(this)); BLEService* service = server_->createService(kNusServiceUuid); + // RXはアプリからESP32へ命令を送る通路です。 BLECharacteristic* rxCharacteristic = service->createCharacteristic( kNusRxUuid, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); rxCharacteristic->setCallbacks(new SensorLoggerCallbacks(this)); + // TXはESP32からアプリへログを送る通路です。 txCharacteristic_ = service->createCharacteristic( kNusTxUuid, BLECharacteristic::PROPERTY_NOTIFY); txCharacteristic_->addDescriptor(new BLE2902()); service->start(); + // BLE広告は「この機器が近くにいます」と周囲へ知らせる仕組みです。 BLEAdvertising* advertising = BLEDevice::getAdvertising(); advertising->addServiceUUID(kNusServiceUuid); advertising->setScanResponse(true); + // A2DP開始後に共通のBluetooth名が変わっても、BLE名を返せるようにします。 BLEAdvertisementData scanResponseData; scanResponseData.setName(bluetoothName_.c_str()); advertising->setScanResponseData(scanResponseData); @@ -92,21 +109,25 @@ void SensorLogger::begin(uint8_t soilPin) { advertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); + // 起動直後にも1回計測して、画面に値を表示できるようにします。 readAndStore(); lastMeasure_ = millis(); lastAdvertise_ = millis(); } bool SensorLogger::update() { + // loopから繰り返し呼ばれ、通信と定期計測を担当します。 processBluetooth(); unsigned long now = millis(); if (!bluetoothConnected_ && now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) { + // 接続されていなければ、定期的にBLE広告を出し直します。 lastAdvertise_ = now; BLEDevice::startAdvertising(); } if (now - lastMeasure_ >= kMeasureIntervalMs) { + // 計測間隔を過ぎたら、新しいログを保存します。 lastMeasure_ = now; readAndStore(); return true; @@ -115,6 +136,7 @@ bool SensorLogger::update() { } void SensorLogger::refreshSensors() { + // センサーを読み直します。ログには保存せず、画面表示だけを更新します。 SensorReading reading = {}; sensors_event_t humidityEvent; sensors_event_t temperatureEvent; @@ -153,11 +175,13 @@ size_t SensorLogger::logCount() const { } bool SensorLogger::formatTimestamp(int64_t timestamp, char* buffer, size_t bufferSize) const { + // 秒数を、人が読みやすい「年/月/日 時:分:秒」に変換します。 if (bufferSize == 0) { return false; } if (timeSynchronized_) { + // 時刻合わせ済みなら、ESP32の標準時刻変換を使います。 time_t unixTimestamp = static_cast(timestamp); struct tm date; localtime_r(&unixTimestamp, &date); @@ -165,6 +189,7 @@ bool SensorLogger::formatTimestamp(int64_t timestamp, char* buffer, size_t buffe return true; } + // 時刻合わせ前は、起動してからの経過秒数を仮の日付として表示します。 int year = static_cast(kInitialYear); int month = 1; int day = 1; @@ -193,6 +218,7 @@ bool SensorLogger::getCurrentTime(char* buffer, size_t bufferSize) const { } void SensorLogger::readAndStore() { + // センサーを読み取り、最新値とログの両方へ保存します。 sensors_event_t humidityEvent; sensors_event_t temperatureEvent; SensorReading reading = {}; @@ -209,6 +235,7 @@ void SensorLogger::readAndStore() { reading.timestamp = currentTimestamp(); latest_ = reading; + // 配列が満杯になったら、一番古いデータから上書きします。 size_t writeIndex = (logStart_ + logCount_) % kMaxLogEntries; if (logCount_ == kMaxLogEntries) { log_[logStart_] = reading; @@ -220,6 +247,7 @@ void SensorLogger::readAndStore() { } void SensorLogger::processBluetooth() { + // 受信した文字を改行ごとの命令に分けて実行します。 while (commandBuffer_.indexOf('\n') >= 0 || commandBuffer_.indexOf('\r') >= 0) { int lineEnd = commandBuffer_.indexOf('\n'); int carriageReturn = commandBuffer_.indexOf('\r'); @@ -235,6 +263,7 @@ void SensorLogger::processBluetooth() { } void SensorLogger::receiveBluetoothData(const String& data) { + // BLEで届いた文字を命令バッファへ追加します。 for (size_t index = 0; index < data.length(); ++index) { char character = data[index]; if (character == '\n' || character == '\r') { @@ -246,6 +275,7 @@ void SensorLogger::receiveBluetoothData(const String& data) { } void SensorLogger::applyHistoricalTimeAdjustment(int64_t epoch) { + // 時刻合わせ前に保存したログへ、正しい時刻との差を加えます。 if (historicalAdjustmentApplied_ || timeSynchronized_) { return; } @@ -263,13 +293,17 @@ void SensorLogger::applyHistoricalTimeAdjustment(int64_t epoch) { } void SensorLogger::processCommand(const String& command) { + // アプリから届いた命令を判定します。 if (command == "GET_LOG") { + // 保存済みのログを順番に送ります。 sendLog(); } else if (command == "CLEAR_LOG") { + // ログを空にして、完了通知を送ります。 clearLog(); txCharacteristic_->setValue("OK_CLEARED\n"); txCharacteristic_->notify(); } else if (command.startsWith("TIME:")) { + // アプリから受け取った時刻で、時計を合わせます。 unsigned long epoch = command.substring(5).toInt(); unsigned int year; unsigned int month; @@ -302,6 +336,7 @@ void SensorLogger::processCommand(const String& command) { } void SensorLogger::sendLog() { + // 保存したログを1行ずつ作り、BLE通知で送ります。 for (size_t index = 0; index < logCount_; ++index) { size_t logIndex = (logStart_ + index) % kMaxLogEntries; const SensorReading& reading = log_[logIndex]; @@ -325,6 +360,7 @@ void SensorLogger::sendLog() { } void SensorLogger::sendNotification(const char* data) { + // BLE通知には一度に送れる長さの制限があるため、短く分割します。 constexpr size_t kNotificationSize = 20; uint8_t notification[kNotificationSize]; size_t length = strlen(data); @@ -338,11 +374,13 @@ void SensorLogger::sendNotification(const char* data) { } void SensorLogger::clearLog() { + // 配列の中身を消す代わりに、件数を0にして空として扱います。 logStart_ = 0; logCount_ = 0; } int64_t SensorLogger::currentTimestamp() const { + // 現在の経過秒数に時刻の差を足して、現在時刻を返します。 int64_t elapsedSeconds = millis() / 1000UL; return timeSynchronized_ ? epochOffset_ + elapsedSeconds : elapsedSeconds; } diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h index 8710819..b37274b 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -6,32 +6,45 @@ class BLECharacteristic; class BLEServer; struct SensorReading { - int64_t timestamp; - float temperature; - float humidity; - float illuminance; - uint16_t soilMoisture; + int64_t timestamp; // 計測した時刻(秒) + float temperature; // 温度(℃) + float humidity; // 湿度(%) + float illuminance; // 明るさ(lux) + uint16_t soilMoisture; // 土壌センサーの値 }; class SensorLogger { public: static constexpr size_t kMaxLogEntries = 2000; + // センサー、BLE、保存データを使い始める準備をします。 void begin(uint8_t soilPin); + // BLEの受信と、一定時間ごとの計測を行います。 bool update(); + // センサーを読み取り、画面表示用の最新値を更新します。 void refreshSensors(); + // 最後に読み取ったセンサー値を返します。 const SensorReading& latest() const; + // 1件以上のログが保存されているかを返します。 bool hasReading() const; + // 保存されているログの件数を返します。 size_t logCount() const; + // 現在時刻を文字列にしてbufferへ書き込みます。 bool getCurrentTime(char* buffer, size_t bufferSize) const; + // 秒数を年月日と時刻の文字列に変換します。 bool formatTimestamp(int64_t timestamp, char* buffer, size_t bufferSize) const; + // BLEで表示する名前を返します。 const char* bluetoothName() const; + // BLE機器が接続中かを返します。 bool isBluetoothConnected() const; private: + // センサーを読み取る間隔(現在は1時間)です。 static constexpr unsigned long kMeasureIntervalMs = 3600000UL; + // BLE接続がないとき、広告を再開する間隔です。 static constexpr unsigned long kAdvertiseRetryIntervalMs = 500UL; + // 内部処理用の関数です。 void readAndStore(); void processBluetooth(); void processCommand(const String& command); @@ -42,22 +55,22 @@ private: void applyHistoricalTimeAdjustment(int64_t epoch); int64_t currentTimestamp() const; - BLEServer* server_ = nullptr; - BLECharacteristic* txCharacteristic_ = nullptr; - SensorReading log_[kMaxLogEntries] = {}; - SensorReading latest_ = {}; - size_t logStart_ = 0; - size_t logCount_ = 0; - uint8_t soilPin_ = 0; - unsigned long lastMeasure_ = 0; - unsigned long lastAdvertise_ = 0; - int64_t epochOffset_ = 0; - int64_t historicalEpochOffset_ = 0; - bool timeSynchronized_ = false; - bool historicalAdjustmentApplied_ = false; - bool bluetoothConnected_ = false; - String bluetoothName_; - String commandBuffer_; + BLEServer* server_ = nullptr; // BLEサーバー本体 + BLECharacteristic* txCharacteristic_ = nullptr; // アプリへ通知する通路 + SensorReading log_[kMaxLogEntries] = {}; // 最大2000件の計測ログ + SensorReading latest_ = {}; // 最新の計測値 + size_t logStart_ = 0; // 一番古いログの位置 + size_t logCount_ = 0; // 現在のログ件数 + uint8_t soilPin_ = 0; // 土壌センサーを接続したGPIO番号 + unsigned long lastMeasure_ = 0; // 最後に計測した時刻 + unsigned long lastAdvertise_ = 0; // 最後にBLE広告を開始した時刻 + int64_t epochOffset_ = 0; // 現在時刻を合わせるための差 + int64_t historicalEpochOffset_ = 0; // 過去ログに加える時刻の差 + bool timeSynchronized_ = false; // 時刻合わせが済んだか + bool historicalAdjustmentApplied_ = false; // 過去ログの補正が済んだか + bool bluetoothConnected_ = false; // BLE接続中か + String bluetoothName_; // BLEに表示する名前 + String commandBuffer_; // BLEから受け取った未処理の文字 friend class SensorLoggerCallbacks; friend class SensorLoggerServerCallbacks; diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 164f065..d199cdd 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,30 +1,34 @@ +// A2DPは、スマートフォンから音楽を受信するBluetoothの仕組みです。 #include "BluetoothA2DPSink.h" #include #include #include "NeoPixelController.h" #include "SensorLogger.h" +// スマートフォンのBluetooth一覧に表示されるスピーカー名です。 const char kA2dpDeviceName[] = "BT_Speaker5.5"; -// I2C接続のOLED設定 (SSD1306 128x64) +// I2Cで接続した128x64ドットのOLED画面です。 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); +// このプログラムで使う機能の担当者を作ります。 BluetoothA2DPSink a2dp_sink; NeoPixelController neoPixelController; SensorLogger sensorLogger; -// 表示用変数 +// 音楽情報と画面更新に使う変数です。 String currentTitle = "未接続"; String currentArtist = ""; -int currentVolumePercent = 0; // 音量 (0 ~ 100%) +int currentVolumePercent = 0; // 音量(0〜100%) bool bluetoothConnected = false; bool musicPlaying = false; unsigned long lastDisplayUpdate = 0; -// 画面全体を再描画する関数 +// OLEDの内容を現在の状態に合わせて描き直します。 void updateDisplay() { u8g2.clearBuffer(); + // 音楽を再生していないときは、センサーの情報を表示します。 if (!bluetoothConnected || !musicPlaying) { u8g2.setFont(u8g2_font_6x10_tf); const SensorReading& reading = sensorLogger.latest(); @@ -61,42 +65,38 @@ void updateDisplay() { return; } - // -------------------------------------------------- - // 1. 音量表示エリア(最上部:英数フォント) - // -------------------------------------------------- + // 音楽を再生中は、音量と曲名を表示します。 + // 上の部分は小さい英数字フォントで音量を表示します。 u8g2.setFont(u8g2_font_6x10_tf); - // "Vol: 80%" のテキスト描画 + // 例: 「Vol: 80%」という文字を作って表示します。 char volStr[16]; snprintf(volStr, sizeof(volStr), "Vol: %3d%%", currentVolumePercent); u8g2.drawStr(0, 10, volStr); - // 音量バーの描画 - int barWidth = map(currentVolumePercent, 0, 100, 0, 50); // 50ピクセル幅に変換 - u8g2.drawFrame(75, 2, 52, 9); // 外枠 - u8g2.drawBox(76, 3, barWidth, 7); // バーの中身 + // 音量の割合を、0〜50ドットの長さに変換して棒グラフにします。 + int barWidth = map(currentVolumePercent, 0, 100, 0, 50); + u8g2.drawFrame(75, 2, 52, 9); // 棒グラフの外枠 + u8g2.drawBox(76, 3, barWidth, 7); // 音量に応じた中身 u8g2.drawLine(0, 13, 128, 13); // 区切り線 - // -------------------------------------------------- - // 2. 曲名・アーティスト表示エリア(日本語フォント) - // -------------------------------------------------- - // 日本語フォントに切り替え (美咲フォント UTF-8) - u8g2.setFont(u8g2_font_unifont_t_japanese1); + // 下の部分は、日本語を含む曲名とアーティスト名を表示します。 + u8g2.setFont(u8g2_font_unifont_t_japanese1); - // 曲名の表示 (y=32) + // 曲名を表示します。drawUTF8は日本語も扱える関数です。 u8g2.drawUTF8(0, 32, currentTitle.c_str()); - // アーティスト名の表示 (y=54) + // アーティスト名が空でないときだけ表示します。 if (currentArtist.length() > 0) { - // アーティスト名も日本語フォントのまま描画します u8g2.drawUTF8(0, 54, currentArtist.c_str()); } - u8g2.sendBuffer(); // 画面転送 + // ここまでの描画内容を、実際のOLED画面へ送ります。 + u8g2.sendBuffer(); } -// メタデータ(曲名・アーティスト名)の受信コールバック +// スマートフォンから曲名やアーティスト名を受け取ったときに呼ばれます。 void avrc_metadata_callback(uint8_t id, const uint8_t *text) { bool updated = false; @@ -113,7 +113,7 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) { } } -// 音量変更時のコールバック関数 +// スマートフォン側で音量が変わったときに呼ばれます。 void volume_changed_callback(int volume) { currentVolumePercent = map(volume, 0, 127, 0, 100); if (bluetoothConnected && musicPlaying) { @@ -122,12 +122,14 @@ void volume_changed_callback(int volume) { } void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { + // 音声が流れ始めたかどうかを保存し、LEDと画面を更新します。 musicPlaying = state == ESP_A2D_AUDIO_STATE_STARTED; neoPixelController.setPlaying(musicPlaying); updateDisplay(); } void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) { + // スマートフォンとの接続状態を保存します。 bluetoothConnected = state == ESP_A2D_CONNECTION_STATE_CONNECTED; if (!bluetoothConnected) { musicPlaying = false; @@ -136,15 +138,15 @@ void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) } void setup() { + // setupは、電源を入れた直後に1回だけ実行されます。 Serial.begin(115200); neoPixelController.begin(18); - // SDAをGPIO 19、SCLをGPIO 32 に設定 - // (配線に合わせて Wire.begin(SDA_PIN, SCL_PIN) の順で指定します) + // I2Cの配線を設定します。Wire.begin(SDA, SCL)の順です。 Wire.begin(21, 22); - // OLED初期化 + // OLEDを使える状態にします。 u8g2.begin(); u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化 @@ -152,7 +154,8 @@ void setup() { updateDisplay(); lastDisplayUpdate = millis(); - // I2S ピン設定 (BCK: 27, WS: 25, DOUT: 26) + // I2Sは、Bluetooth音声をアンプへ送るための通信方式です。 + // BCK=27、WS=25、音声データ出力=DOUT=26に配線しています。 i2s_pin_config_t my_pin_config = { .bck_io_num = 27, .ws_io_num = 25, @@ -162,17 +165,18 @@ void setup() { a2dp_sink.set_pin_config(my_pin_config); - // コールバック登録 + // Bluetoothでイベントが起きたときの通知先を登録します。 a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback); a2dp_sink.set_on_volumechange(volume_changed_callback); a2dp_sink.set_on_audio_state_changed(audio_state_changed_callback); a2dp_sink.set_on_connection_state_changed(connection_state_changed_callback); - // Bluetooth起動 + // Bluetoothスピーカーとして起動します。 a2dp_sink.start(kA2dpDeviceName); } void loop() { + // loopは何度も繰り返し実行されます。 neoPixelController.update(); bool newReading = sensorLogger.update(); unsigned long now = millis();