🦔

ThingsBoard / ThingsBoardからDeviceを操作する

2025/02/17に公開

Overview

ThingsBoardからDeviceの挙動をコントロールする方法について解説します。以下の2つの方法で実装してみます。

  1. AttributeをSubscribeする 関連公式ドキュメント
  2. Remote Procedure Calls (RPC)を使う 関連公式ドキュメント

作るもの

Dashboard上でボタンをクリックするとESP32に接続されたLEDが点灯・消灯する仕組みを作ります。(粗いgifで恐縮です)

  1. AttributeをSubscribeする

  2. Remote Procedure Calls (RPC)を使う

使うもの

以下を使って実装します。

  • Arduino
  • PlatformIO
  • ESP32
  • LED

ESP32の32番PinをLEDと接続します。(Hardwareの知見に乏しいので接続の仕方は割愛します。あしからず。)

AttributeをSubscribeする

ThingsBoardのDeviceにはTelemetryとは別にAttributeというものがあります。基本的にTelemetryが時系列データであるのに対して、AttributeはそのDeviceの属性のイメージです。

このAttributeを以下のように実装することでLEDの点灯・消灯を実現します。

  • Dashboardから更新する
  • Deviceは値の変更を監視(subscribe)し、変更があったらその値を元に操作を行う
    図解すると以下です。

では実装します。

Deviceの準備

ThingsBoard上に適当なDeviceをひとつ用意します。Attributeの設定ができるようなRule Chain、Device Profileを設定しておきます。デフォルトで用意されている Root Rule Chaindefault Device Profileで事足ります。

Dashboardの作成

まずはAttributeを更新するためのDashboard Widgetを作ります。
Dashboardを用意して、Add widget > Buttons > Toggle Button Widgetを追加します。

Deviceにはさきほど作ったDeviceを指定し、下部の

  • Initial State: ダッシュボードを開いたときの操作
  • Check: Uncheck -> Checkに切り替えた時の動作
  • Uncheck: Check -> Uncheckに切り替えた時の動作
    をそれぞれ設定します。

Initial State

Initial Stateには以下を設定します。 Shared AttributespinState を読み取って、Trueなら Check 状態に、 FalseならUncheck 状態にします。

  • Action: Get attribute を選択
  • Attribute Scope: Shared を選択
  • Attribute Key: pinState を入力
  • Action result converter: NoneBoolean、`True を選択

Attribute Scopeには Shared を設定しましたが、Shared Attributeは「ThingsBoardから更新できるが、Deviceからは読み取ることしかできない」ものです。他にも Client AttributeServer Attributeがありますが、Scopeが異なります。詳しくは公式ドキュメントを参照してください。

Check

Checkに切り替えたときには、pinState にTrueを設定するようにします。

Uncheck

Checkの設定とほぼ同じですが、こちらではFalseを設定するようにします。

デザインの設定

見た目はButton appearanceで変更できるので適宜変更してください。Device操作の挙動には影響しません。

以上がToggle Button Widgetの設定です。参考までに以下がWidgetのConfigのJSONです。

Toggle Button Widget JSON

{
"widget": {
"typeFullFqn": "system.toggle_button",
"type": "rpc",
"sizeX": 4,
"sizeY": 2,
"config": {
"showTitle": true,
"backgroundColor": "#ffffff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "0px",
"settings": {
"initialState": {
"action": "GET_ATTRIBUTE",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": "SHARED_SCOPE",
"key": "pinState"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value /\nreturn data;",
"compareToValue": true
}
},
"checkState": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"key": "pinState",
"scope": "SHARED_SCOPE"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": true,
"valueToDataFunction": "/
Convert input boolean value to RPC parameters or attribute/time-series value /\nreturn value;"
}
},
"uncheckState": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"key": "pinState",
"scope": "SHARED_SCOPE"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": false,
"valueToDataFunction": "/
Convert input boolean value to RPC parameters or attribute/time-series value / \n return value;"
}
},
"disabledState": {
"action": "DO_NOTHING",
"defaultValue": false,
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/
Should return boolean value */\nreturn data;"
}
},
"autoScale": true,
"horizontalFill": true,
"verticalFill": false,
"checkedAppearance": {
"type": "outlined",
"showLabel": true,
"label": "On",
"showIcon": true,
"icon": "notifications_active",
"iconSize": 24,
"iconSizeUnit": "px",
"mainColor": "#198038",
"backgroundColor": "#FFFFFF",
"borderRadius": "4px",
"customStyle": {
"enabled": null,
"hovered": null,
"pressed": null,
"activated": null,
"disabled": null
}
},
"uncheckedAppearance": {
"type": "filled",
"showLabel": true,
"label": "Off",
"showIcon": true,
"icon": "notifications",
"iconSize": 24,
"iconSizeUnit": "px",
"mainColor": "#D12730",
"backgroundColor": "#FFFFFF",
"borderRadius": "4px",
"customStyle": {
"enabled": null,
"hovered": null,
"pressed": null,
"activated": null,
"disabled": null
}
},
"background": {
"type": "color",
"color": "#fff",
"overlay": {
"enabled": false,
"color": "rgba(255,255,255,0.72)",
"blur": 3
}
},
"padding": "12px"
},
"title": "Update Attribute",
"dropShadow": true,
"enableFullscreen": false,
"widgetStyle": {},
"actions": {},
"widgetCss": "",
"noDataDisplayMessage": "",
"titleFont": {
"size": 16,
"sizeUnit": "px",
"family": "Roboto",
"weight": "500",
"style": null,
"lineHeight": "1.6"
},
"showTitleIcon": false,
"titleTooltip": "",
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400
},
"pageSize": 1024,
"titleIcon": "home",
"iconColor": "rgba(0, 0, 0, 0.87)",
"iconSize": "24px",
"configMode": "basic",
"targetDevice": {
"type": "device",
"deviceId": "6b01d860-7012-11ef-b982-9fbe01b62b8d"
},
"titleColor": null,
"borderRadius": null,
"datasources": [],
"enableDataExport": true
},
"row": 0,
"col": 0,
"id": "daf67369-00c1-ef64-2182-89f7f7b625d1"
},
"aliasesInfo": {
"datasourceAliases": {},
"targetDeviceAlias": null
},
"filtersInfo": {
"datasourceFilters": {}
},
"originalSize": {
"sizeX": 4,
"sizeY": 4
},
"originalColumns": 24
}

Firmwareの実装

続いてFirmwareの実装です。以下に実装例を示します。 class PinStateHIGH/LOW を切り替えるだけでThingsBoardとの連携とはまったく関係ないので読み飛ばして問題ありません。

https://github.com/kazrin/thingsboard-firmwares/blob/82e4e7390f03a62aeb3f7a0c99a4c770bca376cc/src/subscribeAttributes/togglePin.cpp#L1-L100

大事なのは以下3箇所です。

Callback

Attributeの値が更新されたら実行する処理を callback として関数を定義し、これを pubsubClient に設定しています。

pubsubClient.setCallback(callback);

callbackの中では、

  1. 更新後のAttributeをStringとして incoming に入れる
  2. 扱いやすいように ArduinoJsonJsonDocument に変換する
  3. Attributeの値にあわせてLEDをON/OFFする
    という順に処理をしています。

あとで操作してみるとわかるのですが、{“attribute1”: “value1”, “attribute2”: true} というフォーマットでThingsBoardからデータを取得できます。今回の場合は {“pinState”: true} もしくは {“pinState”: false} です。

Subscribe

MQTT Brokerへの接続(pubsubClient.connect)後にpubsubClient.subscribe("v1/devices/me/attributes") でSubscribeを開始しています。 v1/devices/me/attributes はAttributeの更新をSubscribeするためのトピックです。

bool connected = pubsubClient.connect(
    BasicMQTT::client_id,
    BasicMQTT::user_name,
    BasicMQTT::password);
if (connected)
        {
            pubsubClient.subscribe("v1/devices/me/attributes");
        }

Loop

void loop() 内で、MQTT Brokerに接続できている限り、 pubsubClient.loop() を実行するようにしています。 loop が実行されるとSubscribeしているトピックに新しいメッセージがあるかどうかを確認してcallbackを処理するので必須です。

if (pubsubClient.connected())
{
    pubsubClient.loop();
}

確認

Firmwareを書き込めたらDeviceを起動して、Dashboardを操作してみます。冒頭で示した動画のようにLEDのON/OFFが切り替わります。

Serial Monitorを確認するとDashboardの操作を行うたびに以下のようなログが出力されるような実装になっています。

Message arrived [v1/devices/me/attributes] {"pinState":true}
Message arrived [v1/devices/me/attributes] {"pinState":false}

うまくいかない場合は上記の設定に問題がないかの再確認に加え、WiFiに繋がっているか、MQTT Brokerに繋がっているかを確認してください。

なお、ThingsBoardの当該Deviceの"Attributes"でpinStateの値を確認できます。

Remote Procedure Calls (RPC)を使う

次にPRCを使ってLEDをON/OFFしてみます。RPCとは何か、が気になるところです。Wikipediaを見るといろいろ書いてあったり、gRPCのことを思い出して「難しそう」と思うかもしれませんが、ThingsBoardでいうところのRPCは「遠隔でDeviceの操作をするための、とあるフォーマットに従ったJSONを送る機能」でしかないので、身構える必要はありません。

イメージ的にもAttributeの場合とほとんど変わりません。

Dashboardの作成

まずはDashboard Widgetを作ります。さきほど使ったToggle ButtonでもRPCを実装できますが、別のWidgetを使ってみます。Add widget > Buttons > Command Button Widgetを使います。

Onclickをクリックします。

  • Action: Execute RPCを選択
  • Method: toggle を入力
  • Parameters: Noneで十分なのですが、あとでデータを見てみるためJSONに適当なデータを入れておきます。

保存したら完了です。

Firmwareの実装

実装例です。

https://github.com/kazrin/thingsboard-firmwares/blob/82e4e7390f03a62aeb3f7a0c99a4c770bca376cc/src/serverSideRPC/togglePin.cpp#L1-L104

AttributeのSubscribeとほぼ同じですが、差分を見てみます。

$ diff src/subscribeAttributes/togglePin.cpp src/serverSideRPC/togglePin.cpp
61,62c61,64
<     // Update pin state
<     pinState.setState(doc["pinState"]);
---
>     if (doc["method"] == "toggle")
>     {
>         pinState.toggle();
>     }
95c97
<             pubsubClient.subscribe("v1/devices/me/attributes");
---
>             pubsubClient.subscribe("v1/devices/me/rpc/request/+");

後半の差分を先に説明すると、Attributeの更新のSubscribeからRPCのSubscribeに変更したため対象のTopicも変更しているだけです。RPCの場合は "v1/devices/me/rpc/request/+" になります。

前半の差分は、RPCにしたことで取得できるデータのフォーマットも変更したためにそれに対応しています。さきほど作ったCommand Buttonの設定だと {"method":"toggle","params":{"pin":32}} を取得できますので、 methodtoggleだった場合にLEDもtoggleするように実装しています。

確認

Firmwareを書き込んで起動したら、Command Button Widgetをクリックしてみます。LEDのON/OFFが切り替わります。

ESP32のSerial Monitorには以下のような文字列が出力されます。

Message arrived [v1/devices/me/rpc/request/89] {"method":"toggle","params":{"pin":32}}

まとめ

ThingsBoardからDeviceに対して操作を行う方法について記載しました。当初は公式ドキュメントにもWeb上のどこにもFirmwareについて触れたものがなくて「いい感じに遠隔操作できるなんてスゴイ...!」と思ってましたが、Firmwareの実装は自分でやってねということでした。

とはいえ、ThingsBoardを使うことでUIつきの遠隔操作を実装しやすくなると思います。

Discussion