Реалізація Web Serial API на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Web Serial API на сайті
Складна
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Web Serial API на сайті

Web Serial API відкриває браузеру прямий канал до COM-портів, USB-пристроїв з послідовним інтерфейсом та Bluetooth Serial Port Profile. Принтери етикеток, Arduino, промислові датчики, медичні прилади, POS-термінали — все це тепер доступне з веб-сторінки без нативних програм, Java-апплетів, без Electron. Тільки navigator.serial, тільки браузер.

Це не простий API. Він асинхронний, потоковий, вимагає явного користувацького жесту для вибору порту і працює виключно у Secure Context (HTTPS або localhost). Реалізація без розуміння ReadableStream та WritableStream перетворюється на переповнений буфер обіцянок і завислий UI.

Підтримка та обмеження

API підтримується в Chrome 89+, Edge 89+, Opera 75+. Firefox та Safari — не підтримують. Це означає, що сторінка з Web Serial повинна або вимагати Chromium-браузер, або надавати fallback-інтерфейс (ручний ввід, завантаження файлу).

Перевірка підтримки:

if (!('serial' in navigator)) {
  throw new Error('Web Serial API не підтримується. Використовуйте Chrome 89+')
}

Дозвіл Origin: у продакшені обов'язково додайте в заголовки:

Permissions-Policy: serial=*

Або обмежте конкретним origin:

Permissions-Policy: serial=(self "https://app.example.com")

Архітектура сервісу

Усю роботу з портом ізолюємо в класі. 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
  }
}

Робота з протоколами

Більшість пристроїв використовують текстові або бінарні протоколи поверх 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 витягують. Кабелі болтаються. Потрібен reconnect:

navigator.serial.addEventListener('connect', (event) => {
  const port = (event as Event & { target: SerialPort }).target
  console.log('Пристрій підключено:', port.getInfo())
  // Перевіримо, це наш порт, та переподключимось
})

navigator.serial.addEventListener('disconnect', (event) => {
  const port = (event as Event & { target: SerialPort }).target
  if (port === serialService.currentPort) {
    serialService.handleDisconnect()
  }
})

Запит за USB Vendor/Product ID

Щоб не пропонувати користувачу всі доступні порти, а одразу показати тільки потрібний пристрій:

// Список відомих пристроїв
const DEVICE_FILTERS: SerialPortFilter[] = [
  { usbVendorId: 0x2341 },         // Arduino
  { usbVendorId: 0x0483, usbProductId: 0x5740 }, // STM32 Virtual COM
  { usbVendorId: 0x10C4, usbProductId: 0xEA60 }, // CP2102 (Silicon Labs)
  { usbVendorId: 0x0403, usbProductId: 0x6001 }, // FTDI FT232
]

await serialService.requestPort(DEVICE_FILTERS)

Збереження вибраного порту

Після першого requestPort користувач дає дозвіл. При повторному відкритті сторінки порт можна отримати без нового діалогу:

async function autoConnect(config: SerialConfig): Promise<boolean> {
  const ports = await navigator.serial.getPorts()
  if (ports.length === 0) return false

  // Беремо перший авторизований порт (або фільтруємо за getInfo())
  serialService.port = ports[0]
  await serialService.connect(config)
  return true
}

React-хук

function useSerialPort(config: SerialConfig) {
  const serviceRef = useRef(new SerialService())
  const [connected, setConnected] = useState(false)
  const [lastData, setLastData] = useState<Uint8Array | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const service = serviceRef.current

    const onData = (e: Event) => {
      setLastData((e as any).detail)
    }
    const onError = (e: Event) => {
      setError((e as any).detail?.message ?? 'Помилка порту')
      setConnected(false)
    }

    service.addEventListener('data', onData)
    service.addEventListener('error', onError)

    return () => {
      service.removeEventListener('data', onData)
      service.removeEventListener('error', onError)
    }
  }, [])

  const connect = useCallback(async () => {
    try {
      setError(null)
      await serviceRef.current.requestPort()
      await serviceRef.current.connect(config)
      setConnected(true)
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Не удалось підключитися')
    }
  }, [config])

  const disconnect = useCallback(async () => {
    await serviceRef.current.disconnect()
    setConnected(false)
  }, [])

  const write = useCallback((data: Uint8Array | string) => {
    return serviceRef.current.write(data)
  }, [])

  return { connected, lastData, error, connect, disconnect, write }
}

Що входить у роботу

Аналіз протоколу цільового пристрою, настройка параметрів порту (baud rate, parity, flow control), реалізація класів SerialService та протокольного адаптера, React-хук або Vue composable, обробка переподключень, обробка помилок, UI статусу підключення.

Якщо пристрій використовує проприетарний бінарний протокол — додатковий час на реверс-інжиніринг або вивчення документації.

Строк: 2–4 дні залежно від складності протоколу пристрою.