はじめに
呼び込み君と時計を組み合わせたりミサイルスイッチと組み合わせたりしているうちに、これ、IoTで使えるんじゃないか?
と混乱してきた。折角なので役に立つ呼び込み君にしたい。
やりたい事をまとめる
IT屋の悩みと言えば、コンフリクト。コード書いてる最中に元ブランチと修正箇所がかぶってしまう奴。これを何とかしたい。
上記例では元ブランチが変わった事を開発者2に教えてあげたい。開発者2が受動的にmainブランチの更新を知る事で、あらかじめmainを取り込む事ができる。
Githubからプルリクエストのマージを検知して呼び込み君を鳴らす。こんな感じで通知を行う。
Stream(Filter)はmainブランチの変更かどうかを判断する。
結果
コードはmiyatama/github-actions-iot-flow1にまとめてアップしました。
準備
VMインスタンスなどを先に準備しておく
Github
呼び込み君を起動させる起点となるプロジェクトを作成しておく。プロジェクトの構成や言語は何でも良い。プルリクエストが作成出来てマージできれば何でも良い。
Firebase
Github Actionsから呼び出されるプロジェクトを作成しておく。
Firebase Consoleから新規プロジェクトを作成する。
プロジェクト名: github-event-receiver
Functionsを利用するので、プロジェクトを作成したらアップグレード
RabbitMQ Broker
Firebaseからの通知受け取り、呼び込み君への通知を行う。
VMインスタンスの構成は次の通り
column | value |
---|---|
name | github-actions-amqp-broker |
region/zone | asia-northeast1/asia-northeast1-b |
machine type | e2-standard-2 |
boot disk | debian 10(buster) / 100[GB] |
network service tag | amqp-broker-service |
インスタンスが作成出来たらファイウォールのルールを作成する。
RabbitMQ管理画面のルール
column | value |
---|---|
name | rabbitmq-management |
traffic direction | inbound |
network service tag | amqp-broker-service |
source filter | 0.0.0.0/0 |
port | 15672/tcp |
RabbitMQのBrokerルール
column | value |
---|---|
name | rabbitmq-broker |
traffic direction | inbound |
network service tag | amqp-broker-service |
source filter | 0.0.0.0/0 |
port | 5672/tcp |
RabbitMQのMQTTルール
column | value |
---|---|
name | rabbitmq-mqtt-broker |
traffic direction | inbound |
network service tag | amqp-broker-service |
source filter | 0.0.0.0/0 |
port | 1883/tcp |
呼び込み君
下記を準備しておく。ミサイルスイッチがあるとかっこいいのでミサイルスイッチを使う。
- 本体
- ESP32 DevKit(ブレイクアウト済みのESP32基板)
- 呼び込み君ミニ
- 単三電池4本
- 12[v]昇圧基板
- ミサイルスイッチ
- 5[v]降圧基板
- リレー基板(5[v]駆動)
- 開発環境
- Windows
- Arduino IDE
実装
Github Actions
責務: mainブランチにマージされたらFirebaseに知らせる。
まずはGithub Actionsを一通り読み込んで何が出来るかを確認する。ざっと読むとpull_requestのcloseでマージを確認できそう。と言うかばっちりな記述がRunning your workflow when a pull request marges
って所に記載があったので、これを元に作成する。
監視対象のGithubリポジトリ内にworkflowのyamlを作成する。
cd /path/to/target/project
mkdir -p .github/workflows
touch .github/workflows/main_merge.yaml
on:
pull_request:
types:
- closed
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- run: |
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"event":"merge", "branch":"'${{github.base_ref}}'"}' \
https://us-central1-github-event-receiver.cloudfunctions.net/githubActionsEvent
適当にPull Requestを作ってマージしてみる。下記の様にActionの実行が確認できればOK。
Firebase
責務: Github Actionsから呼び出され、イベント情報をMQTT Brokerにpublishする。
後はいつもの通りプロジェクトを作成する
# functionsのみチェックを付ける
firebase init
ローカルでプロジェクトテンプレートが作成されたら、amqp-ts
の参照を追加してnpm install
。
"dependencies": {
"firebase-admin": "^10.0.2",
"firebase-functions": "^3.18.0",
"amqp-ts": "^1.8.0"
},
後なぜかエラーがでるので、参照を追加する
npm i --save-dev @types/amqplib
コードはこんな感じ。呼ばれたらそのままamqpを叩く。
import * as functions from "firebase-functions";
import * as Amqp from "amqp-ts";
export const githubActionsEvent =
functions.https.onRequest((request, response) => {
functions.logger.info("receive data: ", request.body);
const user = "rabbitmq";
const password = "rabbitmq";
const rabbitMqHost = "RabbitMQ BrokerのIPアドレス";
const rabbitMqEndpoint =
"amqp://" + user + ":" + password + "@" + rabbitMqHost;
const connection = new Amqp.Connection(rabbitMqEndpoint);
const msg = new Amqp.Message(JSON.stringify(request.body));
const exchange = connection.declareExchange(
"amq.direct",
"direct",
{durable: true});
connection.completeConfiguration().then(() => {
exchange.send(msg, "github-actions-event");
functions.logger.info("AMQP publish: ", msg);
setTimeout(function() {
connection.close();
}, 500);
response
.status(200)
.send("AMQP publish succeed");
}).catch((err) => {
functions.logger.error("AMQP connection error: ", err);
response
.status(500)
.send("AMQP connection error");
});
});
RabbitMQ Broker
責務: github-actions-event, device-eventのexchangeを提供する。
RabbitMQの設定は下記の通り
- Exchange
- amq.direct
- ToQueue
- route: github-actions-event
- route: device-event
- ToExchange
- amq.topic
- ToQueue
- amq.topic(mqtt)
- ToQueue
- route: device-event
- ToQueue
- amq.direct
Dockerのセットアップを行う。
sudo apt update
sudo apt upgrade -y
sudo apt install -y \
git
git clone https://github.com/miyatama/github-actions-iot-flow1.git
cd ./github-actions-iot-flow1/rabbitmq-broker/
./docker-setup.sh
Dockerがインストール出来たらコンテをビルドして実行
cd rabbitmq-service
find . -type f -name *.sh | xargs -n 1 -I % chmod +x %
sudo docker-compose build
sudo docker-compose up -d
コンテナは下記の通り。
container | description |
---|---|
broker | exchangeを提供するブローカー |
stream-processor | github-actions-eventを読み取ってdevice-eventへ流すフィルタリングを行う。 |
github-actions-consumer | 挙動確認用。github-actions-eventトピックを購読してログに吐く。 |
device-event-consumer | 挙動確認用。device-eventトピックを購読してログに吐く。 |
dummy-producer | 挙動確認用。github-actions-eventトピックへ試験用のメッセージを送信する。 |
mqtt-device-event-consumer | 挙動確認用。mqttのdevice-eventトピックを購読してログを吐く。 |
詳細はrabbitmq-brokerを参照。
キモはstream-processor。amq.directのgithub-actions-eventを購読してmainブランチへのマージをフィルタリングしてamq.directのdevice-eventへ呼び込み君起動メッセージを送る。
defp consume(channel, tag, redelivered, payload) do
:ok = Basic.ack channel, tag
Logger.info("StreamApp.Consumer.consume() - #{payload}")
Jason.decode!(payload, [{:keys, :atoms}])
|> publish(channel)
rescue
exception ->
:ok = Basic.reject channel, tag, requeue: not redelivered
IO.puts "Error: #{exception}"
end
defp publish(%{event: event, branch: branch}, channel) when event == "merge" and branch == "main" do
Logger.info("publish(channel, map)")
status = AMQP.Basic.publish(channel, "amq.direct", publish_topic(), "{\"event\": \"wakeup\", \"device\": \"yobikomi\"}")
Logger.info("publish status: #{status}")
end
defp publish(data, channel) do
Logger.info("not rise device event")
Logger.info(data)
end
呼び込み君
責務: device-eventを監視し、イベント受領後に呼び込み君を鳴らす。
ESP32-WROOM-32を使って構築する。何となくの回路構成はこんな感じ。
やる気が出たらちゃんと書きます
回路
昇圧基板
単三電池4本でミサイルスイッチ(v12)を動作させる為の12v昇圧基板。
ストロベリーリナックスで買った。
LTC3111 昇降圧DC-DCコンバータモジュール(2.5V~15V出力)
変圧器版
リレーが5v動作なので、NJM7805FAで5vを作る。
リレー基板
リレーはSS1A05Dを利用。トランジスタ載ってるけど、多分要らない。
リレー基板の入力は5vで、出力がNPNトランジスタに繋がる。出力のNPNトランジスタにベースを与えるのが、ESP32の信号で駆動するフォトカプラ(TLP621)。
なので、ESP32は変圧器版とは電気的に分離されており別電源で動作する。
呼び込み君
赤丸の2箇所を接続すると呼び込み始めるので、配線を追加してリレー基板につなぐ。
アプリ
ESP32ではじめようIoT開発ハンズオン 実装用資料を参考にArduino IDEの環境を整える。
呼び込み君制御で利用するGPIOは2つ
- IO15: 呼び込み君起動リレーへ接続
- IO33: WiFi接続状態を表す
#include <WiFi.h>
#include <PubSubClient.h>
const char ssid[] = "wifi-ssid";
const char pass[] = "wifi-password";
const char mqttBroker[] = "34.84.177.33";
const int mqttPort = 1883;
const char mqttUser[] = "mqtt-user";
const char mqttPassword[] = "mqtt-pwd";
const char topic[] = "device-event";
WiFiClient net;
PubSubClient client(net);
unsigned long lastMillis = 0;
void blinkBehavior() {
digitalWrite(33, HIGH);
delay(100);
digitalWrite(33, LOW);
}
void connect() {
blinkBehavior();
Serial.print("checking wifi...");
while (WiFi.status() != WL_CONNECTED) {
blinkBehavior();
Serial.print(".");
delay(1000);
}
digitalWrite(33, HIGH);
client.setServer(mqttBroker, mqttPort);
Serial.print("\nconnecting...");
while (!client.connected()) {
String clientId = getMacAddr();
if(client.connect(clientId.c_str(), mqttUser, mqttPassword)) {
client.setKeepAlive(5);
client.subscribe(topic);
client.setCallback(subscribeTopic);
break;
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
Serial.println("\nconnected!");
client.loop();
}
void subscribeTopic(char* topic, byte* payload, unsigned int length) {
Serial.print("receive topic: ");
Serial.println(topic);
digitalWrite(15, HIGH);
delay(1000);
digitalWrite(15, LOW);
}
String getMacAddr()
{
byte mac[6];
char buf[50];
WiFi.macAddress(mac);
sprintf(buf, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, pass);
pinMode(15, OUTPUT);
digitalWrite(15, LOW);
pinMode(33, OUTPUT);
digitalWrite(33, LOW);
connect();
}
void loop() {
delay(10); // <- fixes some issues with WiFi stability
if (!client.connected()) {
connect();
}
client.loop();
}
ふりかえり
GW中にやってしまおうと思っていたが、色々手を出している内に5月下旬になってしまった。
当初はAtomVMを利用してAMQPを動かそうとしてたけど、ビルド環境として使っていたRaspberryPi 3 Type B ver 1.2との相性が悪すぎるらしく断念。
- AtomVMがビルドできない
- AtomVMのビルドに成功してもバイナリが動かない
- AtomVMのビルドに成功してバイナリが動いてもGPIOでエラー
- etc
Docker使える環境ならとくに苦労なくできるんだろうなと思う。ビルドだけDockerでやってもよかったけど、その前にAtomVMへの熱が冷めてしまった。
直接ESP-IDF使おうとしたらidf.py
の実行で既にエラー。なんだかよく分からない鬼門に入ったので、結局Windows + ArduinoIDEでやる事にした。
アプリが出来たら出来たで降圧基板からの3vでESP32 DevKitが動かないなど、最後までトラブルとの戦いだった。
ESP32 DevKitは意外と電流喰うんだなーと思った。
AtomVMには、いつかまたチャレンジしたい。