Search code examples
javascriptreactjsserial-portuartweb-serial-api

"DOMException: A buffer overrun has been detected" but only if USB serial port left connected for some time?


I have a React application, where I read data from a microcontroller using a UART cable. The microcontroller is continuously printing a JSON string. The application parses this string and displays its values.

The JSON string coming from my microcontroller looks like this:

{"addr":"00xxxxxxxxxxxxxx","1":{"type":"a","value":40.7}}

I followed this blog post to help me with this, and am using a TransformStream with the same LineBreakTransformer that is used in the blog post so that I can parse a complete JSON string. I have a button that triggers this all on click.

Here is the issue I am facing: If I plug in the USB and quickly press the button, everything works fine. I get the prompt asking me to select the correct COM port, I receive data, and am able to parse it. However, if I plug in the USB, and leave it for a while before pressing the button, I get the COM port prompt, but then get these errors:

Uncaught (in promise) DOMException: A buffer overrun has been detected.
Uncaught (in promise) TypeError: Failed to execute 'pipeTo' on 'ReadableStream': Cannot pipe a locked stream

Also, after I refresh the screen when I get this error, and quickly press the button, it works successfully.

Why is this happening, and how can I resolve this?

Thank you.

Source code:

import React, { useState } from 'react'

class LineBreakTransformer {
  constructor() {
    this.chunks = ''
  }

  transform(chunk, controller) {
    this.chunks += chunk
    const lines = this.chunks.split('\r\n')
    this.chunks = lines.pop()
    lines.forEach((line) => controller.enqueue(line))
  }

  flush(controller) {
    controller.enqueue(this.chunks)
  }
}

const App = () => {
  const [macAddr, setMacAddr] = useState('')
  const [sensors, setSensors] = useState([])

  async function onButtonClick() {
    const port = await navigator.serial.requestPort()
    await port.open({ baudRate: 115200, bufferSize: 10000000 })
    while (port.readable) {
      // eslint-disable-next-line no-undef
      const textDecoder = new TextDecoderStream()
      port.readable.pipeTo(textDecoder.writable)
      const reader = textDecoder.readable.pipeThrough(new TransformStream(new LineBreakTransformer())).getReader()
      try {
        while (true) {
          const { value, done } = await reader.read()
          if (done) {
            reader.releaseLock()
            break
          }
          if (value) {
            const { addr, ...sensors } = JSON.parse(value)
            setMacAddr(addr)
            setSensors(sensors)
          }
        }
      } catch (error) {}
    }
  }
  return (
    <div>
      <h1>{`macAddr: ${macAddr}`}</h1>
      {Object.keys(sensors).map((sensor) => (
        <div key={sensor}>
          <div>{`Channel: ${sensor}`}</div>
          <div>{`Temp: ${sensors[sensor].value}`}</div>
        </div>
      ))}
      <button
        onClick={async () => {
          await onButtonClick()
        }}
      >
        CLick
      </button>
    </div>
  )
}

export default App

Solution

  • Alright guys, I figured out what I was doing wrong. The issue had nothing to do with waiting. The issue was actually on JSON.parse().

    Since I am reading streams, occasionally I don't get a full packet, and only get the remaining part of the JSON string being sent from the microcontroller, (e.g. only :1} instead of {"a":1}. It's just less likely for this to happen when I begin reading immediately after plugging in the device.

    When I parse it, my function errors out because it is not valid JSON. Since I have it on a while loop, it then fails at port.readable.pipeTo(textDecoder.writable) because the reader is locked.

    I fixed this by wrapping the parse() in a try catch block.