技術的な話

【体重自動記録ツール】体重計のBLE情報を取得してみる #1

はじめに

最近の体重計ってBluetoothでスマホアプリと連携して体重を記録していくというものが多いですね。価格も\3,000-からと比較的リーズナブルな物も多いです。

ダイエットしているので体重の他にも体脂肪やら骨密度も測定出来て優れものです。
記録を取っていると自身が今どれぐらいの体重か、目標までどれだけ努力する必要があるのか等が明確になるのでやる気も出ます。

ちなみに我が家の体重計はFinc SmartScaleというものを使用しています。

ただ、測定する際に記録を取りたいスマートフォンを持っていくのが結構億劫になりがちです。
体重計の置き場所は大抵脱衣所とかなので、スマートフォンを忘れると取りに戻るのが面倒で測定して記録を取らない…といったことが発生しがちです。

歯抜けの記録を見るとなんだか気持ち悪いです。

自動で記録取ってくれる体重計とかないかなぁ、それかある程度体重計内で記録を保持してくれて、Bluetooth連携時に一括送信してくれると良いのですが。

調べてみるとWi-Fi接続可能な体重計があるらしいです。
クラウドに自動的にデータをアップロード出来るみたいです。
ただお値段がBluetoothと比較してちょっとお高め&種類もあまり無さそうといったところです。
怪しい中国産の安いWi-Fi対応の体重計もありましたが、データ送信先が良く分からないのでちょっと手を出しづらい。

そこで自宅にあるBluetooth対応の体重計をそのまま自動測定出来るようにツールを開発しました。

開発するもの

  • Bluetoothを受信して測定結果を送信可能とする。
  • 体重計との接続は常時行っておく。
    切断されても再接続するようにしておく。
  • 測定した記録はGoogle Spreadsheetや自PCに送信して記録する。
    ついでにスマホアプリと遜色なくグラフを出せたら良い。

今回の目標

  • 体重計とBluetooth接続して体重情報を受信する。

用意するもの

  • RaspberryPi 4

RaspberryPi 3でもBluetoothを内蔵しているので、それでも可能かと思います。
というか最悪Bluetoothを内蔵しているPCでも動くかもしれません。ただし今回紹介するツールはRapsberryPiでのみ動作確認を行っています。

BLEとは

我々が馴染み深いのはマウスやヘッドホン等のBluetooth接続ですが、それらはBluetooth Classicと呼ばれている規格です。
予めペアリングしておき、接続して通信します。

BLEはBluetooth 4.0で追加された規格一つです。
主にはIoT向けに消費電力を限りなく下げているのが特徴です。
BLEにはペアリングのような接続ではないです。例えば接続したい機器とスマートフォンがあった場合、機器側がアドバタイズ信号を送信して、スマートフォンで対象機器のアドバタイズ信号を受信します。その後スマートフォン側で接続処理を行うといった形です。

以下のサイトが参考になります。

https://www.ditto.live/blog/posts/jp-whats-the-difference-between-btclassic-and-ble

RaspberryPiの環境と事前準備

OS情報

pi@raspberrypi:~ $ cat /etc/os-release 
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs

Pythonバージョン

今回はPythonを使用してBLEに接続します。

pi@raspberrypi:~ $ python3 -V
Python 3.7.3

必須ソフトウェアのインストール

pi@raspberrypi:~ $ sudo apt-get install python-dev libbluetooth3-dev
pi@raspberrypi:~ $ sudo apt-get install libglib2.0 libboost-python-dev libboost-thread-dev

Pythonライブラリのインストール

今回はbleakのみを使用します。

pi@raspberrypi:~ $ sudo pip3 install -r requirements.txt

requirements.txt
-------------------------------------------------------
bleak==0.12.1

体重計に接続し、BLE情報を受信する

BLE Handlerについて

体重計への接続処理や情報受信の処理を纏めています。
主に下記動作を行います。

  1. 体重計に接続したら体重測定結果を通知してくれるサービス(UUID)をListenします。
    通知が体重計より送られてきた場合のcallbackは_weight_measurement_notification_handler()を登録します。
    またUUIDは体重計によって異なる可能性があります。その辺の調べ方は後述します。
  2. 接続状態を定期的に確認して、接続解除していれば一定時間待ってから再接続します。
  3. 体重計より情報が送られてきた場合は、2byte、3byte目を取得して体重を計算します。
    ここは体重計によって仕様が異なるので気を付けてください。別の体重計を使用している場合は値を取得してみてどのように計算するかを考える必要があります。
    私は1日位色んな値を取得してみて試行錯誤してようやく計算式が導けました…。
#!/usr/bin/env python3

import sys
import asyncio
import requests
import warnings
from datetime import datetime
from logging import Logger, getLogger, StreamHandler, DEBUG
from typing_extensions import Final
from bleak import BleakClient
from bleak import _logger as logger

WEIGHT_MEASUREMENT:Final[str] = "00002a9d-0000-1000-8000-00805f9b34fb"

warnings.simplefilter('ignore', FutureWarning)

class BleHandler():
    """ble handler.
    """
    SLEEP_TIME:Final[int] = 2
    TIME_OUT:Final[int] = 20

    def __init__(self, address:str, debug:bool=False) -> None:
        self.address:str = address
        self._logger:Logger = getLogger()

        if debug:
            l = getLogger("asyncio")
            l.setLevel(DEBUG)
            h = StreamHandler(sys.stdout)
            h.setLevel(DEBUG)
            l.addHandler(h)
            logger.addHandler(h)

    async def __call__(self) -> None:
        """when call, connect to target address device.
        """
        self._logger.info(f'connect to device({self.address}) start.')
        await self.connect_to_device()
    
    def _calculate_weight(self, data) -> float:
        """calculate weight value from bytearray.
        Args:
            data ([type]): [description]
        Returns:
            float: weight(format : xxx.xx)
        """
        return (data[1] + data[2] * 256) * 0.005

    def _weight_measurement_notification_handler(self, sender, data:bytearray):
        """Weight measurement notification handler."""
        weight:float = (data[1] + data[2] * 256) * 0.005
        self._logger.info(f'weight measurement notification handler: {data.hex()} / {weight} kg')

    def _disconnect_callback(self, client: BleakClient):
        """disconnect callback. only logging message."""
        self._logger.info(f'Client with address {client.address} got disconnected. try to reconnect.')

    async def connect_to_device(self):
        while True:
            self._logger.info("Waiting connect to device.")
            try:
                async with BleakClient(self.address, timeout=self.TIME_OUT, disconnected_callback=self._disconnect_callback) as client:

                    if await client.is_connected():
                        self._logger.info("Connect to device successfuly.")
                        await client.start_notify(
                            WEIGHT_MEASUREMENT, self._weight_measurement_notification_handler
                        )

                        while True:
                            if not await client.is_connected():
                                self._logger.debug("Device disconnected.")
                                break
                            await asyncio.sleep(self.SLEEP_TIME)
                            self._logger.debug("wait for message arraived from device.")
                    else:
                        self._logger.debug("Device disconnected.")
            except Exception as e:
                self._logger.error(f"Exception when connect: {e}")
            
            await asyncio.sleep(self.SLEEP_TIME)

スクリプトメイン箇所について

周囲のBluetooth情報をスキャンして対象の機器(TARGET_ADDRESS)が存在した時に先述のHandlerを登録します。
機器のアドレスの調べ方についても後述します。

#!/usr/bin/env python3

import sys
import asyncio
from logging import getLogger, StreamHandler, Formatter, DEBUG, INFO
from typing_extensions import Final
from blehandler import BleHandler
from typing import List
from bleak import discover

TARGET_ADDRESS:Final[str] = "<your-device-address>"

logger = getLogger()
logger.setLevel(INFO)
_streamHandler = StreamHandler(sys.stdout)
_streamHandler.setLevel(INFO)
_streamHandler.setFormatter(Formatter('[%(asctime)s][%(levelname)s] %(message)s'))
logger.addHandler(_streamHandler)

async def main():
    while True:
        logger.info('start device scan...')
        tasks = []
        devices:List = await discover()
        for device in devices:
            if device.address == TARGET_ADDRESS:
                handler = BleHandler(TARGET_ADDRESS, debug=True)
                tasks.append(asyncio.ensure_future(handler()))
                logger.info(f'found target device : {device}. discover process end.')
        
        if len(tasks) > 0:
            [await task for task in tasks]
        else:
            logger.info('target device not found. rescan after sleep 5s.')
        await asyncio.sleep(5)

asyncio.run(main())

体重計のアドレス、UUIDの調べ方

コード内に記載されている下記を調べます。

  • TARGET_ADDRESS
  • WEIGHT_MEASUREMENT

スマートフォンで適当なBLE受信可能なアプリをインストールします。
私は下記を使用しました。

https://apps.apple.com/jp/app/ble-scanner-4-0/id1221763603

アプリを起動すると下記のような画面になります。
一度体重計に乗って起動させる必要があるかもしれません。
今回使用した体重計はQN-Scaleという、いかにも体重計だよ!っていう名称だったので一発で分かりました。Connectボタンをタップすると接続して各サービス情報画面に遷移します。

接続後の画面になります。ここで体重計のアドレスを調べます。
注目すべきはAdvertisement Manufacture Data部分です。
後ろがMACアドレスのようになっています。
分かりにくい場合はメインスクリプトを修正して、 Bluetooth情報をスキャン時に機器のアドレスと名称を表示するようにしてみてください。

次に体重情報を通知してくれるサービス(UUID)を調べます。
アドレスを調べた画面を下にスクロールするとWeight Scaleという項目があるので、タップすると下記画面が表示されます。

Indicateと表示されているWEIGHT MEASUREMENT(2A9D)がUUIDになります。

上記のUUID(2A9D)は短縮されており、正式には00002a9d-0000-1000-8000-00805f9b34fbになります。2A9D以外は固定です。

この辺のUUIDは機器によって変わるかは調査が必要かもしれません。ある程度規格で定義されていたような気もしますが。

これで体重計のアドレス、体重測定結果を通知してくれるサービス(UUID)が判明しました。
これらを先述したスクリプトに埋め込みます。

BLE情報を取得してみる

実行するには下記コマンドを実行します。
sudoを付与してあげないと権限情報が不足して動作しませんのでご注意ください。

pi@raspberrypi:~ $ sudo python3 weight_measurement.py

動作すると下記のようなログが流れます。
接続時にエラーが発生する場合もありますが、一旦無視しても大丈夫です。
Connect to device successfulyと表示されたら体重計に乗ります。体重が確定するとデータが送信されてきます。11.85kgとなっているのは椅子に座りながら乗ったので…。

この体重計の仕様か不明なのですが、体重が確定されると一方的に接続を切断されます。
基本的には電力消費を抑えるために不要な接続は切るようになっているのでしょう。
しかしこのプログラムでは問答無用で再接続します。

[2021-09-20 23:04:58,447][INFO] start device scan...
[2021-09-20 23:05:03,609][INFO] found target device : xx:xx:xx:xx:xx:xx: QN-Scale. discover process end.
[2021-09-20 23:05:03,610][INFO] connect to device(xx:xx:xx:xx:xx:xx) start.
[2021-09-20 23:05:03,610][INFO] Waiting connect to device.
[2021-09-20 23:05:04,852][INFO] Client with address xx:xx:xx:xx:xx:xx got disconnected. try to reconnect.
[2021-09-20 23:05:13,394][INFO] Waiting connect to device.
[2021-09-20 23:05:15,040][INFO] Connect to device successfuly.
[2021-09-20 23:05:27,596][INFO] weight measurement notification handler: 024209e107010e003023 / 11.85 kg
[2021-09-20 23:05:35,798][INFO] Client with address xx:xx:xx:xx:xx:xx got disconnected. try to reconnect.
[2021-09-20 23:05:39,145][INFO] Waiting connect to device.

まとめ

今回はRapsberryPi上に体重計のBluetoothに接続して体重情報を取得するまでを行いました。
次回はデータ受信後に自身のサーバなり、Google Spreadsheetに送信するまでを行います。

現時点だとまだ下記課題が残っています。その辺も解消していこうと思います。

  • 常時接続しようとするので体重計の電池消耗が激しそう。

次回はこちら

-技術的な話
-, , , , ,