Принтер этикеток не печатает, Arduino не отвечает, промышленный датчик молчит — всё из‑за отсутствия связи браузера с COM-портом. Web Serial API решает эту проблему: браузер получает прямой доступ к последовательному порту без установки драйверов или нативных приложений. Мы обеспечиваем полную интеграцию COM-порта с веб-приложением. Внедряем это решение уже более 5 лет и реализовали более 50 проектов для промышленных датчиков, медицинских приборов и POS‑терминалов. Экономия на лицензиях достигает 50%, затраты на поддержку снижаются на 40%.
Почему Web Serial API лучше нативных приложений?
Web Serial API в 10 раз упрощает развёртывание: не нужно устанавливать драйверы, обновлять приложение или настраивать среду. Пользователь просто открывает страницу в Chromium‑браузере и работает. Сравнение с традиционными подходами:
| Критерий | Web Serial API | Java-апплет | Electron-приложение |
|---|---|---|---|
| Установка | Не требуется | Требуется JRE | Требуется установка |
| Обновление | Автоматически (веб) | Ручное | Ручное |
| Безопасность | Same-origin, HTTPS | Сомнительная | Зависит от сборки |
| Поддержка устройств | USB/Bluetooth/COM | Только COM | Через Native Messaging |
Web Serial API — это современное безопасное решение. Мы внедряем его под ключ за 2–4 дня.
Поддержка и ограничения
API поддерживается в Chrome 89+, Edge 89+, Opera 75+. Firefox и Safari не поддерживают. Поэтому страница должна либо требовать Chromium‑браузер, либо предоставлять Serial API fallback.
Проверка поддержки:
if (!('serial' in navigator)) {
throw new Error('Web Serial API не поддерживается. Используйте Chrome 89+')
}
Разрешение Origin: в продакшене обязательно добавить в заголовки Permissions-Policy: serial=*.
Как обеспечить работу в неподдерживаемых браузерах?
Если аудитория использует Firefox или Safari, предусмотрите fallback. Мы предлагаем три подхода:
| Fallback-метод | Описание | Сложность |
|---|---|---|
| Ручной ввод | Пользователь вводит данные вручную через форму | Минимальная |
| Загрузка файла | Данные экспортируются из устройства в файл, затем загружаются | Низкая |
| WebSocket-агент | На устройстве работает локальный агент, передающий данные через WebSocket | Средняя |
Выбор зависит от типа данных и требований к автоматизации. Например, для сканера этикеток достаточно загрузки файла, а для интерактивного управления Arduino веб-приложение потребует WebSocket‑агент.
Архитектура сервиса: как изолировать работу с портом
Всю работу с портом изолируем в класс SerialService. UI‑компонент не знает про потоки и буферы — он вызывает методы сервиса и получает данные через коллбэки или EventEmitter.
type SerialDataHandler = (data: Uint8Array) => void
type SerialErrorHandler = (error: Error) => void
interface SerialConfig {
baudRate: number
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('Порт не выбран')
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('Порт не открыт')
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
}
}
Подробнее о методах работы с Serial API читайте в официальной документации на MDN.
Как работать с протоколами: адаптер для запрос-ответ
Большинство устройств используют текстовые или бинарные протоколы поверх UART. Пример для устройства с протоколом запрос-ответ через \r\n-разделители:
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: нет ответа на "${command}" за ${timeoutMs}ms`))
}, timeoutMs)
this.pendingResolvers.push((line) => {
clearTimeout(timer)
resolve(line)
})
this.serial.write(command + '\r\n').catch(reject)
})
}
}
// Использование:
const adapter = new LineProtocolAdapter(serialService)
const version = await adapter.sendCommand('VERSION')
const sensorData = await adapter.sendCommand('READ SENSOR 1')
Для фильтрации устройств по USB Vendor/Product ID используйте navigator.serial.requestPort() с опцией filters. После первого разрешения порт можно восстановить без диалога через navigator.serial.getPorts(). Это реализует автоподключение USB. Для бинарных протоколов работайте с Uint8Array напрямую.
Как реализовать интеграцию: пошаговый план
- Анализ протокола устройства. Изучите документацию: настройка baud rate, parity, формат команд. Соберите тестовые данные.
- Настройка порта. Откройте порт с нужными параметрами через
SerialPort.open(). Используйте классSerialServiceиз примера. - Реализация протокольного адаптера. Если протокол текстовый, напишите
LineProtocolAdapter; для бинарных — работайте сUint8Arrayнапрямую. - Интеграция с UI. Оберните сервис в React Serial Service (хук
useSerialService) или Vue-composable. Добавьте обработку ошибок и переподключения. - Тестирование. Проверьте на реальном устройстве или эмуляторе (socat/VSPE). Убедитесь, что Serial API fallback работает.
Типичные ошибки при интеграции
- Забыли удалить лишние символы из буфера — получаете мусор.
- Не обработали отключение устройства — порт зависает.
- Используете неправильный baud rate — данные не читаются.
- Не реализовали таймаут для команд — приложение зависает навсегда.
Что входит в работу
Анализ протокола целевого устройства, настройка параметров порта (baud rate, parity, flow control), реализация классов SerialService и протокольного адаптера, React-хук или Vue-composable, обработка переподключений, fallback для неподдерживаемых браузеров, тестирование на реальном оборудовании или эмуляторе. Если устройство использует проприетарный бинарный протокол — дополнительное время на реверс-инжиниринг или изучение документации. Мы также гарантируем поддержку разработанного решения и бесплатные консультации в течение месяца после внедрения.
Срок: 2–4 дня в зависимости от сложности протокола устройства. Закажите внедрение — наши инженеры адаптируют решение под ваше оборудование.







