SwiftはiOSとmacOSのアプリを書くためだけの言語ではありません.
Webアプリ開発もデバイス制御もできちゃうんです!
iOSDCやtry! SwiftでサーバサイドでSwiftを使う話や,SwiftでRaspberryPiのGPIOを制御する話を聞いたことを思い出し,Swiftだけで作る簡単なIoTシステムを作ってみました.
構成
- サーバサイドはVaporを使ってWebAPIサーバを作り,Herokuにデプロイする
- ラズパイにはbuildSwiftOnARMとSwiftyGPIOを使い,LEDを制御
- iOSアプリでLEDの状態を更新し,ラズパイがそれを読んでGPIOを制御する
- iOSアプリ・WebAPIサーバ・エンドデバイスで動くコードを全部Swiftで書く!
- 認証とかは省略
できあがったもの
とても単純ですが,iOSアプリからラズパイに接続したLEDをコントロールできます.
環境
- 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に対応している)
- Xcodeでの開発(Xcodeプロジェクトの作成)がサポートされている
- VaporがまだNoSQLに標準対応していなかったのと,提供されているapiテンプレートをベースに簡単に作りたかったので,WebAPIリクエストを元にPostgreSQLのDBを更新する設計にした(あまりIoTっぽくないけど)
- Swiftのビルドはマシンパワーが必要で,Herokuへのデプロイには5分くらいかかった
- できあがりコードはこちら: ottijp/all-swift-iot-server
手順
- Vaporのインストール
$ brew install vapor/tap/vapor
- APIテンプレートを元にプロジェクトを作成
$ vapor new all-swift-iot-server --template=api
$ cd all-swift-iot-server
$ vapor xcode
- 今回の設計に合わせてコードを変更
- DBをPostgreSQLへ変更
- デバイス状態のCRUD用のModel/Controller/Routeを追加
- diff参照
- Herokuへデプロイ
$ 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しなおした
$ 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の動作)確認
$ 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みたいなものですが,コードを読むだけでは理解しづらかったです.ちゃんとドキュメントを読むことで理解できました.
PostgreSQLへの変更
apiテンプレートのデフォルトはSQLiteのメモリ保存なのでPostgreSQL用にコードを書き直しましたが,Heroku Postgresへの接続はSSL設定をDB設定に入れる必要があったので,使用するパッケージの変更だけではなく,ちょっと工夫が必要でした.
// 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のビルド済みモジュールを利用
- 拡張ボードを使い,GPIO25ピンからLED(と抵抗)に接続
- uraimo/SwiftyGPIOでGPIO25ピンを制御
- WebAPIへのアクセスはshiroyagicorp/swift-seeurlを利用
- できあがりコードはこちら: ottijp/all-swift-iot-enddevice
手順
- SDカードをmacOSのディスクユーティリティで初期化(FATフォーマット)
- NOOBSをSDカードにコピー
- ラズパイにキーボード,マウス,モニタ,LANケーブル,SDカードを繋いで起動
- ラズパイの起動画面でRaspbianを選択してインストール
- インストール後,SSHが有効になってなかったので,有効化
- メニューバーから
Raspberry Piの設定
を起動し,インタフェース
タブのSSH
をチェックする
- メニューバーから
- uraimo/buildSwiftOnARM用の依存パッケージのインストール
$ sudo apt install clang-3.8 libicu-dev libcurl4-nss-dev
- GitHubのREADMEにリンクがあるPrebuild Binary(Raspbian用)をダウンロードし,展開
$ tar xzvf swift-4.1.3-RPi23-RaspbianStretch.tgz
- これ以下の
swift
コマンドは,展開されたパスのusr/bin
にあるものを使う - READMEの通りにやってもうまくいかなかった(下記「詰まったところ」参照)
- Swift実行可能パッケージの作成
$ mkdir SwiftyPi && cd SwiftyPi
$ swift package init --type executable
-
uraimo/SwiftyGPIOを
Package.swift
に追加 -
Sources/SwiftyPi/main.swift
にコードを書いてビルド&実行
$ swift build
$ API_URL=https://hoge.herokuapp.com/deviceStatus/1 swift run
-
API_URL
はHerokuにデプロイして作成したものを指定する - ここでも実行可能ファイルが出力されない問題があった(下記「詰まったところ」参照)
詰まったところ
clangが見つからない
clang-3.8のインストール後,/usr/bin/clang
が生成されず,swift build
が異常終了してしまいました.
$ swift build
error: terminated(1): which clang output:
これは,手動でシンボリックリンクを作ることで正常動作するようになりました.
$ sudo ln -s /usr/bin/clang-3.8 /usr/bin/clang
実行可能ファイルが生成されない
swift build
するとLinking (実行可能ファイルへのパス)
と出力されますが,実際にそのパスには何も生成されませんでした(下記のように出力されるが,./.build/armv7-unknown-linux-gnueabihf/debug/SwiftyPi
が生成されない.)
$ swift build
Compile Swift Module 'SwiftyPi' (1 sources)
Linking ./.build/armv7-unknown-linux-gnueabihf/debug/SwiftyPi
これは,Swift自体のソースをビルドする時に必要なパッケージを入れてみることで正常に動作するようになりました(どれが足りなかったのかは不明.)
$ 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
WebAPIのURLはハードコーディングしてしまっているので,書き換えてビルドする必要があります.
// このURLはデプロイ先のものに変えてください
fileprivate let API_URL = "https://hoge.herokuapp.com/deviceStatus/1"
まとめ
サーバサイドSwiftやラズパイで動くSwiftを使うことで,Swiftだけで簡単なIoTシステムを作ってみました.
これ以外にも,AWS LambdaでSwiftで作ったファンクションを動かしたり,公式のDockerイメージがあったり,Swiftを色々なところで利用できるようになってきて楽しいですね!