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