LoginSignup
1
1

More than 3 years have passed since last update.

オムロン環境センサ (broadcaster) → micro:bit (observer)として micro:bit に温度湿度表示 (おまけでAE-TYBLE16も)

Posted at

これまでのあらすじ

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関連の部分を消すだけで動ききました。

1
1
0

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
  3. You can use dark theme
What you can do with signing up
1
1