じゅんさんの休日 -4ページ目

Thonny経由じゃないとTLSコネクションのところで固まって、MQTT接続できない!

先週、Raspberry Pi Pico WからAWS IoT CoreへMQTTSのパブリッシュがやっとできたっていう話を書きました。

よーしこれで、今週はサクッとAWS側からのトピックをサブスクライブして、LED操作テストだー!って思ってました。

 

実際、途中までサクッとできたのですが・・。

実はもう1つ落とし穴がありましたっていうのが、今日の話です。

 

 

今週の落とし穴 

 

Thonny経由じゃないとTLSコネクションのところで固まって、MQTT接続できない!

 

ということで、備忘録。

 

まず、現象は上記の通りですが、もう少し詳しく書くと、

ThonnyのEditor実行だとエラーなくWi-Fi接続→DNS名前解決→TLS接続→MQTTSパブリッシュ/サブスクライブ可能なのですが、

同じmain.pyで単体起動(Thonny接続せず電源だけつなぐ起動という意味)すると、

Wi-Fi接続とDNS名前解決はできるけどTLS接続ができないで固まる・・というもの。

 

ググってみても、ほとんどの場合「Thonnyで実行してうまくいく」ようにしか書いてなくて、

(私のリサーチ力不足もありますが、そう見えて)、

いざPico W単体起動したらうまくいかなかった、っていうときの対処がすぐに見つからず・・。

土曜日の午後まるまるつかってやっと解決できました。

 

 

トラブルシュートの道 

 

Pico Wの電源はMicro USB type-B。昔の携帯電話とか、モバイルバッテリーで使ってたやつです。

マイクロBのアダプター、普段使わないので、お道具箱の奥のほうから引っ張り出してくるわけですが、

そんなこともあり、最初は「あれ、アダプター壊れてるかな」程度の感覚から始まりました。

 

で、さっきまでPCにつないでいたUSBケーブルで、Thonnyを接続せずに起動してみてもNG・・

ってことで、底なし沼にいることに気づきました。

 

この時点で、Thonny経由でプログラム実行するときと単体起動で動かすときに、挙動の差があるなんて、

ひとかけらも思わなかったので、「????」のまま、なんども起動停止、プログラム見直し・・。

 

単体起動だと、Wi-Fi接続とDNSの起動タイミングがずれるから、数秒間waitを入れたほうがいい、とか

TLSは時刻同期が必須だけど、単体起動の時NTPの時刻同期が遅れてTLSハンドシェイクが失敗する、とか。

 

なんやかんや試しながら、調べていたら

RaspberryPiフォーラムの掲示板までたどり着いて、どこの国の方か知りませんが、

全く同じ現象でハマってた人たちを見つけました。

 

 

  ■原因のまとめ

 

  1. Wi-Fiモジュールの初期化タイミング問題
    Pico Wの無線チップ(CYW43439)は、起動直後は内部ファームがまだ立ち上がっておらず、
    すぐに wlan.connect() を呼ぶと接続が不安定になりやすい。
     
  2. RTC(内蔵時計)が未同期のままTLSを開始していた
    起動直後のPico Wはシステム時刻が「1970/01/01」らしい。
    この状態でTLSハンドシェイクを行うと、証明書の有効期限チェックで内部的にエラーとなりハングすることがある。
     
  3. TLS開始直前のメモリ状態が不安定
    Wi-Fi接続処理やNTP同期のあと、ヒープメモリが断片化しており、
    そのまま ssl.SSLContext() を呼ぶとTLS初期化中にクラッシュする。
     
  4. MQTTの wait_msg() がブロッキングしていた
    wait_msg() はThonnyのREPLスレッドを前提にしたブロッキング処理。
    スタンドアロン起動では割り込みが発生せず、メッセージ受信が止まる。

めちゃくちゃ勉強になりました。

サーバーでは考えたことがないような現象が、マイコンでは割とあるあるなのかも、

という感覚を持てただけでも大きな収穫です。

 

 

  ■解決した方法

 

  1. Wi-Fiモジュールの初期化
    time.sleep(3) などでチップ起動完了を待つようにしました。
     

    def connect_wifi():

        wlan = network.WLAN(network.STA_IF)

        wlan.active(True)

        time.sleep(3)

        wlan.connect(WIFI_SSID, WIFI_PASSWORD)

        for _ in range(60):

            if wlan.isconnected():

                blink(2, 0.1)  # ★フェーズ1 Wi-Fi成功(起動デバッグ用)

                return

            time.sleep(0.5)

        steady_on()  # ★Wi-Fi失敗(起動デバッグ用)

        while True: time.sleep(1)

    チップ起動時間を稼ぐと良いらしい、というネットの情報をもとに追加しました。

  2. RTC(内蔵時計)が未同期のままTLSを開始していた
    ntptime.settime() でNTP同期を行い、正しい時刻を設定しました。

    時刻同期の関数を定義し、wi-fi接続の後実行するようにしました。
     

    def sync_time():

        for _ in range(3):

            try:

                ntptime.settime()

                blink(3, 0.1)  # ★フェーズ2 NTP成功(起動デバッグ用)

                return

            except:

                time.sleep(3)

        steady_on()  # ★NTP失敗(起動デバッグ用)

        while True: time.sleep(1)

     

  3. TLS開始直前のメモリ状態が不安定
    gc.collect() を数回呼び出してから time.sleep(2) で安定化を待つようにしました。
    正直、これが一番わからなかったところでした。
    gcは、ガベージコレクションのこと。
    メモリ領域のごみ(ガベージ)を整理するやつ。情報処理の試験でしか聞いたことがない。
    お勉強はしておくものだなぁと思った次第です。

    とか言いつつ、実はあんまりちゃんとわかってないです・・・笑
    ネットの情報から「これを使ったらうまくいったよ」というのをもとに、おまじない的に入れてみたら
    うまくいった、というレベルです・・恥ずかしながら。
     

    def mqtt_connect():

        try:

            # ★TLS前準備:GC3回+ウェイト

            for i in range(3):

                gc.collect()

                time.sleep(1)
     

  4. MQTTの wait_msg() がブロッキングしていた
    非ブロッキングの check_msg() に変更し、time.sleep(0.5) のループで周期的にポーリングするようにしました。

    これまた難解だったのですが、
    Thonny(USBシリアル)経由のときは、割り込み処理がThonnyコンソールによって調整されるので、
    wait_msg()内のソケットreadが動くので、MQTTのサブスクライブができていた、ということらしい。

    一方、単体起動ではThonnyコンソール上で動いていないので、割り込み調整がない。
    なので、MicroPythonのイベントループがブロッキング状態で固まってしまう・・ということらしいです。
    (なにせ先人たちの熱い掲示板の賜物でして、あんまり深くわかってません)

    これを解消するには、「非ブロッキングポーリング化」がよいらしいので、
    check_msg()をぐるぐる呼んでサブスクライブできるようにした、っていうことです。

    def main():
        connect_wifi()
        sync_time()
        client = mqtt_connect()

        # 非ブロッキング受信ループ
        while True:
            try:
                client.check_msg()  # ← wait_msg() の代わり
                time.sleep(0.5)

 

  ■振り返り

 

RaspberryPi Picoはマイクロコンピュータ。なので、搭載できるメモリも当然少ない。
だから、開発するときにそういうことも考えなきゃいけないんだなー、って実感。
そもそもワタシ、インフラ屋さんでノンプログラマーですので、未知の世界すぎました。

こんなに苦しんでもやっぱり動いた時の喜びには代えられない。
模型作りもマイコン開発も、根っこは同じですね。

次は、複数LED接続!だけど、今回のことでよくわかった「電力」問題。
ただつなげばよい、ってわけにはいかなそうだなぁ・・。

つづく

 

 

Raspberry Pi Pico WからAWS IoT Core ハマった話

Raspberry Pi Pico WからAWS IoT CoreへMQTT Publishするまでにハマった2つの罠の話。

しばらく間が空いたら、また自分で同じ罠にハマりそうなので、備忘録として残そうかなと。

誰得なブログです。

 

■やったこと

Raspberry Pi Pico W と AWS IoT Core をつないで、センサーのデータを MQTT でパブリッシュする。

■ハマったこと

umqttのバージョン違いによるエラー地獄。

ネット上の記事を見ると、`umqtt.simple` を使うと書いてあるのですが、

GitHubから.pyをコピーして/libに置く手順になっています。

 

これが、libに配置しても全然うまく動かない。

 

TypeError: unexpected keyword argument 'ssl_params'
TypeError: unexpected keyword argument 'ca_certs'

 

■解決した方法

Raspberry Pi Pico WをWi-Fiでインターネットに接続して、mipで最新のmqttをダウンロードした。


Thonnyのコマンドラインから、

import mip
mip.install("umqtt.simple")

を実行

 

これで `/lib/umqtt/simple.py` が配置されます。

/libがなかったら勝手に作ってくれる。

 

■2つ目の罠

当初は `ssl_params` に PEM を渡そうとして失敗しました。

DER形式にすればすんなり動く、みたいにネット記事には書いてあったけどどうにもうまくいかず・・。


正解は DER形式に変換した証明書を Pico W に置き、SSLContext を自作して MQTTClient に渡す方法でした。

■↓こんな感じ

import ssl
from umqtt.simple import MQTTClient

# 証明書(DER形式)
ROOT_CA   = "/certs/routeca.der"
CERT_DER  = "/certs/certificate.der"
KEY_DER   = "/certs/private.der"

def get_ssl_context():
    """AWS IoT 用 SSL/TLS コンテキスト作成"""
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.verify_mode = ssl.CERT_REQUIRED
    ctx.load_verify_locations(cafile=ROOT_CA)            # ルートCA
    ctx.load_cert_chain(certfile=CERT_DER, keyfile=KEY_DER)  # デバイス証明書 & 秘密鍵
    return ctx

def init_mqtt_client():
    ssl_ctx = get_ssl_context()
    client = MQTTClient(
        client_id="mypico_w",
        server="xxxxxx.iot.ap-northeast-1.amazonaws.com",
        port=8883,
        keepalive=60,
        ssl=ssl_ctx
    )
    client.connect()
    print("MQTT Client Connected to AWS IoT Core!")
    return client

 

 

そもそも何でこんなことしてるかっていうと、
クルマのヘッドライト、ウィンカー、ブレーキランプにLEDを仕込んで、

スマホから点灯消灯の操作ができたらおもしろいかなぁってのを始めて、

でっかい装置でいったん完成したけど、「小型化してインターネットにつなげたいよなぁ」っていう野望がありまして。

 

これで基本の接続はできたので、すこしずつ「プラモのIoT」作っていきます!

 

 

 

 

 

 

 

 

1/24 ニッサン RPS13 180sx Type X '96 カスタムホイール #1

今年の3月から制作を開始して、5月頃から中断している180。

 

 

シャシーのS15シルビアの流用です。

実写の180はS13ですが、このキットはコストの関係でしょうか、S15シルビアを流用しています。

サスペンションパーツは180用の新金型。
 

だからなのか、S15シルビアもそうだったのかわかりませんが、

軸がぶれます。
ホイールをつけたらほぼ見えないところなので、気にするほどではないですけどね。

 

リアサスペンション。

ロアアームもきれいに再現されています。

 

オイルパン、ミッションケース、ドライブシャフト、エキマニ、エキパイ、タイコ、

ぜんぶシャシーにモールドされた一体形成なので、がっつりマスキングして塗装します。

 

どうせ完成したら裏っ返してまで見ることはほとんどないんですけどね。

無駄に、複数のシルバーを使い分けて塗装したりなんかしています。

 

ちょっとした模型的演出で、タイコとか触媒部分を薄いゴールドにしてみたりして。

 

ブレーキディスクとキャリパーも塗装します。

 

今回キャリパーは黒色仕上げなので、

 

こんな感じで完了。

赤くしたり青くしたりするときは、さらにここからディスク側をマスクして・・っていう作業になりますが、

今回は黒なのでこれでOK。

 

なんとなく物足りなかったので、サスペンションスプリングをキャンディーブルーに。

 

黒地のショックアブソーバーにダークブルーなスプリング・・・。

全然目立ちませんでした笑

 

リアサスペンションユニットも塗装して

 


実際のクルマがこういう色っていうことではなくて、

駆動系とアームが別物ってわかるように、っていう感じで塗りました。

1つのパーツだけど、複数の部品から成り立っている感、というんですかね。

 

 

足回りのパーツがそろいました。

 

組み上げたらこんな感じ

 

 

 

マフラーはメッキパーツのままで行こうかな、と。

 

 

これが大体3月頃のお話です。9月現在、まだ完成してません・・

 

11月の岡崎展示会に持っていける・・かなぁ。ケッテンクラート優先かなぁ、という感じです。