Swiftだけで作るIoTシステム

SwiftはiOSとmacOSのアプリを書くためだけの言語ではありません．
Webアプリ開発もデバイス制御もできちゃうんです！

iOSDCやtry! SwiftでサーバサイドでSwiftを使う話や，SwiftでRaspberryPiのGPIOを制御する話を聞いたことを思い出し，Swiftだけで作る簡単なIoTシステムを作ってみました．

## 構成

![all-swift-iot-arch.png](https://qiita-image-store.s3.amazonaws.com/0/35592/3712e1b8-33cb-0651-b91c-e82b3a593b78.png)

* サーバサイドは[Vapor](https://vapor.codes/)を使ってWebAPIサーバを作り，Herokuにデプロイする
* ラズパイには[buildSwiftOnARM](https://github.com/uraimo/buildSwiftOnARM)と[SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO)を使い，LEDを制御
* iOSアプリでLEDの状態を更新し，ラズパイがそれを読んでGPIOを制御する
* iOSアプリ・WebAPIサーバ・エンドデバイスで動くコードを全部Swiftで書く！
* 認証とかは省略

## できあがったもの

とても単純ですが，iOSアプリからラズパイに接続したLEDをコントロールできます．

[![all-swift-iot.gif](https://qiita-image-store.s3.amazonaws.com/0/35592/22ffa8b5-1721-521a-d041-d23dab9a1a2b.gif)](https://youtu.be/mbTHfdPEngw)

## 環境

* WebAPIサーバ
    * Swift 4.2
    * Vapor 3.0
    * Heroku (Heroku-16 stack)
* iOSアプリ
    * Swift 4.2
    * iOS 12.1
* エンドデバイス
    * Swift 4.1.3
    * Raspbery Pi 3 Model B
    * Rasbian 2018-11-13 (Debian 9.6)
* 開発環境
    * macOS 10.14.2 (Mojave)
    * Xcode 10.1

## WebAPIサーバ

* Vaporのapiテンプレートをベースに作った
* Vaporは便利！
    * 他の一般的なWebフレームワークを経験していれば，そんなに迷うことなくすんなり使えた
    * DBや認証やWebSocketやテンプレートエンジンなどのパッケージが一通り揃っている
    * Herokuへのデプロイを標準でサポートしている（[buildpack](https://jp.heroku.com/elements/buildpacks)に対応している）
    * Xcodeでの開発（Xcodeプロジェクトの作成）がサポートされている
* VaporがまだNoSQLに標準対応していなかったのと，提供されているapiテンプレートをベースに簡単に作りたかったので，WebAPIリクエストを元にPostgreSQLのDBを更新する設計にした（あまりIoTっぽくないけど）
* Swiftのビルドはマシンパワーが必要で，Herokuへのデプロイには5分くらいかかった
* できあがりコードはこちら: [ottijp/all-swift-iot-server](https://github.com/ottijp/all-swift-iot-server)

### 手順

* Vaporのインストール

```shell:
$ brew install vapor/tap/vapor
```

* APIテンプレートを元にプロジェクトを作成

```shell:
$ vapor new all-swift-iot-server --template=api
$ cd all-swift-iot-server
$ vapor xcode
```

* 今回の設計に合わせてコードを変更
    * DBをPostgreSQLへ変更
    * デバイス状態のCRUD用のModel/Controller/Routeを追加
    * [diff](https://github.com/ottijp/all-swift-iot-server/commits/master)参照
* Herokuへデプロイ

```shell:
$ vapor heroku init
Would you like to provide a custom Heroku app name?
y/n> y
Custom app name:
> hoge
Would you like to deploy to a region other than the US?
y/n> n
https://hoge.herokuapp.com/ | https://git.heroku.com/hoge

Would you like to provide a custom Heroku buildpack?
y/n> n
Setting buildpack...
Are you using a custom Executable name?
y/n> n
Setting procfile...
Committing procfile...
Would you like to push to Heroku now?
y/n> y
This may take a while...

（中略）

Building on Heroku ... ~5-10 minutes [Failed]
Error: Heroku push failed remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Swift app detected
remote:
remote:  ! This buildpack does not support the heroku-18 stack at the moment.
remote:  ! Downgrade your application to the heroku-16 stack with the following command:
remote:      $ heroku stack:set heroku-16 -a <your-application-name>
remote:
remote:  ! Then deploy the application again.
remote:
remote:  !     Push rejected, failed to compile Swift app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !       Push rejected to hoge.
remote:
To https://git.heroku.com/hoge.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/hoge.git'
```

* heroku-18 stackだとサポートしていないというエラーになったので，heroku-16 stackに変更してpushしなおした

```shell:
$ heroku stack:set heroku-16 -a hoge
Stack set. Next release on ⬢ hoge will use heroku-16.
Run git push heroku master to create a new release on ⬢ hoge.

$ git push heroku master
Counting objects: 50, done.

（中略）

remote: Verifying deploy... done.
To https://git.heroku.com/hoge.git
 * [new branch]      master -> master

```

* 作られたHerokuアプリにHeroku Postgresアドオンを追加
* デバイスレコードの作成と更新，取得（WebAPIの動作）確認

```shell:
$ curl -X POST -H 'Content-Type:application/json' -d '{"led":false}' https://hoge.herokuapp.com/deviceStatus
{"id":1,"led":false}

$ curl -X PUT -H 'Content-Type:application/json' -d '{"led":true}' https://hoge.herokuapp.com/deviceStatus/1
{"id":1,"led":true}

$ curl -X GET -H 'Content-Type:application/json' https://hoge.herokuapp.com/deviceStatus/1
{"id":1,"led":true}
```

* ここで作成した`https://hoge.herokuapp.com/deviceStatus/1`をiOSアプリとラズパイのプログラムで指定します（後述）

### 詰まったところ

#### Futureの概念の理解

非同期処理の仕組みとしてFutureというものが使われており，JavaScriptのPromiseみたいなものですが，コードを読むだけでは理解しづらかったです．ちゃんと[ドキュメント](https://docs.vapor.codes/3.0/async/overview/)を読むことで理解できました．

#### PostgreSQLへの変更

apiテンプレートのデフォルトはSQLiteのメモリ保存なのでPostgreSQL用にコードを書き直しましたが，Heroku Postgresへの接続はSSL設定をDB設定に入れる必要があったので，使用するパッケージの変更だけではなく，ちょっと工夫が必要でした．

```swift:Sources/App/configure.swift
    // Configure a PostgreSQL database
    guard let databaseUrl = Environment.get("DATABASE_URL") else {
        fatalError("DATABASE_URL needed")
    }
    guard let pgConfig = PostgreSQLDatabaseConfig(url: databaseUrl, transport: .unverifiedTLS) else {
        fatalError("Invalid DATABASE_URL format")
    }
    let pg = PostgreSQLDatabase(config: pgConfig)
```

## エンドデバイス

ずいぶん前に買って放置していたラズパイを今回始めて使ってみました．

* OSはRasbianを使用し，Swiftの環境は[uraimo/buildSwiftOnARM](https://github.com/uraimo/buildSwiftOnARM)のビルド済みモジュールを利用
* [拡張ボード](https://www.amazon.co.jp/dp/B074H1FNTJ/ref=cm_sw_em_r_mt_dp_U_qS-qCbS3V0HZS)を使い，GPIO25ピンからLED（と抵抗）に接続
* [uraimo/SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO)でGPIO25ピンを制御
* WebAPIへのアクセスは[shiroyagicorp/swift-seeurl](https://github.com/shiroyagicorp/swift-seeurl)を利用
* できあがりコードはこちら: [ottijp/all-swift-iot-enddevice](https://github.com/ottijp/all-swift-iot-enddevice)

### 手順

* SDカードをmacOSのディスクユーティリティで初期化（FATフォーマット）
* [NOOBS](https://www.raspberrypi.org/downloads/noobs/)をSDカードにコピー
* ラズパイにキーボード，マウス，モニタ，LANケーブル，SDカードを繋いで起動
* ラズパイの起動画面でRaspbianを選択してインストール
* インストール後，SSHが有効になってなかったので，有効化
    * メニューバーから`Raspberry Piの設定`を起動し，`インタフェース`タブの`SSH`をチェックする
* [uraimo/buildSwiftOnARM](https://github.com/uraimo/buildSwiftOnARM)用の依存パッケージのインストール

```shell:
$ sudo apt install clang-3.8 libicu-dev libcurl4-nss-dev
```

* GitHubのREADMEにリンクがあるPrebuild Binary（Raspbian用）をダウンロードし，展開

```shell:
$ tar xzvf swift-4.1.3-RPi23-RaspbianStretch.tgz
```

* これ以下の`swift`コマンドは，展開されたパスの`usr/bin`にあるものを使う
* READMEの通りにやってもうまくいかなかった（下記「詰まったところ」参照）
* Swift実行可能パッケージの作成

```shell:
$ mkdir SwiftyPi && cd SwiftyPi
$ swift package init --type executable
```

* [uraimo/SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO)を`Package.swift`に追加
* `Sources/SwiftyPi/main.swift`にコードを書いてビルド＆実行

```shell:
$ swift build
$ API_URL=https://hoge.herokuapp.com/deviceStatus/1 swift run
```

* `API_URL`はHerokuにデプロイして作成したものを指定する
* ここでも実行可能ファイルが出力されない問題があった（下記「詰まったところ」参照）

### 詰まったところ

#### clangが見つからない

clang-3.8のインストール後，`/usr/bin/clang`が生成されず，`swift build`が異常終了してしまいました．

```shell:
$ swift build
error: terminated(1): which clang output:
```

これは，手動でシンボリックリンクを作ることで正常動作するようになりました．

```shell:
$ sudo ln -s /usr/bin/clang-3.8 /usr/bin/clang
```

#### 実行可能ファイルが生成されない

`swift build`すると`Linking （実行可能ファイルへのパス）`と出力されますが，実際にそのパスには何も生成されませんでした（下記のように出力されるが，`./.build/armv7-unknown-linux-gnueabihf/debug/SwiftyPi`が生成されない．）

```shell:
$ swift build
Compile Swift Module 'SwiftyPi' (1 sources)
Linking ./.build/armv7-unknown-linux-gnueabihf/debug/SwiftyPi
```

これは，Swift自体のソースをビルドする時に必要なパッケージを入れてみることで正常に動作するようになりました（どれが足りなかったのかは不明．）

```shell:
$ sudo apt-get install cmake ninja-build clang python uuid-dev libicu-dev icu-devtools libbsd-dev libedit-dev libxml2-dev libsqlite3-dev swig libpython-dev libncurses5-dev pkg-config libblocksruntime-dev libcurl4-openssl-dev autoconf libtool systemtap-sdt-dev
```

#### ビルドでたまにエラーが出る

`swift build`すると，たまに`error: unable to attach DB: error: accessing build database`とエラーが出ます．何度か試すと正しく動き出しますが，こちらは原因不明でした．

## iOSアプリ

LEDのON/OFFを制御するAPIリクエストをサーバに対して送るだけのアプリです．
単純なiOSアプリなので手順は省略します．
できあがりコードはこちら: [ottijp/all-swift-iot-client](https://github.com/ottijp/all-swift-iot-client)
WebAPIのURLはハードコーディングしてしまっているので，書き換えてビルドする必要があります．

```swift:AllSwiftIot/APIClient.swift
// このURLはデプロイ先のものに変えてください
fileprivate let API_URL = "https://hoge.herokuapp.com/deviceStatus/1"
```

## まとめ

サーバサイドSwiftやラズパイで動くSwiftを使うことで，Swiftだけで簡単なIoTシステムを作ってみました．
これ以外にも，[AWS LambdaでSwiftで作ったファンクションを動かしたり](https://dev.classmethod.jp/cloud/aws/swift-lambda/)，[公式のDockerイメージ](https://hub.docker.com/_/swift)があったり，Swiftを色々なところで利用できるようになってきて楽しいですね！

## 参考情報

* [SwiftではじめるRaspberry Pi](https://speakerdeck.com/gucchisk/swiftdehazimeruraspberry-pi)
* [14.04 - clang and clang++ not found after installing the clang-3.5 package - Ask Ubuntu](https://askubuntu.com/questions/584711/clang-and-clang-not-found-after-installing-the-clang-3-5-package)
* [How to Deploy Vapor Apps to Heroku - Twilio](https://www.twilio.com/blog/2016/11/how-to-deploy-vapor-apps-to-heroku.html)
* [サーバーサイドSwiftを学ぶ（基礎の基礎の基礎編） - Qiita](https://qiita.com/hiroeorz@github/items/664031fe3f1daefc1cf0)
* [【2017年版】Swiftフレームワーク7選比較と今後の需要｜フリエン](https://furien.jp/columns/189/)
* [shiroyagicorp/swift-seeurl: cURL based HTTP(S) Client for Swift](https://github.com/shiroyagicorp/swift-seeurl)