ThingsBoard / ThingsBoardからDeviceを操作する
Overview
ThingsBoardからDeviceの挙動をコントロールする方法について解説します。以下の2つの方法で実装してみます。
- AttributeをSubscribeする 関連公式ドキュメント
- Remote Procedure Calls (RPC)を使う 関連公式ドキュメント
作るもの
Dashboard上でボタンをクリックするとESP32に接続されたLEDが点灯・消灯する仕組みを作ります。(粗いgifで恐縮です)
-
AttributeをSubscribeする
-
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 Chain
、default
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 Attributes
の pinState
を読み取って、Trueなら Check
状態に、 FalseならUncheck
状態にします。
- Action:
Get attribute
を選択 - Attribute Scope:
Shared
を選択 - Attribute Key:
pinState
を入力 - Action result converter:
None
、Boolean
、`True を選択
Attribute Scopeには Shared
を設定しましたが、Shared Attribute
は「ThingsBoardから更新できるが、Deviceからは読み取ることしかできない」ものです。他にも Client Attribute
、Server 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 PinState
は HIGH/LOW
を切り替えるだけでThingsBoardとの連携とはまったく関係ないので読み飛ばして問題ありません。
大事なのは以下3箇所です。
Callback
Attributeの値が更新されたら実行する処理を callback
として関数を定義し、これを pubsubClient
に設定しています。
pubsubClient.setCallback(callback);
callback
の中では、
- 更新後のAttributeをStringとして
incoming
に入れる - 扱いやすいように
ArduinoJson
のJsonDocument
に変換する - 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の実装
実装例です。
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}}
を取得できますので、 method
がtoggle
だった場合に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