SORACOM Orbitでボタンのデータを触ってみた(AssemblyScript編)

# この記事は

[SORACOM Advent Calendar 2020](https://qiita.com/advent-calendar/2020/soracom)の21日目の記事で、[SORACOM Orbit](https://soracom.jp/services/orbit/)で[LTE-M Button](https://soracom.jp/products/gadgets/)のデータを触ってみて、実装の際にAssemblyScriptを使ってみたという内容になります。

Orbitを使ってみたという記事はQiitaにもいくつかあるんですがいずれもC++で、AssemblyScriptでやったという記事がほとんど見当たらず、実際にやってみたらちょっと躓いたところもあったのでその辺りを書こうと思います。

なお、対象となるボタンはLTE-M Button for Enterprise(しろボタン)またはLTE-M Button plus(ひげボタン)となります。

# SORACOM Orbitとは

[SORACOM Orbit](https://soracom.jp/services/orbit/)は「インラインプロセッサ」と呼ばれるサービスです。デバイスからSORACOMプラットフォームに渡されたデータが、Beam/Funk/Funnel/Harvestなどの各種SORACOMサービスに**渡る前に**データを加工することができるサービスで、その加工する処理を自作することができます。とても賢いバイナリーパーサを自作できる、と言うとSORACOM好きの人には伝わるでしょうか？

処理するプログラム(soraletと呼ばれます)はユーザがWASMで作成し、それがSORACOMプラットフォーム上で動くという、FaaSっぽいかんじなのが個人的には痺れます。

# SORACOM Orbitの開発環境を整える

[公式ドキュメント](https://dev.soracom.io/jp/orbit/what-is-orbit/)に従って進めます。環境の構築自体は特にハマるところはないかと思います。

構築できたらdev-Container内の`assemblyscript/assembly/index.ts`を参考に修正していきます。

# 入力データをJSONにする

(2021/5/31追記)
[2021年5月のバージョンアップ](https://blog.soracom.com/ja-jp/2021/05/31/soracom-orbit-sdk-2021-05/)で、この入力データ用のクラス作成作業は不要になりました。2021年5月以降のバージョンのSDKであれば、

```typescript
let data: JSON.Obj = <JSON.Obj>(JSON.parse(getInputBufferAsString()));
```

で取得できるようになっています。

2021年5月以前のSDKで準備されてるJSONDecoderで入力データ(デバイスからSORACOMプラットフォームに渡されたデータ)をJSONに変換して扱うには、まず受信するデータに合わせたクラスを作成します。

今回はボタンが対象となりますので、ボタンで送信されるデータに合わせて以下のようなクラスを定義します。index.tsの末尾に記載しましょう。
JSONHandlerクラスを継承して、メンバーで利用する型に合わせてメソッドsetXXを実装します。各メソッドの中では引数で渡されたメンバー名に従って、値を設定します。

```typescript:TypeScript
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をシリアライズした文字列)をこのクラスのインスタンスにデシリアライズするのは、サンプルのコードの通り以下のようになります。

```typescript:Typescript
  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行目辺りです。

```typescript:Typescript
    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`型にコピーしてやります。

```typescript:Typescript
  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ヘッダなどで受け取れますが、この処理をすることでリクエストボディに埋めることが出来ます。

```typescript:Typescript
  const location = getLocation();
  encoder.setFloat("lat", location.lat);
  encoder.setFloat("lon", location.lon);
```

# soraletをアップロードする

最終的なコードは以下のようになります。これをビルドして、wasmファイルをsoraletとして登録します。

```typescript:Typescript
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で確認すると、データはこんな感じになっています。

```json:JSON
{
  "clickType": 1,
  "clickTypeName": "SINGLE",
  "batteryLevel": 1.0,
  "binaryParserEnabled": true,
  "lat": 3?.************,
  "lon": 13?.***************
}
```

OrbitをONにする前のデータと比較してみましょう。

```json:JSON
{
  "clickType": 1,
  "clickTypeName": "SINGLE",
  "batteryLevel": 1,
  "binaryParserEnabled": true
}
```

Orbitによって簡易位置情報が追加されているのと、batteryLevelの値がFloatで取り扱っているので`1.0`になっているのが分かります。

# まとめ

Orbitでassemblyscriptを使う場合、入力データの型を意識してちゃんとクラスを準備しないといけないよ、というお話でした。
実際の手順を簡単にまとめると

- 入力に応じたメンバーを持ったクラスを、JSONHandlerクラスを継承して作成する
- 各メンバーの型ごとのsetXXメソッドをオーバーライドする
- Float(f64)型のメンバーは、setFloatだけでなくsetIntegerメソッドにもsetterを準備する

ということになります。今回はボタンでしたが、GPSマルチユニットや他のデバイスでも同様です。

これからOrbitを触ってみたい！という皆さんの参考になれば幸いです。