用 ESP32-S3 把舊 USB 印表機變成網路印表機
用 ESP32-S3 的 USB OTG 功能,將舊 USB 印表機橋接到 WiFi 網路,透過 RAW Port 9100 協定實現無線列印。電腦端只需新增標準 TCP/IP 印表機,完全不需要改變原有驅動程式設定。
你家有一台舊印表機,只有 USB 接口,每次要印東西都要拔線插線?這篇文章教你用一片 ESP32-S3 開發板,花不到幾百元,讓它變成網路印表機,電腦、筆電通通可以無線列印,再也不用走到桌邊插線。
原理說明
整個架構非常單純,ESP32-S3 扮演「橋接器」的角色:
電腦 / 筆電 → WiFi → ESP32-S3 → USB OTG → 印表機
電腦端透過網路把列印資料送到 ESP32-S3,ESP32-S3 再透過 USB Host 模式把資料轉發給印表機。對電腦來說,就像是在用一台普通的網路印表機,完全感覺不到中間有個小晶片在橋接。
為什麼用 RAW Port 9100
網路列印有幾種協定,這裡選擇最簡單的 RAW(Port 9100),原因有三:
- ESP32 不需要解析任何列印格式,直接透明轉發
- Windows、macOS、Linux 全部原生支援,不需要安裝額外軟體
- 列印資料的格式轉換(PCL、ESC/P 等)由電腦端的驅動程式負責,ESP32 只是管道
這個方案適合哪些印表機
只要印表機符合以下條件就能用:
- 有 USB 接頭
- 屬於標準 USB Printer Class(Class Code 0x07),插上後電腦需要安裝驅動的印表機幾乎都是
不適合的情況:
- 印表機本身已有 WiFi 功能(直接用原廠無線功能就好,不需要 ESP32)
- 使用完全私有 USB 協定的特殊工業印表機
材料準備
| 項目 | 說明 |
|---|---|
| ESP32-S3 開發板(雙 USB) | 需要有兩個 USB 接口的款式 |
| USB OTG 轉接線 | Type-A 母頭 → Type-C 公頭(或視板子接口而定) |
| USB 印表機 | 你現有的印表機 |

關於 ESP32-S3 開發板的選擇
一定要選雙 USB 款,原因如下:

USB-C 燒錄接頭 ← 連接電腦燒錄程式,也用來供電
USB OTG 接頭 ← 連接印表機(這個接頭切換成 Host 模式驅動印表機)
兩個 USB 接頭各司其職,缺一不可。常見的雙 USB S3 開發板如 ESP32-S3等都適合。
開發環境設定
尚未用過ESP32-s3的朋友,建議可以先看我們之前的教學哦!

關鍵的 Arduino IDE 設定
這步是最容易出錯的地方,USB Mode 設定錯誤會導致 USB Host 完全無法運作,請照表設定:
| 設定項目 | 正確值 | 說明 |
|---|---|---|
| Board | ESP32S3 Dev Module | |
| USB Mode | USB-OTG (TinyUSB) | 最關鍵,預設值是錯的 |
| USB CDC On Boot | Disabled | 避免與 OTG 模式衝突 |
| Partition Scheme | Default 4MB 或 Huge APP | |
| Upload Speed | 921600 | 加速燒錄 |
接線方式
充電器 / 電腦
↓
[ESP32-S3 燒錄接頭 USB-C] ← 供電用
印表機 USB 線
↓
[USB OTG 轉接頭(Type-A 母)]
↓
[ESP32-S3 OTG 接頭] ← GPIO 19 (D−) / GPIO 20 (D+)
實際使用時,ESP32-S3 接線供電,印表機透過 OTG 轉接頭插到 OTG 口,兩條線分開,互不干擾。
程式碼
修改 WiFi 設定
下載程式碼後,只需要修改最頂部的兩行:
const char* WIFI_SSID = "你的WiFi名稱";
const char* WIFI_PASSWORD = "你的WiFi密碼";
其他設定保持預設即可,或依需求調整:
const char* PRINTER_NAME = "ESP32-Printer"; // 出現在網路上的名稱,可自訂
const int PRINT_PORT = 9100; // RAW 列印 Port,建議不要更動
完整程式碼
/**
* ESP32-S3 USB OTG 網路列印機橋接器
* 傑森創工獨家開發
* www.jmaker.com.tw
*
* 架構:電腦 → WiFi → TCP Port 9100 → ESP32-S3 → USB → 列印機
*
* Arduino IDE 設定:
* Board: ESP32S3 Dev Module
* USB Mode: USB-OTG (TinyUSB) ← 最重要
* USB CDC On Boot: Disabled
* Partition Scheme: Default 4MB
*/
#include <WiFi.h>
#include <ESPmDNS.h>
#include "usb/usb_host.h"
#include "usb/usb_types_ch9.h"
// ============================================================
// 設定區:只需要改這裡
// ============================================================
const char* WIFI_SSID = "你的WiFi名稱";
const char* WIFI_PASSWORD = "你的WiFi密碼";
const char* PRINTER_NAME = "ESP32-Printer";
const int PRINT_PORT = 9100;
// ============================================================
WiFiServer printServer(PRINT_PORT);
static usb_host_client_handle_t clientHandle = NULL;
static usb_device_handle_t deviceHandle = NULL;
static uint8_t bulkOutEpAddr = 0;
static uint8_t bulkOutEpMps = 64;
static bool printerReady = false;
#define RECV_BUF_SIZE 4096
static uint8_t recvBuf[RECV_BUF_SIZE];
// USB Host 事件回呼:處理印表機插入與拔除
static void usbHostClientCallback(const usb_host_client_event_msg_t* eventMsg, void* arg) {
if (eventMsg->event == USB_HOST_CLIENT_EVENT_NEW_DEV) {
Serial.printf("[USB] 偵測到新裝置,位址: %d\n", eventMsg->new_dev.address);
esp_err_t err = usb_host_device_open(clientHandle, eventMsg->new_dev.address, &deviceHandle);
if (err != ESP_OK) {
Serial.printf("[USB] 開啟裝置失敗: %s\n", esp_err_to_name(err));
return;
}
// 印出裝置 VID/PID,方便識別印表機型號
const usb_device_desc_t* devDesc;
usb_host_get_device_descriptor(deviceHandle, &devDesc);
Serial.printf("[USB] VID: %04X PID: %04X\n", devDesc->idVendor, devDesc->idProduct);
// 掃描描述符,找 Bulk OUT 端點
const usb_config_desc_t* cfgDesc;
usb_host_get_active_config_descriptor(deviceHandle, &cfgDesc);
int offset = 0;
while (offset < cfgDesc->wTotalLength) {
const usb_standard_desc_t* desc =
(const usb_standard_desc_t*)((uint8_t*)cfgDesc + offset);
if (desc->bLength == 0) break;
if (desc->bDescriptorType == USB_B_DESCRIPTOR_TYPE_INTERFACE) {
const usb_intf_desc_t* id = (const usb_intf_desc_t*)desc;
Serial.printf("[USB] 介面 Class: 0x%02X SubClass: 0x%02X\n",
id->bInterfaceClass, id->bInterfaceSubClass);
// 0x07 = USB Printer Class,看到這個代表是標準列印機
}
if (desc->bDescriptorType == USB_B_DESCRIPTOR_TYPE_ENDPOINT) {
const usb_ep_desc_t* ep = (const usb_ep_desc_t*)desc;
bool isBulk = (ep->bmAttributes & 0x03) == USB_TRANSFER_TYPE_BULK;
bool isOut = (ep->bEndpointAddress & 0x80) == 0;
if (isBulk && isOut) {
bulkOutEpAddr = ep->bEndpointAddress;
bulkOutEpMps = ep->wMaxPacketSize;
Serial.printf("[USB] Bulk OUT: 0x%02X MPS: %d\n", bulkOutEpAddr, bulkOutEpMps);
}
}
offset += desc->bLength;
}
if (bulkOutEpAddr == 0) {
Serial.println("[USB] 找不到 Bulk OUT 端點,可能不是標準列印機");
return;
}
err = usb_host_interface_claim(clientHandle, deviceHandle, 0, 0);
if (err != ESP_OK) {
Serial.printf("[USB] 認領介面失敗: %s\n", esp_err_to_name(err));
return;
}
printerReady = true;
Serial.println("[USB] 列印機就緒!");
} else if (eventMsg->event == USB_HOST_CLIENT_EVENT_DEV_GONE) {
Serial.println("[USB] 裝置已移除");
printerReady = false;
bulkOutEpAddr = 0;
if (deviceHandle) {
usb_host_interface_release(clientHandle, deviceHandle, 0);
usb_host_device_close(clientHandle, deviceHandle);
deviceHandle = NULL;
}
}
}
// 透過 USB Bulk Transfer 把資料送給印表機
// 每次最多 512 bytes,送完等 10ms 讓印表機消化
bool sendToPrinter(const uint8_t* data, size_t len) {
if (!printerReady || deviceHandle == NULL || bulkOutEpAddr == 0) {
Serial.println("[USB] 列印機未就緒");
return false;
}
size_t sent = 0;
while (sent < len) {
size_t chunk = min((size_t)512, len - sent);
usb_transfer_t* t = NULL;
if (usb_host_transfer_alloc(chunk, 0, &t) != ESP_OK) {
Serial.println("[USB] 分配 transfer 失敗");
return false;
}
memcpy(t->data_buffer, data + sent, chunk);
t->num_bytes = chunk;
t->device_handle = deviceHandle;
t->bEndpointAddress = bulkOutEpAddr;
t->timeout_ms = 5000;
t->context = NULL;
t->callback = [](usb_transfer_t* x) {
if (x->status != USB_TRANSFER_STATUS_COMPLETED)
Serial.printf("[USB] Transfer 錯誤: %d\n", x->status);
usb_host_transfer_free(x);
};
if (usb_host_transfer_submit(t) != ESP_OK) {
Serial.println("[USB] 傳送失敗");
usb_host_transfer_free(t);
return false;
}
sent += chunk;
vTaskDelay(pdMS_TO_TICKS(10));
}
Serial.printf("[USB] 已送出 %d bytes\n", len);
return true;
}
// USB Host 背景任務,固定跑在 Core 0,不干擾 WiFi(Core 1)
void usbHostTask(void* arg) {
usb_host_config_t hcfg = {
.skip_phy_setup = false,
.intr_flags = ESP_INTR_FLAG_LEVEL1,
};
ESP_ERROR_CHECK(usb_host_install(&hcfg));
Serial.println("[USB] Host 已初始化");
usb_host_client_config_t ccfg = {
.is_synchronous = false,
.max_num_event_msg = 5,
.async = {
.client_event_callback = usbHostClientCallback,
.callback_arg = NULL,
}
};
ESP_ERROR_CHECK(usb_host_client_register(&ccfg, &clientHandle));
Serial.println("[USB] 等待裝置插入...");
while (true) {
usb_host_lib_handle_events(pdMS_TO_TICKS(10), NULL);
usb_host_client_handle_events(clientHandle, pdMS_TO_TICKS(10));
}
usb_host_client_deregister(clientHandle);
usb_host_uninstall();
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=== ESP32-S3 網路列印機橋接器 ===");
Serial.printf("[WiFi] 連接到 %s ...\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\n[WiFi] 已連線!IP: %s\n", WiFi.localIP().toString().c_str());
// mDNS 廣播,部分系統可自動發現印表機
if (MDNS.begin(PRINTER_NAME)) {
MDNS.addService("pdl-datastream", "tcp", PRINT_PORT);
Serial.printf("[mDNS] 廣播名稱: %s.local\n", PRINTER_NAME);
}
printServer.begin();
Serial.printf("[TCP] 列印 Server 啟動,Port: %d\n", PRINT_PORT);
Serial.println("-----------------------------------");
Serial.printf(" Windows: 標準TCP/IP → IP: %s Port: 9100\n",
WiFi.localIP().toString().c_str());
Serial.printf(" Mac: IP → HP Jetdirect-Socket → %s\n",
WiFi.localIP().toString().c_str());
Serial.println("-----------------------------------");
// USB Host 任務固定跑在 Core 0,優先權 5
xTaskCreatePinnedToCore(usbHostTask, "usb_host", 4096, NULL, 5, NULL, 0);
}
void loop() {
WiFiClient client = printServer.available();
if (!client) return;
Serial.printf("[TCP] 收到列印連線,來自: %s\n", client.remoteIP().toString().c_str());
if (!printerReady) {
Serial.println("[!] 列印機未就緒,拒絕連線");
client.stop();
return;
}
size_t totalBytes = 0;
bool transferOk = true;
while (client.connected()) {
int available = client.available();
if (available > 0) {
int n = client.read(recvBuf, min(available, RECV_BUF_SIZE));
if (n > 0) {
if (transferOk) {
if (!sendToPrinter(recvBuf, n)) {
transferOk = false;
Serial.println("[!] USB 傳輸失敗,後續資料略過");
}
}
totalBytes += n;
}
} else {
delay(5);
}
}
client.stop();
Serial.printf("[TCP] 完成,共 %d bytes,USB 傳輸: %s\n",
totalBytes, transferOk ? "成功" : "失敗");
}
燒錄步驟
- 用 USB 線連接 ESP32-S3 的燒錄接頭(不是 OTG 口)到電腦
- Arduino IDE 選好正確的 COM Port
- 確認上方 Arduino IDE 設定表的每個選項都設對
- 點擊上傳按鈕
- 上傳完成後,開啟 Serial Monitor,鮑率設
115200
正常啟動後,Serial Monitor 會看到:
=== ESP32-S3 網路列印機橋接器 ===
[WiFi] 連接到 MyWiFi ...
[WiFi] 已連線!IP: 192.168.1.137
[mDNS] 廣播名稱: ESP32-Printer.local
[TCP] 列印 Server 啟動,Port: 9100
-----------------------------------
Windows: 標準TCP/IP → IP: 192.168.1.137 Port: 9100
Mac: IP → HP Jetdirect-Socket → 192.168.1.137
-----------------------------------
[USB] Host 已初始化
[USB] 等待裝置插入...
接著把印表機 USB 線透過 OTG 轉接頭插到 OTG 口,應該看到:
[USB] 偵測到新裝置,位址: 1
[USB] VID: 04F9 PID: 0027
[USB] 介面 Class: 0x07 SubClass: 0x01 ← 0x07 代表標準列印機
[USB] Bulk OUT: 0x01 MPS: 64
[USB] 列印機就緒!
看到「列印機就緒!」就代表硬體這端完全正常,可以繼續設定電腦端。
電腦端設定
Windows
- 開啟「控制台」→「裝置和印表機」→「新增印表機」

- 選「使用IP位址或主機名稱新增印表機」

- 裝置類型 ,類型選「TCP/IP 裝置」,主機名稱填 ESP32-S3 的 IP(Serial Monitor 顯示的那組)

- 出現「需要其他連接埠資訊」畫面時,選「自訂」→ 點「設定值」

- 確認通訊協定是 原始,連接埠號碼是 9100

- 確認通訊協定是 Raw,連接埠號碼是 9100
- 下一步後選擇印表機驅動程式,找你的印表機型號安裝。注意 !建議要先安裝好驅動程式,這時才能找得到哦!



列印測試
設定好之後,直接從 Word、記事本或任何程式選擇這台印表機印一頁測試。同時看 Serial Monitor,正常的輸出長這樣:
[TCP] 收到列印連線,來自: 192.168.1.50
[USB] 已送出 512 bytes
[USB] 已送出 512 bytes
[USB] 已送出 256 bytes
[TCP] 完成,共 1280 bytes,USB 傳輸: 成功

常見問題排除
插上印表機後沒有任何 USB 訊息
最常見的原因是 USB Mode 設定錯誤。重新確認 Tools → USB Mode → 選「USB-OTG (TinyUSB)」,重新燒錄。
介面 Class 顯示的不是 0x07
代表這台印表機不是標準 USB Printer Class,使用私有協定,ESP32 無法直接驅動。可以試試看是否有其他介面,但成功機率較低。
電腦新增印表機找不到
手動輸入 IP 和 Port 9100。不要依賴自動搜尋,ESP32 的 mDNS 廣播不一定被 Windows 的印表機精靈識別。
印出來是亂碼
驅動程式選錯了。回到「裝置和印表機」,重新選擇正確的印表機型號和驅動。ESP32 只是透明橋接,格式全靠驅動,驅動對了就能正常印。
USB 傳輸失敗
Serial Monitor 顯示 Transfer 錯誤 或 傳送失敗,通常是 OTG 轉接頭接觸不良,或供電不足(印表機耗電較大時,建議 ESP32 用有足夠電流的充電頭供電,不要用電腦 USB 口)。
技術原理補充
USB Host 模式與雙核心分工
ESP32-S3 有兩個 CPU 核心,程式利用這個特性做了分工:
- Core 0:跑 USB Host 任務,專心處理 USB 通訊事件
- Core 1:跑 WiFi 和 TCP Server(Arduino 預設的核心)
兩件事分開跑,不會互相干擾,這也是為什麼 usbHostTask 用 xTaskCreatePinnedToCore 固定在 Core 0。
USB Bulk Transfer 的流量控制
程式每次最多送 512 bytes 給印表機,送完等 10ms。這個設計是為了讓印表機有時間消化資料,避免印表機內部 buffer 溢出導致資料遺失。如果你的印表機速度很快(高速雷射),可以把這個延遲調小或移除。
mDNS 廣播的作用
ESPmDNS 廣播 pdl-datastream 服務,這是 RAW 列印的標準 mDNS 服務類型。部分作業系統的列印服務會自動掃描這個服務,讓印表機自動出現在清單上,不需要手動輸入 IP。
結語
一片 ESP32-S3 加上一條 OTG 轉接線,幾百元的成本,讓束之高閣的舊印表機重新派上用場。如果你手邊剛好有支援標準 PCL 格式的舊印表機,這個方案的相容性會更好,電腦和 Android 手機都能輕鬆使用。
有問題歡迎在傑森創工 Facebook 粉絲團討論。