その昔学生時代にRaspberry Piを使ったプロトタイピングをクラウドソーシングで引き受けていました。
あまり細かく言えませんがざっくりいうとクラウド連携をする車載システムの開発です。
とりあえずひとしきり必要なプログラムを開発し終わったときに「じゃあそのプログラムになにか修正が入ったらクラウドにアップロードして稼働中のRaspberry Piのプログラムを更新できるようにしてほしい」
というんです。そんなのやったことないよ…
「そもそも当時はRaspberry Piのプログラムを自動実行したことないのでそこから調べないといけないし無理だよな…」って考えていたらいつの間にか契約が切れました。
まぁ依頼主の態度も気に入らなかったので結果オーライといったところでしょうw。
あれから数年経った今ならある程度知識が身についたしそれぐらい簡単にできるよなとふと思い立ちました。
というわけで今回はAWSを使ってRaspberry Piで動かしてるプログラムを自動更新するシステムを構築してみたのでその記録です。
なお、今回はどんな感じで作ったのか流れをざっくりまとめるだけなので細かい手順は自分でググってもらえると🙏(AWSは日々UIが変わるので細かい手順書くのやめた)
用意するもの
- Raspberry Pi
- 7セグLED(LN516GA)
システム構成
大まかなシステム構成はこんな感じです。
まず、手元でプログラムを修正したら修正したソースコードをS3にアップロードします。ユーザーが行う作業はこれだけです。
あとはコードがアップロードされたことをトリガーにLambdaを立ち上げてAWS IoT経由で通知を行います。
その後Rapsberry PiでS3からコードをダウンロードしてプログラムの更新を行うという流れです。
AWS IoTでの作業
モノ(AWS IoT上でのインスタンスみたいなやつ)を作成し、ポリシーも作成します。
モノの名前はraspi_update_call
とします。
ポリシーは以下の設定でOKです。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "iot:*", "Resource": "*" } ] }
ポリシーと紐付けた証明書を作成し、証明書をダウンロードします。
Raspberry Piと通信をするために必要なファイルはAmazonRootCA1.pem
と【証明書ID】-certificate.pem.crt
、【証明書ID】-private.pem.key
の3つです。
またAWS IoTのトップ画面の設定
からデバイスデータエンドポイント
をメモしておきます。
細かい作業はこの記事がわかりやすいです。
S3の用意
S3バケットを用意します。バケット名はraspi-codes
とします。
IAMの作成
ついでにIAMも用意します。以下の設定をしたポリシーを用意します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListAllMyBuckets" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetObject" ], "Resource": [ "arn:aws:s3:::raspi-codes", "arn:aws:s3:::raspi-codes/*" ] } ] }
SDKの認証で使うため、IAMユーザーを作成するときはプログラムによるアクセス
を選択します。
後の設定はデフォルトのままでOKです。IAMユーザーを作成したらアクセスキーIDとシークレットアクセスキーをメモしておきます。
Lambdaの作成
S3を連携するためのLambda関数を作成します。
設計図
タブをクリックしてs3-get-object-python
を選択します。
ポリシーのアタッチ
Lambda関数の設定→アクセス権限
から実行ロールで表示されているロールの編集
ボタンをクリックします。
そこから以下の内容のポリシーを新たに追加してLambdaの実行ロールにアタッチします。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "iot:Publish", "Resource": "*" } ] }
コードの作成
Lambdaは以下のソースコードに書き換えます。
import json import urllib.parse import boto3 print('Loading function') s3 = boto3.client('s3') iot = boto3.client('iot-data', region_name="ap-northeast-1") def lambda_handler(event, context): # print("Received event: " + json.dumps(event, indent=2)) # Get the object from the event and show its content type bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') try: print(key) iot.publish( topic="raspberry_pi/msg", qos=0, payload=key ) return 'Success' except Exception as e: print(e) print('publish Failed') raise e
Raspberry Piでの作業
ここからRaspberry Piでの作業です。
SDKとCLIをインストール
SDKのインストール
S3の接続に使うboto3とAWS IoTのSDKをインストールします。
sudo pip3 install boto3 AWSIoTPythonSDK
CLIのインストール
sudo pip3 install awscli
Cofigureを設定
aws configure
access key IDとsecret access key、リージョンを設定します。
リージョンは使用するS3のリージョンを設定します。
プログラムを更新するためのコード
以下のレポジトリのsample/basicPubSub/basicPubSub.py
を参考にS3から受け取った通知に合わせてプログラムを更新するコードを作成します。
以下のソースコードと同じディレクトリ上にcert
ディレクトリを用意して、cert
ディレクトリにAWS IoTの作業のときにダウンロードした3つの認証ファイルを保存します。
host
の値は先程メモしたデバイスデータエンドポイントに書き換えます。
ファイル名は、subscriber.py
とします。
''' /* * Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ ''' from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient import boto3 import subprocess import logging import time s3 = boto3.resource('s3') bucket = s3.Bucket('raspi-codes') # Custom MQTT message callback def customCallback(client, userdata, message): msg = message.payload.decode() print("Received a new message: ") print(msg) print("from topic: ") print(message.topic) print("--------------\n\n") if msg == 'disp_7seg.py': updateCode() def updateCode(): file_name = '/home/pi/disp_7seg.py' try: subprocess.call(['rm', file_name]) bucket.download_file('disp_7seg.py', file_name) subprocess.call(['sudo', 'chmod', '755', file_name]) subprocess.call(['sudo', 'systemctl', 'restart', 'disp_7seg.service']) print('Success') except Exception as e: print('Failed') print(e) host = 'yourhost.iot.ap-northeast-1.amazonaws.com' rootCAPath = 'cert/AmazonRootCA1.pem' certificatePath = 'cert/certificate.pem.crt' privateKeyPath = 'cert/private.pem.key' clientId = 'raspi_update_call' topic = 'raspberry_pi/msg' port = 8883 # Configure logging logger = logging.getLogger("AWSIoTPythonSDK.core") logger.setLevel(logging.DEBUG) streamHandler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') streamHandler.setFormatter(formatter) logger.addHandler(streamHandler) # Init AWSIoTMQTTClient myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId) myAWSIoTMQTTClient.configureEndpoint(host, port) myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath) # AWSIoTMQTTClient connection configuration myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20) myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1) # Infinite offline Publish queueing myAWSIoTMQTTClient.configureDrainingFrequency(2) # Draining: 2 Hz myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10) # 10 sec myAWSIoTMQTTClient.configureMQTTOperationTimeout(5) # 5 sec # Connect and subscribe to AWS IoT myAWSIoTMQTTClient.connect() myAWSIoTMQTTClient.subscribe(topic, 1, customCallback) time.sleep(2) # Publish to the same topic in a loop forever while True: time.sleep(1)
7セグLEDを表示するプログラム
Pythonコード
以下の記事を参考にコードを作成します。
使用する7セグLEDはアノードコモンなのでそれを考慮して以下のコードを保存します。
ファイル名はdisp_7seg.py
です。保存先は/home/pi
です。
#!/usr/bin/env python3 import RPi.GPIO as GPIO GPIO.cleanup() GPIO.setmode(GPIO.BCM) GPIO.setup(23, GPIO.OUT) GPIO.setup(24, GPIO.OUT) GPIO.setup(25, GPIO.OUT) GPIO.setup(12, GPIO.OUT) GPIO.setup(16, GPIO.OUT) GPIO.setup(20, GPIO.OUT) GPIO.setup(21, GPIO.OUT) z_0 = [1, 1, 1, 0, 1, 1, 1] z_1 = [1, 0, 0, 0, 1, 0, 0] z_2 = [1, 1, 0, 1, 0, 1, 1] z_3 = [1, 1, 0, 1, 1, 1, 0] z_4 = [1, 0, 1, 1, 1, 0, 0] z_5 = [0, 1, 1, 1, 1, 1, 0] z_6 = [0, 1, 1, 1, 1, 1, 1] z_7 = [1, 1, 0, 0, 1, 0, 0] z_8 = [1, 1, 1, 1, 1, 1, 1] z_9 = [1, 1, 1, 1, 1, 0, 0] numbers = [z_0, z_1, z_2, z_3, z_4, z_5, z_6, z_7, z_8, z_9] def set_segment(s_or, s_om, s_ol, s_mm, s_ur, s_um, s_ul): GPIO.output(23, GPIO.HIGH if s_or == 0 else GPIO.LOW) GPIO.output(24, GPIO.HIGH if s_om == 0 else GPIO.LOW) GPIO.output(25, GPIO.HIGH if s_ol == 0 else GPIO.LOW) GPIO.output(12, GPIO.HIGH if s_mm == 0 else GPIO.LOW) GPIO.output(16, GPIO.HIGH if s_ur == 0 else GPIO.LOW) GPIO.output(20, GPIO.HIGH if s_um == 0 else GPIO.LOW) GPIO.output(21, GPIO.HIGH if s_ul == 0 else GPIO.LOW) while True: try: set_segment(*z_0) except KeyboardInterrupt: break GPIO.cleanup()
配線
Raspberry Piと7セグLEDの配線は以下の図のとおりです。
Serviceファイル
プログラムを実行するときにはsystemctlを使います。
そこで、以下のserviceファイルをdisp_7seg.service
で保存します。
保存先は/etc/systemd/system
です。
[Unit] Description=Display 7seg LED [Service] ExecStart=/home/pi/disp_7seg.py Environment=PYTHONUNBUFFERED=1 Restart=on-failure [Install] WantedBy=default.target
動作確認
いよいよ動作確認です。
7セグLEDを表示するプログラムを起動
はじめに以下のコマンドでdisp_7seg.py
に対して実行権限を変更します。
sudo chmod 755 ~/disp_7seg.py
次に以下のコマンドでデーモンを再読み込みして7セグLEDを起動します。
sudo systemctl daemon-reload sudo systemctl start disp_7seg.service
実行後に以下のように7セグLEDで0を表示したらプログラムは正常に動作しています。
プログラムを更新するコードを起動
subscriber.py
の保存先ディレクトリ上で以下のコマンドでプログラムを実行します。
python3 subscriber.py
今回はプログラムが更新する様子をデモで見せるためにserviceファイルを用意しませんでしたが、disp_7seg.py
と同様にsystemctlを用意して自動起動してもいいと思います。
S3にソースコードをアップロードする
disp_7seg.py
になにか修正を加えてS3バケットにアップロードしてみます。今回は、以下の数字の1を表示するプログラムに書き換えます。
#!/usr/bin/env python3 import RPi.GPIO as GPIO GPIO.cleanup() GPIO.setmode(GPIO.BCM) GPIO.setup(23, GPIO.OUT) GPIO.setup(24, GPIO.OUT) GPIO.setup(25, GPIO.OUT) GPIO.setup(12, GPIO.OUT) GPIO.setup(16, GPIO.OUT) GPIO.setup(20, GPIO.OUT) GPIO.setup(21, GPIO.OUT) z_0 = [1, 1, 1, 0, 1, 1, 1] z_1 = [1, 0, 0, 0, 1, 0, 0] z_2 = [1, 1, 0, 1, 0, 1, 1] z_3 = [1, 1, 0, 1, 1, 1, 0] z_4 = [1, 0, 1, 1, 1, 0, 0] z_5 = [0, 1, 1, 1, 1, 1, 0] z_6 = [0, 1, 1, 1, 1, 1, 1] z_7 = [1, 1, 0, 0, 1, 0, 0] z_8 = [1, 1, 1, 1, 1, 1, 1] z_9 = [1, 1, 1, 1, 1, 0, 0] numbers = [z_0, z_1, z_2, z_3, z_4, z_5, z_6, z_7, z_8, z_9] def set_segment(s_or, s_om, s_ol, s_mm, s_ur, s_um, s_ul): GPIO.output(23, GPIO.HIGH if s_or == 0 else GPIO.LOW) GPIO.output(24, GPIO.HIGH if s_om == 0 else GPIO.LOW) GPIO.output(25, GPIO.HIGH if s_ol == 0 else GPIO.LOW) GPIO.output(12, GPIO.HIGH if s_mm == 0 else GPIO.LOW) GPIO.output(16, GPIO.HIGH if s_ur == 0 else GPIO.LOW) GPIO.output(20, GPIO.HIGH if s_um == 0 else GPIO.LOW) GPIO.output(21, GPIO.HIGH if s_ul == 0 else GPIO.LOW) while True: try: set_segment(*z_1) except KeyboardInterrupt: break GPIO.cleanup()
起動すると以下の動画のように0と表示していた7セグLEDが1に変わることが確認できたら成功です。
まとめ
今回はRaspberry Pi上で動かしているプログラムを自動更新するシステムをAWSで作ってみました。
今までの知識を駆使すれば意外と簡単でした。ただ、AWS IoTのUIが変化していたので過去の記憶を頼りにカンで作業を進めたら思いの外時間がかかりました。
これをあの頃思いついていたら良かったなーw