SwiftのみでIoT! Raspberry Pi Zeroでサーボを動かすサーバーを立ててiPhoneから制御する

## ハイライト

- Swift のみで Servo を動かす Server を Raspberry Pi 上に構築したよ
- リポジトリは [YutoMizutani/SwiftyServoControl](https://github.com/YutoMizutani/SwiftyServoControl) から
- Raspberry Pi 上での環境構築が難しいよ

## はじめに

みなさん! IoT してますか?
電子工作はとても安価かつ日々の暮らしを便利にしてくれる一方で，はんだ付けからサーバー，クライアントと様々な技術を要求され，最近出回り始めた市販品も高価と，なかなかハードルが高いですよね。

そこで今回は，開発言語を限定し，Swift のみで IoT 開発をしたいと思います。
今回の成果物は [YutoMizutani/SwiftyServoControl - GitHub](https://github.com/YutoMizutani/SwiftyServoControl) から参照できます。

<img src="https://raw.githubusercontent.com/YutoMizutani/SwiftyServoControl/master/pic/photo.jpg" width="300">
([動画](https://twitter.com/EXPENSIVE_MAN/status/1118320693358153728)。カメラモジュールは使用しません)

## 仕様

iPhone にてボタンを押下すると，Raspberry Pi 上の Server に対してリクエストを発し，Raspberry Pi が サーボモーターを動作させるまでが目標です。
HTTP通信およびハードウェア操作の両方が必要となり，仕様としては以下の3点にまとめられます。

#### Client 側
- UIButton を設置し，押下時に [`URLSession`](https://developer.apple.com/documentation/foundation/urlsession)  を用いて通信を行なう

#### Server 側
- [Vapor](https://github.com/vapor/vapor) を用い，POST を受け取る
- 指定されたリクエストを契機に，[SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO)を用いモーターを動作させる ([GPIO](https://www.raspberrypi.org/documentation/usage/gpio/) 制御)

## 材料

- [Raspberry Pi Zero W](https://www.switch-science.com/catalog/3200/) (約 1,300 円)
<img src="https://docid81hrs3j1.cloudfront.net/contents/large/3200_1.jpg" width="100">
- [サーボモータ MG996R](https://www.amazon.co.jp/dp/B010SLRAAS/ref=cm_sw_r_tw_dp_U_x_T-lUCb7PP8ZV2) ([秋月電子さん](http://akizukidenshi.com/catalog/g/gM-12534/)から買うと 1,080 円)
<img src="http://akizukidenshi.com/img/goods/C/M-12534.jpg" width="100">
- [ジャンパワイヤ オス - メス](https://www.amazon.co.jp/dp/B01BV68CYW/ref=cm_sw_r_tw_dp_U_x_bbmUCbT30METR)
<img src="https://images-na.ssl-images-amazon.com/images/I/713HSj2ZdOL._SL1100_.jpg" width="100">

Raspberry Pi のセットアップについては他記事を参照して下さい (分からない方や始めての方は Amazon 等から [ピンはんだ接着やSDカードやHDMI変換器やDCアダプタが付いたセット](https://www.amazon.co.jp/dp/B0746H9881/ref=cm_sw_r_tw_dp_U_x_N6lUCbS2QPHKQ)<img src="https://images-na.ssl-images-amazon.com/images/I/61yo4y3K-zL._SL1024_.jpg" width="50"> を購入することを強くオススメします)。

サーボモータに関しては何でも良いです。スイッチの操作力に合わせて利用してください。今回は[ワイドスイッチ](https://www.amazon.co.jp/dp/B002UNIE62/ref=cm_sw_r_tw_dp_U_x_yuwUCb1A20DM3)<img src="https://images-na.ssl-images-amazon.com/images/I/41HHsAOwarL.jpg" width="50">を用意しました (スイッチ自体の取り替えについては電気工事士の資格が必要です)。
ジャンパワイヤについては，モーターのハウジングが3Pで分離できないという理由から3本だけ必要です。何でも良いですが，片方が**オス**，もう片方が**メス**のものを選択してください。

## 環境構築 - Raspberry Pi に Swift をインストール

(Swift で開発することを望んでいるため，iOS 開発 (macOS) の環境構築は省略します)

今回は Raspberry Pi を DebianOS で利用します。
一方で，[Swift.org](https://swift.org/download/#releases) から確認できるのは Xcode (macOS) または Ubuntu の 2 種類のみです。
そこで，[Swift Arm](https://swift-arm.com) を利用します。
[packagecloud のリリースページ](https://packagecloud.io/swift-arm/release) より，該当の Swift を Raspberry Pi へインストールします。

#### Raspberry Pi 3

```
$ curl -s https://packagecloud.io/install/repositories/swift-arm/release/script.deb.sh | sudo bash
$ sudo apt install swift5
```

#### Raspberry Pi zero

Raspberry Pi Zero に対応した Swift パッケージはまだ 5 に対応していないようです。4 でも問題なく動作しますので、以下のように導入します。

```
$ curl -s https://packagecloud.io/install/repositories/swift-arm/release/script.deb.sh | sudo bash
$ sudo apt install swift4rpi01
```

## Vapor が依存する SSL パッケージのインストール

[Swift Package Manager](https://swift.org/package-manager/) に Vapor を追加するだけでは依存する SSL パッケージの解決はされないため、事前に以下の通りインストールしておきます。

#### macOS ([参考](https://github.com/vapor/vapor/issues/1562))

```
$ brew install libressl
```

#### Raspberry Pi (Debian) ([参考](https://github.com/vapor/vapor/issues/1523#event-1497154725))

```
$ sudo apt install libressl-dev
```

## Vapor CLI tools のインストール (macOS)

プロジェクトのテンプレートを作成してくれる，Vapor の CLI ツールをインストールします。
[Vapor のドキュメント](https://docs.vapor.codes/3.0/install/macos/) を元に，以下の通りにインストールします。

残念ながら Vapor の CLI ツールは macOS または Ubuntu でないと入手できないため，macOS にて Homebrew を用いてインストール，およびプロジェクトの作成を行ないます。
実行には Swift PM より依存関係の導入が行われるため，初期化時以外は不要です。

```
$ brew tap vapor/tap
$ brew install vapor/tap/vapor
```

これで環境は整いました。

## サーバー側実装 (Vaporによるサーバー構築)

#### プロジェクトの作成

上でインストールした vapor ツールによってテンプレートを作成します。
`<hoge>` 部分を書きかえて下さい。
*Swift PM* とは異なり，ディレクトリも作成してくれるため，`$ makedir <hoge>` は不要です。

```
$ vapor new <hoge> --template=api && cd <hoge> && vapor xcode -y
```

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195913/8a1f47eb-55f0-93b4-6267-04d4860b1417.png">

初期状態では *SQLite* により *Todo* のサンプルが記載されています。
今回は不要ですので削除して構いません。
作成されたディレクトリ内容と，その概要は以下の通りです。

```sh
Sources/
  ├ App/
  │　├ Controllers/ # MVC の C
  │　├ Models/ # MVC の M
  │　├ app.swift # main.swift から呼ばれる `Application` インスタンスの生成関数
  │　├ boot.swift # app.swift から呼ばれ，起動時に設定以外の処理を行う
  │　├ configure.swift # app.swift から呼ばれ，DBや設定を反映する
  │  └ routes.swift # configure.swift から呼ばれ，ルートを指定する
  └ Run/
     └ main.swift # app.swift で生成したアプリケーションを開始させる
```

このまま Run (または Terminal から `$ swift run Run`) すれば，localhost から Todo を確認できます。

ホスト名とポート番号の指定方法は，Terminal から `$ swift run Run --hostname localhost --port 8080` と指定するか，もしくは `app.swift` に以下を追記します。

```./Sources/App/app.swift
let serverConfig = NIOServerConfig.default(hostname: "localhost", port: 8080)
services.register(serverConfig)
```

#### POST リクエストの待ち受け

今回は POST リクエストを受けて動作させたいので，ルートを追加します。
`Sources/App/Controllers/` に `ServoController.swift` を追加します。

```Sources/App/Controllers/ServoController.swift
import Foundation
import Vapor

final class ServoController {
    private var servoState: Bool = false

    func toggle(_ request: Request) -> Future<HTTPStatus> {
        return request.future().map { [weak self] in
            guard let self = self else { return }

            self.servoState.toggle()

            print(self.servoState)

        }.transform(to: .ok)
    }
}
```

そして，その受け口となるルートを追加します。 `Sources/App/routes.swift` 内で Controller のインスタンスを生成し，POST リクエストを受け取れるように以下のように追加します。

```Sources/App/routes.swift
import Vapor

public func routes(_ router: Router) throws {
    let servoController = ServoController()           // 追加
    router.post("servo", use: servoController.toggle) // 追加
}
```

こちらをビルドすると，`$ curl -X POST http://localhost:8080/servo` の度に Bool が反転するのが確認できます。無事に POST リクエストを受け取ることができました。

`func toggle(_ request: Request)` のクロージャ内部を変更することで，好きな処理をさせられるようになりました。ここでサーバーとしての実装は完了です。

## サーバー側実装 (サーボモータの制御)

次に，サーボモータの制御を実装します。今回はその制御に [uraimo/SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO) を利用します。
同一プロジェクト内で記述することができますが，パッケージを追加する必要があります。

#### 依存パッケージ [SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO) の追加

`./Package.swift` を編集します。

```./Package.swift
// swift-tools-version:4.2
import PackageDescription

let package = Package(
    name: "servo",
    products: [
        .library(name: "servo", targets: ["App"]),
    ],
    dependencies: [
        .package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0"), // 追加
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
    ],
    targets: [
        .target(name: "App", dependencies: ["SwiftyGPIO", "Vapor"]), // 編集
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: ["App"])
    ]
)
```

dependencies に `.package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0"),`， App の dependencies に `"SwiftyGPIO"` を追記してください。

テストを除くと `App` と `Run` の2つのターゲットがありますが，`App` のみに追加します。

実行可能なターゲットからは import できないようで，そのためにターゲットが分かれているようです。

https://docs.vapor.codes/3.0/getting-started/spm/
> Executable targets (targets that contain a main.swift file) cannot be imported by other modules. This is why Vapor has both an App and a Run target. Any code you include in App can be tested in the AppTests.

パッケージの依存関係を変更した際には `$ swift package update` をして更新しておきましょう。

#### サーボ制御

続いて，サーボ制御に入ります。Raspberry Pi Zero W の場合，以下のようなピン配置になっています。 

<img width="800" src="https://i.stack.imgur.com/yHddo.png">

**5V** と書かれているところにモータの **+V**， **Ground** と書かれているところにモータの **GND**(Ground の略) を繋ぎます。
モータの **SIG** については，回転状態を伝えるためのもので，**GPIO** と書かれたピンに差しましょう。
そのピン番号を指定して電流を流し，モータを制御していきます。

<img width="300" src="http://akizukidenshi.com/img/goods/C/M-12534.jpg">
<img width="800" src="http://akizukidenshi.com/img/goods/2/M-12534.jpg">

`./Sources/App/Models/` 以下に `Servo.swift` を作成します。

```./Sources/App/Models/Servo.swift
import Foundation
import SwiftyGPIO

final class Servo {
    typealias PWMs = [Int: [GPIOName: PWMOutput]]

    var pwms: PWMs!
    var pin: GPIOName!
    var servo: PWMOutput!

    private let periodNs: Int = 20_000_000
    private let duty: (on: Float, off: Float) = (3, 6)

    init(_ board: SupportedBoard, pin: GPIOName) {
        pwms = SwiftyGPIO.hardwarePWMs(for: board)
        self.pin = pin

        guard let servo = pwms?[0]?[pin] else {
            print("Can not found \(pin.rawValue) pin")
            exit(EXIT_FAILURE)
        }

        self.servo = servo
        cofigureServo()
    }

    deinit {
        servo.stopPWM()
    }

    func cofigureServo() {
        servo.initPWM()
    }

    func toggle() {
        print("Start servo")
        servo.startPWM(period: periodNs, duty: duty.on)
        sleep(1)
        servo.stopPWM()
        servo.startPWM(period: periodNs, duty: duty.off)
        sleep(1)
        servo.stopPWM()
        print("Stop servo")
    }
}
```

`init(_ board: SupportedBoard, pin: GPIOName)` における `SupportedBoard` は Raspberry Pi の種類，`GPIOName` はピン番号となっています。

`func toggle()` が呼ばれると，`let periodNs: Int = 20_000_000`(ナノ秒) の周期で `let duty: (on: Float, off: Float) = (3, 6)` だけ回転します。都合によって変更してください。

また，`sleep(1)` を呼んでいますが，あまりに短期間での操作はモータまで信号が伝わらない可能性があるので注意してください。

#### Server のリクエストと紐づける

最後に，Server のリクエストと紐づけましょう。`Sources/App/Controllers/ServoController.swift` を編集します。

```Sources/App/Controllers/ServoController.swift
import Foundation
import Vapor

final class ServoController {
    private var servoState: Bool = false
    private let servo: Servo = Servo(.RaspberryPiPlusZero, pin: .P18) // 追加

    func toggle(_ request: Request) -> Future<HTTPStatus> {
        return request.future().map { [weak self] in
            guard let self = self else { return }
            self.servo.toggle()                                       // 追加
            self.servoState.toggle()
        }.transform(to: .ok)
    }
}
```

`private let servo: Servo = Servo(.RaspberryPiPlusZero, pin: .P18)` にてサーボを初期化しています。

Raspberry Pi Zero W の 18番ピン を用いたため，Servo の `init()` に対し `board` には `(SupportedBoard).RaspberryPiPlusZero`，`pin` には `(GPIOName).P18` を指定しています。

これでサーバー側の実装は完了です。

Raspberry Pi 側で Swift を実行する際には，GPIO 制御を行うため，`sudo` を付ける必要がある (`$ sudo swift run Run`) 場合があります。

## クライアント側実装

特に特別なことはしていませんので全てのコードを記載します。Storyboard に UIButton を設置し，`@IBAction private func tapAction()` に対して紐づけています。
ホスト名およびポート番号については，変更が可能なように環境変数をXcode上から設定しています。
`Xcode > Edit Scheme... > Run - Arguments > Environment Variables`

<img src="https://raw.githubusercontent.com/YutoMizutani/SwiftyServoControl/master/pic/xcode-screenshot.png" width="800">

```swift
import UIKit

class ViewController: UIViewController {
    private var isConnecting = false

    var url: URL!

    override func viewDidLoad() {
        super.viewDidLoad()

        let env = ProcessInfo.processInfo.environment
        let argv = ProcessInfo.processInfo.arguments
        let hostname = env["HOSTNAME"] ?? argv[1]
        let port = env["PORT"] ?? argv[2]
        let route = "/servo"
        let path = "http://\(hostname):\(port)\(route)"
        print("path: \(path)")

        url = URL(string: path)
    }

    private func post(_ completion: @escaping (() -> Void)) {
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"

        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print(error)
            } else if let response = response {
                print(response)
            }
            completion()
        }.resume()
    }

    @IBAction private func tapAction() {
        guard !isConnecting else { return }
        isConnecting = true
        post { [weak self] in
            self?.isConnecting = false
        }
    }
}
```

初期状態では localhost は HTTP にて通信を行うため，iOS では Info.plist から HTTP 通信を許可する設定を追加する必要があります。以下のように，`Allow Arbitrary Loads` に YES を指定して下さい。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195913/741ec60d-d089-328b-2a57-f4176b711c69.png">

## まとめ

IoT と言えば電子工作，電子工作といえばはんだ付け，というようなイメージから，IoT は難しいものだと捉えられがちです。
一方で，令和を控える現在は，はんだ付け不要かつ 1 つの言語のみで操作できる時代になりました。

ここから，オリジナルの発想を経て，ここまでご覧いただいた皆さんの生活がもっと便利ですてきになればうれしいです。

## ライセンス表記
本記事におけるコードのライセンスは [YutoMizutani/SwiftyServoControl](https://github.com/YutoMizutani/SwiftyServoControl) に記載の[ライセンス](https://github.com/YutoMizutani/SwiftyServoControl/blob/master/LICENSE)に準じます。

## References
- [YutoMizutani/SwiftyServoControl](https://github.com/YutoMizutani/SwiftyServoControl)
- [URLSession - Apple Developer](https://developer.apple.com/documentation/foundation/urlsession)
- [vapor/vapor - GitHub](https://github.com/vapor/vapor)
- [uraimo/SwiftyGPIO](https://github.com/uraimo/SwiftyGPIO)
- [GPIO - raspberrypi.org](https://www.raspberrypi.org/documentation/usage/gpio/)
- [releases - Swift.org](https://swift.org/download/#releases)
- [Swift Arm](https://swift-arm.com)
- [Swift.org - Package Manager](https://swift.org/package-manager/)
- [Cannot build latest Vapor RC2 'openssl/conf.h' file not found #1562 - vapor/vapor](https://github.com/vapor/vapor/issues/1562)
- [OpenSSL errors with Vapor 3 on Ubuntu 16.04 #1523 - vapor/vapor](https://github.com/vapor/vapor/issues/1523#event-1497154725)
- [Vapor docs](https://docs.vapor.codes/3.0/install/macos/)
- [GPIO Pinout Orientation RaspberyPi Zero W](https://raspberrypi.stackexchange.com/questions/83610/gpio-pinout-orientation-raspberypi-zero-w)


#### 欄外参照
- [swift-arm release](https://packagecloud.io/swift-arm/release)
- [RaspbianでもSwift 5.0が利用可能に！](https://www.fabshop.jp/swift-5-0/)
- [Vapor のホストネームと起動ポートを変更する](https://qiita.com/takoikatakotako/items/73aa07aa8ba7160b3244)
- [`$ swift build -C` オプションの使い方 💃 #swiftlang](https://qiita.com/jpmartha/items/d661cf65939a6333965a)
- [Makefileでrunするときに引数を渡す](https://qiita.com/tortuepin/items/9861c75853b516c8a279)
- [Xcode Commands to build app and run on simulator](https://samwize.com/2015/03/11/xcode-commands-to-build-app-and-run-on-simulator/)
- [ios-control/ios-sim](https://github.com/ios-control/ios-sim)
- [List empty of devices simulator with Xcode 10 #234](https://github.com/ios-control/ios-sim/issues/234)