[SORACOM Orbit]TinyGoでWASMを試してみた
こんにちは、CX事業本部のうらわです。
以前書いた記事ではC++/EmascriptenでWASMモジュールを作成し、SORACOM Orbit(以下、orbit)を試してみました。
今回はTinyGoで試してみます。
作業環境
Macで作業します。
SORACOM CLIが認証含め利用できる状態を前提とします。
参考: Getting Started: SORACOM CLI をインストールし SIM カードの一覧を取得する | SORACOM CLI 利用ガイド | SORACOM Users
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H15 $ soracom version SORACOM API client v0.7.0
準備
TinyGo
TinyGoをHomebrewでインストールします。
$ brew tap tinygo-org/tools $ brew install tinygo $ tinygo version tinygo version 0.17.0 darwin/amd64 (using go version go1.15.2 and LLVM version 11.0.0)
orbit-development-environment
WASMモジュール開発環境のセットアップのページからSDKのzipファイルをダウンロードし、任意のディレクトリで解凍しておきます。以降はtinygo
ディレクトリ内で作業します。
$ unzip orbit-development-environment-2020-11.zip $ cd tinygo
サンプルコードでorbitを試す
まずはサンプルコードをビルドしorbitをテストします。簡単に試すために、22-34の不要な行をコメントアウトしておきます。
package main import ( "strconv" sdk "github.com/soracom/orbit-sdk-tinygo" ) // application entry point, but the orbit runtime never executes this. func main() { } //export uplink func uplink() sdk.ErrorCode { inputBuffer, err := sdk.GetInputBuffer() if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Input Buffer: " + string(inputBuffer) + "\n") // tagValue, err := sdk.GetTagValue("name") // if err != nil { // sdk.Log(err.Error()) // return -1 // } // sdk.Log("Name: " + string(tagValue) + "\n") // // sourceValue, err := sdk.GetSourceValue("resourceType") // if err != nil { // sdk.Log(err.Error()) // return -1 // } // sdk.Log("Resource type: " + string(sourceValue) + "\n") timestamp := sdk.GetTimestamp() if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Timestamp: " + strconv.FormatInt(timestamp, 10) + "\n") sdk.SetOutputJSON("{\"message\": \"Hello from Orbit with TinyGo\"}") return sdk.ErrorCode(0) }
make release
でbuild/soralet-optimized.wasm
が作成されます。
$ make release
SORACOM CLIを利用してsoraletの作成、WASMモジュールのアップロード、WASMモジュールのテストを行います。
$ soracom soralets create --soralet-id my-soralet $ soracom soralets upload \ --soralet-id my-soralet \ --content-type application/octet-stream \ --body @build/soralet-optimized.wasm $ cat << EOF > test.json { "source": { "resourceType": "Subscriber", "resourceId": "295050919999999999" }, "payload": "{\"value\":23.54,\"name\":\"sorao\"}" } EOF $ soracom soralets exec --soralet-id my-soralet \ --version 1 \ --direction uplink \ --content-type application/json \ --body @test.json { "body": "{\"message\": \"Hello from Orbit with TinyGo\"}", "contentType": "application/json", "encodingType": "plain", "resultCode": 0 }
SORACOMユーザーコンソールにアクセスし、メニューから SORACOM Orbit > Soralet管理でSoralet一覧を確認します。先ほど作成したsoraletを選択すると、LOGSにtest.json
内のpayloadがログとして出力されていることがわかります。
ここまではorbitの基本的な動作を確認しました。次はSDKに付属しているサンプルコードを改修し、いろいろ試してみます。
base64デコード
SORACOM GPSマルチユニットから送信されるデータのpayloadはbase64でエンコードされています。以下はSORACOM Harvestでダウンロードしたjsonの例です。
[ { "resourceType": "Subscriber", "resourceId": "000000000000000", "time": "2021/4/1 20:30:50", "contentType": "application/json", "content": { "payload": "eyJsYXQiOjM1Ljg5ODc1NywibG9uIjoxMzkuNzE5ODE4LCJiYXQiOjMsInJzIjo0LCJ0ZW1wIjoyMy45LCJodW1pIjo1Mi4zLCJ4IjowLjAsInkiOjY0LjAsInoiOi05NjAuMCwidHlwZSI6MH0=" } } ]
今回はWASM内でこのpayloadをデコードしてみます。
※ SORACOM Harvestを使用している場合はデコードする必要はありません。SORACOM BeamやFunnelを使用する場合はデコードが必要になります。
リファレンス: 送信されるデータフォーマット | GPS マルチユニット SORACOM Edition ユーザーガイド | SORACOM Users
注意点として、TinyGoではGoと同じように全ての標準ライブラリを使用できるわけではないため、以下のサイトで使用できるか確認する必要があります。base64デコードのためのencoding/base64
は利用可能です。
それでは、サンプルコードを編集します。以下のように、inputBuffer
をデコードするだけです。
package main import ( "encoding/base64" sdk "github.com/soracom/orbit-sdk-tinygo" ) // application entry point, but the orbit runtime never executes this. func main() { } //export uplink func uplink() sdk.ErrorCode { inputBuffer, err := sdk.GetInputBuffer() if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Input Buffer: " + string(inputBuffer) + "\n") decodedInputBuffer, err := base64.StdEncoding.DecodeString(string(inputBuffer)) if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Base64 Decoded: " + string(decodedInputBuffer) + "\n") return sdk.ErrorCode(0) }
ビルドしてアップロードします(ブラウザでユーザーコンソールにアクセスし、最初にアップロードしたWASMは削除しておきます)。
$ make release $ soracom soralets upload \ --soralet-id my-soralet \ --content-type application/octet-stream \ --body @build/soralet-optimized.wasm
テスト用jsonとして以下を使用します。SORACOM Harvestからダウンロードしたjsonをorbitテスト用に加工しています。
{ "source": { "resourceType": "Subscriber", "resourceId": "000000000000000", "time": "2021/4/1 20:30:50", "contentType": "application/json" }, "payload": "eyJsYXQiOjM1Ljg5ODc1NywibG9uIjoxMzkuNzE5ODE4LCJiYXQiOjMsInJzIjo0LCJ0ZW1wIjoyMy45LCJodW1pIjo1Mi4zLCJ4IjowLjAsInkiOjY0LjAsInoiOi05NjAuMCwidHlwZSI6MH0=" }
テストします。
$ soracom soralets exec --soralet-id my-soralet \ --version 1 \ --direction uplink \ --content-type application/json \ --body @test.json { "body": "", "contentType": "", "encodingType": "", "resultCode": 0 }
ユーザーコンソールでLOGSを確認してみます。成功していればデコードされたjsonが表示されます。
jsonのパース・シリアライズ
デコードして得たjsonに対して何らかの加工をし、再度jsonを出力してみます。これは、orbit-sdk-tinygoリポジトリのexamplesのコードを参考にします。
Goでは標準ライブラリでencoding/json
パッケージが存在しますが、TinyGoでは使用できません。そのため、examplesのコードでも使用されているmoznion/go-json-iceとfmt
を利用している箇所をなくしたbuger/jsonparserを利用します。
上記のような制限事項については以下のページを参照ください。
リファレンス: 制限事項と注意事項 | SORACOM Orbit | SORACOM Users
なお、本項のコードは以下のGitHubリポジトリに格納してあります。
まずは、出力するjsonの構造体を定義します。
package data //go:generate json-ice --type=Output type Output struct { Lat string `json:"lat"` Lon string `json:"lon"` Bat int64 `json:"bat"` Rs int64 `json:"rs"` Temp string `json:"temp"` Humi string `json:"humi"` Timestamp int64 `json:"timestamp"` }
go-json-ice
をインストールし、jsonにシリアライズするためのコードを自動生成します(上記の構造体を基に自動生成されます)。
$ go get -u github.com/moznion/go-json-ice/cmd/json-ice $ cd src/data $ json-ice -type=Output
以下のコードが自動生成されます。
// Code generated by "json-ice -type=Output"; DO NOT EDIT. package data import "github.com/moznion/go-json-ice/serializer" func MarshalOutputAsJSON(s *Output) ([]byte, error) { buff := make([]byte, 1, 184) buff[0] = '{' buff = append(buff, "\"lat\":"...) buff = serializer.AppendSerializedString(buff, s.Lat) buff = append(buff, ',') buff = append(buff, "\"lon\":"...) buff = serializer.AppendSerializedString(buff, s.Lon) buff = append(buff, ',') buff = append(buff, "\"bat\":"...) buff = serializer.AppendSerializedInt(buff, int64(s.Bat)) buff = append(buff, ',') buff = append(buff, "\"rs\":"...) buff = serializer.AppendSerializedInt(buff, int64(s.Rs)) buff = append(buff, ',') buff = append(buff, "\"temp\":"...) buff = serializer.AppendSerializedString(buff, s.Temp) buff = append(buff, ',') buff = append(buff, "\"humi\":"...) buff = serializer.AppendSerializedString(buff, s.Humi) buff = append(buff, ',') buff = append(buff, "\"timestamp\":"...) buff = serializer.AppendSerializedInt(buff, int64(s.Timestamp)) buff = append(buff, ',') if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } return buff, nil }
自動生成された関数を呼び出せるように、src/data/output.go
を修正します。
package data //go:generate json-ice --type=Output type Output struct { Lat string `json:"lat"` Lon string `json:"lon"` Bat int64 `json:"bat"` Rs int64 `json:"rs"` Temp string `json:"temp"` Humi string `json:"humi"` Timestamp int64 `json:"timestamp"` } func (o *Output) SerializeJSON() ([]byte, error) { return MarshalOutputAsJSON(o) }
次に、デコードしたjsonから値を取得するためのパーサーを準備します。buger/jsonparserをforkし、fmt
を使っている箇所をなくしたブランチを作成します。buger/jsonparser
の修正点は以下のコミットを参照してください。
go get
します。
$ go get github.com/urawa72/jsonparser@<上記の修正コミットのハッシュ値>
あとはsrc/main.go
を修正します。
package main import ( "encoding/base64" "strconv" sdk "github.com/soracom/orbit-sdk-tinygo" "github.com/urawa72/jsonparser" "github.com/urawa72/orbit-tinygo/src/data" ) // application entry point, but the orbit runtime never executes this. func main() { } //export uplink func uplink() sdk.ErrorCode { inputBuffer, err := sdk.GetInputBuffer() if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Input Buffer: " + string(inputBuffer) + "\n") decodedInputBuffer, err := base64.StdEncoding.DecodeString(string(inputBuffer)) if err != nil { sdk.Log(err.Error()) return -1 } sdk.Log("Base64 Decoded: " + string(decodedInputBuffer) + "\n") output, err := convertInputToOutput(decodedInputBuffer) if err != nil { sdk.Log(err.Error()) return - 1 } serializedOuput, err := output.SerializeJSON() if err != nil { sdk.Log(err.Error()) return - 1 } sdk.SetOutputJSON(string(serializedOuput)) sdk.Log("Serialize JSON: " + string(serializedOuput) + "\n") return sdk.ErrorCode(0) } func convertInputToOutput(input []byte) (*data.Output, error) { lat, err := jsonparser.GetFloat(input, "lat") if err != nil { return nil, err } slat := strconv.FormatFloat(lat, 'f', -1, 64) lon, err := jsonparser.GetFloat(input, "lon") if err != nil { return nil, err } slon := strconv.FormatFloat(lon, 'f', -1, 64) bat, err := jsonparser.GetInt(input, "bat") if err != nil { return nil, err } rs, err := jsonparser.GetInt(input, "rs") if err != nil { return nil, err } temp, err := jsonparser.GetFloat(input, "temp") if err != nil { return nil, err } stemp := strconv.FormatFloat(temp, 'f', -1, 64) humi, err := jsonparser.GetFloat(input, "humi") if err != nil { return nil, err } shumi := strconv.FormatFloat(humi, 'f', -1, 64) timestamp := sdk.GetTimestamp() return &data.Output{ Lat: slat, Lon: slon, Bat: bat, Rs: rs, Temp: stemp, Humi: shumi, Timestamp: timestamp, }, nil }
32行目でデコードした[]byte型
のデータをパースしてOutput
構造体にセットします。38行目でjson-ice
によって自動生成されたシリアライズ関数を使用してorbitのアウトプットに使用できるようにjsonにシリアライズします。
なお、今回は大した加工はしておらず、属性の取捨選択とタイムスタンプを追加しただけです。
ここまでできたらビルド・アップロード・テストします。テストに使用するjsonはbase64デコードを試したときと同じファイルを使用します。
成功すると、body
にjsonが表示されます。
$ soracom soralets exec --soralet-id my-soralet \ --version 1 \ --direction uplink \ --content-type application/json \ --body @test.json { "body": "{\"lat\":\"35.898757\",\"lon\":\"139.719818\",\"bat\":3,\"rs\":4,\"temp\":\"23.9\",\"humi\":\"52.3\",\"timestamp\":1617369594740}", "contentType": "application/json", "encodingType": "plain", "resultCode": 0 }
orbitのLOGSでも確認できます。
おわりに
TinyGoを使ってWASMモジュールを作成し、orbitを試してみました。基本的にはGitHubのorbit-sdk-go
リポジトリのexamplesを参考にしながら動かすことでなんとかなりました。jsonのパース・シリアライズあたりはGoと同じように標準ライブラリを使用して気軽にできるわけではないため注意が必要です。
次回はRustで試してみたいと思います。