3密見える化センサのCO2値をプッシュ通知する

900ppmを超えるCO2濃度がDiscordに通知された

Senseair Sunriseとは 見出しにジャンプ

今の住宅は、隙間風を減らし計画的な換気を行うよう設計される。ただし、人はCO2を知覚できない(臭気、眠気や頭痛で間接的に感じることもあるが、個人差がある)ため、換気が十分かは実際に測らなければ分からない。
一般消費者におけるCO2濃度計は、精度の高いNDIR方式のセンサを買ってきて、マイコンに繋いで自作するのが一般的で、やや敷居が高かった。

ところが2020年以降、SARS-Cov-2への対策の一つとして換気が推奨され、その指標として人の呼気に含まれるCO2を計測する需要が高まった。
そんな中で登場したのが旭化成の「3密見える化センサ」で、そのあまりカッコよくないネーミングや筐体からは想像できない世界最高性能のCO2センサ「Senseair Sunrise」を搭載している。
Sunrise | Air quality & Gas sensing technology from Senseair

Senseair Sunriseは、海外の科学者・公衆衛生エンスージアストの中でデファクトの「Aranet4」も採用する、高性能なセンサだ。
NDIRの精度に加え、これらの2機種は他の追随を許さない年単位のバッテリーライフを持つ。これならば自作するよりもよい結果が得られると思い、購入に至った。
旭化成 3密見える化センサ – コロナ対策換気視覚化CO2モニター情報

換気View Proの無償化 見出しにジャンプ

ポストコロナ社会では、日常化した感染イベントから身を守る手段の一つとして、CO2計測の重要性が再認識されている。
そんな中、企業向けの「換気View Pro」アプリが無償化され、閾値を超えた際に通知できるようになった。

換気View Proの通知

「換気View Pro」をApp Storeで

「換気View」では断続的にしか確認できなかったため重宝したが、iPhoneがBLEビーコンを購読する際のバッテリーへの影響がそれなりに大きいことが悩みの種だった。

nRF ConnectでCO2濃度を確認 見出しにジャンプ

iOSのnRF Connect AppでBLEビーコンを覗くと...

nRF Connect Appで3密見える化センサのBLEビーコンを取得 換気View Proの表示と比較

平文で、CO2濃度0x0236(566ppm)、温度0x09FD(25.57°C)、湿度0x17B4(60.68%)を示している。
このため、これらのBLEビーコンを継続的にdumpすれば、世界最高性能のCO2センサの値を容易に記録できると分かった。

こちらもBLE?平文かはdumpしてみないと分からない。

bluetoothctl 見出しにジャンプ

最新のQSVが動くミニPCにたまたまBluetoothが付いていただけだが、据え置きのLinuxでBLEビーコンを購読するにあたり白羽の矢が立った。
筆者はN5105モデルを使っているが、廃盤らしい。今買うならN100一択だろう。

Bluezをインストール。後述の通り、hcitoolhcidumpを含むbluez-deprecatedは使用せず、bluetoothctlbtmonを導入。

dnf install usbutils bluez

systemctl enable bluetooth
systemctl start bluetooth

Failed to access management interface 見出しにジャンプ

systemctl status bluetooth
# bluetoothd[1031]: src/adapter.c:adapter_init() Failed to access management interface

bluetoothctl scan on
# Unable to open mgmt_socket

PVEのコンテナからのBluetoothデバイスの動作に失敗した。下記手順でもコンテナからは使えなかった。
Bluetooth device on container | Proxmox Support Forum
Bluetooth dongle not working with LXC | Proxmox Support Forum
PVE 7.4 + Home Assistant: Intel AX200 Bluetooth issues | Proxmox Support Forum
VMオプション設定コマンドqm setが使用できるVMでセットアップしたところ、すんなり動作した。
Error Configuration file 'nodes/proxmox01/qemu-server/100.conf' does not exist | Proxmox Support Forum

  1. PVE側でBluetoothドライバを無効化
+ blacklist btusb

再起動が必要。

  1. Bluetoothデバイスの紐づけ
    ネットワーク設定/ワイヤレス - ArchWiki
lsusb
# Bus 001 Device 003: ID 8087:0a2a Intel Corp. Bluetooth wireless interface

VM -> Hardware -> Add -> USB Deviceで、上記Vender/Device IDを追加。

qm set <VMID> --usb0 host=8087:0a2a

Intel AX211 WiFi 6E PCIe Passthrough no Bluetooth | Proxmox Support Forum
HomeAssistantでSwitchBotスマートロックの鍵状態を表示する(Bluetooth接続編)|nano

BLEビーコンの取得 見出しにジャンプ

bluez-deprecatedの非推奨コマンドhcitool lescan --acceptlisthcidump --manufacturerが動作しなかったため、素直にbluetoothctlを使用した。
新しいBluezの機能をみていくと、どうやらbluetoothctl: hcitool, btmon: hcidumpの関係らしい。

  1. スキャンの実行
bluetoothctl
menu scan
    clear
    transport le
    duplicate-data on
    pattern 00:53:FE:DC:BA:98
    back
scan on
[bluetooth]# SetDiscoveryFilter success
[bluetooth]# Discovery started
[bluetooth]# [CHG] Controller 00:53:AB:CD:EF:01 Discovering: yes
[bluetooth]# [NEW] Device 00:53:12:34:56:78 00-53-12-34-56-78
[bluetooth]# [NEW] Device 00:53:FE:DC:BA:98 AS1s
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 RSSI: 0xffffffca (-54)
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 ManufacturerData.Key: 0x0965 (2405)
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 ManufacturerData.Value:
[bluetooth]#   a1 ** ** 00 02 9d 09 d0 16 8b 00 3c 01 55 00 64  ..e........<.U.d
[bluetooth]#   a1                                               .
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 RSSI: 0xffffffca (-54)
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 ManufacturerData.Key: 0x0965 (2405)
[bluetooth]# [CHG] Device 00:53:FE:DC:BA:98 ManufacturerData.Value:
[bluetooth]#   a1 ** ** 00 02 9d 09 d0 16 8b 00 3c 01 55 00 64  ..e........<.U.d
[bluetooth]#   a1                            
sudo btmon
> HCI Event: LE Meta Event (0x3e) plen 43                                                                                   #6 [hci0] 1.637790
      LE Advertising Report (0x02)
        Num reports: 1
        Event type: Non connectable undirected - ADV_NONCONN_IND (0x03)
        Address type: Random (0x01)
        Address: 00:53:FE:DC:BA:98 (Static)
        Data length: 31
        Company: not assigned (2405)
          Data[17]: a1****00030b0a0c1718003c01550064a1
        Name (short): AS1s
        16-bit Service UUIDs (complete): 1 entry
          Unknown (0xfd29)
        RSSI: -58 dBm (0xc6)
  • スキャン中btmonでは、LE Advertising ReportやLE Meta Eventのデータ部まで確認できる。
  1. スキャンの停止
scan off

Discordへ通知 見出しにジャンプ

ユーザ操作が前提のbluetoothctlを自動化するために、expectで入力をエミュレートする。下記をパ...参考にさせて頂いた。
bluetooth - How to use bluetoothctl like hcitool lescan to report repeated proximity beacons - Stack Overflow

  • max_co2, sensor_id, webhook_url, sensor_addressを設定する
  • sudo crontab -u root -eにでも書いておくとよい
@reboot /bin/bash /home/user/beacon-scan.sh
  • データ部もbluetoothctlで取得できたため、btmonは使っていない
#!/bin/bash
# https://stackoverflow.com/a/52012231

if [ "$(id -u)" != "0" ]; then
    echo "ERROR: must run as root"
    exit 1
fi

max_co2=1000 # ppm (decimal)
sensor_id="0F FF" # Sensor 4095 (hex)
webhook_url="https://discord.com/api/webhooks/XXXXXXXX"

(cat <<'END' | /usr/bin/expect

    set sensor_address "00:53:FE:DC:BA:98"

    set prompt "#"
    set timeout -1

    spawn bluetoothctl

    expect -re $prompt
    send "scan off\r"

    expect -re $prompt
    send "remove *\r"

    expect -re $prompt
    send "menu scan\r"

    expect -re $prompt
    send "clear\r"

    expect -re $prompt
    send "transport le\r"

    expect -re $prompt
    send "duplicate-data on\r"

    expect -re $prompt
    send "pattern $sensor_address\r"

    expect -re $prompt
    send "back\r"

    expect -re $prompt
    send "scan on\r"

    trap {
        expect -re $prompt
        send "scan off\r"

        expect -re $prompt
        send "remove *\r"

        expect -re $prompt
        send "quit\r"
    } SIGINT

    expect eof

END
) | grep -oP --line-buffered "a1 $sensor_id 00 \w+\s\w+" | sed --unbuffered --expression "s/\(a1 $sensor_id 00 \|\s\)//g" | while read -r value; do
    # rate limit
    if [ "$value" != "$before_value" ]; then
        before_value=$value

        # hex to decimal
        decimal_value=$((16#$value))

        if [ "$decimal_value" -gt $max_co2 ]; then
            echo "$decimal_value > $max_co2"
            curl -H "Content-Type: application/json" -X POST -d "{\"content\":\"Current CO2 concentration is $decimal_value ppm.\"}" $webhook_url
        else
            echo "$decimal_value <= $max_co2"
        fi
    fi
done

Grafanaで表示 見出しにジャンプ