An USB/IP server implementation for ESP32-S3 and ESP32-P4 based on usbipdcpp library. This project serves as a practical example of using usbipdcpp on ESP32 platforms.
Note: USB device compatibility depends on the ESP32 chip's USB PHY speed support:
- ESP32-S3: Supports Full Speed (12Mbps) and Low Speed (1.5Mbps) devices
- ESP32-P4: Supports High Speed (480Mbps), Full Speed (12Mbps) and Low Speed (1.5Mbps) devices
High Speed devices (480Mbps) may not work correctly on ESP32-S3 due to PHY limitations.
English | δΈζ
- π Transparent USB Forwarding - Export local USB devices to remote machines via USB/IP protocol
- π Hot-plug Support - Automatic device detection, enumeration, and cleanup on removal
- π Multi-device Support - USB hubs supported, multiple devices can be exported simultaneously
- β‘ Zero-Copy Performance - Direct DMA buffer access eliminates data copying overhead, achieving optimal throughput
- π‘οΈ Robust Connection Handling - Automatic cleanup when devices are unplugged during active sessions
- π₯οΈ Runtime WiFi Configuration - Change WiFi over a web UI or a dedicated serial console; credentials persist in NVS across reboots β no recompilation needed
- π‘ Provisioning AP - When it cannot reach WiFi, the device raises its own hotspot; connect a phone/laptop, open
http://192.168.4.1/and configure β no serial adapter needed - π Device Status Panel - Web UI and serial console list attached devices (busid, VID:PID, remote-client usage)
- ESP32-S3 or ESP32-P4 development board (USB OTG supported)
- USB devices (keyboards, mice, mass storage, etc.)
- USB hub (optional, for multiple devices)
One Board, Two Roles: Flash this firmware onto your ESP32-S3 dev board and it becomes a USB/IP dongle; flash something else and it's back to being a regular dev board. No hardware modifications needed β just swap the firmware whenever you switch projects.
USB OTG Power: Common ESP32-S3 DevKitC boards and their compatible counterparts have a USB OTG power solder pad on the back. Bridging this pad allows the board to supply power to connected USB devices. If you prefer not to modify the hardware, you can use a self-powered USB hub (one with an external power supply) instead β the hub will power the devices while the ESP32 handles data.
Speed Compatibility: Ensure your ESP32 chip's USB PHY supports the USB speed type of your device. For example, High Speed UVC webcams require ESP32-P4.
Flash Size: Default configuration assumes ESP32-S3 (8MB flash) and ESP32-P4 (32MB flash). Modify via
idf.py menuconfigβSerial flasher configβFlash sizeif needed.
- ESP-IDF v5.5
- Linux machine with
usbiptools installed (client side)
Pre-built binary (no toolchain needed): every push to main rebuilds the firmware and publishes it on the latest release page β the link never changes, there are no version numbers, and the release is updated in place instead of piling up old builds. Download the file(s) for your chip and flash:
# Fresh board β merged image (bootloader + partition table + app), write at 0x0:
esptool.py --chip esp32s3 -p <PORT> write_flash 0x0 usbipdcpp_esp32s3_merged.bin
# ESP32-P4:
esptool.py --chip esp32p4 -p <PORT> write_flash 0x0 usbipdcpp_esp32p4_merged.bin
# Upgrade only the application β keeps stored WiFi credentials:
esptool.py --chip esp32s3 -p <PORT> write_flash 0x10000 usbipdcpp_esp32s3_app.bin
esp32s3assumes 8MB flash andesp32p4assumes 32MB flash; for other flash sizes build from source. Writing from0x0erases the NVS partition β configure WiFi after boot (Β§2). The release also ships the separatebootloader/partition-table/appfiles if you prefer to write them individually: bootloader at0x0(0x2000on the ESP32-P4 β the ROM bootloader looks there), partition table at0x8000, app at0x10000.
Build from source:
git clone --recursive https://github.com/yunsmall/usbipdcpp_esp32.git
cd usbipdcpp_esp32The checked-in per-target configs (sdkconfig.defaults.esp32s3 / esp32p4) intentionally contain no WiFi credentials β a fresh build boots without network. Provide credentials one of two ways:
Option A β compile-time default (connects on first boot, build-from-source only):
idf.py menuconfigNavigate to Usbipdcpp WiFi Configuration and set Usbipd WiFi SSID / Usbipd WiFi Password.
Option B β configure at runtime after flashing via the web UI or the dedicated serial console (no recompile needed, credentials persist in NVS across reboots). See the Management & Configuration section below.
idf.py build flash monitorFor specific chip targets, use the appropriate sdkconfig defaults:
# For ESP32-S3
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults.esp32s3" build flash monitor
# For ESP32-P4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults.esp32p4" build flash monitorIf the default sdkconfig doesn't take effect, explicitly specify it with
-DSDKCONFIG_DEFAULTS.
On your Linux machine:
# Load USB/IP kernel modules
sudo modprobe vhci-hcd
# List available devices
sudo usbip list -r <ESP32_IP>
# Attach to a device
sudo usbip attach -r <ESP32_IP> -b <BUSID>WiFi and device status are manageable at runtime. WiFi credentials are stored in NVS (namespace wifi) and applied automatically on every boot β recompiling to switch networks is never required.
Open http://<ESP32_IP>/ in a browser (HTTP port 80):
The page opens with a status bar (connection state, SSID, IP, whether the provisioning hotspot is running). On narrow screens three tabs split the content β no scrolling through a long page; from 880px wide the tabs give way to a two-column layout with everything on screen at once:
- USB devices β attached devices with
busid,VID:PIDand usage state (idle / in use by a remote client). Auto-refreshes every 5 seconds. - WiFi β work mode switch (STA connects to WiFi / AP provisioning hotspot only; takes effect on click); change credentials (saved only once actually online, a failure keeps the previous config); the serial-console rescue wiring hint is folded away. Empty password = open network.
- Provisioning AP β hotspot name/password (stored in NVS; the name field is pre-filled with the current value and the password field notes whether one is set β the password itself never reaches the browser); the device raises this hotspot when it cannot reach WiFi, connect to it and open
http://192.168.4.1/to configure.
While the provisioning hotspot is running, a banner appears at the top; on narrow screens the page also switches to the WiFi tab automatically.
The UI comes in Chinese and English: it follows the browser language on first open, and the button at the right end of the status bar switches it (the choice is remembered in the browser).
REST API: GET /api/status (connection state + config-port GPIOs + hotspot state and work mode), GET /api/devices (device list), POST /api/wifi (form-urlencoded ssid=..&password=..; waits for the connection result β {"ok":true,"ip":..} and saved on success, {"ok":false,"code":..} and not saved on failure), POST /api/ap (same fields, sets the provisioning hotspot name/password), POST /api/mode (mode=sta|ap switches the work mode immediately). Errors always come back as a machine-readable code (the human-readable text is produced by the web UI in the visitor's language β see the err_* entries in main/web/index.html).
In the default STA work mode, if the device fails to reach WiFi for 30 seconds straight after boot (wrong password, router gone, moved elsewhere), it raises its own provisioning hotspot. The STA side keeps retrying in the background β the hotspot is temporary and shuts down automatically once WiFi connects, returning to plain STA.
Connect a phone/laptop to that hotspot (default usbipd-setup / 12345678; change the defaults via Kconfig USBIPD_AP_SSID / USBIPD_AP_PASSWORD), open http://192.168.4.1/ and you get the same management page: a banner at the top says you are in provisioning mode, fill in the WiFi name/password and save. Once connected, the hotspot disappears β use the new IP from then on.
Hotspot name and password can be changed at any time from the Provisioning AP card in the web UI, or ap_set over serial (see table below). Both only store the config, effective the next time the hotspot starts (the running hotspot is left alone β no kicking a client that is halfway through provisioning). Empty password = open hotspot; otherwise at least 8 characters.
Work mode (stored in NVS; switch from the web UI's WiFi tab or wifi_mode over serial β effective immediately, kept across reboots):
| Mode | Behaviour |
|---|---|
sta (default) |
Connects to WiFi; after 30 seconds without success it raises the provisioning hotspot and keeps retrying in the background (if the old AP comes back it reconnects and drops the hotspot) |
ap |
Provisioning hotspot only: no WiFi connection, no retries (for places with no target network β avoids endless scanning and log spam) |
After you provision WiFi successfully while in ap mode, the mode falls back to sta automatically β otherwise the next reboot would go back to hotspot-only and your freshly configured network would be ignored.
If the network is unreachable, configure through the dedicated config UART β the pins differ per chip (Kconfig: USBIPD_CFG_UART_TX_GPIO / USBIPD_CFG_UART_RX_GPIO):
| Chip | Config UART TX | Config UART RX |
|---|---|---|
| ESP32-S3 | GPIO17 | GPIO18 |
| ESP32-P4 | GPIO4 | GPIO5 |
Wire a USB-UART adapter crossed: adapter RX β device TX, adapter TX β device RX, GND common. Open the terminal at 115200 8N1 and use the built-in commands (help lists all):
| Command | Purpose |
|---|---|
wifi_set <ssid> [password] |
Set WiFi and reconnect; saved to NVS only once the AP is actually reached, a failure keeps the previous config. Omit password for an open network |
wifi_show / wifi_reset |
Show current config (incl. provisioning hotspot and work mode) / clear NVS back to compile-time defaults |
ap_set <ssid> [password] |
Set the provisioning hotspot name/password (stored in NVS, effective next time the hotspot starts). Omit password for an open hotspot |
wifi_mode [sta|ap] |
Show/set the work mode (stored in NVS): sta = connect to WiFi, ap = provisioning hotspot only |
devices |
List attached USB devices (busid / VID:PID / usage state) |
mem |
Print heap usage |
logs |
Mirror the main UART0 log stream to the config port (Ctrl-C to stop) |
about |
What this firmware is and how to manage it |
The mirrored log stream is written with \r\n line endings so it renders correctly on real serial terminals.
wifi_setwaits for the actual connection result, up to 15 seconds: the config is saved to NVS only once the AP is reached; a timeout, a wrong password or a missing AP saves nothing and keeps the previous config.The
<ssid>/[password]in the command list is placeholder notation β do not type the angle brackets or square brackets. To connect toMyAPwith password12345678, runwifi_set MyAP 12345678(quote the password if it contains spaces).
The web UI is a single static file (main/web/index.html) embedded at compile time. To iterate on layout/scripts without flashing the firmware: run python main/web/mock_server.py and open http://127.0.0.1:8000. The mock serves fake /api/status and /api/devices responses; edit the top of the script to preview different states (disconnected, disabled config port, long SSID, provisioning mode via ap_active=Trueβ¦).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Linux Client β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β App A β β App B β β App C β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β β β
β βββββββββ΄ββββββββ β
β β usbip-vhci β β
β βββββββββ¬ββββββββ β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β TCP/IP Network
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β βββββββββ΄ββββββββ β
β β ESP32-S3 β β
β β USB/IP Server β β
β βββββββββ¬ββββββββ β
β β β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β β β β β
β ββββββ΄βββββ βββββββ΄ββββββ βββββββ΄ββββββ β
β β USB Hub β β USB Dev 1 β ... β USB Dev N β β
β βββββββββββ βββββββββββββ βββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The USB/IP server core lives in the components/usbipdcpp component (usbipdcpp submodule + the esp32_handler glue layer on top of the IDF USB-host stack). It has no dependency on main/ β everything under main/ is just this firmware's interactive shell (WiFi management, serial console, web UI, status panel). Strip main/ to just the forwarding engine.
Bring the component into your project:
cp -r components/usbipdcpp <your_project>/components/
# the component embeds the usbipdcpp git submodule β init it inside your repo:
git submodule add https://github.com/yunsmall/usbipdcpp <your_project>/components/usbipdcpp/usbipdcppThe component's idf_component_register declares its own requirements (asio spdlog usb pthread lwip sock_utils), so your main only needs PRIV_REQUIRES usbipdcpp asio spdlog. USB devices are accessed through the standard IDF usb component (enable USB_HOST_HUBS_SUPPORTED if you use hubs).
Minimal working integration β no WiFi/console/UI; your application provides the network stack, the component only listens on TCP:
#include <thread>
#include <freertos/FreeRTOS.h>
#include <usb/usb_host.h>
#include "esp32_handler/Esp32Server.h"
// usb_host library-level event loop: needs its own task for the whole lifetime
static void usb_host_event_loop() {
while (true) {
uint32_t event_flags;
ESP_ERROR_CHECK(usb_host_lib_handle_events(portMAX_DELAY, &event_flags));
}
}
extern "C" void app_main() {
// run the main flow in a std::thread: spdlog/asio need a full pthread context
// (see the comments above thread_main in main/esp32_usbipdcpp.cpp)
std::thread main_thread([&] {
const usb_host_config_t host_cfg = {
.skip_phy_setup = false,
.intr_flags = ESP_INTR_FLAG_LEVEL3,
.enum_filter_cb = nullptr,
};
ESP_ERROR_CHECK(usb_host_install(&host_cfg));
std::thread(usb_host_event_loop).detach();
usbipdcpp::Esp32Server server;
server.init_client(); // registers the usb_host client (device hot-plug)
asio::ip::tcp::endpoint ep{asio::ip::tcp::v4(), 3240};
auto ec = server.start(ep); // starts listening + spawns internal threads
if (ec) { /* handle listen failure */ }
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
main_thread.join();
}Esp32Server::start() spawns the network and client-event threads internally; plugged-in USB devices are enumerated and exported automatically β from a computer run usbip list -r <esp32-ip> then usbip attach -r <esp32-ip> -b <busid>.
Notes:
- Networking is your job β the component does not initialise WiFi/Ethernet or any PHY; write your own network bring-up (STA connection, Ethernet, static/DHCP, whatever your project needs) and only then start the server on it.
- Scope β usbipdcpp covers exactly three things: registering the usb_host client (
Esp32Server::init_client), device hot-plug handling/binding, and the USB/IP protocol sessions over TCP. Everything else stays with your application, includingusb_host_install(shown in the example above) and the USB PHY setup it performs. usb_host_installis process-wide and must be called exactly once, before creatingEsp32Server.Esp32Server::startnever throws; errors come back via the returnederror_code. Device bind failures are logged and rolled back internally.- The
main/files are the reference usage βesp32_usbipdcpp.cpp(thread_main) shows the full sequence, with WiFi management / serial console / web UI as optional extras around the same core.
| Device Type | Status | Notes |
|---|---|---|
| USB Keyboard | β Working | |
| USB Mouse | β Working | |
| USB Flash Drive (MSC) | β Working | Bulk transfer tested; requires ESP32-P4 β most flash drives are High Speed, which the S3 PHY does not support |
| USB Audio | π Testing | |
| USB Webcam (UVC) | π Testing | Requires ESP32-P4 for High Speed |
Bulk and interrupt transfers have been verified to work correctly. Ensure your ESP32 chip's USB PHY supports the target device's speed type.
This implementation leverages usbipdcpp's zero-copy architecture for maximum throughput:
- Direct DMA Buffer Access: USB transfer buffers are allocated in DMA-capable memory and accessed directly for network I/O, eliminating intermediate data copies
- RAII Transfer Management:
TransferHandleautomatically manages buffer lifecycle, ensuring proper cleanup without manual memory management - ESP32-Specific Optimizations:
- Bulk/Interrupt IN transfers aligned to endpoint Max Packet Size for hardware efficiency
- Control transfer buffers pre-allocated with setup packet space
- Object pooling for callback structures reduces allocation overhead
- usbipdcpp - A cross-platform USB/IP protocol library. This project is an ESP32 implementation using usbipdcpp.
If you use this project in a product, please display the following information prominently in your product documentation or about page:
- Project URL:
https://github.com/yunsmall/usbipdcpp_esp32 - Author:
yunsmall(GitHub) - Contact:
yun_small@163.com
Large USB transfers (e.g. 65536 bytes for firmware flashing) require a single DMA-capable buffer of the same size. When DMA memory is fragmented, a large contiguous allocation can fail even though the total free space is sufficient.
Chunked transfer addresses this by splitting the large allocation into multiple smaller blocks (default: 16384 bytes each), trading one large contiguous allocation for several smaller ones β dramatically increasing the probability of successful allocation.
The chunked transfer implementation has been moved to the feature/chunked-transfer branch. To try it out, switch to that branch and build:
git checkout feature/chunked-transfer
idf.py build flash monitorHowever, chunking is disabled by default (enable_chunking = false in Esp32DeviceHandler.cpp) due to a persistent timeout issue: when enabled, some bulk transfer scenarios (e.g. remote JLINK firmware flashing) cause the first chunk to NAK indefinitely while waiting for device data. The host times out after ~1 second, sends CMD_UNLINK, and the cycle repeats. The root cause has not been fully identified.
If you know how to fix this, pull requests are welcome.
ESP32-P4 is strongly recommended over ESP32-S3 for USB/IP use. ESP32-S3's internal DMA-capable memory is very limited (~300KB), which can cause large USB transfer buffer allocations to fail under load. ESP32-P4 supports DMA access to PSRAM, which removes the need for chunking in most cases.
Apache License 2.0