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

# これまでのあらすじ
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でしょう）
　<strong>Broadcaster モードは過去の温度等を保存しないモードなので、履歴保存と排他になります。（Weather News のスマホアプリなどで過去履歴も読む運用をしたい場合はこのページのコードとは両立できません。）</strong>

## 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 0C4C<strong>3042</strong>-7700-46F4-AA96-D5E974E32A54 の書き換え。

スマホアプリで長い16進文字列を手で打つのは面倒ですので（元の値コピペできるソフトあればいいのですが知らないので）、ラズパイ等があれば (1) のほうが楽ですね。(2)の記事では、書き込む文字列は Limited Broadcaster の時間が長い設定になっているので、(BigEndianになってる？)デフォルトからBeacon modeだけ書き換えるなら(1)の記事同様 0808A0000A0032000400 等にすると良いと思います。General Broadcaster でしか使わないならどちらでも同じですが。

# 参考サイトを見ながら ArduinoIDE での開発の準備を
参考サイト: [BBC micro:bit (DEKO のアヤシいお部屋)](https://ht-deko.com/arduino/microbit.html)
micro:bit のプログラムを ArduinoIDE で開発するための情報が豊富です。

(1)こちらの「セットアップ」をすることで、Arduino IDEでの開発が可能になります。([Sandeep Mistry による arduino-nRF51](https://github.com/sandeepmistry/arduino-nRF5)
 を設定する）
(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ライブラリ](https://www.arduino.cc/en/Reference/ArduinoBLE)を使えばセントラルにスキャンさせることはできるが、manufacturer data を読まない。
・Sandeep Mistry は arduino-nRF51 で利用する [BLEPeripheral ライブラリ](https://github.com/sandeepmistry/arduino-BLEPeripheral)を用意しているが、BLECentral ライブラリはない
・[Adafruit の nRF52 ライブラリ](https://github.com/adafruit/Adafruit_nRF52_Arduino)  で 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 が見つからない等エラーになります）

<details><summary>nRF51-microbit.ino </summary><div>

```c
#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
}
```
</div></details>

# その他
## 動画
https://x.com/ajtajta_j/status/1368473493633986561

## AE-TYBLE16でもできました
AE-TYBLE16もnRF51822搭載で、qiita内で Arduino化の記事がいくつかありますが(たとえば[こちら](https://qiita.com/hktechno/items/f594f87493fbe11de3fb))、ArduinoIDEを使って S130を入れることができます。（SoftDeviceを入れ直すとUICRの再設定が必要。J-LINKを使う or STLinkしか持ってなければ system_nrf51.c を改造 ([この文書](https://www.yuden.co.jp/wireless_module/document/datareport2/jp/TY_BLE_EYSGJNZ_DataReport_V1_8_20171215J.pdf)のp31のとおり)で対応可能）
BLEPeripheralライブラリが動く状態のAE-TYBLE16 であれば、上述のコードから micro:bit 固有のボタンやLED関連の部分を消すだけで動ききました。