はじめに
Omronの環境センサーを使ったよくある例では、環境センサーのモードをデータ保存なしのBeaconモードに変更し、BLEのアドバタイズパケットに乗っているそのときそのときの観測データを取得しています。
この方法にはデータ受信側での処理が簡単になる反面、データの欠測が発生しやすいデメリットがあります。
この記事で紹介する手法では、Omronの環境センサーのBeaconモードをデータ保存有りモードで運用し、データの欠測をなるべく防ぐことを目指します。
Omronの環境センサーのBeaconモードをデータ保存有りモードに変更すると、BLEアドバタイズパケットには観測データが乗らなくなりますが、環境センサー内のフラッシュメモリに観測データが保存されるようになります。
2022.1.15追記
bluepyの方でBlueZとのpipeを開いてBLE接続処理を行っているようなので、アプリ側からの強制的なタイムアウトでの切断処理をしないように変更しました。
raspi-wxbeacon2
今回つくったプログラムです。
https://github.com/cnaos/raspi-wxbeacon2
プログラムの使い方
- config.yamlにデータを取得したい環境センサーデバイスのBLE MACアドレスを設定する
- main.pyを実行する
- これでSQLiteのDBファイルに観測データが蓄積される。
- post_influx.dbを実行する
- SQLiteのDBファイルに保存されている観測データをconfig.yamlの設定に従ってinfluxdbへPOSTする。
これを定期的に繰り返せばinfluxDBにデータが転送されるので、grafanaなどでグラフ化しましょう。
etcフォルダ以下にsystemd用の設定ファイルのサンプルを用意しておきました。
WorkingDirectoryとExecStartを書き換えて利用してください。
プログラムの構成
- main.py
- OMRON環境センサーからSQLiteDBに観測データを取得する
- post_influxdb.py
- SQLiteDBに保存されている観測データをinfluxDBへPOSTする
- influxDB(別途インストールしてください)
- 時系列データベース
- Grafana(別途インストールしてください)
- 時系列データの表示
Omron環境センサーのコネクトモードおよびデータ保存ありモードについて
要点だけ抜粋して説明します。
詳細はOmronのユーザーズマニュアルを参照してください。
Omronの環境センサーにはフラッシュメモリが搭載されており、設定した間隔での観測データの記録が可能です。
観測データを記録するためには、以下の2つが必要です。
- 環境センサーのビーコンモードをデータ保存有りにモードに変更する
- 環境センサーに現在時刻を設定する
環境センサーのビーコンモードは電源投入直後の初期値のビーコンモード
0x08 :Event Beacon (ADV)
を想定しています。
ビーコンモードを簡単に判別するには、Bluetoothの検索アプリなどで
デバイスの短い名前が「Env」、
デバイスの名前が「EnvSensor-BL01」と表示されていればOKです。
環境センサーの測定間隔変更時の注意
環境センサーの測定間隔の変更を行うと、データの記録ページ位置がリセットされ0ページからの保存になります。
古いデータは上書きされてしまうので注意しましょう。
main.pyコマンドに測定間隔の変更および現在時刻の設定用オプションを用意しました。
測定間隔を変更したいデバイスのBLE MACアドレスと測定間隔(秒)を指定します。
./main.py --addr XX:XX:XX:XX:XX:XX --setinterval 300
※XX:XX:XX:XX:XX:XX
の部分には環境センサのBLE MACアドレスを指定してください。
環境センサーからデータ保存有りモードでのデータの読み出し方
この記事でのBLEのUUIDの表記について
Omronの環境センサーのBLEサービスのUUIDやBLE CharacteristicsのUUIDはちゃんと書くと
「0C4CXXXX-7700-46F4-AA96D5E974E32A54」
という形式なのですが、変化するのはXXXXの4桁だけなので、この部分だけを取り出して
「Time Information(0x3031)」
と記述します。
step1. bluepyでの基本的な流れ
- BLEデバイスと接続する
- 読み出したいBLE Serviceを取得する
- 読み出したいBLE Characteristicsを取得する
- BLEデバイスからデータを読み出す
- 読みだしたデータを人間が読める形にデコードする
以下の例ではOmron環境センサーのLatest Data(0x3001)を読み出しています。
このBLE Characteristicsからは環境センサーの最新の観測データが読み出せます。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import struct
from bluepy.btle import Peripheral, UUID
OMRON_LATEST_DATA_UUID = UUID('%08X-7700-46F4-AA96-D5E974E32A54' % (0x0C4C0000 + 0x3001))
OMRON_SENSOR_SERVICE_UUID = UUID('%08X-7700-46F4-AA96-D5E974E32A54' % (0x0C4C0000 + (0xFFF0 & 0x3000)))
parser = argparse.ArgumentParser(description='OMRONの環境センサーからLatestDataを取得します')
parser.add_argument("--addr", required=True, type=str, help='環境センサーのMACアドレスを指定する')
args = parser.parse_args()
# 環境センサーに接続する
ble_peripheral = Peripheral()
print(f"connecting... {args.addr}")
ble_peripheral.connect(addr=args.addr, addrType="random")
print(f"ble_peripheral={ble_peripheral}")
# BLE サービスを取得
service = ble_peripheral.getServiceByUUID(uuidVal=OMRON_SENSOR_SERVICE_UUID)
print(f"service = {service}")
# BLE Characteristicsを取得
ble_char = service.getCharacteristics(forUUID=OMRON_LATEST_DATA_UUID)[0]
print(f"ble_char = {ble_char}")
# LatestDataから測定データの読み出し
raw_data = ble_char.read()
print(f"raw_data = {raw_data}")
# 生の測定データを変換
(row_number, temperature, humidity, light, uv_index, pressure, noise, discomfort_index, heat_stroke,
battery_level) = struct.unpack('<BhhhhhhhhH', raw_data)
temperature /= 100
humidity /= 100
uv_index /= 100
pressure /= 10
noise /= 100
discomfort_index /= 100
heat_stroke /= 100
battery_level /= 1000
# 変換結果を表示
print(
f"temperature = {temperature}, humidity = {humidity}, uv_index={uv_index}, pressure={pressure}, noise={noise}"
f", discomfort_index={discomfort_index}, heat_stroke={heat_stroke}, battery_level={battery_level}")
ble_peripheral=<bluepy.btle.Peripheral object at 0xb657d5b0>
service = Service <uuid=0c4c3000-7700-46f4-aa96-d5e974e32a54 handleStart=23 handleEnd=37>
ble_char = Characteristic <0c4c3001-7700-46f4-aa96-d5e974e32a54>
raw_data = b"\x03\t\t\x03\t\x00\x00\x01\x00\xef'X\x0e-\x1aZ\x06*\x0b"
temperature = 23.13, humidity = 23.07, uv_index=0.01, pressure=1022.3, noise=36.72, discomfort_index=67.01, heat_stroke=16.26, battery_level=2.858
デバイスへの接続が失敗すると以下のようなエラーになります。
Traceback (most recent call last):
File "./example1.py", line 20, in <module>
ble_peripheral.connect(addr=args.addr, addrType="random")
File "/home/cnaos/.local/share/virtualenvs/dev-raspi-wxbeacon2-cfd34nEC/lib/python3.7/site-packages/bluepy/btle.py", line 445, in connect
self._connect(addr, addrType, iface)
File "/home/cnaos/.local/share/virtualenvs/dev-raspi-wxbeacon2-cfd34nEC/lib/python3.7/site-packages/bluepy/btle.py", line 439, in _connect
raise BTLEDisconnectError("Failed to connect to peripheral %s, addr type: %s" % (addr, addrType), rsp)
bluepy.btle.BTLEDisconnectError: Failed to connect to peripheral XX:XX:XX:XX:XX:XX, addr type: random
step2. BLEデバイスへの接続処理にリトライを入れる
BLEデバイスへの接続処理が一番失敗しやすいので、ここにリトライ処理を入れます。
以下の例はLatest Data(0x3001)の読み出しの接続処理部分を改良したもので、
connect()メソッドで接続とリトライ処理をまとめて行っています。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import struct
import time
from bluepy.btle import Peripheral, UUID, BTLEException
OMRON_LATEST_DATA_UUID = UUID('%08X-7700-46F4-AA96-D5E974E32A54' % (0x0C4C0000 + 0x3001))
OMRON_SENSOR_SERVICE_UUID = UUID('%08X-7700-46F4-AA96-D5E974E32A54' % (0x0C4C0000 + (0xFFF0 & 0x3000)))
def connect(addr: str, max_retry=5, retry_interval_sec=1) -> Peripheral:
ble_peripheral = None
is_connected = False
for i in range(max_retry):
try:
print(f'connecting to {addr} {i + 1}/{max_retry}')
ble_peripheral = Peripheral(deviceAddr=addr, addrType="random")
except BTLEException as e:
print(f'ERROR: try {i + 1}: BTLE Exception while connecting ')
print(f'ERROR: type:' + str(type(e)))
print(f'ERROR: args:' + str(e.args))
time.sleep(retry_interval_sec)
else:
is_connected = True
print(f'connected.')
return ble_peripheral
if not is_connected:
print(f"ERROR: connect failed.")
raise Exception(F"BTLE connect to {addr} failed.")
def main():
parser = argparse.ArgumentParser(description='OMRONの環境センサーからLatestDataを取得します')
parser.add_argument("--addr", required=True, type=str, help='環境センサーのMACアドレスを指定する')
args = parser.parse_args()
# 環境センサーに接続する
ble_peripheral = connect(addr=args.addr)
# BLE サービスを取得
service = ble_peripheral.getServiceByUUID(uuidVal=OMRON_SENSOR_SERVICE_UUID)
# BLE Characteristicsを取得
ble_char = service.getCharacteristics(forUUID=OMRON_LATEST_DATA_UUID)[0]
# LatestDataから測定データの読み出し
raw_data = ble_char.read()
# 生の測定データを変換
(row_number, temperature, humidity, light, uv_index, pressure, noise, discomfort_index, heat_stroke,
battery_level) = struct.unpack('<BhhhhhhhhH', raw_data)
temperature /= 100
humidity /= 100
uv_index /= 100
pressure /= 10
noise /= 100
discomfort_index /= 100
heat_stroke /= 100
battery_level /= 1000
# 変換結果を表示
print(
f"temperature = {temperature}, humidity = {humidity}, uv_index={uv_index}, pressure={pressure}, noise={noise}"
f", discomfort_index={discomfort_index}, heat_stroke={heat_stroke}, battery_level={battery_level}")
if __name__ == "__main__":
main()
connecting to XX:XX:XX:XX:XX:XX 1/5
ERROR: try 1: BTLE Exception while connecting
ERROR: type:<class 'bluepy.btle.BTLEDisconnectError'>
ERROR: args:('Failed to connect to peripheral XX:XX:XX:XX:XX:XX, addr type: random', {'rsp': ['stat'], 'state': ['disc'], 'mtu': [0], 'sec': ['low']})
connecting to XX:XX:XX:XX:XX:XX 2/5
ERROR: try 2: BTLE Exception while connecting
ERROR: type:<class 'bluepy.btle.BTLEDisconnectError'>
ERROR: args:('Failed to connect to peripheral XX:XX:XX:XX:XX:XX, addr type: random', {'rsp': ['stat'], 'state': ['disc'], 'mtu': [0], 'sec': ['low']})
connecting to XX:XX:XX:XX:XX:XX 3/5
connected.
temperature = 10.76, humidity = 43.24, uv_index=0.01, pressure=1021.3, noise=32.35, discomfort_index=53.43, heat_stroke=9.57, battery_level=2.654
step3. 環境センサーとのデータのやり取り部分をクラス化する
このまま直接データを読み書きしてもいいのですが、UUIDがどのBLE Characteristicsを示すのかがわかりにくかったり、読みだしたデータのデコードもCharacteristics毎に異なるので、Omron環境センサーのBLE CharacteristicsのUUIDとデータのエンコード・デコードを行うクラスを使うように書き換えます。
以下の例ではmain()メソッド内でomron/env_sensor_data.py
内のクラスOmronLatestData
を利用しています。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('../')
import argparse
import time
from bluepy.btle import Peripheral, BTLEException
from omron.env_sensor_data import OmronLatestData
def connect(addr: str, max_retry=5, retry_interval_sec=1) -> Peripheral:
ble_peripheral = None
is_connected = False
for i in range(max_retry):
try:
print(f'connecting to {addr} {i + 1}/{max_retry}')
ble_peripheral = Peripheral(deviceAddr=addr, addrType="random")
except BTLEException as e:
print(f'ERROR: try {i + 1}: BTLE Exception while connecting ')
print(f'ERROR: type:' + str(type(e)))
print(f'ERROR: args:' + str(e.args))
time.sleep(retry_interval_sec)
else:
is_connected = True
print(f'connected.')
return ble_peripheral
if not is_connected:
print(f"ERROR: connect failed.")
raise Exception(F"BTLE connect to {addr} failed.")
def main():
parser = argparse.ArgumentParser(description='OMRONの環境センサーからLatestDataを取得します')
parser.add_argument("--addr", required=True, type=str, help='環境センサーのMACアドレスを指定する')
args = parser.parse_args()
# 環境センサーに接続する
ble_peripheral = connect(addr=args.addr)
# BLE サービスを取得
latest_data = OmronLatestData()
service = ble_peripheral.getServiceByUUID(uuidVal=latest_data.serviceUuid)
# BLE Characteristicsを取得
ble_char = service.getCharacteristics(forUUID=latest_data.uuid)[0]
# LatestDataから測定データの読み出し
raw_data = ble_char.read()
# 生の測定データを変換
latest_data.parse(raw_data)
# 変換結果を表示
print(f"latest_data={latest_data.to_dict()}")
if __name__ == "__main__":
main()
step4. 1ページ分のデータの読み出し
環境センサーに保存されている観測データの構造
- 観測データはページという単位で保存されています。
- ページにはそのページの記録開始日時(unixtime)と、観測データが13件含まれます。
- ページ内の観測データと次の観測データの間にはデバイスに設定した測定間隔の時間が空いています。
- ページは0から2047まであり、最後までページが利用されるとふたたび0ページ目からデータが記録されます。
- 最後のページまで記録されると再び0ページからデータを保存するリングバッファになっているようです。
1ページ分のデータを読み出すための処理フロー
- RequestPage(0x3003)で読み出したいページと行数を書き込む
- ResponseFlag(0x3004)を読み出す
3. ResponseFlagのupdate flagが0x01
になっていたらページの読み出し準備完了
4. ResponseFlagのupdate flagが0x00
なら準備中なので ResponseFlagの読み出しからやり直し
5. ResponseFlagのupdate flagが 上記以外ならエラーなので終了する。 - 指定した読み出し行数だけResponseDataの読み出しを繰り返す
実装サンプル
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('../')
import argparse
import time
from bluepy.btle import Peripheral, BTLEException
from omron.env_sensor_data import OmronRequestPage, OmronResponseFlag, OmronResponseData
def connect(addr: str, max_retry=5, retry_interval_sec=1) -> Peripheral:
ble_peripheral = None
is_connected = False
for i in range(max_retry):
try:
print(f'connecting to {addr} {i + 1}/{max_retry}')
ble_peripheral = Peripheral(deviceAddr=addr, addrType="random")
except BTLEException as e:
print(f'ERROR: try {i + 1}: BTLE Exception while connecting ')
print(f'ERROR: type:' + str(type(e)))
print(f'ERROR: args:' + str(e.args))
time.sleep(retry_interval_sec)
else:
is_connected = True
print(f'connected.')
return ble_peripheral
if not is_connected:
print(f"ERROR: connect failed.")
raise Exception(F"BTLE connect to {addr} failed.")
def write_request_page(ble_peripheral: Peripheral, page: int, row: int):
assert 0 <= page <= 2047
assert 0 <= row <= 12
request_page = OmronRequestPage()
ble_service = ble_peripheral.getServiceByUUID(uuidVal=request_page.serviceUuid)
ble_char = ble_service.getCharacteristics(forUUID=request_page.uuid)[0]
write_value = request_page.encode_data(page, row)
print(f'write_request_page(page={page}, row={row}) write_value={write_value}')
ble_char.write(write_value)
def read_response_flag(ble_peripheral: Peripheral) -> OmronResponseFlag:
response_flag = OmronResponseFlag()
ble_service = ble_peripheral.getServiceByUUID(uuidVal=response_flag.serviceUuid)
ble_char = ble_service.getCharacteristics(forUUID=response_flag.uuid)[0]
print(f'read_response_flag')
raw_data = ble_char.read()
return response_flag.parse(raw_data)
def read_response_data(ble_peripheral: Peripheral):
response_data = OmronResponseData()
ble_service = ble_peripheral.getServiceByUUID(uuidVal=response_data.serviceUuid)
ble_char = ble_service.getCharacteristics(forUUID=response_data.uuid)[0]
print(f'read_response_data')
raw_data = ble_char.read()
return response_data.parse(raw_data)
def main():
parser = argparse.ArgumentParser(description='OMRONの環境センサーから指定したページの観測データを読み出します。')
parser.add_argument("--addr", required=True, type=str, help='環境センサーのMACアドレスを指定する')
parser.add_argument("--page", type=int, default=0, help='読み出したいページ番号')
parser.add_argument("--row", type=int, default=12, help='読み出したいページ行数')
args = parser.parse_args()
assert 0 <= args.page <= 2047
assert 0 <= args.row <= 12
# 環境センサーに接続する
ble_peripheral = connect(addr=args.addr)
# RequestPageを書き込む
write_request_page(ble_peripheral, args.page, args.row)
# ResponseFlgの読み出し
is_ready_response_data = False
for i in range(3):
response_flag = read_response_flag(ble_peripheral)
print(f'response_flag({i})={response_flag}')
if response_flag.update_flag == 0x01: # 更新完了
is_ready_response_data = True
break
elif response_flag.update_flag == 0x00: # 更新中
continue
else: # 更新失敗
print(f'ERROR: response flag failed.')
raise IOError
if not is_ready_response_data:
print(f'ERROR: response flag failed.')
raise IOError
# 指定したページのデータの読み出し
for i in range(args.row + 1):
response_data = read_response_data(ble_peripheral)
print(F'response_data[{i}] = {response_data}')
ble_peripheral.disconnect()
if __name__ == "__main__":
main()
write_request_page(page=1, row=12) write_value=b'\x01\x00\x0c'
read_response_flag
response_flag(0)={'update_flag': 1, 'unix_time': 1607876503, 'datetime': '2020-12-14T01:21:43+09:00'}
read_response_data
response_data[0] = {'row': 12, 'temperature': 14.78, 'humidity': 53.08, 'light': 0, 'uv': 0.02, 'barometric_pressure': 1002.5, 'noise': 33.94, 'discomfort_index': 58.44, 'heat_stroke': 13.62, 'battery_level': 2.858}
read_response_data
response_data[1] = {'row': 11, 'temperature': 14.81, 'humidity': 53.06, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1002.6, 'noise': 31.9, 'discomfort_index': 58.48, 'heat_stroke': 13.64, 'battery_level': 2.858}
read_response_data
response_data[2] = {'row': 10, 'temperature': 14.83, 'humidity': 53.0, 'light': 0, 'uv': 0.02, 'barometric_pressure': 1002.7, 'noise': 33.18, 'discomfort_index': 58.51, 'heat_stroke': 13.66, 'battery_level': 2.858}
read_response_data
response_data[3] = {'row': 9, 'temperature': 14.84, 'humidity': 52.97, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.0, 'noise': 33.18, 'discomfort_index': 58.52, 'heat_stroke': 13.6, 'battery_level': 2.858}
read_response_data
response_data[4] = {'row': 8, 'temperature': 14.85, 'humidity': 52.92, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.0, 'noise': 32.35, 'discomfort_index': 58.54, 'heat_stroke': 13.61, 'battery_level': 2.858}
read_response_data
response_data[5] = {'row': 7, 'temperature': 14.86, 'humidity': 52.86, 'light': 0, 'uv': 0.02, 'barometric_pressure': 1003.3, 'noise': 32.35, 'discomfort_index': 58.55, 'heat_stroke': 13.62, 'battery_level': 2.858}
read_response_data
response_data[6] = {'row': 6, 'temperature': 14.88, 'humidity': 52.77, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.2, 'noise': 33.57, 'discomfort_index': 58.58, 'heat_stroke': 13.63, 'battery_level': 2.858}
read_response_data
response_data[7] = {'row': 5, 'temperature': 14.88, 'humidity': 52.72, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.4, 'noise': 32.77, 'discomfort_index': 58.58, 'heat_stroke': 13.63, 'battery_level': 2.861}
read_response_data
response_data[8] = {'row': 4, 'temperature': 14.9, 'humidity': 52.63, 'light': 0, 'uv': 0.02, 'barometric_pressure': 1003.4, 'noise': 31.58, 'discomfort_index': 58.6, 'heat_stroke': 13.61, 'battery_level': 2.861}
read_response_data
response_data[9] = {'row': 3, 'temperature': 14.92, 'humidity': 52.57, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.6, 'noise': 31.58, 'discomfort_index': 58.63, 'heat_stroke': 13.62, 'battery_level': 2.861}
read_response_data
response_data[10] = {'row': 2, 'temperature': 14.93, 'humidity': 52.48, 'light': 0, 'uv': 0.02, 'barometric_pressure': 1003.7, 'noise': 32.77, 'discomfort_index': 58.64, 'heat_stroke': 13.63, 'battery_level': 2.861}
read_response_data
response_data[11] = {'row': 1, 'temperature': 14.95, 'humidity': 52.4, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.9, 'noise': 32.35, 'discomfort_index': 58.67, 'heat_stroke': 13.65, 'battery_level': 2.865}
read_response_data
response_data[12] = {'row': 0, 'temperature': 14.99, 'humidity': 52.25, 'light': 0, 'uv': 0.01, 'barometric_pressure': 1003.7, 'noise': 32.77, 'discomfort_index': 58.72, 'heat_stroke': 13.67, 'battery_level': 2.865}
STEP5. 観測データの差分読み出し
環境センサーからの観測データ1ページあたりの読み出しに4秒から5秒程度かかるようで、接続のたびに全ページの読み出しを行うのは現実的ではありません。
そのため、DBに観測データを保存しておき、前回のコマンド起動時に取得した観測データからの差分だけを取得する差分読み出しを行うようにします。
差分読み出しのおおまかな処理フロー
- 環境センサーデバイスに接続する
- LatestPage(0x3002)を取得する
- 最新の観測データのページ番号と行番号、測定間隔が取得できる。
- DBに保存してある前回の読み出し済みの位置から、LatestPageで取得した最新のページ番号までのデータ取得のページ範囲を決める。
- ページごとの観測データ読み出し処理
- Request page(0x3003)で読み出し対象ページと読み出す行数を送って、読み出し対象のデータを指定する
- Response flag(0x3004)を読み出して、環境センサー側のデータ準備が完了するまで待つ
1. Response flagで読み出しを指定したページの観測開始日時が分かる - 読み出し対象のデータがなくなるまでResponse Data(0x3005)の読み出しを繰り返す
1. ResponseDataの1回の読み出しで取得できるのは1行分の観測データ
2. ResponseDataのrowが0になったら読み出し完了 - 読みだした1ページ分のデータの観測日時をページの観測開始日時とデータの測定間隔と行番号から求めて変換する。
- 読みだした観測データをDBへ保存する
実装サンプル
実装サンプルは手抜きで用意しなかったので、main.pyのprocess_target_device()メソッドを見てください。
参考にさせていただいた資料
-
https://ambidata.io/samples/temphumid/ble-gw-omron/
- ここのpythonスクリプトをベースにしました。
- https://github.com/AmbientDataInc/EnvSensorBleGw/blob/master/src/gw_RPi/env2ambientCS.py
-
https://iot-plus.net/make/raspi/visualizing-watt-environment-using-influxdb-grafana/
- influxdbとgrafanaをraspberry pi上にインストールする際の参考にしました。
- 2JCIE-BL01のユーザーズマニュアル