The label printer won't print, Arduino doesn't respond, industrial sensor is silent — all due to the lack of browser-to-COM port connection. Web Serial API solves this: the browser gains direct access to the serial port without installing drivers or native applications. We provide complete COM port integration with web applications designed for industrial equipment web control, barcode scanners, and Arduino web projects. We have been implementing this solution for over 5 years (since 2019) and completed more than 50 projects for industrial sensors, medical devices, and POS terminals. License savings reach 50%, maintenance costs are reduced by 40%. Integration costs start from $800.
Why Use Web Serial API over Native Apps?
It simplifies deployment 10x: no drivers to install, no app updates, no environment configuration. The user simply opens a page in a Chromium browser and works. Comparison with traditional approaches:
| Criteria | Web Serial API | Java Applet | Electron App |
|---|---|---|---|
| Installation | Not required | JRE required | Installation required |
| Update | Automatic (web) | Manual | Manual |
| Security | Same-origin, HTTPS | Questionable | Depends on build |
| Device support | USB/Bluetooth/COM | Only COM | Via Native Messaging |
The API is a modern, secure solution. We implement it turnkey in 2–4 days.
Browser Support and Limitations
The API is supported in Chrome 89+ (since March 2021), Edge 89+, Opera 75+. Firefox and Safari do not support it. Therefore, the page must either require a Chromium browser or provide a Serial API fallback.
Support check:
if (!('serial' in navigator)) {
throw new Error('Web Serial API is not supported. Use Chrome 89+')
}
Origin permission: In production, you must add the header Permissions-Policy: serial=*.
Ensuring Operation in Unsupported Browsers
If your audience uses Firefox or Safari, provide a fallback. We offer three approaches:
| Fallback Method | Description | Complexity |
|---|---|---|
| Manual Input | User enters data manually via a form | Minimal |
| File Upload | Data exported from the device to a file, then uploaded | Low |
| WebSocket Agent | A local agent on the device transmits data via WebSocket | Medium |
Choice depends on data type and automation requirements. For example, for a barcode scanner, file upload is sufficient; for interactive Arduino control, the web app will require a WebSocket agent.
Service Architecture: Isolating Port Work
We isolate all port work in a SerialService class. The UI component doesn't know about streams and buffers — it calls the service methods and receives data via callbacks or EventEmitter.
type SerialDataHandler = (data: Uint8Array) => void
type SerialErrorHandler = (error: Error) => void
interface SerialConfig {
baudRate: number // baud rate configuration
dataBits?: 7 | 8
stopBits?: 1 | 2
parity?: 'none' | 'even' | 'odd'
bufferSize?: number
flowControl?: 'none' | 'hardware'
}
class SerialService extends EventTarget {
private port: SerialPort | null = null
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null
private readLoopActive = false
async requestPort(filters: SerialPortFilter[] = []): Promise<void> {
this.port = await navigator.serial.requestPort({ filters })
}
async connect(config: SerialConfig): Promise<void> {
if (!this.port) throw new Error('No port selected')
await this.port.open({
baudRate: config.baudRate,
dataBits: config.dataBits ?? 8,
stopBits: config.stopBits ?? 1,
parity: config.parity ?? 'none',
bufferSize: config.bufferSize ?? 4096,
flowControl: config.flowControl ?? 'none',
})
this.writer = this.port.writable!.getWriter()
this.startReadLoop()
}
private async startReadLoop(): Promise<void> {
if (!this.port?.readable) return
this.readLoopActive = true
while (this.port.readable && this.readLoopActive) {
this.reader = this.port.readable.getReader()
try {
while (true) {
const { value, done } = await this.reader.read()
if (done) break
if (value) {
this.dispatchEvent(
Object.assign(new Event('data'), { detail: value })
)
}
}
} catch (error) {
if (this.readLoopActive) {
this.dispatchEvent(
Object.assign(new Event('error'), { detail: error })
)
}
} finally {
this.reader.releaseLock()
}
}
}
async write(data: Uint8Array | string): Promise<void> {
if (!this.writer) throw new Error('Port not open')
const bytes =
typeof data === 'string'
? new TextEncoder().encode(data)
: data
await this.writer.write(bytes)
}
async disconnect(): Promise<void> {
this.readLoopActive = false
this.reader?.cancel()
this.writer?.releaseLock()
await this.port?.close()
this.port = null
this.reader = null
this.writer = null
}
get isConnected(): boolean {
return this.port !== null && this.port.readable !== null
}
}
For more details on Serial API methods, refer to the official documentation on MDN.
How to Work with Protocols: Adapter for Request-Response
Most devices use text or binary protocols over UART browser communication. Example for a device with a request-response protocol using \r\n delimiters:
class LineProtocolAdapter {
private buffer = ''
private pendingResolvers: Array<(line: string) => void> = []
constructor(private serial: SerialService) {
serial.addEventListener('data', (e: Event) => {
const event = e as Event & { detail: Uint8Array }
this.buffer += new TextDecoder().decode(event.detail)
this.flushLines()
})
}
private flushLines(): void {
const lines = this.buffer.split('\r\n')
this.buffer = lines.pop() ?? ''
for (const line of lines) {
if (line.trim()) {
const resolver = this.pendingResolvers.shift()
if (resolver) resolver(line.trim())
}
}
}
async sendCommand(command: string, timeoutMs = 2000): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingResolvers = this.pendingResolvers.filter(r => r !== resolve)
reject(new Error(`Timeout: no response to "${command}" in ${timeoutMs}ms`))
}, timeoutMs)
this.pendingResolvers.push((line) => {
clearTimeout(timer)
resolve(line)
})
this.serial.write(command + '\r\n').catch(reject)
})
}
}
// Usage:
const adapter = new LineProtocolAdapter(serialService)
const version = await adapter.sendCommand('VERSION')
const sensorData = await adapter.sendCommand('READ SENSOR 1')
To filter devices by USB Vendor/Product ID, use navigator.serial.requestPort() with filters option. After initial authorization, you can restore the port without a dialog via navigator.serial.getPorts(). This enables USB auto-connect reconnection. For binary protocols, work directly with Uint8Array.
How to Implement Integration: Step-by-Step Plan
- Analyze the device protocol. Study the documentation: baud rate configuration, parity, command format. Collect test data.
- Configure the port. Open the port with required parameters via
SerialPort.open(). Use the SerialService class from the example. - Implement the protocol adapter. For text protocols, write
LineProtocolAdapter; for binary protocols, work directly withUint8Array. - Integration with UI. Wrap the service in a React Serial Service (hook
useSerialService) or Vue-composable. Add error handling and reconnection. - Testing. Test on a real device or emulator (socat/VSPE). Ensure Serial API fallback works.
Typical Integration Mistakes
- Forgot to trim extra characters from the buffer — get garbage.
- Did not handle device disconnection — port hangs.
- Use incorrect baud rate — data not readable.
- Did not implement command timeout — application hangs forever.
What's Included in the Work
- Protocol analysis of the target device
- Configuration of port parameters (baud rate configuration, parity, flow control)
- Implementation of SerialService and protocol adapter classes
- React hook or Vue-composable
- Reconnection handling
- Fallback for unsupported browsers
- Testing on real hardware or emulator
If the device uses a proprietary binary protocol, additional time is required for reverse engineering or studying the documentation. We also guarantee support for the developed solution and free consultations for one month after deployment.
Timeline: 2–4 days depending on device protocol complexity. Place an order — our engineers will adapt the solution to your equipment. Prices start at $800 for a standard integration.







