Реализация Web USB API на сайте
WebUSB API открывает доступ к USB-устройствам прямо из браузера — без драйверов, без нативного приложения. Принтеры этикеток, POS-терминалы, промышленные сканеры, Arduino-устройства, программаторы микроконтроллеров — всё это подключается к веб-интерфейсу напрямую.
Ограничения и реальность
Поддержка: Chrome/Edge 61+ (десктоп и Android). Safari и Firefox — не поддерживают.
Только HTTPS. Только user gesture. USB-устройства с активными ядерными USB-драйверами ОС (HID, масс-стораж, принтеры с системными драйверами) — недоступны через WebUSB: браузер не перехватывает их у ОС. Нужен специальный firmware с WebUSB дескрипторами, или устройство без автоматически устанавливаемого драйвера.
Запрос и подключение устройства
interface USBDeviceInfo {
vendorId: number // из документации производителя
productId: number
}
async function requestUSBDevice(filters: USBDeviceInfo[]): Promise<USBDevice> {
const device = await navigator.usb.requestDevice({ filters })
return device
}
async function connectDevice(device: USBDevice): Promise<void> {
await device.open()
// Если устройство имеет несколько конфигураций — выбираем нужную
if (device.configuration === null) {
await device.selectConfiguration(1)
}
// Захватываем интерфейс (нужный номер — из документации или USB descriptor)
await device.claimInterface(0)
console.log(`Подключено: ${device.manufacturerName} ${device.productName}`)
console.log(`USB ${device.usbVersionMajor}.${device.usbVersionMinor}`)
}
Принтер этикеток ZPL (Zebra/аналоги)
Принтеры Zebra используют ZPL (Zebra Programming Language). Команды отправляются как plain text через bulk transfer:
class ZebraPrinter {
private device: USBDevice
private interfaceNumber = 0
private endpointOut = 1 // bulk OUT endpoint — из USB descriptor устройства
constructor(device: USBDevice) {
this.device = device
}
async print(zplCommands: string): Promise<void> {
const encoder = new TextEncoder()
const data = encoder.encode(zplCommands)
const result = await this.device.transferOut(this.endpointOut, data)
if (result.status !== 'ok') {
throw new Error(`Print error: ${result.status}`)
}
}
async printLabel(params: {
barcode: string
title: string
price: string
sku: string
}): Promise<void> {
// ZPL разметка этикетки 60x40mm
const zpl = `
^XA
^CI28
^FO20,10^A0N,24,24^FD${params.title}^FS
^FO20,40^BY2^BCN,50,Y,N,N^FD${params.barcode}^FS
^FO20,100^A0N,20,20^FDSKU: ${params.sku}^FS
^FO20,125^A0N,28,28^FD${params.price}^FS
^XZ
`.trim()
await this.print(zpl)
}
async getStatus(): Promise<string> {
// Host Status Command
await this.print('~HS')
// Читаем ответ с bulk IN endpoint
const result = await this.device.transferIn(1, 64) // endpoint IN, 64 bytes
const decoder = new TextDecoder()
return decoder.decode(result.data!)
}
}
Arduino: двусторонний обмен данными
Arduino с заливкой CDC firmware работает как USB Serial. Но без CDC-драйвера ОС — нужен кастомный подход через WebUSB библиотеку для Arduino:
class ArduinoDevice {
private device: USBDevice
private interfaceNumber = 2
private endpointIn = 5
private endpointOut = 4
private decoder = new TextDecoder()
private encoder = new TextEncoder()
private readBuffer = ''
private isReading = false
constructor(device: USBDevice) {
this.device = device
}
async startReading(onData: (line: string) => void) {
this.isReading = true
while (this.isReading) {
try {
const result = await this.device.transferIn(this.endpointIn, 64)
const chunk = this.decoder.decode(result.data!, { stream: true })
this.readBuffer += chunk
const lines = this.readBuffer.split('\n')
this.readBuffer = lines.pop()! // Последний элемент — неполная строка
for (const line of lines) {
if (line.trim()) onData(line.trim())
}
} catch (err) {
if ((err as Error).name === 'NetworkError') break // Устройство отключилось
throw err
}
}
}
async sendCommand(command: string): Promise<void> {
const data = this.encoder.encode(command + '\n')
await this.device.transferOut(this.endpointOut, data)
}
stopReading() {
this.isReading = false
}
}
// Использование
const arduino = new ArduinoDevice(device)
arduino.startReading((line) => {
// Парсим данные датчика: "TEMP:23.5,HUM:65.2"
const match = line.match(/TEMP:([\d.]+),HUM:([\d.]+)/)
if (match) {
setSensorData({ temp: parseFloat(match[1]), humidity: parseFloat(match[2]) })
}
})
await arduino.sendCommand('LED:ON')
await arduino.sendCommand('SERVO:90')
React-хук для WebUSB
function useWebUSB() {
const [device, setDevice] = useState<USBDevice | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [isSupported] = useState(() => 'usb' in navigator)
useEffect(() => {
if (!isSupported) return
function onConnect(event: USBConnectionEvent) {
console.log('USB подключено:', event.device.productName)
}
function onDisconnect(event: USBConnectionEvent) {
if (event.device === device) {
setDevice(null)
setIsConnected(false)
}
}
navigator.usb.addEventListener('connect', onConnect)
navigator.usb.addEventListener('disconnect', onDisconnect)
return () => {
navigator.usb.removeEventListener('connect', onConnect)
navigator.usb.removeEventListener('disconnect', onDisconnect)
}
}, [device, isSupported])
// Переподключение к ранее авторизованным устройствам (без диалога)
async function reconnectPaired() {
const devices = await navigator.usb.getDevices()
if (devices.length > 0) {
const dev = devices[0]
await connectDevice(dev)
setDevice(dev)
setIsConnected(true)
}
}
return { device, isConnected, isSupported, reconnectPaired }
}
Чтение USB-дескриптора
Когда документации нет — читаем дескрипторы устройства напрямую:
function inspectDevice(device: USBDevice) {
console.log('Vendor ID:', device.vendorId.toString(16))
console.log('Product ID:', device.productId.toString(16))
device.configuration?.interfaces.forEach((iface) => {
console.log(`\nInterface ${iface.interfaceNumber}:`)
iface.alternates.forEach((alt) => {
alt.endpoints.forEach((ep) => {
console.log(` Endpoint ${ep.endpointNumber}: ${ep.direction} ${ep.type}, packet size: ${ep.packetSize}`)
})
})
})
}
Это позволяет найти нужные endpoint-номера без документации.
Что делаем
Получаем Vendor ID и Product ID устройства, изучаем документацию на протокол или делаем реверс через USB-анализатор. Реализуем подключение, отправку команд и приём данных, обработку отключения, UI статуса. Тестируем на целевых ОС (Windows требует особого внимания с WinUSB-драйверами).
Срок: интеграция с задокументированным USB-устройством — 3–5 дней. Реверс-инжиниринг протокола — 8–14 дней.







