7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Github Actionsを利用して呼び込み君を起動する

Posted at

はじめに

呼び込み君と時計を組み合わせたりミサイルスイッチと組み合わせたりしているうちに、これ、IoTで使えるんじゃないか?と混乱してきた。折角なので役に立つ呼び込み君にしたい。

yobikomi_04.png
呼び込み君ミニ

やりたい事をまとめる

IT屋の悩みと言えば、コンフリクト。コード書いてる最中に元ブランチと修正箇所がかぶってしまう奴。これを何とかしたい。

上記例では元ブランチが変わった事を開発者2に教えてあげたい。開発者2が受動的にmainブランチの更新を知る事で、あらかじめmainを取り込む事ができる。

Githubからプルリクエストのマージを検知して呼び込み君を鳴らす。こんな感じで通知を行う。

Stream(Filter)はmainブランチの変更かどうかを判断する。

結果

コードはmiyatama/github-actions-iot-flow1にまとめてアップしました。

準備

VMインスタンスなどを先に準備しておく

Github

呼び込み君を起動させる起点となるプロジェクトを作成しておく。プロジェクトの構成や言語は何でも良い。プルリクエストが作成出来てマージできれば何でも良い。

github_01.png
マジでどうでもいいって感じのプロジェクト名

Firebase

Github Actionsから呼び出されるプロジェクトを作成しておく。

Firebase Consoleから新規プロジェクトを作成する。

プロジェクト名: github-event-receiver

firebase_02.png

Functionsを利用するので、プロジェクトを作成したらアップグレード

firebase_01.png

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
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。

github_02.png

Firebase

責務: Github Actionsから呼び出され、イベント情報をMQTT Brokerにpublishする。

後はいつもの通りプロジェクトを作成する

# functionsのみチェックを付ける
firebase init

ローカルでプロジェクトテンプレートが作成されたら、amqp-tsの参照を追加してnpm install

package.json
  "dependencies": {
    "firebase-admin": "^10.0.2",
    "firebase-functions": "^3.18.0",
    "amqp-ts": "^1.8.0"
  },

後なぜかエラーがでるので、参照を追加する

npm i --save-dev @types/amqplib

コードはこんな感じ。呼ばれたらそのままamqpを叩く。

index.ts
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
    • amq.topic(mqtt)
      • ToQueue
        • route: device-event

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出力)

yobikomi_06.png

変圧器版

リレーが5v動作なので、NJM7805FAで5vを作る。

yobikomi_07.png

リレー基板

リレーはSS1A05Dを利用。トランジスタ載ってるけど、多分要らない。

yobikomi_08.png

リレー基板の入力は5vで、出力がNPNトランジスタに繋がる。出力のNPNトランジスタにベースを与えるのが、ESP32の信号で駆動するフォトカプラ(TLP621)。
なので、ESP32は変圧器版とは電気的に分離されており別電源で動作する。

呼び込み君

赤丸の2箇所を接続すると呼び込み始めるので、配線を追加してリレー基板につなぐ。

yobikomi_05.png

アプリ

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には、いつかまたチャレンジしたい。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?