Xiaomi Mijia (LYWSD03MMC)にカスタムファームウェアを入れてnode-redで受信する

## aliexpressで安く買える温湿度計はカスタムファームを入れることでBLEビーコンになる
![41X05LkF3mL._SL1280_.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2231449/51274585-7fdd-9a00-759f-1140036f240c.jpeg)

+ カスタムファームだと.01単位で温度湿度測定できて良い
+ 電池が１年くらい持つとのこと
+ OTA対応製品がまだ出回っている（2024.07現在）

OTA対応のファーム(2024.07購入：2023.08生産でOTA対応）であればスマホ上でファームウウェア入れ替えと設定が可能。

https://github.com/pvvx/ATC_MiThermometer?tab=readme-ov-file

## ファームウェア入れ替えと設定手順
上記に書いてある通り。flasherでMijiaを対象選択（LYWSD03...）してファームがOTA対象であることを確認したら、その時点での安定板（2024.08時点では4.7)を選んでフラッシュする。なお、他のホストとbluetooth接続していると（液晶にロゴが出ます）選択対象として出てこない。
フラッシュ後にデフォルト通りであればadvertising formatはcustom formatになっているが、念の為flasherでもう一度接続（ATC...)して設定する。

https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html

<details open><summary>format</summary>

uint8_t     size;   // = 18
uint8_t     uid;    // = 0x16, 16-bit UUID
uint16_t    UUID;   // = 0x181A, GATT Service 0x181A Environmental Sensing
uint8_t     MAC[6]; // [0] - lo, .. [6] - hi digits
int16_t     temperature;    // x 0.01 degree
uint16_t    humidity;       // x 0.01 %
uint16_t    battery_mv;     // mV
uint8_t     battery_level;  // 0..100 %
uint8_t     counter;        // measurement count
uint8_t     flags;  // GPIO_TRG pin (marking "reset" on circuit board) flags: 
                    // bit0: Reed Switch, input
                    // bit1: GPIO_TRG pin output value (pull Up/Down)
                    // bit2: Output GPIO_TRG pin is controlled according to the set parameters
                    // bit3: Temperature trigger event
                    // bit4: Humidity trigger event
</details>

## node-redでBLEを受信する

何を使っても良いかと思われるが、ファームウェアのフォーラム

https://github.com/atc1441/ATC_MiThermometer/issues/54

にあったnode-red-contrib-ble-scanを利用。

https://flows.nodered.org/node/node-red-contrib-ble-scan

nobleは自動では入らなかったのでコマンドプロンプトから。
~~~ bash:
sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
npm install noble
~~~

## BLE受信とparse

受信ができるとあちこちのビーコンを拾うので、フォーラムにあったfunctionノードで対象のものだけをフィルター。
ファームウェアのフォーマットに従ってnode-red-contrib-buffer-parser（ラズパイのnode-redでは標準）でパースする。

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2231449/04b8a097-bf43-3df9-b091-99bcc416a65a.png)

~~~ json
[
    {
        "id": "2638cecad749fa1e",
        "type": "tab",
        "label": "sensors",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "282e1cbf3dc18174",
        "type": "debug",
        "z": "2638cecad749fa1e",
        "name": "debug 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 560,
        "y": 320,
        "wires": []
    },
    {
        "id": "c08cddf3cd1437ba",
        "type": "BLE Scan",
        "z": "2638cecad749fa1e",
        "name": "",
        "x": 80,
        "y": 40,
        "wires": [
            [
                "a6375ab2fe258940"
            ]
        ]
    },
    {
        "id": "a6375ab2fe258940",
        "type": "function",
        "z": "2638cecad749fa1e",
        "name": "filter xiaomi",
        "func": "var payload = msg.payload;\nif (payload.address.startsWith(\"a4:c1:38\") &&\n    payload.advertisement.localName.startsWith(\"ATC_\")) {\n    var buffer = payload.advertisement.serviceData[0].data;\n    var mac = payload.address\n    msg.topic = mac;\n\n    msg.payload = {\n       \"buffer\": buffer\n    };\n    return msg;\n}\nreturn null;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 250,
        "y": 40,
        "wires": [
            [
                "1d7bbc567f124044"
            ]
        ]
    },
    {
        "id": "1d7bbc567f124044",
        "type": "buffer-parser",
        "z": "2638cecad749fa1e",
        "name": "",
        "data": "payload.buffer",
        "dataType": "msg",
        "specification": "spec",
        "specificationType": "ui",
        "items": [
            {
                "type": "int16le",
                "name": "temp",
                "offset": 6,
                "length": 1,
                "offsetbit": 0,
                "scale": "0.01",
                "mask": ""
            },
            {
                "type": "uint16le",
                "name": "humidity",
                "offset": 8,
                "length": 1,
                "offsetbit": 0,
                "scale": ".01",
                "mask": ""
            },
            {
                "type": "uint16le",
                "name": "mV",
                "offset": 10,
                "length": 1,
                "offsetbit": 0,
                "scale": "1",
                "mask": ""
            },
            {
                "type": "uint8",
                "name": "battery",
                "offset": 12,
                "length": 1,
                "offsetbit": 0,
                "scale": "1",
                "mask": ""
            }
        ],
        "swap1": "",
        "swap2": "",
        "swap3": "",
        "swap1Type": "swap",
        "swap2Type": "swap",
        "swap3Type": "swap",
        "msgProperty": "payload",
        "msgPropertyType": "str",
        "resultType": "value",
        "resultTypeType": "return",
        "multipleResult": true,
        "fanOutMultipleResult": true,
        "setTopic": true,
        "outputs": 4,
        "x": 430,
        "y": 40,
        "wires": [
            [
                "282e1cbf3dc18174",
                "339d05cfc0501b4b"
            ],
            [
                "282e1cbf3dc18174",
                "acd3a6735d4046ab"
            ],
            [
                "282e1cbf3dc18174"
            ],
            [
                "282e1cbf3dc18174"
            ]
        ]
    },
    {
        "id": "6a6ad82253b970e2",
        "type": "ui_chart",
        "z": "2638cecad749fa1e",
        "name": "temp chart",
        "group": "89dfaf28c135d6ea",
        "order": 1,
        "width": 0,
        "height": 0,
        "label": "",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": 1,
        "removeOlderPoints": "",
        "removeOlderUnit": "86400",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 110,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "ed92b03f0ac04d53",
        "type": "ui_text",
        "z": "2638cecad749fa1e",
        "group": "89dfaf28c135d6ea",
        "order": 2,
        "width": "2",
        "height": "1",
        "name": "temp",
        "label": "<font>   <i class=\"fa fa-thermometer-empty \"></i> </font>",
        "format": "{{msg.payload}}°C",
        "layout": "row-center",
        "className": "",
        "style": false,
        "font": "",
        "fontSize": 16,
        "color": "#000000",
        "x": 90,
        "y": 180,
        "wires": []
    },
    {
        "id": "7e964d382647eec1",
        "type": "ui_text",
        "z": "2638cecad749fa1e",
        "group": "89dfaf28c135d6ea",
        "order": 2,
        "width": "1",
        "height": "1",
        "name": "humidity",
        "label": "",
        "format": "{{msg.payload}}%",
        "layout": "row-center",
        "className": "",
        "style": false,
        "font": "",
        "fontSize": 16,
        "color": "#000000",
        "x": 100,
        "y": 240,
        "wires": []
    },
    {
        "id": "339d05cfc0501b4b",
        "type": "function",
        "z": "2638cecad749fa1e",
        "name": "two decimal",
        "func": "msg.payload = (msg.payload.toFixed(2));\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 610,
        "y": 20,
        "wires": [
            [
                "6a6ad82253b970e2",
                "ed92b03f0ac04d53"
            ]
        ]
    },
    {
        "id": "acd3a6735d4046ab",
        "type": "function",
        "z": "2638cecad749fa1e",
        "name": "two decimal",
        "func": "msg.payload = (msg.payload.toFixed(2));\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 610,
        "y": 60,
        "wires": [
            [
                "7e964d382647eec1"
            ]
        ]
    },
    {
        "id": "89dfaf28c135d6ea",
        "type": "ui_group",
        "name": "graph",
        "tab": "859daaa640503ab3",
        "order": 6,
        "disp": false,
        "width": 4,
        "collapse": false,
        "className": ""
    },
    {
        "id": "859daaa640503ab3",
        "type": "ui_tab",
        "name": "home",
        "icon": "dashboard",
        "order": 1,
        "disabled": false,
        "hidden": false
    }
]
~~~