3.自動給水を行う – SwiftでラズパイのGPIOを操作する
この記事はiOSDC2023で発表したセッション、Swiftでりんごを育ててみたの発表の第3部の内容をまとめた記事になります。
「2.ラズパイでFirebaseにデータを送る – SwiftのCLIツールを動かす」の記事の続編です。もしまだ読んでいない方は読んでいただけると!
今回は④の給水の実装、そして①~④の処理をスケジューリングする方法をまとめます
家庭菜園の植物に自動で給水できるようにする
ラズパイとポンプを使い、給水を制御できる仕組みを作ることを目指します
今回はこちらの記事を参考に実装を進めました
使用するポンプはこちらで12v電源で動きます、リレーはこちらのものです!
最初にポンプを動かした時に12vではない電源を使っていて、動作しなかったので、しっかり12vのものを用意しましょう!
今回はラズパイのGPIOを利用して、このポンプを制御します
そもそものラズパイ(RapberryPi)やGPIOについては、昨年度の発表記事に説明があるので、詳細はそちらを確認してください
今回、配線は以下のようになっています
ちなみにリレーモジュールとは、以下のようなものです
リレーモジュールとは?
電気回路や電子デバイスで高電流や高電圧の回路を低電流・低電圧の信号で制御するためのスイッチの役割を果たす電子部品
今回の実装では、GPIO21ピンから、リレーのスイッチを操作し、12vの電気の流れをON/OFFできるようにします
SwiftyGPIOで実装する
上記のリレーの切り替えの実装を昨年も利用したSwiftyGPIOを利用して行います
なおSwiftyGPIOのコードはLinux環境でしか動作しないので、Linuxでのみ動くことを想定しています
import SwiftyGPIO
final class GPIOManager {
static let shared = GPIOManager()
private let waterDrainGPIO: GPIO
init() {
let gpios = SwiftyGPIO.GPIOs(for: .RaspberryPi4)
waterDrainGPIO = gpios[.P21]! // ①
waterDrainGPIO.direction = .OUT // ②
}
func drainWater() async {
waterDrainGPIO.value = 1
print("Started draining water")
// Can't use UInt64 because raspberry-pi is 32bit
try! await Task.sleep(nanoseconds: 5 * 1_000_000_000)
waterDrainGPIO.value = 0
print("Stopped draining water")
}
}
今回は21ピンを操作するので、21ピン用のインスタンスを用意します(①)
また今回は電流を流すので、direction
を.OUT
に指定します
次にdrainWater
関数の実装です
給水の挙動としては、GPIOピンのインスタンスのvalue
プロパティを1
にすると給水、0
にすると給水停止状態になります
今回は5秒だけ給水するようにしたいので、給水開始→5秒sleep→給水停止という処理を実装しました
今回トラップだったのが、このラズパイのOSが32ビットだったので、Task.sleep(nanoseconds:)
に明示的なUInt64
を渡すとエラーをthrowするので、後続の給水停止処理が実行されない点です
最初はこのsleep時間を以下のように実装していましたが、そうするとエラーがthrowされてしまったので、上記のような実装になっています
func drainWater(second: Int) async {
waterDrainGPIO.value = 1
print("Started draining water")
// Can't use UInt64 because raspberry-pi is 32bit
try! await Task.sleep(nanoseconds: UInt64(second * 1_000_000_000))
waterDrainGPIO.value = 0
print("Stopped draining water")
}
これを実行すると、こんな感じでポンプから水が出ます!
思ったよりもポンプの水圧が高かったので、給水の仕方は要改善です!
スクリプトを定期実行できるようにする
次にこれまで実装した処理をスケジューリングできるようにします
今回は①〜③の処理を1時間に1回、④の処理を1日1回実行できるようにします
そのための準備として、以下の作業を行います
- 上記の処理をそれぞれコマンドのオプションをつけて呼び出せるようにする
systemd
を使って、スケジューリングする
コマンドのオプションを用意する
今回は以下のようなコマンドを実行して、動くようにします
- SwiftGardenPi –captureData
- SwiftGardenPi –drainWater
terminalで実行した時のオプションはCommandLine.arguments
にStringの配列の形式で取得できます
そこでそのオプションをenumに変換できるようにします
enum CommandKind {
case captureData
case drainWater
case switchLight(isON: Bool)
static func converted(from arguments: [String]) -> CommandKind {
print(arguments)
if arguments.isEmpty {
fatalError("Parameter is required!")
} else if arguments.count == 3 {
switch arguments {
case "--switchLight":
switch arguments {
case "isOn":
return .switchLight(isON: true)
case "isOff":
return .switchLight(isON: false)
default:
fatalError("Invalid parameter")
}
default:
fatalError("Invalid parameter")
}
} else if arguments.count == 2 {
switch arguments {
case "--captureData":
return .captureData
case "--drainWater":
return .drainWater
default:
fatalError("Invalid parameter")
}
} else {
fatalError("Parameter is required!")
}
}
}
あとはこのenumをswitchして、それぞれ実行する関数を呼び出し分ければOKです!
systemdでスケジューリングする
そもそもsystemd
とは何かとわからない人向けに(作者も知らなかったので)、ChatGPTに聞いてみました
systemdとは?
systemd
はLinuxシステムの初期化プロセスとして動作し、システムの起動やサービスの管理を担当するシステムおよびセッションマネージャーです。近年の多くの主要なLinuxディストリビューションで、従来のSysVinitやUpstartといった初期化システムの代わりに採用されています。
systemd
の主な特徴や機能には以下のようなものがあります:
- 並行処理: systemdはサービスを並行して起動することで、システムの起動時間を短縮します。
- サービスの依存関係: サービス間の依存関係を定義することで、必要なサービスが利用可能になったときに自動的に起動します。
- システムの状態管理:
systemctl
コマンドを使用して、サービスの起動、停止、再起動、状態の確認などを行うことができます。 - ログ管理:
journald
というコンポーネントを使用してシステムのログを集中的に管理します。 - cgroupの統合: リソースの使用量(CPU、メモリ、ディスクIOなど)を制御・監視することができます。
- タイマー: Cronのように、計画的なジョブの実行をスケジュールすることができます。
- ソケットベースのアクティベーション: サービスは要求が来たときにのみ起動することができるため、リソースを節約できます。
これらの特徴により、systemd
はLinuxシステムの起動と運用の効率性を向上させるとともに、管理の容易さも提供しています。ただし、その設計思想や独自の方法について賛否両論があります。
これを使用すると、指定時刻に指定のスクリプトを実行するといったことができるようになります
今回はこちらの記事を参考にさせてもらいました
例えば給水処理の場合、以下のような設定をしました
実行するスクリプトは、ログ用のファイル出力もしたいので、別のファイルを用意します
#!/bin/bash
current_date=$(date +"%Y%m%d%H%M%S")
output_file="/home/${HOME_DIRECTORY_NAME}/result/Result/DrainWater/output_drain_water_${current_date}.txt"
SwiftGardenPi --drainWater >> $output_file
次にサービスファイル(1)をタイマーファイル(2)というものを用意します
# 1
sudo nano /etc/systemd/system/drain_water.service
# 2
sudo nano /etc/systemd/system/drain_water.timer
サービスファイルは以下のようにしています
[Unit]
Description=Drain water automatically(This is comment)
After=network.target
[Service]
ExecStart=/path/to/your/drain_water.sh
WorkingDirectory=/home/${HOME_DIRECTORY_NAME}/${APP_DIRECTORY}
ExecStart
に実行するスクリプトファイルのパスを記載しますDescription
はわかりやすいコメントを書いています
[Unit]
Description=Drain water every 9 A.M.
After=network.target
[Timer]
OnCalendar=*-*-* 09:00:00
[Install]
WantedBy=timers.target
タイマーファイルでは、OnCalendar
の箇所にどのタイミングで実行するかを指定します
記載方法はこちらに詳細があります
これで以下のコマンドを実行して、ラズパイを再起動すればOKです!
sudo systemctl enable drain_water.timer
これで指定時刻になると、指定の処理を実行し、かつ実行後にそのログをログのファイルに残します
ログに残るのは、swiftでprint
したものなどです
これでラズパイ側の実装は完了です!
まとめ
この記事では、SwiftGardenの発表の3部の自動給水の内容をまとめました
次の記事は「4. iOSアプリでデータを確認する – Swift Chartsの実装」です!