SORACOMでIoT + wasmを簡単に試す

# 概要
SORACOM Orbitという、WASMベースのサービスを使って、IoTデバイスから収集したデータを変換し、可視化・通知する簡単なシステムを構築してみた。

# 背景
最近、[WASM](https://webassembly.org/)をよく耳にする。
WASMは、軽量で安全、言語中立、ポータブルな実行環境であり、WebブラウザでJS以外のプログラムを高速実行するときなどに使われているが、[IoTやサーバレスなど様々な分野でも注目されている](https://www.publickey1.jp/blog/21/webassemblyrustthe_state_of_webassembly_2021.html)らしい。

一方、[SORACOM](https://soracom.jp/)は、誰でも簡単にIoT向けのSIMやIoTデバイスを安価に購入でき、データ収集や可視化まで行えるIoTプラットフォーム。

SORACOMでは、[SORACOM Orbit](https://soracom.jp/services/orbit/)という、IoTデバイスから収集したデータをWASMで処理できるサービスが提供されており、どんなものか試してみた。

# 作ったもの
## システムの目的
私は現在2人で生活しているが、私が家にいて、同居人が外出というシチュエーションが割と多い。
そういうとき、同居人がいきなり家に帰ってくるとびっくりすることがよくあった。

これを防ぐため、同居人のカバンの位置情報をIoTデバイスでモニタし、デバイスから自宅までの距離が1 km以下になったら(=家に着きそうになったら)自動でSlackに通知を送れるようにしたい。

## システム構成

以下のような感じ。
![Screen Shot 2021-07-04 at 15.30.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692447/25f38515-7055-8b18-30f3-d3a890ec6f11.png)


SORACOMの[GPSマルチユニット](https://soracom.jp/store/5235/)を同居人の私用カバンに入れ、位置情報を定期的にSORACOMのクラウドへ送信する。

送られてくる位置情報は緯度経度の形式なので、これを自宅までの距離に変換する必要がある。
この処理をSORACOM Orbit内のWASMモジュールで行った。

計算した距離データは、SORACOM Harvest(データ蓄積サービス)に格納し、SORACOM Lagoon(Grafanaを使った可視化、アラートサービス)に送信。

自宅までの距離が1 km以下になったら、LagoonからSlackへ通知。

# 作成手順
## 1. デバイスの購入
[SORACOMのIoTストア](https://soracom.jp/store/5235/)で、GPSマルチユニットを購入。購入時にアカウント登録も行う。私の場合、デバイスは翌日に届いた。

## 2. デバイスを設定して、データをHarvest、Lagoonに送れるようにする
GPSマルチユニットが届いたら、[こちらのチュートリアル](https://soracom.jp/recipes_index/3830/)を参考に、SIMの取り付け、SIMグループの設定、データをHarvestに定期送信する設定、LagoonでGPSマルチユニットのデータをダッシュボード化する設定を行う。
なお、このとき、GPSマルチユニットから定期送信するデータには、「位置情報(GPS)」を含める。

この時点で、GPSマルチユニット→Harvest→Lagoonというパイプラインが出来上がる。

## 3. SORACOM Orbit用のWASMモジュールを開発
基本的に[公式チュートリアル](https://users.soracom.io/ja-jp/docs/orbit/setup/)通りに進める。

### 開発環境を作る
チュートリアルを参照し、前提条件インストール、SAM、SORACOM CLIの設定を行う。
また、WASMモジュールの[サンプルコード](https://s3-ap-northeast-1.amazonaws.com/soracom-files/orbit/orbit-development-environment-2021-05.zip)をダウンロード。

今回はRustで開発したので、サンプルコードのrustディレクトリをVSCodeで開き、チュートリアルにしたがってVSCode拡張機能のインストールなどを行う。

### コードをいじる
サンプルコードの、`src/lib.rs`を改変して、今回の目的の処理ができるようにしていく。

なお、コードの全体は[GitHub](https://github.com/nakaakist/soracom-orbit-distance-from-home)においた(issue等あればぜひお願いします)。

改変後の`src/lib.rs`は以下の通り。

```rust:src/lib.rs
mod distance;
mod home_location;

use soracom_orbit_sdk as orbit;

#[macro_use]
extern crate serde_derive;

type Error = Box<dyn std::error::Error + Send + Sync + 'static>;

#[repr(i32)]
#[derive(Debug)]
pub enum ErrorCode {
    Ok = 0,
    ExecError = -1,
}

// デバイスから送られてくるデータ形式
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Input {
    lat: f64,
    lon: f64,
    bat: i32,
    rs: i32,
    #[serde(alias = "type")]
    send_type: i32,
}

// SORACOMプラットフォーム側に送るデータ形式
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Output {
    distance_from_home: f64,
    lat: f64,
    lon: f64,
    bat: i32,
    rs: i32,
    #[serde(alias = "type")]
    send_type: i32,
}

// データ変換処理
#[no_mangle]
pub fn uplink() -> ErrorCode {
    let buf = orbit::get_input_buffer();
    let output = process_uplink(buf);
    if let Err(e) = output {
        orbit::log(format!("{}", e).as_str());
        return ErrorCode::ExecError;
    }
    let output = output.unwrap();

    orbit::set_output_json(output.as_str());

    ErrorCode::Ok
}

fn process_uplink(buf: Vec<u8>) -> Result<String, Error> {
    let input: Input = serde_json::from_slice(buf.as_ref())?;

    let output = Output {
        distance_from_home: distance::calc(
            distance::Location {
                lat_deg: input.lat,
                lon_deg: input.lon,
            },
            home_location::LOCATION,
        ),
        lat: input.lat,
        lon: input.lon,
        bat: input.bat,
        rs: input.rs,
        send_type: input.send_type,
    };

    let output_json = serde_json::to_string(&output)?;

    Ok(output_json)
}

```
主な改変点としては、まず、デバイスからの入力データ構造(`Input`)と、出力データ構造(`Output`)を、今回用いるGPSマルチユニットに即した形式に直している。
入力データ構造を直すにあたっては、SORACOMユーザーコンソールのガジェット管理 -> GPSマルチユニット -> データを確認　から、今まで送られてきたデータのペイロード形式を確認した(もっといい方法あるかも)。
また、出力データ構造には、`distanceFromHome`フィールドを追加している。

経度緯度情報を自宅からの距離に変換する計算は、`distance`というモジュールを以下のように別途作って行った。

```rust:src/distance.rs
use std::f64::consts::PI;

pub struct Location {
    pub lat_deg: f64,
    pub lon_deg: f64,
}

// ２点の緯度経度(deg.)から、距離(km)を計算する。
// 計算の公式はhttps://keisan.casio.jp/exec/system/1257670779　を参照した。
pub fn calc(location1: Location, location2: Location) -> f64 {
    let lat1_rad = to_rad(location1.lat_deg);
    let lon1_rad = to_rad(location1.lon_deg);
    let lat2_rad = to_rad(location2.lat_deg);
    let lon2_rad = to_rad(location2.lon_deg);

    let earth_radius = 6138.137;

    return earth_radius
        * (lat1_rad.sin() * lat2_rad.sin()
            + lat1_rad.cos() * lat2_rad.cos() * (lon1_rad - lon2_rad).cos())
        .acos();
}

fn to_rad(deg: f64) -> f64 {
    return deg / 180.0 * PI;
}

```
このモジュールに関してはユニットテストを書きたかったのだが、私がrust, WASM超初心者ということもあってなんかうまくいかなかった。。(ユニットテストは諦めて、何度か実際にモジュールをデプロイして手動でテストした)

また、自宅の緯度経度は、別ファイルで`home_location`というモジュールを作って定義。

### WASMコンパイル、SORACOM Orbitにデプロイ
コードをいじったら、チュートリアルにしたがって`cargo build --release`でコンパイル、できた.wasmファイルを、ユーザーコンソールからSORACOM Orbitへアップロードする。

その後、チュートリアルにしたがってGPSマルチユニットのSIMグループでOrbitを利用するように設定。このとき、uplink(データのアップロード)のみを使用するようにする。

この設定を行うと、手順2で構築したGPSマルチユニット→Harvest→Lagoonというパイプラインが、GPSマルチユニット→Orbit→Harvest→Lagoonという形に変更される。

## 4. SORACOM Lagoonで自宅までの距離を可視化、Slackへの通知設定
LagoonのAlerting設定で、notification channelsに通知したいSlackのチャネルを追加する(incoming webhookのURLが必要になるので、事前にSlack側で取得しておく)。

手順2で設定したSORACOM Lagoonのダッシュボードにおいて、新規パネルを作成し、パネルのQueryで、GPSマルチユニットから`distanceFromHome`というメトリクスが取得できるようになっているので、それを選択。
また、パネルの設定で、distanceFromHomeが1 km以下になったら、上で追加したslackのnotification channelに通知を送るようにする。

以上の手順で、下図のように通知が届くようになった！
![Screen Shot 2021-07-05 at 20.08.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692447/3ae55c79-3e49-68eb-538a-baf44637bf73.png)


# 最後に
IoT, Rust, WASM、すべて超初心者だったが、なんとなくシステムが動くまで比較的簡単に行けた。
チュートリアル通りに進めれば、特にWASMの詳細に関しては何も知らなくても実装できたと感じる。

Orbitは、現時点ではシステムコールが絡む処理(e.g., ネットワークへのアクセス)をユーザーが独自で行うことはできないので、できることはデータ変換ぐらいだが、今後SORACOM外サービスのAPIを呼べたりできるようになったら面白そうと感じた。