#本記事の内容
- Raspberry PiにインストールしたAlexaを遠隔で自由にしゃべらせたいと思い、LINE経由でAlexaをしゃべらせることが出来るシステムを構築しました。
Alexaに指示を出すアプリとしてLINEを活用しました。システムは以下のように連携しています。
-
LINEで送ったメッセージが、LINEプラットフォームのLINE Messaging API経由で、Google Cloud Functionに連携される。
-
Google Cloud Functionは、受け取ったデータの中からテキストデータのみをGoogleのNoSQL DBであるCloud Firestoreに登録する。
-
Raspberry Piで稼働しているPythonアプリが、Cloud Firestoreのデータ更新を監視しており、データ更新時にそのデータをCloud Firestoreから取得する。
-
Pythonアプリが取得したメッセージでAlexaの音声再生スクリプトを実行し、AlexaをLINEで指定したメッセージでしゃべらせる。
LINE Messaging APIから連携されるWebhookを受け取る方法として、Cloud Functions for Firebaseで作成したアプリを利用することも可能ですが、FirabaseのCloud Functionの開発言語はJavaScriptだけで、Pythonが利用できないので、Google Cloud PlatformのCloudFunctionを利用することにしました。
本記事ではこれらのシステム構築に必要なことをまとめました。
#実施環境
-
ローカルPC環境
- Windows 10
- TeraTarm Version 4.9.4
-
ラズベリーパイ
-
Raspberry Pi 2 Model B
-
PLANEX 無線LAN子機 GW-USNANO2A ※Raspberry Pi 2はWi-Fi通信モジュールがないため、無線LAN子機をUSBに接続しWi-Fi通信を行う。
-
家庭内の無線LANネットワーク環境にて実施
-
音声認識マイク
-
マイクをUSBに接続する変換機
-
スピーカ
-
Python 3.9.7
-
Alexa Voice Service (AVS) Device SDK 1.24.0
-
Snowboy 1.3.0
-
-
クラウド環境
- LINE Messaging API
- Google Cloud Platform(以降、GCPと略す)
- Cloud Function
- Google Firebase
- Cloud Firestore
#手順の流れ
- 1.Cloud Firestoreの構築
- 1.1.プロジェクトの作成
- 1.2.Cloud Firestoreの設定
- 1.3.認証キーの取得
- 2.Cloud Functionの構築
- 2.1.Cloud Functionの有効化
- 2.2.関数の作成
- 3.LINE Messaging APIの構築
- 3.1.LINE Messaging API用のLINEアカウント作成とWebhook設定
- 4.Raspberry Piの設定
- 4.1.TeraTarmでsshログインする
- 4.2.Firebase環境の構築
- 4.3.Cloud Storeへのテスト接続
- 4.3.Pythonアプリの配置
- 5.動作確認
- 5.1.Alexaの起動
- 5.2.Pythonアプリの起動
- 5.3.LINEでテスト
1.Cloud Firestoreの構築
1.1.プロジェクトの作成
Google Firebaseのコンソールにログインし、プロジェクトを作成
をクリックする。
プロジェクト名
を入力し、規約を確認し同意のチェックをクリックし、続行
をクリックする。
Google アナリティクスの設定画面が出るので必要に応じて有効化する。
私は使わないため有効にするのチェックを外し、プロジェクトを作成
をクリック。
「新しいプロジェクトの準備ができました」のメッセージが出るので、続行
をクリックする。
1.2.Cloud Firestoreの設定
左のメニューからFirestore Database
をクリックし、データベースの作成
をクリックする。
ロケーションに変更の必要がなければそのままの設定で、有効にする
をクリックする。
コレクションIDを入力し、次へ
をクリックする。※ここの値はPythonアプリで利用するのでメモしておく。
1.3.認証キーの取得
プロジェクトの概要の隣の設定ボタン(歯車) > プロジェクトの設定
をクリック
プロジェクトの設定上部のタブのサービスアカウント
をクリックし、Admin SDK 構成スニペットをPython
に変更し、新しい秘密鍵の生成
をクリックする。
ここで取得した秘密鍵はラズパイからCloud Firestoreにアクセスする際に利用する。
2.Cloud Functionの構築
2.1.Cloud Functionの有効化
前提としてすでに、GCPにプロジェクトを作成済みであること。
もし未作成であれば、こちらの記事を参考に作成してください。
ラズパイ連携に向けたGoogleSheetsAPIにおけるサービスアカウントの作成
GCPのコンソールを開いて、左のメニューからCloud Function
をクリックします。
2.2.関数の作成
関数名、リージョン、トリガーのタイプ(HTTP)、認証(未認証の呼び出しを許可)を設定し、次に進む。
ここで表示されるURLは、LINEのWebhook設定で利用するのでメモしておくこと。
※本当であれば、認証ありでやりたいのですが、LINE Messagin APIのWebhookの仕組みにCloud Functionの認証を組み込めないので、未認証とします。
ランタイムの内容はデフォルトで問題なければそのまま進み、画面下部の次へ
をクリックする。
メッセージでCloud Build APIの有効化を促されたので、APIを有効にする
をクリックして有効化する。
関数の編集画面に戻り、ランタイムをPython 3.9
に変更し、エントリポイントをmain()
に変更する。
main.pyの関数には、LINEのWebhook応答用に暫定的に以下のコードを入力する。
def main(request):
return {"statusCode": 200}
左側のrequirements.txtには以下の値を記載しておく。
# Function dependencies, for example:
# package>=version
line-bot-sdk
firebase-admin
左側の+
ボタンを押下し、1.3で取得した認証キーファイルと同じものを作成しておく。
ファイル名は、serviceAccountKey.jsonしておく。
{
"type": "service_account",
"project_id": "",
"private_key_id": "",
"private_key": "",
"client_email": "",
"client_id": "",
"auth_uri": "",
"token_uri": "",
"auth_provider_x509_cert_url": "",
"client_x509_cert_url": ""
}
変更後の画面はこのような形になるので、最後にデプロイ
をクリックする。
★★ 3.1の手順最後にあるLINEのWebhookの検証が完了したら、main.pyは以下の本番用のソースコードに差し替えること。 ★★
import base64
import hashlib
import hmac
import os
import firebase_admin
from firebase_admin import firestore
from firebase_admin import credentials
from flask import abort
from linebot import (
LineBotApi, WebhookParser
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage
)
def main(request):
channel_secret = os.environ.get('LINE_ACOUNT_SECRET')
channel_access_token = os.environ.get('LINE_ACCOUNT_ACCESS_TOKEN')
line_bot_api = LineBotApi(channel_access_token)
parser = WebhookParser(channel_secret)
# Line Message APIのリクエスト検証
body = request.get_data(as_text=True)
hash = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
signature = base64.b64encode(hash).decode()
if signature != request.headers['X_LINE_SIGNATURE']:
return abort(405)
try:
events = parser.parse(body, signature)
except InvalidSignatureError:
return abort(405)
# テキストメッセージがWebhookされたときだけ処理を行う。
for event in events:
if not isinstance(event, MessageEvent):
continue
if not isinstance(event.message, TextMessage):
continue
print(f'event message : {event.message} , type : {type(event.message)}')
print(f'event message text : {event.message.text}')
# Firebase 初期化
if not firebase_admin._apps:
cred = credentials.Certificate("./serviceAccountKey.json")
firebase_admin.initialize_app(cred)
db = firestore.client()
# テキストメッセージの登録
doc_ref = db.collection(u'line-message')
doc_ref.add({
u'text': f'{event.message.text}'
})
# 応答メッセージを返却
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=event.message.text + " をお家ちゃんが読み上げます")
)
return {"statusCode": 200}
また、Google Cloud Founctionのランタイム環境変数に以下を設定する。
LINE_ACOUNT_SECRET:LINE Messageing APIのチャネルシークレット
LINE_ACCOUNT_ACCESS_TOKEN:LINE Messageing APIのチャネルアクセストークン
そうすることで、Line Message APIのリクエスト検証が実施される。
3.LINE Messaging APIの構築
3.1.LINE Messaging API用のLINEアカウント作成とWebhook設定
LINE Developersのウェブページにアクセスし、ログイン
をクリックする。
画面の右下にある言語設定を"English"から"日本語"に変更する。
"開発者名(Developer name)"と"メールアドレス(Your email)"を入力し、契約を確認し同意のチェックをクリックし、アカウントを作成
をクリックする。
ポータル画面に遷移するので画面下部の新規プロバイダー作成
をクリックする。
"チャンネル名"、"チャンネル説明"、"大業種"、"小業種"の情報を入力し、規約の同意をチェックし、作成
をクリックする。
チャンネルが作成されたので、"Messaging API設定"をクリックする。
下にスクロールするとWebhook URL設定があるので、編集
をクリックする。Webhook URLに値を入力し更新
をクリックする。
URLには先程Cloud Functionで設定したURLを設定する。
検証をクリックする。問題なければ、以下のように「成功」のメッセージが表示される。
3.2.LINE Messaging API用の自動応答メッセージの無効化
上記の画面で「応答メッセージ」の右側にある「編集」をクリックする。
「ステータス」が「オン」になっているので「オフ」に切り替える。
こちらを行うことで、Google Cloud Floumctionで生成したリプライメッセージが返却できる。
4.Raspberry Piの設定
4.1.TeraTarmでsshログインする
WindowsにてTeraTarmを起動して、ラズパイにSSH接続する。
4.2.Firebase環境の構築
・Firebase用のディレクトリ作成
cd /home/pi;pwd
mkdir firestoreapp
ls -ld firestoreapp
・Pythonの仮想環境作成
cd /home/pi/firestoreapp;pwd
python -m venv --system-site-packages ./venv
source ./venv/bin/activate
python -V
pip install --upgrade pip
・Firebaseのライブラリーを追加する
pip install --upgrade firebase-admin
上記のコマンドで、google-cloud-coreとgoogle-cloud-firestoreも入るはず。
ここでインストールされるgrpcio-1.41.1に後々苦しめられることになった。。対処法は次の内容。
・grpcioの再インストール
Pythonアプリを動かす中で以下のようなエラーが発生した。
★エラー1
from grpc._cython import cygrpc as _cygrpc
ImportError: /home/pi/firestoreapp/venv/lib/python3.9/site-packages/grpc/_cython/cygrpc.cpython-39-arm-linux-gnueabihf.so: undefined symbol: __atomic_exchange_8
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/pi/firestoreapp/./rs-listener-test.py", line 3, in <module>
from firebase_admin import firestore
File "/home/pi/firestoreapp/venv/lib/python3.9/site-packages/firebase_admin/firestore.py", line 28, in <module>
raise ImportError('Failed to import the Cloud Firestore library for Python. Make sure '
ImportError: Failed to import the Cloud Firestore library for Python. Make sure to install the "google-cloud-firestore" module.
★エラー2
File "/home/pi/firestoreapp/venv/lib/python3.9/site-packages/grpc/__init__.py", line 22, in <module>
from grpc import _compression
File "/home/pi/firestoreapp/venv/lib/python3.9/site-packages/grpc/_compression.py", line 15, in <module>
from grpc._cython import cygrpc
ImportError: /lib/arm-linux-gnueabihf/libc.so.6: version `GLIBC_2.33' not found (required by /home/pi/firestoreapp/venv/lib/python3.9/site-packages/grpc/_cython/cygrpc.cpython-39-arm-linux-gnueabihf.so)
これらのエラーについていろいろ調査しトライ&エラーしたところ、これから記載する対応でクリアできた。
どうやらfirebase-adminのインストール途中にgrpcioのインストールがこけていたり、
grpcioが要求するglibcバージョンがインストールされているglibcのバージョンと異なっているためにエラーとなっているらしいと情報を掴んだため、
少し前の世代のgrpcioのインストールパッケージをPyPiから取得し、強制的に再インストールすることにした。
PyPIのgrpcioのページに行き、arm用の「grpcio-1.40.0-cp39-cp39-linux_armv7l.whl」をPCにダウンロードする。
ダウンロードしたパッケージをTera Tarmのscp機能を用いて、ラズパイに配置し、以下のコマンドで再インストールを行う。
pip install --force-reinstall grpcio-1.40.0-cp39-cp39-linux_armv7l.whl
※このコマンドは、ダウンロードしたパッケージを置いた場所で実施すること。
TeraTarmのファイル転送機能を利用し、1.3で取得した認証キーをラズパイのFirebaseディレクトリに格納する。
ls -l /home/pi/firestoreapp/serviceAccountKey.json
4.3.Cloud Storeへのテスト接続
接続確認用のアプリ(rs-listener-test.py)を配置し、実行権限を付与する。
ls -l /home/pi/firestoreapp/rs-listener-test.py
chmod 755 /home/pi/firestoreapp/rs-listener-test.py
ls -l /home/pi/firestoreapp/rs-listener-test.py
接続確認用のアプリのコードは以下のようなもの。
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
def main():
print('auth start')
cred = credentials.Certificate("./serviceAccountKey.json")
firebase_admin.initialize_app(cred)
print('auth finish')
# Firestore アクセス
db = firestore.client()
doc_ref = db.collection(u'line-message') #Firesotre構築時に記載したコレクションIDを指定する。
# テストデータの登録
doc_ref.add({
u'text': u'Test Access Succes'
})
# テストデータの読み込み
docs = doc_ref.stream()
for doc in docs:
print(f'{doc.id} => {doc.to_dict()}')
if __name__ == '__main__':
main()
仮想環境の有効化
cd /home/pi/firestoreapp;pwd
source ./venv/bin/activate
接続確認用アプリの実行
python ./rs-listener-test.py
接続が上手くいけば以下のようなデータがログとして出力されます。
(venv) pi@raspberrypi:~/firestoreapp $ python ./rs-listener-test.py
auth start
auth finish
6WNqnDJCuee2NK4Zrb7y => {'text': 'Test Access Succes'}
VOd4hHeB8NORqkHW5eNU => {}
(venv) pi@raspberrypi:~/firestoreapp $
Firestoreのコンソール上でも以下のようにテストデータが値が追加されたことが分かります。
(接続用アプリ実行後)
Test Access Succesのデータが登録されています。
4.3.Pythonアプリの配置
本番用のアプリ(rs-listener.py)をTeratarmのSCP機能を用いて配置し、実行権限を付与する。
ls -l /home/pi/firestoreapp/rs-listener.py
chmod 755 /home/pi/firestoreapp/rs-listener.py
ls -l /home/pi/firestoreapp/rs-listener.py
アプリのコードは以下です。
import subprocess
import threading
import time
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
def auth():
print('auth start')
# Firebase 初期化
if not firebase_admin._apps:
cred = credentials.Certificate("/home/pi/firestoreapp/serviceAccountKey.json")
firebase_admin.initialize_app(cred)
print('auth finish')
def listen_document():
# [START firestore_listen_document]
print('client set')
db = firestore.client()
# Create an Event for notifying main thread.
callback_done = threading.Event()
print('set callback')
# Create a callback on_snapshot function to capture changes
def on_snapshot(doc_snapshot, changes, read_time):
for doc in doc_snapshot:
print(f'Received document snapshot: {doc.id} ,data : {doc.to_dict()} ,message : {doc.to_dict().get("text")}')
cmd = ["/home/pi/tool/alexa-remote-control/alexa_remote_control.sh", "-e", f"""speak:<amazon:domain name='conversational'>{doc.to_dict().get("text")}</amazon:domain>"""]
subprocess.call(cmd)
db.collection(u'line-message').document(doc.id).delete()
callback_done.set()
doc_ref = db.collection(u'line-message')
# Watch the document
doc_watch = doc_ref.on_snapshot(on_snapshot)
print('set Watch the document')
# [END firestore_listen_document]
if __name__ == '__main__':
auth()
listen_document()
while True:
time.sleep(5)
5.動作確認
5.1.Alexaの起動
以下のコマンドで起動。
cd /home/pi/sdk-folder/sdk-build
PA_ALSA_PLUGHW=1 ./SampleApp/src/SampleApp ./Integration/AlexaClientSDKConfig.json ../third-party/snowboy/resources INFO
※Alexaのインストールについては、Raspberry piのスマートスピーカー化 (Amazon Alexaの公式インストールシェルが利用できないのでソースからインストールしてみた)の記事を参照ください。
5.2.Pythonアプリの起動
仮想環境の有効化
cd /home/pi/firestoreapp;pwd
source ./venv/bin/activate
Pythonアプリの実行
python ./rs-listener.py
以下のようなログが出力され、Cloud Firestoreの更新待ちとなる。
(venv) pi@raspberrypi:~/firestoreapp $ python ./rs-listener.py
auth start
auth finish
client set
set callback
set Watch the document
5.3.LINEでテスト
ここまでで動作確認の準備は終了。
以下の動画のように、LINEを使ってメッセージを入力してみました。
ラズベリーパイにインストールしたAmazon AlexaをLINEで自由に喋らせたく連携システムを構築しました。構成は、LINEアプリ×LINE Messaging Api×Google Cloud Function×Cloud Firestore×ラズパイ内のpythonアプリとalexa。動画は、LINEで入力したメッセージをalexaが喋る様子です。 pic.twitter.com/eOpaeB4D6p
— kaname kun (@kanamekunkun) November 25, 2021
LINEに入力した「こんにちは、今日はいい天気ですね」が、無事 Alexaから再生され、やりたいことが実現できました。
いろいろと手こずりましたが、何とか達成できて嬉しいです。
また、LINEで入力したメッセージを無視されることがあることが分かりました。
応答メッセージがないので、LINE Messaging APIとCloud Function間の連携でこけていると予想します。
やりたいことが出来たのでいったん完了とし、調査はまた今度とします。
追記
追記1(2021/11/26).LINEで入力したメッセージが無視されることがある件の改善
また、LINEで入力したメッセージを無視されることがあることが分かりました。
応答メッセージがないので、LINE Messaging APIとCloud Function間の連携でこけていると予想します。
やりたいことが出来たのでいったん完了とし、調査はまた今度とします。
こちら調査したところ、Cloud Functionにおけるfirebase_adminの初期化処理において、初期化済みであるにも関わらずも一度初期化を行ったときに発生するエラーで処理がこけていました。
check if a Firebase App is already initialized in python
★エラー
in initialize_app raise ValueError(( ValueError: The default Firebase app already exists. This means you called init ialize_app() more than once without providing an app name as the second argument. In most cases you only need to cal l initialize_app() once. But if you do want to initialize multiple apps,
pass a second argument to initialize_app() to give each app a unique name.
関数の一回目と二回目の起動時間が短いと、一回目の関数で初期化したfirebase_adminが破棄されずに残っているようです。
プログラムの中で、firebase_admin._appsが残っているかどうか判定してから初期化するコードに改善しました。
# Firebase 初期化
if not firebase_admin._apps:
cred = credentials.Certificate("./serviceAccountKey.json")
firebase_admin.initialize_app(cred)
追記2(2021/11/26). Pythonのアプリの自動起動設定
Alexaを自動起動しているのに、わざわざPythonアプリを手動起動させるのは面倒なので、自動起動設定をしました。
以下のファイルの読み込みをフルパス指定に変更。
cred = cedentials.Certificate("/home/pi/firestoreapp/serviceAccountKey.json")
起動スクリプトの作成。
vi /home/pi/alexa-line-control.sh
chmod 755 /home/pi/alexa-line-control.sh
alexa-line-control.shシェルの中身は以下の通り。
#!/bin/bash
source /home/pi/firestoreapp/venv/bin/activate
python /home/pi/firestoreapp/rs-listener.py
自動起動用のファイルを作成する。
sudo vi /etc/systemd/system/lineControler.service
ファイルの中身は以下の通り。
[Unit]
Description = Line Controler for Alexa
[Service]
Restart = always
WorkingDirectory=/home/pi/
ExecStart = /home/pi/alexa-line-control.sh
ExecReload = /bin/kill -s HUP ${MAINPID}
ExecStop=/bin/kill -s TERM ${MAINPID}
user=pi
[Install]
WantedBy = multi-user.target
システムコマンドで起動できるか確認。
sudo systemctl start lineControler.service
sudo systemctl status lineControler.service
起動すると以下のようなメッセージが出る。
pi@raspberrypi:~ $ sudo systemctl status lineControler.service
● lineControler.service - Line Controler for Alexa
Loaded: loaded (/etc/systemd/system/lineControler.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2021-11-26 13:36:42 JST; 4s ago
Main PID: 1241 (alexa-line-cont)
Tasks: 2 (limit: 1935)
CGroup: /system.slice/lineControler.service
tq1241 /bin/bash /home/pi/alexa-line-control.sh
mq1242 python /home/pi/firestoreapp/rs-listener.py
11月 26 13:36:42 raspberrypi systemd[1]: Started Line Controler for Alexa.
pi@raspberrypi:~ $
システムコマンドで停止できるか確認。
sudo systemctl stop lineControler.service
sudo systemctl status lineControler.service
停止が上手くいけば以下のようなメッセージが出る。
pi@raspberrypi:~ $ sudo systemctl status lineControler.service
● lineControler.service - Line Controler for Alexa
Loaded: loaded (/etc/systemd/system/lineControler.service; enabled; vendor preset: enabled)
Active: inactive (dead) since Fri 2021-11-26 13:38:00 JST; 4s ago
Process: 1241 ExecStart=/home/pi/alexa-line-control.sh (code=killed, signal=TERM)
Process: 1292 ExecStop=/bin/kill -s TERM ${MAINPID} (code=exited, status=0/SUCCESS)
Main PID: 1241 (code=killed, signal=TERM)
11月 26 13:36:42 raspberrypi systemd[1]: Started Line Controler for Alexa.
11月 26 13:38:00 raspberrypi systemd[1]: Stopping Line Controler for Alexa...
11月 26 13:38:00 raspberrypi systemd[1]: lineControler.service: Main process exited, code=killed, status=15/TERM
11月 26 13:38:00 raspberrypi systemd[1]: lineControler.service: Succeeded.
11月 26 13:38:00 raspberrypi systemd[1]: Stopped Line Controler for Alexa.
pi@raspberrypi:~ $
最後に自動起動設定の有効化
sudo systemctl enable lineControler.service
これでラズパイ起動時に「rs-listener.py」が自動実行されるはず。
追記3(2022/08/24). CloudFunctionにおけるランタイム環境変数の設定
同じような事を再びやる機会があったため、本文を見直したところランタイム環境変数の設定手順が無いことに気づいた。
修正を実施しました。
Google Cloud Founctionにランタイム環境変数に以下を設定する。
LINE_ACOUNT_SECRET:LINE Messageing APIのチャネルシークレット
LINE_ACCOUNT_ACCESS_TOKEN:LINE Messageing APIのチャネルアクセストークンそうすることで、Line Message APIのリクエスト検証が実施される。
参考文献
本記事の作成に当たり、以下の情報を参考にさせて頂きました。
- Messaging APIを始めよう
- メッセージ(Webhook)を受信する
- LINEのメッセージでRaspberryPIのLEDをON/OFFしてみる(その1)設定
- Google HomeにLINEのメッセージを読み上げさせる(LINE BOT+Webhook+Firebase+Node.js)
- LINE MessagingAPIで遊ぶ①動かしてみる)
- Cloud Firestore でリアルタイム アップデートを入手する
- PythonのOnSnapshotを持つFirebase Cloud FireStore.
- リアルタイムなデータの保存・同期を手軽に実現するCloud Firestore入門
- 【解決】 Python3のfirebase_adminでCollectionをon_snapshot()してるとhread-ConsumeBidirectionalStream caught unexpected exception
- Cloud Firestore を使ってみる
- [Python] ImportError: Failed to import the Cloud Firestore library for Python.
- LineBotをCloud Functionsで利用してGCEの起動停止できるようにしてみた
- check if a Firebase App is already initialized in python
- Raspberry Piで最強の防犯カメラを作ってみる(動画記録・配信、動体検知・Line通知、顔検知・顔認証、Alexa搭載)[5/6]