← Blog

2026-04-26

An ESP32 as a network-attached USB stick

ESP32-iSCSI-USB started as a way to wire bizarre lab equipment into the LAN. Plug a $5 board into a benchtop instrument, and the instrument sees a USB mass-storage device whose contents live on iSCSI.

by Tom #esp32#iscsi#lab-automation

A lot of lab equipment was designed in the era when the answer to “how do I move data on or off this thing” was “with a USB stick.” A bench oscilloscope writes a screenshot to a USB stick. A protocol analyser reads its firmware update from a USB stick. A 3D printer reads its job from a USB stick. The instrument’s firmware was written in 2009 and won’t be getting an Ethernet port retrofit, ever.

What you’d actually like is for the same flow to work over your network. You want the instrument to see a USB stick, but the bytes behind the stick should live on the LAN where the rest of your tooling lives.

ESP32-iSCSI-USB is what I came up with. The headline:

+-------------+   USB-OTG    +----------+   WiFi   +----------+
|  Lab thing  |<------------>| ESP32-S3 |<-------->| iSCSI    |
| (sees mass  |              | bridge   |          | target   |
|  storage)   |              |          |          |          |
+-------------+              +----------+          +----------+

The ESP32-S3 has WiFi, USB-OTG, and ~512 KB of free SRAM in the configuration I’m using — enough to:

  • run an iSCSI initiator in software,
  • present the iSCSI LUN to the host as a USB mass-storage class device,
  • shim the SCSI READ/WRITE commands the host sends in one direction onto the iSCSI session in the other.

The instrument doesn’t know it’s not talking to a stick. From its perspective: USB enumerates, gets back a SCSI inquiry that says “yes, mass storage, here are the LBAs,” and proceeds to read or write blocks. Each of those blocks ends up on the remote target.

What “block bridge” actually means

USB Mass Storage class is a SCSI transport — exactly the same SCSI commands as iSCSI, just with a different framing. So the firmware doesn’t have to interpret what blocks mean (filesystem, partitions, bootloader); it just has to forward READ_10 and WRITE_10 across the two transports.

That keeps the firmware tiny. The ESP32 doesn’t run a filesystem; it runs a 30-line dispatcher and an iSCSI session state machine. WiFi handles the heavy lifting; the USB side is mostly DMA.

Throughput is constrained by USB 2.0 (~30 MB/s peak in practice for the ESP32-S3) and WiFi (variable, but usually the limiting factor). Plenty for “instrument writes a 5 MB screenshot to USB.” Not enough for “instrument streams 4K video off USB” — but nothing in the target audience does that anyway.

What I actually use it for

  • Bench oscilloscope screenshots. The scope writes screen_001.png to “USB”; the file appears in a directory on my desktop, in the iSCSI overlay. Combined with a dropbox-style sync I no longer need to walk over and pull the stick out.
  • Firmware updates for embedded boards. A board boots from “USB,” but USB is now a network-mounted image with the build I just shipped. Same loop as the Pi netboot story but for boards too small to carry an Ethernet stack of their own.
  • Lab-equipment configuration. A handful of older instruments expect their config to come from a stick at power-on. Now the config is a file on the LAN, version- controlled, and changing it doesn’t require physically carrying a thumb drive between machines.

Web flasher

The firmware ships with a browser-based flasher because the target audience here is people who have hardware on their desk and would rather not download a toolchain. Plug an ESP32-S3 into a laptop, open the flasher page in the browser, hit the button. Two minutes later the board is configured for your WiFi and pointed at your iSCSI target.

It’s a web-serial + esptool-js combo — works in any Chromium-family browser, no extension. The page also configures the WiFi credentials (stored in the ESP32’s NVS), the iSCSI target IQN, and the CHAP credentials if you’re using a non-anonymous target.

What’s not solved

  • Single-host. USB Mass Storage has weird semantics around “two hosts mounting the same block device” — namely, it breaks. The ESP32 only presents to one host at a time.
  • Hot-swap of the iSCSI session. If WiFi drops mid-write, the host sees the USB device as having “ejected” without warning. The instrument’s firmware copes with that the same way it copes with a real stick being yanked. Reconnect and power-cycle the USB cable.
  • Encryption. WiFi WPA2 on your side; iSCSI plain or TLS to the target. The ESP32-S3 has the headroom for TLS but the iSCSI-TLS handshake is heavier than anonymous TCP iSCSI; I haven’t yet decided whether to ship it as default for the bridge.

How scsipub fell out of this (and the Pi shim)

I had the bridge working against a local target on my desk within a long weekend. The harder problem was the same as for the Pi shim: writing “plug this into your scope and configure the WiFi” instructions when there was nowhere on the public internet to point the bridge at.

There are zero public iSCSI targets you can just connect to over the open internet. Every offering I could find was either inside someone’s rack (private) or behind a cloud-provider console with an account creation flow attached (also private, in the sense that you can’t show a stranger a one-line example).

scsipub started as the missing dependency — the open-internet target that the Pi shim and the ESP32 bridge could each have in their “try it now” instructions. It kept growing because running iSCSI on the public internet at all turns out to be its own pile of small decisions; that pile is the subject of a separate post.

See also

Source, schematics, and the web flasher all live at ESP32-iSCSI-USB. The companion PI4-iSCSI-shim project is the right answer if you’re netbooting Raspberry Pis instead of bridging USB to lab gear.