Skip to content

Repository files navigation

ESP32 USB/IP Server

ESP-IDF License

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 | δΈ­ζ–‡

✨ Features

  • πŸ”„ 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
  • πŸ“Š Device Status Panel - Web UI and serial console list attached devices (busid, VID:PID, remote-client usage)

πŸ“‹ Requirements

Hardware

  • 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 size if needed.

Software

  • ESP-IDF v5.5
  • Linux machine with usbip tools installed (client side)

πŸš€ Quick Start

1. Clone the Repository

git clone --recursive https://github.com/yunsmall/usbipdcpp_esp32.git
cd usbipdcpp_esp32

2. Configure WiFi (Optional)

The 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):

idf.py menuconfig

Navigate 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.

3. Build and Flash

idf.py build flash monitor

For 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 monitor

If the default sdkconfig doesn't take effect, explicitly specify it with -DSDKCONFIG_DEFAULTS.

4. Connect from Linux Client

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>

πŸ–₯️ Management & Configuration

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.

Web UI (when online)

Open http://<ESP32_IP>/ in a browser (HTTP port 80):

  • USB devices card β€” attached devices with busid, VID:PID and usage state (idle / in use by a remote client). Auto-refreshes every 5 seconds.
  • WiFi card (collapsed by default) β€” current SSID/IP, change credentials (the device disconnects and reconnects to the new AP), plus a wiring hint for the fallback serial console. Empty password = open network.

REST API: GET /api/status (connection state + config-port GPIOs), GET /api/devices (device list), POST /api/wifi (form-urlencoded ssid=..&password=..).

Serial Console (when offline / misconfigured WiFi)

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; omit password for an open network)
wifi_show / wifi_reset Show current config / clear NVS back to compile-time defaults
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.

Local Web Preview (no flashing)

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…).

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    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 β”‚   β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”Œ Using usbipdcpp in Your Own Project

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/usbipdcpp

The 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, including usb_host_install (shown in the example above) and the USB PHY setup it performs.
  • usb_host_install is process-wide and must be called exactly once, before creating Esp32Server.
  • Esp32Server::start never throws; errors come back via the returned error_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.

πŸ“ Tested Devices

Device Type Status Notes
USB Keyboard βœ… Working
USB Mouse βœ… Working
USB Flash Drive (MSC) βœ… Working Bulk transfer tested
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.

⚑ Performance Optimization

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: TransferHandle automatically 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

πŸ“š Related Project

  • usbipdcpp - A cross-platform USB/IP protocol library. This project is an ESP32 implementation using usbipdcpp.

πŸ“’ Commercial Use

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

⚠️ Known Issues

Chunked Transfer β€” Currently Disabled

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 monitor

However, 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.

Hardware Recommendation

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.

πŸ“„ License

Apache License 2.0

About

A usbip server in esp32

Topics

Resources

Stars

45 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages