#はじめに
この記事では、ThingWorx のカスタムサービスを実装する際にヒントとなるちょっとしたスクリプトの断片を紹介します。
対象バージョン
ThingWorx 6.x, 7.x, 8.x
必要となる技術
- ThingWorx の開発基礎
- JavaScript
#プロパティの変更
UpdatePropertyValues()
Thing には組み込みのサービスとして UpdatePropertyValues() があります。このサービスを使うと、Thing のプロパティを一括して更新できます。
前提として、UpdatePropertyValues()は INFOTABLE を引数として取ります。したがって、サービスの中で INFOTABLE を用意します。また、このサービスに対する引数も INFOTABLE としておいたほうが使いやすいでしょう。
// created by kanoloa@sdc
// on Mar 8, 2020
// このサービスへの引数の INFOTABLE が 0 件ではなかったら、
if (properties.rows.length > 0) {
var input = properties.rows[0];
// UpdatePropertyValues() への引数となる空の INFOTABLE を作る
var updateValues = Resources["InfoTableFunctions"].CreateInfoTable(
{infoTableName: "valueTable"}
);
// "name" フィールドを INFOTABLE に追加
var nameField = new Object();
nameField.name = "name";
nameField.baseType = "STRING";
updateValues.AddField(nameField);
// "value" フィールドを INFOTABLE に追加
var valueField = new Object();
valueField.name = "value";
valueField.baseType = "STRING";
updateValues.AddField(valueField);
var updateRow = new Object();
if (input.Property_1) {
updateRow.name = "Property_1";
updateRow.value = input.Property_1;
updateValues.AddRow(updateRow);
}
if (input.Property_2) {
updateRow.name = "Property_2";
updateRow.value = input.Property_2;
updateValues.AddRow(updateRow);
}
Things["MyThing"].UpdatePropertyValues({
values: updateValues
});
PASSWORD 型プロパティ
ThingWorx には PASSWORD というデータ型(BaseType)があります。データベースの接続パスワードなどを扱うためのデータ型ですが、サービスの入力パラメータを定義する際、この PASSWORD 型を選べません。サービスへは String 型で渡し、サービス内部で PASSWORD 型へ変換する必要があります。
コードは下記の通り。
// password は Input パラメータ
if (password) {
var encPass = Resources["EncryptionServices"].EncryptPropertyValue({
data: password
});
Things["myThing"]encryptedPassword = encPass;
}
#コンフィギュレーションテーブル
ThingWorx の Thing には、プロパティとよく似た働きをするコンフィグレーションテーブルというものあります。よく目に触れるのは、Database ThingTemplet を継承した Thing ですね。
Database Thing
画像からわかるとおり、Database Thing の場合には接続に使う JDBC 接続文字列や、ユーザー名、パスワードなどがコンフィグレーションテーブルに保存されています。どちらかというと、リソースを使うために必要となる設定を保存するのがコンフィグレーションテーブルの役目だと考えて差し支えありません。
コンフィグレーションテーブルの設定は ThingWorx の実行中に動的に読み出したり書き込んだりできますが、プロパティのように直感的な方法は用意されていません。スクリプト例を下記に紹介します。
// Database Thing の接続情報の例
// 現在の情報を獲得する。
table = Things[DatabaseThing].GetConfigurationTable({
tableName: "ConnectionInfo"
});
// 新しい JDBC 接続文字列をセット。ConnectionString は Input パラメータ。
table.rows[0].jDBCConnectionURL = ConnectionString;
// 接続情報を更新する。
Things[DatabaseThing].SetConfigurationTable({
tableName: "ConnectionInfo",
configurationTable: table,
persistent: true
});
Things[DBThing].RestartThing();
#制御構造
##スリープ処理
ThingWorx のサービスは JavaScript で記述しますが、実際には Rhino を介して JVM 上で実行されています。こうした環境のため、通常使えるいくつかの JavaScript のメソッドが使えないことああリます。その代表的なものが、setTimeout() および setInterval() です。サービスの中に setTimeout() や setInterval() を記述してもエラーにはなりませんが、期待した通りの効果にもなりません。
サービスの中で一定期間処理を止めたい場合には、下記のサービス(もしくはサービス内部の関数)を作成して、それを呼びます。
var start = new Date();
var now = null;
do {
now = new Date();
// 下記、timeout はこのサービスへの Input パラメータ
} while (now - start < timeout);
var end = new Date();
var elapsed = (end - start) / 1000;
logger.debug("waited for " + elapsed + "sec");
##ログ出力にサービス名をつける
サービスの内部からログ出力を行うには logger.info()
などのファンクションを呼びますが、出力文字列の冒頭部分にサービス名を付け足したいことがよくあります。
そんなときには、サービスの冒頭で logger
のラッパー関数を書いてしまうのが手っ取り早いです。
svcname = me.name + ": myService(): ";
var debugLog = function(message) {logger.debug(svcname + message);};
var errorLog = function(message) {logger.error(svcname + message);};
var warnLog = function(message) {logger.warn(svcname + message);};
var infoLog = function(message) {logger.info(svcname + message);};
me.name
には、このサービスを持っている Thing 名が入ります。
info や warn など、各クラス用のラッパー関数を定義し、それをコードから呼び出します。
debugLog("This is test output.");
ScriptLog には次のように出力されます。
2020-03-10 08:38:50.190+0900 [L: DEBUG] [U: kanoloa] [S: ] [T: http-nio-80-exec-8] test: myService(): This is test output.
#カスタム・ウィジェット
この章では、Thing のサービスではなく、カスタム開発のウィジェットの中で使用する JavaScript コードのヒントを記載します。
##ファイルレポジトリに画像を保存する
API ドキュメントからはわかりづらいのですが、ウィジェットや REST API 経由でファイルリポジトリの SaveImage() サービスを呼ぶ際、content パラメータに渡すデータはバイナリデータではありません。Base64 でエンコードされた文字列を渡します。<input type="file">
でファイルを読み込む時には、FileReader
の readAsDataURL()
を使って下記のように Base64 にエンコードされたデータを作ります。
TW.Runtime.Widgets.photoshoot= function () {
var thisWidget = this;
this.renderHtml = function () {
return '<div class="widget-content widget-photoshoot">' +
'<form name="psForm" class="psForm" action="" method="post" ' +
'enctype="multipart/form-data" target="psFakeWindow">' +
'<div class="psContainer">' +
'<input type="file" class="psSelector" name="file" accept="image/*" capture>' +
'</div>' +
'</form>' +
'<iframe name="psFakeWindow" class="psFakeWindow"></iframe>' +
'</div>';
};
this.afterRender = function () {
psSelector.change(function() {
// ファイル名を取得し、ウィジェットのプロパティに格納。
var file = psSelector.prop('files')[0];
thisWidget.setProperty('File Name', file.name);
var reader = new FileReader();
// reader.readAsDataURL 完了時のコールバックを書く
reader.onload = function() {
// reader.result は Data URL 形式なので、冒頭の余分な文字列を削り取る。
var row_data = reader.result.slice(reader.result.indexOf(',') + 1);
// ウィジェットの Content プロパティに値をセット。このプロパティは isBindingSource = true。
thisWidget.setProperty('Content', row_data);
thisWidget.jqElement.triggerHandler('Submitted');
};
// Data URL として入力ファイルを受け取る
reader.readAsDataURL(file);
});
}
}
reader.result
もしくは setProperty()
で Content
プロパティに保存した内容をファイルリポジトリに保存するにはいくつかの方法が考えられます。
ひとつめはウィジェットの JavaScript コードから ThingWorx サーバー内のエンティティのサービスを直接呼び出す方法で、 ThingworxInvoker というサービスを使用します。しかしこの ThingworxInvoker はドキュメントがほぼ存在しませんので、使用は推奨しません。
次に this.afterRender()
関数の中から xhr でファイルリポジトリの SaveImage() サービスを呼び出すこともできます。
お勧めの方法は、マッシュアップビルダーでウィジェットのプロパティと SaveImage() サービスの入力パラメタである content をバインドしてしまうことです。この方法であれば追加のコードは必要ないですし、バインド先を変えることでさまざまなユースケースに対応できそうです。
注意すべき点として、Content
プロパティはバインドソースとして宣言されていなければなりません。
this.widgetProperties = function () {
return {
'name': 'PhotoShoot',
'description': 'Take and upload a photo.',
'category': ['Common'],
'properties': {
'File Name' : {
'description': 'This holds actual uploaded file name.',
'baseType': 'STRING',
'isBindingSource': true
},
'Content': {
'description': 'Captured image content.',
'baseType': 'IMAGE',
'dafaultValue': false,
'isBindingSource': true
}
}
}
};