Cisco Spacesに頼らずCatalyst 9800でBLEセンサーを読む: Sensor ConnectとMQTT/protobuf decode入門
はじめに
Cisco Catalyst 9800 Wireless Controller と Cisco AP の BLE radio を使い、Cisco Spaces の cloud firehose に依存せずに BLE センサーの値を取得することを試みたので、記事として残します。
Cisco のドキュメントでは、このソリューション全体は Cisco Sensor Connect for IoT Services、9800 上に deploy する IOx application / UI / API endpoint は IoT Orchestrator と説明されています。そのため、この記事では運用上の表示名を Cisco Sensor Connect (IoT Orchestrator) として扱います。
拙作 home-metrics の内部名は実装経緯と endpoint の役割が分かるように cisco_iot_orchestrator / CISCO_IOT_ORCH_* のまま維持しています。
Cisco Sensor Connect for IoT Services とは何か
Cisco Sensor Connect for IoT Services は、Cisco AP の BLE radio を BLE gateway として使い、BLE device の onboarding、control、telemetry 取得を行うための仕組みです。key component は IoT Orchestrator で、これは Cisco Catalyst 9800 Wireless Controller 上に deploy する Cisco IOx application として動作します。
ざっくり言うと、以下のような役割を持ちます。
| 役割 | 内容 |
|---|---|
| BLE onboarding | SCIM API で BLE device を IoT Orchestrator に登録する |
| BLE control | connect / disconnect / GATT read / write / subscribe などを control API で扱う |
| BLE telemetry | AP が scan した advertisement や GATT notification を MQTT で data app に渡す |
| policy / inventory | IoT Orchestrator UI で AP inventory、BLE inventory、scan policy、allow-list などを見る |
重要なのは、Sensor Connect が「センサー値をすべて標準 JSON に変換してくれるサービス」ではないことです。少なくとも今回の Minew S1 のような advertisement 型 sensor では、MQTT payload の中に BLE advertisement の raw bytes が protobuf binary として届き、その中身をアプリケーション側で decode する必要がありました。
公式資料では、cloud-based IoT solution が使いにくい環境、たとえば air-gapped requirement、internet connectivity が不安定な環境、data sovereignty の要件がある環境での利用シナリオが説明されています。つまり、Cisco Spaces の cloud 経由 firehose ではなく、9800 と AP の既存 wireless infrastructure を使って on-prem に近い形で BLE data path を作れるのが特徴です。
Cisco Spaces との違い
Cisco Spaces の IoT Service / Firehose API と、Cisco Sensor Connect (IoT Orchestrator) は混同しやすいです。どちらも Cisco AP を BLE gateway として活用しますが、data path と運用責任がかなり違います。
| 観点 | Cisco Spaces / IoT Service | Cisco Sensor Connect (IoT Orchestrator) |
|---|---|---|
| 主な data path | AP/WLC から Cisco Spaces cloud / Connector へ telemetry を送る | AP から WLC 上の IoT Orchestrator へ送り、MQTT で外部 app が受ける |
| 依存先 | Cisco Spaces cloud、Connector、Firehose API | Catalyst 9800 上の IoT Orchestrator、MQTT、SCIM/control API |
| 受けるデータ | Firehose API の event / telemetry JSON | protobuf binary に入った BLE advertisement / event |
| device onboarding | Cisco Spaces 側の workflow に寄る | SCIM API で app 側から登録 |
| data app 管理 | Cisco Spaces 側の app / firehose subscription | IoT Orchestrator の data app / topic / subscribe |
| 向いている用途 | cloud 連携、Cisco Spaces の location/IoT service と合わせた運用 | cloud に依存しにくい構成、独自 app で BLE raw data を扱いたい構成 |
Cisco Spaces より単純になるというより、cloud 側のブラックボックスを減らす代わりに、アプリケーション側で API call と decode を持つ と考えるのがよいです。
また、Cisco Spaces では IoT Marketplace のセンサーのみがオンボード可能ですが、Cisco Sensor Connect (IoT Orchestrator)ではそのような制限がないというのも1つの特長だと思います。
Catalyst 9800 Wireless Controller へ設定が必要なこと
Catalyst 9800 側で行うことは、大きく分けると次の 2 段階になります。
- Day-0: IoT Orchestrator application を Catalyst 9800 上に deploy して起動します。
- Day-1: IoT Orchestrator から WLC/AP を管理し、AP の BLE radio と外部 application をつなぎます。
Cisco Software Download の Spaces Orchestrator Software セクションから IoT Orchestrator の image をダウンロードし、WLC Web UI の Configuration > Services > IoT Services から deploy します。
内部的には IOx container を Catalyst 9800 上で動かす構成ですが、通常の app-hosting CLI や IOx Web UI で手作業するものではありません。Cisco の Quick Start Guide でも、IoT Services UI から Day-0 / Day-1 operation を行う前提になっています。
Day-0 で決める一番重要な値は、IoT Orchestrator container の IP address と、その default gateway になる WLC 側 VirtualPortGroup の IP address です。今回の検証では、NAT ではなく上位スイッチで routing する構成にしました。
Catalyst 9800 / WLC側 VirtualPortGroup: 192.168.67.5/30
IoT Orchestrator container: 192.168.67.6/30
IoT Orchestrator の IP address は、Catalyst 9800 の既存 interface や他の network と重複しないものを選ぶ必要があります。/30 にすると、WLC 側 gateway と container 側 IP の関係が明確で、上位 network へ route を出す範囲も小さくできます。
この /30 は、次の 3 方向から到達できる必要があります。
| 通信元 | 通信先 | 必要な理由 |
|---|---|---|
| 管理端末 / application server | IoT Orchestrator :8081
|
UI、SCIM API、control API を呼び出す |
| AP | IoT Orchestrator :43626, :50221
|
AP から BLE advertisement / inventory / control path を IoT Orchestrator に送る |
| data receiver app | IoT Orchestrator :41883
|
MQTT broker に接続して topic を subscribe する |
今回の例では、上位 switch で 192.168.67.4/30 を routing し、管理端末、AP subnet、拙作application から 192.168.67.6 に到達できるようにしました。WLC と AP が同じ管理 network に見えていても、AP から IoT Orchestrator container IP へ到達できなければ BLE advertisement は MQTT まで流れません。
NAT でも構成可能とされていますが、今回は routing 構成で検証しました。NAT 構成では AP から IoT Orchestrator へ向かう port と、外部 app から REST/MQTT に向かう port の見え方が複雑になります。最初の検証では、IoT Orchestrator の IP address をそのまま routing した方が切り分けしやすいです。
Catalyst 9800 WLC で deploy が完了すると、以下のように表示されます。
Day-0 で確認することは以下です。
| 項目 | 確認内容 |
|---|---|
| IoT Services deployment | IoT Orchestrator image を upload し、application が Running になっているか |
| IP address | VirtualPortGroup 側 IP と IoT Orchestrator IP が重複していないか |
| UI access | 管理端末から https://<orchestrator-ip>:8081 にアクセスできるか |
| initial login | IoT Orchestrator UI に login し、初期 wizard を完了できるか |
| route | 上位 switch から Orchestrator の /30 に到達できるか |
Day-1 で確認することは以下です。
| 項目 | 確認内容 |
|---|---|
| WLC registration | IoT Orchestrator UI から Catalyst 9800 WLC を登録・接続できているか |
| AP Inventory | 対象 AP が Connected として見えるか |
| AP version / platform | 対象 AP が Sensor Connect 対応 platform / software version か |
| IoT Radio Mode | 対象 AP が Scan になっているか |
| BLE allow-list / scan policy | 対象 MAC prefix/full MAC が filter で落ちていないか |
| BLE live log | 対象 BLE MAC の advertisement が見えるか |
| Data App Topics |
registerDataApp / registerTopic 後に topic が見えるか |
| MQTT | data app credential で :41883 に接続できるか |
今回の検証では、AP Inventory で AP が connected、IoT Radio Mode が Scan、BLE live log に Minew S1 の MAC 00:FA:B6:07:DE:4B が見えるところまで確認してから、SCIM onboarding と data app / topic 登録に進みました。
切り分けの順番としては、以下が分かりやすかったです。
- 管理端末から IoT Orchestrator UI/API
:8081に到達できることを確認します。 - AP から IoT Orchestrator へ到達でき、AP Inventory が connected になることを確認します。
- BLE live log で対象 sensor の advertisement が見えることを確認します。
- SCIM onboarding で device ID を取得します。
-
registerDataApp、registerTopic、subscribeを実行します。 - data app credential で MQTT
:41883に接続し、payload が届くことを確認します。
ここで 1 から 3 が通っていなければ WLC/AP/route/policy 側の問題、4 以降で詰まるなら application ID、API key、topic、subscribe の問題として切り分けやすいです。
参考:
- Cisco Sensor Connect for IoT Services Quick Start Guide
https://www.cisco.com/c/en/us/td/docs/wireless/spaces/iot-orchestrator/qsg/sensor-connect-iot-qsg.html - Cisco Sensor Connect for IoT Services Partner Developer Guide
https://developer.cisco.com/docs/spaces-connect-for-iot-services/ - Cisco Spaces IoT Service Configuration Guide
https://www.cisco.com/c/en/us/td/docs/wireless/spaces/iot-services-wireless/b_iot_services/m_overview.html
先に今回のポイント
今回の構成で重要だった点は、細かい decode の前に次の 3 つでした。
- app ID は
onboard、control、dataを別々に作る必要がありました。 - API call は SCIM onboarding、control API による data app / topic 登録、data app による MQTT subscribe の順で揃える必要がありました。
- BLE には scan と connect がありますが、Minew S1 は scan で見える advertisement だけで temperature / humidity / battery を取得できました。
最初に詰まったのは 1 つ目で、IoT Orchestrator の UI では同じ app ID で 3 つの役割を持てるように見えますが、実際にはそれぞれで別の app ID を作成しないと API Key が作成されません。
2 つ目は、SCIM onboarding が「device を IoT Orchestrator の管理対象に入れる」処理であり、「その device の telemetry を data app に流す」処理ではないことです。onboard 後に、control API で data app と topic を登録し、その topic に BLE device を subscribe させる必要があります。
3 つ目は BLE の動作モードです。BLE sensor data の取得方法には大きく scan と connect があります。scan は advertisement を受信するだけで、connect は GATT read / notify のために AP から BLE device へ接続します。今回の Minew S1 は scan-only の扱いでよく、GATT connect は不要でした。
参考:
- Cisco Sensor Connect for IoT Services Release Notes 1.2
https://www.cisco.com/c/en/us/td/docs/wireless/spaces/iot-orchestrator/release-notes/1-2/cisco-sensor-connect-iot-servcies-release-notes-1-2.html - Cisco Sensor Connect for IoT Services Programmability Guide - Overview
https://www.cisco.com/c/en/us/td/docs/wireless/spaces/iot-orchestrator/programmability-guide/b-spaces-connect-iot-pgm-guide/m-overview-of-cisco-spaces-connect-for-iot-services.html - Cisco Sensor Connect for IoT Services Quick Start Guide
https://www.cisco.com/c/en/us/td/docs/wireless/spaces/iot-orchestrator/qsg/sensor-connect-iot-qsg.html
今回作ったもの
Cisco Sensor Connect for IoT Services (IoT Orchestrator) からデータ取得を行い、すでに作成してあった拙作 home-metrics の collector に追加しました。
今回は主に Minew S1 のセンサー実機データをもとに、必要になった項目をまとめていきます。
- collector 本体
https://github.com/hshimomura/home-metrics/blob/main/cmd/hm-cisco-iot-orchestrator-collector/main.go - collector の decode / aggregation test
https://github.com/hshimomura/home-metrics/blob/main/cmd/hm-cisco-iot-orchestrator-collector/main_test.go - Docker Compose service
https://github.com/hshimomura/home-metrics/blob/main/compose.yaml - env example
https://github.com/hshimomura/home-metrics/blob/main/examples/home-metrics.compose.env.example - DB schema / migrations
https://github.com/hshimomura/home-metrics/tree/main/db
構成は次の通りです。
BLE sensor
-> Cisco AP BLE radio
-> Catalyst 9800 上の IoT Orchestrator
-> MQTT broker :41883
-> hm-cisco-iot-orchestrator-collector
-> PostgreSQL / TimescaleDB sensor_minute
-> home-metrics Web UI
IoT Orchestrator の REST API は :8081、MQTT は :41883 を使います。AP から IoT Orchestrator へは 43626/tcp と 50221/tcp が必要になります。
scan と connect の違い
BLE の値取得には、主に次の 2 種類があります。
| 種類 | 使うもの | 特徴 | 今回 |
|---|---|---|---|
| scan | BLE advertisement | device に接続せず、AP が聞こえた advertising packet を拾う | Minew S1 はこれ |
| connect | GATT read / notify | AP が BLE device に接続して characteristic を読む | 今回は使わない |
Minew S1 のように advertisement に温度、湿度、battery が入っている sensor は scan だけでよいです。connect を使わないため、AP の BLE active connection 数、GATT service discovery、notify subscription の安定性に依存しません。
IoT Orchestrator の UI では AP の IoT Radio Mode が Scan になっていることを確認しました。BLE Inventory に device が出ない場合でも、Data App / Topic / Subscribe が未設定だと MQTT へ出てこないため、scan policy と data path は分けて切り分けた方がよいです。
Minew S1 実機では、IoT Orchestrator の Orchestrator debug live log に次のような response が出ました。
2026-06-02T15:14:16.820 ... [00:FA:B6:07:DE:4B] Onboarded:
AP = 84:5a:3e:d6:b7:80
BLE = 00:FA:B6:07:DE:4B
RSSI = -68
TIME = 2026-06-02 15:14:16.82
advData = 0201061B166AFE03050610FF2098041103FFFF041600FFFF03133A1A02123B
2026-06-02T15:14:16.821 ... [00:FA:B6:07:DE:4B]
Sending the advertisement of deviceId: d96231e7-13b4-4ccd-bafa-ec5c60b95c88
to topics: [ioslab/home-metrics/ble/advertisements/v1]
この時点で、AP は Minew S1 の advertisement を scan できており、IoT Orchestrator は deviceId と topic の対応を使って MQTT 側へ送ろうとしていることが分かります。
アプリケーション ID は分けた
最終的には、用途ごとに app ID と API key を分けました。
CISCO_IOT_ORCH_ONBOARD_APP_ID=onboard
CISCO_IOT_ORCH_ONBOARD_API_KEY=...
CISCO_IOT_ORCH_CONTROL_APP_ID=control
CISCO_IOT_ORCH_CONTROL_API_KEY=...
CISCO_IOT_ORCH_DATA_APP_ID=data
CISCO_IOT_ORCH_DATA_API_KEY=...
CISCO_IOT_ORCH_TOPIC=ioslab/home-metrics/ble/advertisements/v1
最初は同じ app ID を使い回せると思っていましたが、Data App Topics に期待した topic が出ませんでした。少なくとも今回の環境では、onboarding / control / data receiver を分ける必要がある挙動でした。
役割は次のように整理しました。
| app ID | 用途 | 使う API / protocol |
|---|---|---|
onboard |
BLE device を IoT Orchestrator に登録する | SCIM |
control |
data app / topic / subscribe を登録する | Control API |
data |
MQTT broker に接続して telemetry を受ける | MQTT |
API key 認証では IoTOrchestrator自体は証明書認証もサポートしていますが今回は API Keyをつかうので、 header は X-API-Key を使います。
SCIM onboarding の挙動
BLE device の登録は SCIM API で行います。
参考:
- Onboarding BLE Devices Using SCIM
https://developer.cisco.com/docs/spaces-connect-for-iot-services/onboarding-ble-devices-using-scim/ - Onboard a BLE device
https://developer.cisco.com/docs/spaces-connect-for-iot-services/onboard-a-ble-device/
SCIM の endpoint は次です。
POST /scim/v2/Devices
BLE device extension では MAC address と random address かどうかを渡します。今回対象にしたセンサーは public MAC として扱うため isRandom: false にしました。
概念的にはこのような payload になります。
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Device",
"urn:ietf:params:scim:schemas:extension:ble:2.0:Device",
"urn:ietf:params:scim:schemas:extension:endpointapps:2.0:Device"
],
"deviceDisplayName": "Living",
"adminState": true,
"urn:ietf:params:scim:schemas:extension:ble:2.0:Device": {
"deviceMacAddress": "00:FA:B6:07:DE:4B",
"isRandom": false,
"mobility": true,
"pairingMethods": [
"urn:ietf:params:scim:schemas:extension:pairingNull:2.0:Device"
]
},
"urn:ietf:params:scim:schemas:extension:endpointapps:2.0:Device": {
"onboardingApp": "onboard",
"controlApp": "control"
}
}
SCIM の response で IoT Orchestrator 側の device ID が返ります。この ID が後続の topic 登録や subscribe で必要になります。
SCIM onboarding が成功したときの response は、SCIM device resource として返ります。実際には version や設定によって field の有無が変わりますが、後続処理で最も重要なのは id です。
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Device",
"urn:ietf:params:scim:schemas:extension:ble:2.0:Device",
"urn:ietf:params:scim:schemas:extension:endpointapps:2.0:Device"
],
"id": "5ae0c228-24c9-433c-9001-5a23f063ffd0",
"deviceDisplayName": "Living",
"adminState": true,
"meta": {
"resourceType": "Device",
"location": "/scim/v2/Devices/5ae0c228-24c9-433c-9001-5a23f063ffd0"
},
"urn:ietf:params:scim:schemas:extension:ble:2.0:Device": {
"deviceMacAddress": "00:FA:B6:07:DE:4B",
"isRandom": false
},
"urn:ietf:params:scim:schemas:extension:endpointapps:2.0:Device": {
"onboardingApp": "onboard",
"controlApp": "control"
}
}
この例では 5ae0c228-24c9-433c-9001-5a23f063ffd0 が IoT Orchestrator 側の device ID です。以降の registerTopic や subscribe では、BLE MAC ではなくこの id を渡す場面があります。
重要なのは、SCIM onboarding だけでは MQTT にデータは流れない ことです。BLE Inventory に device が見えても、Data App / Topic / Subscribe が揃っていないと collector 側には何も来ません。
SCIM onboarding が成功すると、IoT Orchestrator にも BLEデバイスの一覧が表示されます

Data App と topic の登録
データを受けるには、少なくとも次の順番で揃える必要がありました。
- BLE device を SCIM で onboard します。
- Data receiver application を登録します。
- advertisement topic を登録します。
- BLE device を topic に subscribe します。
- Data app ID / API key で MQTT broker に接続して subscribe します。
参考:
- Register a DataApp
https://developer.cisco.com/docs/spaces-connect-for-iot-services/register-a-dataapp/ - Register a Topic
https://developer.cisco.com/docs/spaces-connect-for-iot-services/register-a-topic/ - Subscribe a BLE Device
https://developer.cisco.com/docs/spaces-connect-for-iot-services/subscribe-a-ble-device/ - Data Telemetry from BLE Devices
https://developer.cisco.com/docs/spaces-connect-for-iot-services/data-telemetry-from-ble-devices/
Data app 登録は次の endpoint です。
POST /control/registration/registerDataApp
Cisco の guide にある形に合わせて、home-metrics では次の payload を送ります。
{
"dataApps": [
{
"dataAppID": "data"
}
],
"topic": "ioslab/home-metrics/ble/advertisements/v1",
"dataFormat": "default",
"controlApp": "control"
}
成功時の response は、DevNet の schema では status と topic が返る形になっています。
{
"status": "SUCCESS",
"topic": "ioslab/home-metrics/ble/advertisements/v1"
}
ここで status が SUCCESS であることと、topic が意図した topic 名で返っていることを確認します。HTTP status code だけでなく response body を見るのが重要でした。
Topic 登録は次の endpoint で行います。
POST /control/registration/registerTopic
DevNet の Register a Topic では、SCIM onboarding response で得た device ID、control app、topic、data format、BLE event type を渡します。advertisement を受ける場合は ble.type を advertisements にします。
payload の例は次のようになります。
{
"technology": "ble",
"id": "5ae0c228-24c9-433c-9001-5a23f063ffd0",
"controlApp": "control",
"ids": [
"5ae0c228-24c9-433c-9001-5a23f063ffd0"
],
"topic": "ioslab/home-metrics/ble/advertisements/v1",
"dataFormat": "default",
"ble": {
"type": "advertisements",
"serviceID": "fe6a",
"characteristicID": "fe6a"
}
}
serviceID / characteristicID は GATT の read / notify では実際の service / characteristic UUID を指します。今回の Minew S1 は scan-only の advertisement を使うため、アプリケーション側で見るべき UUID は BLE advertisement 内の Service Data UUID 0xfe6a です。環境によって advertisements でも schema 上これらの field を要求されるため、API の実挙動と DevNet schema に合わせて指定します。
成功時の response は registerDataApp と同じく、topic 登録の結果を示します。
{
"status": "SUCCESS",
"topic": "ioslab/home-metrics/ble/advertisements/v1"
}
この response が返っても、まだ MQTT で data が流れるとは限りません。次に対象 BLE device を topic に subscribe させる必要があります。
Subscribe は次の endpoint で行います。
POST /control/data/subscribe
成功時の response は、対象 device ID と request ID を含みます。
{
"status": "SUCCESS",
"id": "5ae0c228-24c9-433c-9001-5a23f063ffd0",
"requestID": "ebfd7e14-d4b7-4aa4-97f9-2dfaffed7c08"
}
ここまで揃ってから、data app ID / data app API key で MQTT broker に接続して topic を subscribe します。SCIM onboarding、Data App 登録、Topic 登録、Device subscribe のどこかが欠けると、AP が BLE advertisement を scan できていても MQTT payload は届きません。
実装箇所:
この処理は CISCO_IOT_ORCH_REGISTER_DATA_APP=true のとき collector 起動時に実行するようにしました。通常運用では毎回登録する必要はないため、false で起動できます。
BLE UUID の指定方法
Cisco Sensor Connect 側の register topic では、BLE の topic type として advertisements / gatt / connection_events があります。今回は connectionless advertisement を MQTT へ流したいので、advertisements に寄せました。
Minew S1 は scan だけで sensor data が出るため、topic は advertisement 用にします。GATT read / notify の UUID を登録する方向ではなく、advertisement payload 内の Service Data UUID をアプリケーション側で decode する方向にしました。
一方で、センサー値そのものは BLE advertisement の AD structure から読みます。今回の collector は AD type 0x16、つまり Service Data - 16-bit UUID を見ています。
実装では次の 16-bit UUID を許可しています。
uuid == 0xfe6a || uuid == 0xffe1 || uuid == 0xfeaa
BLE advertisement 上の 16-bit UUID は little endian で入るため、payload 先頭の 2 byte を binary.LittleEndian.Uint16 で読みます。service UUID の 2 byte を取り除いた残りが、センサー固有の service data になります。
Minew S1 実機の温度・湿度 frame では、advertisement 全体は次のような byte 列でした。
02 01 06 1b 16 6a fe 03 05 06 10 ff 20 98 04 11 03 ff ff 04 16 00 ff ff 03 13 3a 1a 02 12 3b
AD structure として分解すると次の通りです。
| bytes | 意味 |
|---|---|
02 01 06 |
Flags |
1b |
次の AD structure の length |
16 |
AD type: Service Data - 16-bit UUID |
6a fe |
16-bit UUID 0xfe6a little endian |
03 05 06 10 ff 20 98 04 11 03 ff ff 04 16 00 ff ff 03 13 3a 1a 02 12 3b |
Minew S1 service data |
0xfe6a は little endian なので packet 上は 6a fe として出ます。decode ではこの 2 byte を外し、残りを sensor payload として扱います。
同じ Minew S1 でも battery は別 frame として出ていました。
02 01 06 11 16 6a fe 02 80 02 06 53 04 32 35 5a 69 30 30 30 32
この frame では service data の 02 80 02 ... を battery payload として扱い、0x53 を 83% として decode しました。
実装箇所:
別の sensor family を追加する場合は、まず serviceDataFromAdvertisement の UUID allow list を増やし、次に decodeServiceData に payload decode を追加します。
MQTT に push される内容
IoT Orchestrator の MQTT payload は JSON ではなく protobuf 形式の binary data でした。
Cisco の Data Telemetry guide では、telemetry message は DataBatch に encapsulate され、その中に複数の DataSubscription が入ると説明されています。DataSubscription には device ID、BLE payload、subscription 情報、BLE advertisement の MAC / RSSI などが入ります。
home-metrics では外部の protobuf 生成コードを持たず、必要な field だけを読む小さな decoder にしました。
読んでいる主な field は次です。
| field | 内容 | home-metrics での扱い |
|---|---|---|
| DataBatch field 1 | repeated DataSubscription | message list として展開 |
| DataSubscription field 1 | device_id | BLE MAC が無い場合の fallback |
| DataSubscription field 2 | data bytes | BLE advertisement payload |
| DataSubscription field 3 | timestamp |
sensor_minute.ts の元時刻 |
| DataSubscription field 4 | AP MAC | 現状は保存しない |
| DataSubscription field 12 | BLEAdvertisement | BLE MAC と RSSI |
| DataSubscription field 16 | ApplicationEvent | restart/stop event の decode 用 |
Minew S1 00:fa:b6:07:de:4b の実機ログをもとに home-metrics の test data を作ると、MQTT で受け取る protobuf binary data は次のような hex になります。
0a7d0a2464393632333165372d313362342d346363642d626166612d656335633630623935633838121f0201061b166afe03050610ff2098041103ffff041600ffff03133a1a02123b1a0608c8e6fbd006221138343a35613a33653a64363a62373a383062190a1130303a46413a42363a30373a44453a344210bcffffff0f
これは 127 bytes の DataBatch として扱っています。field を分解すると次のようになります。
| protobuf path | hex | decode |
|---|---|---|
DataBatch[1] |
0a7d ... |
length 125 の DataSubscription
|
DataSubscription.device_id |
0a24 643936... |
d96231e7-13b4-4ccd-bafa-ec5c60b95c88 |
DataSubscription.data |
121f 0201061b166afe... |
BLE advertisement raw bytes |
DataSubscription.timestamp.seconds |
1a06 08c8e6fbd006 |
1780413256 = 2026-06-02T15:14:16Z
|
DataSubscription.ap_mac |
2211 38343a3561... |
84:5a:3e:d6:b7:80 |
DataSubscription.ble_advertisement.mac |
6219 0a11 30303a46413a42363a30373a44453a3442 ... |
00:FA:B6:07:DE:4B |
DataSubscription.ble_advertisement.rssi |
10bcffffff0f |
-68 dBm |
RSSI は protobuf の varint として入ってきますが、実装側では int32 として扱うことで 0xffffffbc を -68 として解釈しています。
この protobuf を 拙作home-metrics の decoder に通すと、まず次の中間表現になります。
DeviceID: d96231e7-13b4-4ccd-bafa-ec5c60b95c88
BLEMAC: 00:FA:B6:07:DE:4B
APMAC: 84:5a:3e:d6:b7:80
RSSI: -68
Timestamp: 2026-06-02T15:14:16Z
Data: 02 01 06 1b 16 6a fe 03 05 06 10 ff 20 98 04 11 03 ff ff 04 16 00 ff ff 03 13 3a 1a 02 12 3b
その後、Data を BLE advertisement としてさらに decode します。ここで初めて temperature / humidity / battery が出てきます。
mac: 00:fa:b6:07:de:4b
rssi_dbm: -68
temperature_c: 26.23
humidity_percent: 59
別の battery frame を受けると、同じ minute window に battery_percent: 83 が入ります。
つまり MQTT payload の段階では「Cisco が温度や湿度を JSON でくれる」のではなく、protobuf の中に BLE advertisement raw bytes が入っており、その中の service data をアプリケーション側で読む必要があります。
MQTT server 側で最低限必要な設定は、IoT Orchestrator に data app と topic が登録され、対象 device がその topic に subscribe されていることです。外部に Mosquitto などを別途立てるのではなく、IoT Orchestrator の MQTT broker :41883 に data app credential で接続します。
今回の最小構成は次です。
host: 192.168.67.6
port: 41883
username: data
password: <data app api key>
topic: ioslab/home-metrics/ble/advertisements/v1
payload: default format / protobuf binary
home-metrics collector では、最低限次の env が必要になります。
CISCO_IOT_ORCH_MQTT_ADDR=192.168.67.6:41883
CISCO_IOT_ORCH_DATA_APP_ID=data
CISCO_IOT_ORCH_DATA_API_KEY=<data app api key>
CISCO_IOT_ORCH_TOPIC=ioslab/home-metrics/ble/advertisements/v1
mosquitto で確認するなら、概念的には次の形になります。
mosquitto_sub -h 192.168.67.6 -p 41883 \
-u data --pw '<data app api key>' \
-t 'ioslab/home-metrics/ble/advertisements/v1' -v
実装では MQTT 3.1.1 の CONNECT / SUBSCRIBE / PINGREQ / PUBLISH だけを処理する軽量 client にしました。
decode の方法
今回の BLE センサーは、Cisco Sensor Connect が値を解釈して JSON 化してくれるわけではありません。MQTT で受けた data bytes から、アプリケーション側で sensor vendor の advertisement format を decode する必要があります。
処理の流れは次です。
MQTT payload
-> protobuf-like DataBatch を読む
-> DataSubscription.data を取り出す
-> BLE advertisement の AD structure を走査
-> AD type 0x16 の service data を取り出す
-> service UUID 2 byte を外す
-> sensor family ごとに値を decode
-> 1分 window に集約
-> median を sensor_minute に upsert
Minew S1 実機では、温度・湿度と battery が別々の advertisement frame として届きました。
実際のサンプルとして、00:fa:b6:07:de:4b の Minew S1 は次のように decode できます。
BLE MAC: 00:fa:b6:07:de:4b
advertisement: 02 01 06 1b 16 6a fe 03 05 06 10 ff 20 98 04 11 03 ff ff 04 16 00 ff ff 03 13 3a 1a 02 12 3b
service UUID: 0xfe6a
service data: 03 05 06 10 ff 20 98 04 11 03 ff ff 04 16 00 ff ff 03 13 3a 1a 02 12 3b
温度・湿度 frame の重要な marker は次です。
| marker | bytes | decode |
|---|---|---|
| temperature | 03 13 3a 1a |
0x1a3a / 256 = 26.23 C |
| humidity | 02 12 3b |
0x3b = 59% |
battery frame は別の advertisement として届きました。
advertisement: 02 01 06 11 16 6a fe 02 80 02 06 53 04 32 35 5a 69 30 30 30 32
service UUID: 0xfe6a
service data: 02 80 02 06 53 04 32 35 5a 69 30 30 30 32
battery: 0x53 = 83%
このサンプルは、home-metrics では次の値として sensor_minute に入ります。
mac: 00:fa:b6:07:de:4b
temperature_c: 26.23
humidity_percent: 59
battery_percent: 83
対応する test は TestDecodeMinewS1ScanOnlyAdvertisementSample として追加しています。
なお、拙作 home-metrics では、環境センサー系は過去の ble-scan で直接 BLE を読んだ実装と PostgreSQL の過去データを照合し、marker で値を拾う形にしました。
03 13 <temp little-endian int16 / 256>
02 12 <humidity>
03 05 17 <pressure little-endian float32>
04 1f 07 <co2 little-endian uint16>
04 1f 08 <etvoc little-endian uint16>
03 20 <lux little-endian uint16>
また、decode 後は異常値を捨てる実装にしています。
| metric | accepted range |
|---|---|
| temperature | -40 .. 85 C |
| humidity | 0 .. 100 % |
| battery | 0 .. 100 % |
| RSSI | -127 .. 20 dBm |
| pressure | 300 .. 1100 hPa |
| CO2 | 0 .. 10000 ppm |
| lux | 0 .. 65534 |
| eTVOC | 0 .. 60000 |
DB には raw telemetry は保存しません。sensor_minute に 1分単位の median として保存しています。
この読み取ったデータを拙作 home-metrics では内部で postgresql に保存し時系列データとして出す事が可能です。
API call でひっかかった点
Data App Topics は UI では作れなかった
IoT Orchestrator UI の Data App Topics は確認には使えますが、今回の環境では作成操作は API 側で行う必要がありました。registerDataApp、registerTopic、subscribe を明示的に実行します。
registerDataApp の payload 形
DevNet のサンプルでは、dataApps の register にあたる API payload は以下の形になっています。
{
"dataApps": [
{
"dataAppID": "telemetry-app-1"
}
],
"topic": "enterprise/hospital/pulse_oximeter",
"dataFormat": "default",
"controlApp": "controlApplication"
}
Devnet に記載がある実装 が参考になります。
今回の最終形は次のようにしました。
{
"dataApps": [
{
"dataAppID": "data"
}
],
"topic": "ioslab/home-metrics/ble/advertisements/v1",
"dataFormat": "default",
"controlApp": "control"
}
この API は control 側の API key で呼び出します。data app の API key は MQTT 接続時に使う、という切り分けにすると整理しやすいです。
HTTP 200 でも body を見る
Cisco の release notes には、API の結果と response code の扱いに注意が必要な known issue があります。アプリケーション側では、HTTP status だけでなく JSON body の status が FAILURE ではないかも見るようにしました。
onboarding と data telemetry は別物
BLE Inventory に device が見えることと、MQTT に advertisement が push されることは別の状態でした。特に以下の切り分けが重要でした。
- AP Inventory で AP が connected か。
- BLE allow-list / scan policy が対象 MAC を通すか。
- SCIM onboarding が成功し device ID があるか。
- Data app が登録されているか。
- Topic が
advertisementsとして登録されているか。 - Device が topic に subscribe されているか。
- MQTT に data app ID / data API key で接続しているか。
connectionless advertisement に寄せる
GATT read / notify が必要な sensor もありますが、今回の対象は advertisement だけで温度、湿度、battery、CO2、lux、pressure、eTVOC を取得できました。したがって、GATT connect の安定性や AP ごとの同時 BLE connection 数に依存しない形に寄せました。
Release Notes でも AP ごとの BLE connection 数や connect operation に関する注意点があるため、advertisement だけで済む sensor は connectionless にする方が運用しやすいです。
まとめ
Cisco Sensor Connect (IoT Orchestrator) は、Cisco Spaces cloud firehose とは別経路で BLE telemetry を受けられます。ポイントは、AP が BLE を見ることだけではなく、SCIM onboarding、Data App 登録、Topic 登録、Device subscribe、MQTT decode がすべて必要なことでした。
拙作 home-metrics での実装では、利用しているセンサーの特性を考慮して、connectionless advertisement を MQTT で受け、protobuf-like payload と BLE service data を decode し、1分 median として TimescaleDB に保存する形にしました。実装の詳細は GitHub の collector と test を参照してください。






