Web USB API Implementation on Website
WebUSB API opens access to USB devices directly from the browser without drivers or native applications. Label printers, POS terminals, industrial scanners, Arduino devices, microcontroller programmers — all connect to a web interface directly.
Limitations and Reality
Support: Chrome/Edge 61+ (desktop and Android). Safari and Firefox — not supported.
HTTPS only. User gesture only. USB devices with active OS USB drivers (HID, mass storage, system driver printers) — not available via WebUSB: browser doesn't intercept them from OS. Need special firmware with WebUSB descriptors or device without auto-installed drivers.
Request and Connect Device
interface USBDeviceInfo {
vendorId: number // from manufacturer documentation
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 has multiple configurations — select needed one
if (device.configuration === null) {
await device.selectConfiguration(1)
}
// Claim interface (number from documentation or USB descriptor)
await device.claimInterface(0)
console.log(`Connected: ${device.manufacturerName} ${device.productName}`)
console.log(`USB ${device.usbVersionMajor}.${device.usbVersionMinor}`)
}
Label Printer ZPL (Zebra/analogues)
Zebra printers use ZPL (Zebra Programming Language). Commands sent as plain text via bulk transfer:
class ZebraPrinter {
private device: USBDevice
private interfaceNumber = 0
private endpointOut = 1 // bulk OUT endpoint — from 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 layout for 60x40mm label
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')
// Read response from 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: Bidirectional Data Exchange
Arduino with CDC firmware acts as USB Serial. But without CDC OS driver — custom approach via Arduino WebUSB library:
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()! // Last element — incomplete line
for (const line of lines) {
if (line.trim()) onData(line.trim())
}
} catch (err) {
if ((err as Error).name === 'NetworkError') break // Device disconnected
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
}
}
// Usage
const arduino = new ArduinoDevice(device)
arduino.startReading((line) => {
// Parse sensor data: "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 Hook for 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 connected:', 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])
// Reconnect to previously authorized devices (without dialog)
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 }
}
Reading USB Descriptor
When documentation is missing — read device descriptors directly:
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}`)
})
})
})
}
This allows finding needed endpoint numbers without documentation.
What We Do
Get Vendor ID and Product ID, study device protocol documentation or reverse-engineer it. Implement connection, sending commands and receiving data, handling disconnection, connection status UI. Test on target OS (Windows requires special attention with WinUSB drivers).
Timeline: integration with documented USB device — 3–5 days. Protocol reverse-engineering — 8–14 days.







