ESP32-WIZnet Chip-DevKit Camera Streaming
One ESP32-S3 streams the same camera over Ethernet (hardware TCP/IP) and Wi-Fi at once. Pull the cable and the surviving link gets 3x faster. Here is why.
One ESP32-S3. One camera. Two networks at the same time - Ethernet throughthe chip's own TCP/IP, Wi-Fi through the MCU. Both serve the same live video to a browser, each keeping its own frame count. Pull either cable and the other keeps streaming. Nobody presses anything.
The failover is not a feature you configure. It is what happens when the two paths share nothing but the sensor.
Use Cases
- Production-line machine vision — Use wired Ethernet for the PLC network, with Wi-Fi as an automatic backup path.
- Unattended remote sites — Use Ethernet normally at pump stations, substations, or rooftop equipment, while Wi-Fi keeps access available if the cable is unplugged or damaged.
- Lab and test benches — Connect the logging PC through the lab Ethernet switch while engineers nearby access the same camera from a phone over Wi-Fi.
One camera. Two networks. Pull either cable and it keeps streaming.
Wi-Fi cameras die when someone parks a forklift in front of the AP.
Ethernet cameras die when someone unplugs the wrong port in a cabinet.
So I built one that runs both at the same time — on one ESP32-S3, from one sensor — and then yanked the cable to see what happened.
ESP32-S3, OV3660 on the ribbon, W5500 module on SPI. Blue cable is Ethernet.
What it does
Two HTTP servers. Same camera. Different network stacks.
- Port 80 → Ethernet, through the W5500's hardware TCP/IP. The MCU runs no TCP code at all.
- Port 81 → Wi-Fi, through lwIP on the MCU. The normal ESP-IDF way.
Open both in two browser windows. You are watching one camera through two completely different stacks, side by side, each with its own frame counter.
Nothing is mirrored. Nothing is proxied. Both servers grab their own frames.
Both links up. Ethernet 11.4 fps on the left, Wi-Fi 7.6 fps on the right. Same sensor, same 640x480.
Then I pulled the Ethernet cable
Five seconds later. Left: dead, frozen, amber. Right: still live — and 3x faster.
| Wi-Fi side | Both up | Ethernet unplugged |
|---|---|---|
| Frame rate | 7.6 fps | 23.7 fps |
| Bandwidth | 0.7 Mbps | 2.3 Mbps |
| JPEG size | 12 KB | 12 KB |
3x faster, from unplugging the other cable.
The JPEG size is in that table for a reason. Same 12 KB before and after — so this is not the picture getting simpler. Something was in the way, and it left.
Why the survivor speeds up
One sensor. Two servers. They take turns.
The frame buffer belongs to the camera driver, so a server has to hold it for the entire send. While one holds it, the other waits.
With both links up:
- Ethernet: 5 ms grabbing the frame + 66 ms sending it = holds the camera 71 ms
- Wi-Fi: only gets the leftovers → 7.6 fps, even though its own work takes just 34 ms
Cable out:
- Wi-Fi has the camera to itself: 13 ms + 25 ms = 38 ms per frame → 23.7 fps
The math closes. Nothing else changed.
The bottleneck was never the network. It was the camera they were sharing.
That is not the answer I expected when I started.
"So Wi-Fi beat the hardware stack?"
Someone is going to read 11.4 vs 23.7 and say exactly that. Fair. Here is the honest answer.
Look at the send times: Ethernet 66 ms, Wi-Fi 25 ms.
That gap is SPI. Every JPEG byte crawls to the W5500 over a 33 MHz bus. It is not the TCP stack. Widen the bus and the number moves.
And speed was never what a hardware stack sells you. It sells you CPU.
The Ethernet path runs no retransmit timers, holds no socket buffers, wakes on no ACKs. It hands over a pointer and goes back to the camera.
You will not see that in a frame counter. You see it in what else the chip still has room to do.
5 things that broke
1. Pulling the cable killed the board for 27 seconds
First cable pull: 6 watchdog timeouts, 27 seconds frozen.
The driver's send waits for space in the chip's TX buffer by hammering a register in a loop. No yield. No timeout.
Cable out → that space never comes → the task never lets anything else run.
Fixed in the driver: obey SO_SNDTIMEO, watch the socket state too, and yield 1 ms per pass.
2. Fixing that turned the Wi-Fi picture black
Board stopped hanging. New symptom: the Wi-Fi view went black for ~10 seconds.
Same shared camera. Ethernet was holding it while its socket retried, so Wi-Fi had nothing to send.
Fix: a 2-second send timeout on the stream socket. Which only worked after the driver fix above — before that, SO_SNDTIMEO was stored and never read.
3. Plugging the cable back in did nothing
Link up. Server fine. Browser still frozen.
Browsers do not retry a broken MJPEG stream. And the page only set img.src when you pressed START.
Fix: the page watches the frame counter. Server says "streaming" but the counter has not moved for 3 seconds? Nothing is arriving — ask for the stream again.
Cable still out, it fails quietly and retries. Cable back in, the picture returns by itself.
4. The page lied about being connected
My favourite bug, because everything looked fine.
That stall detector? It lives inside the status handler. Which only runs when the status request succeeds.
Pull the cable and the request itself fails. So:
- Handler never runs
- Counter never advances
- Dot stays green
- Frame rate sits frozen at its last value, looking perfectly healthy
The page had no idea "unreachable" was even a thing.
Now it does. Two failed polls → UNREACHABLE, amber dot, and a line saying the numbers below are the last ones the device reported.
It keeps showing those numbers on purpose. They were true. Blanking them would throw away the comparison this whole page exists for.
How did I find it? I tried to screenshot the failover and the screenshot showed both sides healthy. The screenshot was right. The page was wrong.
5. accept() that is not accept()
Not a bug. But it catches everyone using a hardware TCP/IP chip.
I (1363) cam_server: [eth] server on port 80, 3 listeners
I (2872) cam_server: [wifi] server on port 81, 1 listenerThree listeners on Ethernet. One on Wi-Fi. Same job.
On lwIP, accept() hands you a new socket and the listener keeps listening. One listener, unlimited clients.
On the hardware stack, the listening socket becomes the connection. While a stream is open, nobody is listening on that port anymore.
The page holds /stream open forever while polling status every second. That is 2 live connections minimum, or the charts freeze.
So: N clients = N listening sockets. Miss this and your server looks perfect until the second request shows up.
Boot to both servers: 2.87 seconds
I (214) esp_psram: Found 8MB PSRAM device
I (658) camera: Detected OV3660 camera
I (967) cam_hal: Allocating 61440 Byte frame buffer in PSRAM
I (1037) cam: sensor PID 0x3660 up at 640x480, quality 12, xclk 20 MHz
I (1193) wsm_driver_spi: W5500 version check OK: 0x04
I (1196) wiztoe_net: TOE up: 192.168.11.2 (WIZnet hardware TCP/IP)
I (1363) cam_server: [eth] camera server on port 80, 3 listeners
I (2844) esp_netif_handlers: sta ip: 192.168.11.8
I (2872) cam_server: [wifi] camera server on port 81, 1 listenerRead the timestamps, not the words:
- Ethernet has an IP at 1.196 s
- Wi-Fi finishes DHCP at 2.844 s
The wire is ready 1.6 seconds earlier. No association, no DHCP round trip, no beacon to wait for. Cable in, link up.
And that is on a good day with a strong AP.
Every sensor knob, in the browser
The control panel is not written into the page. The firmware ships a list of 23 controls — names, ranges, groups — and the page builds itself from it.
Add a control in C, it shows up in the browser. No HTML edit.
Image, exposure, correction, orientation — all generated from what the device reported.
Including the sensor's built-in test pattern, which answers "is the network broken or the camera?" in about two seconds:
Colour bars made inside the OV3660. Clean bars = everything after the sensor is fine.
The sensor can also say no. Ask for a resolution it cannot do and the firmware rolls back and replies 409 with what it actually has — so the slider snaps back to the truth instead of pretending.
Hardware
- ESP32-S3 with 8 MB PSRAM — frame buffers live there. No PSRAM, no camera.
- OV3660 camera
- W5500 on SPI at 33 MHz
Code is examples/camera_stream in the WSM driver repo. Pin maps, idf.py commands, menuconfig options and the Python test tools are all in that example's README — better there than here, where they would rot.
One trick worth stealing: the whole example includes lwIP in exactly one file. Everything else talks to a tiny socket vtable. Both servers start with the same function call and a different vtable. That is the entire reason one codebase serves two stacks.
What is next
- 66 ms of SPI is the Ethernet ceiling. A faster bus moves it directly.
- The shared camera is the real ceiling. A frame buffer per interface would let both run flat out instead of taking turns.
But the failover works, and nobody has to press anything. That was the point.
Was this what you needed?
Tell us how this would be used on your site, or what is missing. A repository issue or a comment below both work. What you tell us about your deployment is what sets the next priority.
- Repository: https://github.com/theoim/wsm_driver/tree/ionic-example/examples/camera_stream
- Board product pages: (link)
wsm_driver: https://github.com/Wiznet/wsm_driver


