BTMETER社製 デジタル風速計 BT-100-APP

BTMETER社製デジタル風速計BT-100-APPは、風速と気温を同時に測定でき、測定結果をBLE(Bluetooth Low Energy)で送信することもできます。

BT-100-APPの形状を以下に示します(写真はAmazonの購入ページより)。

仕様:風速0.3〜30m / s(測定値の+/- 5%)解像度0.1m/s
   気温-10℃〜45℃(+/- 2℃)解像度0.2℃

風量は、m/s、Km/h、ft/min、Knots、mphの5つから単位を選べます。また、気温の表示は、摂氏と華氏を切り換えられます。

1.目的

BT-100-APPを拡張MSMとしてしようするためPDHとBLE接続で接続して、風速と温度をNode-REDのダッシュボードに表示してみます。

2. 動作の確認

BT-100-APPのBLEによるデータ送信を、PDHのpythonのbleakライブラリを使って受信してみます。
取得方法の詳細は、R-CPS-HP 第6巻 5-1. BLE機器との接続1-3. BLE接続に必要な情報を参照ください。

2-1. MACアドレスの確認

ターミナル画面で以下のコマンドを実行し、“TH Sensor”が表示されるかを調べます。

$ python3 scan_ble_devices.py

結果を以下に示します。“Anemometer”が表示されています。合わせてMACアドレスも確認できました。

2-2. uuidの確認

上記で分かったMACアドレスを使って、uuidを調べます。

$ python3 get_device_uuids.py 02:B3:EC:C3:4B:A6

結果を以下に示します。[’notify’]サービスのUUIDが見つかりました。

2-3. データの読出し確認

上記のuuidでデータが読み出せるかどうかを簡単なプログラムを作って確認しました。

結果、14Byteのbytearrayで送られてきています。この中のどこが、風速でどこが気温を示しているのかを調べていきます。

3. 温度と風速のデータの解読

ここから、気温と風速のデータの解読を行います。かなり地道な作業になりました。結果として、気温と風速に変換できるようにはなりましたが、数値をどのように送信データに関しているのかの変換のロジックは分かりませんでした。

手順として、風速0m/sの場合の気温を解読し、その後で風速を解読します。

3-1. 風速=0m/sの時の気温データの解読

上記のbytearayを2ByteごとにHexの文字に変換して温度とデータの対応を調べました。結果を以下の表にまとめます。
17.3℃~23.4℃の間で気温の上昇に合わせ、データを取得しました。
赤字の部分が小数点第1位のデータを、緑の部分が1の位のデータを、紫の部分が10の位のデータを表していることが分かりました。

これをまとめると次のような表になります。なぜか、1の位だけ、1ずれた数字になっています。

3.2. 風速データの解読

次に、風を風速計に当てて、データのどこに表示されるかを調べました。

なんと、気温データに4bit毎に入れ子になって表示されていることが分かりました。
また、データも温度と同じデータの様です。ただし、1の位は0m/sの時に気温と同じ’be’ですが、10の位は0m/sの時に’00’となっており、気温と異なります。

そこでもう少し強い風を当ててみました。なかなか10m/s以上にはならなくて、取れたデータから確認すると、’a0’が10の位の’1’を表しいそうなことが分かりました。1の位と小数点1位は、気温と同じデータが使えそうです。

風力の換算表は、以下の様になりました。

4. 単位の解読

気温の摂氏(℃)と華氏(℉)の切換えおよび風速のm/s、Km/h、ft/min、Knots、mphの切換えが、データのどこに現れるのかを調べてみました。

・摂氏から’1’を引いた値が華氏であることを示します。
・風速の単位切替は初めの2Byteの上位1Byteで行われています。


まとめると以下の表になります。この表に従って、JSONに数字だけではなく、単位を追加します。

5. PDHでのデータの読取り

前章での解読結果を使って、PDHで風速計”BT-100-APP”からnotifyされる気温と風量を読取り、ダッシュボードに表示させます。ダッシュボードへの表示結果を以下に示します。

5-1.notifyデータの読取り

notifyのデータを読み取るために、BT-100-APPにBLE(Bluetooth Low Energy)で接続する必要があります。
“2. 動作の確認”で調べたMACアドレスとサービスのuuidを使用します。
  MACアドレス          02:B3:EC:C3:4B:A6
  サービスのUUID    0000ffb2-0000-1000-8000-00805f9b34fb

読取ったデータを、“3. 温度と風速のデータの解読”で求めた変換表を使って、気温、風速に変換して、mqttでlocalhostへpublishします。プログラムはpythonで記載しています。コードはAppendix.1を参照ください。

5-2.Node-REDでのダッシュボード表示

次にNode-REDのフロー画面を示します。デバッグ画面に読み取った気温(temp)と風速(velocity)が表示されています。
単位も4章で解読したデータを読み取って、JSONデータを切り換えています(unit-t:気温、unit-v:風速)。
  ① mqtt inで受け取って、
  ② changeノードで温湿度のデータを振り分け
  ③ dashboardのノードでゲージとチャートを表示しています。

ダッシュボードの画面は章の初めをご覧ください。

Appendix.1 BLE接続で風速と気温を読み取るプログラム

# -*- coding: utf-8 -*-
import sys
import asyncio
from bleak import BleakClient

import paho.mqtt.client as mqtt
import time
import json
import urllib.request
import struct

# setting for BLE
ADDRESS = (
    "02:B3:EC:C3:4B:A6" # Anemometer BTMETER BT-100-APP
)
# UUID for notify
CHARACTERISTIC_UUID1 = "0000ffb2-0000-1000-8000-00805f9b34fb" # read & notify

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "anemometer"
# MQTT クライアントを作成
mqtt_client = mqtt.Client()

sdata = {    # 辞書型データ
    'sensor': 'BT-100-APP',
    'func':'Anemometer',
    'temp': 20.0,
    'unit-t':'deg',
    'velocity': 0.0,
    'unit-v':'m/s'
}

def publish(sdata):
    # Publish on MQTT
    # JSON データを文字列に変換
    json_data = json.dumps(sdata)
    # トピックに JSON データをパブリッシュ
    mqtt_client.publish(topic, json_data)
    
def parse_comma_separated_numbers(input_string):
    # カンマで文字列を分割し、各部分を浮動小数点数に変換
    parts = input_string.split(',')
    numbers = []
    for part in parts:
        try:
            number = float(part)
            numbers.append(number)
        except ValueError:
            # 数値に変換できない場合は無視
            pass
    return numbers

def bytes_to_hex(data):
    # bytearrayをバイト単位で16進数に変換
    data1 = ''.join(format(x, '02x') for x in data)
    # 4バイトごとの文字列に分割
    X_hex = [data1[i:i+4] for i in range(0, len(data1), 4)]
    return X_hex

def unit_read(data):
    ct = data[3]
    cv = data[2]
    unit_t = 'Error'
    unit_v = 'Error'
    
    if ct=='1' or ct=='5' or ct=='9':
        unit_t = 'degF'
    elif ct=='2' or ct=='6' or ct=='a': 
        unit_t = 'degC'

    if cv=='4':
        unit_v = 'm/s'
    elif cv=='2':
        unit_v = 'Km/h'
    elif cv=='1':
        unit_v = 'ft/min'
    elif cv=='0':
        if ct=='a' or ct=='9':
            unit_v = 'Knots'
        elif ct=='6' or ct=='5':
            unit_v = 'mph'

    return unit_t, unit_v

def conv01(sval):  # 気温 10の位
    hex_to_val = {
        'bf': 0,
        'a1': 1,
        'db': 2,
        'f9': 3,
        'e5': 4,
        '7d': 5,
        '7f': 6,
        'a9': 7,
        'ff': 8,
        'fd': 9
    }
    return hex_to_val.get(sval, 0)

def conv02(sval):   # 風速 10の位
    hex_to_val = {
        'be': 0,
        'a0': 1
    }
    return hex_to_val.get(sval, 0)
        
def conv03(sval):  # 気温、風速 1の位
    hex_to_val = {
        'be': 0,
        'a0': 1,
        'da': 2,
        'f8': 3,
        'e4': 4,
        '7c': 5,
        '7e': 6,
        'a8': 7,
        'fe': 8,
        'fc': 9
    }
    return hex_to_val.get(sval, 0)

def tens_digit(data):   # 10の位を変換
    temp = data[1]+data[3]
    velocity = data[0]+data[2]

    val_t = conv01(temp) * 10
    val_v = conv02(velocity) * 10
    return val_t, val_v

def unit_digit(data):  # 1の位を変換
    temp = data[1]+data[3]
    velocity = data[0]+data[2]

    val_t = conv03(temp)
    val_v = conv03(velocity)
    return val_t, val_v

def place_of_decimal(data):  # 小数点1位を変換
    temp = data[1]+data[3]
    velocity = data[0]+data[2]

    val_t = conv01(temp) * 0.1
    val_v = conv01(velocity) * 0.1
    return val_t, val_v

def value_read(data):
    temp = 0.0
    velocity = 0.0
    value = 0.0

    val = tens_digit(data[2])        # 10の位を変換
    temp = temp + val[0]
    velocity = velocity + val[1]
    val = unit_digit(data[1])        # 1の位を変換
    temp = temp + val[0]
    velocity = velocity + val[1]
    val = place_of_decimal(data[0])  # 小数点1位を変換
    temp = temp + val[0]
    velocity = velocity + val[1]
    return round(temp,1), round(velocity,1)

def notification_handler1(sender, data: bytearray):
    
    data1 = bytes_to_hex(data)
    unit = unit_read(data1[0]) # 単位読取
    val = value_read(data1[1:])

    sdata['temp'] = val[0]
    sdata['unit-t'] = unit[0]
    sdata['velocity'] = val[1]
    sdata['unit-v'] = unit[1]
    print(sdata)
    publish(sdata)

async def main(address):
    char_uuid1 = CHARACTERISTIC_UUID1

    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")

    print(address, char_uuid1)
    async with BleakClient(address) as client:
        print(f"Connected: {client.is_connected}")
        await client.start_notify(char_uuid1, notification_handler1)
        await asyncio.sleep(120.0)
        await client.stop_notify(char_uuid1)

    # MQTT クライアントを切断
    mqtt_client.disconnect()
        
if __name__ == "__main__":
    asyncio.run(
        main(
            sys.argv[1] if len(sys.argv) > 1 else ADDRESS,
        )
    )