SORACOM Orbit試してみた 〜TinyGOでWASM作ってみた〜

SORACOM Orbit試してみた4回目です。

----

SORACOM UG Online #1でOrbitで利用可能な言語の4つ目として登場したTinyGO
[![thumbnail](https://files.speakerdeck.com/presentations/44997a0bda5d4300b03094dd3b4fd264/slide_0.jpg)](https://speakerdeck.com/soracom/soracom-orbit-ug-online-number-1)

もともとGoでバッチ作ったり、CLIっぽいの作ったりしてたので、これはやらんとなー。と思いつつ、
年を越えてしまいました。(反省)

## ソース

ベースにしたのは、
ソラコムさんのgithubにあるLTE-Mボタン用のサンプルになります。
https://github.com/soracom/orbit-sdk-tinygo/tree/master/examples/lte-m-button

コード自体は、
[WASM モジュール開発環境のセットアップ](https://dev.soracom.io/jp/orbit/setup/)
でダウンロードする開発環境構築用のzipファイルに含まれるTinyGo用のディレクトリ内の
`src/main.go`
に組み込む形にしています。

```go
package main

import (
	"strings"

	"github.com/moznion/go-json-ice/serializer"
	"github.com/moznion/jsonparser"
	sdk "github.com/soracom/orbit-sdk-tinygo"
)

type Output struct {
	Lat  float64 `json:"lat"`
	Lon  float64 `json:"lon"`
	Bat  int64   `json:"bat"`
	Rs   int64   `json:"rs"`
	Temp float64 `json:"temp"`
	Humi float64 `json:humi"`
	X    float64 `json:"x"`
	Y    float64 `json:"y"`
	Z    float64 `json:"z"`
	Type int64   `json:"type"`
	Name string
}
type GpsMultiUnitErrorCode sdk.ErrorCode

const (
	OkErrorCode   GpsMultiUnitErrorCode = 0
	ExecErrorCode                       = -1
)

// 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")

	output, err := convertInputToOutput(inputBuffer)
	if err != nil {
		sdk.Log(err.Error())
		return sdk.ErrorCode(ExecErrorCode)
	}

	tagValueName, err := sdk.GetTagValue("name")
	if err != nil {
		sdk.Log(err.Error())
		return -1
	}
	sdk.Log("Name: " + string(tagValueName) + "\n")
	output.Name = string(tagValueName)

	serializedOutput, err := MarshalOutputAsJSON(output)
	if err != nil {
		sdk.Log(err.Error())
		return sdk.ErrorCode(ExecErrorCode)
	}

	sdk.SetOutputJSON(string(serializedOutput))

	return sdk.ErrorCode(0)
}

func convertInputToOutput(input []byte) (*Output, error) {
	lat, err := GetFloatForLocation(input, "lat")
	if err != nil {
		return nil, err
	}
	lon, err := GetFloatForLocation(input, "lon")
	if err != nil {
		return nil, err
	}
	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
	}

	humi, err := jsonparser.GetFloat(input, "humi")
	if err != nil {
		return nil, err
	}

	x, err := jsonparser.GetFloat(input, "x")
	if err != nil {
		if strings.HasPrefix(err.Error(), "Value is not a number") {
			x = 0
		} else {
			return nil, err
		}
	}
	y, err := jsonparser.GetFloat(input, "y")
	if err != nil {
		return nil, err
	}

	z, err := jsonparser.GetFloat(input, "z")
	if err != nil {
		return nil, err
	}

	sendType, err := jsonparser.GetInt(input, "type")
	if err != nil {
		return nil, err
	}

	return &Output{
		Lat:  lat,
		Lon:  lon,
		Bat:  int64(bat),
		Rs:   int64(rs),
		Temp: temp,
		Humi: humi,
		X:    x,
		Y:    y,
		Z:    z,
		Type: int64(sendType),
	}, nil
}

func MarshalOutputAsJSON(s *Output) ([]byte, error) {
	buff := make([]byte, 1, 210)
	buff[0] = '{'
	if float64(s.Lat) != 0 {
		buff = append(buff, "\"lat\":"...)
		buff = serializer.AppendSerializedFloat(buff, float64(s.Lat))
		buff = append(buff, ',')
	}
	if float64(s.Lon) != 0 {
		buff = append(buff, "\"lon\":"...)
		buff = serializer.AppendSerializedFloat(buff, float64(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.AppendSerializedFloat(buff, float64(s.Temp))
	buff = append(buff, ',')
	buff = append(buff, "\"humi\":"...)
	buff = serializer.AppendSerializedFloat(buff, float64(s.Humi))
	buff = append(buff, ',')
	buff = append(buff, "\"x\":"...)
	buff = serializer.AppendSerializedFloat(buff, float64(s.X))
	buff = append(buff, ',')
	buff = append(buff, "\"y\":"...)
	buff = serializer.AppendSerializedFloat(buff, float64(s.Y))
	buff = append(buff, ',')
	buff = append(buff, "\"z\":"...)
	buff = serializer.AppendSerializedFloat(buff, float64(s.Z))
	buff = append(buff, ',')
	buff = append(buff, "\"type\":"...)
	buff = serializer.AppendSerializedInt(buff, int64(s.Type))
	buff = append(buff, ',')
	buff = append(buff, "\"name\":"...)
	buff = serializer.AppendSerializedString(buff, s.Name)
	buff = append(buff, ',')
	if buff[len(buff)-1] == ',' {
		buff[len(buff)-1] = '}'
	} else {
		buff = append(buff, '}')
	}
	return buff, nil
}

// based on https://github.com/moznion/jsonparser/blob/master/parser.go#GetFloat
// GetFloat returns the value retrieved by `Get`, cast to a float64 if possible.
// The offset is the same as in `Get`.
// If key data type do not match, it will return an error.
func GetFloatForLocation(data []byte, keys ...string) (val float64, err error) {
	v, t, _, e := jsonparser.Get(data, keys...)

	if e != nil {
		return 0, e
	}

	if t != jsonparser.Number {
		return 0, nil
	}

	return jsonparser.ParseFloat(v)
}
```

## 実装してみて
### GPSマルチユニットのGPS座標(latとlon)
困ったのが、GPSマルチユニットのGPS座標(latとlon)。

室内配置しているのですが、GPS座標がまあ取れない。
無効にすればいいんですが、そのままにしています。
（外持ち出したい時にONにするの忘れるからという理由）

ところが、それだと、エラーになる。

調べてみたところ、
JSONをパースする `jsonparser` のGetFloatで、数値型じゃなかった場合は、エラーになっていた。
さて、どうしたものかと思って、
そのメソッドをベースにして、数値型じゃない場合は、0を返すメソッドを作りました(GetFloatForLocation)。
※最初はエラーメッセージ拾ってたけど、イケてなかったので、変更

プルリク送った方がいいかなーと
ちょっと思わなくもないので、ちょっと検討。

余談ですが、Funk経由で、Lambda側で、GoogleMapへのリンクを生成していますが、
このままだと、経度0度、緯度0度になるので、ヌル島という架空の島にいることになります。
[ヌル島](https://ja.wikipedia.org/wiki/%E3%83%8C%E3%83%AB%E5%B3%B6)

それでもいいかなと思ったんですが、
outputする時に、
Lat、Lonは0の場合は、セットしないようにしています。

### fmtパッケージが使えない
[開発者ガイド](https://dev.soracom.io/jp/orbit/limitations/)にも記載がありますが、
「SORACOM Orbit の実行環境は TinyGo の JS ランタイム (syscall/js.*) に依存する WASM モジュールを実行できません。たとえば fmt パッケージを使用していると Soralet へアップロードする際に`unknown import function: env.syscall/js.valueGet (SLM1011) `のようなエラーとなります。」
とあるように、fmtパッケージが使えないので、ロジック内でエラーを生成(fmt.Error)できないんですよね。
`GetFloatForLocation` でエラー生成して返したかったんですが、諦めました。
もしかしたら、うまい手があるのかもしれませんが・・・。

### 次回
これは途中の段階なので、
これをさらに改良します。