Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

ハイライト

  • Swift のみで Servo を動かす Server を Raspberry Pi 上に構築したよ
  • リポジトリは YutoMizutani/SwiftyServoControl から
  • Raspberry Pi 上での環境構築が難しいよ

はじめに

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

そこで今回は,開発言語を限定し,Swift のみで IoT 開発をしたいと思います。
今回の成果物は YutoMizutani/SwiftyServoControl - GitHub から参照できます。


(動画。カメラモジュールは使用しません)

仕様

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

Client 側

  • UIButton を設置し,押下時に URLSession を用いて通信を行なう

Server 側

  • Vapor を用い,POST を受け取る
  • 指定されたリクエストを契機に,SwiftyGPIOを用いモーターを動作させる (GPIO 制御)

材料

Raspberry Pi のセットアップについては他記事を参照して下さい (分からない方や始めての方は Amazon 等から ピンはんだ接着やSDカードやHDMI変換器やDCアダプタが付いたセット を購入することを強くオススメします)。

サーボモータに関しては何でも良いです。スイッチの操作力に合わせて利用してください。今回はワイドスイッチを用意しました (スイッチ自体の取り替えについては電気工事士の資格が必要です)。
ジャンパワイヤについては,モーターのハウジングが3Pで分離できないという理由から3本だけ必要です。何でも良いですが,片方がオス,もう片方がメスのものを選択してください。

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

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

今回は Raspberry Pi を DebianOS で利用します。
一方で,Swift.org から確認できるのは Xcode (macOS) または Ubuntu の 2 種類のみです。
そこで,Swift Arm を利用します。
packagecloud のリリースページ より,該当の 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 に Vapor を追加するだけでは依存する SSL パッケージの解決はされないため、事前に以下の通りインストールしておきます。

macOS (参考)

$ brew install libressl

Raspberry Pi (Debian) (参考)

$ sudo apt install libressl-dev

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

プロジェクトのテンプレートを作成してくれる,Vapor の CLI ツールをインストールします。
Vapor のドキュメント を元に,以下の通りにインストールします。

残念ながら 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

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

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 を利用します。
同一プロジェクト内で記述することができますが,パッケージを追加する必要があります。

依存パッケージ 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" を追記してください。

テストを除くと AppRun の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 の場合,以下のようなピン配置になっています。

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


./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).RaspberryPiPlusZeropin には (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

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 を指定して下さい。

まとめ

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

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

ライセンス表記

本記事におけるコードのライセンスは YutoMizutani/SwiftyServoControl に記載のライセンスに準じます。

References

欄外参照

YutoMizutani
iOS Tech Lead。RxSwiftが好き。設計が好き。
https://github.com/YutoMizutani
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away