この記事は
SORACOM Advent Calendar 2020の21日目の記事で、SORACOM OrbitでLTE-M Buttonのデータを触ってみて、実装の際にAssemblyScriptを使ってみたという内容になります。
Orbitを使ってみたという記事はQiitaにもいくつかあるんですがいずれもC++で、AssemblyScriptでやったという記事がほとんど見当たらず、実際にやってみたらちょっと躓いたところもあったのでその辺りを書こうと思います。
なお、対象となるボタンはLTE-M Button for Enterprise(しろボタン)またはLTE-M Button plus(ひげボタン)となります。
SORACOM Orbitとは
SORACOM Orbitは「インラインプロセッサ」と呼ばれるサービスです。デバイスからSORACOMプラットフォームに渡されたデータが、Beam/Funk/Funnel/Harvestなどの各種SORACOMサービスに渡る前にデータを加工することができるサービスで、その加工する処理を自作することができます。とても賢いバイナリーパーサを自作できる、と言うとSORACOM好きの人には伝わるでしょうか?
処理するプログラム(soraletと呼ばれます)はユーザがWASMで作成し、それがSORACOMプラットフォーム上で動くという、FaaSっぽいかんじなのが個人的には痺れます。
SORACOM Orbitの開発環境を整える
公式ドキュメントに従って進めます。環境の構築自体は特にハマるところはないかと思います。
構築できたらdev-Container内のassemblyscript/assembly/index.ts
を参考に修正していきます。
入力データをJSONにする
(2021/5/31追記)
2021年5月のバージョンアップで、この入力データ用のクラス作成作業は不要になりました。2021年5月以降のバージョンのSDKであれば、
let data: JSON.Obj = <JSON.Obj>(JSON.parse(getInputBufferAsString()));
で取得できるようになっています。
2021年5月以前のSDKで準備されてるJSONDecoderで入力データ(デバイスからSORACOMプラットフォームに渡されたデータ)をJSONに変換して扱うには、まず受信するデータに合わせたクラスを作成します。
今回はボタンが対象となりますので、ボタンで送信されるデータに合わせて以下のようなクラスを定義します。index.tsの末尾に記載しましょう。
JSONHandlerクラスを継承して、メンバーで利用する型に合わせてメソッドsetXXを実装します。各メソッドの中では引数で渡されたメンバー名に従って、値を設定します。
class ButtonData extends JSONHandler {
public clickType: i64;
public clickTypeName: string;
public binaryParserEnabled: boolean;
public batteryLevel: f64;
setInteger(name: string, value: i64): void{
if(name == "clickType") {
this.clickType = value;
} else if(name == "batteryLevel") {
this.batteryLevel = value as f64;
}
}
setString(name: string, value: string): void {
if (name == "clickTypeName") {
this.clickTypeName = value;
}
}
setBoolean(name: string, value: boolean): void{
if (name == "binaryParserEnabled"){
this.binaryParserEnabled = value;
}
}
setFloat(name: string, value: f64): void {
if (name == "batteryLevel") {
this.batteryLevel = value;
}
}
}
そして、実際に入力データ(JSONをシリアライズした文字列)をこのクラスのインスタンスにデシリアライズするのは、サンプルのコードの通り以下のようになります。
const data = new ButtonData();
const decoder = new JSONDecoder<ButtonData>(data);
decoder.deserialize(getInputBuffer());
ここで私が最初うまく動かなかったのがbatteryLevel
の取り扱いです。batteryLevelの値は0.25 / 0.5 / 0.75 / 1となるのでメンバーをfloat(f64)で定義し、値をセットするハンドラーとしてsetFloat()
だけを準備していたですが、正常に値が設定されませんでした。
これは、入力で「1」という値が渡ってくるとJSONDecoderがこれを「integer」だと判断してsetInteger("batteryLevel",1)
という形でハンドラーを呼び出すためです。ちなみに該当のコードはassemblyscript/assembly/lib/decoder.ts
の254行目辺りです。
if (digits > 0) {
if (numStr.indexOf(".") > 0) {
this.handler.setFloat(this.state.lastKey, parseFloat(numStr) * (sign as f64));
} else {
this.handler.setInteger(this.state.lastKey, (parseInt(numStr) as i64) * sign);
}
return true;
}
JSONDecoder.deserialize()は文字列で入ってきた入力値をパースしていって数値があった場合、小数点があればsetFloat()
ハンドラーを、なければsetInteger()
ハンドラーを呼んでいます。
このため、ハンドラーとして呼び出されるsetInteger()
メソッドとsetFloat()
メソッドの両方にbatteryLevel
の値を設定するコードを準備し、setInteger()
ではcastしてデータを設定するようにする必要があります。
入力データを出力側にコピーする
まずは入力がそのまま出力に渡るように、先ほど作成したButtonData
型に入力をJSONDecoderで取り込み、出力データのJSONEncoder
型にコピーしてやります。
const encoder = new JSONEncoder();
encoder.setInteger("clickType", data.clickType);
encoder.setString("clickTypeName", data.clickTypeName);
encoder.setFloat("batteryLevel", data.batteryLevel);
encoder.setBoolean("binaryParserEnabled", data.binaryParserEnabled);
入力データを追加する
そして、いよいよデータをいじってみましょう。とりあえず今回はサンプルにもあるコードをそのまま使い、データに簡易位置情報を追加してみましょう。
簡易位置情報はそのままでも例えばBeamであればHTTPヘッダなどで受け取れますが、この処理をすることでリクエストボディに埋めることが出来ます。
const location = getLocation();
encoder.setFloat("lat", location.lat);
encoder.setFloat("lon", location.lon);
soraletをアップロードする
最終的なコードは以下のようになります。これをビルドして、wasmファイルをsoraletとして登録します。
import {
getInputBuffer,
setOutputJSON,
log,
getLocation,
getTagValue,
getSourceValue,
getTimestamp,
getUserdata,
} from "orbit-sdk-assemblyscript";
import { JSONDecoder, JSONHandler } from "./lib/decoder";
import { JSONEncoder } from "./lib/encoder";
export function uplink(): i32 {
const data = new ButtonData();
const decoder = new JSONDecoder<ButtonData>(data);
decoder.deserialize(getInputBuffer());
const encoder = new JSONEncoder();
encoder.setInteger("clickType", data.clickType);
encoder.setString("clickTypeName", data.clickTypeName);
encoder.setFloat("batteryLevel", data.batteryLevel);
encoder.setBoolean("binaryParserEnabled", data.binaryParserEnabled);
const location = getLocation();
encoder.setFloat("lat", location.lat);
encoder.setFloat("lon", location.lon);
setOutputJSON(encoder.toString());
return 0;
}
class ButtonData extends JSONHandler {
public clickType: i64;
public clickTypeName: string;
public binaryParserEnabled: boolean;
public batteryLevel: f64;
setInteger(name: string, value: i64): void{
if(name == "clickType") {
this.clickType = value;
} else if(name == "batteryLevel") {
this.batteryLevel = value as f64;
}
}
setString(name: string, value: string): void {
if (name == "clickTypeName") {
this.clickTypeName = value;
}
}
setBoolean(name: string, value: boolean): void{
if (name == "binaryParserEnabled"){
this.binaryParserEnabled = value;
}
}
setFloat(name: string, value: f64): void {
if (name == "batteryLevel") {
this.batteryLevel = value;
}
}
}
ボタンの所属するSIMグループでOrbitの設定をします。Airで簡易位置情報をONにし、Orbitでも簡易位置情報をONにしておきます。
実行してみる
それでは実行してみましょう。データがどういう形になったかを確認するために、HarvestDataをONにして、ボタンをポチッとします。
HarvestDataで確認すると、データはこんな感じになっています。
{
"clickType": 1,
"clickTypeName": "SINGLE",
"batteryLevel": 1.0,
"binaryParserEnabled": true,
"lat": 3?.************,
"lon": 13?.***************
}
OrbitをONにする前のデータと比較してみましょう。
{
"clickType": 1,
"clickTypeName": "SINGLE",
"batteryLevel": 1,
"binaryParserEnabled": true
}
Orbitによって簡易位置情報が追加されているのと、batteryLevelの値がFloatで取り扱っているので1.0
になっているのが分かります。
まとめ
Orbitでassemblyscriptを使う場合、入力データの型を意識してちゃんとクラスを準備しないといけないよ、というお話でした。
実際の手順を簡単にまとめると
- 入力に応じたメンバーを持ったクラスを、JSONHandlerクラスを継承して作成する
- 各メンバーの型ごとのsetXXメソッドをオーバーライドする
- Float(f64)型のメンバーは、setFloatだけでなくsetIntegerメソッドにもsetterを準備する
ということになります。今回はボタンでしたが、GPSマルチユニットや他のデバイスでも同様です。
これからOrbitを触ってみたい!という皆さんの参考になれば幸いです。