【ITニュース解説】ESP-Now master-slave in Micropython
2025年09月27日に「Dev.to」が公開したITニュース「ESP-Now master-slave in Micropython」について初心者にもわかりやすく解説しています。
ITニュース概要
ESP32とMicroPythonで、独立したデバイス間通信プロトコルESP-Nowを使い、多数のデバイスを繋ぐマスター・スレーブネットワークを構築する方法を紹介。複雑な通信課題を解決するための実践的なコードと設定手順を解説する。
ITニュース解説
この解説では、ESP32デバイス間で簡潔な通信を可能にする「ESP-Now」プロトコルを用いて、マスター・スレーブネットワークを構築する方法と、その技術的な詳細について説明する。ESP-NowはWi-FiチップであるESP32に搭載された低レベルの通信機能であり、一般的なインターネット通信で使われるHTTPプロトコルとは独立して動作するのが特徴だ。これにより、より高速で効率的なデバイス間の直接通信が実現できるが、複雑なネットワークを構築しようとすると、特有の課題に直面しやすい。本記事は、そうした複雑なシナリオに対応するため、数週間の試行錯誤とAIの助けを借りて開発された改良版のコードについて解説している。
このシステムは、一つのマスター(ハブ)デバイスが中心となり、多数のスレーブデバイスが星形(スター)ネットワークとして接続される構成だ。マスターは外部ネットワークへのゲートウェイとして機能し、スレーブデバイスはマスターを介して通信を行う。前回の記事では、スレーブがさらに自身のローカルネットワークを構築することでWi-Fiの到達距離や接続可能なデバイス数の制限を克服する試みも紹介されたが、今回はそのコードの制約を解決するための、より堅牢なクラスが提示されている。
提示されたESPCommsクラスの主な役割は、指定されたMACアドレスへ少量のデータパケットを送り、その結果を呼び出し元に返すことにある。ESP-NowはIPアドレスやアクセスポイント名といったOSI参照モデルのより上位層の概念を扱わないため、非常に直接的な通信が可能だが、デバイスが事前にアクセスポイント(AP)モードまたはステーション(STA)モードのいずれかに設定されている必要がある。また、ESP-Now通信はHTTPなどの上位機能から「見えない」ため、これらの機能と同時に実行できる。
クラスの初期化関数である__init__()は、システム設定において非常に重要だ。マスターデバイスとスレーブデバイスでは、Wi-Fiインターフェースの設定順序が厳密に定められている。マスターは最初にSTAインターフェースを有効にして家庭用ルーターに接続し、そのルーターが使用しているWi-Fiチャンネルをシステム全体のチャンネルとして設定する。その後APインターフェースを有効にし、最後にESP-Nowをアクティブにする。この順序により、ESP-NowがSTAインターフェースと同じWi-Fiチャンネルを使用することが保証される。一方、スレーブデバイスはまずAPインターフェースを有効にしてチャンネルを設定し、その後にSTAインターフェースを有効にする。この逆の順序によって、APインターフェースのチャンネルがESP-Now通信に使われるようにする。この順序を間違えると、予期せぬエラーが発生し、正しく動作しない可能性がある。システム設定はconfig.pyという別のモジュールで管理され、ルーターのSSIDやパスワード、I/Oピンの割り当てなどがJSONファイルに保存され、必要に応じて読み書きされる。
closeAP()関数はセキュリティ機能を提供する。デバイス起動後2分間は、管理者がブラウザ経由でAPに接続し、設定ファイルを書き換えたりデバイスを再起動したりできる。しかしこの期間を過ぎると、APのSSIDは「-」に変更され、ランダムなパスワードが適用されるため、外部からの接続はほぼ不可能になる。これにより、物理的にデバイスを再起動しない限り、悪意のある干渉を防ぐことができる。
ESP-Nowのピア管理は非常に繊細だ。addPeer()関数は、e.add_peer()メソッドを使って通信相手(ピア)のMACアドレスを登録するが、これは一度しか実行できない。そのため、この関数はすでに登録済みのピアを内部リストで管理し、重複登録によるエラーを防いでいる。また、ESP32デバイスは通常、STAとAPの二つのインターフェースそれぞれにMACアドレスを持つため、両方のMACアドレスをピアとして登録する必要がある場合がある。Wi-Fiチャンネルが変更されるとピアテーブルが無効になり、再作成するにはデバイスのリセットが必要となる点も重要だ。
send()関数とreceive()関数は、非同期で動作するこのシステムの中心的な機能だ。receive()関数はメッセージを待ち受け、応答を返すループの中で非同期に実行される。初期のバージョンでは、送受信が同時に行われると問題が生じる可能性があったため、本コードでは排他制御の仕組みが導入されている。他の方法としてロックの使用が検討されることもあるが、追跡困難な問題を引き起こす可能性があるため、ここではrequestToSendとsendingという二つのフラグを使った独自の方法が採用されている。
具体的には、receiveループ内でメッセージを受信し、処理中はsendingフラグがFalseに設定される。メッセージ送信要求が発生すると、requestToSendフラグがTrueになり、send関数はsendingフラグがTrueになるまで待機する。receive関数はrequestToSendを監視しており、これがTrueになるとsendingをTrueにしてsend関数を解放し、メッセージが送信される。その後receive関数はsend処理の完了を待つ。
このシステムはWi-Fiルーターのチャンネル変更にも対応するよう設計されている。ルーターが自動的に空いているチャンネルへ移動すると、スレーブデバイスはマスターからの通信が途絶えたと認識する。しばらくして、スレーブはマスターに対して「ping」メッセージを送信し、マスターが「pong」で応答すれば適切なチャンネルにいると判断する。応答がなければ、スレーブはチャンネルを変更して再度「ping」を試みる。send関数が応答を待っている間に「ping」メッセージが届く可能性があるため、コードはこれらを検出し、自身の応答ではない「ping」メッセージを無視するようになっている。
さらに、receive()関数にはメッセージのリレー機能も含まれている。受信したメッセージが「!」で始まる場合、それはこのデバイス宛ではなく、他のスレーブデバイスへの転送要求と判断される。メッセージから送信先のMACアドレスと実際のメッセージ内容を抽出し、指定されたスレーブへ転送し、その結果を元の呼び出し元に返す。ただし、このシステムはスレーブからの「ping」以外のメッセージに対しては、その発信元を特定するためのアドレス指定機能を持たない。より汎用的な双方向通信が必要な場合は、メッセージ送信者を識別するためのアドレス指定が必要になるだろう。
2.4GHz帯のWi-Fiは混雑しやすいため、多くのルーターは空いているチャンネルを自動的に探索し、数時間おきにチャンネルを変更することがある。パソコンなどのデバイスはルーターとの対話を通じて事前の通知を受け取れるため、ユーザーにとってはほとんど意識されない。しかし、シンプルなIoTデバイスはこのようなチャンネル変更に自力で対応するのが難しい。このシステムは、そのようなチャンネル変更問題を解決するため、Channelsクラスと連携して動作するよう設計されており、その詳細は本シリーズの続編で解説される予定だ。
本記事で紹介されているコードは、単純なシナリオを扱う既存の例ではカバーしきれない複雑な問題に対処するために、多くの試行錯誤とAI(DeepSeek)の支援を受けて開発された。同様の課題を解決し、その知見を公開している例は、本コードの開発時点では見当たらなかったという。完全なコードとドキュメントはGitHubリポジリで公開されており、常に最新のバージョンを参照することが推奨される。