Compare commits

..

No commits in common. "2e3cd4220a9d58b1e2190471c6d775cdc5048d68" and "87d111e3d9ad450ed9d9688092ce65d226650edc" have entirely different histories.

5 changed files with 49 additions and 168 deletions

View File

@ -45,7 +45,6 @@ jobs:
run: | run: |
arduino-cli lib install "Adafruit AHTX0" arduino-cli lib install "Adafruit AHTX0"
arduino-cli lib install "BH1750" arduino-cli lib install "BH1750"
arduino-cli lib install "Adafruit NeoPixel"
# コンパイル実行 (output-dir を指定して構成ファイルを確実に生成) # コンパイル実行 (output-dir を指定して構成ファイルを確実に生成)
- name: Compile Sketch - name: Compile Sketch

View File

@ -1,14 +1,6 @@
# arduino_esp32_a2dp # arduino_esp32_a2dp
ESP32-WROOM-32E を使用した、Arduino版 A2DP Bluetoothスピーカーのファームウェアリポジトリです。 ESP32-WROOM-32E を使用した、Arduino版 A2DP Bluetoothスピーカーのファームウェアリポジトリです。GitHub Actions を利用して、ブラウザ上だけでファームウェアの書き換えとビルドが行えます。
GitHub Actions を利用して、ブラウザ上だけでファームウェアの書き換えとビルドが行えます。
ESP32-WROOM-32E 用の Bluetooth スピーカー主な機能:
- NeoPixcel:デバイスの動作モードに合わせて発光色を変化
- 音楽再生時: 楽曲名、歌手名、音量を表示
- 音楽停止時: 各種センサー値(温度・湿度・照度・土壌水分)を表示
- 環境データ出力: 1時間ごとにデータを蓄積し、Webブラウザから Bluetooth 経由でデータをダウンロード(CSV形式で保存可能)
## 手順 ## 手順

View File

@ -6,7 +6,6 @@
#include <BLEDevice.h> #include <BLEDevice.h>
#include <BLEServer.h> #include <BLEServer.h>
#include <BLEUtils.h> #include <BLEUtils.h>
#include <Wire.h>
#include <time.h> #include <time.h>
namespace { namespace {
@ -25,11 +24,6 @@ BH1750 bh1750;
bool aht20Ready = false; bool aht20Ready = false;
bool bh1750Ready = false; bool bh1750Ready = false;
bool isI2cDevicePresent(uint8_t address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
bool isLeapYear(int year) { bool isLeapYear(int year) {
// 4年ごと。ただし100年ごとは除き、400年ごとはうるう年です。 // 4年ごと。ただし100年ごとは除き、400年ごとはうるう年です。
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
@ -58,7 +52,6 @@ public:
void onConnect(BLEServer*) override { void onConnect(BLEServer*) override {
// 接続中は、画面に接続中であることを表示できます。 // 接続中は、画面に接続中であることを表示できます。
logger_->bluetoothConnected_ = true; logger_->bluetoothConnected_ = true;
Serial.println("[INFO] BLE client connected");
} }
void onDisconnect(BLEServer* server) override { void onDisconnect(BLEServer* server) override {
@ -67,77 +60,54 @@ public:
logger_->commandBuffer_ = ""; logger_->commandBuffer_ = "";
logger_->lastAdvertise_ = millis(); logger_->lastAdvertise_ = millis();
server->startAdvertising(); server->startAdvertising();
Serial.println("[WARNING] BLE client disconnected; advertising restarted");
} }
private: private:
SensorLogger* logger_; SensorLogger* logger_;
}; };
void SensorLogger::begin(uint8_t soilPin, bool i2cConnected, bool bleDataConnected) { void SensorLogger::begin(uint8_t soilPin) {
// センサーとBLE通信を使い始めるための初期設定です。 // センサーとBLE通信を使い始めるための初期設定です。
soilPin_ = soilPin; soilPin_ = soilPin;
bleDataConnected_ = bleDataConnected;
pinMode(soilPin_, INPUT); pinMode(soilPin_, INPUT);
analogReadResolution(12); analogReadResolution(12);
// センサーが接続されているときだけ初期化します。 // センサーの初期化に成功したかを保存します。
// 未接続時にライブラリへアクセスすると、不要なI2Cエラーや待ち時間が発生します。 aht20Ready = aht20.begin();
if (i2cConnected) { bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
if (isI2cDevicePresent(0x38)) {
aht20Ready = aht20.begin();
if (!aht20Ready) {
Serial.println("[ERROR] AHT20 initialization failed");
}
} else {
Serial.println("[WARNING] AHT20 not detected at 0x38");
}
if (isI2cDevicePresent(0x23)) {
bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
if (!bh1750Ready) {
Serial.println("[ERROR] BH1750 initialization failed");
}
} else {
Serial.println("[WARNING] BH1750 not detected at 0x23");
}
}
if (bleDataConnected_) { // ESP32固有の番号を名前に加え、同じ名前の機器と区別します。
// ESP32固有の番号を名前に加え、同じ名前の機器と区別します。 uint64_t mac = ESP.getEfuseMac();
uint64_t mac = ESP.getEfuseMac(); char name[14];
char name[14]; snprintf(name, sizeof(name), "%s-%05llX", kBleDeviceNamePrefix, mac & 0xFFFFF);
snprintf(name, sizeof(name), "%s-%05llX", kBleDeviceNamePrefix, mac & 0xFFFFF); bluetoothName_ = name;
bluetoothName_ = name; BLEDevice::init(bluetoothName_.c_str());
BLEDevice::init(bluetoothName_.c_str());
Serial.printf("[INFO] BLE initialized as '%s'\n", bluetoothName_.c_str());
// BLEサーバーと、センサー通信用のサービスを作ります。 // BLEサーバーと、センサー通信用のサービスを作ります。
server_ = BLEDevice::createServer(); server_ = BLEDevice::createServer();
server_->setCallbacks(new SensorLoggerServerCallbacks(this)); server_->setCallbacks(new SensorLoggerServerCallbacks(this));
BLEService* service = server_->createService(kNusServiceUuid); BLEService* service = server_->createService(kNusServiceUuid);
// RXはアプリからESP32へ命令を送る通路です。 // RXはアプリからESP32へ命令を送る通路です。
BLECharacteristic* rxCharacteristic = service->createCharacteristic( BLECharacteristic* rxCharacteristic = service->createCharacteristic(
kNusRxUuid, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); kNusRxUuid, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
rxCharacteristic->setCallbacks(new SensorLoggerCallbacks(this)); rxCharacteristic->setCallbacks(new SensorLoggerCallbacks(this));
// TXはESP32からアプリへログを送る通路です。 // TXはESP32からアプリへログを送る通路です。
txCharacteristic_ = service->createCharacteristic( txCharacteristic_ = service->createCharacteristic(
kNusTxUuid, BLECharacteristic::PROPERTY_NOTIFY); kNusTxUuid, BLECharacteristic::PROPERTY_NOTIFY);
txCharacteristic_->addDescriptor(new BLE2902()); txCharacteristic_->addDescriptor(new BLE2902());
service->start(); service->start();
// BLE広告は「この機器が近くにいます」と周囲へ知らせる仕組みです。 // BLE広告は「この機器が近くにいます」と周囲へ知らせる仕組みです。
BLEAdvertising* advertising = BLEDevice::getAdvertising(); BLEAdvertising* advertising = BLEDevice::getAdvertising();
advertising->addServiceUUID(kNusServiceUuid); advertising->addServiceUUID(kNusServiceUuid);
advertising->setScanResponse(true); advertising->setScanResponse(true);
// A2DP開始後に共通のBluetooth名が変わっても、BLE名を返せるようにします。 // A2DP開始後に共通のBluetooth名が変わっても、BLE名を返せるようにします。
BLEAdvertisementData scanResponseData; BLEAdvertisementData scanResponseData;
scanResponseData.setName(bluetoothName_.c_str()); scanResponseData.setName(bluetoothName_.c_str());
advertising->setScanResponseData(scanResponseData); advertising->setScanResponseData(scanResponseData);
advertising->setMinPreferred(0x06); advertising->setMinPreferred(0x06);
advertising->setMinPreferred(0x12); advertising->setMinPreferred(0x12);
BLEDevice::startAdvertising(); BLEDevice::startAdvertising();
Serial.println("[INFO] BLE advertising started");
}
// 起動直後にも1回計測して、画面に値を表示できるようにします。 // 起動直後にも1回計測して、画面に値を表示できるようにします。
readAndStore(); readAndStore();
@ -147,13 +117,10 @@ void SensorLogger::begin(uint8_t soilPin, bool i2cConnected, bool bleDataConnect
bool SensorLogger::update() { bool SensorLogger::update() {
// loopから繰り返し呼ばれ、通信と定期計測を担当します。 // loopから繰り返し呼ばれ、通信と定期計測を担当します。
if (bleDataConnected_) { processBluetooth();
processBluetooth();
}
unsigned long now = millis(); unsigned long now = millis();
if (bleDataConnected_ && !bluetoothConnected_ if (!bluetoothConnected_ && now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) {
&& now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) {
// 接続されていなければ、定期的にBLE広告を出し直します。 // 接続されていなければ、定期的にBLE広告を出し直します。
lastAdvertise_ = now; lastAdvertise_ = now;
BLEDevice::startAdvertising(); BLEDevice::startAdvertising();
@ -296,10 +263,6 @@ void SensorLogger::processBluetooth() {
} }
void SensorLogger::receiveBluetoothData(const String& data) { void SensorLogger::receiveBluetoothData(const String& data) {
if (!bleDataConnected_) {
return;
}
// BLEで届いた文字を命令バッファへ追加します。 // BLEで届いた文字を命令バッファへ追加します。
for (size_t index = 0; index < data.length(); ++index) { for (size_t index = 0; index < data.length(); ++index) {
char character = data[index]; char character = data[index];
@ -307,8 +270,6 @@ void SensorLogger::receiveBluetoothData(const String& data) {
commandBuffer_ += character; commandBuffer_ += character;
} else if (commandBuffer_.length() < 64) { } else if (commandBuffer_.length() < 64) {
commandBuffer_ += character; commandBuffer_ += character;
} else if (commandBuffer_.length() == 64) {
Serial.println("[ERROR] BLE command is too long and was truncated");
} }
} }
} }
@ -332,10 +293,6 @@ void SensorLogger::applyHistoricalTimeAdjustment(int64_t epoch) {
} }
void SensorLogger::processCommand(const String& command) { void SensorLogger::processCommand(const String& command) {
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
return;
}
// アプリから届いた命令を判定します。 // アプリから届いた命令を判定します。
if (command == "GET_LOG") { if (command == "GET_LOG") {
// 保存済みのログを順番に送ります。 // 保存済みのログを順番に送ります。
@ -372,20 +329,13 @@ void SensorLogger::processCommand(const String& command) {
txCharacteristic_->setValue("OK_TIME\n"); txCharacteristic_->setValue("OK_TIME\n");
txCharacteristic_->notify(); txCharacteristic_->notify();
} else { } else {
Serial.printf("[ERROR] Invalid TIME command: %s\n", command.c_str());
txCharacteristic_->setValue("ERROR_TIME\n"); txCharacteristic_->setValue("ERROR_TIME\n");
txCharacteristic_->notify(); txCharacteristic_->notify();
} }
} else {
Serial.printf("[WARNING] Unknown BLE command: %s\n", command.c_str());
} }
} }
void SensorLogger::sendLog() { void SensorLogger::sendLog() {
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
return;
}
// 保存したログを1行ずつ作り、BLE通知で送ります。 // 保存したログを1行ずつ作り、BLE通知で送ります。
for (size_t index = 0; index < logCount_; ++index) { for (size_t index = 0; index < logCount_; ++index) {
size_t logIndex = (logStart_ + index) % kMaxLogEntries; size_t logIndex = (logStart_ + index) % kMaxLogEntries;
@ -410,10 +360,6 @@ void SensorLogger::sendLog() {
} }
void SensorLogger::sendNotification(const char* data) { void SensorLogger::sendNotification(const char* data) {
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
return;
}
// BLE通知には一度に送れる長さの制限があるため、短く分割します。 // BLE通知には一度に送れる長さの制限があるため、短く分割します。
constexpr size_t kNotificationSize = 20; constexpr size_t kNotificationSize = 20;
uint8_t notification[kNotificationSize]; uint8_t notification[kNotificationSize];

View File

@ -18,7 +18,7 @@ public:
static constexpr size_t kMaxLogEntries = 2000; static constexpr size_t kMaxLogEntries = 2000;
// センサー、BLE、保存データを使い始める準備をします。 // センサー、BLE、保存データを使い始める準備をします。
void begin(uint8_t soilPin, bool i2cConnected, bool bleDataConnected); void begin(uint8_t soilPin);
// BLEの受信と、一定時間ごとの計測を行います。 // BLEの受信と、一定時間ごとの計測を行います。
bool update(); bool update();
// センサーを読み取り、画面表示用の最新値を更新します。 // センサーを読み取り、画面表示用の最新値を更新します。
@ -69,7 +69,6 @@ private:
bool timeSynchronized_ = false; // 時刻合わせが済んだか bool timeSynchronized_ = false; // 時刻合わせが済んだか
bool historicalAdjustmentApplied_ = false; // 過去ログの補正が済んだか bool historicalAdjustmentApplied_ = false; // 過去ログの補正が済んだか
bool bluetoothConnected_ = false; // BLE接続中か bool bluetoothConnected_ = false; // BLE接続中か
bool bleDataConnected_ = false; // BLEデータ送受信を使うか
String bluetoothName_; // BLEに表示する名前 String bluetoothName_; // BLEに表示する名前
String commandBuffer_; // BLEから受け取った未処理の文字 String commandBuffer_; // BLEから受け取った未処理の文字

View File

@ -1,10 +1,3 @@
// I2C機器(OLED・センサー)を接続していない場合は false にします。
const bool kI2cConnected = true; // I2C機器が接続されていない場合は false にします。
// BLEでのセンサーデータ送受信を使わない場合は false にします。
const bool kBleDataConnected = true; // BLEでのセンサーデータ送受信を使わない場合は false にします。
// NeoPixelの接続ピンです。未接続の場合は -1 にすると一切使用しません。
const int kNeoPixelPin = 18; // 使用しない場合は -1 に、通常は 18番ピンに接続します。
// A2DPは、スマートフォンから音楽を受信するBluetoothの仕組みです。 // A2DPは、スマートフォンから音楽を受信するBluetoothの仕組みです。
#include "BluetoothA2DPSink.h" #include "BluetoothA2DPSink.h"
#include <Wire.h> #include <Wire.h>
@ -13,12 +6,7 @@ const int kNeoPixelPin = 18; // 使用しない場合は -1 に、通常は 18
#include "SensorLogger.h" #include "SensorLogger.h"
// スマートフォンのBluetooth一覧に表示されるスピーカー名です。 // スマートフォンのBluetooth一覧に表示されるスピーカー名です。
const char kA2dpDeviceName[] = "BT_Speaker5.7"; const char kA2dpDeviceName[] = "BT_Speaker5.5";
// I2C接続のSDA/SCLピンです。配線に合わせてここだけ変更します。
// 通常基板:SDA=21, SCL=22
// 開発用基板:SDA=19, SCL=5
const uint8_t kI2cSdaPin = 21;
const uint8_t kI2cSclPin = 22;
// I2Cで接続した128x64ドットのOLED画面です。 // I2Cで接続した128x64ドットのOLED画面です。
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
@ -35,12 +23,6 @@ int currentVolumePercent = 0; // 音量(0〜100%)
bool bluetoothConnected = false; bool bluetoothConnected = false;
bool musicPlaying = false; bool musicPlaying = false;
unsigned long lastDisplayUpdate = 0; unsigned long lastDisplayUpdate = 0;
bool oledReady = false;
bool isI2cDevicePresent(uint8_t address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
// UTF-8の文字列を1文字ずつ描画します。 // UTF-8の文字列を1文字ずつ描画します。
// フォントにない文字は、フォントに頼らず四角形を直接描きます。 // フォントにない文字は、フォントに頼らず四角形を直接描きます。
@ -111,10 +93,6 @@ void drawUtf8WithTofu(uint8_t x, uint8_t y, const String& text) {
// OLEDの内容を現在の状態に合わせて描き直します。 // OLEDの内容を現在の状態に合わせて描き直します。
void updateDisplay() { void updateDisplay() {
if (!oledReady) {
return;
}
u8g2.clearBuffer(); u8g2.clearBuffer();
// 音楽を再生していないときは、センサーの情報を表示します。 // 音楽を再生していないときは、センサーの情報を表示します。
@ -195,8 +173,6 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) {
} else if (id == ESP_AVRC_MD_ATTR_ARTIST) { } else if (id == ESP_AVRC_MD_ATTR_ARTIST) {
currentArtist = (char*)text; currentArtist = (char*)text;
updated = true; updated = true;
} else {
Serial.printf("[WARNING] Unsupported AVRCP metadata id: %u\n", id);
} }
if (updated && bluetoothConnected && musicPlaying) { if (updated && bluetoothConnected && musicPlaying) {
@ -215,19 +191,13 @@ void volume_changed_callback(int volume) {
void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) { void audio_state_changed_callback(esp_a2d_audio_state_t state, void *) {
// 音声が流れ始めたかどうかを保存し、LEDと画面を更新します。 // 音声が流れ始めたかどうかを保存し、LEDと画面を更新します。
musicPlaying = state == ESP_A2D_AUDIO_STATE_STARTED; musicPlaying = state == ESP_A2D_AUDIO_STATE_STARTED;
Serial.printf("[INFO] Audio state changed: %d (playing=%s)\n", neoPixelController.setPlaying(musicPlaying);
state, musicPlaying ? "yes" : "no");
if (kNeoPixelPin >= 0) {
neoPixelController.setPlaying(musicPlaying);
}
updateDisplay(); updateDisplay();
} }
void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) { void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) {
// スマートフォンとの接続状態を保存します。 // スマートフォンとの接続状態を保存します。
bluetoothConnected = state == ESP_A2D_CONNECTION_STATE_CONNECTED; bluetoothConnected = state == ESP_A2D_CONNECTION_STATE_CONNECTED;
Serial.printf("[INFO] A2DP connection state changed: %d (connected=%s)\n",
state, bluetoothConnected ? "yes" : "no");
if (!bluetoothConnected) { if (!bluetoothConnected) {
musicPlaying = false; musicPlaying = false;
} }
@ -237,39 +207,17 @@ void connection_state_changed_callback(esp_a2d_connection_state_t state, void *)
void setup() { void setup() {
// setupは、電源を入れた直後に1回だけ実行されます。 // setupは、電源を入れた直後に1回だけ実行されます。
Serial.begin(115200); Serial.begin(115200);
Serial.println("[INFO] Firmware starting");
if (kNeoPixelPin >= 0) { neoPixelController.begin(18);
neoPixelController.begin(static_cast<uint8_t>(kNeoPixelPin));
}
if (kI2cConnected) { // I2Cの配線を設定します。Wire.begin(SDA, SCL)の順です。
// I2Cの配線を設定します。Wire.begin(SDA, SCL)の順です。 Wire.begin(21, 22);
// OLED未接続時もSDA/SCLが浮かないようにします。
pinMode(kI2cSdaPin, INPUT_PULLUP); // OLEDを使える状態にします。
pinMode(kI2cSclPin, INPUT_PULLUP); u8g2.begin();
Wire.begin(kI2cSdaPin, kI2cSclPin); u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化
Wire.setTimeOut(50);
// OLEDが接続されているときだけ初期化と描画を行います。 sensorLogger.begin(32);
uint8_t oledAddress = 0;
if (isI2cDevicePresent(0x3C)) {
oledAddress = 0x3C;
} else if (isI2cDevicePresent(0x3D)) {
oledAddress = 0x3D;
}
if (oledAddress != 0) {
u8g2.setI2CAddress(oledAddress << 1);
u8g2.begin();
u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化
oledReady = true;
Serial.printf("[INFO] OLED detected at 0x%02X\n", oledAddress);
} else {
Serial.println("[WARNING] OLED not detected at 0x3C or 0x3D");
}
}
sensorLogger.begin(32, kI2cConnected, kBleDataConnected);
updateDisplay(); updateDisplay();
lastDisplayUpdate = millis(); lastDisplayUpdate = millis();
@ -292,14 +240,11 @@ void setup() {
// Bluetoothスピーカーとして起動します。 // Bluetoothスピーカーとして起動します。
a2dp_sink.start(kA2dpDeviceName); a2dp_sink.start(kA2dpDeviceName);
Serial.printf("[INFO] A2DP started as '%s'\n", kA2dpDeviceName);
} }
void loop() { void loop() {
// loopは何度も繰り返し実行されます。 // loopは何度も繰り返し実行されます。
if (kNeoPixelPin >= 0) { neoPixelController.update();
neoPixelController.update();
}
bool newReading = sensorLogger.update(); bool newReading = sensorLogger.update();
unsigned long now = millis(); unsigned long now = millis();
if ((!bluetoothConnected || !musicPlaying) if ((!bluetoothConnected || !musicPlaying)