chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

ThingsBoardでCustom Widgetを作る

背景:ThingsBoardが少しづつ使えるようになってきた。できたら、自分が使いたいと思うWidgetでデータを可視化したい。
課題:TypeScript + Angularの組み合わせで新しいWidgetを作ることができるらしい。TutorialやManualを見て、簡単なCustomWidgetを作る
取り組み:Angularのパーツを使うのが基本らしいが、理解のためにまずはフレームワークやパーツを使わず素で書く(HTMLとTypeScript(JavaScript)で書く)

ビジュアルは全く凝っていないが最低限動くようになったWidgetは以下。左側は既成のWidgetで、右側が素で書いたCustomWidget。やってることはPCに接続された仮想センサから送信されるデータを取得してWidgetに表示している。PCには最大3つの仮想センサが付いてる前提

上記Widgetを実現するコードは以下
画面を構成するHTML

<html>
    <body>
        latest value:<span id="v0">###</span>, <span id="v1">###</span>, <span id="v2">###</span>
    </body>
</html>

HTMLの内容を更新するTypeScriptは以下(TSの文法を無視して実質JavaScriptになっているだろう)

self.onInit = function() {
       document.getElementById("v0").innerText = "let's start!!";
}
self.onDataUpdated = function() {   
    console.log('**update**')
    let v0=-1;
    let v1=-1;
    let v2=-1; 
    if (self.ctx.defaultSubscription.data.length > 0){
       if (self.ctx.defaultSubscription.data[0].data.length){
          v0 = self.ctx.defaultSubscription.data[0].data[0][1];
       }
    }
    if (self.ctx.defaultSubscription.data.length > 1){
       if (self.ctx.defaultSubscription.data[1].data.length){
          v1 = self.ctx.defaultSubscription.data[1].data[0][1];
       }
    }
    if (self.ctx.defaultSubscription.data.length > 2){
       if (self.ctx.defaultSubscription.data[2].data.length){
           v2 = self.ctx.defaultSubscription.data[2].data[0][1];
       }
    }
    document.getElementById("v0").innerText = v0;
    document.getElementById("v1").innerText = v1;
    document.getElementById("v2").innerText = v2;
}
self.onResize = function() {
}
self.onDestroy = function() {
}

以下はWidget LibraryのWidget編集画面でCustom Widget を作成しているところ

右下の画面に、作成したWidgetの挙動が表示される。

PCからPOSTしている疑似デバイス用ShellScriptは以下

$ cat ./client.sh
#!/bin/sh
while [ 1 ]
do
   val1=$(($RANDOM % 100 + 1))
   val2=$(($val1 + 5))
   val3=$(($val1 + 15))
   echo $val1,$val2,$val3
   curl  -X POST http://192.168.10.100:8080/api/v1/kJz*********M/telemetry --header Content-Type:application/json --data "{sensor01:$val1, sensor02:$val2, sensor03:$val3}"
   sleep 5
done

CustomWidgetを作る上での最低限の理解は以下かと

  • Widgetに表示するデータをGUIで選択する(ここは既成のWidgetも同じ)
  • 選択されたセンサからの入力が発生すると、self.onDataUpdated が呼び出される
  • センサのデータはself.ctx.defaultSubscription.data[0].data[0][1]に値が入る
  • 複数のセンサ値を指定した場合は、配列の要素数が増える
  • 値の処理はTypeScriptで書く(自分が書いたのはJSかTSか分からず)
  • Widgetの見た目はHTML/CSSで書く
  • JavaScript/TypeScriptのDOMの仕組みを使って、CustomWidget内の表示パーツを更新する (DOMで更新せずにAngularのフレームワークを使うのが本来の使い方)

■追記
センサーの値に加えて、センサ名等を追加してみる。公式ドキュメントは細かすぎてどこを読んでいいのか分かりづらいので、ブラウザでデバッグしながら当たりをつける。ソース側でログ出力設定をしておくと、DevToolsのConsoleで値を調べることができる。

DevToolsではオブジェクトの階層を潜っていけるので、onDataUpdatedで参照している変数(self.ctx)のデータ構造がどうなっているのか調べることができる。オブジェクトをばらしていくと、Entityの名前、センサ値の名前は以下に入っていそうだと見当がつくので、、

self.ctx.defaultSubscription.data[0].datasource.entityName;
self.ctx.defaultSubscription.data[0].dataKey.name;

センサ名等を入れる改訂版CustomWidgetを以下としてみる

<html>
    <body>
        <p>
              dev: <span id="dataSource0">dev</span>, sensor: <span id="dataKey0">sensor0</span>
        </p>
        <p>
               latest value:<span id="v0">###</span>, <span id="v1">###</span>, <span id="v2">###</span>
        </p>
    </body>
</html>
self.onInit = function() {
       document.getElementById("v0").innerText = "let's start!!";
}
self.onDataUpdated = function() {    
    console.log('**update**');
    console.log(self.ctx.defaultSubscription.data[0]);   /* for debug */
    let v0=-1;
    let v1=-1;
    let v2=-1;
    let dataSource0 = '';
    let dataKey0 = '';
    
    if (self.ctx.defaultSubscription.data.length > 0){
       if (self.ctx.defaultSubscription.data[0].data.length){
          v0 = self.ctx.defaultSubscription.data[0].data[0][1];
          dataSource0 = self.ctx.defaultSubscription.data[0].datasource.entityName;
          dataKey0 = self.ctx.defaultSubscription.data[0].dataKey.name;

       }
    }
    if (self.ctx.defaultSubscription.data.length > 1){
       if (self.ctx.defaultSubscription.data[1].data.length){
          v1 = self.ctx.defaultSubscription.data[1].data[0][1];
       }
    }
    if (self.ctx.defaultSubscription.data.length > 2){
       if (self.ctx.defaultSubscription.data[2].data.length){
           v2 = self.ctx.defaultSubscription.data[2].data[0][1];
       }
    }
    document.getElementById("v0").innerText = v0;
    document.getElementById("v1").innerText = v1;
    document.getElementById("v2").innerText = v2;
    document.getElementById("dataSource0").innerText = dataSource0;
    document.getElementById("dataKey0").innerText = dataKey0;
}
self.onResize = function() {
}
self.onDestroy = function() {
}

Dashboardで確認、センサーデバイス名、センサー名が表示されたことを確認*1。上がCustomWidget、下は既成のWidget(Time Series Tableだったか)

■追記
ThingsBoardは情報を公開してくれているので、その気になったらソースを見て調べることもできるのでありがたい。だが。。規模がでかくてややこしい。B2B用に作られているので、現場でまともに使えるにはこれだけの複雑さになってしまうということか。公式ドキュメントも素人には難しすぎる。自分はJavaScript/TypeScript/Angularとかそっち系がまったく分かっていないので、変数の値をログに出力してデータ構造を分解しないとどうしていいのか分からない。JSのフレームワークを使いこなせる人にとってはCustomWidgetを作るのはそれほど苦労ないのだろうなと思われる。ThingsBoardのAPIを駆使すると、もっとシンプルに書けるのだろう。

■参考URL
ThingsBoard社によるCustomWidget作成の解説文(全仕様なのでえらく長い。サンプルを作りながら全部読み切るには数カ月かかりそうだ)
Widgets Development Guide | ThingsBoard Community Edition
YouTube上にもCustomWidgetの説明動画がある
https://www.youtube.com/watch?v=-w14Fa4QUv4
YouTubeではAngularのパーツ使って作成している

*1: 乱数の正しいスペルはrandom (rundomではない!!)