ハイライト
- 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 Zero W (約 1,300 円)
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
に以下を追記します。
let serverConfig = NIOServerConfig.default(hostname: "localhost", port: 8080)
services.register(serverConfig)
POST リクエストの待ち受け
今回は POST リクエストを受けて動作させたいので,ルートを追加します。
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 リクエストを受け取れるように以下のように追加します。
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
を編集します。
// 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 できないようで,そのためにターゲットが分かれているようです。
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 と書かれているところにモータの +V, Ground と書かれているところにモータの GND(Ground の略) を繋ぎます。
モータの SIG については,回転状態を伝えるためのもので,GPIO と書かれたピンに差しましょう。
そのピン番号を指定して電流を流し,モータを制御していきます。
./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
を編集します。
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
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/SwiftyServoControl
- URLSession - Apple Developer
- vapor/vapor - GitHub
- uraimo/SwiftyGPIO
- GPIO - raspberrypi.org
- releases - Swift.org
- Swift Arm
- Swift.org - Package Manager
- Cannot build latest Vapor RC2 'openssl/conf.h' file not found #1562 - vapor/vapor
- OpenSSL errors with Vapor 3 on Ubuntu 16.04 #1523 - vapor/vapor
- Vapor docs
- GPIO Pinout Orientation RaspberyPi Zero W