背景:Maqueenの制御をEPS32で行えるよう改造している。ESP32はWiFi接続可能なので、マイコン側からWebAPIを呼び出したり、MQTTで双方向通信したりして、Maqueenを賢くしたい
課題:Alexaの音声制御によってMaqueenを制御できるようにする(動作テストに音声が使えたらいろいろ便利だし)
取り組み:数年使っていないAmazon Alexa Consoleからカスタムスキルを設定する。どうやったか忘れているので試行錯誤する。
結論:カスタムスキルを定義してAlexa経由でMQTT Brokerに記事を投稿するところまではできた(あとはMaqueenからMQTTにSubscribeしておけば指示を受信できるはず)
詳細:
数年以上使っていないAmazon Alexa Consoleに行ってみるとアカウントが消えているというのか無効化されているのかよく分からない状況であった。Alexa Consoleでは基本的に物販サイトのAmazonのアカウントで使うらしいので、パスワードReminderやらいろいろ試行錯誤してアカウントを有効にした。
使えるようになったAmazon Alexa Consoleからお試しのカスタムスキルを作ったものの、自宅のAlexaではどうも反応してくれず、Alexa Consoleのアカウントと自宅のAlexaが紐づいているのかどうかすら怪しかった。Alexaアプリを調べているとBluePrintというサービスがあって、スマフォとAlexaだけでカスタムスキル?のようなものが作れるようであった。そこで、まずはBluePrintで最低限動くカスタムスキルを作って、それをAlexaのDeveloperサイトに格上げしてLambdaと接続できるように改造しようと考えている。
幸い、BluePrintで作ったお試しスキル?がAmazon Alexa Consoleで表示される状況なので、自宅のAlexaとAlexa Consoleのアカウントは紐づいていることが確認できた。紐づきが確認できたので、BluePrintで作ったスキルを改造して、Lambdaを呼び出せるようにして、音声でMaqueenを制御できるようにしたい。
いろいろ触っているうちに、スマフォ上のAlexaアプリ内で、マイスキルというメニューがあり、その中で開発というタブがあった。どうもここで試作中のスキルを有効化する必要があるらしかった(ここは推測。開発マニュアルとか読まず当てずっぽうで作ってるので)。スマフォのAlexaアプリで試作中のスキルが表示されているので、紐づけは問題ないと改めて確認できた。試作スキルを有効化する
試行錯誤の結果、Maqueenを制御するためのカスタムスキルを登録して、前進、停止の指示でLambda関数が呼び出されるようにした。
ソースは基本的にテンプレートそのままで、GoCarIntentHandlerとStopCarIntentHandlerだけ追加した。応答文言も修正
# -*- coding: utf-8 -*- import logging import ask_sdk_core.utils as ask_utils from ask_sdk_core.skill_builder import SkillBuilder from ask_sdk_core.dispatch_components import AbstractRequestHandler from ask_sdk_core.dispatch_components import AbstractExceptionHandler from ask_sdk_core.handler_input import HandlerInput from ask_sdk_model import Response logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) class LaunchRequestHandler(AbstractRequestHandler): """Handler for Skill Launch.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("LaunchRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "こんにちは。マクイーンです。ご指示をお願いいたします" ask_output = "ご遠慮なさらず、何なりとお申し付けください" return ( handler_input.response_builder .speak(speak_output) .ask(ask_output) .response ) class GoCarIntentHandler(AbstractRequestHandler): """Handler for Go Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("GoCarIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "前進します" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) class StopCarIntentHandler(AbstractRequestHandler): """Handler for StopCar Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("StopCarIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "停止します" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) class HelloWorldIntentHandler(AbstractRequestHandler): """Handler for Hello World Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("HelloWorldIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "ハロー世の中、でもそんな甘いもんじゃございませんて" speak_query = 'とりあえず、希望されることを話してみてね' return ( handler_input.response_builder .speak(speak_output) .ask(speak_query) .response ) class HelpIntentHandler(AbstractRequestHandler): """Handler for Help Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("AMAZON.HelpIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "お助けモードです。マクイーンに対するコマンドとして、前進、後退、停止、右旋回、左旋回、オブジェクト探査が使えます" speak_query = "何かコマンドを発話してみてください" return ( handler_input.response_builder .speak(speak_output) .ask(speak_query) .response ) class CancelOrStopIntentHandler(AbstractRequestHandler): """Single handler for Cancel and Stop Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return (ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input) or ask_utils.is_intent_name("AMAZON.StopIntent")(handler_input)) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "Goodbye!" return ( handler_input.response_builder .speak(speak_output) .response ) class FallbackIntentHandler(AbstractRequestHandler): """Single handler for Fallback Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response logger.info("In FallbackIntentHandler") speech = "よく分かりません。ヘルプと発話してください" reprompt = "ご要望は何でしょうか?何なりとお申し付けください" return handler_input.response_builder.speak(speech).ask(reprompt).response class SessionEndedRequestHandler(AbstractRequestHandler): """Handler for Session End.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("SessionEndedRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response # Any cleanup logic goes here. return handler_input.response_builder.response class IntentReflectorHandler(AbstractRequestHandler): """The intent reflector is used for interaction model testing and debugging. It will simply repeat the intent the user said. You can create custom handlers for your intents by defining them above, then also adding them to the request handler chain below. """ def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("IntentRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response intent_name = ask_utils.get_intent_name(handler_input) speak_output = "トリガーされました " + intent_name + "." return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response ) class CatchAllExceptionHandler(AbstractExceptionHandler): """Generic error handling to capture any syntax or routing errors. If you receive an error stating the request handler chain is not found, you have not implemented a handler for the intent being invoked or included it in the skill builder below. """ def can_handle(self, handler_input, exception): # type: (HandlerInput, Exception) -> bool return True def handle(self, handler_input, exception): # type: (HandlerInput, Exception) -> Response logger.error(exception, exc_info=True) speak_output = "すみません、問題が発生しました。もう一度お願いします" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) # The SkillBuilder object acts as the entry point for your skill, routing all request and response # payloads to the handlers above. Make sure any new handlers or interceptors you've # defined are included below. The order matters - they're processed top to bottom. sb = SkillBuilder() sb.add_request_handler(LaunchRequestHandler()) sb.add_request_handler(HelloWorldIntentHandler()) sb.add_request_handler(GoCarIntentHandler()) sb.add_request_handler(StopCarIntentHandler()) sb.add_request_handler(HelpIntentHandler()) sb.add_request_handler(CancelOrStopIntentHandler()) sb.add_request_handler(FallbackIntentHandler()) sb.add_request_handler(SessionEndedRequestHandler()) sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers sb.add_exception_handler(CatchAllExceptionHandler()) lambda_handler = sb.lambda_handler()
前進や停止の発話に対応できるようにintentを追加する
Lambda関数からMQTTブローカにPublishできれば、Maqueenを遠隔制御できるはず。AWSなんだからAWS IoTを使うのが正道なんだろうか。Alexaの開発環境において、IAMとかどうなっているのか分からず。
■追記
AlexaのLambdaからAWS IoTを呼び出すには、基本的にAlexaのLambdaのアカウントと、AWS IoTのアカウントを同一にすべきらしい。もし異なっている場合は、クロスアカウントでAWS IoTを使えるように設定が必要だと。あるいは、、Lambda側がMQTT Clientになってしまって普通にPublisしたらいいのではないか?(Client鍵を持っている一般利用者の立場でアクセス)
ちょっと面倒なので、最初はPublicなMQTT Brokerで接続してみるか。。
LambdaでMQTT Libraryをつかってみようとするとpahoがないと怒られる。最初CloudWatchログでログを見つけられなかったのだが、なぜかリージョンがオレゴンであった。カスタムスキルのために借りたサーバのRegionがオレゴンだったのか?
自分のアカウントだったらライブラリとか自由に追加できるのだけど、フェデレーテッドユーザとして制限下で使わせてもらっているAWSの場合、外部公開のライブラリはLambdaの環境に足せるのだろうか??
少し調べた範囲では、AWS Lambdaコンソールにアクセスできても権限がありませんと怒られる一方なので、ライブラリ等をLambda実行環境にアップするのは無理そうである。AlexaのDeveloperサイトを触っていると、「普段使っているアカウントと連携させますか?その場合、IAMポリシーを足して」というPopUpが出たので、フェデレーテッドユーザとしての制限を超えて使いたい場合は、個人のアカウントのAWSと連携してそっちでいろいろやってくれという事なんだろうと推測。MQTTだめならRequestsでHTTPでポストするか。。?
requestsライブラリは使えるようであった(エラーにならなかったので)。さらに調べていると、uploadメニューでライブラリを含めてZIP形式でアップできるようなので、このメニューからpahoとコードを一緒に上げられるのではと期待
test.mosquitto.org を確認するとMQTTプロトコルしかサポートしていないようであった。であれば、、時々使っているshiftr.ioを使うか。shiftr.ioはHTTPプロトコルでも使える(認証付きでないと使えないが。。)
Lambdaからrequestsライブラリを使ってHTTPプロトコルで記事を投稿するAPIを呼び出し実装を加えた。Lambda内にID/PWDを書くというかなりセキュリティ的にまずい実装なのだが、環境変数設定とかできないようなのでしょうがない。Alexaに「前進して」と言うと、Lambda経由でshiftr.ioにHTTP APIでJSONの記事を投稿できた。次は、ESP32からshiftr.ioにsubscriptしておけば、Alexaに指示を出すと、イベントとして受け取れるはず。
requestsライブラリを使ってMQTT Brokerに記事を投稿する機能を加えたLambda(抜粋)は以下。ソースコード内にID/PWDが書かれているセキュリティ的にはまずい実装・・・
*略* import requests import json SECRET='xxxxxxx' INSTANCE_NAME='xxxxxx' TOPIC='req/maqueen00/control' DOMAIN='cloud.shiftr.io' URL=f'https://{INSTANCE_NAME}:{SECRET}@{INSTANCE_NAME}.{DOMAIN}/broker/{TOPIC}' class GoCarIntentHandler(AbstractRequestHandler): """Handler for Go Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("GoCarIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "前進します" data = { "control": "go", } response = requests.post(URL, data=json.dumps(data), headers={"Content-Type": "application/json"}) # if response.status_code==200: # print(response.text) # else: # print(response.text) return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) *略* sb = SkillBuilder() sb.add_request_handler(GoCarIntentHandler()) *略* lambda_handler = sb.lambda_handler()
■追記
別アカウントのLambdaから自アカウントのLambdaの呼び出しを許可する方法をcopilotに質問すると以下と回答が得られた。
呼び出されるLambda関数のリソースポリシーを設定: 自アカウントのLambda関数に対して、別のアカウントからの呼び出しを許可するリソースポリシーを追加します。例えば、以下のようなポリシーを設定します: json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::別のアカウントID:root" }, "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:リージョン:自アカウントID:function:関数名" } ] }
Amazon Alexa Consoleの画面で、他のAWSアカウントと連携するには、これを設定してねとPopUPに出ていたARN(下記)を自アカウントのポリシーに足せば呼び出せるようになるのだろう。。多分
■関連URL
Alexa Console
Amazon Sign-In