Реализация 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, обработка переподключений, fallback для неподдерживаемых браузеров, тестирование на реальном оборудовании или эмуляторе (socat/VSPE).

Если устройство использует проприетарный бинарный протокол — дополнительное время на реверс-инжиниринг или изучение документации.

Срок: 2–4 дня в зависимости от сложности протокола устройства.