Raspberry Pi でグローバルIPを取得し、AWS Lambda に送信する手順

published_with_changes 更新日: event_note 公開日:

labelamazon alexa labelBlynk labelIoT labelRaspberry pi

数年前に作成したAlexaスキル「アレクサ、部屋の温度を教えて」を実現のための第8歩になります。

その時は、AWS のLambda関数に事前に調べたルータのグローバルIPアドレスを書き込む方法で開発しました。(Blynk local server を使うとAlexaが答えてくれない


Blynkが答えるためには、ルータにぶら下がるBlynk Local serverが外部インターネットから見えてる必要があります。

このときの問題は、ルータが再起動するとグローバルIPアドレスが変わってしまうことにありました。そのたびに、Lambda関数のグローバルIPアドレスのコードをアップデートしなければならず、非常に使い勝手の悪いものでした。

最近、その対策を思い立ちLambda関数のコードを見直して、外部のサイトを利用してIPアドレスの取得を試みましたが、次の根本的問題がわかり頓挫してしまいました。

  1. アドレス取得をLambda関数に実装 (http.get('http://checkip.amazonaws.com')) すると、 ユーザーの実際のグローバルIPではなくLambdaのパブリックIP を取得してしまう。理由はわからないが、そういう仕様らしい。
  2. AWS Lambda関数が外部サイトからグローバルIPアドレスを取得できない理由は、Lambda関数が固定のIPアドレスを持たないかららしい。Lambda関数は、実行されるたびに異なるIPアドレスが割り当てられるため、外部サイトから見ると常に変わることになる。
  3. この問題はLambda関数が固定のIPアドレスをもつようにすれば解決できるが、そのためには有料のサービスの利用が必要である。(VPC + NAT Gateway、AWS Global Acceleratorなど。まったく何のことかわからない。)
  4. 「ユーザーのグローバルIP」を取得したい場合は、クライアント(ユーザーのデバイスやブラウザ)でIPを取得し、それをLambdaにプッシュ送信する設計にする必要がある。

4番目の問題点を眺めていると、Bylnk Local server を使えば、3,4番の問題は解決する気がして来ました。そして、悪戦苦闘しながらIPアドレスの自動取得に成功した記録です。


Bylnk Local server が定期的に自動で取得した globalIP を使用して、Blynk Local server へセンサーのデータを取得をリクエストできます。

もっとも、AWSの知識はないし習得する気もないので、  ChatGPTや Copilot Copilotを駆使して開発しました。


🔹 手順

  1. 準備

    (1-1) Raspberry Pi に Python3 がインストールされていることを確認
    (1-2) 必要なライブラリのインストール
  2. Python スクリプトの作成

    (2-1) グローバルIPを取得するスクリプトの作成
    (2-2) スクリプトを Raspberry Pi に保存
  3. 定期実行の設定

    (3-1) crontab を使って 10 分ごとにスクリプトを実行
  4. AWS Lambda との通信

    (4-1) API Gateway を設定して Lambda 関数を呼び出せるようにする
    (4-2) Lambda 関数を作成し、IP を受け取る処理を書く
    (4-3) Raspberry Pi から API Gateway にリクエストを送る
  5. 動作確認とトラブルシューティング

    (5-1) Raspberry Pi のスクリプトが正しく動いているか確認
    (5-2) API Gateway と Lambda の接続を確認
    (5-3) ログをチェックし、エラーを修正

1. 準備

この章では、Raspberry Pi に必要な環境を整えます。


(1-1) Raspberry Pi に Python3 がインストールされていることを確認

Raspberry Pi には通常 Python3 がプリインストールされていますが、念のため確認し、なければインストールします。

✅ Python3 のバージョンを確認する

以下のコマンドを Raspberry Pi のターミナルで実行してください。

python3 --version

表示例:

Python 3.9.2

「Python 3.x.x」と表示されれば OK です。

🔧 Python3 がインストールされていない場合

もし command not found などのエラーが出る場合、以下のコマンドでインストールします。

sudo apt update
sudo apt install -y python3 python3-pip

(1-2) 必要なライブラリのインストール

Raspberry Pi の Python スクリプトでは、requests というライブラリを使って API を呼び出します。
以下のコマンドを実行し、requests をインストールしてください。

pip3 install requests

✅ インストールの確認

次のコマンドを実行し、正しくインストールされたか確認します。

python3 -c "import requests; print(requests.__version__)"

バージョン番号(例:2.25.1)が表示されれば OK です。
エラーが出た場合は、インストールがうまくいっていない可能性があります。


これで 「1. 準備」 が完了しました。
次は 「2. Python スクリプトの作成」 に進みますが、ここまでで不明点はありますか?

2. Python スクリプトの作成

この章では、Raspberry Pi 上で Python スクリプトを作成し、定期的にグローバル IP を取得して AWS Lambda に送信する処理を実装します。


(2-1) Python スクリプトの作成

✅ スクリプトを保存するディレクトリに移動

まず、スクリプトを保存するためのディレクトリを作成し、そこに移動します。

mkdir -p ~/blynk-ip-reporter
cd ~/blynk-ip-reporter

✅ Python スクリプトを作成する

テキストエディタ (nano) を使って、新しい Python スクリプトを作成します。

nano report_ip.py

以下のコードをコピーして貼り付けてください。

import requests
import time

# AWS API Gateway のエンドポイント(後で設定)
LAMBDA_ENDPOINT = "https://your-api-gateway-url.amazonaws.com/prod/ip-report"

def get_global_ip():
    """外部サービスを使ってグローバル IP を取得"""
    try:
        response = requests.get("https://checkip.amazonaws.com")
        return response.text.strip()
    except Exception as e:
        print(f"Error getting global IP: {e}")
        return None

def send_ip_to_lambda(ip):
    """取得した IP を Lambda に送信"""
    try:
        data = {"ip": ip}
        response = requests.post(LAMBDA_ENDPOINT, json=data)
        print(f"Sent IP: {ip}, Response: {response.status_code}, {response.text}")
    except Exception as e:
        print(f"Error sending IP to Lambda: {e}")

if __name__ == "__main__":
    while True:
        ip = get_global_ip()
        if ip:
            send_ip_to_lambda(ip)
        time.sleep(600)  # 10分ごとに実行

編集が終わったら、Ctrl + XYEnter で保存して終了します。


(2-2) スクリプトの実行権限を付与

作成した Python スクリプトを実行できるように、以下のコマンドを実行してください。

chmod +x report_ip.py

(2-3) スクリプトの動作確認

以下のコマンドを実行し、スクリプトが正しく動作するか確認します。

python3 report_ip.py
  • 正常に動作すれば、以下のようなメッセージが表示されます。
    Sent IP: 123.45.67.89, Response: 200, OK
    
  • もしエラーが出た場合、どのようなエラーなのか教えてください。

これで 「2. Python スクリプトの作成」 は完了です。
次は 「3. Raspberry Pi でスクリプトを自動実行」 に進みますが、ここまでで不明点はありますか?

(2-4) エラーの修正 1

スクリプトの動作確認で、下記のエラーが出ました。
SyntaxError: Non-UTF-8 code starting with '\xa4' in file /home/pi/blynk-ip-reporter/report_ip.py on line 4, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

このエラーは、Python スクリプトの文字エンコーディングが UTF-8 でないために発生しています。これは、ファイルの保存時に文字コードが ASCIIISO-8859-1 になってしまった可能性があります。

以下の手順で修正してください。


(1) ファイルのエンコーディングを確認

まず、現在のファイルのエンコーディングを確認します。

file -i report_ip.py

utf-8 でない場合は、次の手順で修正します。


(2) ファイルのエンコーディングを UTF-8 に修正

  1. ファイルを開き直す
    nano エディタでファイルを開いて、上部に UTF-8 のエンコーディング宣言を追加します。

    nano report_ip.py
    
  2. 1行目に以下のコメントを追加

    # -*- coding: utf-8 -*-
    

    修正後の最上部は以下のようになります。

    # -*- coding: utf-8 -*-
    import requests
    import time
    
  3. 保存して終了
    Ctrl + XYEnter で保存してください。


(3) スクリプトの実行権限を再設定

chmod +x report_ip.py

(4) 再度スクリプトを実行

もう一度、スクリプトを実行してください。

python3 report_ip.py

この手順で修正できるはずですが、もしまだエラーが出る場合は、エラーメッセージを教えてください。

(2-5) エラーの修正 2

まだ、エラーが出ます。
File "/home/pi/blynk-ip-reporter/report_ip.py", line 9 """\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\u04e5\ufffd\ufffd\ufffd\u0224ä\u01a5\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdХ\ufffd IP \ufffd\ufffd\ufffd\ufffd\ufffd""" ^ SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xb3 in position 0: invalid start byte

エラーの原因は、report_ip.py のファイル内部に文字化けが発生していることです。これは、ファイルのエンコーディングが UTF-8 以外の形式で保存されている、または編集時に壊れた可能性があります。


(1) まず、現在のファイルのエンコーディングを確認する

以下のコマンドを実行し、ファイルのエンコーディングを確認してください。

file -i report_ip.py

出力例:

report_ip.py: text/x-python; charset=iso-8859-1

もし utf-8 以外の charset=iso-8859-1 などが表示された場合、エンコーディングを UTF-8 に変換する必要があります。


(2) ファイルのエンコーディングを UTF-8 に変換する

以下のコマンドを実行して、エンコーディングを修正してください。

iconv -f ISO-8859-1 -t UTF-8 report_ip.py -o report_ip_utf8.py

もし ISO-8859-1 以外だった場合、-f の値を file -i コマンドの出力に合わせて変更してください。

変換後、元のファイルを置き換えます。

mv report_ip_utf8.py report_ip.py

(3) nano エディタで開いて確認する

nano report_ip.py
  • 文字化けがないことを確認する
  • 最上部に UTF-8 のエンコーディング宣言があることを確認する
    # -*- coding: utf-8 -*-
    
  • 保存方法: Ctrl + XYEnter

(4) Python スクリプトを実行

python3 report_ip.py

(5) それでも解決しない場合

もしまだエラーが出る場合、次の情報を教えてください。

  1. file -i report_ip.py の結果
  2. cat report_ip.py の出力(文字化けがある場合、部分的に教えてください)
  3. nano report_ip.py で開いたときに、ファイルの内容が読めるか

この手順で修正できる可能性が高いですが、もしうまくいかなければ、追加の対策を考えます。

了解しました。では、API Gateway の設定 から一緒に進めていきましょう。
「API Gateway の作成」 から説明します。


4. AWS Lambda との通信

(4-1) API Gateway の作成(REST API を使用)

1. API Gateway を開く

  1. AWS マネジメントコンソール にログイン
  2. 検索バー に「API Gateway」と入力し、開く

2. REST API を作成

  1. 「API の作成」 をクリック
  2. 「REST API」 を選択
    • 「HTTP API」ではなく、REST API を使います(設定が簡単なため)
  3. 「構築」 をクリック

3. API の基本設定

  1. 「新しい API を作成」 を選択
  2. API 名 を入力(例: ip-reporter-api
  3. エンドポイントタイプ は「リージョン」を選択
  4. 「API を作成」 をクリック

4. リソースとメソッドを作成

① リソースを作成

  1. 左のメニューで「リソース」を開く
  2. 「アクション」 → 「リソースの作成」 をクリック
  3. リソース名を ip-report に設定
  4. リソースパスを /ip-report に設定
  5. 「リソースの作成」をクリック

② メソッドを作成

  1. 「アクション」 → 「メソッドの作成」 をクリック
  2. 「POST」 を選択し、チェックマークをクリック
  3. 統合タイプ:「Lambda 関数」 を選択
  4. Lambda 関数名に「ip-reporter-lambda」(後で作成)を入力
  5. 「保存」をクリックし、「OK」で確認

5. API のデプロイ

  1. 左メニューで「ステージ」を開く
  2. 「ステージの作成」をクリック
  3. ステージ名を prod に設定(他の名前でもOK)
  4. 「デプロイ」をクリック
  5. 「Invoke URL」 が表示されるので、コピー(例: https://abcd1234.execute-api.ap-northeast-1.amazonaws.com/prod

6. Raspberry Pi に設定

  1. report_ip.py を開く
  2. LAMBDA_ENDPOINT を以下のように変更
    LAMBDA_ENDPOINT = "https://abcd1234.execute-api.ap-northeast-1.amazonaws.com/prod/ip-report"
    
  3. スクリプトを実行してテスト
    python3 report_ip.py
    

これで API Gateway のエンドポイントが完成し、Raspberry Pi からデータを送信できるようになります。
ここまでの手順で不明点があれば教えてください!

ステージの作成でトラブっている。ステージ名:prod, 「ステージの作成」 をクリックすると、「デプロイ」枠にDeployment is required. のエラーがでて、ステージの作成ができない。

おっしゃる通り、**「ステージの作成」**をクリックした際に、「Deployment is required」というエラーが表示される場合、ステージを作成する前にデプロイを行う必要があるという状況です。このエラーは、API Gatewayにリソースやメソッドを設定した後、その設定を「デプロイ」していないため、ステージを作成できないことを意味しています。

解決方法

  1. デプロイの確認
    ステージを作成する前に、まずAPIの設定をデプロイする必要があります。これを行わないと、ステージが作成できません。以下の手順でデプロイを行ってください。

    • API Gateway ダッシュボードにアクセス
    • 左側のメニューから 「API」 を選択し、作成したAPIをクリック
    • 左のメニューから 「アクション」 ボタンをクリックし、その中から 「デプロイ」 を選択
    • 「デプロイ先ステージ」「新しいステージを作成」 または既存のステージを選択
    • ステージ名 を入力(例: prod
    • デプロイ をクリック

    この操作を行うことで、API設定がデプロイされ、その後にステージ作成が可能になります。

  2. ステージの作成
    デプロイが成功した後、以下の手順でステージを作成できます。

    • API Gateway ダッシュボードに戻り、再度ステージの作成を行います。
    • 先程のデプロイ時に作成したステージ(例えば prod)を選び、ステージが正常に作成されるはずです。

補足

  • **ステージ作成に必要な「デプロイ」**がまだ行われていない場合、どんな設定をしてもステージは作成できません。デプロイを先に行うことで、その後ステージ作成が可能になります。

もしこの手順でも問題が解決しない場合、もう一度API Gatewayの設定やエラーメッセージについて確認をお願いします。

(4-2) Lambda 関数を作成

Lamda関数はRaspberry pi が送信したクローバルIPアドレスを受信して、Blynkサーバへ必要な情報をリクエストする処理を行っている。ただし、Raspberry pi が送った時には正しいクローバルIPアドレスを受信できるが、Alexaスキルを呼び出すと、IPアドレスが消失するという問題がある。

そこで、今までのLambda関数を次の内容に改変する。
  • 定数 globalIP を追加し、Raspberry Pi から受信した時にこの値を更新します。
  • Alexaスキルを呼び出す際には、格納された globalIP を使います。

Lambda関数のコード順の処理説明

'use strict';

const http = require('http');
let globalIP = 'デフォルトのIPアドレス';
  • グローバルIPアドレスの初期化:
    • globalIP 変数を初期化します。Raspberry Piから受信するまでデフォルトのIPアドレスが設定されています。
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { /*...*/ }
function buildResponse(sessionAttributes, speechletResponse) { /*...*/ }
  • レスポンスの生成関数:
    • Alexaスキルのレスポンスを生成するためのヘルパー関数です。タイトル、メッセージ、リプロンプトテキスト、セッション終了フラグを受け取り、スキルの応答を構築します。
function getWelcomeResponse(callback) { /*...*/ }
  • ウェルカムメッセージの生成:
    • スキルが起動されたときのウェルカムメッセージを生成します。このメッセージはユーザーにスキルの使い方を案内します。
function handleSessionEndRequest(callback) { /*...*/ }
  • セッション終了時の処理:
    • ユーザーがスキルを終了したときに呼び出される関数です。終了メッセージを生成してセッションを終了します。
function createLocationAttributes(location) { /*...*/ }
  • セッション属性の生成:
    • 指定された場所の属性をセッションに追加します。
async function readTemperatureInSession(intent, session, callback) { /*...*/ }
  • 温度情報の取得:
    • 指定された場所と測定項目(例:温度、湿度、気圧など)に基づいてBlynkサーバーからセンサーデータを取得し、ユーザーに提供します。
    • エラーハンドリングを行い、データの取得に失敗した場合に適切なメッセージを表示します。
function onSessionStarted(sessionStartedRequest, session) { /*...*/ }
  • セッション開始時の処理:
    • スキルのセッションが開始されたときに呼び出される関数です。セッションの開始をログに記録します。
function onLaunch(launchRequest, session, callback) { /*...*/ }
  • スキルの起動時の処理:
    • スキルが起動されたときにウェルカムメッセージを表示する関数です。
function onIntent(intentRequest, session, callback) { /*...*/ }
  • インテントリクエストの処理:
    • ユーザーがインテント(特定のアクションや質問)を発したときに呼び出される関数です。対応する処理関数(例:readTemperatureInSession)を呼び出します。
function onSessionEnded(sessionEndedRequest, session) { /*...*/ }
  • セッション終了時の処理:
    • スキルのセッションが終了されたときに呼び出される関数です。セッションの終了をログに記録します。
exports.handler = (event, context) => { /*...*/ }
  • Lambda関数のエントリーポイント:
    • すべてのリクエストを受け取り、適切な処理関数を呼び出します。
    • グローバルIPアドレスの更新、セッションの管理、ロギングとデバッグを行います。

これで各処理がどの関数で行われているかをコードと対比して理解できるようになりました。他に質問や修正が必要な箇所があれば教えてくださいね。

Lambda関数のコード


'use strict';

const http = require('http');
let globalIP = 'デフォルトのIPアドレス';  // 初期化

// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}

// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = "二階の温度を教えてなどと問いかけると、それを答えます。";
    const repromptText = "私はこの部屋を見てるわよっ!何が知りたいのっ!   ってかっ";
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function createLocationAttributes(location) {
    return { location };
}

async function readTemperatureInSession(intent, session, callback) {
    const cardTitle = intent.name;
    const LocationSlot = intent.slots.Location;
    const MeasurementSlot = intent.slots.Measurement;
    let repromptText = '';
    let sessionAttributes = session.attributes || {};
    const shouldEndSession = false;
    let speechOutput = '';
    let body = '';
    const blynkport = '8080';
    let blynkAuthToken;
    let blynkPin;

    // Raspberry PiからのglobalIPを使用
    let location = LocationSlot.value || sessionAttributes.location || '二階';
    sessionAttributes.location = location;

    let ip = sessionAttributes.globalIP || globalIP;

    switch (location) {
        case '二階':
            blynkAuthToken = 't1uC6ciQZrTtNt1wpbmLskRMBvSyi9Lw';
            break;
        case '仏間':
            blynkAuthToken = 'slWRpvQMI-NTHGPb28JBMKIRS7vi4ch2';
            break;
        case '屋根裏':
            blynkAuthToken = 'uwPwuHpaIxEo7t9FRfaCCQ6R4268eEeI';
            break;
        default:
            speechOutput = '指定された場所が見つかりませんでした。';
            repromptText = '他の場所を指定してください。';
            callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
    }

    let measurement = MeasurementSlot.value || '温度';
    switch (measurement) {
        case '何度':
        case '気温':
        case '室温':
        case '温度':
            blynkPin = 'V0';
            break;
        case '湿気':
        case '湿度':
            blynkPin = 'V1';
            break;
        case '大気圧':
        case '気圧':
            blynkPin = 'V2';
            break;
        case '熱中度指数':
        case '熱中度':
        case '暑さ指数':
        case '暑さ度':
        case '暑さ':
            blynkPin = 'V4';
            break;
        default:
            speechOutput = '指定された測定項目が見つかりませんでした。';
            repromptText = '他の測定項目を指定してください。';
            callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
    }

    try {
        console.log(`Sending request to Blynk server with Auth Token: ${blynkAuthToken}, Pin: ${blynkPin}, IP: ${ip}`);

        const httpPromise = new Promise((resolve, reject) => {
            http.get({
                host: ip,
                path: `/${blynkAuthToken}/get/${blynkPin}`,
                port: blynkport,
            }, (response) => {
                response.on('data', (d) => {
                    body += d;
                });
                response.on('end', () => {
                    resolve('Done Sending');
                });
            }).on('error', (e) => {
                reject(e);
            });
        });

        await httpPromise;

        console.log(`Received response from Blynk server: ${body}`);

        const info = parseFloat(JSON.parse(body));
        if (isNaN(info)) {
            speechOutput = '温度の取得に失敗しました。';
            repromptText = 'もう一度お試しください。';
            callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        } else {
            speechOutput = location + 'の ' + measurement + 'は ';
            repromptText = speechOutput;
            switch (measurement) {
                default:
                case '何度':
                case '気温':
                case '室温':
                case '温度':
                    speechOutput += `${info.toFixed(1)}度 です。`;
                    repromptText += `${info.toFixed(1)}℃です。`;
                    break;
                case '湿気':
                case '湿度':
                    speechOutput += `${info.toFixed(1)}パーセント です。`;
                    repromptText += `${info.toFixed(1)}%です。`;
                    break;
                case '大気圧':
                case '気圧':
                    speechOutput += `${info.toFixed(0)}ヘクトパスカル です。`;
                    repromptText += `${info.toFixed(0)}hPaです。`;
                    break;
                case '熱中度指数':
                case '熱中度':
                case '暑さ指数':
                case '暑さ度':
                case '暑さ':
                    let annotation = '';
                    if (info >= 31) {
                        annotation = '危険! ブラックです。';
                    } else if (info >= 28) {
                        annotation = '厳重警戒! レッドです。';
                    } else if (info >= 25) {
                        annotation = '警戒! オレンジです。';
                    } else if (info >= 21) {
                        annotation = '注意です。';
                    }
                    speechOutput += `${info.toFixed(0)}度 です。${annotation}`;
                    repromptText += `${info.toFixed(0)}℃です。${annotation}`;
                    break;
            }
            callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        }
    } catch (error) {
        console.error(`Error: ${error}`);
        speechOutput = '温度の取得中にエラーが発生しました。';
        repromptText = 'もう一度お試しください。';
        callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}

// --------------- Events -----------------------

function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
    getWelcomeResponse((sessionAttributes, speechletResponse) => {
        callback(sessionAttributes, speechletResponse);
    });
}

function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    if (intentName === 'GetMeasurement') {
        readTemperatureInSession(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else {
        throw new Error('Invalid intent');
    }
}

function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}

exports.handler = (event, context) => {
    try {
        console.log('Event:', JSON.stringify(event));  // イベント全体をログに記録

        // Initialize session attributes if not present
        if (!event.session.attributes) {
            event.session.attributes = {};
        }

        // グローバルIPアドレスをイベントから取得して設定
        if (event.ip) {
            globalIP = event.ip;
            event.session.attributes.globalIP = globalIP;  // IPをセッション属性に保存
            console.log(`Updated globalIP to: ${globalIP}`);
        } else if (event.session.attributes.globalIP) {
            globalIP = event.session.attributes.globalIP;  // セッション属性からIPを取得
            console.log(`Using globalIP from session attributes: ${globalIP}`);
        } else {
            console.error('IPアドレスが提供されていません。');
        }

        if (event.session && event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }
        if (event.request && event.request.type) {
            if (event.request.type === 'LaunchRequest') {
                onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                    console.log('LaunchRequest succeeded');
                });
            } else if (event.request.type === 'IntentRequest') {
                onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                    console.log('IntentRequest succeeded');
                });
            } else if (event.request.type === 'SessionEndedRequest') {
                onSessionEnded(event.request, event.session);
                context.succeed();
                console.log('SessionEndedRequest succeeded');
            }
        } else {
            console.error('event.request が見つかりません。');
            context.fail('event.request が見つかりません。');
        }
    } catch (e) {
        console.error(`Exception: ${e}`);
        context.fail(`Exception: ${e}`);
    }
};


3. 定期実行の設定

(2-1) Python スクリプトの作成で作った report_ip.py は10分毎にIPアドレスを送信するので、これを修正します。

以下は、crontab を利用して 10 分ごとに実行するため、スクリプト自体は単発実行になるように修正したコード例です。

(※ 元のコードの内容はほぼそのまま残し、ループと time.sleep を削除しています。)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import json

# AWS API Gateway のエンドポイントと API キー
LAMBDA_ENDPOINT = "https://your-api-gateway-url.amazonaws.com/prod/ip-report"
API_KEY = "YOUR_API_KEY"  # 実際の API キーに置き換えてください

def get_global_ip():
    """checkip.amazonaws.com からグローバル IP アドレスを取得する"""
    try:
        response = requests.get("https://checkip.amazonaws.com")
        return response.text.strip()
    except Exception as e:
        print(f"Error getting global IP: {e}")
        return None

def send_ip_to_lambda(ip):
    """取得した IP アドレスを Lambda に送信する"""
    try:
        payload = {
            'session': {
                'new': True,
                'sessionId': 'SessionId',
                'application': {
                    'applicationId': 'amzn1.ask.skill.[unique-value-here]'
                },
                'attributes': {},
                'user': {
                    'userId': 'amzn1.ask.account.[unique-value-here]'
                }
            },
            'request': {
                'type': 'LaunchRequest',
                'requestId': 'EdwRequestId',
                'locale': 'ja-JP'
            },
            'ip': ip
        }
        headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
        response = requests.post(LAMBDA_ENDPOINT, json=payload, headers=headers)
        print(f"Sent IP: {ip}, Response: {response.status_code}, {response.text}")
    except Exception as e:
        print(f"Error sending IP to Lambda: {e}")

if __name__ == "__main__":
    ip = get_global_ip()
    if ip:
        send_ip_to_lambda(ip)

(3-1) crontab を使って 10 分ごとにスクリプトを実行

  1. スクリプトの保存場所と実行権限の確認
    このコードを /home/pi/blynk-ip-reporter/report_ip.py に保存してください。
    また、実行権限を付与します。

    chmod +x /home/pi/blynk-ip-reporter/report_ip.py
    
  2. crontab の編集
    ターミナルで以下のコマンドを実行して crontab エディタを開きます。

    crontab -e
    
  3. 10 分ごとの実行スケジュールを追加
    エディタの一番下に、以下の行を追加します。

    */10 * * * * /usr/bin/python3 /home/pi/blynk-ip-reporter/report_ip.py >> /home/pi/blynk-ip-reporter/report_ip.log 2>&1
    
    • */10 * * * * は「毎時 10 分ごと」に実行する設定です。
    • /usr/bin/python3 は Python3 のパスです。環境に合わせて which python3 で確認してください。
    • ログは /home/pi/blynk-ip-reporter/report_ip.log に出力されます。
  4. crontab の保存
    nano などのエディタの場合、Ctrl+XYEnter で保存します。

  5. 設定確認
    次のコマンドで現在の crontab 設定を確認できます。

    crontab -l
    

ポイント

  • このコードは単発実行用に作成しています。
  • crontab によって 10 分ごとにスクリプトが起動されるため、スクリプト内で無限ループする必要はありません。
  • 送信する JSON は、Alexa スキル用の情報も含んでいますので、元のコードの構造をほぼ踏襲しています。

このように設定することで、Raspberry Pi のスクリプトが crontab によって 10 分ごとに実行され、グローバル IP アドレスが Lambda に送信されるようになります。

(3-2) 補足: ログファイルが大きくなりすぎるのを防ぐ

ログファイルが大きくなりすぎるのを防ぐためには、以下のような方法があります。

1. logrotate を利用する方法

  • logrotate は Linux の標準的なログローテーションツールです。
  • /etc/logrotate.conf/etc/logrotate.d/ 内に設定ファイルを作成し、一定期間またはサイズに応じてログファイルをローテーション(分割・圧縮・削除)できます。
例: /etc/logrotate.d/report_ip

以下のような設定ファイルを作成します(root 権限で作業してください)。

/home/pi/blynk-ip-reporter/report_ip.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    create 644 pi pi
}
以下、それぞれの設定の意味を簡単に説明します。
  • daily
  • → ログファイルを毎日1回、新しいファイルに切り替えます。
  • rotate 7
  • → 古いログファイルを7世代まで(直近7日分)保持します。7日より前のログは自動的に削除されます。
  • compress
  • → ローテーションしたログファイルをgzipで圧縮し、ディスク容量を節約します。
  • missingok
  • → ログファイルが存在しなくてもエラーにせず、処理を続けます。
  • notifempty
  • → ログファイルが空の場合は、ローテーション(切り替え)を行いません。
  • create 644 pi pi
  • → 新しく作成するログファイルに、パーミッション「644」と、所有者をユーザー「pi」とグループ「pi」に設定します。

つまり、この設定では毎日ログが切り替えられ、直近7日分のログが保存され、古いログは削除されます。また、空のログは保存されず、ログファイルは圧縮されるのでディスク容量が節約されます。

2. スクリプト内でログ出力を最小限にする

  • 例えば、デバッグ用のログ出力を必要最低限にするか、または不要な情報を出力しないように変更します。
  • また、>> でなく > にすると、実行毎に上書きされます。ただし、過去のログが必要な場合は注意が必要です。

例:

crontab のエントリを以下のように変更すると、ログが毎回上書きされます(前回のログが失われるので、履歴が不要な場合のみ推奨):

*/10 * * * * /usr/bin/python3 /home/pi/blynk-ip-reporter/report_ip.py > /home/pi/blynk-ip-reporter/report_ip.log 2>&1

まとめ

  • 長期間のログ蓄積が不要なら、> を使って上書きする方法もあります。
  • 履歴を保持しつつログサイズを管理するなら、logrotate を設定するのが一般的です。

どちらの方法が適しているかは、用途と必要なログ履歴に応じて選んでください。


課題

Lambda関数の仕様は下記だが、しばらく使っていると問題が見えてきました。
  • 定数 globalIP を追加し、Raspberry Pi から受信した時にこの値を更新します。
  • Alexaスキルを呼び出す際には、格納された globalIP を使います。

Raspberry pi は10分毎にIPアドレスをAPI Gatewayを介して、Lambda関数に渡します。Lambda関数はセッション(Alexaとの対話)の間、渡された globalIPを使えますが、セッションが終了すると、globalIPが消失してしまします。

言い換えれば、Raspberry pi からLambda関数に渡されたIPアドレスは、Lambda関数が終了すると消えてしまい、次のRaspberry pi からの送信までIPアドレスがない状態になります。この間にスキルからLambda関数を呼び出して、セッションを開始してもエラーになります。

実験をした結果、10分ではダメで、5分ならOKでした。
Lambda関数のタイムアウト(セッションを持続する時間)は3秒で、API Gatewayのタイムアウトは29秒なので、時間スケールが異なります。なぜこの時間になるのかはわかりません。

もう一点の課題は、常にLambda関数が動いていなければ、スキルが正常に動作しないことです。Alexaに部屋の温度を聞くのは、極まれに気の向いた時だけなのに、その時のためにLambda関数は常に動かしている必要があることです。本来であれば、スキルが呼び出されたら、動作を始めるべきだと思います。

Powered by Blogger | Designed by QooQ

keyboard_double_arrow_down

keyboard_double_arrow_down