初めはぼやきから・・・
本当は、STM32系列のボードをいじった話を先に出そうと思っていたのですが、2020年頃からだいぶSTM32開発環境の世界が変わっているようで追試が十分できず(統合開発環境であるSW4STM32をSTM32CubeMXの最新バージョンから選択できなくなってるーdisable扱いにされてるー・・・のに後継になるはずのSTM32CubeIDEのインストーラがなぜか私のWindows 10マシンでは起動できず・・・なのです)。まだしばらくお蔵入りになりそうです。STM32系の開発ボードってそこそこ国内でも入手可能ですが、結構独自開発しようとするとハードル高そうですね(旧来の環境構築の記事とメチャクチャ混ざってるし~。ドキュメントが英語なのは良いとしても、その中で「いつまでサポートするか分かんないから、こっち使って」とか書いてあるし・・・とかで)。というようなわけで、STM32WB55をいじってみた記事はもう少々お待ちください(だれも期待してないのは知ってますよ。自分に言い聞かせてます。笑)
ということで、今回はESP32 Bluetooth Low Energyの使い方の1つとして、1台のESP32プロセッサで複数サービスに対応させる、という話にします・・・はあ。
サンプルコードはいっぱいあるのですが・・・
ESP32 BLE上でシリアルプロファイルっぽい挙動の通信ができるサンプルコードは世の中にたくさん転がっています(一度書けば大体同じになるので)。ところが、それって元祖をたどればespressifのble_spp_serverに基づいたコードなので、サービスは1個、その下にキャラクタリスティックが2個ある構成のがほとんどなのです。SPP Serverに当たるモノを提供しつつ、バッテリー管理もしたい、となると(まぁble_spp_server経由で独自メッセージを流しちゃいけないってわけではないのですけれど)本来はbatteryに関わるサービスとキャラクタリスティック値が予約されているので、そっちを使いたいなあと思ったわけなのですけれど。

一応できます。が、もうちょっと美しく書けると思いますが
ble_spp_serverのソースコードに乗っかるとすると、どういう原理でサービス立ち上げまでが行われているのか、そのコールバック関数の遷移を読める必要があります。これはしょうがないんで、シーケンスを覚えておく他ないと思います。
BLEスタックの初期化を行う関数(app_main関数だったり、各タスクに分けていたり、色々ですが)で、GAP, GATTに関するコールバック関数を設定するAPI呼び出しを必ずしているかと思います。esp_ble_gap_register_callback関数とesp_ble_gatts_register_callback関数がそれです。下のような感じで。
/* コールバック関数の登録 (GAP, GATT Server) */
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret == ESP_OK) {
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Event handlers have been set");
}
}
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Event handlers couldn't be set!");
esp_restart();
}
この登録処理の後でGATTサーバに上記2つのサービスを持ったアプリケーションを登録することになります(アプリケーションIDの定数値PROFILE_APP_IDは0です)。
/* GATTサーバへのアプリケーション登録 */
ret = esp_ble_gatts_app_register(PROFILE_APP_ID);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Couldn't register this application!");
esp_restart();
}
esp_ble_gatts_app_register関数が呼び出されると、GATT上でイベントESP_GATTS_REG_EVTが発生するので、コールバック関数gatt_event_handlerが呼び出されます。この中でESP_GATTS_REG_EVTイベントに対してはble_spp_serverサービス側の属性テーブル(サービスとキャラクタリスティックをまとめて書いたテーブル)をgatt_spp_db, batteryサービス側の属性テーブルをgatt_batt_dbとして、それを登録します。それぞれテーブルが別なので、サービスID値は変更します。
そして、属性テーブルを書き込むタイミングで ESP_GATTS_CREAT_ATTR_TAB_EVTイベントがやっぱりGATT上で発生するので、イベントパラメータからどのサービスIDのテーブル書き込みかを判定してあげれば2つ以上サービスを開始させることができます。以下のようなコード・・・です。赤地部分が主に追記している部分ですね。
#define SPP_SVC_INSTID 0
#define BATT_SVC_INSTID 1
// サービス・キャラクタリスティック UUID
#define ESP_GATT_UUID_SPP 0xABF0
#define ESP_GATT_UUID_DATA_RECEIVE 0xABF1
#define ESP_GATT_UUID_DATA_NOTIFY 0xABF2
// Batteryサービス・キャラクタリスティックUUID
// BatteryサービスはUUID16で0x180F, 電池レベルを取得するキャラクタリスティック値
は0x2A19が予約されている
#define ESP_BATTERY_SERVICE_UUID 0x180F
#define ESP_BATTERY_LEVEL_CHAR 0x2A19
#define CHAR_DECLARATION_SIZE (sizeof(uint8_t))
static const uint16_t PRIM_SERVICE_UUID = ESP_GATT_UUID_PRI_SERVICE;
static const uint16_t CHAR_DECL_UUID = ESP_GATT_UUID_CHAR_DECLARE;
static const uint16_t CHAR_CLIENT_CONFIG_UUID = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
static const uint8_t CHAR_PROP_READ_NOTIFY = ESP_GATT_CHAR_PROP_BIT_READ|ESP_GATT_CHAR_PROP_BIT_NOTIFY;
static const uint8_t CHAR_PROP_READ_WRITE = ESP_GATT_CHAR_PROP_BIT_WRITE_NR|ESP_GATT_CHAR_PROP_BIT_READ
// SPPサービス側属性のインデックス
enum {
SPP_IDX_SERVICE = 0,
SPP_IDX_DATA_RECV_CHAR,
SPP_IDX_DATA_RECV_VAL,
SPP_IDX_DATA_NOTIFY_CHAR,
SPP_IDX_DATA_NOTIFY_VAL,
SPP_IDX_DATA_NOTIFY_CFG,
SPP_IDX_NUM,
};
// Batteryサービス側属性のインデックス
enum {
BATTERY_IDX_SERVICE = 0,
BATTERY_LEVEL_CHAR,
BATTERY_LEVEL_NOTIFY_VAL,
BATTERY_LEVEL_NOTIFY_CFG,
BATTERY_IDX_NUM,
};
// XEBEC_CPS Service & Battery Service
#define CHAR_DECL_SIZE (sizeof(uint8_t))
static const uint16_t SPP_SERVICE_UUID = ESP_GATT_UUID_SPP;
static const uint16_t SPP_PRIM_SERVICE_UUID = ESP_GATT_UUID_PRI_SERVICE;
static const uint16_t SPP_CHAR_DECL_UUID = ESP_GATT_UUID_CHAR_DECLARE;
static const uint16_t SPP_RECEIVE_UUID = ESP_GATT_UUID_DATA_RECEIVE;
static const uint8_t SPP_DATA_RECV_VAL[SPP_DATA_MAXLEN] = {0x00};
static const uint16_t SPP_DATA_NOTIFY_UUID = ESP_GATT_UUID_DATA_NOTIFY;
static uint8_t SPP_DATA_NOTIFY_VAL[SPP_DATA_MAXLEN] = {0x00};
static const uint16_t SPP_DATA_NOTIFY_CFG_UUID = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
static const uint8_t SPP_DATA_NOTIFY_CCC[2] = {0x00, 0x00};
static const uint16_t BATTERY_SERVICE_UUID = ESP_BATTERY_SERVICE_UUID;
static const uint16_t BATTERY_LEVEL_UUID = ESP_BATTERY_LEVEL_CHAR;
static uint8_t BATTERY_LEVEL_VAL[1] = {0x00};
static const uint8_t BATTERY_LEVEL_NOTIFY_CCC[2] = {0x00, 0x00};
static const uint8_t char_prop_read_notify = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
static const uint8_t char_prop_read_write = ESP_GATT_CHAR_PROP_BIT_WRITE_NR | ESP_GATT_CHAR_PROP_BIT_READ;
// 属性追加用構造体
static const esp_gatts_attr_db_t gatt_spp_db[] =
{
/* SPPサービス定義 */
[SPP_IDX_SERVICE] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&PRIM_SERVICE_UUID, ESP_GATT_PERM_READ,
sizeof(SPP_SERVICE_UUID), sizeof(SPP_SERVICE_UUID), (uint8_t *)&SPP_SERVICE_UUID}},
/* Data Receive キャラクタリスティック定義 */
[SPP_IDX_DATA_RECV_CHAR] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_CHAR_DECL_UUID, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, (uint8_t *)&CHAR_PROP_READ_WRITE}},
/* Data Receive キャラクタリスティック値 */
[SPP_IDX_DATA_RECV_VAL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_RECEIVE_UUID, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
SPP_DATA_MAXLEN, sizeof(SPP_DATA_RECV_VAL), (uint8_t *)SPP_DATA_RECV_VAL}},
/* Data Notify キャラクタリスティック定義 */
[SPP_IDX_DATA_NOTIFY_CHAR] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_CHAR_DECL_UUID, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, (uint8_t *)&CHAR_PROP_READ_NOTIFY}},
/* Data Notify キャラクタリスティック値 */
[SPP_IDX_DATA_NOTIFY_VAL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_NOTIFY_UUID, ESP_GATT_PERM_READ,
SPP_DATA_MAXLEN, sizeof(SPP_DATA_NOTIFY_VAL), (uint8_t *)SPP_DATA_NOTIFY_VAL}},
/* Data Notify 設定書き込み定義 */
[XEBEC_CPS_IDX_DATA_NOTIFY_CFG] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_NOTIFY_CFG_UUID ,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
sizeof(uint16_t), sizeof(SPP_DATA_NOTIFY_CCC), (uint8_t *)SPP_DATA_NOTIFY_CCC}},
};
// Batteryサービスの属性追加用構造体
static esp_gatts_attr_db_t gatt_batt_db[] = {
/* Batteryサービス定義 */
[BATTERY_IDX_SERVICE] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&PRIM_SERVICE_UUID, ESP_GATT_PERM_READ,
sizeof(BATTERY_SERVICE_UUID), sizeof(BATTERY_SERVICE_UUID),
(uint8_t *)&BATTERY_SERVICE_UUID}},
/* Battery Levelキャラクタリスティック定義 */
[BATTERY_LEVEL_CHAR] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_CHAR_DECL_UUID, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, (uint8_t *)&CHAR_PROP_READ_NOTIFY}},
/* Battery Levelキャラクタリスティック値 */
[BATTERY_LEVEL_NOTIFY_VAL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&BATTERY_LEVEL_UUID, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, BATTERY_LEVEL_VAL}},
/* Battery Level設定書き込み定義 */
[BATTERY_LEVEL_NOTIFY_CFG] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_NOTIFY_CFG_UUID ,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
sizeof(uint16_t), sizeof(BATTERY_LEVEL_NOTIFY_CCC), (uint8_t *)BATTERY_LEVEL_NOTIFY_CCC}},
};
/* アプリケーションプロファイル */
static struct gatts_profile_inst xebec_profile_tab[1] = {
[0] = {
.gatts_cb = gatts_profile_event_handler,
.gatts_if = ESP_GATT_IF_NONE,
},
};
/* アプリケーションプロファイルからのGATTサーバイベントハンドラ */
static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param)
{
/* 登録イベントであった場合 */
if (event == ESP_GATTS_REG_EVT) {
// SPPサービス属性を作成してGATTサーバスタック側に渡す => 完了するとESP_GATTS_CREAT_ATTR_TAB_EVTイベント発生
esp_ble_gatts_create_attr_tab(gatt_spp_db, gatts_if, SPP_IDX_NUM, SPP_SVC_INSTID);
// Batteryサービス属性を作成してGATTサーバスタック側に渡す
esp_ble_gatts_create_attr_tab(gatt_batt_db, gatts_if, BATTERY_IDX_NUM, BATT_SVC_INSTID);
}
/* 属性テーブル作成した場合 */
else if (event == ESP_GATTS_CREAT_ATTR_TAB_EVT) {
if (param->add_attr_tab.status == ESP_GATT_OK) {
if (param->add_attr_tab.svc_inst_id == SPP_SVC_INSTID && param->add_attr_tab.num_handle == SPP_IDX_NUM) {
memcpy(spp_handle_table, param->add_attr_tab.handles, sizeof(spp_handle_table));
esp_ble_gatts_start_service(xebec_cps_handle_table[SPP_IDX_SERVICE]);
}
else if (param->add_attr_tab.svc_inst_id == BATTSVC_INSTID &&
param->add_attr_tab.num_handle == BATTERY_IDX_NUM)) {
memcpy(batt_handle_table, param->add_attr_tab.handles, sizeof(batt_handle_table));
esp_ble_gatts_start_service(batt_handle_table[BATTERY_IDX_SERVICE]);
}
}
else {
ESP_LOGE(TAG, "Invalid ESP_GATTS_CREAT_ATTR_TAB_EVT.");
}
}
}
/* GATT Serverのイベント処理 */
static void gatt_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param)
{
int i;
/* プロファイルの登録時のイベントが発生した場合 */
/* ステータス正常でアプリケーションID値も正当なものならそのI/Fを登録する */
switch (event) {
case ESP_GATTS_REG_EVT:
if (param->reg.status == ESP_GATT_OK && param->reg.app_id < NUM_PROFILES) {
profile_tab[param->reg.app_id].gatts_if = gatts_if;
} else {
ESP_LOGE(XEBEC_GATTS_TAG, "reg app failed, app_id: %04x, status: %d",
param->reg.app_id,
param->reg.status);
return;
}
break;
default:
break;
}
/* gatts_ifが一致するアプリケーションコールバックのみ呼び出す */
/* まだ一度も登録がされていない場合はすべてのコールバックを呼び出す */
for (i = 0; i < NUM_PROFILES; i++) {
if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONEで呼び出されているときはすべて呼ばれる */
gatts_if == profile_tab[i].gatts_if) {
if (profile_tab[i].gatts_cb != NULL) {
profile_tab[i].gatts_cb(event, gatts_if, param);
}
}
}
ただ、私個人としては、これってあんまりきれいにまとまってないよね?って思っていまして。アプリケーションプロファイルの構造体周りに無駄がたくさんあるように思っています・・・でもとりあえず最小限度の変更で2つ以上サービスを立ち上げるまで行きたい方(≒ものぐさな人?。それは言い過ぎか、でもこんなところで悩みたくない人の方が多いのは事実でしょうからね)にとって参考になれば良いなーと思って恥ずかしながら公開しております。