LoRa + MQTT + OpenRemote によるIoTシステムの構築

ThingsBoard CEはApache License 2.0のオープンソースだが、PEでは有償ライセンスが必要で、CEとPEで機能差があります。一方、OpenRemoteはAGPLv3で100%オープンソースとして提供されており、ライセンス費用なしで利用できるため、OpenRemoteも検討することにしました。

OpenRemoteは、ベースとなる地図データやGeoJSONによる独自の地図レイヤーを利用し、地図上にセンサー、設備、機器などの任意のデバイスを配置・管理できるIoTプラットフォームです。デバイスごとに多様なプロパティや状態を定義でき、ルールエンジンによるデータ処理や自動制御にも対応しています。

これにより、フィールドオートメーション、スマートシティ、スマートビルディング、エネルギー管理など、位置情報とIoTデータを組み合わせた幅広いシステムを構築できます。

Dockerコンテナで稼動。デプロイするサービスと環境変数については下記参照。

ドキュメント

GitHub

フォーラム

モバイルアプリ

Android

iOS

マップデータのインポート

OpenMapTiles

ここからマップデータを取得

入手したデータのファイル名 ***.mbtilesをinput.mbtilesへ変更。
mapディレクトリを作成し、この中へ移動。

./map/input.mbtiles

ダウンロードしたマップデータの一部領域のみ必要な場合は、以下のNodeJSコンテナを利用して変換処理を行います。

領域の経緯は以下のサイトから算出

left(West)=5.53 bottom(South)=47.23 right(East)=15.38 top(North)=54.96

$ docker run -it --rm --network host -v ./map:/mapdata nikolaik/python-nodejs:python3.8-nodejs12 bash
# npm install -g --unsafe @mapbox/mbtiles @mapbox/tilelive
# tilelive-copy --minzoom=0 --maxzoom=14 --bounds="5.53,47.23,15.38,54.96" /mapdata/input.mbtiles /mapdata/output.mbtiles

output.mbtiles を mapdata.mbtiles へ変更し /deployment/map ディレクトリへコピー

地図データ設定ファイルの座標も変更すること。中心座標は経緯度の平均値。

deployment/map/mapsettings.json

{
  "options" : {
    "default" : {
      "center" : [ 139.785, 35.655 ],
      "bounds" : [ 139.71, 35.59, 139.86,35.72 ],
      "zoom" : 15,
      "minZoom" : 0,
      "maxZoom" : 19,
      "boxZoom" : false,
      "geocodeUrl" : "https://nominatim.openstreetmap.org"
    },

URLを指定して地図データを取得する場合

URLを指定して保存すると、デフォルトのマップデータがOpenFreeMapに切り替わります。
URLを指定してImportボタンを押し保存すると、URLで指定した書式のみ反映されます。

URL: https://tiles.openfreemap.org/styles/positron


ウェザーマップAPI

Request query parameters

{
  "current": [
    "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,wind_speed_10m,wind_direction_10m,wind_gusts_10m,uv_index,shortwave_radiation"
  ],
  "wind_speed_unit": [
    "ms"
  ],
  "timezone": [
    "Asia/Tokyo"
  ],
  "latitude": [
    "35.662162168484286"
  ],
  "longitude": [
    "139.77089481216473"
  ]
}

注)temperature_2mなど各種パラメータの書式はOpen-Meteoから入手すること。

各デバイスをアセットとして地図上の任意の位置へ配置

6つのデバイス(アセット)を配置

フィールドを俯瞰表示して各デバイスを表示(カラーバルーン)

デバイス設置位置とステータス表示

モニターしたい任意のデバイスのステータスをグラフやゲージなどで表示

Agent/Protocol

MQTT

注)OpenRemoteはデフォルトでMQTTブローカとして Apache Artemis が組込まれています。

OpenWeatherMap

OpenWeatherMapのAPIに対応したエージェントですが有料サービスへの登録が必要(有料サービスの中で無料枠が設定されています)

HTTP

Open-MeteoのAPIを利用したデータ取得はここを参照。

E-MAIL

注)送信側SMTPの設定はManagerサービス(Dockerコンテナ)で設定

# Configure Email, SMTP server to send password reset emails etc.
      OR_EMAIL_HOST: smtp.gmail.com
      OR_EMAIL_USER: [email protected]
      OR_EMAIL_PASSWORD: XXXXXXXXXXXXXXXX
      OR_EMAIL_PORT: 587
      OR_EMAIL_TLS: true
      OR_EMAIL_X_HEADERS: # Headers to set on all outbound emails
      OR_EMAIL_PROTOCOL: # smtp or smtps for SSL (if TLS not set or is true then smtp is assumed)

受信側クライアント設定(GmailのIMAP設定)
ユーザ名とパスワードのJSON形式での記述

{
  "username": "[email protected]",
  "password": "XXXXXXXXXXXXXXXX"
}

カスタマイズ

マップ画面で表示される各マーカのデータ表示や出力値による色分けを追加。

例)マップ上のマーカを風速で色分け

  "pages": {
    "map": {
      "markers": {
        "WeatherAsset": {
          "attributeName": "windSpeed",
          "showLabel": true,
          "showUnits": true,
          "colours": {
            "type": "range",
            "ranges": [
              {
                "min": 0,
                "colour": "39B54A"
              },
              {
                "min": 10,
                "colour": "F7931E"
              },
              {
                "min": 20,
                "colour": "C1272D"
              }
            ]
          }
        }
      },

MQTTテスト

MQTTによるAPIを利用したリモート端末からのアセットへのサブスクリプション・パブリッシュ。

リモート操作用のサービスユーザmqttuserをOpenRemote側で別途用意する必要があります。

サービスユーザに割当てられるパスワードxxxxxxxx は、MQTTブローカへの認証に使用されます。

テスト用のアセットを作成(Thing Assetを選択)—> Add attribute: testValue (boolean)
アセットID XXXXXXXX はURLの末尾に表示されます。

他の端末のターミナルからmqttクライアントコマンドを実行。

注)clientIdは任意ですが、サブとパブで同一のIdを使用しないこと。

サブスクリプション
{realm}/{clientId}/attribute/{attributeName}/{assetId}
{realm}/{clientId}/attributevalue/{attributeName}/{assetId}

$ mosquitto_sub -h iot.example.com -p 8883   -u "master:mqttuser"   -P "xxxxxxxx"   -i "test-client-01"   -t "master/test-client-01/attribute/testValue/XXXXXXXX" -d

パブリッシュ
{realm}/{clientId}/writeattributevalue/{attributeName}/{assetId}

$ mosquitto_pub -h iot.example.com -p 8883   -u "master:mqttuser"   -P "xxxxxxxx"   -i "test-client-02"   -t "master/test-client-02/writeattributevalue/testValue/XXXXXXXX"   -m "true"

STM32 DX-LR30 メッシュセンサーネットワーク

STM32F103C8 + DX-LR30 (SX1262) + BMI160 + ESP32-C6 による、Node / Repeater / Gateway 兼用のLoRaメッシュセンサーネットワーク一式。Gatewayはメッシュのデータを MQTTS (TLS) でMQTTブローカーへ転送し、Wi-Fi設定ポータル (APモード) で設定します。

参照元リポジトリ:

ディレクトリ構成

project/
├── platformio.ini            # STM32側: CLI / mesh / mesh_debug の3ビルド環境
├── src/
│   ├── cli_main.cpp          # 元のインタラクティブLoRa CLI(変更なし)
│   └── mesh_main.cpp         # Node/Repeater/Gateway統合ファームウェア(本体)
└── esp32_gateway/            # ESP32-C6用の独立したPlatformIOプロジェクト (ESP-IDF)
    ├── platformio.ini
    ├── sdkconfig.defaults    # TLS証明書バンドル、mainタスクのスタックサイズ
    └── src/
        ├── main.cpp          # Wi-Fi (STA)・SNTP・MQTTS publish/subscribe・STM32とのUART橋渡し
        ├── gw_prov.cpp       # NVSへの設定保存 + APモードの設定ポータル (Webフォーム)
        ├── gw_prov.h
        ├── CMakeLists.txt    # コンポーネントの依存関係定義
        ├── Kconfig.projbuild # ビルド時オプション (UARTピン、初期値、設定用AP など)
        └── ca_cert.pem       # (任意) 自己署名・自前CAを使う場合のみ

lib/SX1262_Custom/(sx1262_driver.h/.cpp)は元リポジトリのものをそのまま project/lib/SX1262_Custom/ にコピーして使用(本一式には含まれていません)。

ESP32-C6 Gateway

ESP32-C6は公式PlatformIOレジストリのespressif32プラットフォームで、 espidfフレームワークのみサポート対象(arduinoフレームワークは未対応。 platformio/platform-espressif32 issue #1694 で報告されている既知の課題)。 そのため esp32_gateway は最初からESP-IDFで実装。

ESP32-C6はUSB Serial/JTAGを内蔵しているため、USB-Cケーブル1本で書き込み・ シリアルモニタ・JTAGデバッグ(VSCode+PlatformIOのDebug機能)が完結するため外付けのJTAGプローブは不要。

Gatewayの役割

  • 起動直後からSTM32のPING (0xF0) にPONG (0xF1) で応答し、STM32にGatewayと判定させます。 UARTはWi-Fi/SNTPより先に初期化するため、接続処理中や設定モード中でもPINGに応答できます。
  • STM32からUARTで受け取ったフレームを、TLSでMQTTにpublish。 結果バイト(0x01=成功 / 0x02=失敗)をSTM32へリターン。
  • リレー操作コマンドをsubscribeし、UARTでSTM32へ転送。

MQTTトピック

<prefix> は設定ポータルで指定するトピックプレフィックス(初期値はKconfig)。

方向 トピック ペイロード
Publish <prefix>/<node_id>/data JSON: node_id, seq, event_type (heartbeat/motion), rssi, snr, hop_limit、存在する場合は ax〜gz
Publish <prefix>/<node_id>/status JSON: node_id, seq, commanded_on, feedback_pin_high, last_cmd_seq_applied, rssi, snr
Subscribe <prefix>/+/cmd/relay ON または 1 でオン、それ以外はオフ

publishはQoS 0です。プレフィックスは、先頭と末尾に / を付けず、# と + を含まず、 47文字以内にしてください。

設定方法: 設定ポータル (APモード)

Wi-FiとMQTTの設定はソースに埋め込まず、Gateway自身が提供するWebフォームから入力し、 NVSに保存。

設定モードに入る条件

きっかけ 動作
保存済みの設定がない(初回起動) 設定モードで起動
起動時にWi-Fi接続が30秒失敗 次回の起動で設定モードに入るよう再起動
動作中にBOOTボタン(初期値GPIO9)を5秒長押し 設定モードに入るよう再起動

動作中にWi-Fiが切れた場合は設定モードに入らず、そのまま再接続を繰り返す。

ポータルの使い方

  1. スマホやPCでWi-Fi LoRa-GW-XXXX に接続します(XXXX はSoftAPのMACアドレス下位2バイト)。 パスワードは CONFIG_GATEWAY_PROV_AP_PASSWORD(初期値 gateway-setup。必ず変更してください)。

  2. ブラウザで http://192.168.4.1/ へアクセス。キャプティブポータル用のDNSは実装していないため、 アドレスは手入力してください。スマホが「インターネットに接続されていません」と表示したら、 接続を維持する側を選択。

  3. フォームに入力して 保存して再起動 を押下。Gatewayが再起動し、STAモードで動作開始。

項目 備考
SSID 設定モード開始時にスキャンした周辺のSSIDが候補として表示
Wi-Fiパスワード 空欄で保存すると現在の値を維持。保存済みの値がなく空欄の場合はオープンネットワーク扱い
ブローカURI mqtts:// を指定。例: mqtts://mqtt.example.com:8884
MQTTユーザー名 / パスワード パスワードを空欄にすると現在の値を維持
トピックプレフィックス 上記のトピックの規則を参照

設定ポータルに CONFIG_GATEWAY_PROV_TIMEOUT_SEC(初期値300秒)のあいだ操作がなければ、 Gatewayは再起動してSTAモードを試し直します(ルーターがまだ復旧していない停電復旧時でも、 自力で復帰できるようにするため)。

ビルド時オプション (Kconfig)

$ cd project/esp32_gateway
$ pio run -t menuconfig
# "Gateway Configuration" メニュー
オプション 用途
GATEWAY_UART_PORT_NUM, GATEWAY_UART_TX_PIN, GATEWAY_UART_RX_PIN STM32とのUART接続
GATEWAY_MQTT_BROKER_URI 設定フォームに表示される初期値
GATEWAY_MQTT_TOPIC_PREFIX 設定フォームに表示されるプレフィックスの初期値
GATEWAY_NTP_SERVER 時刻同期に使うNTPサーバ(初期値 ntp.nict.jp)
GATEWAY_MQTT_USE_CUSTOM_CA 公的CAバンドルの代わりに ca_cert.pem を埋め込む
GATEWAY_PROV_AP_PASSWORD 設定用APのパスワード(8文字以上)
GATEWAY_PROV_BUTTON_GPIO 5秒長押しに使うボタン(初期値9)
GATEWAY_PROV_TIMEOUT_SEC 設定モードの無操作タイムアウト(初期値300)

ESP32-C6のGPIO9はストラップピン。起動後に押す分には問題ありませんが、 リセット中にLowにしているとROMのダウンロードモードに入るので注意。

sdkconfig.defaults

CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144

最初の2行は、Let’s Encryptなど公的CAの証明書を検証するためのCAバンドルを有効にします。 スタックサイズについては、PlatformIOが「Default Value Mismatch」と通知しますが、 情報表示であり問題ありません。

src/CMakeLists.txt

idf_component_register(
    SRCS "main.cpp" "gw_prov.cpp"
    INCLUDE_DIRS "."
    REQUIRES nvs_flash esp_wifi esp_event esp_netif esp_timer
             esp_http_server mqtt json driver mbedtls
)

自前CAを使う場合は、esp32_gateway/platformio.ini に board_build.embed_txtfiles = src/ca_cert.pem も追加すること。

MQTTSとブローカーの設定

通信の保護とユーザー認証は、別の仕組み。

  • TLS(サーバ証明書): Gatewayがブローカーを確認し、通信を暗号化。 Let’s Encryptなどの公的CAなら、ESP32が内蔵のCAバンドルで証明書を検証するため、 デバイスに証明書ファイルを置く必要はなく、証明書の更新でファームウェアを書き換える必要もありません。

  • ユーザー認証: GatewayがTLSの中でMQTTのユーザー名とパスワードを送信。 クライアント証明書は不要。

TLSのハンドシェイクでは証明書の有効期間を検証するため、GatewayはMQTT開始前にSNTPで時刻を合わせます。 URIのホスト名は、証明書のCN/SANと一致している必要があります(IPアドレスでは検証に失敗します)。

推奨構成: HAProxyでTLS終端

ESP32-C6 --TLS 8884--> HAProxy (TLS終端) --平文 1883--> Mosquitto (パスワード + ACL)

OpenRemote Proxyの既存の listen mqtt ブロックを参考にした例。自分用の haproxy.cfg にこのブロックを追加し、既存の 8883 のブロックはそのまま残してください。

haproxy

listen mosquitto_mqtts
    bind *:8884 ssl crt /etc/haproxy/certs crt "${CERT_DIR}" crt "${CUSTOM_CERT_DIR}" no-tls-tickets
    mode tcp
    option clitcpka
    timeout client 3h
    timeout server 3h
    option logasap
    balance leastconn
    server mosquitto mosquitto:1883 resolvers docker_resolver

yaml

services:
  proxy:
    ports:
      - "8884:8884"
    volumes:
      - ./haproxy.cfg:/data/proxy/haproxy.cfg
    environment:
      HAPROXY_CONFIG: '/data/proxy/haproxy.cfg'

Mosquittoの1883はホストに未公開。Dockerネットワーク内で、 プロキシからのみ到達。

Mosquitto

mosquitto.conf

listener 1883 0.0.0.0
protocol mqtt

allow_anonymous false
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/acl.conf

Gateway専用のユーザーを作成し(パスワードファイルが既にある場合は -c を付けない)、 ACLで権限を絞ります。lora はトピックプレフィックスに置き換えてください。

bash

$ docker compose exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd gw01 'your-password'

conf

# /mosquitto/config/acl.conf
user gw01
topic write lora/+/data
topic write lora/+/status
topic read  lora/+/cmd/relay

acl_file を有効にすると、ACLに書かれていないユーザーは何も読み書きできなくなります。 このブローカーを使う他のクライアントがあれば、そのエントリも追加してください。

PCからの接続テスト:

bash

$ mosquitto_pub -h mqtt.example.com -p 8884 --capath /etc/ssl/certs \
  -u gw01 -P 'your-password' -t lora/1/cmd/relay -m ON

Gatewayが接続できないとき

MQTT_EVENT_ERROR のときに、シリアルログへ原因が出力されます。

ログ 考えられる原因
TLS/TCP error: esp_tls_err=... tls_stack_err=... 証明書の検証失敗、時刻が未同期、ホスト名の不一致、または8884へ到達できない
MQTT connection refused, return code=5 ユーザー名/パスワードの誤り
接続はできるがメッセージが届かない ACLで拒否されている可能性(ACLの拒否はエラーにならず、publishは黙って捨てられる)。ACLのトピックとプレフィックスが一致しているか確認
Time synced が出ない SNTPの同期に失敗。時刻が合うまで、証明書が「まだ有効でない」と判定されることがある

ビルド (STM32)

bash

# CLIデバッグビルド(動作確認・RF調整用)
$ pio run -e bluepill_cli -t upload

# 本番メッシュファームウェア
$ pio run -e bluepill_mesh -t upload

配線表

信号 STM32ピン 接続先
SPI1 NSS/SCK/MISO/MOSI PA4/PA5/PA6/PA7 DX-LR30
USART1 (デバッグ、CLIビルドのみ) PA9/PA10 USB-シリアル変換器
USART2 (ESP32通信) PA2(TX)/PA3(RX) ESP32-C6 UART(ピンはKconfigで設定。TX/RXはクロス接続)
I2C1 (BMI160) PB6(SCL)/PB7(SDA) BMI160モジュール
BMI160 電源 3.3V直結ピン BMI160モジュールの3.3Vピン(VINではなくLDOバイパス側)
BMI160 INT1 PA1 BMI160モジュールのINT1
Repeater判定ジャンパー PB0 GND短絡でRepeater、オープンでNode
ステータスLED PB11 外付けの青色LED(アクティブHIGH配線。極性が逆ならLED_ACTIVE_HIGHを0に)

役割判定ロジック

  1. ESP32がUARTのPING(0xF0)/PONG(0xF1)ハンドシェイクに応答 → Gateway
  2. PB0がGNDに落ちている(ジャンパー装着) → Repeater
  3. どちらでもない → Node(省電力・BMI160センサー)

起動時にLEDが役割に応じて点滅(1回=Node、2回=Repeater、3回=Gateway)。 Nodeにも関わらずBMI160が検出できなかった場合、5連の速い点滅でエラー表示し、 モーション検知なしのハートビート専用モードにフォールバック。

STM32F103ピン配置

STM32F103C8T6

ノード、リピータ、ゲートウェイのベースマイコンに使用。

適正受信感度

注)法規制の注意
920MHz 帯は特定小電力無線として使えますが、次の点に注意。

  • 技適(工事設計認証)取得したモジュールを使う必要があります。SX1262 の素のチップではなく、DX-LR30 のような技適取得済みモジュールで、取得時の構成(アンテナ含む)のまま使うことが前提です。
  • 920MHz 帯には送信時間制限(デューティ)、キャリアセンス(LBT)、チャンネル幅、出力**の規定があります。コメントにもあるとおり ARIB STD-T108 を確認してください。

ゲートウェイとノードを一対一接続した場合の目安

項目 値
周波数 920MHz帯
SF 9
帯域幅 125kHz
送信出力 13dBm(約20mW)
環境 到達距離の目安
見通しの良い屋外 2〜5km
住宅街(屋外同士) 300m〜1km
住宅街(片方が屋内) 100〜400m
鉄筋コンクリート建物内(壁・階をまたぐ) 数十m〜100m 程度

RSSI が -95dBm 程度まで