Compare commits
10 Commits
87d111e3d9
...
2e3cd4220a
| Author | SHA1 | Date |
|---|---|---|
|
|
2e3cd4220a | |
|
|
b8eb7bafe8 | |
|
|
8bc573069e | |
|
|
d6105a8f9a | |
|
|
86c2ab9060 | |
|
|
d0131a479b | |
|
|
37eaa5d112 | |
|
|
74bbba5168 | |
|
|
076cb99f9e | |
|
|
9f776a3a9f |
|
|
@ -45,6 +45,7 @@ jobs:
|
|||
run: |
|
||||
arduino-cli lib install "Adafruit AHTX0"
|
||||
arduino-cli lib install "BH1750"
|
||||
arduino-cli lib install "Adafruit NeoPixel"
|
||||
|
||||
# コンパイル実行 (output-dir を指定して構成ファイルを確実に生成)
|
||||
- name: Compile Sketch
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -1,6 +1,14 @@
|
|||
# arduino_esp32_a2dp
|
||||
|
||||
ESP32-WROOM-32E を使用した、Arduino版 A2DP Bluetoothスピーカーのファームウェアリポジトリです。GitHub Actions を利用して、ブラウザ上だけでファームウェアの書き換えとビルドが行えます。
|
||||
ESP32-WROOM-32E を使用した、Arduino版 A2DP Bluetoothスピーカーのファームウェアリポジトリです。
|
||||
GitHub Actions を利用して、ブラウザ上だけでファームウェアの書き換えとビルドが行えます。
|
||||
|
||||
ESP32-WROOM-32E 用の Bluetooth スピーカー主な機能:
|
||||
|
||||
- NeoPixcel:デバイスの動作モードに合わせて発光色を変化
|
||||
- 音楽再生時: 楽曲名、歌手名、音量を表示
|
||||
- 音楽停止時: 各種センサー値(温度・湿度・照度・土壌水分)を表示
|
||||
- 環境データ出力: 1時間ごとにデータを蓄積し、Webブラウザから Bluetooth 経由でデータをダウンロード(CSV形式で保存可能)
|
||||
|
||||
## 手順
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <Wire.h>
|
||||
#include <time.h>
|
||||
|
||||
namespace {
|
||||
|
|
@ -24,6 +25,11 @@ BH1750 bh1750;
|
|||
bool aht20Ready = false;
|
||||
bool bh1750Ready = false;
|
||||
|
||||
bool isI2cDevicePresent(uint8_t address) {
|
||||
Wire.beginTransmission(address);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool isLeapYear(int year) {
|
||||
// 4年ごと。ただし100年ごとは除き、400年ごとはうるう年です。
|
||||
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
|
||||
|
|
@ -52,6 +58,7 @@ public:
|
|||
void onConnect(BLEServer*) override {
|
||||
// 接続中は、画面に接続中であることを表示できます。
|
||||
logger_->bluetoothConnected_ = true;
|
||||
Serial.println("[INFO] BLE client connected");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
|
|
@ -60,54 +67,77 @@ public:
|
|||
logger_->commandBuffer_ = "";
|
||||
logger_->lastAdvertise_ = millis();
|
||||
server->startAdvertising();
|
||||
Serial.println("[WARNING] BLE client disconnected; advertising restarted");
|
||||
}
|
||||
|
||||
private:
|
||||
SensorLogger* logger_;
|
||||
};
|
||||
|
||||
void SensorLogger::begin(uint8_t soilPin) {
|
||||
void SensorLogger::begin(uint8_t soilPin, bool i2cConnected, bool bleDataConnected) {
|
||||
// センサーとBLE通信を使い始めるための初期設定です。
|
||||
soilPin_ = soilPin;
|
||||
bleDataConnected_ = bleDataConnected;
|
||||
pinMode(soilPin_, INPUT);
|
||||
analogReadResolution(12);
|
||||
|
||||
// センサーの初期化に成功したかを保存します。
|
||||
aht20Ready = aht20.begin();
|
||||
bh1750Ready = bh1750.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
|
||||
// センサーが接続されているときだけ初期化します。
|
||||
// 未接続時にライブラリへアクセスすると、不要なI2Cエラーや待ち時間が発生します。
|
||||
if (i2cConnected) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
if (bleDataConnected_) {
|
||||
// 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());
|
||||
Serial.printf("[INFO] BLE initialized as '%s'\n", 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サーバーと、センサー通信用のサービスを作ります。
|
||||
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);
|
||||
advertising->setMinPreferred(0x06);
|
||||
advertising->setMinPreferred(0x12);
|
||||
BLEDevice::startAdvertising();
|
||||
// BLE広告は「この機器が近くにいます」と周囲へ知らせる仕組みです。
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(kNusServiceUuid);
|
||||
advertising->setScanResponse(true);
|
||||
// A2DP開始後に共通のBluetooth名が変わっても、BLE名を返せるようにします。
|
||||
BLEAdvertisementData scanResponseData;
|
||||
scanResponseData.setName(bluetoothName_.c_str());
|
||||
advertising->setScanResponseData(scanResponseData);
|
||||
advertising->setMinPreferred(0x06);
|
||||
advertising->setMinPreferred(0x12);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("[INFO] BLE advertising started");
|
||||
}
|
||||
|
||||
// 起動直後にも1回計測して、画面に値を表示できるようにします。
|
||||
readAndStore();
|
||||
|
|
@ -117,10 +147,13 @@ void SensorLogger::begin(uint8_t soilPin) {
|
|||
|
||||
bool SensorLogger::update() {
|
||||
// loopから繰り返し呼ばれ、通信と定期計測を担当します。
|
||||
processBluetooth();
|
||||
if (bleDataConnected_) {
|
||||
processBluetooth();
|
||||
}
|
||||
|
||||
unsigned long now = millis();
|
||||
if (!bluetoothConnected_ && now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) {
|
||||
if (bleDataConnected_ && !bluetoothConnected_
|
||||
&& now - lastAdvertise_ >= kAdvertiseRetryIntervalMs) {
|
||||
// 接続されていなければ、定期的にBLE広告を出し直します。
|
||||
lastAdvertise_ = now;
|
||||
BLEDevice::startAdvertising();
|
||||
|
|
@ -263,6 +296,10 @@ void SensorLogger::processBluetooth() {
|
|||
}
|
||||
|
||||
void SensorLogger::receiveBluetoothData(const String& data) {
|
||||
if (!bleDataConnected_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// BLEで届いた文字を命令バッファへ追加します。
|
||||
for (size_t index = 0; index < data.length(); ++index) {
|
||||
char character = data[index];
|
||||
|
|
@ -270,6 +307,8 @@ void SensorLogger::receiveBluetoothData(const String& data) {
|
|||
commandBuffer_ += character;
|
||||
} else if (commandBuffer_.length() < 64) {
|
||||
commandBuffer_ += character;
|
||||
} else if (commandBuffer_.length() == 64) {
|
||||
Serial.println("[ERROR] BLE command is too long and was truncated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,6 +332,10 @@ void SensorLogger::applyHistoricalTimeAdjustment(int64_t epoch) {
|
|||
}
|
||||
|
||||
void SensorLogger::processCommand(const String& command) {
|
||||
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// アプリから届いた命令を判定します。
|
||||
if (command == "GET_LOG") {
|
||||
// 保存済みのログを順番に送ります。
|
||||
|
|
@ -329,13 +372,20 @@ void SensorLogger::processCommand(const String& command) {
|
|||
txCharacteristic_->setValue("OK_TIME\n");
|
||||
txCharacteristic_->notify();
|
||||
} else {
|
||||
Serial.printf("[ERROR] Invalid TIME command: %s\n", command.c_str());
|
||||
txCharacteristic_->setValue("ERROR_TIME\n");
|
||||
txCharacteristic_->notify();
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[WARNING] Unknown BLE command: %s\n", command.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void SensorLogger::sendLog() {
|
||||
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存したログを1行ずつ作り、BLE通知で送ります。
|
||||
for (size_t index = 0; index < logCount_; ++index) {
|
||||
size_t logIndex = (logStart_ + index) % kMaxLogEntries;
|
||||
|
|
@ -360,6 +410,10 @@ void SensorLogger::sendLog() {
|
|||
}
|
||||
|
||||
void SensorLogger::sendNotification(const char* data) {
|
||||
if (!bleDataConnected_ || txCharacteristic_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// BLE通知には一度に送れる長さの制限があるため、短く分割します。
|
||||
constexpr size_t kNotificationSize = 20;
|
||||
uint8_t notification[kNotificationSize];
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public:
|
|||
static constexpr size_t kMaxLogEntries = 2000;
|
||||
|
||||
// センサー、BLE、保存データを使い始める準備をします。
|
||||
void begin(uint8_t soilPin);
|
||||
void begin(uint8_t soilPin, bool i2cConnected, bool bleDataConnected);
|
||||
// BLEの受信と、一定時間ごとの計測を行います。
|
||||
bool update();
|
||||
// センサーを読み取り、画面表示用の最新値を更新します。
|
||||
|
|
@ -69,6 +69,7 @@ private:
|
|||
bool timeSynchronized_ = false; // 時刻合わせが済んだか
|
||||
bool historicalAdjustmentApplied_ = false; // 過去ログの補正が済んだか
|
||||
bool bluetoothConnected_ = false; // BLE接続中か
|
||||
bool bleDataConnected_ = false; // BLEデータ送受信を使うか
|
||||
String bluetoothName_; // BLEに表示する名前
|
||||
String commandBuffer_; // BLEから受け取った未処理の文字
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
// 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の仕組みです。
|
||||
#include "BluetoothA2DPSink.h"
|
||||
#include <Wire.h>
|
||||
|
|
@ -6,7 +13,12 @@
|
|||
#include "SensorLogger.h"
|
||||
|
||||
// スマートフォンのBluetooth一覧に表示されるスピーカー名です。
|
||||
const char kA2dpDeviceName[] = "BT_Speaker5.5";
|
||||
const char kA2dpDeviceName[] = "BT_Speaker5.7";
|
||||
// I2C接続のSDA/SCLピンです。配線に合わせてここだけ変更します。
|
||||
// 通常基板:SDA=21, SCL=22
|
||||
// 開発用基板:SDA=19, SCL=5
|
||||
const uint8_t kI2cSdaPin = 21;
|
||||
const uint8_t kI2cSclPin = 22;
|
||||
|
||||
// I2Cで接続した128x64ドットのOLED画面です。
|
||||
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
|
||||
|
|
@ -23,6 +35,12 @@ int currentVolumePercent = 0; // 音量(0〜100%)
|
|||
bool bluetoothConnected = false;
|
||||
bool musicPlaying = false;
|
||||
unsigned long lastDisplayUpdate = 0;
|
||||
bool oledReady = false;
|
||||
|
||||
bool isI2cDevicePresent(uint8_t address) {
|
||||
Wire.beginTransmission(address);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
// UTF-8の文字列を1文字ずつ描画します。
|
||||
// フォントにない文字は、フォントに頼らず四角形を直接描きます。
|
||||
|
|
@ -93,6 +111,10 @@ void drawUtf8WithTofu(uint8_t x, uint8_t y, const String& text) {
|
|||
|
||||
// OLEDの内容を現在の状態に合わせて描き直します。
|
||||
void updateDisplay() {
|
||||
if (!oledReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
u8g2.clearBuffer();
|
||||
|
||||
// 音楽を再生していないときは、センサーの情報を表示します。
|
||||
|
|
@ -173,6 +195,8 @@ void avrc_metadata_callback(uint8_t id, const uint8_t *text) {
|
|||
} else if (id == ESP_AVRC_MD_ATTR_ARTIST) {
|
||||
currentArtist = (char*)text;
|
||||
updated = true;
|
||||
} else {
|
||||
Serial.printf("[WARNING] Unsupported AVRCP metadata id: %u\n", id);
|
||||
}
|
||||
|
||||
if (updated && bluetoothConnected && musicPlaying) {
|
||||
|
|
@ -191,13 +215,19 @@ 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);
|
||||
Serial.printf("[INFO] Audio state changed: %d (playing=%s)\n",
|
||||
state, musicPlaying ? "yes" : "no");
|
||||
if (kNeoPixelPin >= 0) {
|
||||
neoPixelController.setPlaying(musicPlaying);
|
||||
}
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
void connection_state_changed_callback(esp_a2d_connection_state_t state, void *) {
|
||||
// スマートフォンとの接続状態を保存します。
|
||||
bluetoothConnected = state == ESP_A2D_CONNECTION_STATE_CONNECTED;
|
||||
Serial.printf("[INFO] A2DP connection state changed: %d (connected=%s)\n",
|
||||
state, bluetoothConnected ? "yes" : "no");
|
||||
if (!bluetoothConnected) {
|
||||
musicPlaying = false;
|
||||
}
|
||||
|
|
@ -207,17 +237,39 @@ void connection_state_changed_callback(esp_a2d_connection_state_t state, void *)
|
|||
void setup() {
|
||||
// setupは、電源を入れた直後に1回だけ実行されます。
|
||||
Serial.begin(115200);
|
||||
Serial.println("[INFO] Firmware starting");
|
||||
|
||||
neoPixelController.begin(18);
|
||||
if (kNeoPixelPin >= 0) {
|
||||
neoPixelController.begin(static_cast<uint8_t>(kNeoPixelPin));
|
||||
}
|
||||
|
||||
// I2Cの配線を設定します。Wire.begin(SDA, SCL)の順です。
|
||||
Wire.begin(21, 22);
|
||||
if (kI2cConnected) {
|
||||
// I2Cの配線を設定します。Wire.begin(SDA, SCL)の順です。
|
||||
// OLED未接続時もSDA/SCLが浮かないようにします。
|
||||
pinMode(kI2cSdaPin, INPUT_PULLUP);
|
||||
pinMode(kI2cSclPin, INPUT_PULLUP);
|
||||
Wire.begin(kI2cSdaPin, kI2cSclPin);
|
||||
Wire.setTimeOut(50);
|
||||
|
||||
// OLEDを使える状態にします。
|
||||
u8g2.begin();
|
||||
u8g2.enableUTF8Print(); // UTF-8(日本語)描画を有効化
|
||||
// OLEDが接続されているときだけ初期化と描画を行います。
|
||||
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);
|
||||
sensorLogger.begin(32, kI2cConnected, kBleDataConnected);
|
||||
updateDisplay();
|
||||
lastDisplayUpdate = millis();
|
||||
|
||||
|
|
@ -240,11 +292,14 @@ void setup() {
|
|||
|
||||
// Bluetoothスピーカーとして起動します。
|
||||
a2dp_sink.start(kA2dpDeviceName);
|
||||
Serial.printf("[INFO] A2DP started as '%s'\n", kA2dpDeviceName);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// loopは何度も繰り返し実行されます。
|
||||
neoPixelController.update();
|
||||
if (kNeoPixelPin >= 0) {
|
||||
neoPixelController.update();
|
||||
}
|
||||
bool newReading = sensorLogger.update();
|
||||
unsigned long now = millis();
|
||||
if ((!bluetoothConnected || !musicPlaying)
|
||||
|
|
|
|||
Loading…
Reference in New Issue