search
LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

Organization

師走の勢いで BLE をやっていく

SRA Advent Calender 2020 20 日目の記事です。年々過疎っていきますねこの企画・・・

はいどうも、昨年は DX 部だったけど今年度から古巣の産業1に戻った hiroki-i さんですよ!
あいかわらず職場は家だし環境は全然変わってないんですけどね。

さてこのご時世、平日も休日も家に籠って、1日を通して会話をするのは家族だけ、下手すれば食事以外で口を動かさない日もあった、なんて事ありませんでした? 俺はありました。けっこうありました。家庭の事情で数ヶ月ほど必要以外の外出禁止の一人暮らしをしていたのでマジで引き篭りでした。
人間関係なんて煩わしいだけ、テキストベースのコミュニケーションがあれば十分、なんて言ってたりもしますし実際そうなんですけど、やはりどこか思ってしまいますよね、寂しい・・・。ともなると考えますよね、繋がりたい・・・。でも物理的にってのはありえない・・・。それとインターネットで人生消耗するのもアレなので何というかエネルギー消費を抑えて通信したい・・・。

そう、BLEです。 (出だしに迷った挙句超適当なすべりだし)

ということでバーっと BLE (Bluetooth Low Energy) をやっていきます。

実際のところ、ボーナスがガッカリだったので楽しみにしていた M1 Mac を買うに至れなかったため、せめて ARM でも買ってやろうと IoT なデバイスを見繕ってたんだけど、そこで一目惚れして即買いした ARM ですらないデバイスを遊んでみたよレポートです。

今回のおもちゃ

ESP32 という IoT 界隈じゃかなり支配的らしいつよつよチップを使います。

Wi-Fi 802.11 b/g/n/e/i (WPA/WPA2) と Bluetooth 4.2 BR/EDR & BLE を備えてデュアルコア、インターフェースも盛り盛りでプレイバリュー鬼なのに単品で 550 円(秋月)、評価ボードでも Amazon で国内倉庫品でも 900円弱という素敵な価格。

しかも開発環境が Arduino 対応、Lua でも遊べるということで死角がない。次のボーナスまで余裕で遊べる。

ということで評価ボード(写真上)を Amazon で 2 つほど購入しました。

IMG_7606.jpeg

主に自作キーボード界隈でおなじみの Pro Micro (写真下)と比較して二周りくらいデカいですね。

IMG_7604.jpeg

あとハンダ済みのピンヘッダの取り付けが浮いてるし斜めってるし脚曲がってるしハンダもイモってるしで動作に問題はないものの精神衛生上よろしくないので気にするタイプの人は調達元はちゃんと気を使いましょう。俺はこの後ぜんぶ修正しました。

環境構築

今回は Arduino を使用します。
Arduino IDE をインストールして、ESP32 向けライブラリもインストールします。適当にダウンロードしてインストーラ走らせて、設定からリポジトリURL張りつけて Arduino IDE からライブラリインストールさせるだけだしハウツー記事はそこらじゅうに溢れているのでここでは説明を省きます。いいかんじにググってね。

動作確認

おなじみ L チカをやります

IMG_7724.jpeg IMG_7725.jpeg
離すと消える 押すと点く

D4とGNDに繋いだプッシュスイッチを押すと評価ボード内蔵のLEDが光ります。

みんな意外としらないけど INPUT_PULLUP を指定すると内蔵プルアップ抵抗つき入力になるんです。もちろんチップの実装次第なのでちゃんとデータシートを読むべきなんだけど、ちゃんとやればいちいち抵抗挟む必要がなくなるので色々ラクなんです。
以降のサンプルでは、このスイッチと内蔵LEDを I/O に使用していくのでけっこう重要です。

// ピンアサイン
static const int LED_BUILTIN = 2; // 内蔵LED
static const int PUSH_BUTTON = 4; // スイッチ

// 初期設定
void
setup() {
  pinMode(PUSH_BUTTON, INPUT_PULLUP); // スイッチは input
  pinMode(LED_BUILTIN, OUTPUT);       // 内蔵LED は output
}

// メインループ
void
loop() {
  // ボタンが押されていたら内蔵LEDを光らせる
  if (digitalRead(PUSH_BUTTON) == LOW) {
    digitalWrite(LED_BUILTIN, HIGH);
  } else {
    digitalWrite(LED_BUILTIN, LOW);
  }
}

ということで開発環境の構築と動作確認が済み、バーっとやっていく準備が整った。

BLE サクっとおさらい

まず Bluetooth があって、Classic (所謂普通のBluetooth) と、BLE ってのがあります。

  • Bluetooth
    • Classic
      • わりとはやい
      • そこそこ消費大
      • ヘッドホンとか
    • BLE
      • 比較的おそい
      • めっちゃ消費小
      • キーボードとか

で、今回はこの BLE のほうをやっていきます。

BLE はクライアント/サーバーみたいな感じでセントラル/ペリフェラルという風に分けます。

ble.png

セントラルは基本的に使う側で、主に PC とか スマホとなる。ペリフェラルは使われる側で、キーボードとかマウスとかなんかそういうデバイスとなる。
登場人物がわかったら主なシーケンスを見ていきます。

ble-seq.png

  1. セントラルは scanning する事で接続先の advertise を待ち構える
    • スマホとかの Bluetooth 設定画面でデバイス一覧を待機してぐるぐるしてる時を想像するとわかりやすい
  2. ペリフェラルは advertise で周囲のデバイスに自分が接続先を探していることをアピールする
    • これは Bluetooth イヤホンの電源ボタン長押しとかして探索モードになってる時がまさにそんなかんじ
  3. さらに、セントラルは scanning 中に advertise をしてきたペリフェラルに対して接続を行う。
  4. あとは BLE デバイス中の任意の Service (後述) の Characteristic (後述) に対して read/write を行ったり notify を受け取ったりする。
  5. 最後にお互い不要になったら適宜 disconnect する。

まあかなりシンプルですね。

最後にデータ構造。上記シーケンスを見ても分かる通り、データはペリフェラルが持つ。
細かいことは本腰入れてやろうって気になってからちゃんとやればいいので、超ざっくり。

ble-data.png

  • ペリフェラルは複数の Service を持ち、 Service は複数の Characteristic を持つ。
  • ペリフェラルはデバイス名で識別し、Service, Characteristic は UUID をもって識別する。
  • Characteristic は read/write/notify 等の権限 (プロパティ) を持ち、必要に応じて Descriptor という追加情報も持つことができる

とりあえずこれだけ分かってれば OK、というかしっかり全部やってくとただでさえ業務多忙になりがちな12月中に終らせることは不可能となるのであとは雰囲気でやっていく。

ペリフェラルを書く

まずは素直にただのデバイスとして振る舞うペリフェラルを書きます。コメントもりもり。

#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>

static const int LED_BUILTIN = 2; // 内蔵LED
static const int PUSH_BUTTON = 4; // スイッチ

// デバイス名。
static const char *BLE_DEVICE_NAME = "ESP32-PERIPHERAL";
// 各種 UUID。ペリフェラル側とおなじにするよ
static const char *SERVICE_UUID = "567636bb-3d58-4856-ba31-c0415cd1d67d";
static const char *COUNT_CHARACTERISTIC_UUID = "88dc75e5-8e0c-455d-81b7-d00a0bfc5eb9";
static const char *PUSHED_CHARACTERISTIC_UUID = "e393e28c-3c77-4f89-8eac-b789bae29c0d";

static boolean connected = false; // 接続状態

// Bluetooth はデバイス(ペリフェラル)側がサーバー。
static BLEServer *server = NULL;
// BLE Server は、複数 Service を持ち、1 つの Service に対し複数の Characteristic をもつ。
static BLECharacteristic *countCharateristic = NULL;
static BLECharacteristic *pushCharateristic = NULL;

// サーバのコールバック。接続状態をグローバル変数に渡すだけ
class ServerCallbacks : public BLEServerCallbacks {
  void
  onConnect(BLEServer *server) {
    connected = true;
  };
  void
  onDisconnect(BLEServer *server) {
    connected = false;
  }
};

// 初期化
void
setup() {
  pinMode(PUSH_BUTTON, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);
  BLEDevice::init(BLE_DEVICE_NAME);
  // Bluetooth はデバイス(ペリフェラル)側がサーバー。
  server = BLEDevice::createServer();          // サーバを作成
  server->setCallbacks(new ServerCallbacks()); // コールバック登録
  // BLE Server は、複数 Service を持ち、1 つの Service に対し複数の Characteristic をもつ。
  auto *service = server->createService(SERVICE_UUID);
  // 値が増えるだけの Charateristic
  countCharateristic = service->createCharacteristic(COUNT_CHARACTERISTIC_UUID,
                                                     BLECharacteristic::PROPERTY_READ
                                                         | BLECharacteristic::PROPERTY_NOTIFY);
  // セントラル(クライアント)が notify するなら付けておくと良い
  countCharateristic->addDescriptor(new BLE2902());

  // スイッチの状況を渡す Charateristic
  pushCharateristic = service->createCharacteristic(PUSHED_CHARACTERISTIC_UUID,
                                                    BLECharacteristic::PROPERTY_READ
                                                        | BLECharacteristic::PROPERTY_NOTIFY);
  // セントラル(クライアント)が notify するなら付けておくと良い
  pushCharateristic->addDescriptor(new BLE2902());

  service->start(); // サービスを開始
  auto *advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID); // advertise に載せるサービスUUIDを設定
  BLEDevice::startAdvertising();             // advertise を開始
  Serial.println("Waiting a client connection to notify...");
}

uint32_t count = 0;
// メインループ
void
loop() {
  auto isPushed = digitalRead(PUSH_BUTTON) == LOW;
  Serial.print(count);
  Serial.print("\t");
  Serial.println(isPushed);
  // スイッチが押されてたら内蔵 LED を光らせる
  digitalWrite(LED_BUILTIN, (isPushed ? HIGH : LOW));

  if (connected) {
    countCharateristic->setValue((uint8_t *)&count, 4);   // uint32 を uint8[4] として set
    countCharateristic->notify();                         // set 結果を通知
    pushCharateristic->setValue((uint8_t *)&isPushed, 1); // bool を uint8[1] として set
    pushCharateristic->notify();                          // set 結果を通知
    count++;
  }
  delay(50);
}

まあコメント読めばだいたいわかって欲しい。
起動すると ESP32-PERIPHERAL という名前のデバイスとして advertise を開始して、セントラル側から connect が来ると connected が true になり、生きてる限りインクリメントされ続ける値 (COUNT_CHARACTERISTIC_UUID) の Charteristic と D4 に繋いだスイッチが押されてるかどうかの値 (PUSHED_CHARACTERISTIC_UUID) の Charteristic の 2 つを提供する Service (SERVICE_UUID) を advertise において提供サービス ID として公開している。

BLE2902 については説明がすごくめんどくさいのでサボります。笑

動作確認

これを焼いた ESP32 を走らせても多分スマホやPCの Bluetooth デバイス一覧には ESP32-PERIPHERAL デバイスが見えてくることはないと思います。
これはセントラルであるPCやスマホが SERVICE_UUID サービスを扱う方法がわからないしそんなサービスを提供するデバイスを求めていないので除外しているものと考えられます。

このままだとどうにもならないので、デバッグに便利な BLE Scanner というアプリを使います。Androidも似たようなアプリがあると思うんで探せばいいと思います、知らんけど。

IMG_7771.png IMG_7772.png
デバイスがServiceを公開している様 ServiceがCharteristicを提供している様

COUNT_CHARACTERISTIC_UUID のほうの Charteristic は延々値が増え続けている様が、PUSHED_CHARACTERISTIC_UUID のほうの Charteristic は D4 に繋いだスイッチを押していると値が 0x01 に、離していると 0x00 になっている様が確認できる。

セントラルを書く

ペリフェラルができたならセントラルも欲しくなるよね。せっかく 2 枚評価ボード買ったんだしね。

という事でセントラルも書きます。こっちはちょっとボリュームあります。

#include "BLEDevice.h"

static const int LED_BUILTIN = 2; // 内蔵LED
static const int PUSH_BUTTON = 4; // スイッチ

// 各種 UUID。ペリフェラル側とおなじにするよ
static BLEUUID SERVICE_UUID("567636bb-3d58-4856-ba31-c0415cd1d67d");
static BLEUUID COUNT_CHARACTERISTIC_UUID("88dc75e5-8e0c-455d-81b7-d00a0bfc5eb9");
static BLEUUID PUSHED_CHARACTERISTIC_UUID("e393e28c-3c77-4f89-8eac-b789bae29c0d");

// Bluetooth はデバイスに接続する(セントラル)側がクライアント
static BLEClient *client = NULL;
static BLEAdvertisedDevice *server = NULL;
static bool doConnect = false;

// スイッチの状況を取る Charateristic における Notify があったときのコールバック
void
pushedNotifyCallback(BLERemoteCharacteristic *pushedCharacteristic,
                     uint8_t *data,
                     size_t length,
                     bool isNotify) {
  Serial.print("Notify from pushed characteristic = ");
  Serial.println(data[0]);
  digitalWrite(LED_BUILTIN, (data[0] ? HIGH : LOW));
}

// サーバに接続
void
connectToServer(BLEAdvertisedDevice *server) {
  Serial.println("connect to server");
  client->connect(server);
  Serial.print("connected");
  // サービスを探す
  auto *bleRemoteService = client->getService(SERVICE_UUID);
  if (bleRemoteService == nullptr) {
    Serial.println("Failed to find our service");
    client->disconnect();
  }
  // 値が増えていくだけの Charateristic
  auto countCharacteristic = bleRemoteService->getCharacteristic(COUNT_CHARACTERISTIC_UUID);
  if (countCharacteristic == nullptr) {
    Serial.println("Failed to find count characteristic");
    client->disconnect();
  }
  // スイッチの状況を取る Charateristic
  auto pushedCharacteristic = bleRemoteService->getCharacteristic(PUSHED_CHARACTERISTIC_UUID);
  if (pushedCharacteristic == nullptr) {
    Serial.println("Failed to find pushed characteristic");
    client->disconnect();
  }
  // 値が増えていくだけの Charateristic を読んでみる
  if (countCharacteristic->canRead()) {
    Serial.print("read count characteristic  = ");
    Serial.println(countCharacteristic->readUInt32());
  }
  // スイッチの状況を取る Charateristic の Notify を登録
  if (pushedCharacteristic->canNotify()) {
    pushedCharacteristic->registerForNotify(pushedNotifyCallback);
  }
}

// advertise のコールバック。
class AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  void
  onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());
    // advertise してる service ID が欲しいものと一致したら求めてるサーバと判断
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(SERVICE_UUID)) {
      BLEDevice::getScan()->stop(); // スキャン停止
      Serial.println("scan stop");
      // 接続開始
      server = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
    }
  }
};

// 初期化
void
setup() {
  pinMode(PUSH_BUTTON, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);
  // クライアント側は特にデバイス名とかなくても良い
  BLEDevice::init("");
  // Bluetooth はデバイスに接続する(セントラル)側がクライアント
  client = BLEDevice::createClient();
  // スキャン開始
  auto *bleScan = BLEDevice::getScan();
  bleScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());
  bleScan->setInterval(1349);
  bleScan->setWindow(449);
  bleScan->setActiveScan(true);
  Serial.println("scan start");
  bleScan->start(5);
}

// メインループ
void
loop() {
  // 接続が切れてるならスキャンする
  if (!client->isConnected()) {
    Serial.println("scan start");
    BLEDevice::getScan()->start(0);
  }
  // 接続する
  if (doConnect) {
    connectToServer(server);
    doConnect = false;
  }
  delay(1000); // Delay a second between loops.
} // End of loop

長いけど内容はそんなに難しくない。起動と同時に scanning を開始し、 SERVICE_UUID を advertise しているデバイス (つまり上記で作ったペリフェラル) を見つけたら、いったん doConnect を true にして
メインループから connect 処理を行う。
なんで scanning のハンドラから直接 connect をしないのかというと実際そうすると上手くいかないから。おそらくハンドラ内から connect をしちゃいけないっぽいんだけどちょっと調べても理由はわからなかった。

ともかく connect をして、成功したら 生きてる限りインクリメントされ続ける値 (COUNT_CHARACTERISTIC_UUID) の Charteristic を1度 read する。
また ペリフェラルのスイッチが押されてるかどうかの値 (PUSHED_CHARACTERISTIC_UUID) の Charteristic に対して notify を購読し、 notify を受けたらその値に応じて セントラル側の内蔵 LED を点けたり消したりする。

ようするに、ペリフェラル側のスイッチが押されるとセントラル側のLEDも光るようになる。

動作確認

左がペリフェラル、右がセントラル。
動かしてみるとわかるんだけど、肉眼レベルでは全くラグを感じない。誰だBLEは比較的遅いとか言ったやつ。
もう押したらほぼ同時に光る。ツーと言ったら食い気味にカーが来るくらいの素早さ。

IMG_7778.JPG IMG_7779.JPG
離すとどっちも消える 押すとどっちも点く

ちなみに黄色・オレンジ・緑のジャンプワイヤは何かというと USB から 5V 電源を引っぱっているだけです。ESP32 は 3.3V で動くので単三電池2本直列繋ぎにするだけで安易に動くんですけど、まあそれよりもっと安易に USB から 5V を取って評価ボード付属の 3 端子レギュレータに入力してやれば永久に動き続けてラクです。
評価ボードの USB 端子使えばいいじゃんって話は実際それはそうなんだけど、写真的になんというかね、そのね、無線してる感みたいなのがね、いやそれなら電池使えばいいじゃんて話もそうなんだけどね、電池ボックス1本のやつしか余ってなくてね、昇圧回路とかも一応試してみたんだけどね、電流が全然足りんくてね、いやもう本当。まあいいよね。

まとめ

ということで、年末進行で業務どちゃくそ多忙ながらも何とか BLE でお話しするような感じまではいきました。

ここまでの時点でけっこう長くなっちゃったので没にしたけど、上記を応用して左右分離型ワイヤレスキーボードを実装してみたり、組込みの分際でデュアルコアなのを利用して高速化(実際計測したわけではないので本当に高速化できてるのかどうかはかなり怪しい)したり、wifiでHTTPサーバ/クライアントやってみたり、これ2枚でかなり遊べてるので年末年始も育休中も退屈しないで済みそうですね。

ここで小遣い温存したので次のボーナスでこそ M1 Mac 買うぞ!! 待ってろよビッグエンディアン!!!

お役立ち情報

評価ボード幅広すぎてブレッドボード刺さらんのだが

写真見ればなんとなくわかるはず!! それが嫌なら幅の広いお高いブレッドボードを買うと良いかと思います!

Big Sur にしたらビルドできんくなったんだが

既に解決済みっぽいです。情報はやい人たちありがたい。
https://qiita.com/optimisuke/items/15b042d939ff21c8ceaa

Arduino IDE 使いにくすぎるのだが

VSCode で ino ファイルを開きましょう。あとはだいたい雰囲気で全部どうにかなります。

INPUT_PULLUP が効かんのだが

使えるピンと使えないピンがあります。まとめありがたい。
https://qiita.com/no_clock/items/a3bc8a9816534cf8c930

Vin に電源つないでもパワーLEDは光るのに動かんのだが

パワーLEDは電圧かかってればとりあえず光るのであんまり信用ならない。ボードに組込まれてる3端子レギュレータはドロップ電圧が1V近くあるナメた仕様なので4V強は入力しないといけません。Vinは無視して3V3に単三直列2本ぶんくらいを突っ込むほうが現実的です。プログラム次第ではありますが電池消耗してだいぶ電圧落ちてきても動くらしいです。ただつまりリチウムイオン電池を使う時などは特に過放電に気をつけないといけないとか色々ノウハウがあるのでググるといいです。

あと前述しましたが電流はけっこう必要っぽいので昇圧回路とかで入力電圧作っても動かないとか結構電気電子的な学びもあって普段ソフトばっかやってる脳味噌にはけっこう刺激的でした。

なんかもうちょっと参入障壁低そうな感じのものない?

IoT とか Maker とかの界隈じゃイケイケの玩具として有名な M5Stack がまさに ESP32 を搭載してて周辺モジュールだとか諸々面白いものが上手いことパッケージ化されてて面白いらしいです。
今回の Advent Calender でも大人気だった AWS の AWS IoT EduKit ってサービスと連携するリファレンスHWとしても使われてたり、
ちょっと最近話題になってた出来が超良さそうな e-ink デバイスの M5Paper とか、けっこう注目株なシリーズなのでここから入るとそれはそれで楽しいと思います。

どうせなら PC とかスマホと繋がる BLE 体験をしたいんだが

かかったな

BLE Scanner のようなデバッグ用のアプリ以外で何かをしたいんなら、キーボードだとかマウスだとかの HID を書いて回路組んで・・・をやってくのが一番楽しいと思います (実際楽しかった)

実装方法は色々あるけど、めちゃくちゃ簡単なライブラリがあってその紹介記事まであったのでサクッと紹介させてもらいますね。

キーボードの回路組むのはここらへんが非常にわかりやすいです(が、現在画像リンクが死んでますね。。。)

書籍ならこちら!! → https://booth.pm/ja/items/780027

キーボードの試作回路組むならこういうのがあると簡単です。なんならそのままワンオフ品として運用可能ですしね!!

回路組んだらスイッチだとかキーキャップも必要になりますよね?
他の人が作ったキーボードも気になってきますよね?
買うだけじゃなくてキーボードのカスタムも楽しくなってきそうですよね?

さぁ、年末はESP32で慣らして、来年こそ自作キーボードだよ!! 遊舎工房行こう!! ようこそ沼へ!!!! (結局これが言いたかった)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1