ESP32で有線LANを使う20行

![SS 192.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/a41cd967-f549-8677-1efa-2e99bf69518d.png) ややこしいIP関連を順にときほぐす




# やりたいこと

ESP32DevKitC等のマイコンとPCを有線LANで接続し、通信を行いたいと思います。
そんなものはつなぐだけでOKであろう...と思ったのですが、実際には少し設定にコツが必要だったのでメモにまとめておきます。

![Meridian_W5500a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/10dde049-91cb-44f0-8c52-89d3a7185a5b.png)

↑このようなつなぎ方をして、

↓今回は下記のようにIPを振り分けたい。

![Meridian_W5500b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/afca4616-939e-4231-9b30-5216e274947f.png)

### なぜIPを1つのグループではなく、２つのグループに振り分けたいのか

上の図では、IPアドレスが青のグループと緑のグループに別れています。
なぜ２つのグループにわけたいかというと、

①Wi-Fiは、状況に応じていろんなアクセスポイントにつなぎなおしたい
②ESP等のデバイスと有線LAN通信する際は、いつも同じIPで使いたい
③もし野良MACアドレスがあっても物理接続内に閉じておきたい

という理由があるからです。
ここで少し不安になるかもしれませんが、１台のパソコンで２つの独立したIPアドレスを持ち、それぞれ別物として扱うことは可能です。

# 必要な前提知識
### MACアドレスとIPアドレスとDHCPサーバー
**MACアドレス**（xx:xx:xx:xx:xx:xxというやつ）は機器固有の番号で、物理アドレスとも言われます。小さなガジェットに至るまで、流通するネットワーク機器にはすべてIEEE管理の世界で１つだけの番号（グローバルアドレス）が割り当てられるのが原則です。
この記事では使用する機器のすべてにIEEE発行のMACアドレスが与えられていることを前提に話を進めますので、MACアドレス付きの安心な機器を使用してください。
<details>
  <summary>MACアドレスが与えられていない時（※自己責任）</summary>

電子工作ではMACアドレスをユーザーが登録できる場合もあります。
この時、アドレス衝突事故が起きないように十分な知識をもって挑む必要があります。
ランダム生成であれば衝突する計算上の確率は低そうですが、マイコンの起動直後に同じシードで番号を生成すれば同じ番号になりえます。また、任意のアドレスをうっかり２つ以上の機器に書き込んでしまうなどのヒューマンエラーもありえます。しっかり留意しないと衝突確率はすぐ100%になるわけです。

重複を避けるため、世界で一つだけの番号（が書かれたEEPROM）を[秋葉原で購入する](https://akizukidenshi.com/catalog/g/g101412/)こともできます。（アドレスの読み出し方は[こちら](https://qiita.com/infinite1oop/items/8b43acd6b576a7bd989d)が参考になります。）
転記と同時にオリジナルを破壊すれば理論上、IEEE管理のMACアドレスと重複することはなくなります。

また、MACアドレスは最初の2番目のビットで、IEEE管理のグローバルアドレスか、そうではないローカルアドレスかを判定しているという説明があります。[wiki記事](https://ja.wikipedia.org/wiki/MAC%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9)
具体的にはカンマで区切られた先頭の数字（オクテット）の0x02に該当するビットがオンだとIEEEが管理ではないローカルアドレスと判別されますので、先頭を0x02や0x06などに設定しておくと少し安心です。プライベートIP内で使用しているMACアドレスは原則的にインターネットには出て行かないことになっているので、自身が使用している環境内で重複がないようにしておけば、個人的な実験の範囲内ではよいでしょう。0x01に該当するビットにも特別な意味があり、ここは0（マルチキャストではなくユニキャストを示す）がよいです。
</details>

**IPアドレス**（xx:xx:xx:xxというやつ）もインターネットの世界では固有のアドレスとなります。
ただし例外として、**192.168.x.xだけはプライベートIP**として各家庭で自由に使えるアドレスとなっています。
192.168.x.xがなぜ自由に使えるか、重複しないかというと、インターネット全体が192.168.x.xを無視するようにできているためです。無視されるならインターネットにつなげないのではと心配になりますが、ルーターが192.168.x.xを固有のものに変換して通信を行ってくれる(NAT)ので大丈夫です。
また、家庭用ではよく**サブネット**として255.255.255.0という値を設定しますが、これはピリオドで区切られた４つの値をどこでネットワーク部とホスト部で区切るかの設定です。住所にたとえるとネットワーク部が町名、ホスト部は番地のような感じです。たとえば、192.168.3.10の場合、192.168.3がネットワーク部、10の部分がホスト部となります。
ネットワーク部までが同じIPアドレス同士あればそのまま通信が届きますが、ネットワーク部の番号が異なる相手とは、ブリッジなどの特殊な操作を加えない限り通信が行えません。
前述のMACアドレスも、基本的にはネットワーク部を越えて影響することはありません。
また**ゲートウェイ**という言葉もよく聞きます。これはネットワーク部の外に出るためのアドレスです。WiFiに接続されたPCのIPアドレスがたとえば192.168.3.2の場合、Wi-Fiのルーター192.168.3.1となります。

**DHCPサーバー**は、重複しないIPアドレスを発行し自動で割り振る仕組みです。家庭用のルーターは、プライベートIPアドレス帯(192.168)の範囲に収まるアドレスを発行します。
DHCPのIP発行機能はルーターのみが持ち、MacやWindowsといったOSは基本的にはこの機能を持ちません。複数の機器が勝手にIPを発行できてしまうと面倒なことになるでしょう。（もちろん可能ではありますが、今回は使用しないので説明は割愛します。）

# 実験環境
- Mac (M1ノートを使用)
- ターミナル
- python
- ESP32DecKitC
- W5500搭載ボード（有線LANモジュール）
- VSCode/PlatformIO


# 準備運動

ターミナルでIP関連を調べるコマンドをいくつか試しておきます。ご存知の方はとばしてください。

### ifconfig
ネットワークインターフェース（eth0, wlan0, loなど）の状態を確認・設定するためのコマンドです。
一覧が表示されます。

### arp -a

```
% arp -a
? (192.168.3.1) at xx:xx:xx:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.3.2) at xx:xx:xx:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.3.9) at xx:xx:xx:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.3.13) at xx:xx:xx:xx:xx:xx on en0 ifscope [ethernet]
...
```
APIは（Address Resolution Protocol）の略で、どのIPアドレスとどのMACアドレスが対応しているかの一覧を表示します。ちなみに最近通信した機器情報のキャッシュなので、通信していない機器のアドレスは表示されません。

### ping

```
% ping 192.168.3.1
PING 192.168.3.1 (192.168.3.1): 56 data bytes
64 bytes from 192.168.3.1: icmp_seq=0 ttl=64 time=10.773 ms
64 bytes from 192.168.3.1: icmp_seq=1 ttl=64 time=11.287 ms
64 bytes from 192.168.3.1: icmp_seq=2 ttl=64 time=12.007 ms
...
```
狙ったIPアドレスにシグナルを送り返信があるかを確かめるコマンドです。潜水艦のソナー（発した音波が物体に当たり跳ね返ってくる音"ping"があるか調べる方法）にちなんでいます。
固定IPアドレスの機器などに一度pingを送れば、arp -aのリストにも載るようになります。

### その他
ネットワークのIPを一斉にスキャンする方法などもありますので、必要に応じて調べてみてください。今回はarp -a とpingだけ知っていれば進められます。

# 有線LANドングルを設定する

有線LANドングルをUSBに接続し、固定IPを設定します。
有線LANドングルの固定IPが、もう一つのMac本体のIPアドレスということになります。

ドングルを接続し、アップルメニューから「システム設定...」→「ネットワーク」を選びます。
「その他のサービス」の中に、「USB 10/100/1000 LAN」とか「AX88179」「Realtek RTL8153」などの具体的なチップ名が表示されます。

##### 同様のものが複数表示され該当の機器がどれか判別できない場合
リストには過去に接続したものが記録されているので、整理します。
機器名をクリックし、「サービスを削除」→「削除」→「次回接続したときxxxを戻しますか？"はい"」を選択していきます。
リストがクリアになった状態で改めてドングルをUSBに接続すれば、該当のものがリストに現れますので特定できます。

次に、有線LANドングルに固定IPアドレスを設定します。
機器名を選択し、「詳細...」のボタンを押すと設定画面に移るので「TCP/IP」タグを選択します。
IPv4の構成の欄がデフォルトでは「DHCPサーバーを使用」となっていますので、これを「手動」に切り替えます。

![SS 255.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/e8d56ba6-9dfd-4852-b562-4dd2c1ea8473.png)

IPアドレスを設定できるようになるので、今回は「192.168.90.1」と設定してみます。
サブネットマスクは「255.255.255.0」とします。

![SS 258.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/b5febab5-96bd-437d-8e73-188233415cef.png)

この時点ではUSBドングルがW5500/ESP32に接続されていないので、**設定完了後も未接続状態のまま**となります。

# ESP32DecKitCとW5500を接続する

今回は下記のようにSPIを接続します。SPIの説明は割愛します。

| ESP32DecKitC |  | W5500 |
|-----|-----|-----|
| IO 19 | MISO | MI |
| IO 23 | MOSI | MO |
| IO 18 | SCK | SCK |
| IO 5 | CS | CS |
| IO 14 | RESET | INIT |
| 3.3V | 3.3V | V |
| GND | RESET | G |

# 通信内容などを決める

今回は、ESP32をクライアント、Mac(PC)をサーバとしてみます。
UDPという通信方式に簡単なテキストを乗せ、１秒ごとに双方向に通信を行い、受信したデータ内容をシリアルモニタやターミナルで表示するようにします。
（コードはエラー処理などを省き、なるべく簡略化しています。）

# ESP32DecKitCにコードを書き込む

PlatformIOなどでESP32用のプロジェクトを作成し、下記のコードを一部修正の上、ESP32DevKitCに書き込んでください。
AdruinoIDEの場合でも新規にプロジェクトを作成し、下記のコードをコピペの上、必要箇所を変更して書き込めば動くと思います。

#### 問題はMACアドレス
前述の通りMACアドレスは基本的にIEEE管理の与えられたものを使用してください。W5500を使う時にMACアドレスをユーザーが書き込む必要がある場合は、対応方法は前述のどこかに書いておきましたが自己責任となります。

```C++:main.cpp
#include <Ethernet.h>    // Ethernet用のライブラリ
#include <EthernetUdp.h> // UDP通信用のライブラリ

// ネットワーク設定
byte mac[] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}; // W5500のMACアドレス(固有のものに置き換え)
IPAddress local_ip(192, 168, 90, 2);               // このESP32のIPアドレス
IPAddress pc_ip(192, 168, 90, 1);                  // PCのIPアドレス

// UDP通信とデータ格納用の変数
EthernetUDP udp;        // UDP通信を行うためのオブジェクト
char packetBuffer[256]; // 受信したデータを格納するためのバッファ（最大256文字）

void setup()
{
  // ========== 初期設定（最初に1回だけ実行される） ==========
  Serial.begin(115200);          // シリアル通信を115200bpsで開始（PCとの通信用）
  Ethernet.init(5);              // Ethernetチップの制御ピンを5番に設定
  Ethernet.begin(mac, local_ip); // Ethernetを開始（MACアドレスとIPアドレスを設定）
  udp.begin(22224);              // UDP通信をポート22224で開始（このポートで受信待機）
}

void loop()
{
  // ========== メインループ（繰り返し実行される） ==========

  // ========== 受信処理 ==========
  int packetSize = udp.parsePacket(); // 受信したパケット（データの塊）のサイズを確認
  if (packetSize > 0)
  {                                     // パケットが届いている場合（サイズが0より大きい）
    udp.read(packetBuffer, packetSize); // パケットの内容をバッファに読み込み
    packetBuffer[packetSize] = '\0';    // 文字列の終端文字を追加（C言語の文字列ルール）
    Serial.print("Received: ");         // 「受信した：」と表示
    Serial.println(packetBuffer);       // 受信した内容を1行で表示
  }

  // ========== 送信処理 ==========
  udp.beginPacket(pc_ip, 22222);                  // PCのIP、ポート22222への送信を開始
  udp.write("Hello, This is ESP32. Are you PC?"); // 送信するメッセージを指定
  udp.endPacket();                                // パケットの送信を完了

  delay(1000); // 1000ミリ秒（1秒）待機してから次のループを実行
}
```
（タイトルに書いた20行の収まっていませんがご容赦を...）

書き込んだらあとはシリアルポートの反応を見るだけですが、この時点ではまだなにも受信できません。PC側の送信用のコードを実行していないためです。


# pythonコード
PC（MAC）用の送受信コードは下記になります。
こちらも要点を絞り、なるべく短くしました。

```python:mac_udp_server.py
#!/usr/bin/env python3
import socket  # ネットワーク通信用のライブラリ
import time

# ネットワーク設定（IPアドレスとメッセージを定義）
PC_IP = "192.168.90.1"                    # このPC（パソコン）のIPアドレス
ESP32_IP = "192.168.90.2"                 # ESP32のIPアドレス
MESSAGE = "Hi, This is PC. Are you ESP32?" # ESP32に送るメッセージ内容

recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP受信用ソケット
recv_sock.bind((PC_IP, 22222))            # ポート22222で受信待機
recv_sock.settimeout(0.1)                 # 0.1秒でタイムアウト

send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP送信用ソケット

last_send_time = 0  # 最後に送信した時刻を記録

while True:  # 無限ループで通信を継続
    # ========== 受信処理 ==========
    try: # ESP32からのメッセージを受信を試みる
        data, addr = recv_sock.recvfrom(1024)  # data=受信データ、addr=送信元情報
        print(f"Received: {data.decode('utf-8')}")  # 受信したバイトデータを文字列に変換し表示
    except socket.timeout:  # 0.1秒以内に何も受信しなかった場合
        pass  # 何もしない（エラーではないので無視）
    
    # ========== 送信処理 ==========
    if time.time() - last_send_time >= 1:  # 前回の送信から1秒以上なら
        send_sock.sendto(MESSAGE.encode('utf-8'), (ESP32_IP, 22224))  # メッセージを文字列からバイトに変換しESP32に送信
        last_send_time = time.time()  # 送信時刻を更新
```

# 実行

コードのあるディレクトリにCDなどで移動し、実行します。

```
$ CD (いま書いたコードのあるディレクトリ)
$ python mac_udp_server.py
```

ファイアーウォールがONになっている場合は、下記のアラートが出ますので許可します。使用しているpython自体への許可となりますので、ご注意ください。（※自己責任）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/d5bd5078-679e-4caa-b768-259d5bb7420e.png" width = 400>



ESP32DecKitCについては、PC（MAC）と有線LANで接続しつつ、書き込み時に使用したUSBシリアルもモニタリング用として接続したままにします。
ESP32DecKitCに電源が入っていて有線LANと接続されていない場合、Python側は相手先のIPを見つけられず実行直後にエラーとなります。

万事うまくいけば下記のようになります。

![SS 259.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/30f96007-2232-4d89-b8a2-0d9abcabeb47.png)

画像の上側の白いウィンドウがpythonを実行したターミナルで、
画像の下側の黒い画面がESP32のシリアルモニタの表示です。
それぞれ、受信した内容を表示します。

# まとめ

このように２種類のローカルIPアドレスを１つのシステムに共存させることができれば、たとえばラズパイなどを複数使うロボットなどで、制御や画像処理の情報は固定IPによる閉じたLAN環境のネットワークで通信し、WIFIについては開いた環境のネットワークで通信、と使い分け、閉じたLAN環境を設定の変更なく安定して使用することができるようになると思います。
自前で準備した固定IPが接続先のDHCPと重複しないかとドキドキしたり、そのドキドキを避けるためにDHCPを使用し、結果、接続のたびに設定を書き直す、というような手間を省けるかもしれません。