From 37dd61b229225edc691763fd95085cd660b62633 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Mon, 17 Aug 2026 20:20:21 +0900 Subject: [PATCH 01/18] WS2812B --- firmware/firmware.ino | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index c82f9c3..c242f17 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,6 +1,12 @@ #include "BluetoothA2DPSink.h" #include #include +#include + +// WS2812B設定 +constexpr uint8_t WS2812B_PIN = 13; +constexpr uint16_t WS2812B_COUNT = 1; +Adafruit_NeoPixel ws2812b(WS2812B_COUNT, WS2812B_PIN, NEO_GRB + NEO_KHZ800); // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -77,6 +83,12 @@ void volume_changed_callback(int volume) { void setup() { Serial.begin(115200); + // WS2812Bを緑で点灯 + ws2812b.begin(); + ws2812b.setBrightness(255); + ws2812b.setPixelColor(0, ws2812b.Color(0, 255, 0)); + ws2812b.show(); + // SDAをGPIO 19、SCLをGPIO 32 に設定 // (配線に合わせて Wire.begin(SDA_PIN, SCL_PIN) の順で指定します) Wire.begin(19, 5); From 9f28dc5d6915d82df6634cc47045457484809fc6 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Mon, 17 Aug 2026 20:57:25 +0900 Subject: [PATCH 02/18] =?UTF-8?q?7=E8=89=B2=E5=A4=89=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index c242f17..e1a465a 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -4,9 +4,18 @@ #include // WS2812B設定 -constexpr uint8_t WS2812B_PIN = 13; +constexpr uint8_t WS2812B_PIN = 16; constexpr uint16_t WS2812B_COUNT = 1; Adafruit_NeoPixel ws2812b(WS2812B_COUNT, WS2812B_PIN, NEO_GRB + NEO_KHZ800); +const uint32_t ws2812bColors[] = { + 0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, + 0x0000FF, 0x4B0082, 0x8F00FF +}; +constexpr uint8_t WS2812B_COLOR_COUNT = sizeof(ws2812bColors) / sizeof(ws2812bColors[0]); +constexpr unsigned long WS2812B_INTERVAL_MS = 500; +bool isPlaying = false; +uint8_t ws2812bColorIndex = 0; +unsigned long lastWs2812bUpdate = 0; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -80,13 +89,23 @@ void volume_changed_callback(int volume) { updateDisplay(); } +void audio_state_changed_callback(esp_a2d_audio_state_t state) { + isPlaying = (state == ESP_A2D_AUDIO_STATE_STARTED); + + if (!isPlaying) { + ws2812bColorIndex = 3; + ws2812b.setPixelColor(0, ws2812bColors[ws2812bColorIndex]); + ws2812b.show(); + } +} + void setup() { Serial.begin(115200); // WS2812Bを緑で点灯 ws2812b.begin(); ws2812b.setBrightness(255); - ws2812b.setPixelColor(0, ws2812b.Color(0, 255, 0)); + ws2812b.setPixelColor(0, ws2812bColors[3]); ws2812b.show(); // SDAをGPIO 19、SCLをGPIO 32 に設定 @@ -100,7 +119,7 @@ void setup() { // 初期画面 u8g2.clearBuffer(); u8g2.setFont(u8g2_font_unifont_t_japanese1); - u8g2.drawUTF8(0, 20, "BTスピーカー5.2"); + u8g2.drawUTF8(0, 20, "BTスピーカー5.3"); u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0, 40, "Ready..."); u8g2.sendBuffer(); @@ -118,10 +137,17 @@ void setup() { // コールバック登録 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); // Bluetooth起動 - a2dp_sink.start("BT_Speaker5.2"); + a2dp_sink.start("BT_Speaker5.3"); } void loop() { + if (isPlaying && millis() - lastWs2812bUpdate >= WS2812B_INTERVAL_MS) { + lastWs2812bUpdate = millis(); + ws2812bColorIndex = (ws2812bColorIndex + 1) % WS2812B_COLOR_COUNT; + ws2812b.setPixelColor(0, ws2812bColors[ws2812bColorIndex]); + ws2812b.show(); + } } \ No newline at end of file From b41e20f5a252f39d611e01766964af6ed3744609 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Mon, 17 Aug 2026 21:15:10 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=E5=A4=89=E5=8C=96=E3=82=92=E3=82=86?= =?UTF-8?q?=E3=81=A3=E3=81=8F=E3=82=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index e1a465a..f0cd2a1 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -12,10 +12,12 @@ const uint32_t ws2812bColors[] = { 0x0000FF, 0x4B0082, 0x8F00FF }; constexpr uint8_t WS2812B_COLOR_COUNT = sizeof(ws2812bColors) / sizeof(ws2812bColors[0]); -constexpr unsigned long WS2812B_INTERVAL_MS = 500; +constexpr unsigned long WS2812B_INTERVAL_MS = 30; +constexpr unsigned long WS2812B_TRANSITION_MS = 3000; bool isPlaying = false; uint8_t ws2812bColorIndex = 0; unsigned long lastWs2812bUpdate = 0; +unsigned long ws2812bTransitionStart = 0; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -89,9 +91,14 @@ void volume_changed_callback(int volume) { updateDisplay(); } -void audio_state_changed_callback(esp_a2d_audio_state_t state) { +void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { isPlaying = (state == ESP_A2D_AUDIO_STATE_STARTED); + if (isPlaying) { + ws2812bTransitionStart = millis(); + lastWs2812bUpdate = ws2812bTransitionStart; + } + if (!isPlaying) { ws2812bColorIndex = 3; ws2812b.setPixelColor(0, ws2812bColors[ws2812bColorIndex]); @@ -145,9 +152,26 @@ void setup() { void loop() { if (isPlaying && millis() - lastWs2812bUpdate >= WS2812B_INTERVAL_MS) { - lastWs2812bUpdate = millis(); - ws2812bColorIndex = (ws2812bColorIndex + 1) % WS2812B_COLOR_COUNT; - ws2812b.setPixelColor(0, ws2812bColors[ws2812bColorIndex]); + unsigned long now = millis(); + lastWs2812bUpdate = now; + unsigned long transitionElapsed = now - ws2812bTransitionStart; + if (transitionElapsed >= WS2812B_TRANSITION_MS) { + ws2812bColorIndex = (ws2812bColorIndex + transitionElapsed / WS2812B_TRANSITION_MS) + % WS2812B_COLOR_COUNT; + ws2812bTransitionStart = now; + transitionElapsed = 0; + } + uint8_t nextColorIndex = (ws2812bColorIndex + 1) % WS2812B_COLOR_COUNT; + uint32_t currentColor = ws2812bColors[ws2812bColorIndex]; + uint32_t nextColor = ws2812bColors[nextColorIndex]; + uint8_t red = ((currentColor >> 16) * (WS2812B_TRANSITION_MS - transitionElapsed) + + (nextColor >> 16) * transitionElapsed) / WS2812B_TRANSITION_MS; + uint8_t green = (((currentColor >> 8) & 0xFF) * (WS2812B_TRANSITION_MS - transitionElapsed) + + ((nextColor >> 8) & 0xFF) * transitionElapsed) / WS2812B_TRANSITION_MS; + uint8_t blue = ((currentColor & 0xFF) * (WS2812B_TRANSITION_MS - transitionElapsed) + + (nextColor & 0xFF) * transitionElapsed) / WS2812B_TRANSITION_MS; + ws2812b.setPixelColor(0, red, green, blue); ws2812b.show(); + } } \ No newline at end of file From 38893368293e17e1abc24b8d6bc5a8c57f1c1a18 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Thu, 3 Sep 2026 09:39:58 +0900 Subject: [PATCH 04/18] =?UTF-8?q?PIN=E7=95=AA=E5=8F=B7=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index f0cd2a1..ffbb598 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -4,7 +4,7 @@ #include // WS2812B設定 -constexpr uint8_t WS2812B_PIN = 16; +constexpr uint8_t WS2812B_PIN = 18; constexpr uint16_t WS2812B_COUNT = 1; Adafruit_NeoPixel ws2812b(WS2812B_COUNT, WS2812B_PIN, NEO_GRB + NEO_KHZ800); const uint32_t ws2812bColors[] = { @@ -117,7 +117,7 @@ void setup() { // SDAをGPIO 19、SCLをGPIO 32 に設定 // (配線に合わせて Wire.begin(SDA_PIN, SCL_PIN) の順で指定します) - Wire.begin(19, 5); + Wire.begin(21, 22); // OLED初期化 u8g2.begin(); From 48cf4020c31189a781abdcd1d19b907373ed6000 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Thu, 3 Sep 2026 09:55:45 +0900 Subject: [PATCH 05/18] =?UTF-8?q?NeoPixcel=20=E3=83=A2=E3=82=B8=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/NeoPixelController.cpp | 63 +++++++++++++++++++++++++++++++++ firmware/NeoPixelController.h | 20 +++++++++++ firmware/firmware.ino | 62 +++----------------------------- 3 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 firmware/NeoPixelController.cpp create mode 100644 firmware/NeoPixelController.h diff --git a/firmware/NeoPixelController.cpp b/firmware/NeoPixelController.cpp new file mode 100644 index 0000000..b67c73a --- /dev/null +++ b/firmware/NeoPixelController.cpp @@ -0,0 +1,63 @@ +#include "NeoPixelController.h" + +#include + +namespace { +constexpr uint8_t kPin = 18; +constexpr uint16_t kCount = 1; +constexpr uint32_t kColors[] = { + 0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, + 0x0000FF, 0x4B0082, 0x8F00FF +}; +constexpr uint8_t kColorCount = sizeof(kColors) / sizeof(kColors[0]); + +Adafruit_NeoPixel pixels(kCount, kPin, NEO_GRB + NEO_KHZ800); +} + +void NeoPixelController::begin() { + pixels.begin(); + pixels.setBrightness(255); + pixels.setPixelColor(0, kColors[kStoppedColorIndex]); + pixels.show(); +} + +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; + } + + unsigned long now = millis(); + lastUpdate_ = now; + unsigned long transitionElapsed = now - transitionStart_; + if (transitionElapsed >= kTransitionMs) { + colorIndex_ = (colorIndex_ + transitionElapsed / kTransitionMs) % kColorCount; + transitionStart_ = now; + transitionElapsed = 0; + } + + 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) + + ((nextColor >> 8) & 0xFF) * transitionElapsed) / kTransitionMs; + uint8_t blue = ((currentColor & 0xFF) * (kTransitionMs - transitionElapsed) + + (nextColor & 0xFF) * transitionElapsed) / kTransitionMs; + pixels.setPixelColor(0, red, green, blue); + pixels.show(); +} \ No newline at end of file diff --git a/firmware/NeoPixelController.h b/firmware/NeoPixelController.h new file mode 100644 index 0000000..d9004f4 --- /dev/null +++ b/firmware/NeoPixelController.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class NeoPixelController { +public: + void begin(); + void setPlaying(bool playing); + void update(); + +private: + static constexpr uint8_t kStoppedColorIndex = 3; + static constexpr unsigned long kUpdateIntervalMs = 30; + static constexpr unsigned long kTransitionMs = 3000; + + bool isPlaying_ = false; + uint8_t colorIndex_ = 0; + unsigned long lastUpdate_ = 0; + unsigned long transitionStart_ = 0; +}; \ No newline at end of file diff --git a/firmware/firmware.ino b/firmware/firmware.ino index ffbb598..d7f224f 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,28 +1,13 @@ #include "BluetoothA2DPSink.h" #include #include -#include - -// WS2812B設定 -constexpr uint8_t WS2812B_PIN = 18; -constexpr uint16_t WS2812B_COUNT = 1; -Adafruit_NeoPixel ws2812b(WS2812B_COUNT, WS2812B_PIN, NEO_GRB + NEO_KHZ800); -const uint32_t ws2812bColors[] = { - 0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, - 0x0000FF, 0x4B0082, 0x8F00FF -}; -constexpr uint8_t WS2812B_COLOR_COUNT = sizeof(ws2812bColors) / sizeof(ws2812bColors[0]); -constexpr unsigned long WS2812B_INTERVAL_MS = 30; -constexpr unsigned long WS2812B_TRANSITION_MS = 3000; -bool isPlaying = false; -uint8_t ws2812bColorIndex = 0; -unsigned long lastWs2812bUpdate = 0; -unsigned long ws2812bTransitionStart = 0; +#include "NeoPixelController.h" // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); BluetoothA2DPSink a2dp_sink; +NeoPixelController neoPixelController; // 表示用変数 String currentTitle = "未接続"; @@ -92,28 +77,13 @@ void volume_changed_callback(int volume) { } void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { - isPlaying = (state == ESP_A2D_AUDIO_STATE_STARTED); - - if (isPlaying) { - ws2812bTransitionStart = millis(); - lastWs2812bUpdate = ws2812bTransitionStart; - } - - if (!isPlaying) { - ws2812bColorIndex = 3; - ws2812b.setPixelColor(0, ws2812bColors[ws2812bColorIndex]); - ws2812b.show(); - } + neoPixelController.setPlaying(state == ESP_A2D_AUDIO_STATE_STARTED); } void setup() { Serial.begin(115200); - // WS2812Bを緑で点灯 - ws2812b.begin(); - ws2812b.setBrightness(255); - ws2812b.setPixelColor(0, ws2812bColors[3]); - ws2812b.show(); + neoPixelController.begin(); // SDAをGPIO 19、SCLをGPIO 32 に設定 // (配線に合わせて Wire.begin(SDA_PIN, SCL_PIN) の順で指定します) @@ -151,27 +121,5 @@ void setup() { } void loop() { - if (isPlaying && millis() - lastWs2812bUpdate >= WS2812B_INTERVAL_MS) { - unsigned long now = millis(); - lastWs2812bUpdate = now; - unsigned long transitionElapsed = now - ws2812bTransitionStart; - if (transitionElapsed >= WS2812B_TRANSITION_MS) { - ws2812bColorIndex = (ws2812bColorIndex + transitionElapsed / WS2812B_TRANSITION_MS) - % WS2812B_COLOR_COUNT; - ws2812bTransitionStart = now; - transitionElapsed = 0; - } - uint8_t nextColorIndex = (ws2812bColorIndex + 1) % WS2812B_COLOR_COUNT; - uint32_t currentColor = ws2812bColors[ws2812bColorIndex]; - uint32_t nextColor = ws2812bColors[nextColorIndex]; - uint8_t red = ((currentColor >> 16) * (WS2812B_TRANSITION_MS - transitionElapsed) - + (nextColor >> 16) * transitionElapsed) / WS2812B_TRANSITION_MS; - uint8_t green = (((currentColor >> 8) & 0xFF) * (WS2812B_TRANSITION_MS - transitionElapsed) - + ((nextColor >> 8) & 0xFF) * transitionElapsed) / WS2812B_TRANSITION_MS; - uint8_t blue = ((currentColor & 0xFF) * (WS2812B_TRANSITION_MS - transitionElapsed) - + (nextColor & 0xFF) * transitionElapsed) / WS2812B_TRANSITION_MS; - ws2812b.setPixelColor(0, red, green, blue); - ws2812b.show(); - - } + neoPixelController.update(); } \ No newline at end of file From 37e6a14cf7fdcb9699eff86edd279e268b49e169 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 14:59:31 +0900 Subject: [PATCH 06/18] =?UTF-8?q?PIN=E7=95=AA=E5=8F=B7=E3=81=A8=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E6=8C=87=E5=AE=9A=E4=BD=8D=E7=BD=AE=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/NeoPixelController.cpp | 6 +++--- firmware/NeoPixelController.h | 2 +- firmware/firmware.ino | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/firmware/NeoPixelController.cpp b/firmware/NeoPixelController.cpp index b67c73a..5624ebb 100644 --- a/firmware/NeoPixelController.cpp +++ b/firmware/NeoPixelController.cpp @@ -3,7 +3,6 @@ #include namespace { -constexpr uint8_t kPin = 18; constexpr uint16_t kCount = 1; constexpr uint32_t kColors[] = { 0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, @@ -11,10 +10,11 @@ constexpr uint32_t kColors[] = { }; constexpr uint8_t kColorCount = sizeof(kColors) / sizeof(kColors[0]); -Adafruit_NeoPixel pixels(kCount, kPin, NEO_GRB + NEO_KHZ800); +Adafruit_NeoPixel pixels(kCount, 0, NEO_GRB + NEO_KHZ800); } -void NeoPixelController::begin() { +void NeoPixelController::begin(uint8_t pin) { + pixels.setPin(pin); pixels.begin(); pixels.setBrightness(255); pixels.setPixelColor(0, kColors[kStoppedColorIndex]); diff --git a/firmware/NeoPixelController.h b/firmware/NeoPixelController.h index d9004f4..f091f81 100644 --- a/firmware/NeoPixelController.h +++ b/firmware/NeoPixelController.h @@ -4,7 +4,7 @@ class NeoPixelController { public: - void begin(); + void begin(uint8_t pin); void setPlaying(bool playing); void update(); diff --git a/firmware/firmware.ino b/firmware/firmware.ino index d7f224f..b311f48 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -3,6 +3,8 @@ #include #include "NeoPixelController.h" +const char kBluetoothName[] = "BT_Speaker5.31"; + // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -83,7 +85,7 @@ void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { void setup() { Serial.begin(115200); - neoPixelController.begin(); + neoPixelController.begin(18); // SDAをGPIO 19、SCLをGPIO 32 に設定 // (配線に合わせて Wire.begin(SDA_PIN, SCL_PIN) の順で指定します) @@ -96,7 +98,7 @@ void setup() { // 初期画面 u8g2.clearBuffer(); u8g2.setFont(u8g2_font_unifont_t_japanese1); - u8g2.drawUTF8(0, 20, "BTスピーカー5.3"); + u8g2.drawUTF8(0, 20, kBluetoothName); u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0, 40, "Ready..."); u8g2.sendBuffer(); @@ -117,7 +119,7 @@ void setup() { a2dp_sink.set_on_audio_state_changed(audio_state_changed_callback); // Bluetooth起動 - a2dp_sink.start("BT_Speaker5.3"); + a2dp_sink.start(kBluetoothName); } void loop() { From 05afe2f4391a7a80bd1f9894092b93b3264b47d6 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 15:34:42 +0900 Subject: [PATCH 07/18] =?UTF-8?q?=E3=82=BB=E3=83=B3=E3=82=B5=E3=83=BC?= =?UTF-8?q?=E5=80=A4=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 89 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index b311f48..067fc2c 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,24 +1,62 @@ #include "BluetoothA2DPSink.h" #include #include +#include +#include #include "NeoPixelController.h" -const char kBluetoothName[] = "BT_Speaker5.31"; +const char kBluetoothName[] = "BT_Speaker5.32"; +constexpr uint8_t kSoilMoisturePin = 32; +constexpr unsigned long kSensorUpdateIntervalMs = 1000; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); BluetoothA2DPSink a2dp_sink; NeoPixelController neoPixelController; +Adafruit_AHTX0 aht20; +BH1750 bh1750; // 表示用変数 String currentTitle = "未接続"; String currentArtist = ""; int currentVolumePercent = 0; // 音量 (0 ~ 100%) +bool bluetoothConnected = false; +bool aht20Ready = false; +bool bh1750Ready = false; +unsigned long lastSensorUpdate = 0; +float temperature = 0.0f; +float humidity = 0.0f; +float illuminance = 0.0f; +int soilMoisture = 0; // 画面全体を再描画する関数 void updateDisplay() { u8g2.clearBuffer(); + + if (!bluetoothConnected) { + u8g2.setFont(u8g2_font_6x10_tf); + + char sensorLine[32]; + if (aht20Ready) { + snprintf(sensorLine, sizeof(sensorLine), "T:%5.1fC H:%5.1f%%", temperature, humidity); + } else { + snprintf(sensorLine, sizeof(sensorLine), "AHT20: error"); + } + u8g2.drawStr(0, 12, sensorLine); + + if (bh1750Ready) { + snprintf(sensorLine, sizeof(sensorLine), "Light: %6.1f lx", illuminance); + } else { + snprintf(sensorLine, sizeof(sensorLine), "BH1750: error"); + } + u8g2.drawStr(0, 30, sensorLine); + + snprintf(sensorLine, sizeof(sensorLine), "Soil: %4d", soilMoisture); + u8g2.drawStr(0, 48, sensorLine); + u8g2.sendBuffer(); + return; + } // -------------------------------------------------- // 1. 音量表示エリア(最上部:英数フォント) @@ -67,7 +105,7 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) { updated = true; } - if (updated) { + if (updated && bluetoothConnected) { updateDisplay(); } } @@ -75,13 +113,38 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) { // 音量変更時のコールバック関数 void volume_changed_callback(int volume) { currentVolumePercent = map(volume, 0, 127, 0, 100); - updateDisplay(); + if (bluetoothConnected) { + updateDisplay(); + } } void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { neoPixelController.setPlaying(state == ESP_A2D_AUDIO_STATE_STARTED); } +void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) { + bluetoothConnected = state == ESP_A2D_CONNECTION_STATE_CONNECTED; + updateDisplay(); +} + +void updateSensorDisplay() { + sensors_event_t humidityEvent; + sensors_event_t temperatureEvent; + + if (aht20Ready) { + aht20.getEvent(&humidityEvent, &temperatureEvent); + temperature = temperatureEvent.temperature; + humidity = humidityEvent.relative_humidity; + } + + if (bh1750Ready) { + illuminance = bh1750.readLightLevel(); + } + + soilMoisture = analogRead(kSoilMoisturePin); + updateDisplay(); +} + void setup() { Serial.begin(115200); @@ -95,13 +158,12 @@ void setup() { u8g2.begin(); u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化 - // 初期画面 - u8g2.clearBuffer(); - u8g2.setFont(u8g2_font_unifont_t_japanese1); - u8g2.drawUTF8(0, 20, kBluetoothName); - u8g2.setFont(u8g2_font_6x10_tf); - u8g2.drawStr(0, 40, "Ready..."); - u8g2.sendBuffer(); + aht20Ready = aht20.begin(); + bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); + analogReadResolution(12); + + updateSensorDisplay(); + lastSensorUpdate = millis(); // I2S ピン設定 (BCK: 27, WS: 25, DOUT: 26) i2s_pin_config_t my_pin_config = { @@ -117,6 +179,7 @@ void setup() { 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起動 a2dp_sink.start(kBluetoothName); @@ -124,4 +187,10 @@ void setup() { void loop() { neoPixelController.update(); + + unsigned long now = millis(); + if (!bluetoothConnected && now - lastSensorUpdate >= kSensorUpdateIntervalMs) { + lastSensorUpdate = now; + updateSensorDisplay(); + } } \ No newline at end of file From 3279c296a0977f0cb9b986a944604db45ad5e3d4 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 16:13:03 +0900 Subject: [PATCH 08/18] =?UTF-8?q?BT=E3=82=B5=E3=83=9D=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 5 ++ firmware/SensorLogger.cpp | 149 ++++++++++++++++++++++++++++++++++++ firmware/SensorLogger.h | 44 +++++++++++ firmware/firmware.ino | 64 +++------------- 4 files changed, 209 insertions(+), 53 deletions(-) create mode 100644 firmware/SensorLogger.cpp create mode 100644 firmware/SensorLogger.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4cf9ec..f3a5159 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,11 @@ jobs: - name: Install U8g2 library run: arduino-cli lib install "U8g2" + - name: Install sensor libraries + run: | + arduino-cli lib install "Adafruit AHTX0" + arduino-cli lib install "BH1750" + # コンパイル実行 (output-dir を指定して構成ファイルを確実に生成) - name: Compile Sketch run: | diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp new file mode 100644 index 0000000..765094c --- /dev/null +++ b/firmware/SensorLogger.cpp @@ -0,0 +1,149 @@ +#include "SensorLogger.h" + +#include +#include +#include + +namespace { +Adafruit_AHTX0 aht20; +BH1750 bh1750; +bool aht20Ready = false; +bool bh1750Ready = false; +} + +void SensorLogger::begin(uint8_t soilPin, const char* bluetoothName) { + soilPin_ = soilPin; + bluetoothName_ = bluetoothName; + pinMode(soilPin_, INPUT); + analogReadResolution(12); + + aht20Ready = aht20.begin(); + bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); + + bluetooth_.begin(bluetoothName); + readAndStore(); + lastMeasure_ = millis(); +} + +bool SensorLogger::update() { + processBluetooth(); + + unsigned long now = millis(); + if (now - lastMeasure_ >= kMeasureIntervalMs) { + lastMeasure_ = now; + readAndStore(); + return true; + } + return false; +} + +const SensorReading& SensorLogger::latest() const { + return latest_; +} + +bool SensorLogger::hasReading() const { + return logCount_ != 0; +} + +void SensorLogger::readAndStore() { + sensors_event_t humidityEvent; + sensors_event_t temperatureEvent; + SensorReading reading = {}; + + if (aht20Ready) { + aht20.getEvent(&humidityEvent, &temperatureEvent); + reading.temperature = temperatureEvent.temperature; + reading.humidity = humidityEvent.relative_humidity; + } + if (bh1750Ready) { + reading.illuminance = bh1750.readLightLevel(); + } + reading.soilMoisture = analogRead(soilPin_); + reading.timestamp = currentTimestamp(); + latest_ = reading; + + size_t writeIndex = (logStart_ + logCount_) % kMaxLogEntries; + if (logCount_ == kMaxLogEntries) { + log_[logStart_] = reading; + logStart_ = (logStart_ + 1) % kMaxLogEntries; + } else { + log_[writeIndex] = reading; + ++logCount_; + } +} + +void SensorLogger::processBluetooth() { + while (bluetooth_.available()) { + char character = static_cast(bluetooth_.read()); + if (character == '\n' || character == '\r') { + if (commandBuffer_.length() != 0) { + processCommand(commandBuffer_); + commandBuffer_ = ""; + } + } else if (commandBuffer_.length() < 64) { + commandBuffer_ += character; + } + } +} + +void SensorLogger::processCommand(const String& command) { + if (command == "GET_LOG") { + sendLog(); + } else if (command == "CLEAR_LOG") { + clearLog(); + bluetooth_.println("OK_CLEARED"); + } else if (command.startsWith("TIME:")) { + unsigned long epoch = command.substring(5).toInt(); + unsigned int year; + unsigned int month; + unsigned int day; + unsigned int hour; + unsigned int minute; + unsigned int second; + if (sscanf(command.c_str(), "TIME:[%u,%u,%u,%u,%u,%u]", + &year, &month, &day, &hour, &minute, &second) == 6) { + struct tm date = {}; + date.tm_year = year - 1900; + date.tm_mon = month - 1; + date.tm_mday = day; + date.tm_hour = hour; + date.tm_min = minute; + date.tm_sec = second; + epoch = static_cast(mktime(&date)); + } + if (epoch != 0) { + epochOffset_ = epoch - millis() / 1000UL; + timeSynchronized_ = true; + bluetooth_.println("OK_TIME"); + } else { + bluetooth_.println("ERROR_TIME"); + } + } +} + +void SensorLogger::sendLog() { + bluetooth_.println("timestamp,temp,humid,light,soil,device"); + for (size_t index = 0; index < logCount_; ++index) { + size_t logIndex = (logStart_ + index) % kMaxLogEntries; + const SensorReading& reading = log_[logIndex]; + bluetooth_.printf("%lu,%.2f,%.2f,%.2f,%u,%s\n", + reading.timestamp, + reading.temperature, + reading.humidity, + reading.illuminance, + reading.soilMoisture, + bluetoothName_); + delay(20); + } + bluetooth_.println("END_LOG"); +} + +void SensorLogger::clearLog() { + logStart_ = 0; + logCount_ = 0; +} + +unsigned long SensorLogger::currentTimestamp() const { + unsigned long elapsedSeconds = millis() / 1000UL; + return timeSynchronized_ ? epochOffset_ + elapsedSeconds : elapsedSeconds; +} diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h new file mode 100644 index 0000000..482cd3a --- /dev/null +++ b/firmware/SensorLogger.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +struct SensorReading { + unsigned long timestamp; + float temperature; + float humidity; + float illuminance; + uint16_t soilMoisture; +}; + +class SensorLogger { +public: + static constexpr size_t kMaxLogEntries = 2000; + + void begin(uint8_t soilPin, const char* bluetoothName); + bool update(); + const SensorReading& latest() const; + bool hasReading() const; + +private: + static constexpr unsigned long kMeasureIntervalMs = 3600000UL; + + void readAndStore(); + void processBluetooth(); + void processCommand(const String& command); + void sendLog(); + void clearLog(); + unsigned long currentTimestamp() const; + + BluetoothSerial bluetooth_; + SensorReading log_[kMaxLogEntries] = {}; + SensorReading latest_ = {}; + size_t logStart_ = 0; + size_t logCount_ = 0; + uint8_t soilPin_ = 0; + unsigned long lastMeasure_ = 0; + unsigned long epochOffset_ = 0; + bool timeSynchronized_ = false; + const char* bluetoothName_ = ""; + String commandBuffer_; +}; diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 067fc2c..5dab087 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,34 +1,23 @@ #include "BluetoothA2DPSink.h" #include #include -#include -#include #include "NeoPixelController.h" +#include "SensorLogger.h" const char kBluetoothName[] = "BT_Speaker5.32"; -constexpr uint8_t kSoilMoisturePin = 32; -constexpr unsigned long kSensorUpdateIntervalMs = 1000; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); BluetoothA2DPSink a2dp_sink; NeoPixelController neoPixelController; -Adafruit_AHTX0 aht20; -BH1750 bh1750; +SensorLogger sensorLogger; // 表示用変数 String currentTitle = "未接続"; String currentArtist = ""; int currentVolumePercent = 0; // 音量 (0 ~ 100%) bool bluetoothConnected = false; -bool aht20Ready = false; -bool bh1750Ready = false; -unsigned long lastSensorUpdate = 0; -float temperature = 0.0f; -float humidity = 0.0f; -float illuminance = 0.0f; -int soilMoisture = 0; // 画面全体を再描画する関数 void updateDisplay() { @@ -36,23 +25,17 @@ void updateDisplay() { if (!bluetoothConnected) { u8g2.setFont(u8g2_font_6x10_tf); + const SensorReading& reading = sensorLogger.latest(); char sensorLine[32]; - if (aht20Ready) { - snprintf(sensorLine, sizeof(sensorLine), "T:%5.1fC H:%5.1f%%", temperature, humidity); - } else { - snprintf(sensorLine, sizeof(sensorLine), "AHT20: error"); - } + snprintf(sensorLine, sizeof(sensorLine), "T:%5.1fC H:%5.1f%%", + reading.temperature, reading.humidity); u8g2.drawStr(0, 12, sensorLine); - if (bh1750Ready) { - snprintf(sensorLine, sizeof(sensorLine), "Light: %6.1f lx", illuminance); - } else { - snprintf(sensorLine, sizeof(sensorLine), "BH1750: error"); - } + snprintf(sensorLine, sizeof(sensorLine), "Light: %6.1f lx", reading.illuminance); u8g2.drawStr(0, 30, sensorLine); - snprintf(sensorLine, sizeof(sensorLine), "Soil: %4d", soilMoisture); + snprintf(sensorLine, sizeof(sensorLine), "Soil: %4u", reading.soilMoisture); u8g2.drawStr(0, 48, sensorLine); u8g2.sendBuffer(); return; @@ -127,24 +110,6 @@ void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) updateDisplay(); } -void updateSensorDisplay() { - sensors_event_t humidityEvent; - sensors_event_t temperatureEvent; - - if (aht20Ready) { - aht20.getEvent(&humidityEvent, &temperatureEvent); - temperature = temperatureEvent.temperature; - humidity = humidityEvent.relative_humidity; - } - - if (bh1750Ready) { - illuminance = bh1750.readLightLevel(); - } - - soilMoisture = analogRead(kSoilMoisturePin); - updateDisplay(); -} - void setup() { Serial.begin(115200); @@ -158,12 +123,8 @@ void setup() { u8g2.begin(); u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化 - aht20Ready = aht20.begin(); - bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); - analogReadResolution(12); - - updateSensorDisplay(); - lastSensorUpdate = millis(); + sensorLogger.begin(32, kBluetoothName); + updateDisplay(); // I2S ピン設定 (BCK: 27, WS: 25, DOUT: 26) i2s_pin_config_t my_pin_config = { @@ -187,10 +148,7 @@ void setup() { void loop() { neoPixelController.update(); - - unsigned long now = millis(); - if (!bluetoothConnected && now - lastSensorUpdate >= kSensorUpdateIntervalMs) { - lastSensorUpdate = now; - updateSensorDisplay(); + if (sensorLogger.update() && !bluetoothConnected) { + updateDisplay(); } } \ No newline at end of file From 68a5f0dcce21670943b6b91297fe87ec53dd2112 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 16:34:30 +0900 Subject: [PATCH 09/18] =?UTF-8?q?EnvLog=E3=82=B5=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 113 ++++++++++++++++++++++++++++++++------ firmware/SensorLogger.h | 15 +++-- firmware/firmware.ino | 2 +- 3 files changed, 109 insertions(+), 21 deletions(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 765094c..8bd63bd 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -2,25 +2,76 @@ #include #include +#include +#include +#include +#include #include namespace { +constexpr char kNusServiceUuid[] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; +constexpr char kNusRxUuid[] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; +constexpr char kNusTxUuid[] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; + Adafruit_AHTX0 aht20; BH1750 bh1750; bool aht20Ready = false; bool bh1750Ready = false; } -void SensorLogger::begin(uint8_t soilPin, const char* bluetoothName) { +class SensorLoggerCallbacks : public BLECharacteristicCallbacks { +public: + explicit SensorLoggerCallbacks(SensorLogger* logger) : logger_(logger) {} + + void onWrite(BLECharacteristic* characteristic) override { + logger_->receiveBluetoothData(characteristic->getValue().c_str()); + } + +private: + SensorLogger* logger_; +}; + +class SensorLoggerServerCallbacks : public BLEServerCallbacks { +public: + void onConnect(BLEServer*) override {} + + void onDisconnect(BLEServer* server) override { + server->startAdvertising(); + } +}; + +void SensorLogger::begin(uint8_t soilPin) { soilPin_ = soilPin; - bluetoothName_ = bluetoothName; pinMode(soilPin_, INPUT); analogReadResolution(12); aht20Ready = aht20.begin(); bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); - bluetooth_.begin(bluetoothName); + uint64_t mac = ESP.getEfuseMac(); + char name[14]; + snprintf(name, sizeof(name), "EnvLog-%05llX", mac & 0xFFFFF); + bluetoothName_ = name; + BLEDevice::init(bluetoothName_.c_str()); + + server_ = BLEDevice::createServer(); + server_->setCallbacks(new SensorLoggerServerCallbacks()); + BLEService* service = server_->createService(kNusServiceUuid); + BLECharacteristic* rxCharacteristic = service->createCharacteristic( + kNusRxUuid, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); + rxCharacteristic->setCallbacks(new SensorLoggerCallbacks(this)); + txCharacteristic_ = service->createCharacteristic( + kNusTxUuid, BLECharacteristic::PROPERTY_NOTIFY); + txCharacteristic_->addDescriptor(new BLE2902()); + service->start(); + + BLEAdvertising* advertising = BLEDevice::getAdvertising(); + advertising->addServiceUUID(kNusServiceUuid); + advertising->setScanResponse(true); + advertising->setMinPreferred(0x06); + advertising->setMinPreferred(0x12); + BLEDevice::startAdvertising(); + readAndStore(); lastMeasure_ = millis(); } @@ -73,13 +124,25 @@ void SensorLogger::readAndStore() { } void SensorLogger::processBluetooth() { - while (bluetooth_.available()) { - char character = static_cast(bluetooth_.read()); + while (commandBuffer_.indexOf('\n') >= 0 || commandBuffer_.indexOf('\r') >= 0) { + int lineEnd = commandBuffer_.indexOf('\n'); + int carriageReturn = commandBuffer_.indexOf('\r'); + if (lineEnd < 0 || (carriageReturn >= 0 && carriageReturn < lineEnd)) { + lineEnd = carriageReturn; + } + String command = commandBuffer_.substring(0, lineEnd); + commandBuffer_.remove(0, lineEnd + 1); + if (command.length() != 0) { + processCommand(command); + } + } +} + +void SensorLogger::receiveBluetoothData(const String& data) { + for (size_t index = 0; index < data.length(); ++index) { + char character = data[index]; if (character == '\n' || character == '\r') { - if (commandBuffer_.length() != 0) { - processCommand(commandBuffer_); - commandBuffer_ = ""; - } + commandBuffer_ += character; } else if (commandBuffer_.length() < 64) { commandBuffer_ += character; } @@ -91,7 +154,8 @@ void SensorLogger::processCommand(const String& command) { sendLog(); } else if (command == "CLEAR_LOG") { clearLog(); - bluetooth_.println("OK_CLEARED"); + txCharacteristic_->setValue("OK_CLEARED\n"); + txCharacteristic_->notify(); } else if (command.startsWith("TIME:")) { unsigned long epoch = command.substring(5).toInt(); unsigned int year; @@ -114,28 +178,45 @@ void SensorLogger::processCommand(const String& command) { if (epoch != 0) { epochOffset_ = epoch - millis() / 1000UL; timeSynchronized_ = true; - bluetooth_.println("OK_TIME"); + txCharacteristic_->setValue("OK_TIME\n"); + txCharacteristic_->notify(); } else { - bluetooth_.println("ERROR_TIME"); + txCharacteristic_->setValue("ERROR_TIME\n"); + txCharacteristic_->notify(); } } } void SensorLogger::sendLog() { - bluetooth_.println("timestamp,temp,humid,light,soil,device"); + sendNotification("timestamp,temp,humid,light,soil,device\n"); for (size_t index = 0; index < logCount_; ++index) { size_t logIndex = (logStart_ + index) % kMaxLogEntries; const SensorReading& reading = log_[logIndex]; - bluetooth_.printf("%lu,%.2f,%.2f,%.2f,%u,%s\n", + char line[128]; + snprintf(line, sizeof(line), "%lu,%.2f,%.2f,%.2f,%u,%s\n", reading.timestamp, reading.temperature, reading.humidity, reading.illuminance, reading.soilMoisture, - bluetoothName_); + bluetoothName_.c_str()); + sendNotification(line); delay(20); } - bluetooth_.println("END_LOG"); + sendNotification("END_LOG\n"); +} + +void SensorLogger::sendNotification(const char* data) { + constexpr size_t kNotificationSize = 20; + uint8_t notification[kNotificationSize]; + size_t length = strlen(data); + for (size_t offset = 0; offset < length; offset += kNotificationSize) { + size_t chunkSize = min(kNotificationSize, length - offset); + memcpy(notification, data + offset, chunkSize); + txCharacteristic_->setValue(notification, chunkSize); + txCharacteristic_->notify(); + delay(10); + } } void SensorLogger::clearLog() { diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h index 482cd3a..0ad9f42 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -1,7 +1,9 @@ #pragma once #include -#include + +class BLECharacteristic; +class BLEServer; struct SensorReading { unsigned long timestamp; @@ -15,7 +17,7 @@ class SensorLogger { public: static constexpr size_t kMaxLogEntries = 2000; - void begin(uint8_t soilPin, const char* bluetoothName); + void begin(uint8_t soilPin); bool update(); const SensorReading& latest() const; bool hasReading() const; @@ -26,11 +28,14 @@ private: void readAndStore(); void processBluetooth(); void processCommand(const String& command); + void receiveBluetoothData(const String& data); + void sendNotification(const char* data); void sendLog(); void clearLog(); unsigned long currentTimestamp() const; - BluetoothSerial bluetooth_; + BLEServer* server_ = nullptr; + BLECharacteristic* txCharacteristic_ = nullptr; SensorReading log_[kMaxLogEntries] = {}; SensorReading latest_ = {}; size_t logStart_ = 0; @@ -39,6 +44,8 @@ private: unsigned long lastMeasure_ = 0; unsigned long epochOffset_ = 0; bool timeSynchronized_ = false; - const char* bluetoothName_ = ""; + String bluetoothName_; String commandBuffer_; + + friend class SensorLoggerCallbacks; }; diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 5dab087..8754b3d 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -123,7 +123,7 @@ void setup() { u8g2.begin(); u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化 - sensorLogger.begin(32, kBluetoothName); + sensorLogger.begin(32); updateDisplay(); // I2S ピン設定 (BCK: 27, WS: 25, DOUT: 26) From 5e052dba930be6288f38af2af03e155b093bb5e4 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 17:24:08 +0900 Subject: [PATCH 10/18] =?UTF-8?q?RTC=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 53 ++++++++++++++++++++++++++++++++++++--- firmware/SensorLogger.h | 8 +++--- firmware/firmware.ino | 40 ++++++++++++++++++++++------- 3 files changed, 86 insertions(+), 15 deletions(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 8bd63bd..90a7061 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -12,11 +12,17 @@ namespace { constexpr char kNusServiceUuid[] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusRxUuid[] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusTxUuid[] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; +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) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} } class SensorLoggerCallbacks : public BLECharacteristicCallbacks { @@ -96,6 +102,47 @@ bool SensorLogger::hasReading() const { return logCount_ != 0; } +size_t SensorLogger::logCount() const { + return logCount_; +} + +bool SensorLogger::getCurrentTime(char* buffer, size_t bufferSize) const { + if (bufferSize == 0) { + return false; + } + + int64_t timestamp = currentTimestamp(); + if (timeSynchronized_) { + time_t unixTimestamp = static_cast(timestamp); + struct tm date; + localtime_r(&unixTimestamp, &date); + strftime(buffer, bufferSize, "%Y/%m/%d %H:%M:%S", &date); + return true; + } + + int year = static_cast(kInitialYear); + int month = 1; + int day = 1; + int64_t days = timestamp / kSecondsPerDay; + int seconds = static_cast(timestamp % kSecondsPerDay); + while (days >= (isLeapYear(year) ? 366 : 365)) { + days -= isLeapYear(year) ? 366 : 365; + ++year; + } + const int daysInMonth[] = { + 31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31 + }; + while (days >= daysInMonth[month - 1]) { + days -= daysInMonth[month - 1]; + ++month; + } + day += static_cast(days); + snprintf(buffer, bufferSize, "%04d/%02d/%02d %02d:%02d:%02d", + year, month, day, seconds / 3600, (seconds / 60) % 60, seconds % 60); + return true; +} + void SensorLogger::readAndStore() { sensors_event_t humidityEvent; sensors_event_t temperatureEvent; @@ -193,7 +240,7 @@ void SensorLogger::sendLog() { size_t logIndex = (logStart_ + index) % kMaxLogEntries; const SensorReading& reading = log_[logIndex]; char line[128]; - snprintf(line, sizeof(line), "%lu,%.2f,%.2f,%.2f,%u,%s\n", + snprintf(line, sizeof(line), "%lld,%.2f,%.2f,%.2f,%u,%s\n", reading.timestamp, reading.temperature, reading.humidity, @@ -224,7 +271,7 @@ void SensorLogger::clearLog() { logCount_ = 0; } -unsigned long SensorLogger::currentTimestamp() const { - unsigned long elapsedSeconds = millis() / 1000UL; +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 0ad9f42..adfe885 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -6,7 +6,7 @@ class BLECharacteristic; class BLEServer; struct SensorReading { - unsigned long timestamp; + int64_t timestamp; float temperature; float humidity; float illuminance; @@ -21,6 +21,8 @@ public: bool update(); const SensorReading& latest() const; bool hasReading() const; + size_t logCount() const; + bool getCurrentTime(char* buffer, size_t bufferSize) const; private: static constexpr unsigned long kMeasureIntervalMs = 3600000UL; @@ -32,7 +34,7 @@ private: void sendNotification(const char* data); void sendLog(); void clearLog(); - unsigned long currentTimestamp() const; + int64_t currentTimestamp() const; BLEServer* server_ = nullptr; BLECharacteristic* txCharacteristic_ = nullptr; @@ -42,7 +44,7 @@ private: size_t logCount_ = 0; uint8_t soilPin_ = 0; unsigned long lastMeasure_ = 0; - unsigned long epochOffset_ = 0; + int64_t epochOffset_ = 0; bool timeSynchronized_ = false; String bluetoothName_; String commandBuffer_; diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 8754b3d..177b172 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -4,7 +4,7 @@ #include "NeoPixelController.h" #include "SensorLogger.h" -const char kBluetoothName[] = "BT_Speaker5.32"; +const char kBluetoothName[] = "BT_Speaker5.4"; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -18,25 +18,37 @@ String currentTitle = "未接続"; String currentArtist = ""; int currentVolumePercent = 0; // 音量 (0 ~ 100%) bool bluetoothConnected = false; +bool musicPlaying = false; +unsigned long lastDisplayUpdate = 0; // 画面全体を再描画する関数 void updateDisplay() { u8g2.clearBuffer(); - if (!bluetoothConnected) { + if (!bluetoothConnected || !musicPlaying) { u8g2.setFont(u8g2_font_6x10_tf); const SensorReading& reading = sensorLogger.latest(); char sensorLine[32]; + char timeLine[24]; + if (sensorLogger.getCurrentTime(timeLine, sizeof(timeLine))) { + u8g2.drawStr(0, 9, timeLine); + } else { + u8g2.drawStr(0, 9, "RTC: --/--/-- --:--:--"); + } + + snprintf(sensorLine, sizeof(sensorLine), "Log: %u", static_cast(sensorLogger.logCount())); + u8g2.drawStr(0, 20, sensorLine); + snprintf(sensorLine, sizeof(sensorLine), "T:%5.1fC H:%5.1f%%", reading.temperature, reading.humidity); - u8g2.drawStr(0, 12, sensorLine); + u8g2.drawStr(0, 32, sensorLine); snprintf(sensorLine, sizeof(sensorLine), "Light: %6.1f lx", reading.illuminance); - u8g2.drawStr(0, 30, sensorLine); + u8g2.drawStr(0, 44, sensorLine); snprintf(sensorLine, sizeof(sensorLine), "Soil: %4u", reading.soilMoisture); - u8g2.drawStr(0, 48, sensorLine); + u8g2.drawStr(0, 56, sensorLine); u8g2.sendBuffer(); return; } @@ -88,7 +100,7 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) { updated = true; } - if (updated && bluetoothConnected) { + if (updated && bluetoothConnected && musicPlaying) { updateDisplay(); } } @@ -96,17 +108,22 @@ 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) { + if (bluetoothConnected && musicPlaying) { updateDisplay(); } } void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { - neoPixelController.setPlaying(state == ESP_A2D_AUDIO_STATE_STARTED); + 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; + } updateDisplay(); } @@ -125,6 +142,7 @@ void setup() { sensorLogger.begin(32); updateDisplay(); + lastDisplayUpdate = millis(); // I2S ピン設定 (BCK: 27, WS: 25, DOUT: 26) i2s_pin_config_t my_pin_config = { @@ -148,7 +166,11 @@ void setup() { void loop() { neoPixelController.update(); - if (sensorLogger.update() && !bluetoothConnected) { + bool newReading = sensorLogger.update(); + unsigned long now = millis(); + if ((!bluetoothConnected || !musicPlaying) + && (newReading || now - lastDisplayUpdate >= 1000UL)) { + lastDisplayUpdate = now; updateDisplay(); } } \ No newline at end of file From 787f236fc66abde1da84a390c1c20033e9158c62 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 18:33:22 +0900 Subject: [PATCH 11/18] =?UTF-8?q?BLE=E5=BA=83=E5=91=8A=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 38 ++++++++++++++++++++++++++++++++++++-- firmware/SensorLogger.h | 5 +++++ firmware/firmware.ino | 21 +++++++++++++++------ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 90a7061..8ef22f3 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -39,11 +39,19 @@ private: class SensorLoggerServerCallbacks : public BLEServerCallbacks { public: - void onConnect(BLEServer*) override {} + explicit SensorLoggerServerCallbacks(SensorLogger* logger) : logger_(logger) {} + + void onConnect(BLEServer*) override { + logger_->bluetoothConnected_ = true; + } void onDisconnect(BLEServer* server) override { + logger_->bluetoothConnected_ = false; server->startAdvertising(); } + +private: + SensorLogger* logger_; }; void SensorLogger::begin(uint8_t soilPin) { @@ -61,7 +69,7 @@ void SensorLogger::begin(uint8_t soilPin) { BLEDevice::init(bluetoothName_.c_str()); server_ = BLEDevice::createServer(); - server_->setCallbacks(new SensorLoggerServerCallbacks()); + server_->setCallbacks(new SensorLoggerServerCallbacks(this)); BLEService* service = server_->createService(kNusServiceUuid); BLECharacteristic* rxCharacteristic = service->createCharacteristic( kNusRxUuid, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); @@ -94,6 +102,24 @@ bool SensorLogger::update() { return false; } +void SensorLogger::refreshSensors() { + SensorReading reading = {}; + sensors_event_t humidityEvent; + sensors_event_t temperatureEvent; + + if (aht20Ready) { + aht20.getEvent(&humidityEvent, &temperatureEvent); + reading.temperature = temperatureEvent.temperature; + reading.humidity = humidityEvent.relative_humidity; + } + if (bh1750Ready) { + reading.illuminance = bh1750.readLightLevel(); + } + reading.soilMoisture = analogRead(soilPin_); + reading.timestamp = latest_.timestamp; + latest_ = reading; +} + const SensorReading& SensorLogger::latest() const { return latest_; } @@ -102,6 +128,14 @@ bool SensorLogger::hasReading() const { return logCount_ != 0; } +const char* SensorLogger::bluetoothName() const { + return bluetoothName_.c_str(); +} + +bool SensorLogger::isBluetoothConnected() const { + return bluetoothConnected_; +} + size_t SensorLogger::logCount() const { return logCount_; } diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h index adfe885..fedfff1 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -19,10 +19,13 @@ public: void begin(uint8_t soilPin); bool update(); + void refreshSensors(); const SensorReading& latest() const; bool hasReading() const; size_t logCount() const; bool getCurrentTime(char* buffer, size_t bufferSize) const; + const char* bluetoothName() const; + bool isBluetoothConnected() const; private: static constexpr unsigned long kMeasureIntervalMs = 3600000UL; @@ -46,8 +49,10 @@ private: unsigned long lastMeasure_ = 0; int64_t epochOffset_ = 0; bool timeSynchronized_ = false; + bool bluetoothConnected_ = false; String bluetoothName_; String commandBuffer_; friend class SensorLoggerCallbacks; + friend class SensorLoggerServerCallbacks; }; diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 177b172..37f25b0 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -31,24 +31,32 @@ void updateDisplay() { char sensorLine[32]; char timeLine[24]; + if (sensorLogger.isBluetoothConnected()) { + u8g2.setDrawColor(1); + u8g2.drawBox(0, 0, 128, 10); + u8g2.setDrawColor(0); + } + u8g2.drawStr(0, 9, sensorLogger.bluetoothName()); + u8g2.setDrawColor(1); + if (sensorLogger.getCurrentTime(timeLine, sizeof(timeLine))) { - u8g2.drawStr(0, 9, timeLine); + u8g2.drawStr(0, 20, timeLine); } else { - u8g2.drawStr(0, 9, "RTC: --/--/-- --:--:--"); + u8g2.drawStr(0, 20, "RTC: --/--/-- --:--:--"); } snprintf(sensorLine, sizeof(sensorLine), "Log: %u", static_cast(sensorLogger.logCount())); - u8g2.drawStr(0, 20, sensorLine); + u8g2.drawStr(0, 31, sensorLine); snprintf(sensorLine, sizeof(sensorLine), "T:%5.1fC H:%5.1f%%", reading.temperature, reading.humidity); - u8g2.drawStr(0, 32, sensorLine); + u8g2.drawStr(0, 42, sensorLine); snprintf(sensorLine, sizeof(sensorLine), "Light: %6.1f lx", reading.illuminance); - u8g2.drawStr(0, 44, sensorLine); + u8g2.drawStr(0, 53, sensorLine); snprintf(sensorLine, sizeof(sensorLine), "Soil: %4u", reading.soilMoisture); - u8g2.drawStr(0, 56, sensorLine); + u8g2.drawStr(0, 64, sensorLine); u8g2.sendBuffer(); return; } @@ -171,6 +179,7 @@ void loop() { if ((!bluetoothConnected || !musicPlaying) && (newReading || now - lastDisplayUpdate >= 1000UL)) { lastDisplayUpdate = now; + sensorLogger.refreshSensors(); updateDisplay(); } } \ No newline at end of file From e83a7cff8030524f9cf524ee4cb3a0c94877be9a Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 19:40:21 +0900 Subject: [PATCH 12/18] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=A0=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=83=97=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 48 +++++++++++++++++++++++++++++++++------ firmware/SensorLogger.h | 8 ++++++- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 8ef22f3..98ee1d6 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -47,6 +47,8 @@ public: void onDisconnect(BLEServer* server) override { logger_->bluetoothConnected_ = false; + logger_->commandBuffer_ = ""; + logger_->lastAdvertise_ = millis(); server->startAdvertising(); } @@ -88,12 +90,18 @@ void SensorLogger::begin(uint8_t soilPin) { readAndStore(); lastMeasure_ = millis(); + lastAdvertise_ = millis(); } bool SensorLogger::update() { processBluetooth(); unsigned long now = millis(); + if (!bluetoothConnected_ && now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) { + lastAdvertise_ = now; + BLEDevice::startAdvertising(); + } + if (now - lastMeasure_ >= kMeasureIntervalMs) { lastMeasure_ = now; readAndStore(); @@ -140,12 +148,11 @@ size_t SensorLogger::logCount() const { return logCount_; } -bool SensorLogger::getCurrentTime(char* buffer, size_t bufferSize) const { +bool SensorLogger::formatTimestamp(int64_t timestamp, char* buffer, size_t bufferSize) const { if (bufferSize == 0) { return false; } - int64_t timestamp = currentTimestamp(); if (timeSynchronized_) { time_t unixTimestamp = static_cast(timestamp); struct tm date; @@ -177,6 +184,10 @@ bool SensorLogger::getCurrentTime(char* buffer, size_t bufferSize) const { return true; } +bool SensorLogger::getCurrentTime(char* buffer, size_t bufferSize) const { + return formatTimestamp(currentTimestamp(), buffer, bufferSize); +} + void SensorLogger::readAndStore() { sensors_event_t humidityEvent; sensors_event_t temperatureEvent; @@ -230,6 +241,23 @@ void SensorLogger::receiveBluetoothData(const String& data) { } } +void SensorLogger::applyHistoricalTimeAdjustment(int64_t epoch) { + if (historicalAdjustmentApplied_ || timeSynchronized_) { + return; + } + + const int64_t elapsedSeconds = millis() / 1000UL; + historicalEpochOffset_ = epoch - elapsedSeconds; + for (size_t index = 0; index < logCount_; ++index) { + size_t logIndex = (logStart_ + index) % kMaxLogEntries; + const int64_t storedSeconds = log_[logIndex].timestamp; + if (storedSeconds <= elapsedSeconds) { + log_[logIndex].timestamp = storedSeconds + historicalEpochOffset_; + } + } + historicalAdjustmentApplied_ = true; +} + void SensorLogger::processCommand(const String& command) { if (command == "GET_LOG") { sendLog(); @@ -257,6 +285,7 @@ void SensorLogger::processCommand(const String& command) { epoch = static_cast(mktime(&date)); } if (epoch != 0) { + applyHistoricalTimeAdjustment(static_cast(epoch)); epochOffset_ = epoch - millis() / 1000UL; timeSynchronized_ = true; txCharacteristic_->setValue("OK_TIME\n"); @@ -269,17 +298,22 @@ void SensorLogger::processCommand(const String& command) { } void SensorLogger::sendLog() { - sendNotification("timestamp,temp,humid,light,soil,device\n"); + sendNotification("タイムスタンプ,温度,湿度,土壌水分,光量,デバイス名\n"); for (size_t index = 0; index < logCount_; ++index) { size_t logIndex = (logStart_ + index) % kMaxLogEntries; const SensorReading& reading = log_[logIndex]; - char line[128]; - snprintf(line, sizeof(line), "%lld,%.2f,%.2f,%.2f,%u,%s\n", - reading.timestamp, + char timestamp[32]; + char line[160]; + const bool timestampValid = formatTimestamp(reading.timestamp, timestamp, sizeof(timestamp)); + if (!timestampValid) { + timestamp[0] = '\0'; + } + snprintf(line, sizeof(line), "%s,%.2f,%.2f,%u,%.2f,%s\n", + timestamp, reading.temperature, reading.humidity, - reading.illuminance, reading.soilMoisture, + reading.illuminance, bluetoothName_.c_str()); sendNotification(line); delay(20); diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h index fedfff1..67d3ad2 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -24,11 +24,13 @@ public: bool hasReading() const; size_t logCount() const; bool getCurrentTime(char* buffer, size_t bufferSize) const; + bool formatTimestamp(int64_t timestamp, char* buffer, size_t bufferSize) const; const char* bluetoothName() const; bool isBluetoothConnected() const; private: - static constexpr unsigned long kMeasureIntervalMs = 3600000UL; + static constexpr unsigned long kMeasureIntervalMs = 10000UL; + static constexpr unsigned long kAdvertiseRetryIntervalMs = 500UL; void readAndStore(); void processBluetooth(); @@ -37,6 +39,7 @@ private: void sendNotification(const char* data); void sendLog(); void clearLog(); + void applyHistoricalTimeAdjustment(int64_t epoch); int64_t currentTimestamp() const; BLEServer* server_ = nullptr; @@ -47,8 +50,11 @@ private: 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_; From ae692f67af13d9d3b1e3db282716c743f6f2b38c Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 19:51:29 +0900 Subject: [PATCH 13/18] =?UTF-8?q?CSV=E4=BF=9D=E5=AD=98=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 4 ++-- firmware/SensorLogger.h | 2 +- firmware/firmware.ino | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 98ee1d6..58bf167 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -12,6 +12,7 @@ namespace { constexpr char kNusServiceUuid[] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusRxUuid[] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; constexpr char kNusTxUuid[] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; +constexpr char kBleDeviceNamePrefix[] = "EnvLog"; constexpr int64_t kInitialYear = 1700; constexpr int64_t kSecondsPerDay = 86400; @@ -66,7 +67,7 @@ void SensorLogger::begin(uint8_t soilPin) { uint64_t mac = ESP.getEfuseMac(); char name[14]; - snprintf(name, sizeof(name), "EnvLog-%05llX", mac & 0xFFFFF); + snprintf(name, sizeof(name), "%s-%05llX", kBleDeviceNamePrefix, mac & 0xFFFFF); bluetoothName_ = name; BLEDevice::init(bluetoothName_.c_str()); @@ -298,7 +299,6 @@ void SensorLogger::processCommand(const String& command) { } void SensorLogger::sendLog() { - sendNotification("タイムスタンプ,温度,湿度,土壌水分,光量,デバイス名\n"); for (size_t index = 0; index < logCount_; ++index) { size_t logIndex = (logStart_ + index) % kMaxLogEntries; const SensorReading& reading = log_[logIndex]; diff --git a/firmware/SensorLogger.h b/firmware/SensorLogger.h index 67d3ad2..8710819 100644 --- a/firmware/SensorLogger.h +++ b/firmware/SensorLogger.h @@ -29,7 +29,7 @@ public: bool isBluetoothConnected() const; private: - static constexpr unsigned long kMeasureIntervalMs = 10000UL; + static constexpr unsigned long kMeasureIntervalMs = 3600000UL; static constexpr unsigned long kAdvertiseRetryIntervalMs = 500UL; void readAndStore(); diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 37f25b0..d974a8e 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -4,7 +4,7 @@ #include "NeoPixelController.h" #include "SensorLogger.h" -const char kBluetoothName[] = "BT_Speaker5.4"; +const char kA2dpDeviceName[] = "BT_Speaker5.4"; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); @@ -169,7 +169,7 @@ void setup() { a2dp_sink.set_on_connection_state_changed(connection_state_changed_callback); // Bluetooth起動 - a2dp_sink.start(kBluetoothName); + a2dp_sink.start(kA2dpDeviceName); } void loop() { From c6ffbacb0370c431184569a2c597460a94527e0c Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 20:38:31 +0900 Subject: [PATCH 14/18] =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/SensorLogger.cpp | 3 +++ firmware/firmware.ino | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware/SensorLogger.cpp b/firmware/SensorLogger.cpp index 58bf167..c6958b3 100644 --- a/firmware/SensorLogger.cpp +++ b/firmware/SensorLogger.cpp @@ -85,6 +85,9 @@ void SensorLogger::begin(uint8_t soilPin) { BLEAdvertising* advertising = BLEDevice::getAdvertising(); advertising->addServiceUUID(kNusServiceUuid); advertising->setScanResponse(true); + BLEAdvertisementData scanResponseData; + scanResponseData.setName(bluetoothName_.c_str()); + advertising->setScanResponseData(scanResponseData); advertising->setMinPreferred(0x06); advertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); diff --git a/firmware/firmware.ino b/firmware/firmware.ino index d974a8e..164f065 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -4,7 +4,7 @@ #include "NeoPixelController.h" #include "SensorLogger.h" -const char kA2dpDeviceName[] = "BT_Speaker5.4"; +const char kA2dpDeviceName[] = "BT_Speaker5.5"; // I2C接続のOLED設定 (SSD1306 128x64) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); From e7f9fb4c1b12bbfd62b2906a758a1732cba625d0 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 20:43:53 +0900 Subject: [PATCH 15/18] =?UTF-8?q?=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/NeoPixelController.cpp | 8 +++++ firmware/NeoPixelController.h | 7 ++++ firmware/SensorLogger.cpp | 38 ++++++++++++++++++++ firmware/SensorLogger.h | 55 ++++++++++++++++++----------- firmware/firmware.ino | 62 ++++++++++++++++++--------------- 5 files changed, 120 insertions(+), 50 deletions(-) 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(); From 2b3b7cfc4e2535aebf43f6a9e0bc096c9db8ff17 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 20:59:30 +0900 Subject: [PATCH 16/18] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AB=E3=81=AA=E3=81=84=E6=96=87=E5=AD=97=E3=81=AF=E3=80=81?= =?UTF-8?q?=E3=83=88=E3=83=BC=E3=83=95=E3=81=B8=E7=BD=AE=E3=81=8D=E6=8F=9B?= =?UTF-8?q?=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 67 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index d199cdd..997b225 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -24,6 +24,65 @@ bool bluetoothConnected = false; bool musicPlaying = false; unsigned long lastDisplayUpdate = 0; +// UTF-8の文字列を1文字ずつ調べ、フォントにない文字を□へ置き換えます。 +// U8g2は、知らない文字を自動では表示せず、次の文字へ進んでしまいます。 +String replaceMissingGlyphs(const String& text) { + String result; + + for (size_t index = 0; index < text.length();) { + const uint8_t firstByte = static_cast(text[index]); + uint32_t codePoint = 0; + size_t byteCount = 0; + + // UTF-8の先頭バイトから、1文字のバイト数を判定します。 + if (firstByte < 0x80) { + codePoint = firstByte; + byteCount = 1; + } else if ((firstByte & 0xE0) == 0xC0 && index + 1 < text.length()) { + codePoint = firstByte & 0x1F; + byteCount = 2; + } else if ((firstByte & 0xF0) == 0xE0 && index + 2 < text.length()) { + codePoint = firstByte & 0x0F; + byteCount = 3; + } else if ((firstByte & 0xF8) == 0xF0 && index + 3 < text.length()) { + codePoint = firstByte & 0x07; + byteCount = 4; + } else { + // 壊れたUTF-8も、表示できない文字としてトーフにします。 + result += "□"; + ++index; + continue; + } + + // 2バイト目以降をつなげて、Unicodeの文字コードを作ります。 + bool valid = true; + for (size_t offset = 1; offset < byteCount; ++offset) { + const uint8_t nextByte = static_cast(text[index + offset]); + if ((nextByte & 0xC0) != 0x80) { + valid = false; + break; + } + codePoint = (codePoint << 6) | (nextByte & 0x3F); + } + + if (!valid) { + result += "□"; + ++index; + continue; + } + + // 幅が0なら、その文字の絵がフォントにないと判断します。 + if (u8g2.getGlyphWidth(codePoint) == 0) { + result += "□"; + } else { + result += text.substring(index, index + byteCount); + } + index += byteCount; + } + + return result; +} + // OLEDの内容を現在の状態に合わせて描き直します。 void updateDisplay() { u8g2.clearBuffer(); @@ -84,12 +143,14 @@ void updateDisplay() { // 下の部分は、日本語を含む曲名とアーティスト名を表示します。 u8g2.setFont(u8g2_font_unifont_t_japanese1); - // 曲名を表示します。drawUTF8は日本語も扱える関数です。 - u8g2.drawUTF8(0, 32, currentTitle.c_str()); + // フォントにない文字は□に置き換えてから表示します。 + String displayTitle = replaceMissingGlyphs(currentTitle); + u8g2.drawUTF8(0, 32, displayTitle.c_str()); // アーティスト名が空でないときだけ表示します。 if (currentArtist.length() > 0) { - u8g2.drawUTF8(0, 54, currentArtist.c_str()); + String displayArtist = replaceMissingGlyphs(currentArtist); + u8g2.drawUTF8(0, 54, displayArtist.c_str()); } // ここまでの描画内容を、実際のOLED画面へ送ります。 From ca05ef14e5efda32cef4bd70d7f1fd3846965607 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 21:02:22 +0900 Subject: [PATCH 17/18] =?UTF-8?q?getUTF8Width=20=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 997b225..bbda449 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -31,21 +31,16 @@ String replaceMissingGlyphs(const String& text) { for (size_t index = 0; index < text.length();) { const uint8_t firstByte = static_cast(text[index]); - uint32_t codePoint = 0; size_t byteCount = 0; // UTF-8の先頭バイトから、1文字のバイト数を判定します。 if (firstByte < 0x80) { - codePoint = firstByte; byteCount = 1; } else if ((firstByte & 0xE0) == 0xC0 && index + 1 < text.length()) { - codePoint = firstByte & 0x1F; byteCount = 2; } else if ((firstByte & 0xF0) == 0xE0 && index + 2 < text.length()) { - codePoint = firstByte & 0x0F; byteCount = 3; } else if ((firstByte & 0xF8) == 0xF0 && index + 3 < text.length()) { - codePoint = firstByte & 0x07; byteCount = 4; } else { // 壊れたUTF-8も、表示できない文字としてトーフにします。 @@ -54,7 +49,7 @@ String replaceMissingGlyphs(const String& text) { continue; } - // 2バイト目以降をつなげて、Unicodeの文字コードを作ります。 + // 2バイト目以降がUTF-8の続きになっているか確認します。 bool valid = true; for (size_t offset = 1; offset < byteCount; ++offset) { const uint8_t nextByte = static_cast(text[index + offset]); @@ -62,7 +57,6 @@ String replaceMissingGlyphs(const String& text) { valid = false; break; } - codePoint = (codePoint << 6) | (nextByte & 0x3F); } if (!valid) { @@ -71,11 +65,15 @@ String replaceMissingGlyphs(const String& text) { continue; } + // 1文字分のUTF-8文字列を作ります。 + String character = text.substring(index, index + byteCount); + // 幅が0なら、その文字の絵がフォントにないと判断します。 - if (u8g2.getGlyphWidth(codePoint) == 0) { + // getGlyphWidthは使えるU8g2の版が限られるため、getUTF8Widthを使います。 + if (u8g2.getUTF8Width(character.c_str()) == 0) { result += "□"; } else { - result += text.substring(index, index + byteCount); + result += character; } index += byteCount; } From c15850a074248d646b6809a4b9db2f2701b14082 Mon Sep 17 00:00:00 2001 From: Koichi Ito Date: Sun, 20 Sep 2026 21:27:34 +0900 Subject: [PATCH 18/18] =?UTF-8?q?=E3=83=88=E3=83=BC=E3=83=95=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/firmware.ino | 54 +++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/firmware/firmware.ino b/firmware/firmware.ino index bbda449..5b0ba36 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -24,32 +24,40 @@ bool bluetoothConnected = false; bool musicPlaying = false; unsigned long lastDisplayUpdate = 0; -// UTF-8の文字列を1文字ずつ調べ、フォントにない文字を□へ置き換えます。 -// U8g2は、知らない文字を自動では表示せず、次の文字へ進んでしまいます。 -String replaceMissingGlyphs(const String& text) { - String result; +// UTF-8の文字列を1文字ずつ描画します。 +// フォントにない文字は、フォントに頼らず四角形を直接描きます。 +void drawUtf8WithTofu(uint8_t x, uint8_t y, const String& text) { + constexpr uint8_t kTofuSize = 12; + constexpr uint8_t kTofuAdvance = 16; + uint8_t cursorX = x; for (size_t index = 0; index < text.length();) { const uint8_t firstByte = static_cast(text[index]); size_t byteCount = 0; + uint32_t codePoint = 0; // UTF-8の先頭バイトから、1文字のバイト数を判定します。 if (firstByte < 0x80) { + codePoint = firstByte; byteCount = 1; } else if ((firstByte & 0xE0) == 0xC0 && index + 1 < text.length()) { + codePoint = firstByte & 0x1F; byteCount = 2; } else if ((firstByte & 0xF0) == 0xE0 && index + 2 < text.length()) { + codePoint = firstByte & 0x0F; byteCount = 3; } else if ((firstByte & 0xF8) == 0xF0 && index + 3 < text.length()) { + codePoint = firstByte & 0x07; byteCount = 4; } else { - // 壊れたUTF-8も、表示できない文字としてトーフにします。 - result += "□"; + // 壊れたUTF-8も、表示できない文字としてトーフを描きます。 + u8g2.drawFrame(cursorX, y - kTofuSize, kTofuSize, kTofuSize); + cursorX += kTofuAdvance; ++index; continue; } - // 2バイト目以降がUTF-8の続きになっているか確認します。 + // 2バイト目以降をつなげて、Unicodeの文字コードを作ります。 bool valid = true; for (size_t offset = 1; offset < byteCount; ++offset) { const uint8_t nextByte = static_cast(text[index + offset]); @@ -57,28 +65,30 @@ String replaceMissingGlyphs(const String& text) { valid = false; break; } + codePoint = (codePoint << 6) | (nextByte & 0x3F); } if (!valid) { - result += "□"; + u8g2.drawFrame(cursorX, y - kTofuSize, kTofuSize, kTofuSize); + cursorX += kTofuAdvance; ++index; continue; } - // 1文字分のUTF-8文字列を作ります。 - String character = text.substring(index, index + byteCount); - - // 幅が0なら、その文字の絵がフォントにないと判断します。 - // getGlyphWidthは使えるU8g2の版が限られるため、getUTF8Widthを使います。 - if (u8g2.getUTF8Width(character.c_str()) == 0) { - result += "□"; + // フォントに文字があれば、実際のグリフを描画します。 + // 16ビットを超える文字は、このフォントでは表示できないためトーフにします。 + if (codePoint <= 0xFFFF && u8g2_IsGlyph(u8g2.getU8g2(), codePoint)) { + u8g2.drawGlyph(cursorX, y, static_cast(codePoint)); + cursorX += static_cast(u8g2_GetGlyphWidth( + u8g2.getU8g2(), static_cast(codePoint))); } else { - result += character; + // □の字形がフォントにない場合でも、四角形なら必ず表示できます。 + u8g2.drawFrame(cursorX, y - kTofuSize, kTofuSize, kTofuSize); + cursorX += kTofuAdvance; } + index += byteCount; } - - return result; } // OLEDの内容を現在の状態に合わせて描き直します。 @@ -141,14 +151,12 @@ void updateDisplay() { // 下の部分は、日本語を含む曲名とアーティスト名を表示します。 u8g2.setFont(u8g2_font_unifont_t_japanese1); - // フォントにない文字は□に置き換えてから表示します。 - String displayTitle = replaceMissingGlyphs(currentTitle); - u8g2.drawUTF8(0, 32, displayTitle.c_str()); + // フォントにない文字は、四角形のトーフとして表示します。 + drawUtf8WithTofu(0, 32, currentTitle); // アーティスト名が空でないときだけ表示します。 if (currentArtist.length() > 0) { - String displayArtist = replaceMissingGlyphs(currentArtist); - u8g2.drawUTF8(0, 54, displayArtist.c_str()); + drawUtf8WithTofu(0, 54, currentArtist); } // ここまでの描画内容を、実際のOLED画面へ送ります。