0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESP32 × RustでARPパケットを打ってみる

0
Last updated at Posted at 2026-05-02

はじめに

前回記事ではArduinoを使ったセグメントチェッカーを作った。

今回はESP32 × Rustという構成で、ESPからPCに向かって ARPパケットを投げてみる。
VSCodeを使ったRustの環境構築から始めて、最後はWiresharkでパケット受信しているところまで確認する。

Rustのコードは最終的にはGithubに上げる。
まだこの記事の段階では検証中なので、この記事にはGithubへのURLは記載されていない。
次か、次の次の記事あたりではGithubにPushしたい。

完成品

ESP→W5500→PCの順にARP問い合わせフレームが流れる
PCはARPに応答する

IMG_3469.jpeg

7, 8行目でARPの問い合わせと応答が行われている。

image.png

Rustのコードは長いので巻末に載せる
当たり前だが main.rs だけでは動かない、依存するライブラリが必要

環境構築(ソフトウェア編)

まずは環境構築と最初のLチカならぬ「Hello, world!」を拝むまでの手順です。

1. ツールチェーンのインストール

RustでESP32を扱うには、通常のRustツールチェーンに加えて、Espressif(ESP32のメーカー)専用のツールが必要です。以下のコマンドで一気に揃えます。

# ESP32用環境構築ツール
cargo install espup
# ESP32ツールチェーンのインストール(数分かかります)
espup install
# テンプレート生成ツール
cargo install cargo-generate
# 書き込み・モニターツール
cargo install ldproxy cargo-espflash

インストールが終わったら、環境変数をロードして準備完了です。

. $HOME\export-esp.ps1

確認結果:

xtensa-esp-elf-gcc.exe (crosstool-NG esp-15.2.0_20250920) 15.2.0

2. プロジェクトの生成

テンプレートから雛形を作成します。ここで作業用ディレクトリ名を入力する。

cargo generate esp-rs/esp-idf-template cargo
  • MCU: esp32
  • STD support: true

3. Windowsの壁「パス長制限」を回避する

Windows環境で普通にビルドを始めると、中間ファイルの階層が深すぎて「パスが長すぎます(Too long output directory)」というエラーで落ちます。

これを回避するため、ビルド専用の作業場をドライブ直下に作成します。今回は D:\tmp を作成しました。

次に、プロジェクト内の .cargo/config.toml に以下の設定を追記し、ビルド先(targetフォルダ)をここに逃がします。

[build]
target-dir = "D:/tmp"
target = "xtensa-esp32-espidf"

これが非常に重要なポイントです。 ソースコードはいつもの場所に置いたまま、ビルドだけを安全な場所で行うことができます。

4. 運命のビルド実行

いよいよESP32に書き込みます。

cargo espflash flash --monitor

ここで注意したいのが、esp-idf-sys というライブラリのビルド。これがとにかく長い!裏でC言語のSDKを丸ごとビルドしているため、コーヒーを淹れて気長に待つ必要があります。

私の環境では、無事にビルドが完了するまで 約10分 かかりました。

ビルド完了!:

Finished `dev` profile [optimized + debuginfo] target(s) in 9m 41s
Chip type:         esp32 (revision v3.1)
Crystal frequency: 40 MHz
Flash size:        4MB
MAC address:       30:76:f5:ac:39:e0

5. 勝利の「Hello, world!」

フラッシュが完了し、シリアルモニターが立ち上がると……。

I (407) main_task: Started on CPU0
I (407) main_task: Calling app_main()
I (407) esp32_loopchk: Hello, world!
I (407) main_task: Returned from app_main()

見事、Rustで書かれたコードがESP32上で産声をあげました!

ここまでのまとめ

WindowsでのESP32 Rust開発は、「ビルド先を短いパスに設定する(target-dir)」 ことさえ知っていれば、非常に快適にスタートできます。

次回は、いよいよW5500(イーサネットモジュール)を2枚接続し、SPI通信の初期化に挑戦します。

寄り道~開発サイクル~

環境構築が完了したら、次は「コードを書いて、動かす」という日々のサイクルをいかに効率化するかが重要です。寄り道として、VSCodeのターミナルからPCアプリ感覚でESP32を操るための設定と、基本操作をまとめます。

1. 魔法のコマンド cargo run

通常、マイコン開発では「ビルド」と「書き込み(フラッシュ)」を別々に行いますが、.cargo/config.toml を適切に設定することで、PC向け開発と同じように cargo run 一発で書き込みまで完了させることができます。

プロジェクトの .cargo/config.toml に以下の記述があることを確認します。

[target.'cfg(target_os = "espidf")']
runner = "espflash flash --monitor"

この runner 設定のおかげで、「コード変更」→「cargo run」 という流れるような開発が可能になります。

2. 開発サイクル:Edit -> Run -> Stop

Step 1: コードを書き換える

src/main.rs を編集し、保存します。

Step 2: 実行する

ターミナル(環境変数がロードされた状態)でコマンドを叩きます。

cargo run

ビルドが始まり、成功すると自動的にESP32への書き込みが開始されます。

Step 3: 動作を確認する

書き込みが終わると、そのまま 「シリアルモニター」 が立ち上がります。ESP32から送られてくるログ(println! の結果など)がリアルタイムに表示されます。

Step 4: 終了する(ストップ)

モニターを終了して、次のコード変更に戻りたいときは以下のキーを打つだけです。

  • Ctrl + C

これで、モニターが止まり、コマンド入力画面に戻ります。
※モニターを止めてもESP32側のプログラムは動き続けているので、再度モニターしたいときは cargo espflash monitor で繋ぎ直すことも可能です。

3. モニター中の便利な小技

モニターモード中に「最初から動きを確認したいな」と思ったら、基板のボタンを押さなくてもキーボードでリセットをかけられます。

  • Ctrl + R :ESP32をソフトリセット(再起動)

前回の環境構築編に続き、今回はついにW5500(イーサネットモジュール)をRustで制御し、実際にパケットをネットワークへ放流するまでの道のりをまとめます。

「Hello, World」の次がいきなり「自作パケットの送出」というハードな内容ですが、Rustの強力なライブラリ群のおかげで、驚くほどスマートに実装できました。


ネットワーク実践編:Rustでパケットを自作して放流する

環境が整ったので、次はハードウェアの制御に移ります。今回の標的はARP(Address Resolution Protocol)パケット。ネットワーク上の機器に「あなたのMACアドレスを教えて!」と尋ねる、通信の第一歩となるパケットです。

1. W5500との接続(物理層の開通)

まずはESP32とW5500をSPIで接続します。
W5500は「MACRAWモード」という、イーサネットフレームを丸ごと生データとして扱えるモードで動作させます。これにより、Rust側でパケットの中身を1ビット単位で制御できるようになります。

配線ピンアサイン

今回のARPテストで使用した、ESP32とW5500(1台目)の接続表です。

W5500 ピン名 ESP32 ピン名 (GPIO) 役割 備考
VCC 3.3V 電源 パスコン(0.1μF)をGNDとの間に入れるとなおよし
GND GND グランド 共通グランド
SCK GPIO 18 SPI クロック peripherals.spi2 を使用
MISO GPIO 19 SPI データ入力 W5500 → ESP32
MOSI GPIO 23 SPI データ出力 ESP32 → W5500
CS GPIO 5 チップセレクト SpiDeviceDriver で制御
RST GPIO 17 ハードリセット 起動時に PinDriver で操作

W5500のピン配置はW5500 ピン配置を参考にさせていただきました。

2. Rustでのパケット生成:smoltcpの威力

C言語ではバイト配列を手計算で組み立てるのが一般的ですが、Rustには smoltcp という鉄壁のネットワークスタックがあります。

今回はこれを利用して、以下のような流れでパケットを構築しました

  • Ethernetフレーム の作成(送信元MACを設定)
  • その中に ARPリクエスト をカプセル化(「10.0.0.254(私のPC)はどこ?」という質問を込める)

3. 実践! cargo run 後の世界

コードを書き終え、いよいよ実行です。

cargo run

ビルドが終わり、書き込みが完了した瞬間のログがこちら。

I (370) main_task: Started on CPU0
I (370) efuse_init: Chip rev: v3.1
I (400) esp32_loopchk: === W5500 MACRAW ARP Sender Start ===
I (510) esp32_loopchk: Interface up: mac=02:ad:be:ef:fe:ed, ip=10.0.0.200
I (520) esp32_loopchk: ARP Request Sent to 10.0.0.254

ログにはしっかりと 「ARP Request Sent」 の文字が刻まれました。

勝利の瞬間:Wiresharkでのキャプチャ

プログラムが動いていることを証明するため、PC側でWiresharkを立ち上げて網を張ります。
ターゲットは自分のPCのIPアドレス(10.0.0.254)。

ついにその時が来ました。

画面にパッと現れたのは、淡い青色の一行。

  • Source: 02:ad:be:ef:fe:ed(Rustで指定した偽のMACアドレス)
  • Info: Who has 10.0.0.254? Tell 10.0.0.200

ESP32から放たれたパケットが、LANケーブルを通っておれのPCに正しく届き、PC側も「俺はここだよ!」とARP Replyを返してくれました。RustとWindowsが、ネットワークの深淵で握手した瞬間です。

(再度掲載)Wiresharkのキャプチャ

image.png


考察:なぜ「Rust」なのか?

今回の実装を通じて、改めてC言語とRustを比較してみました。

比較項目 C言語 Rust (smoltcp)
安全性 配列の境界ミスで即クラッシュ コンパイラがバッファミスを許さない
可読性 0x08, 0x06...と16進数が並ぶ ArpOperation::Request と名前で書ける
保守性 メモリ管理に常に気を張る 所有権システムが自動で守ってくれる

特にネットワークパケットのような「サイズを間違えると通信が崩壊する」データを扱う場合、Rustの型システムは最強のガードレールになります。

今回のまとめと次への展望

  • W5500をMACRAWモードで制御成功。
  • smoltcpによるARPリクエストの生成・送信に成功。
  • Wiresharkで物理パケットの疎通を確認。

ハードウェアの潔白は証明されました。次はいよいよ、このW5500を 「2枚」 同時に使い、「自分が出したパケットを自分で拾う」 というループ検知の核心部分へ挑みます。

インジケータも赤(ループ検知)と緑(健全)の2色を用意して、現場で使えるプロ仕様のツールに仕上げていくぜ!

展望図解

W5500を2つ使ってループ検知装置を作りたい。概要はこちら
コンデンサの挿入位置やVINの線がW5500にのびてるところが変だけど、ざっくりこんな感じのイメージ

image.png

もっとビジュアルよく、かつ意味不明な配線

image.png

糸冬了!!

参考

付録:Rustコード

main.rs
use std::thread;
use std::time::{Duration, Instant as StdInstant};

use cotton_w5500::smoltcp::Device as W5500Device;
use esp_idf_hal::{
    gpio::PinDriver,
    peripherals::Peripherals,
    spi::{self, SpiDeviceDriver, SpiDriver},
    units::Hertz,
};
use smoltcp::{
    phy::{Device, TxToken},
    time::Instant,
    wire::{
        ArpOperation, ArpPacket, ArpRepr, EthernetAddress, EthernetFrame, EthernetProtocol,
        EthernetRepr, Ipv4Address,
    },
};

const LOCAL_MAC: [u8; 6] = [0x02, 0xAD, 0xBE, 0xEF, 0xFE, 0xED];
const LOCAL_IP: [u8; 4] = [10, 0, 0, 200];
const TARGET_IP: [u8; 4] = [10, 0, 0, 254];

fn now_from(start: &StdInstant) -> Instant {
    Instant::from_millis(start.elapsed().as_millis() as i64)
}

fn send_arp_request<D: Device>(
    dev: &mut D,
    src_mac: EthernetAddress,
    src_ip: Ipv4Address,
    target_ip: Ipv4Address,
    now: Instant,
) {
    if let Some(tx) = dev.transmit(now) {
        let arp = ArpRepr::EthernetIpv4 {
            operation: ArpOperation::Request,
            source_hardware_addr: src_mac,
            source_protocol_addr: src_ip,
            target_hardware_addr: EthernetAddress([0x00; 6]),
            target_protocol_addr: target_ip,
        };
        let eth = EthernetRepr {
            src_addr: src_mac,
            dst_addr: EthernetAddress::BROADCAST,
            ethertype: EthernetProtocol::Arp,
        };
        let frame_len = eth.buffer_len() + arp.buffer_len();

        tx.consume(frame_len, |buf| {
            let mut frame = EthernetFrame::new_unchecked(buf);
            eth.emit(&mut frame);

            let mut arp_packet = ArpPacket::new_unchecked(frame.payload_mut());
            arp.emit(&mut arp_packet);
        });

        log::info!("ARP Request Sent to {}", target_ip);
    } else {
        log::warn!("No TX token available; ARP request not sent");
    }
}

fn main() {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    thread::Builder::new()
        .stack_size(65536)
        .spawn(worker)
        .unwrap()
        .join()
        .unwrap();
}

fn worker() {
    log::info!("=== W5500 MACRAW ARP Sender Start ===");

    let peripherals = Peripherals::take().unwrap();
    let pins = peripherals.pins;

    let mut rst = PinDriver::output(pins.gpio17).unwrap();
    rst.set_low().unwrap();
    thread::sleep(Duration::from_millis(100));
    rst.set_high().unwrap();
    thread::sleep(Duration::from_millis(500));

    let spi_driver = SpiDriver::new(
        peripherals.spi2,
        pins.gpio18,
        pins.gpio23,
        Some(pins.gpio19),
        &spi::SpiDriverConfig::new(),
    )
    .unwrap();

    let spi_dev_cfg = spi::config::Config::new().baudrate(Hertz(10_000_000));
    let spi_dev = SpiDeviceDriver::new(&spi_driver, Some(pins.gpio5), &spi_dev_cfg).unwrap();

    let bus = w5500::bus::FourWire::new(spi_dev);
    let local_mac = EthernetAddress(LOCAL_MAC);
    let local_ip = Ipv4Address::from_octets(LOCAL_IP);
    let target_ip = Ipv4Address::from_octets(TARGET_IP);

    let mut dev = W5500Device::new(bus, &LOCAL_MAC);
    log::info!("Interface up: mac={}, ip={}", local_mac, local_ip);

    let start = StdInstant::now();

    loop {
        send_arp_request(&mut dev, local_mac, local_ip, target_ip, now_from(&start));
        thread::sleep(Duration::from_secs(5));
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?