An iroh powered smart fan
❄️ Building a Smart Fan Powered by Iroh
By Rüdiger Klaehn | July 2, 2026
If you are currently residing in the Northern Hemisphere—specifically Europe—you are likely enduring a brutal heatwave. To combat the rising temperatures and bring some much-needed "chill" back into the room, let's build a project using iroh.
While ESP32s are often used for simple data echoing, their true value lies in being an affordable gateway to read sensors and trigger actuators. In this guide, we will develop a straightforward end-to-end system: an ESP32 that monitors temperature and manages a fan.
The Core Philosophy: Unlike the vast majority of IoT ecosystems, this project contains
no cloud componentszero cloud dependency. Instead, it utilizes a lightweight website accessible from any browser with WebAssembly (WASM) support, regardless of where you are in the world.
🛠️ Hardware Requirements
For this build, the primary choice is an ESP32-WROVER devkit equipped with 4 MiB of PSRAM. This ensures we have the memory overhead required for Iroh's full networking suite, including relay connections for global remote access.
Hardware Compatibility Matrix
| Device | PSRAM | Compatibility | Notes |
|---|---|---|---|
| ESP32-WROVER | Yes | ✅ Full | Recommended for all features. |
| M5StickC-Plus2 | Yes | ✅ Partial | Requires GPIO pin adjustments. |
| ESP32-S3 | Yes | ✅ Partial | Requires minor configuration tweaks. |
| Standard ESP32 | No | ⚠️ Limited | Must disable relays & tweak QUIC buffers. |
⚙️ Basic Setup
We begin by leveraging an existing echo example from the iroh-esp32-examples repository. For the desktop side, we use a standard, "vanilla" client.
To transform this into a smart fan project, we perform the following renames:
server-esp32-psramserver-smart-fanclientsmart-fan-cli
Important Note: Because the ESP32 variant may require a specific patch of Iroh and utilizes a different toolchain than the desktop client, these are maintained as two entirely separate Rust projects.
Setup Checklist
- Clone
iroh-esp32-examples - Rename server project to
server-smart-fan - Rename client project to
smart-fan-cli - Configure WiFi credentials
🚀 Initial Flashing
To get the system running, execute cargo run within the server project. This command automatically detects the ESP32 via USB and flashes the firmware.
Compilation & Performance
- First Build: This is slow because the compiler must build both Iroh and the operating system for the
xtensaarchitecture. - Subsequent Builds: Much faster, as results are cached in the
.embuilddirectory. - Flashing Speed: Limited by the chip's data rate. You can optimize this using the
ESPFLASH_BAUDenvironment variable.
To streamline the process, you can export your WiFi credentials so you don't have to type them repeatedly:
export WIFI_CONFIG=SSID:PASSWORD
Execution Command:
ESPFLASH_BAUD=230400 WIFI_CONFIG=myap:mypass cargo run --release
Terminal Output:
cargo run --release
Finished `release` profile [optimized] target(s) in 0.56s
Running `espflash flash --monitor target/xtensa-esp32-espidf/release/esp32-psram`
[2026-07-01T07:14:23Z INFO ] 🚀 A new version of espflash is available: v4.4.0
[2026-07-01T07:14:23Z INFO ] Serial port: '/dev/cu.usbserial-210'
[2026-07-01T07:14:30Z INFO ] Using flash stub
Chip type: esp32 (revision v3.1)
Flash size: 4MB
App/part. size: 3,953,296/4,128,768 bytes, 95.75%
[00:04:11] [========================================] 2295/2295 0x10000 Verifying...
As indicated by the logs, the flash usage is quite high:
However, adding pure Rust dependencies (like irpc) has a negligible impact on the binary size, so there is still some breathing room.
🔍 Testing the System
At this stage, the ESP32 is acting as a simple echo server. A critical requirement for IoT is a stable Endpoint ID. We don't want the ID to change every time the device reboots.
To achieve this, the device generates a secret key on the first boot and stores it in Non-Volatile Memory (NVM). Since flashing doesn't overwrite NVM, the device retains its identity unless the memory is explicitly wiped.
The Startup Sequence
Once the device boots, you will see the following logs:
I (7413) server_esp32_psram: Iroh endpoint bound
I (7413) server_esp32_psram: Listening on: 192.168.0.186:51831
I (7413) server_esp32_psram: Endpoint ID: 03b43add965a3eaa2d20d3b60dcb1aa2fa8fdd36cdc1544511af11c26f45fd4b
I (7423) server_esp32_psram: Short ticket: endpointaab3iow5sznd5krnedj3mdoldkrpvd65g3g4cvcfcgxrdqtpix6uwaa
I (7433) server_esp32_psram: Long ticket: endpointaab3iow5sznd5krnedj3mdoldkrpvd65g3g4cvcfcgxrdqtpix6uwaibadakqaf266kag
I (7443) server_esp32_psram: Router started, accepting connections
The device is assigned a local IP (e.g., 192.168.0.186:51831) via DHCP. While it provides both short and long tickets, it is initially only reachable locally via the long ticket.
Finally, the device uses QAD (Query Address Discovery) to determine its global position via various relay URLs:
W (7473) iroh::net_report: QADv4; relay_url=https://aps1-1.relay.n0.iroh.link./
W (7473) iroh::net_report: QADv4; relay_url=https://euc1-1.relay.n0.iroh.link./
W (7483) iroh::net_report: QADv4; relay_url=https://use1-1.relay.n0.iroh.link./
W (7493) iroh::net_report: QADv4; relay_url=https://usw1-1.relay.n0.iroh.link./
Assuming an active internet connection, the device is now ready to be controlled from anywhere.