Принтер етикеток не друкує, 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 дні залежно від складності протоколу пристрою. Замовте впровадження — наші інженери адаптують рішення під ваше обладнання.







