ArduinoBLEライブラリを用いて複数データをマイコン同士でBLE通信

プログラミング

ガジェット

目的

ロボットハンドを遠隔操作するためのデータグローブを新しく作るにあたって、seeed xiao nrf52 BLE senceという親指サイズなのにbluetoothとマイクと6軸センサーを積んだマイコンを買った。

これを機に今までのちょっと怪しい315Mhzの無線モジュールを卒業して技適の通ったBLE(Bluetooth Low Energy)通信で遠隔操作をしようと思う。

はじめに

まず、ロボットハンド側もBLE通信に対応させるためseeed xiao ESP32を購入した。この二つの間でデータのやり取りをする。

BLE通信にはGAT( General Advertising Profile)とGATT(General Attribute Profile)通信があり、前者はペアリングせずに一方向通信をするが、後者はペアリングしたセントラル-ペリフェラル間でのみ双方向通信ができる。今回の使用範囲では正直ブロードキャストで十分だが、構想ではいつか双方向が必要になるので今回は2つのマイコン間で自動でペアリングをして通信をする。

しかしネットで探してもスマホとマイコン間の通信ばかりでマイコン間のものが少なかった。見つけてもESP間であり、nrf52では使えないライブラリだったので両方で使えたarduinoBLE.hを用いる。

BLEとは

概要

BLEはBluetoothの規格の一つであり、Bluetooth4.0以降から追加された電力消費が少ない通信方式のこと。普通のものに比べて何十分の1という消費電力で済む。

ワイヤレスイヤホンのような通信量が多く、リアルタイム性が求められるものには向かないが、今回のような数バイトのデータをやり取りする程度なら十分な性能。

Bluetoothにはセントラルとペリフェラルがあり、セントラルはスマホ、ペリフェラルはイヤホンやセンサーなどの役割を持つ。

通信のしくみ

まず、ペリフェラルが自分の「サービス名」と「UUID」という固有のアドレスをブロードキャストという方式で周囲にばらまく。これをアドバタイズといい、セントラルがこれを受信するとスマホでいう周囲のBluetoothデバイス欄に表示される。ちなみにこの時の電波の強さもわかるのでイヤホンを見つけるアプリはこれを使っている。

ペリフェラルは複数のCharacteristicをもち、これらを包括した名前をサービスという。Characteristicは通信構造のことであり、データグローブで例えると、5つの指の曲げ具合の数値を2つのCharacteristicに格納し、DataGroveというサービスがその2つのCharacteristicを持っている。

送受信したいデータを入れる箱が「Characteristic」、その箱たちが入ったトラックが「サービス」というイメージだ。

ArduinoBLEライブラリ

導入経緯

今回はどちらもseeed XIAOを使っているが、マイコンがESPとnrf52なので使えるBLE通信ライブラリがArduinoBLE.hに限られた。そのためこのライブラリを使うが、今回はseeedのボードマネージャーやライブラリの導入は省く。

ただし、ESPのBLEライブラリと競合してコンパイルエラーが出るので、ArduinoBLEはボードのライブラリフォルダに入れておく。

ArduinoBLEのリファレンスもあるが、実際に使わないと分かりにくいのでスケッチ例を参考にする。

追記:このリファレンスは決して参考にしてはならない。これのせいで開発が数ヶ月遅れてしまった。これを参考にせず、直接ライブラリの中身を見よう。

解説

まず、実際に通信に成功し、実使用しているプログラムを書く。

ペリフェラル側

#include <ArduinoBLE.h>

const int ledPin = LED_BUILTIN; // set ledPin to on-board LED
const int buttonPin = 2;
int oldButtonState = LOW;

BLEService DataGrove("19B10010-E8F2-537E-4F6C-D104768A1214"); // create service

// create switch characteristic and allow remote device to read and write
BLECharacteristic thumbCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite, 32);
BLECharacteristic indexCharacteristic("b6fe67e7-8cb2-47a5-99da-24e8cee99b56", BLERead | BLEWrite, 32);

float finger[5];

void setup() {
  Serial.begin(115200);
//  while (!Serial);

  Serial.println("start");
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  delay(1000);
  digitalWrite(ledPin, HIGH);
  // configure the button pin as input
  pinMode(buttonPin, INPUT);
  pinMode(8, OUTPUT);
  // begin initialization
  if (!BLE.begin()) {
    Serial.println("starting Bluetooth® Low Energy module failed!");

    while (1);
  }

  // set the local name peripheral advertises
  BLE.setDeviceName("DataGrove");
  BLE.setLocalName("DataGrove");
  // set the UUID for the service this peripheral advertises:
  BLE.setAdvertisedService(DataGrove);

  // add the characteristics to the service
  //  ledService.addCharacteristic(ledCharacteristic);
  //  ledService.addCharacteristic(buttonCharacteristic);
  DataGrove.addCharacteristic(thumbCharacteristic);
  DataGrove.addCharacteristic(indexCharacteristic);
//  DataGrove.addCharacteristic(middleCharacteristic);
//  DataGrove.addCharacteristic(ringCharacteristic);
//  DataGrove.addCharacteristic(pinkyCharacteristic);

  // add the service
  BLE.addService(DataGrove);

  //  thumbCharacteristic.writeValue(0);
  //  indexCharacteristic.writeValue(0);

  // start advertising
  BLE.advertise();

  //  BLE.begin();

  Serial.println("Bluetooth® device active, waiting for connections...");
}

void loop() {
  // poll for Bluetooth® Low Energy events
  //  BLE.poll();
  BLEDevice central = BLE.central();

  if (central) {
    Serial.print("Connected to central: ");
    // print the central's MAC address:
    Serial.println(central.address());

    // while the central is still connected to peripheral:
    while (central.connected()) {
      digitalWrite(5, HIGH);
      digitalWrite(6, HIGH);
      int sensor[5] = {123, 124, 125, 126, 127};
      digitalWrite(ledPin, LOW);
      sensor[0] = analogRead(0);
      sensor[0] = constrain(sensor[0], 430, 530);
      sensor[0] = map(sensor[0], 530, 430, 0, 180);
      sensor[1] = analogRead(1);
      sensor[1] = constrain(sensor[1], 180, 390);
      sensor[1] = map(sensor[1], 390, 180, 0, 180);
      sensor[2] = analogRead(2);
      sensor[2] = constrain(sensor[2], 220, 420);
      sensor[2] = map(sensor[2], 420, 220, 0, 180);
      sensor[3] = analogRead(3);
      sensor[3] = constrain(sensor[3], 170, 300);
      sensor[3] = map(sensor[3], 300, 170, 0, 180);
      sensor[4] = analogRead(4);
      //sensor[4]++;
      sensor[4] = constrain(sensor[4], 200, 480);
      sensor[4] = map(sensor[4], 480, 200, 0, 180);

      uint32_t bend[5];
      bend[0] = sensor[0] | sensor[1] <<8 | sensor[2] <<16 | sensor[3] << 24; 
//         bend[0] = sensor[0] | sensor[1] <<8; 
      bend[1] = sensor[4];
      for (int i = 0; i < 5; i++) {
//          sensor[i]=analogRead(i);
//        bend[i]=sensor[i];
        Serial.print(i);
        Serial.print(": ");
        Serial.print(sensor[i]);
        Serial.print(" ");
      }
      Serial.println();

      thumbCharacteristic.writeValue(bend[0],true);
      indexCharacteristic.writeValue((byte)sensor[4]);
//      middleCharacteristic.writeValue(bend[2]);
//      ringCharacteristic.writeValue(bend[3],true);
//      pinkyCharacteristic.writeValue(bend[4],true);
      
      //      delay(10);
    }
    if (!central.connected()) {
      digitalWrite(ledPin, HIGH);
      digitalWrite(5, LOW);
      digitalWrite(6, HIGH);
    }
  }
  else {
    digitalWrite(ledPin, HIGH);
    digitalWrite(5, LOW);
    digitalWrite(6, HIGH);
  }



  // when the central disconnects, print it out:
  Serial.print(F("Disconnected from central: "));
  Serial.println(central.address());
}

やっていること

  1. セントラルを探す
  2. DataGloveという名前のサービスを見つけたら接続する
  3. 5つの曲げセンサーの値を読む
  4. 4つのデータをバイトシフトののち32byteのint型変数にまとめる
  5. 小指用のCharacteristicも作成し送信

以上を繰り返している。

詳説

ここからは細かい解説をしていく。まず、

BLEService DataGrove("19B10010-E8F2-537E-4F6C-D104768A1214"); // create service

ここでサービス名を設定する。括弧の中の文字列はUUIDといい、このサービス固有のものを書く必要がある。UUIDに関してはUUID生成とかで検索すればそれ用のサイトが出てくるので、ランダム生成したものを入れる。

BLECharacteristic firstCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite, 32);
BLECharacteristic secondCharacteristic("b6fe67e7-8cb2-47a5-99da-24e8cee99b56", BLERead | BLEWrite, 32);

ここでは二つのキャラクターを生成している。括弧の中は左から、UUID,プロパティ,データの最大サイズとなっている。

プロパティはBLEBroadcast、BLERead、BLEWriteWithoutResponse、BLEWrite、BLENotify、BLEIndicateがあるが、ここではデータの読み書きができればいいのでBLERead | BLEWriteとした。

どうやらArduinoBLEの仕様上、32byteまでしかデータが送れないようなので最大で設定。次に

  if (!BLE.begin()) {
    Serial.println("starting Bluetooth® Low Energy module failed!");
    while (1);
  }

ここではBLEの初期化をしている。成功すればループを抜ける。

 // set the local name peripheral advertises
  BLE.setDeviceName("DataGrove");
  BLE.setLocalName("DataGrove");

ここではデバイス名とサービス名を設定している

今回は同じ名前にしている。

  // set the UUID for the service this peripheral advertises:
  BLE.setAdvertisedService(DataGrove);

ついで、

  DataGrove.addCharacteristic(thumbCharacteristic);
  DataGrove.addCharacteristic(indexCharacteristic);

  BLE.addService(DataGrove);

ここでキャラクターとサービスを追加している。

  BLE.advertise();

ここでアドバタイズしている。初めの方で述べたが、これで周りに自分のデバイス名やサービス名、キャラクター名とそのUUIDをばらまいている。ここまでで通信の前に行う準備が完了した。

通信

ここからは通信に入る。

まず通信をしているloop内の構造を大まかに記述する。

void loop() {
 BLEDevice central = BLE.central();
 if (central) {//セントラルと接続できたとき
  Serial.print("Connected to central: ");

  // print the central's MAC address:

  Serial.println(central.address());
  while (central.connected()) {//セントラルと接続中

  LEDを付ける
  通信したいデータの処理
  データの送信
  }

  if (!central.connected()) {//セントラルから切断されたとき
   LEDを消す
  }

 }
 else {
  LEDを消す
 }
 // when the central disconnects, print it out:
 Serial.print(F("Disconnected from central: "));
 Serial.println(central.address());
}

まず、データグローブがセントラルと接続できればシリアル通信でセントラルMACアドレスを送信する。その後接続しているうちは、曲げセンサーの値を処理して送信を続ける。もし接続が途切れれば、切断されたとシリアル通信で送信する。LEDは接続確認用につけてある。

ここからは細かい解説を入れる。

BLEDevice central = BLE.central();

ここで接続したセントラルにcentralと名付ける。これ以降はセントラル関連の関数はcentral.~()と書く。

central.address()

セントラルのMACアドレスを表示する。MACアドレスとはデバイス固有のIDのことを指す。

firstCharacteristic.writeValue(bend[0],true);
secondCharacteristic.writeValue((byte)sensor[4]);

ここではキャラクターに数値を送信している。ただし、ここでの書き方には注意が必要だ。ArduinoBLEのリファレンスでは

bleCharacteristic.writeValue(buffer, length)
bleCharacteristic.writeValue(value)
ledCharacteristic.writeValue((byte)0x01);

としか書かれていないが、実際には送るデータやデータの大きさによって少し変わる。ライブラリーの中身を見てみると、

BLECharacteristic::writeValue(const uint8_t value[], int length, bool withResponse)
BLECharacteristic::writeValue(const void* value, int length, bool withResponse)
BLECharacteristic::writeValue(const char* value, bool withResponse)
BLECharacteristic::writeValue(uint8_t value, bool withResponse)
BLECharacteristic::writeValue(int8_t value, bool withResponse)
BLECharacteristic::writeValue(uint16_t value, bool withResponse)
BLECharacteristic::writeValue(int16_t value, bool withResponse)
BLECharacteristic::writeValue(uint32_t value, bool withResponse)
BLECharacteristic::writeValue(int32_t value, bool withResponse)

といろいろな種類の引数がある。文字を送りたいときや、数値のサイズによって引数を上記に合わせて適切に書かないとエラーが出るので注意が必要。

これを書いてくれなかったせいで、1バイトのデータしか送れないと勘違いして5つのキャラクターを作ってBLE通信したら8Hzくらいしか出なかった


おまけ

ビットシフト

ここでデータを送る際、データをまとめて送りたいときにはビットシフトが便利です。具体的に書いていく。

例えば100,110、120、130という4つのデータがある。これを4つのキャラクターで送ると処理が重くなってしまうので、一つのキャラに収めたいところだ。1つのキャラで送れる最大のデータ量は32byte、つまり4,294,967,296までしか表せない。3桁ずらして足すと100,110,120,130となり、超過してしまう。ここで活躍するのがビットシフトだ。

ビットシフトとは二進数で表した数値の桁をずらしていく操作のことをいう。100は2進数では01100100という8桁の数になる。1バイト、つまり256までの数は8桁内に収まるので、32byte中に4つ1バイトデータを入れることができます。この操作をarduinoで書くと次のようになります。

int senso[4] = {100, 110, 120, 130, 140};
uint32_t bend[5];
bend[0] = sensor[0] | sensor[1] <<8 | sensor[2] <<16 | sensor[3] << 24; 

これで4つのデータを一つにまとめることができた。あとはこれを受け取り側で4つに分解する方法である。arduinoには標準でデータのうち下1バイト、つまり下4桁のみを読む機能と続く4桁を読む機能があり、それぞれ

lowByte()
highByte()

とかく。これを用いて、先ほど用意した複合データを4つに分解するには

finger[0] = lowByte(bned[0]);
finger[1] = highByte(bend[0]);
bend[3] = bend[0]>>16;
finger[2] = lowByte(bend[3]);
finger[3] = highByte(bend[3]);  

と書ける。8~12桁を読んでくれる関数はないので、途中で16桁右にずらして再び読んでいる。

これでfinger[0]に100、finger[3]に130が代入される。通信するのは結構必須のテクニックなので覚えておいて損はないはずだ。5つのデータを送りたかったので結局2つのキャラを作らなければならなかったのは内緒


思っていたより長くなったので、セントラル編は次の記事にする。

Discussion

コメントはまだありません。

ログインするとコメントできます!

新着記事

eyecatch of pipicow_platformio

Raspberry Pi Pico WをPlatformIOでLチカ

by かいと

eyecatch of tocbot

【Tocbot】Next.js+MicroCMSのブログ記事に目次を作る方法が簡単すぎた話

by Yoshi_Zen

eyecatch of dev_pixel_tab

大学生向け Pixel Tablet の使用感

by アンティキラのポンコツ

eyecatch of civil_test

国家公務員総合職試験受験のすすめ

by アンティキラのポンコツ

eyecatch of Civil_Engineering_Soil_mec_4

[土質力学]地盤沈下の区別とその計算

by アンティキラのポンコツ

eyecatch of cpp1

【C++】#0:はじめに

by Yoshi_Zen

すべての記事を見る