これまでのあらすじ
micro:bit に乗ってるnRF51822を使うBLEプログラムを Arduino IDE で書けるらしいじゃん、と言うことで、早速オムロン環境センサ(BLEペリフェラル)に温度湿度をアドバタイズさせてmicro:bitで受信させてやろうと思った私たち。
しかし、central/observer として動作して advertise されたパケットの manufacturer data を読めるライブラリが見つからない。
かくして私たちは SoftDevice(S130)のAPIでの実装に挑むことになったのでした。
オムロン環境センサの設定 & おことわり
・USB型 2JCIE-BU01 はデフォルト(Advertising mode 0x01)で Sensor Data をアドバタイズパケットに乗せてきます。
・BAG型 2JCIE-BL01 はデフォルト(Beacon mode 0x08) では Sensor Data をアドバタイズパケットには乗せません。
・(PCB型は持ってないのですがたぶんBAG型と同じ)
BAG型ではBeacon mode を General Broadcaster 2 または Limited Broadcaster 2 に変更して使うのが本ページの前提です。 (General/Limited Broadcaster 1 でも動きますが、加速度データないので設定するなら2でしょう)
Broadcaster モードは過去の温度等を保存しないモードなので、履歴保存と排他になります。(Weather News のスマホアプリなどで過去履歴も読む運用をしたい場合はこのページのコードとは両立できません。)
Beacon mode の変更方法
下記参考サイト参照
(1) armadillo のコマンドラインから設定:https://armadillo.atmark-techno.com/howto/armadillo_2JCIE_Advertising
「形2JCIE-BL01の設定」のところです。BDアドレスが解れば
# gatttool -t random -b xx:xx:xx:xx:xx:xx --char-write-req --handle=0x0048 --value=0808a0000a0032000400
て感じ。(--value=0808a0000a0032000400 の後ろから3・4文字目が04 なので、General broadcaster 2)
ラズパイでも sudo apt-get install bluez
すれば hcitool と gattool が使えるようになるので同じように設定できます。
(2) android/iOS アプリ BLEScanner を使ってスマホから設定: https://qiita.com/komde/items/7209b36159da69ae79d2
CHERACTERISTIC UUID 0C4C3042-7700-46F4-AA96-D5E974E32A54 の書き換え。
スマホアプリで長い16進文字列を手で打つのは面倒ですので(元の値コピペできるソフトあればいいのですが知らないので)、ラズパイ等があれば (1) のほうが楽ですね。(2)の記事では、書き込む文字列は Limited Broadcaster の時間が長い設定になっているので、(BigEndianになってる?)デフォルトからBeacon modeだけ書き換えるなら(1)の記事同様 0808A0000A0032000400 等にすると良いと思います。General Broadcaster でしか使わないならどちらでも同じですが。
参考サイトを見ながら ArduinoIDE での開発の準備を
参考サイト: BBC micro:bit (DEKO のアヤシいお部屋)
micro:bit のプログラムを ArduinoIDE で開発するための情報が豊富です。
(1)こちらの「セットアップ」をすることで、Arduino IDEでの開発が可能になります。(Sandeep Mistry による arduino-nRF51
を設定する)
(2)また「LEDスクリーン」という項目の後半に「ライブラリ」という項目があり、micro:bit のLEDを操作するライブラリを公開してくださっていますので、microbit_Screen.cpp , microbit_Screen.h を入手します。(.ino と同じ場所に置いてヘッダを include すれば使えます)
(3)また「BLE」の項目の途中にある「SoftDevice の導入手順」の項目に従って SoftDevice S130 をインストールします…が、
s130_nrf51_2.0.1_softdevice.hex がないと失敗するので、事前に
https://www.nordicsemi.com/Software-and-tools/Software/S130/Download から入手して
Arduino15\packages\sandeepmistry\hardware\nRF5\0.7.0\cores\nRF5\SDK\components\softdevice\s130\hex\
に置いておきます。 (0.7.0 のところは、arduino-nRF5 のバージョン番号なので新しいのを入れれば変わります)
Arduino15 フォルダの場所がわからないときは、ArduinoIDE で「ファイル-環境設定」を開くと、下の方に Arduino15\preference.txt へのフルパスが書いてあるので、そこから辿ります。
他にも有益な情報がたくさんありますが、ひとまずこれくらいで。
「manufacturer data を読める(nRF51用の)ライブラリが見つからない」とは
オムロン環境センサに温度湿度をアドバタイズさせると manufacturer data がついたアドバタイズパケットが飛んできます。それを読んでくれるライブラリでないと、温度湿度を取得できません。が。
・Arduino公式が提供するArduinoBLEライブラリを使えばセントラルにスキャンさせることはできるが、manufacturer data を読まない。
・Sandeep Mistry は arduino-nRF51 で利用する BLEPeripheral ライブラリを用意しているが、BLECentral ライブラリはない
・Adafruit の nRF52 ライブラリ で central_scan サンプルが良い感じなのだが、nRF52 の S132 は、nRF51 の S130 とコードが違う(参考にはなる)
・RedBearLab の BLE Nano とそのライブラリとかがあったらしいが、2021/3/7 現在、サイトも消滅している。
他の所にあるかもしれませんが見つけられませんでした。
S130 APIでの実装
結局、Adafruit の S132 のコードを見て流れを理解して、S130の仕様が違うから直しつつ、自分の言葉でコード書いて(誰がかいてもこうなるわ、ってのはコピペになってますが)、みたいなことをして実装しました。
いうてもオブザーバーですから
(1) setup 時に SoftDevice初期化(sd_softdevice_enable
)とBLEの初期化(sd_ble_enable
)
(2) setup 時に タイムアウトなしのスキャン開始(sd_ble_gap_scan_start
)
(setup では micro:bit のボタンなども初期化も行う)
(3) loop で SoftDevice のBLEイベントを確認(sd_ble_evt_get
)して、イベント発生していたら処理
(loop では micro:bit のボタン押されているかも確認して対応。その間はBLEイベントは無視)
こんだけですので。関数のパラメータの設定の仕方を Nordic の文書やサンプルとか Adafruit ライブラリとか BLEPeripheral ライブラリとかを見ながらやればいいという話。
あとはコード見て貰うのが早いですかね。
オムロン環境センサのアドバタイズパケットの解析は qiita 内でも記事いろいろあるのでここでは説明割愛です。
コードで
// 複数のセンサーに対応
以下にある SENSORNUM
, LED_VIEW_SENSOR
, sensor_bdaddr[SENSORNUM]
あたりを書き換えれば使えます。(SENSORNUM
1 でも動く)
LED_VIEW_SENSOR
は micro:bit のLEDに表示するセンサーの番号(0以上SENSORNUM
未満)を指定します。
指定センサーの直近の受信データに基づいて、Aボタンを押せば温度、Bボタンを押せば湿度が表示されます。
動作確認できたら
//#define NOTUSE_SERIAL
のコメントを外してシリアル出力を止めるとLEDがちかちかしなくてすみます。
オムロン環境センサとmicro:bit(と電源)を持っていけばどこでもサクっと温度湿度が参照できます。
Arduino IDE でのコード
・microbit_Screen.cpp, microbit_Screen.h は ino と同じ場所に置くこと。
・ボードの設定にも注意(S130指定漏れしていると ble.h が見つからない等エラーになります)
nRF51-microbit.ino
#include "ble.h"
#include "ble_gap.h"
#include "ble_types.h"
#include "nrf_sdm.h"
#include "microbit_Screen.h"
// コメントをはずすと、シリアル出力無効化
//#define NOTUSE_SERIAL
#ifdef NOTUSE_SERIAL
#define MY_PRINTLN(arg)
#define MY_PRINTERR(arg1,arg2)
#else
#define MY_PRINTLN(arg) Serial.println(arg)
#define MY_PRINTERR(arg1,arg2) my_printerr(arg1,arg2)
#endif
//////////////////////////////////////////////////////////////////////
// 複数のセンサーに対応
#define SENSORNUM 3 /* センサーの数 */
#define LED_VIEW_SENSOR 1 /* どのセンサーをLEDに表示するか */
// オムロン環境センサのBDアドレスを列挙する。下記3機種に対応。
// USB型 2JCIE-BU01
// BAG型 2JCIE-BL01
// PCB型 2JCIE-BL01-P1 (BAG型と同じパケットを出すので、SerialにはBAGと出る)
const char* sensor_bdaddr[SENSORNUM] = {
"db:51:39:XX:XX:XX",
"d5:16:e2:XX:XX:XX",
"cc:64:e3:XX:XX:XX"
};
int16_t temp_stored[SENSORNUM];
int16_t rh_stored[SENSORNUM];
//////////////////////////////////////////////////////////////////////
// 出力関連
#ifndef NOTUSE_SERIAL
const char* errstr[] = {
"NRF_SUCCESS",
"NRF_ERROR_SVC_HANDLER_MISSING",
"NRF_ERROR_SOFTDEVICE_NOT_ENABLED",
"NRF_ERROR_INTERNAL",
"NRF_ERROR_NO_MEM",
"NRF_ERROR_NOT_FOUND",
"NRF_ERROR_NOT_SUPPORTED",
"NRF_ERROR_INVALID_PARAM",
"NRF_ERROR_INVALID_STATE",
"NRF_ERROR_INVALID_LENGTH",
"NRF_ERROR_INVALID_FLAGS",
"NRF_ERROR_INVALID_DATA",
"NRF_ERROR_DATA_SIZE",
"NRF_ERROR_TIMEOUT",
"NRF_ERROR_NULL",
"NRF_ERROR_FORBIDDEN",
"NRF_ERROR_INVALID_ADDR",
"NRF_ERROR_BUSY",
"NRF_ERROR_CONN_COUNT",
"NRF_ERROR_RESOURCES"
};
// NRF_ERROR* を文字列として出したかったので。
void my_printerr( const char* comment, uint32_t err ) {
Serial.print( "Return Value: " );
Serial.print( comment );
Serial.print( " = " );
Serial.print( err );
if ( err <= sizeof(errstr) / sizeof(char*) ) {
Serial.print( " : " );
Serial.print( errstr[err] );
}
Serial.println("");
}
#endif
//////////////////////////////////////////////////////////////////////
// BLE関連
// SoftDevice の初期化で指定する、エラーコールバック関数
static void nrf_error_cb(uint32_t id, uint32_t pc, uint32_t info)
{
MY_PRINTLN( "nrf_error_cb called" );
}
bool SD_begin() // SoftDevice と BLE の初期化
{
uint32_t ret;
// SoftDevice の初期化
// Low frequency Clock setting
#if defined( USE_LFXO )
MY_PRINTLN( "USE_LFXO" ); // Low frequency crystal oscillator : LFXO
nrf_clock_lf_cfg_t clock_cfg =
{
.source = NRF_CLOCK_LF_SRC_XTAL,
.rc_ctiv = 0,
.rc_temp_ctiv = 0,
.accuracy = NRF_CLOCK_LF_ACCURACY_20_PPM
};
#elif defined( USE_LFRC )
MY_PRINTLN( "USE_LFRC" ); // Low frequency clock に RC oscillator : micro:bit はboards.txt で USE_LFRC と定義されている
nrf_clock_lf_cfg_t clock_cfg =
{
.source = NRF_CLOCK_LF_SRC_RC,
.rc_ctiv = 16,
.rc_temp_ctiv = 2
};
#else
#error Clock Source is not defined. Please define USE_LFXO or USE_LFRC in variant.h or boards.txt.
#endif
#ifdef ANT_LICENSE_KEY
ret = sd_softdevice_enable(&clock_cfg, nrf_error_cb, ANT_LICENSE_KEY);
#else
ret = sd_softdevice_enable(&clock_cfg, nrf_error_cb);
#endif
MY_PRINTERR( "sd_softdevice_enable", ret );
// SoftDevice の初期化 ここまで
// BLE の初期化
extern uint32_t __data_start__;
uint32_t app_ram_base = (uint32_t) &__data_start__;
ble_enable_params_t enable_param;
memset(&enable_param, 0, sizeof(ble_enable_params_t)); // 接続しないので0埋めるだけ
ret = sd_ble_enable( &enable_param, &app_ram_base );
MY_PRINTERR( "sd_ble_enable", ret );
// BLE の初期化 ここまで
return true;
}
void SD_scan_start(uint16_t interval, uint16_t window, uint16_t timeout)
{
// scan start
ble_gap_scan_params_t scan_param;
scan_param.active = 0;
scan_param.selective = 0;
scan_param.p_whitelist = NULL;
scan_param.interval = interval;
scan_param.window = window; /* Between 0x0004 and 0x4000 in 0.625ms units (2.5ms to 10.24s). */
scan_param.timeout = timeout; /* Between 0x0001 and 0xFFFF in seconds, 0x0000 disables timeout. */
uint32_t ret = sd_ble_gap_scan_start(&scan_param);
MY_PRINTERR( "sd_ble_gap_scan_start", ret );
}
//////////////////////////////////////////////////////////////////////
// setup
void setup() {
// micro:bit buttons and screen
pinMode(PIN_BUTTON_A, INPUT_PULLUP);
pinMode(PIN_BUTTON_B, INPUT_PULLUP);
SCREEN.begin();
#ifndef NOTUSE_SERIAL
Serial.begin(115200);
Serial.println("Start BLE Scan test");
#endif
// SoftDevice and BLE init
SD_begin();
SD_scan_start(0x00A0, 0x0080, 0x0000); // 3rd param = 0 なので、無限にスキャンする
}
//////////////////////////////////////////////////////////////////////
// loop
// 1. sd_ble_evt_get で何か起きていたら ble_hander に投げる
// 2. micro:bit ボタン状況を見て文字列出力
void loop() {
// 1. ble イベント処理
uint32_t err = NRF_SUCCESS;
while ( err != NRF_ERROR_NOT_FOUND ) { // ore or more pending event(s).
uint8_t * ev_buf;
uint16_t ev_len;
err = sd_ble_evt_get(NULL, &ev_len); // buf 必要サイズ取得
if ( err == NRF_ERROR_NOT_FOUND ) {
// break;
}
else if ( err == NRF_SUCCESS ) {
ev_buf = (uint8_t*)malloc(ev_len);
err = sd_ble_evt_get(ev_buf, &ev_len);
if ( err == NRF_SUCCESS ) {
ble_handler( (ble_evt_t*)ev_buf );
}
else if ( err != NRF_ERROR_NOT_FOUND ) {
MY_PRINTERR( "loop: Failed in checking event data", err );
}
free(ev_buf);
}
else {
MY_PRINTERR( "loop: Failed in checking event length", err );
}
}
// 2. micro:bit ボタンに応じてLED表示
char outbuf[20];
if (digitalRead(PIN_BUTTON_A) == LOW) {
MY_PRINTLN("A Button pressed");
sprintf( outbuf, "%c%d", (temp_stored[LED_VIEW_SENSOR] >= 0) ? '+' : '-', (abs(temp_stored[LED_VIEW_SENSOR]) + 50) / 100 ); // 50足してから100で割る=四捨五入
SCREEN.showString(outbuf);
delay(200);
}
if (digitalRead(PIN_BUTTON_B) == LOW) {
MY_PRINTLN("B Button pressed");
sprintf( outbuf, "%d%c", (abs(rh_stored[LED_VIEW_SENSOR]) + 50) / 100, '%' );
SCREEN.showString(outbuf);
delay(200);
}
}
//////////////////////////////////////////////////////////////////////
// アドバタイズパケットの解析と温度湿度の取得
// センサのアドバタイズパケットの冒頭部分: length 31 以外のは温度湿度を含まないので、そこで判断するため head_bag_A, head_bag_Badv は使わない。
//uint8_t head_bag_A[] = {0x02, 0x01, 0x06, 0x1a, 0xff, 0x4c, 0x00, 0x02, 0x15 }; // Beacon Advertise, len == 30
//uint8_t head_bag_Badv[] = {0x02, 0x01, 0x06, 0x03, 0x02, 0x0a, 0x18, 0x04, 0x08, 'E', 'n', 'v' }; // Connection Advertise 1, len == 12
uint8_t head_bag_Bresp[] = {0x1e, 0xff, 0xd5, 0x02 }; // 温度のオフセットは 20
uint8_t head_bag_C[] = {0x02, 0x01, 0x06, 0x03, 0x02, 0x0A, 0x18, 0x12, 0xff, 0xd5, 0x02 }; // Connection Advertise 2: len == 31 だが、温度湿度を含まない
uint8_t head_bag_DE[] = {0x02, 0x01, 0x06, 0x17, 0xff, 0xd5, 0x02 }; // 29-30 が "EP" のときD, "IM" のときE だが、どちらも温度のオフセットは8、
uint8_t head_usb[] = {0x02, 0x01, 0x06, 0x16, 0xff, 0xd5, 0x02 }; // manual pdf に4バイト目が0x17と書いてあるが、length なので 0x16 が正,温度のオフセットは9
// オムロン環境センサのアドバタイズパケット中身を見て、温度湿度を設定して返す。(length31以外では呼ばないこと)
// 下記値の OR を返す(USBで値がなければ2, あれば3, BAGで値がなければ4,あれば5, USB/BAGどちらでもない場合 0)
#define GETTEMPRH_HASVAL 1
#define GETTEMPRH_USBTYPE 2
#define GETTEMPRH_BAGTYPE 4
int8_t gettemprh( uint8_t * pdata, int16_t* ptemp, int16_t* prh )
{
int8_t ret = 0;
int8_t datapos = -1;
int16_t shortint_buf[2];
if ( memcmp( pdata, head_usb, sizeof(head_usb)) == 0 && memcmp( pdata + 28, "Rbt", 3 ) == 0 ) { // USB型
ret |= GETTEMPRH_USBTYPE;
if ( pdata[7] == 0x01 || pdata[7] == 0x04 ) { // sensor data or scan response
datapos = 9;
}
}
else {
ret |= GETTEMPRH_BAGTYPE;
if ( memcmp( pdata, head_bag_Bresp, sizeof(head_bag_Bresp)) == 0 ) {
datapos = 20;
}
else if ( memcmp( pdata, head_bag_DE, sizeof(head_bag_DE)) == 0 ) {
datapos = 8;
}
}
if ( datapos > 0 ) {
memcpy( shortint_buf, pdata + datapos, 4 );
*ptemp = shortint_buf[0];
*prh = shortint_buf[1];
ret |= GETTEMPRH_HASVAL;
}
return ret;
}
//////////////////////////////////////////////////////////////////////
// callback 関数な体だけど、loop() の中で自分で呼んでる
void ble_handler(ble_evt_t* evt)
{
if ( evt->header.evt_id == BLE_GAP_EVT_TIMEOUT ) {
if (evt->evt.gap_evt.params.timeout.src == BLE_GAP_TIMEOUT_SRC_SCAN) {
MY_PRINTLN("Scan timeout");
return;
}
}
if ( evt->header.evt_id == BLE_GAP_EVT_ADV_REPORT ) {
char buf[200]; // addr: 3*6, temp rh, rssi, data(len): 60, data 3*32, margin: 26
int16_t temp = -100;
int16_t rh = 0;
int8_t result_of_gettemprh = 0;
bool omron_found = false;
// アドレスの文字列化
for ( int8_t i = 0; i < 6; i++ ) {
sprintf(buf + (i * 3) , "%02x:", evt->evt.gap_evt.params.adv_report.peer_addr.addr[5 - i]);
}
buf[strlen(buf) - 1] = ' '; // replace the last ':' to ' '
// オムロン環境センサであるかどうか sensor_bdaddr で判断して、温度湿度の取得
for ( int8_t i = 0; i < SENSORNUM; i++ ) {
if (strstr(buf, sensor_bdaddr[i]) == buf && evt->evt.gap_evt.params.adv_report.dlen == 31 ) {
omron_found = true;
result_of_gettemprh = gettemprh( evt->evt.gap_evt.params.adv_report.data, &temp, &rh );
if ( result_of_gettemprh & GETTEMPRH_HASVAL ) {
temp_stored[i] = temp;
rh_stored[i] = rh;
}
break;
}
}
if ( !omron_found ) {
return; // ここをコメントアウトすると、その他のアドバタイズも Serial に出力できる
}
#ifndef NOTUSE_SERIAL
// シリアルに出力
if ( result_of_gettemprh & GETTEMPRH_USBTYPE ) {
Serial.print( "USB Addr: ");
}
else if ( result_of_gettemprh & GETTEMPRH_BAGTYPE ) {
Serial.print( "BAG Addr: ");
}
else {
Serial.print( "??? Addr: ");
}
Serial.print( buf ); // BDAddr + ' ' が入ってる
if ( result_of_gettemprh & GETTEMPRH_HASVAL ) {
sprintf( buf, "temp = %4d : rh = %4d : ", temp, rh );
}
else {
sprintf( buf, "( adv without temp/rh ) : " );
}
Serial.print( buf );
sprintf( buf, "RSSI = %d : Data(%2d): ", evt->evt.gap_evt.params.adv_report.rssi, evt->evt.gap_evt.params.adv_report.dlen );
Serial.print( buf );
for ( int8_t i = 0; i < evt->evt.gap_evt.params.adv_report.dlen; i++ ) {
sprintf(buf, "%02x ", evt->evt.gap_evt.params.adv_report.data[i]);
Serial.print(buf);
}
Serial.println();
#endif
} // BLE_GAP_EVT_ADV_REPORT
}
その他
動画
AE-TYBLE16でもできました
AE-TYBLE16もnRF51822搭載で、qiita内で Arduino化の記事がいくつかありますが(たとえばこちら)、ArduinoIDEを使って S130を入れることができます。(SoftDeviceを入れ直すとUICRの再設定が必要。J-LINKを使う or STLinkしか持ってなければ system_nrf51.c を改造 (この文書のp31のとおり)で対応可能)
BLEPeripheralライブラリが動く状態のAE-TYBLE16 であれば、上述のコードから micro:bit 固有のボタンやLED関連の部分を消すだけで動ききました。