前回の記事では、ThinkLink REST APIを使用して500デバイスを半日でオンボーディングする方法を説明しました。デバイスがオンラインになると、すぐに次の問題が発生します: データは流れているのに、誰もそれを読むことができません。
コンソールには次のようなアップリンクが表示されます:
IQcDDG4PAADWBIsC34IG
ビジネス側が実際に求めているのは、温度23.8 °C、湿度65.1 %、バッテリー3.40 V、ステータス正常、レポート間隔15分です。これら2つの状態の間に位置する層が モノのモデルデコーダースクリプト であり、これが本記事の主題です。
1つのアップリンクの実際の中身
まず、そのBase64文字列を分解してみましょう。これは標準のThinkLink LoRaWAN ASメッセージ内の msg.userdata.payload 標準のThinkLink LoRaWAN ASメッセージ内の20 Base64文字、15バイト:
21 07 03 0C 6E 0F 00 00 D6 04 8B 02 DF 82 06
KS52温湿度センサーのプライベートフレームフォーマットに照らし合わせると:
| バイトオフセット |
生値 |
意味 |
デコード |
結果 |
| 0-2 |
21 07 03 |
固定フレームタグ |
直接比較 |
有効なフレーム |
| 3-4 |
0C 6E |
シーケンス / 予約 |
このモデルでは解析されません |
— |
| 5-6 |
0F 00 |
レポート間隔 |
uint16 LE → 15(分) |
15分 |
| 7 |
00 |
ステータスビット |
bit0 = 故障、bit4 = 漏水 |
正常、漏水なし |
| 8-9 |
D6 04 |
温度 |
uint16 LE → 1238、(1238−1000)/10 |
23.8 °C |
| 10-11 |
8B 02 |
湿度 |
uint16 LE → 651、×0.1 |
65.1 % |
| 12 |
DF |
バッテリー電圧 |
uint8 → 223、(223×1.6)/254+2.0 |
3.40 V |
| 13-14 |
82 06 |
予約 / チェックサム |
このモデルでは解析されません |
— |
15バイトが5つのビジネス値を運びます。これがLoRaWANエンドデバイスがJSONではなくコンパクトなバイナリを使用する理由です — エンジニアが難解さを好むからではなく、無線インターフェースが他の何も許容できないからです。
1。デバイスが単にJSONを送信できない理由
LoRaWANに不慣れな誰もがこれを尋ねます:アプリケーション層が結局JSONを必要とするなら、なぜデバイスに送信させないのか {"temperature":23.8,"humidity":65.1}?
答えはエアタイムです。SX1262無線、BW125 kHz、CR 4/5の場合:
| ペイロード |
SF7でのエアタイム |
15バイトに対する相対値 |
| 15バイト(コンパクトバイナリ) |
46.3 ms |
1.0× |
| 120バイト(フィールド名とタイムスタンプ付きJSON) |
199.9 ms |
4.3× |
拡散因子を加えると、その差は無視できなくなります — 同じ15バイトのペイロード:
| 拡散因子 |
エアタイム |
SF7に対する相対値 |
| SF7 |
46.3 ms |
1.0× |
| SF10 |
329.7 ms |
7.1× |
| SF12 |
1155.1 ms |
24.9× |
15 分ごとに 1 回のアップリンク(1 日あたり 96 回)の場合、1 台のデバイスは 1 日あたり 4.4 秒 を SF7 での高出力送信に費やし、これに対して 111 秒 を SF12 で費やします。これが本シリーズのバッテリー寿命に関する記事の背景にある仕組みです: 送信する余分な1バイトごと、そしてフォールバックするSFの1段階ごとが、バッテリーから支払われる。
また、厳格な上限もあります。RP002-1.0.5 の地域パラメータでは、中国の CN470 帯域は最大 MAC ペイロード 15 バイト を最低データレート(DR0)で許可しています。上記のフレーム長は偶然ではありません — 最悪のリンク条件下で規制上の制限が強制する値なのです。
したがって、LoRaWAN における役割分担は固定されています: デバイスは可能な限り強く圧縮し、プラットフォームは人間が読めるものを復元します。 デコーダースクリプトは、その復元ステップの作業負荷のすべてです。
この記事の知識をプロジェクトに活かす
ManThink(門思科技)は、デバイス接続、ネットワーク、データ活用に向けたIoT製品を提供しています。この記事の技術情報とあわせて、以下の製品情報をご覧ください。
KS52 温湿度・漏水センサー · ThinkLink
ManThinkの製品を確認し、プロジェクトを相談する →
2. ThinkLink がデコードを配置する場所
ThinkLink でデコードを担当するモジュールは デバイスモデルです。その役割:LoRaWAN ネットワークサーバー(NS)— またはその他のプロトコル — から到着する生データを、標準化されたテレメトリー(telemetry_data)と共有属性(shared_attrs)に変換し、テーブル、ダッシュボード、アラーム、プロトコルマッピングに提供します。
デバイスモデル には 4 つの設定ブロックがあり、いずれかを省略すると手戻りが発生します:
| ブロック |
目的 |
よくある省略 |
| パーサースクリプト |
バイナリフレームを名前付きフィールドに変換 |
— |
| 表示フィールド |
UI のテーブル/カードに表示する内容を定義 |
スクリプトが動けば完了したと想定される |
| Modbus マッピング |
Modbus TCP 経由でデータを公開 |
産業統合時に不足が判明 |
| BACnet フィールド |
BACnet/IP 経由でデータを公開し、ポイントリストを生成 |
建物引き渡し時に不足が判明 |
公式ドキュメントには引用に値する指摘があります: スクリプトがあるからといって、マッピングが完了しているわけではありません。 これは実際のプロジェクトで最もよくある手戻り項目です — スクリプトは問題なく動作するものの、BMS インテグレーターがポイントリストを要求した時点で、BACnet フィールドが 1 つも定義されていないのです。
デバイスモデル の作成には 名前 (表示用)と ID 名 (コードと API が使用する識別子)の両方が必要です。これらは独立しており、名前だけを入力すると問題が残ります。
3. スクリプトが実行される場所:まずサンドボックスのルール
パーサースクリプトは vm2 サンドボックスで実行され、Node.js メインプロセスから完全に分離されています。これらの制限のすべてが、スクリプトが動作するかどうかを直接左右します:
| 制限 |
値 |
結果 |
| 実行タイムアウト |
1000 ms |
超過すると中止され、このパースはスキップされる |
console |
利用不可 |
print デバッグ不可 — デバイスモデル のログを使用 |
async / await |
非対応 |
同期コードのみ |
require / import |
非対応 |
サードパーティパッケージ不可 |
eval |
無効 |
動的コード実行不可 |
| WebAssembly |
無効 |
.wasm の読み込み不可 |
setTimeout / setInterval |
利用不可 |
タイマーや遅延不可 |
process / fs / path |
利用不可 |
呼び出すと ReferenceError がスローされる |
Buffer は制限付きビルド(SafeBuffer)で、6 つの静的メソッドのみを公開します: Buffer.alloc、 Buffer.from、 Buffer.isBuffer、 Buffer.byteLength、 Buffer.compare、 Buffer.concat。 Buffer.allocUnsafe() は明示的にブロックされています。インスタンスの読み取り/書き込みメソッド(readUInt8、 readUInt16LE、…)は通常どおり動作します。
これらのルールを合わせると、1 つの結論に導かれます: デコーダスクリプトは、純粋で同期的な副作用のないバイト操作関数であるべきです。 外部サービスを呼び出したり、データベースに問い合わせたり、デコード中に応答を待ったりするものはここに置くべきではありません。そうした処理はトリガーモデルとRPCモデルに属します。
3.1 注入される変数
パラメータを宣言する必要はありません。プラットフォームが以下を注入します:
| 変数 |
説明 |
device |
デバイスオブジェクト。vm.freezeで凍結され、読み取り専用 |
msg |
生のアップリンクメッセージ本文 |
thingModelId |
現在のデバイスモデル ID |
noticeAttrs |
この実行をトリガーした属性変更を示すフラグ |
org_params |
テナントレベルの環境変数(例: 共有しきい値) |
subDevices |
親デバイスの子デバイスキャッシュマップ |
env / plugins |
プラグインの環境変数とインスタンス(上級者向け) |
Buffer |
制限付きSafeBuffer |
BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper |
すべてのtklHelperクラス。インポートなしでサンドボックスのグローバルとして利用可能 |
最も関連性の高い device デコード用のプロパティ:
| プロパティ |
用途 |
eui |
デバイスEUI。グローバルに一意な主キー |
device_type |
NORMAL / SUB_DEVICE / VIRTUAL |
data_from |
"LoRaWAN" または "OTHER"(直接MQTT) |
online / active_time |
オンライン状態と最終アップリンク時刻 |
shared_attrs |
双方向に同期される属性(インターバル、しきい値) |
server_attrs |
プラットフォーム側のみの属性(アラームしきい値、リンクされたEUI) |
mac_attrs |
LoRaWAN MAC層の属性(バンド、ADR) |
telemetry_data |
デバイスモデルごとの最新テレメトリスナップショット。thingModelIdをキーとする |
注意: device はvm.freezeで凍結されています。代入は例外を投げませんが、効果もありません。 戻り値が唯一の出力チャネルです。 存在しないプロパティはundefinedを返すため、オプショナルチェーン(?.)を全体で使用してください。
3.2 何から読み取るか msg
LoRaWANアップリンクでは、4つのフィールドでほぼすべてのニーズをカバーできます:
| データ |
アクセス |
| ペイロード(Base64) |
msg.userdata.payload |
| ポート |
msg.userdata.port |
| RSSI |
msg.gwrx[0].rssi |
| SNR |
msg.gwrx[0].lsnr |
MQTT経由で直接届くJSONデータの場合は、通常どおりJSON階層をたどります。
3.3 戻り値: 4つのフィールド、間違えるとデータを失う
スクリプトは固定構造を返す必要があります:
| フィールド |
デフォルト |
目的 |
telemetry_data |
null |
ダッシュボード、トレンド、履歴クエリ用のテレメトリ |
shared_attrs |
null |
双方向に同期される属性(インターバル、アドレス、しきい値) |
server_attrs |
null |
プラットフォーム側のみのメタデータ |
sub_device |
null |
子デバイス識別子 — 例: RS-485アドレス。プラットフォームが子デバイスを作成しEUIを割り当てます |
どのフィールドもnullにできますが、 nullはこのアップリンクがその種のデータを生成しないことを意味します。 実務で最も頻繁な間違い: パラメータ応答が telemetry_dataに解析され、報告インターバルが測定値としてプロットされてしまうことです。
sub_device はLoRaWANデプロイで最も過小評価されているフィールドです — セクション5を参照してください。
4. 最初のスクリプト: 手書きのBufferバージョン
ヘルパーライブラリなしの完全なバージョンで、15バイトのフレームをフィールドごとに展開します。これがパーサースクリプトタブの全内容です(裸の関数本体。プラットフォームがラップして呼び出します):
let payload = Buffer.from(msg?.userdata?.payload, "base64");
let port = msg?.userdata?.port || null;
function parseTelemetry(payload) {
// port must be 11, frame tag must be 21 07 03, length must be 15
if (port != 11) { return null }
if (payload[0] != 0x21 || payload[1] != 0x07 || payload[2] != 0x03) { return null }
if (payload.length != 15) { return null }
let telemetry_data = {};
telemetry_data.period_data = payload.readUInt16LE(5);
telemetry_data.status = (payload[7] & 0x01) != 0 ? "fault" : "normal";
telemetry_data.leakStatus = (payload[7] & 0x10) != 0 ? true : false;
telemetry_data.temperature = Number(((payload.readUInt16LE(8) - 1000) / 10).toFixed(2));
telemetry_data.humidity = Number((payload.readUInt16LE(10) / 10).toFixed(2));
let vbat = payload.readUInt8(12);
telemetry_data.vbat = Number(((vbat * 1.6) / 254 + 2.0).toFixed(2));
telemetry_data.rssi = msg.gwrx[0].rssi;
telemetry_data.snr = msg.gwrx[0].lsnr;
return telemetry_data;
}
return {
sub_device: null,
telemetry_data: parseTelemetry(payload),
server_attrs: null,
shared_attrs: null
};
先頭からBase64文字列を入力すると、次が得られます:
{ period_data: 15, status: "normal", leakStatus: false, temperature: 23.8, humidity: 65.1, vbat: 3.40, rssi: -92, snr: 13.2 }
4.1 そのコードに隠された6つの判断
三重の検証。 ポート、フレームタグ、長さ — どれか1つを落とすと、スクリプトはいずれ誤ったフレームを解析します。ポート11は測定データ、214はパラメータ応答、51は透過転送です。同じデバイスが3つすべてでまったく異なるものを送信します。ポートチェックなしでは、3種類のメッセージを1つのパーサーに流し込むことになります。
エンディアンはマニュアルから取らなければなりません。 ここでの温度は readUInt16LEです。代わりに readUInt16BE と書くと、1238が56,594になります — 温度5559.4 °C。エラーは投げず、静かにナンセンスを生成し、通常は顧客の受け入れ時に発見されます。
オフセットと係数は推測ではなくデバイスプロトコルから取ります。 (raw − 1000) / 10 が存在するのは、デバイスが0 °Cを1000としてエンコードし、負の数を避けるためです。 (vbat × 1.6) / 254 + 2.0 8ビットのADC値を2.0–3.6 Vの範囲にマッピングし直します。どちらもデバイスマニュアルから来ています。
ラップ toFixed in Number()。 toFixed は文字列を返します。これをそのまま返すとフロントエンドが値をテキストとして扱い、並べ替えやしきい値比較が壊れます。
RSSI と SNR も一緒に持ち回ります。 これらは msg.gwrx[0]に存在し、ペイロード内にはありません。リンク品質をテレメトリに入れておけば、「デバイス X の値が飛び回っている」と報告があったときに、まずカバレッジを確認でき、現地訪問を省けます。
パース失敗時は null を返し、throw しないでください。 null は「このアップリンクからのテレメトリなし」を意味し、プラットフォームはそれをスキップします。throw するとエラーが書き込まれ、フローが中断されます。たまに発生する破損フレームや他のポート上のフレームには、null が正しいセマンティクスです。
5。2 番目のスクリプト: tklHelper による宣言的な書き換え
手書き版は読みやすいものの、新しいデバイスモデルごとに同じ検証とビットシフトを繰り返すことになります。ThinkLink の tklHelper ライブラリはこれを 3 つの宣言的ブロックに抽象化します: frameInfo (フレームの妥当性)、 appInfo (変数定義)、 paraInfo (パラメータ定義)。
ライブラリのドキュメントはバージョン 1.01.043 (2026-10-12) に同期されています。デバイスモデル、RPC、トリガースクリプト全体で、その 6 つのクラス (BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper) はサンドボックスのグローバルであり、インポートは不要です。
5.1 frameInfo — フレーム検証
| フィールド |
デフォルト |
説明 |
port |
-1 |
ポート番号。正の値の場合、一致しないフレームは破棄されます。-1 はチェックを無効にします |
dataLen |
-1 |
期待されるフレーム長。正の値の場合、一致しないものは破棄されます |
status |
-1 |
ステータスバイトのオフセット。≥ 0 の場合、uint8 として読み取られ tdata.status |
battery |
-1 |
バッテリーバイトのオフセット。≥ 0 の場合、(v×1.6)/254+2.0 で変換され tdata.battery |
rssi |
false |
true の場合、RSSI/SNR を msg.gwrx[0] |
tagList |
[] |
フレームタグのチェック。各 { index, tag }。一致しない場合はフレームを破棄します |
subDevice |
{ index: -1, type: "uint8" } |
子デバイスアドレスの位置と型。値が 0 の場合、フレームを破棄します |
この例の場合:
let frameInfo = {
port: 11,
dataLen: 15,
rssi: true,
tagList: [ { index: 0, tag: 0x21 }, { index: 1, tag: 0x07 }, { index: 2, tag: 0x03 } ]
};
前節の三重検証が、4 行の宣言的な記述になります。
5.2 appInfo — 変数定義
| パラメータ |
説明 |
name / field_name |
表示名 / プログラム上のフィールド名 ( tdata内のキー) |
unit |
単位文字列 |
index |
開始バイトオフセット |
type |
型文字列 (下記参照) |
coefficient |
乗数 — 数値、または 2 パス目の乗算用に別フィールドの名前 |
decimal |
小数桁数、デフォルト 0 |
illegal |
">0x80" や "=0xFF" などの妥当性チェック。一致した場合、そのフィールドは黙って破棄されます |
options |
値マップ、例: { 0: "normal", 1: "fault" } |
postProcess |
入力として value を用いる JS 式文字列 |
サポートされる型は十分に広く、ビット操作が必要になることはほとんどありません:
| カテゴリ |
型文字列 |
| 整数 |
int8 / uint8、int16 / uint16 (デフォルトはビッグエンディアン)、int16le / uint16le、int24 / uint24、int32 / uint32 (le バリアントあり)、int40–int64 シリーズ |
| 浮動小数点 |
floatbe / floatle (IEEE 754 単精度)、floatcdab (バイト順 2-3-0-1、Modbus デバイスで一般的)、intcdab |
| エンコードされた数値 |
bcdbe<N> / bcdle<N>(BCD)、hexbe<N> / hexle<N>(16進文字列) |
| ビットフィールド |
bitbe<S>-<E> / bitle<S>-<E> — ビット範囲を直接抽出 |
| ブール値 |
bool<N> (1/0 を返す)、boolean<N> (true/false を返す) |
| 文字列 |
string<N> (N バイトの ASCII) |
型文字列は大文字小文字を区別しません。 floatcdab と intcdab は Modbus デバイスのバイト順の罠のために存在します。多くのメーターは 32 ビット値を 2-3-0-1 として格納しており、手作業では間違えやすいですが、宣言するのは簡単です。
ビットフィールド型はもう一つのよくあるケースを解決します: 8 つのフラグを保持する 1 つのステータスバイトです。 bitle0-0 を障害ビットに、 bitle4-4 をリークビットに使えば、 & 0x01 や & 0x10のようなマスクでゼロを数える必要はありません。
5.3 完全な宣言版
let frameInfo = { port: 11, dataLen: 15, rssi: true, tagList: [ { index: 0, tag: 0x21 }, { index: 1, tag: 0x07 }, { index: 2, tag: 0x03 } ] };
let appInfo = [
{ name: "Status", field_name: "status", unit: "", index: 7, type: "bitle0-0" },
{ name: "Leak", field_name: "leakStatus", unit: "", index: 7, type: "bitle4-4" },
{ name: "Temperature", field_name: "temperature", unit: "C", index: 8, type: "uint16le", postProcess: "value = (value - 1000) * 0.1; return Number(value.toFixed(1))" },
{ name: "Humidity", field_name: "humidity", unit: "%", index: 10, type: "uint16le", coefficient: 0.1, decimal: 1 },
{ name: "Battery", field_name: "vbat", unit: "V", index: 12, type: "uint8", postProcess: "return Number(((value * 1.6) / 254 + 2.0).toFixed(2))" }
];
let payParser = new PayloadParser({ device, msg, frameInfo, appInfo });
let tdata = payParser.telemetry();
return { sub_device: null, telemetry_data: tdata, server_attrs: null, shared_attrs: null };
5.4 どちらのスタイルをいつ使うか
| シナリオ |
推奨スタイル |
理由 |
| 固定フレーム構造、規則的なフィールド |
宣言型のtklHelper |
40行未満で、検証が組み込まれており、フィールドの変更はテーブルの編集で行える |
| 不規則な構造、動的なフィールド位置 |
手書きのBuffer |
宣言型の設定では条件分岐を表現できない |
| 1つのフレームに複数のセンサーを格納 |
MSparser サブクラス |
IDごとにオフセットを自動で進める(5.5を参照) |
| フィールド間の計算 |
手書きまたは postProcess |
coefficient フィールド参照をサポート。複雑なロジックには依然としてコードが必要 |
PayloadParser.telemetry() 固定の順序で実行される: port → dataLen → tagList → subDevice を検証し、その後 status / battery / rssi と各 appInfo フィールドを抽出し、illegal → coefficient → decimal → postProcess → options を適用する。他のフィールドを参照する係数については2回目のパスを行う。この順序を知ることが、「なぜoptionsが反映されなかったのか」をデバッグする方法である(optionsは decimal の後に適用され、スキップされた postProcessの前には適用されない)。
5.5 MSparser — 1つのフレームに複数のセンサー
一部のコレクターは、複数の測定点を繰り返し [ID | field data...] ブロック。 MSparser extends PayloadParser と requires extraData:
new MSparser({ device, msg, frameInfo, extraData })
読み取る各IDについて、 user_0x<ID>_<j> を extraData 内で検索し(j = 0..3、IDごとに最大4フィールド)、オフセットを自動的に進める。 frameInfo.idType IDの読み取り方法を設定する。通常は"uint16be"。
6。パラメータ応答: port 214 と共有属性
デコーダースクリプトは測定値だけを扱うわけではない。デバイス設定(間隔、しきい値、キャリブレーションオフセット)は port 214で送信され、それらの応答も解析する必要がある — 出力先は shared_attrsであり、 telemetry_dataではない。
KS52のパラメータアドレス(COV = 変化量しきい値、offset = キャリブレーション):
| パラメータ |
アドレス(10進 / 16進) |
型と単位 |
適用対象 |
| 温度COV |
144 / 0x90 |
uint8 × 0.1 °C |
KS52, KS53 |
| 湿度COV |
145 / 0x91 |
uint8 × 0.1 %RH |
KS52のみ |
| 温度オフセット |
146 / 0x92 |
int8 × 0.1 °C |
KS52, KS53 |
| 湿度オフセット |
147 / 0x93 |
int8 × 0.1 %RH |
KS52のみ |
| 測定間隔 |
142 / 0x8E |
uint16 秒 |
KS52, KS53 |
tklHelperでは、パラメータは paraInfo 内で <segment>_<address> キーを使用して宣言する。ここでsegmentは app / cf / fw / radio / ds / status のいずれか:
let paraInfo = {
app_142: { name: "Measure interval", field_name: "measurePeriod", unit: "s", type: "uint16le" },
app_144: { name: "Temperature COV", field_name: "covTemperature", unit: "C", type: "uint8", coefficient: 0.1, decimal: 1 },
app_145: { name: "Humidity COV", field_name: "covHumidity", unit: "%", type: "uint8", coefficient: 0.1, decimal: 1 },
app_146: { name: "Temperature offset", field_name: "offsetTemperature", unit: "C", type: "int8", coefficient: 0.1, decimal: 1 }
};
let payParser = new PayloadParser({ device, msg, frameInfo, appInfo, paraInfo });
let tdata = null, pdata = null;
if (port === 214) { pdata = payParser.paras(); } else { tdata = payParser.telemetry(); }
return { sub_device: null, telemetry_data: tdata, server_attrs: null, shared_attrs: pdata };
paras() はport 214でのみ動作し、それ以外ではnullを返すため、portのチェックは必須である。
6.1 オフセットはCOVではない
この2つはしばしば混同され、その結果「キャリブレーション後にデータが悪化した」という事態になる:
| 概念 |
動作内容 |
単位 |
| COV(変化量) |
送信する タイミングを決定する — 変化がしきい値を超えた場合のみ送信 |
0.1 °C / 0.1 %RH |
| オフセット |
測定値そのものを キャリブレーションする |
0.1 °C / 0.1 %RH |
オフセットは符号付き8ビットで、範囲は−128から127、すなわち0.1刻みで−12.8から+12.7である。負の値は2の補数を使用する:−1.0 °Cは F6として書き込まれる。読み戻し時には、128以上のバイトは0.1を掛ける前に256を引く必要がある — これはデコーダーで明示的に処理しなければならず、そうしないとすべての負のキャリブレーションが黙って正の値になってしまう。
もう1つのフィールドの教訓:オフセットはデバイス側のパラメータである。プラットフォーム側でも補正すると、誤差が倍になる。
7. sub_device:1台のDTUをN台のデバイスに変える
sub_device は、既存設備のレトロフィットにおけるThinkLinkの最も実用的な設計判断です。
典型的な構成は次のとおりです。KC11(DINレール、RS-485、AC 85~270 V)またはKC25 DTUがLoRaWAN経由で報告し、1つのフレームに複数のRS-485スレーブ(電力量計、水道メーター、温湿度伝送器)のデータが含まれます。DTUは1台のLoRaWANデバイスですが、その背後には8または16台の物理メーターが存在します。
デコーダーは単純に次を返します。
return { sub_device: "03", telemetry_data: tdata, server_attrs: null, shared_attrs: null };
sub_device 子デバイスのバスアドレス(ここではRS-485アドレス)を保持します。その後ThinkLinkは デバイス管理で仮想子デバイスを作成し、一意のEUIを割り当てます。その結果は次のとおりです。
- 親(DTU)はLoRaWANのIDとリンク属性を保持します
- 各RS-485スレーブは、個別に照会・アラーム・グラフ化できるオブジェクトになります
- 上流システムは、どのDTUがデータをパッケージしたかを知る必要がありません
これが「既存の有線資産を維持し、最後のホップだけを置き換える」を実際に機能させる技術的前提です。 LoRaWANは無線区間を解決しますが、デバイス資産管理は依然として物理メーターごとに展開されます。
frameInfo.subDevice ( index >= 0の場合、アドレスを次で読み取ります type から tdata.addrへ。0の値はフレームを破棄します)はこのフィールドと連携して、フレームから直接スレーブアドレスを抽出します。
8。デコード後:フィールドからビジネス価値へ
スクリプトの実行は第一歩にすぎません。成果物としてのデバイスモデルには、さらに3つが必要です。
8.1 表示フィールド
どのフィールドをUIに表示するかを宣言します。設定項目は次のとおりです。
| 項目 |
説明 |
| フィールド名 |
次のキーと一致する必要があります telemetry_data 正確に — 大文字と小文字を区別します |
| エイリアス |
フロントエンド表示名 |
| 単位 |
°C、%、V、m³、… |
| 型 |
number / string / boolean / object — 数値フィールドにはnumberを選択して書式設定が機能するようにします |
| 順序 |
テーブルやカードでの表示順 |
| アイコン |
任意。埋め込みSVGに対応 |
フィールド名の不一致は、「スクリプトにはデータがあるのにページが空白」の最大の原因です。
8.2 アラーム
アラームは トリガーモデルに属し、デバイスモデルには属しません。推奨パターンは、1つのトリガースクリプトで作成とクリアを処理します。デフォルトは action 「clear」とし、条件が満たされた場合のみ「new」に切り替えます。これにより、アップリンクのたびに条件が正常に戻ったアラームがクリアされ、手動介入は不要です。
tklHelperは TriggerHelper 基底クラスを提供します — これを拡張してオーバーライドしてください _alarmProcess(info) トリガーの戻りオブジェクト全体を手書きする代わりに。 Utils.LEVEL は4つのレベル(low / mid / high / urgent)を定義し、 Utils.ACTION はno / new / clearを定義します。
8.3 BACnetおよびModbusマッピング
これはビルおよび産業の引き渡しにおける最終ステップです。
BACnet: デバイスモデルにBACnetフィールドを定義します。フィールド名はデバイスモデル定義と一致させ、さらにオブジェクトタイプ(Analog Input、Binary Output、…)、単位、COV増分、デフォルト値、リンクされたRPCを指定します。次に Operations → BACnetに移動し、増分生成をクリックして対象のデバイスモデルを選択すると、プラットフォームがオブジェクトタイプ、インスタンス番号、オブジェクト名、説明、データ型、単位、読み書き権限を含むポイントリストを生成します — BMSインテグレーターに渡す準備が整います。試運転時にはYABE(Yet Another BACnet Explorer)を使用して、データが正しく公開されていることを確認してください。
Modbus: デバイスモデルのModbusマッピングは、テレメトリフィールドをレジスタアドレスで公開し、SCADAマスターがModbus TCP経由で読み取れるようにします。逆方向の制御はRPCモデルで処理されます。 RPCHelper は次を提供します buildmodbusFrame06 (FC06 単一レジスタ書き込み)、 buildmodbusFrame10 (FC10 複数書き込み)、 buildmodbusFrameRead (FC03/FC04 読み取り)、CRC-16/Modbusを内蔵。
一言で言えば: デコーダーはデータを読み出せるかどうかを決め、表示フィールド、アラーム、プロトコルマッピングはビジネスがそれを活用できるかどうかを決めます。
9. なしでのデバッグ console
サンドボックスには console.logがありません。これが始めたばかりのときに最も難しい調整点です。4つの手法がそれを置き換えます。
デバイスモデルエディターのテストタブ。 サンプルのBase64ペイロードとポートを入力すると、解析結果がすぐに表示されます。通常のテストではデバイスが存在する必要があり、そうでない場合プラットフォームは「Device not found」を返します。
フラグ。 skipDeviceCheck フラグ。 実際のデバイスなしでスクリプトロジックのみを検証したい場合(たとえばNFCテンプレートを設定するとき)、テストアクションは次をサポートします skipDeviceCheck = true。プラットフォームはEUI検索をスキップし、合成プレースホルダーデバイス(eui「empty」、現在のテナント所有)を注入するため、一致するデバイスがなくてもテストが完了します。
デバイスモデルログ。 ランタイム出力と例外はここに記録され、consoleを置き換えます。
バージョン履歴と復元。 保存のたびにスナップショットが記録されます。エディターで履歴を開き、バージョンを確認し、隣接バージョンと比較し、復元できます — 復元は 新しい バージョンレコードを作成するため、履歴が失われることはありません。RPCモデル、デバイスモデル、トリガーモデル、フォワーダースクリプトエディターはすべてこれを共有します。自由に反復でき、間違いは元に戻せます。
もう1つのショートカット: まず公開モデルライブラリを確認してください。 次の場所にあります Marketplace plugins → Model Center 「Organisation models」と「Public models」の2つのタブがあります。一般的なデバイスタイプには通常すでにデバイスモデルがあるため、ゼロから始めるのではなくコピーして適応してください。
10。既存スクリプトの持ち込み
チームが別のLoRaWANプラットフォームでパーサースクリプトをすでに作成している場合、書き直す必要はありません。ThinkLinkデバイスモデルは次を提供します ChirpStack互換モード — インターフェースはすでに適応されているため、元のパーサーコードをそのまま貼り付けて機能します。
これは移行コストの規模ゆえに重要である。Eclipse FoundationとPW Consultingの調査によると 移行コストは移行予算全体の35%~60%を占める。デコーダスクリプトはLoRaWANアプリケーション層で最も扱いにくい部分であり、自動検証が最も難しく、書き換え時に新たなバグを持ち込みやすい箇所である。そのまま流用できる部分が多いほど、抱え込む不確実性は小さくなる。
エンジニアのコストがおおよそ時給50米ドルの場合、中規模プロジェクト(10種類のデバイス)向けにデコーダスクリプトをゼロから作成・調整するには通常40~60時間、すなわち 2,100~3,100米ドル既存のスクリプトを再利用すれば、それを約三分の一に削減できます。 節約できるのは入力の時間ではなく、現場検証の時間である。これこそがLoRaWANプロジェクトにおける真のコスト要因である。
11. 現場での出現頻度順に並べた8つの誤り
1. エンディアンの逆転。 1238が56594として読み取られ、5559.4 °Cとなる。エラーは出ず、ただ無意味な値になるだけで、通常は受け入れ時に発覚する。マニュアルで確認すること。両方を試して推測してはならない。
2. 2の補数の忘れ。 符号付きフィールド(int8のオフセット、int16の温度)は、生値が≥128 / ≥32768の場合に256 / 65536を減算する必要がある。これがないと、すべての負値が正値になってしまう。
3. toFixed を付けずに Number()を使用する。 文字列が返され、ソートやしきい値チェックが黙って壊れる。
4. ポートチェックの欠如。 ポート11(測定)、214(パラメータ)、51(透過)はまったく異なる構造を持つ。分岐がないと、3種類のメッセージが1つのパーサに流れ込む。
5. 長さチェックの欠如。 ファームウェア更新でフレーム長が変わった後、あるいは別モデルのフレームが届いた場合、チェックのないパーサは範囲外を読み取る。SafeBufferはfalseを返すか例外を投げるかもしれないが、有効だが誤ったオフセットを読み取る方がさらに悪い。
6. パラメータ応答を telemetry_dataに書き込んでしまう。 報告間隔が測定値としてプロットされ、トレンドチャートが台無しになる。パラメータは shared_attrsに、測定値は telemetry_dataに入れるべきである。
7. 表示フィールド名がスクリプト出力と一致しない。 「データはあるのにページが空白」の最大の原因。フィールド名は大文字と小文字を区別する。
8. 動作するスクリプトを完成した仕事とみなす。 表示フィールド、Modbusマッピング、BACnetフィールドは3つの別個の設定であり、スクリプトはマッピングを意味しない。BACnetポイントリストは別途生成する必要がある。
12. この層を正しく仕上げる
LoRaWANにおける役割分担は明確で、逆にはできない: エンドデバイスは可能な限り圧縮し、ネットワークがそれを運び、プラットフォームがそれをビジネス上の意味に復元する。
デコーダは3層の中で最も地味に見える——15バイト、40行のJavaScript、30分の作業。しかし、チェーン全体の最終的な形を決める:
- それは決める バッテリーがどれだけ持つか (より厳密なエンコード、より少ないエアタイム)——前回の記事の2700 mAhセルを再訪しよう:SF7からSF12に下げると送信時間は24.9倍になる
- それは決める ビジネスシステムがデータを消費できるか (フィールド名、単位、型はここで固定される)
- それは決める ポイントリストを引き渡せるか (BACnet / Modbusマッピングは同じフィールド定義に依存する)
- それは決める 既存資産をデバイス単位で管理できるか (
sub_device は1台のDTU背後の16台のメーターを16個の独立したオブジェクトにする)
ManThinkの製品ラインはこのチェーンを中心に構築されている:KS51 / KS52 / KS53 / KS61センサーがコンパクトなフレームを生成し、GD61x / GDI51 / GDI52 / GD73 / GDO51ゲートウェイがそれを運び、KC11 / KC25 / KC21が既存のRS-485メーターを接続し、ThinkLinkがプラットフォーム側でデコード、アラーム、プロトコルマッピング、転送を処理する。
ThinkLinkの3つのデプロイ形態——Cloud(1,000デバイスまで無料)、TKEオンプレミス(1,000デバイス、無制限のデータ処理と保存)、ゲートウェイに組み込まれたTKG(100デバイス、1台のゲートウェイが完全なシステム)——はすべて 同じデバイスモデル、RPCモデル、トリガールールを共有し、相互に移行できる。
これは特にデコーダスクリプトにとって重要である: Cloudで書いたデバイスモデルはTKEやTKGでもそのまま動作する。 スクリプトは資産であり、1つのデプロイ形態の付属品ではない。
まとめ
- LoRaWANにおけるコンパクトなバイナリは好みではなく必然である:SF7では、15バイトは46.3 msかかるのに対し、120バイトのJSONペイロードは199.9 msかかる(4.3倍);CN470 DR0はペイロードを15バイトに制限する
- ThinkLinkでのデコードは デバイスモデルにあり、vm2サンドボックスで動作する:1000 msのタイムアウト、consoleなし、asyncなし、requireなし、戻り値が唯一の出力チャネル
- 各戻りフィールドには役割がある:測定値は
telemetry_dataへ、設定は shared_attrsへ、子アドレスは sub_device
- tklHelperは宣言的なスタイルを提供する:
frameInfo は検証用、 appInfo はフィールド用、 paraInfo はパラメータ用で、Modbusデバイスで一般的なfloatcdabバイトオーダーを含む30以上のデータ型を備える
- ポート214のパラメータ応答には独自の分岐が必要;オフセットとCOVは異なる概念であり、負のオフセットには2の補数の処理が必要
sub_device はDTU背後のすべてのRS-485メーターを独立したデバイスにする——既存設備の改修の前提である
- デコード後、表示フィールド、アラームルール、BACnet/Modbusマッピングを設定する;動作するスクリプトは納品完了ではない
- 既存のパーサスクリプトは互換モードで再利用できる;節約できるのは主に現場検証の時間である
次の記事では トリガーモデルとアラームルールを扱う:データがデコードされた後、値が範囲外になった瞬間に適切な人に確実に届くようにする方法について。
参照:ThinkLink ドキュメント — デバイスモデル https://www.think-link.net/docs/zh/Model/ThingModel.html、tklHelper https://www.think-link.net/docs/zh/Model/tklHelper.html、RPC Model https://www.think-link.net/docs/zh/Model/RPCModel.html、Trigger Model https://www.think-link.net/docs/zh/Model/TriggerModel.html、KS52 センサーマニュアル https://www.think-link.net/docs/zh/Device/LoRaWANSensor/ks52.html。エアタイムの数値は Semtech LoRa Calculator(SX1262、BW125 kHz、CR 4/5)で計算。