Help us understand the problem. What is going on with this article?

[応用編]JavaScriptでバイナリデータを扱ってみる~Bluetoothから取得した温度データを解析~(3/3)

More than 1 year has passed since last update.

BRIGHT VIE Advent Calendar 2017 - Qiita の6日目です!

「JavaScriptでバイナリデータを扱ってみる」シリーズでお届けしておりますが、本日は最終回!

なお、ここまでの記事は下記をご参考下さい。

はじめに

昨日はBluetooth経由で温度データを扱う浮動小数点数について、
「IEEE-754で定義されているfloat」と「IEEE-11073で定義されているFLOAT」の違いについて理解しました。

本日はこれまでの内容を踏まえて、実際にBluetoothから取得したデータを元にデータを解析してみます。

実際のデータ

早速ですが、実際にBluetoothから取得したデータを下記としてデータを解析してみます。

16進数で定義

var temperatureMeasurement = [0x06, 0x69, 0x01, 0x00, 0xFF, 0xE1, 0x07, 0x0C, 0x05, 0x00, 0x20, 0x28, 0x02];

1. 受信したデータを準備

本来はここはBluetoothから取得したデータを用いますが、今回は明示的に定義します。
(弊社ではAngular/Ionicを利用してBluetoothと連携しており、実際に取得する処理は、Ionic Advent Calendar 2017の方で記載しようと思います。)

// 上記で定義した、「temperatureMeasurement」の配列分のバッファーを確保する(今回だと13バイト)
var buffer = new ArrayBuffer(temperatureMeasurement.length);
let dv = new DataView(buffer);
// データをセットする
temperatureMeasurement.forEach((value, index) => {
  dv.setUint8(index, value);
}); 

2. データを解析する

上記のバイナリデータから
- 計測温度
- 計測時間
- 計測タイプ
をそれぞれ取得したいと思います。

実際のフォーマットや浮動小数点の計算方法などは前回までの記事で記載しているため、今回はプログラムだけを記載します。

BLEで送られてくるデータを解析する処理

  • 出来ること
    • readFLOAT
      • IEEE-11073 32-bit FLOAT型をバイナリから取得し実数に変換する
    • readSFLOAT
      • IEEE-11073 16-bit SFLOAT型をバイナリから取得し実数に変換する
    • readDateTime
      • 計測時間を返す
    • readTemperatureType
      • 計測時の状況を返す
/**
 * Bluetoothで記録されている各データを読み込みための便利クラス
 */
class BluetoothFormat {

  /**
   * IEEE-11073 32-bit FLOAT型をバイナリから取得し実数に変換する
   * 
   * @param dv:DataView   バイナリデータ
   * @param offset:Number バイナリデータを読み取る開始位置
   * @param littleEndian  バイトオーダーの扱い(デフォルトは、fakse = ビッグエンディアン方式:)
   */
  static readFLOAT(dv, offset = 0, littleEndian = false) {

    // 1. バイトオーダーを指定して32ビットデータを取得
    var data = dv.getUint32(offset, littleEndian);

    // 2. 仮数部を取得する(32ビットの場合は下位24ビットが仮数部)
    //    - 取得した32ビットデータに下位24ビットの全てのフラグを立てた値を掛け算することで取得
    var mantissa = (data & 0x00FFFFFF);

    // 仮数部が2の補数かどうかを判定する
      // 下位24ビットのうちの上位1ビット目にフラグが立っていれば2の補数
    if ((mantissa & 0x00800000) > 0) {
      // 負の値の場合は、2の補数を用いて数値を求める
      mantissa = -1 * (~(mantissa - 0x01) & 0x00FFFFFF)
    }

    // 3. 指数部を取得する(32ビットの場合は上位8ビットが指数部)
    // 上位8ビットを取得するためには、32ビットの場合右へ24ビットシフトさせると取得可能
    // なお、JavaScriptの場合「>>」を利用すると、符号を維持する右シフトであるため、 
    // ビット演算を用いると2の補数も考慮して10進数に変換してくれる
    var exponential = data >> 24;

    // 「仮数部 × 基数部 ^ 指数部」の公式に当てはめて変換
    return mantissa * Math.pow(10, exponential);
  }

  /**
   * IEEE-11073 16-bit SFLOAT型をバイナリから取得し実数に変換する
   * 
   * @param dv:DataView   バイナリデータ
   * @param offset:Number バイナリデータを読み取る開始位置
   * @param littleEndian  バイトオーダーの扱い(デフォルトは、fakse = ビッグエンディアン方式:)
   */
  static readSFLOAT(dv, offset = 0, littleEndian = false) {
    // 1. バイトオーダーを指定して16ビットデータを取得
    var data = dv.getUint16(offset, littleEndian);

    // 2. 仮数部を取得する(16ビットの場合は下位12ビットが仮数部)
    //    - 取得した16ビットデータに下位12ビットの全てのフラグを立てた値を掛け算することで取得
    var mantissa = (data & 0x0FFF);

    // 仮数部も2の補数で判定するため16ビットのうちの12ビット中の上位1桁がマイナスを表していないかを確認
    if ((mantissa & 0x0800) > 0) {
        // もしマイナス値の場合は、1を引いた後反転処理と12ビット分の論理和を取得する)
        mantissa = -1 * (~(mantissa - 0x01) & 0x0FFF)
    }

    // 3. 指数部を取得する(16ビットの場合は上位4ビットが指数部)
    // 上位4ビットを取得するためには16ビットの場合右へ12ビットシフトさせると取得可能
    var exponential = data >> 12;

    // 「仮数部 × 基数部 ^ 指数部」の公式に当てはめて変換
    return mantissa * Math.pow(10, exponential);
  }

  /**
   * 計測時間を返す
   * - Type: org.bluetooth.characteristic.date_time - Assigned Number: 0x2A08
   * 
   * @param dv:DataView   バイナリデータ
   * @param _offset:Number バイナリデータを読み取る開始位置
   * @param littleEndian  バイトオーダーの扱い(デフォルトは、fakse = ビッグエンディアン方式:)
   */
  static readDateTime(dv, _offset = 0, littleEndian = false) {

    let offset = _offset;

    // 時間は、下記の順で計7バイトのデータを扱う
    // 2バイト: 年、 1バイト: 月, 日, 時, 分, 秒
    let year, month, day, hour, minute, second;

    year = dv.getUint16(offset, littleEndian);
    offset += 2;
    // JavaScriptでMonthは0から始まるため、取得した月から1を引いておく
    month = dv.getUint8(offset) - 1;
    offset++;
    day = dv.getUint8(offset);
    offset++;
    hour = dv.getUint8(offset);
    offset++;
    minute = dv.getUint8(offset);
    offset++;
    second = dv.getUint8(offset);

    var dt = new Date(year, month, day, hour, minute, second);
    // UnixTimestampをミリ秒単位で返す
    return dt.getTime();
  }

  /**
   * 計測時の状況を返す
   * - Type: org.bluetooth.characteristic.temperature_type - Assigned Number: 0x2A1D
   * 
   * @param dv:DataView   バイナリデータ
   * @param _offset:Number バイナリデータを読み取る開始位置
   */
  static readTemperatureType(dv, offset = 0) {
    let typeValue = dv.getUint8(offset);
    let type;
    switch(typeValue) {
      case 1:
        type = 'Armpit';
        break;
      case 2:
        type = 'Body (general)';
        break;
      case 3:
        type = 'Ear (usually ear lobe)';
        break;
      case 4:
        type = 'Finger';
        break;
      case 5:
        type = 'Gastro-intestinal Tract';
        break;
      case 6:
        type = 'Mouth';
        break;
      case 7:
        type = 'Rectum';
        break;
      case 8:
        type = 'Toe';
        break;
      case 9:
        type = 'Tympanum (ear drum)';
        break;
      default:
        type = '';
    }
    return type;
  }
}

BLEで送られてくる温度を取り扱う処理

  • 出来ること
    • getValue
      • [温度データ]摂氏(℃) または 華氏(°F)にて温度データを取得する
    • getUnit
      • [温度単位]温度データの単位を取得する
    • getTimestamp
      • [温度計測時間]計測時間があれば、UnixTimestamp形式で返す
    • getType
      • [温度タイプ]計測タイプがあれば、計測タイプを返す
/**
 * BLEで受信した温度データを扱うクラス
 *
 * Name: Temperature Measurement
 * Type: org.bluetooth.characteristic.temperature_measurementDownload / View
 * Assigned Number: 0x2A1C
 *
 * 参考)
 *    https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.temperature_measurement.xml
 */
class TemperatureMeasurement {

  constructor(buffer, littleEndian = false) {
    // バイナリデータを管理する
    this.buffer = new DataView(buffer);
    this.littleEndian = littleEndian;

    // 各種データのメモリ使用量を定義する
    this.offset = {
      init: 0,                        // 初期バイト位置を記録
      flags: 1,                       // フラグ管理に1バイト利用する
      temperatureMeasurementValue: 4, // 温度は、4バイト利用
      timestamp: 7,                   // 計測時間は、7バイト利用
      temperatureType: 1,             // 計測タイプは、1バイト利用
    };
  }

  /****************************************************
   * 1バイト目の各種フラグが立っているかをチェックする
   ****************************************************/
  /**
   * 1ビット目: 温度の設定をチェック
   */
  _checkTemperatureMeasurementValueFlats() {
    return ((this.getFlags() & 0x01) >= 1);
  }
  /**
   * 2ビット目: 計測時間の記録有無をチェック
   */
  _checkTimestampFlags() {
    return ((this.getFlags() & 0x02) >= 1);
  }
  /**
   * 3ビット目: 温度タイプの記録有無をチェック
   */
  _checkTemperatureTypeFlags() {
    return ((this.getFlags() & 0x03) >= 1);
  }


  /****************************************************
   * バイナリデータの読み込み位置を取得する
   ****************************************************/
  /**
   * 計測時間のバイナリデータのoffsetを取得する
   */
  getTimestampOffset() {
    return this.offset.init + this.offset.flags + this.offset.temperatureMeasurementValue;
  }

  /**
   * 計測タイプのバイナリデータのoffsetを取得する
   *
   * - 計測時間の記録があるときとないときで開始位置が異なる
   */
  getTemperatureTypeOffset() {
    let offset = this.getTimestampOffset();
    if (this._checkTimestampFlags()) {
      offset += this.offset.timestamp;
    }
    return offset;
  }


  /****************************************************
   * バイナリデータから各種データを読み込む処理
   ****************************************************/
  /**
   * [Flags]
   * 各データがどのように保持されているかの情報を取得する
   */
  getFlags() {
    return this.buffer.getUint8(this.offset.init);
  }

  /**
   * [温度データ]
   * 摂氏(℃) または 華氏(°F)にて温度データを取得する
   *
   * どちらの場合でも、2バイト目から「IEEE-11073 32-bit FLOAT」形式で保存されている
   */
  getValue() {
    let offset = this.offset.init + this.offset.flags;
    // 2~5バイト目までが温度を管理している部分であるため、対象のバイナリデータを抜き出す
    return BluetoothFormat.readFLOAT(this.buffer, offset, this.littleEndian);
  }

  /**
   * [温度単位]
   * 単位を取得する
   *
   * Flagsの1ビット目の値が、
   *  - 0の場合は、摂氏(℃)  : Celsius
   *  - 1の場合は、華氏(°F) : Fahrenheit.
   */
  getUnit() {
    if (this._checkTemperatureMeasurementValueFlats()) {
      return 'Fahrenheit';
    } else {
      return 'Celsius';
    }
  }


  /**
   * [温度計測時間]
   * 計測時間があれば、UnixTimestamp形式で返す
   *
   * Flagsの2ビット目の値が1であれば時間を返す
   */
  getTimestamp() {
    if (this._checkTimestampFlags()) {
      let offset = this.getTimestampOffset();
      return BluetoothFormat.readDateTime(this.buffer, offset, this.littleEndian);
    } else {
      return 0;
    }
  }

  /**
   * [温度タイプ]
   * 計測タイプがあれば、計測タイプを返す
   *
   * Flagsの3ビット目の値が1であれば時間を返す
   */
  getType() {
    if (this._checkTemperatureTypeFlags()) {
      let offset = this.getTemperatureTypeOffset();
      return BluetoothFormat.readTemperatureType(this.buffer, offset, this.littleEndian);
    } else {
      return 0;
    }
  }
}

データ解析

上記処理を利用してデータを解析してみます。

「1. 受信したデータを準備」で設定したDataViewインスタンスからバッファを取り出し、
温度を温度を扱うクラスを初期化します。

let littleEndian = true;
let tm = new TemperatureMeasurement(dv.buffer, littleEndian);
console.log('温度:' + tm.getValue());                        // 温度:36.1
console.log('単位:' + tm.getUnit());                         // 単位:Celsius
console.log('計測時間(Timestamp):' + tm.getTimestamp());      // 計測時間(Timestamp): 1512401560000
console.log('計測時間(日時):' + new Date(tm.getTimestamp()));  // 計測時間(日時):Tue Dec 05 2017 00:32:40 GMT+0900 (JST)
console.log('計測タイプ:' + tm.getType());                     // 計測タイプ:Body(general)

それぞれ正常に取得できました!

こちらのコードはGitHubにも公開しておりますので、もしよろしければどうぞ!

まとめ

さて、JavaScriptでバイナリデータを扱ってみるシーリズ、いかがでしたでしょうか。
Webエンジニアからすると、バイナリデータを扱うことはあまり馴染みがないことだと思いますので、
自分自身にとっても、非常に新鮮な分野であり楽しい開発案件の思い出でした。

今後、
- Web Bluetooth APIを利用したデータ取得
- 他の言語でのバイナリ操作
などなど試してみたいなと思いました。

megadreams14
平成元年生,兵庫県出身,スタートアップ企業(介護×IT)でCTOやってます。 AWS Summit 2014Tokyo,Jenkins Conference 2015, Developers Summit 2015で発表!! 介護×ITという分野に興味ある方、お気軽にご連絡下さい!!
https://brightvie.me/
brightvie
「あなたの“困った・できたらいいな“をカタチに」 ブライト・ヴィーは手作りのICTシステムをお届けするエンジニアチームです。
https://brightvie.me/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away