はじめに
前回記事ではArduinoを使ったセグメントチェッカーを作った。
今回はESP32 × Rustという構成で、ESPからPCに向かって ARPパケットを投げてみる。
VSCodeを使ったRustの環境構築から始めて、最後はWiresharkでパケット受信しているところまで確認する。
Rustのコードは最終的にはGithubに上げる。
まだこの記事の段階では検証中なので、この記事にはGithubへのURLは記載されていない。
次か、次の次の記事あたりではGithubにPushしたい。
完成品
ESP→W5500→PCの順にARP問い合わせフレームが流れる
PCはARPに応答する
7, 8行目でARPの問い合わせと応答が行われている。
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のキャプチャ
考察:なぜ「Rust」なのか?
今回の実装を通じて、改めてC言語とRustを比較してみました。
| 比較項目 | C言語 | Rust (smoltcp) |
|---|---|---|
| 安全性 | 配列の境界ミスで即クラッシュ | コンパイラがバッファミスを許さない |
| 可読性 | 0x08, 0x06...と16進数が並ぶ |
ArpOperation::Request と名前で書ける |
| 保守性 | メモリ管理に常に気を張る | 所有権システムが自動で守ってくれる |
特にネットワークパケットのような「サイズを間違えると通信が崩壊する」データを扱う場合、Rustの型システムは最強のガードレールになります。
今回のまとめと次への展望
- W5500をMACRAWモードで制御成功。
- smoltcpによるARPリクエストの生成・送信に成功。
- Wiresharkで物理パケットの疎通を確認。
ハードウェアの潔白は証明されました。次はいよいよ、このW5500を 「2枚」 同時に使い、「自分が出したパケットを自分で拾う」 というループ検知の核心部分へ挑みます。
インジケータも赤(ループ検知)と緑(健全)の2色を用意して、現場で使えるプロ仕様のツールに仕上げていくぜ!
展望図解
W5500を2つ使ってループ検知装置を作りたい。概要はこちら
コンデンサの挿入位置やVINの線がW5500にのびてるところが変だけど、ざっくりこんな感じのイメージ
もっとビジュアルよく、かつ意味不明な配線
糸冬了!!
参考
付録:Rustコード
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));
}
}



