※2018/12/13 に初回投稿したときは Arduino-ESP32 1.0.0 でやってますが、1.0.1 になったらコード変えないといけない(BLEClient::connect を呼ぶとき AdvetisedDevice を渡す関数にするか、BLEAddress と「type = BLE_ADDR_TYPE_RANDOM」をきちんと渡すかする必要がある) なので 1.0.1 狙いの方は追記まで目を通してください。
#これまでのあらすじ
ESP32-DevKitC を自作のニキシー管時計に仕込めば wifi で自動時計合わせとかできて便利じゃない?って思ったのも束の間、消費電力多いときは 600mA オーバーってそんなの常時起動してたくないよーと思った私たちは、時計側を BLEペリフェラルにして ESP32-DevKitC が wifi で知った時刻をBLEセントラルとしてペリフェラルに教える構成を考える。
安くて(ESP32ほどじゃないけど)小さくて消費電力もだいじょぶそうでマイコンとUARTでやりとりできそうな AE-TYBLE16 を知ったはいいけど、搭載されている太陽誘電の EYSGJNAWY_WX はなんでかネットの情報が少ない。ていうか買ってユーザー登録した人にしか見せないドキュメントがあるので、その中身は記事に出来ない。
どうにかして開示されている brief data report とAndroid/iOS側 Central サンプルソースコード だけで備忘録書けないかなぁ?
#おことわり
TAIYO YUDEN Original Service UUID、TAIYO YUDEN Characteristic UUID については、公開されている brief data report には記載がなく、購入者向けの data report には載っているので、このサイトの記事では記載しないこととします。Source Code of "Terminal Application for Android" の Terminal.java に書いてありますので、そちらをご参照下さい。
(公開されているサンプルコードに書いてあるとはいえ今後公開停止されることもあろうかと言う事で)
#ESP32 で WiFi と Bluetooth (Classic or BLE)は一緒に使うのは大変らしい
なんか SW_COEXIST_ENABLE とかでぐぐればできるらしいけど、自分の用途では同時に両方通信する必要がないので片方ずつ使うコードにする。
WiFi 側はこんな感じ。抜粋だけど 空の loop() つけりゃ動くんじゃないかしら。ぐぐったら出てくるから詳細ははぶくけど最後の WiFi.disconnect(true);
のパラメータは true で wifi には深い眠りについてもらう。
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiServer.h>
#include <WiFiUdp.h>
const char *ssid = "SSID";
const char *password = "PASS";
void setup() {
struct tm timeInfo;
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
if (WiFi.begin(ssid, password) != WL_DISCONNECTED) {
ESP.restart();
}
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
while (1) {
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
delay(500);
getLocalTime(&timeInfo);
if ( timeInfo.tm_year > 100 ) break; // after A.D. 2000
}
WiFi.disconnect(true);
getLocalTime(&timeInfo);
Serial.printf( "CurrentTime: %04d/%02d/%02d %02d:%02d:%02d\r\n",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
}
#EYSGJNAWY_WX の brief data report から解る、AE-TYBLE16 の pin
Breif Data Report v1.3 (TY_BLE_EYSGJNAWY_WX_BriefDataReport_V1_3_20170925J.pdf) を読むと、15/41 ページにピンの説明がある。必要そうなのは
Pin Name | Pin Function |
---|---|
P0.00 | UART_RTS |
P0.01 | UART_TX |
P0.02 | UART_CTS |
P0.03 | UART_RX |
AE-TYBLE16 モジュール上にはP0.00 とか書いてあるので使うピンが解る。フロー制御しないのでCTSとRTSは短絡。
TXは ATMEGA328PのRXへ、RXはATMEGA328PのTX……だと電圧合わないので分圧抵抗なりでレベルシフトモジュールなりであわせる。もちろんV+とGNDに電源。
#advertised services はいくつ?どれ? Arduino-ESP32 のライブラリを書き直して調べる
BLE_Client.ino サンプルプログラムで使われている変数 advertisedDevice
は class BLEAdvertisedDevice
で、private なメンバ変数として std::vector<BLEUUID> m_serviceUUIDs
ってのを持っているのが、外部からはこれに複数の serviceUUID が入っていても参照できない(isAdvertisingService関数で、指定したUUIDが含まれるかどうかは解るが。)
しょうがないので調査段階では以下の2つのメンバ関数をライブラリのソースに書き足した。(最終のプログラムではisAdvertisingServiceを使うので読者が真似する必要は無い。)
//BLEAdvertisedDevice の定義内 public の uint8_t* ,getPayload(); の下にでも
int getServiceUUIDCount();
BLEUUID getServiceUUID(uint8_t n);
int BLEAdvertisedDevice::getServiceUUIDCount() {
return m_serviceUUIDs.size();
}
BLEUUID BLEAdvertisedDevice::getServiceUUID(uint8_t n) {
if ( n < m_serviceUUIDs.size() ) {
return m_serviceUUIDs[n];
}
return m_serviceUUIDs[0];
} // getServiceUUID
で、サンプルが見つけた device, service を表示させてみると、AE-TYBLE16 が advertise していて、ESP32が自分が用意したコードで見つけられたサービスは下記2つ(独自ではない標準的なサービスUUID)であった。これが仕様なのか等は不明。
TX Power: "00001804-0000-1000-8000-00805f9b34fb"
Battery : "0000180f-0000-1000-8000-00805f9b34fb"
これらはEYSGJNAWY_WX のサンプルコードに載っている太陽誘電独自のものとは違っているので、「何で対象となるデバイスを発見したと判定するか」「接続したときどのサービスを指定するか」で異なるサービスUUID を使うことにした。(それがスマートな解決方法なのかは怪しいと思っているが、まあいいでしょう)
#EYSGJNAWY_WX のサンプルコードから解る、SPPっぽい通信のクライアント(セントラル)側コード
必要なUUID はサンプルコード(Android Central用なら Terminal.java )に書いてある4つ+前述 Advertise される2つ。
//の抜粋引用
private static final UUID CCCD = UUID.fromString("元のファイルを直接ご参照ください");
public static final UUID TY_SERVICE_UUID = UUID.fromString("元のファイルを直接ご参照ください"); // Taiyo Yuden Service UUID
public static final UUID RX_CHARACTERISTIC_UUID = UUID.fromString("元のファイルを直接ご参照ください"); // Taiyo Yuden Rx Characteristic
public static final UUID TX_CHARACTERISTIC_UUID = UUID.fromString("元のファイルを直接ご参照ください"); // Taiyo Yuden Tx Characteristic
このTY_SERVICE_UUID はうちのESP32はadvertised の段階では見つけられなかったので、名前とかで特定して繋ぎに行くこととする。
Centralは
1.デバイス探して見つけて名前とかサービスとかで確認して Connect
2.onConnect でサービスオブジェクト、TX/RXのcharacteristic objectを手配して (sample の OnServiceConnected)
3.さらにRX characteristicの descriptor に BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE なる値を write して通知を有効にする(sample のpageInit)
4.以後、ESP32からの送信は TX characteristicへの write, 受信はRX characteristicの notification で処理。
って感じ。
#BLE_Client.ino サンプルを見ながら実装
前項で書いた中で、BLE_Client.ino と違うのは
・BLE_Client.inoは TX/RX のcharacteristic が別々になってない。
BLERemoteCharacteristic のインスタンス?が2つになるだけで、大した話ではない。
・descriptor に write するって部分が BLE_Client.ino にはない。
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE は調べると 0x01 0x00 の2バイトっぽい。
Arduino IDEのライブラリのソースを漁って調べて以下のように実装。大した話ではない。
BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(uuidCCCD);
if (pDescriptor != nullptr ) {
uint8_t data[2] = {0x01, 0x00}; //Notify
pDescriptor->writeValue(data, 2, false);
Serial.println("Write Descriptor for notification.");
・AE-TYBLE16は 1つの device に複数の service がある。
BLE_Client.ino サンプル通りにやると TX Power の Service が見つかる。そんなんだけで AE-TYBLE16であると特定した気になるわけにもいかないので、名前(デフォルトでは "TYSA-B 3.0.0",変更可能)も確認する。よって MyAdvertisedDeviceCallbacks でのデバイス発見判定は以下のような感じに。
if (advertisedDevice.haveServiceUUID() &&
advertisedDevice.isAdvertisingService(uuidTY_SERVICE1) && // Tx Power
advertisedDevice.haveName() &&
advertisedDevice.getName() == std::string("TYSA-B 3.0.0") ) {
#ソースと実行結果
例によってソースは sample にあった不要行の消し忘れとかありそうなやーつ。
どういう順番で処理されるかは Step1, Step2 みたいにコメントしてあるので Step で検索。
長いソース: ESP32_ntp_clock_client.ino
#define VERBOSE 1
#include "BLEDevice.h"
static BLEUUID uuidCCCD( "太陽誘電のサンプルコードに書いて有るとおり" );
static BLEUUID uuidTY_SERVICE2( "太陽誘電のサンプルコードに書いて有るとおり" ); // Taiyo Yuden Service UUID
static BLEUUID uuidTY_SERVICE1( "00001804-0000-1000-8000-00805f9b34fb" ); // TX Power
static BLEUUID uuidRX_CHARACTERISTIC( "太陽誘電のサンプルコードに書いて有るとおり"); // Taiyo Yuden Rx Characteristic, incoming
static BLEUUID uuidTX_CHARACTERISTIC( "太陽誘電のサンプルコードに書いて有るとおり" ); // Taiyo Yuden Tx Characteristic to write
static BLEClient *pCurrentClient;
static BLEAddress *pServerAddress;
static boolean doConnect = false;
static boolean connected = false;
static BLERemoteService* pRemoteService;
static BLERemoteCharacteristic* pRemoteCharacteristicTX = NULL; // TX とRX2つ分
static BLERemoteCharacteristic* pRemoteCharacteristicRX;
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiServer.h>
#include <WiFiUdp.h>
const char *ssid = "SSID";
const char *password = "PASS";
const unsigned long write_interval = 1000 * 5; // 3 seconds
unsigned long last_write = 0;
static void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
// Step7 返事が来たら Serial に報告(返事が来るようにペリフェラル側を作ってある)
static String strCallback;
for ( int i = 0; i < length; i++ ) {
strCallback += (char)(pData[i]);
if ( pData[i] == (uint8_t)'\n' ) {
Serial.print( "Notified: String: " );
Serial.print( strCallback.c_str() );
strCallback = String("");
}
}
}
class MyClientCallbacks : public BLEClientCallbacks {
void onConnect(BLEClient *pClient) {
Serial.println( "ClientCallback: onConnect" );
pCurrentClient = pClient;
connected = true;
return;
}
void onDisconnect(BLEClient *pClient) {
Serial.println( "ClientCallback: onDisconnect, rebooting in 10 sec..." );
connected = false;
pRemoteCharacteristicTX = NULL;
pRemoteService = NULL;
pClient->disconnect();
delay(10000);
ESP.restart();
}
};
bool connectToServer(BLEAddress pAddress) { // Step4 BLEClient 作って client callback用意して(大した事しないけど), connect
Serial.print("Forming a connection to ");
Serial.println(pAddress.toString().c_str());
BLEClient* pClient = BLEDevice::createClient();
if ( pClient == nullptr ) {
Serial.println( "Failed to create client object." );
return false;
}
Serial.println(" - Created client");
pClient->setClientCallbacks(new MyClientCallbacks());
Serial.println(" - registerd MyClientCallbacks, trying to connect");
pClient->connect(pAddress);
// Serial.println(" - connected?");
delay(100);
if ( !connected ) return false;
return true;
}
/**
* Scan for BLE servers and find the first one that advertises the service we are looking for.
*/
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
/**
* Called for each advertising BLE server.
*/
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.print("BLE Advertised Device found: ");
Serial.println(advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(uuidTY_SERVICE1) && advertisedDevice.haveName() && advertisedDevice.getName() == std::string("TYSA-B 3.0.0") ) {
// Step2 scan で対象デバイスが見つかったら scan を止めて、BLEAddress を保持して、 connect すべしというフラグ(doConnect)をtrueに。
#if VERBOSE
Serial.print("Found our device! Address:");
Serial.println(advertisedDevice.getAddress().toString().c_str());
/* ここは、ライブラリ改造した場合のコード。qiita の本文参照
Serial.print( " #ofService: " );
Serial.println( advertisedDevice.getServiceUUIDCount() );
for ( int i = 0 ; i < advertisedDevice.getServiceUUIDCount(); i++ ) {
Serial.print(" - " );
Serial.println( advertisedDevice.getServiceUUID(i).toString().c_str() );
}
*/
#endif
advertisedDevice.getScan()->stop();
pServerAddress = new BLEAddress(advertisedDevice.getAddress());
doConnect = true;
} // Found our server
} // onResult
}; // MyAdvertisedDeviceCallbacks
void setup() {
struct tm timeInfo;
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
if (WiFi.begin(ssid, password) != WL_DISCONNECTED) {
ESP.restart();
}
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
while (1) {
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
delay(500);
getLocalTime(&timeInfo);
if ( timeInfo.tm_year > 100 ) break; // after A.D. 2000
}
WiFi.disconnect(true);
getLocalTime(&timeInfo);
Serial.printf( "CurrentTime: %04d/%02d/%02d %02d:%02d:%02d\r\n",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
// Step1 setup() では、 BLE はデバイスを初期化して、callback 登録してscan 開始まで
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
}
void loop() {
struct tm timeInfo;
char s[40];
String res;
getLocalTime(&timeInfo);
Serial.printf( "%04d/%02d/%02d %02d:%02d:%02d\r\n",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
delay(100);
// Step3 doConnect が true であれば接続しにいく。
if (doConnect == true && connected != true ) {
if (connectToServer(*pServerAddress)) {
#if VERBOSE
Serial.println("We are now connected to the BLE Server.");
#endif
}
else {
Serial.println("We have failed to connect to the server; there is nothin more we will do.");
delay(10000);
ESP.restart();
}
doConnect = false;
}
#if 1
if (doConnect == true && connected != true ) { // なんで2回やってるんだっけ?たぶん #if 0 でいいんだと思う
if (connectToServer(*pServerAddress)) {
Serial.println("We are now connected to the BLE Server.");
}
else {
Serial.println("We have failed to connect to the server; there is nothin more we will do.");
res = Serial.readStringUntil( '\n' );
if ( res.toInt() > 0 || last_write == 0 ) {
Serial.println( "delay 10 sec" );
delay(10000);
}
else {
Serial.println( "delay 60 sec" );
delay( 1000 * 60 );
}
ESP.restart();
}
doConnect = false;
}
#endif
// Step5 接続できたら、太陽誘電のサービスが使えることを確認して、
// TX,RXの Characteristic 取って、
// RXの BLERemoteDescriptor 取って 0x01 0x00 を write( notify をenable )
// notify callback も登録(これで接続完了)
if ( connected && pCurrentClient && pRemoteService == NULL ) {
Serial.print("Client getting Service... ");
// Obtain a reference to the service we are after in the remote BLE server.
pRemoteService = pCurrentClient->getService(uuidTY_SERVICE2);
if (pRemoteService == nullptr) {
Serial.print("Failed to find our service UUID: ");
Serial.println(uuidTY_SERVICE2.toString().c_str());
return;
}
Serial.print(" - Found our service:");
Serial.println( pRemoteService->toString().c_str() );
}
if ( connected && pRemoteService != NULL && pRemoteCharacteristicTX == NULL ) {
Serial.print("getting Characteristic... ");
// Obtain a reference to the characteristic in the service of the remote BLE server.
pRemoteCharacteristicRX = pRemoteService->getCharacteristic(uuidRX_CHARACTERISTIC);
if (pRemoteCharacteristicRX == nullptr) {
Serial.print("Failed to find our characteristic UUID: ");
Serial.println(uuidRX_CHARACTERISTIC.toString().c_str());
return;
}
Serial.print(" - Found our RX characteristic: ");
Serial.println(pRemoteCharacteristicRX->toString().c_str());
pRemoteCharacteristicTX = pRemoteService->getCharacteristic(uuidTX_CHARACTERISTIC);
if (pRemoteCharacteristicTX == nullptr) {
Serial.print("Failed to find our characteristic UUID: ");
Serial.println(uuidTX_CHARACTERISTIC.toString().c_str());
return;
}
Serial.print(" - Found our TX characteristic: ");
Serial.println(pRemoteCharacteristicTX->toString().c_str());
delay(100);
#if VERBOSE
Serial.println("Getting Descriptor ");
#endif
BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(uuidCCCD);
if (pDescriptor != nullptr ) {
uint8_t data[2] = {0x01, 0x00}; //Notify
//uint8_t data[2] = {0x02, 0x00}; //Indicate
pDescriptor->writeValue(data, 2, false);
Serial.println("Write Descriptor for notification.");
delay(200); // Here must be delay !
pRemoteCharacteristicRX->registerForNotify(notifyCallback);
Serial.println("NotifyCallbackFunction Registered");
}
else {
Serial.println("null Descriptor ");
delay(200000);// wait for onDisconnect
}
}
// If we are connected to a peer BLE Server, update the characteristic each time we are reached
// with the current time since boot.
// Step6 現在の日時を文字列にして pRemoteCharacteristicTX に writeValue (一度に20bytesまで送れるみたい),返事は notify callbackにて
// writeValue の後は delay してから再起動。(シリアルの受信内容で動作が変わるのは実験用に仕込んだもので、実用上の意味はない)
if (connected && pRemoteCharacteristicTX) {
doConnect == false;
if ( last_write == 0 || ( millis() > write_interval + last_write ) ) {
getLocalTime(&timeInfo);
sprintf(s, "%04d/%02d/%02d %02d:%02d:%02d",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
Serial.print("Setting new characteristic value to current time: " );
Serial.println( s );
// Set the characteristic's value to be the array of bytes that is actually a string.
pRemoteCharacteristicTX->writeValue( (uint8_t*)s, strlen(s), true );
delay(50);
pRemoteCharacteristicTX->writeValue( (uint8_t*)"\r\n", 2, true );
last_write = millis();
// delay 時間を変えてるだけでどっちみち writeValue の後は ESP.restart
Serial.println( "delay 60 sec before restart" );
delay( 1000 * 60 );
ESP.restart();
}
}
// Step8 connect できなかった(ペリフェラルが見つからない場合等)はここにくる。無限に来るので抜ける手段を用意してあるがリセットボタンのが早い
Serial.printf("doConnect: %d , connected: %d ", doConnect, connected);
res = Serial.readStringUntil( '\n' );
if ( res.toInt() > 0 ) {
ESP.restart();
}
delay(150);
}
実行結果(Serial.print の出力)はなんか保存してなかった…そのうち載せるかも……
#その他
このソースで Arduino-ESP32 1.0.0 で動いてるが、2018/12/10現在、開発版の Arduino-ESP32 1.0.1-rc1 にすると動かない(BLEClient の connect event 待ちで帰ってこなくなる)
#注
2019/2/3 時点、解決済みなのでそこまで読み飛ばして大丈夫です。
#追記1 2019/1/4
Arduino-ESP32 1.0.1-rc2 でもやはりダメなのだが、ここらへん で更新された BLEClient.cpp を使うといけるかもしれん(試してない)
#追記2 2019/1/13
1.0.1 が正式版になったけどやっぱりBLEClient の connect event 待ちで帰ってこなくなる。どうも m_semaphoreOpenEvt.wait してるのに、 disconnect が先に来てしまうのが問題のようなので
case ESP_GATTC_DISCONNECT_EVT: {
// If we receive a disconnect event, set the class flag that indicates that we are
// no longer connected.
if ( !m_isConnected) m_semaphoreOpenEvt.give(evtParam->open.status);
とかして、m_isConnected == false なのに DISCONNECT_EVT 来たら OpenEvt.give してやりつつ
uint32_t rc = m_semaphoreOpenEvt.wait("connect"); // Wait for the connection to complete.
if ( !m_isConnected ) {
ESP_LOGE(LOG_TAG, "m_semaphoreOpenEvt.wait terminated by disconnection" );
ESP_LOGD(LOG_TAG, "<< connect(), rc=%d", rc==ESP_GATT_OK);
return false;
}
こちらも、m_isConnected 立ってないのに OpenEvt 来たら失敗したんやなーという処理をしてやると、とりあえずフリーズはしない。
そして失敗してもまた BLEScan するようにしてみたら、2回目の試行ではちゃんと接続できる。なんなんだ。わからん。
わからんけど twitter でボヤいたら反応あったので別記事にしといた。
https://qiita.com/ajtajta_j/items/1ae50f189967214c10e9
#さらに続報 2019/2/1
Arduino-ESP32 1.0.1 でやる場合は
https://github.com/nkolban/esp32-snippets/issues/767 にて
wakwak-koba さんが指摘されているとおり BLEClient::connect( BLEAddress address, esp_ble_addr_type_t type )
の type
を適切に設定する、
または
chegewara さんが挙げているとおり BLEClient::connect( BLEAdvertisedDevice* device )
のほうで呼ぶ
ことで回避できる、気がします(私の最新のコードは( BLEAdvertisedDevice* device )
のほうで呼んでいるから動いてる説)
自分で検証はまだできてませんが、取り急ぎ。
#1.0.1対応、最終版(?) 2019/2/3
検証しました。
// "BLEClient.h" (1.0.1)
bool connect(BLEAdvertisedDevice* device);
bool connect(BLEAddress address, esp_ble_addr_type_t type = BLE_ADDR_TYPE_PUBLIC); // Connect to the remote BLE Server
1.0.1 の connect は2種類あるのですが、「device を指定する方法を使う」か「アドレスとタイプを指定する場合はタイプも指定する(AE-YTBLE 16 の場合は BLE_ADDR_TYPE_RANDOM
)」で解決します。
逆にダメなのは「アドレスを指定して type を指定しない」場合です。
1.0.0 のときは
bool connect(BLEAddress address); // Connect to the remote BLE Server
こうなっていたので、そのときのコードをそのまま流用してると動かない。(そもそも、1.0.0は BLE_ADDR_TYPE_PUBLIC
決めうちだったっぽくて、なんで動いていたのか解らないのですが…)
どうだと動いてどうだと動かないか解ったっぽいので、これにて解決。