google/web-serial-polyfill

Vanilla javascript example

karelv opened this issue · 16 comments

karelv commented

Hello, I'm trying to use this package to have my website communicate with my arduino sketch. On desktop chrome I have it working with USB Web Serial from 'navigator'. However in Android this is not made available.
This is where this lib comes useful.
I have found the old demo.html file and the dist/serial.js. However it shows me only the USB_VID/PID I can't get the send/receive function to work.

Would it be possible to share an example on how to use this library?
I like jquery, but plain vanilla javascript would be very helpful.
I have never used .ts files, but if know this is the way to use this lib, I'm open to learn that file format as well. (or at least how this concept is working).

Thanks in advance.

If you're using vanilla JavaScript (rather than building your app with a bundler that can import the NPM module), do this:

<script type="module">
import { serial as webSerialPolyfill } from "https://cdn.jsdelivr.net/npm/web-serial-polyfill@1.0.15";
// Use webSerialPolyfill as you would navigator.serial.
</script>
karelv commented

Awesome, that works!
At least for 99.9% of the cases.
After a while(some seconds) I got an error:
NetworkError: Failed to execute 'transferIn' on 'USBDevice': A transfer error has occurred.
I have quite some data coming in over the CDC UART, but I have not seen this on a desktop with navigator.serial.
Any hints?

Open chrome://device-log on the device to see the low-level USB error message. It may also be helpful to collect a bug report from the device in order to look at the kernel log. When running on a phone it's possible there are novel issues like insufficient power to reliably run the device.

karelv commented

Thanks for your prompt answer!

Yeah, I understand it is a different beast...
See here the device-log:
image

karelv commented

Sorry, the screenshot shows a little big... also note that I got the 'transferIn' error only once on my webpage. It disconnects.
When the error arrives on my web-page, it is displayed in textarea, the datetime was "2023/11/21 11:17:06.862"

karelv commented

This made me realize I need to supply the bottom of that page....
image

karelv commented

Some other datapoints:
1] I'm trying to discern what happens on the MCU-side. After the error message appears, I clear the device-log, then I reload the page(app), and re-connect to the serial port, while keeping the MCU in the USB port of the phone. This works correctly, until that error appears again. Therefore I believe the MCU keeps running in case of that event. (Also I need to sent something to the MCU before it sends on a regular basis something back).
2] I have tried to send less often (every 2 seconds), but that does not help either.
3] I don't know how big the data-chunks are that the MCU is sending over, is there a limit of what web-serial-polyfill can receive at once?
3bis] I changed the firmware to perform a 'flush' every 32 bytes, no change.
4] Same behavior without the 'Generic Billboard Device' (usb-hub device), and thus using a direct connection between MCU and the phone (USBC to USBA).

I think the issue is that the Linux kernel imposes a limit on the total size of outstanding USB transfers in order to limit the amount of kernel memory that is consumed. It's not about how much data the device is sending but how much the page is asking for at once. If it ends up creating too many transferIn() calls or specifies too large a transfer size then you'll get this error. As written the polyfill should only be issuing one transferIn() call at once (having multiple would be a nice performance optimization) and the transfer size is based on the desiredSize reported by the ReadableStream, which comes from the bufferSize parameter passed in when opening the port. What do you have this set to?

karelv commented

I guess you mean this parameter:
https://developer.mozilla.org/en-US/docs/Web/API/SerialPort/open#buffersize
I'm trying to increase it, I was using the default, which is 255 bytes, but I can confirm my chunks are larger!
I keep you posted.

karelv commented

Okay here it is:
I have increased the bufferSize to 10000 ==> after first datachunk received, it failed
Then I changed it to 32 ==> similar results as before
Then I changed it to 5000 ==> it failed after the 2nd datachunk.

Note: A datachuck is 4635 bytes long.

Is there something I can do in the webpage, to always empty the buffer; and thus buffer it inside the browser in stead of inside the kernel?

Note2: I checked 'device-log', same error.

Is there something I can do in the webpage, to always empty the buffer; and thus buffer it inside the browser in stead of inside the kernel?

USB is a pull-based transport rather than a push-based transport. This means that the device never sends data unless asked. So data can't get buffered in the kernel. The site asks for N bytes of data, the kernel sets aside N bytes of memory and starts asking the device for data. When it gets some that completes the transaction and the data is sent to the site. If the device sent more than N bytes that's called a "babble" error and doesn't seem to be the problem you're seeing here.

The "out of memory" error is the kernel complaining that the sum of all the transfers Chrome has requested is greater than an internal limit it has set. Given how the polyfill is implemented this should't happen because there's only ever one IN transfer requested at a time and the size is based on the requested buffer size, which you've said is very small.

My best guess of what's going on here is that there is a bug in the polyfill causing it to not wait for each transfer to complete and is instead submitting lots of transfers at once. The larger the buffer size the more quickly that will cause problems. Either that or you are also trying to send a very large chunk of data to the device.

To debug further you'll need to put some debug logging into the polyfill itself to understand when and how often it is calling transferIn(). You can rebuild the library by checking out this repository and running,

npm install   # One-time setup
npm run build # Generates dist/serial.js
karelv commented

This is again very helpful!
I have build the dist/serial.js
And have since I have no console.log, I added a div on the page and updated the serial.js file as follows:

            try {
                let elem = document.querySelector('#console');
                my_counter = my_counter + 1;
                let local_counter = my_counter;
                elem.innerHTML = elem.innerHTML + "<hr>START["+local_counter.toString()+"]: chunkSize: "+chunkSize.toString()+" -- this.endpoint_.endpointNumber: "+this.endpoint_.endpointNumber.toString()+" <br>";
                const result = await this.device_.transferIn(this.endpoint_.endpointNumber, chunkSize);
                elem.innerHTML = elem.innerHTML + "RESULT["+local_counter.toString()+"]: "+result.status+"<br>";
                if (result.status != 'ok') {
                    controller.error(`USB error: ${result.status}`);
                    this.onError_();
                }
                if ((_a = result.data) === null || _a === void 0 ? void 0 : _a.buffer) {
                    elem.innerHTML = elem.innerHTML + "DATA["+local_counter.toString()+"]: byteOffset: "+result.data.byteOffset.toString()+" | byteLength: "+result.data.byteLength.toString()+" <hr>";
                    const chunk = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
                    controller.enqueue(chunk);
                }

Note that my_counter is a global variable, while local_counter has a scope only for within the function.

Now, I must say that my knowledge about await and asynch programming is limited. So I hope you can make sense of the log it has generated. (see below)

karelv commented
console:
START[1]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[1]: ok
DATA[1]: byteOffset: 0 | byteLength: 38
START[2]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[3]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[2]: ok
DATA[2]: byteOffset: 0 | byteLength: 2
START[4]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[5]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[3]: ok
DATA[3]: byteOffset: 0 | byteLength: 109
START[6]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[7]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[4]: ok
DATA[4]: byteOffset: 0 | byteLength: 1
START[8]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[9]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[5]: ok
DATA[5]: byteOffset: 0 | byteLength: 2
START[10]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[11]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[6]: ok
DATA[6]: byteOffset: 0 | byteLength: 1
START[12]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[13]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[7]: ok
DATA[7]: byteOffset: 0 | byteLength: 1
START[14]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[15]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[8]: ok
DATA[8]: byteOffset: 0 | byteLength: 1
START[16]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[17]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[9]: ok
DATA[9]: byteOffset: 0 | byteLength: 24
START[18]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[19]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[10]: ok
DATA[10]: byteOffset: 0 | byteLength: 1
START[20]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[21]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[11]: ok
DATA[11]: byteOffset: 0 | byteLength: 1
START[22]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[23]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[12]: ok
DATA[12]: byteOffset: 0 | byteLength: 2
START[24]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[25]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[13]: ok
DATA[13]: byteOffset: 0 | byteLength: 2
START[26]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[27]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[14]: ok
DATA[14]: byteOffset: 0 | byteLength: 1
START[28]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[29]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[15]: ok
DATA[15]: byteOffset: 0 | byteLength: 1
START[30]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[31]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[16]: ok
DATA[16]: byteOffset: 0 | byteLength: 1
START[32]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[33]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[17]: ok
DATA[17]: byteOffset: 0 | byteLength: 1
START[34]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[35]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[18]: ok
DATA[18]: byteOffset: 0 | byteLength: 1
START[36]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[37]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[19]: ok
DATA[19]: byteOffset: 0 | byteLength: 1
START[38]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[39]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[20]: ok
DATA[20]: byteOffset: 0 | byteLength: 2
START[40]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[41]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[21]: ok
DATA[21]: byteOffset: 0 | byteLength: 1
START[42]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[43]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[22]: ok
DATA[22]: byteOffset: 0 | byteLength: 2
START[44]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[45]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[23]: ok
DATA[23]: byteOffset: 0 | byteLength: 1
START[46]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[47]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[24]: ok
DATA[24]: byteOffset: 0 | byteLength: 1
START[48]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[49]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[25]: ok
DATA[25]: byteOffset: 0 | byteLength: 1
START[50]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[51]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[26]: ok
DATA[26]: byteOffset: 0 | byteLength: 1
START[52]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[53]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[27]: ok
DATA[27]: byteOffset: 0 | byteLength: 1
START[54]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[55]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[28]: ok
DATA[28]: byteOffset: 0 | byteLength: 1
START[56]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[57]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[29]: ok
DATA[29]: byteOffset: 0 | byteLength: 2
START[58]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[59]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[30]: ok
DATA[30]: byteOffset: 0 | byteLength: 1
START[60]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[61]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[31]: ok
DATA[31]: byteOffset: 0 | byteLength: 1
START[62]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[63]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[32]: ok
DATA[32]: byteOffset: 0 | byteLength: 1
START[64]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[65]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[33]: ok
DATA[33]: byteOffset: 0 | byteLength: 1
START[66]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[67]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[34]: ok
DATA[34]: byteOffset: 0 | byteLength: 1
START[68]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[69]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[35]: ok
DATA[35]: byteOffset: 0 | byteLength: 2
START[70]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[71]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[36]: ok
DATA[36]: byteOffset: 0 | byteLength: 1
START[72]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[73]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[37]: ok
DATA[37]: byteOffset: 0 | byteLength: 1
START[74]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[75]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[38]: ok
DATA[38]: byteOffset: 0 | byteLength: 1
START[76]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[77]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[39]: ok
DATA[39]: byteOffset: 0 | byteLength: 55
START[78]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[79]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[40]: ok
DATA[40]: byteOffset: 0 | byteLength: 161
START[80]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[81]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[41]: ok
DATA[41]: byteOffset: 0 | byteLength: 1
START[82]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[83]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[42]: ok
DATA[42]: byteOffset: 0 | byteLength: 1
START[84]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[85]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[43]: ok
DATA[43]: byteOffset: 0 | byteLength: 1
START[86]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[87]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[44]: ok
DATA[44]: byteOffset: 0 | byteLength: 2
START[88]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[89]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[45]: ok
DATA[45]: byteOffset: 0 | byteLength: 1
START[90]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[91]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[46]: ok
DATA[46]: byteOffset: 0 | byteLength: 1
START[92]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[93]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[47]: ok
DATA[47]: byteOffset: 0 | byteLength: 1
START[94]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[95]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[48]: ok
DATA[48]: byteOffset: 0 | byteLength: 1
START[96]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[97]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[49]: ok
DATA[49]: byteOffset: 0 | byteLength: 1
START[98]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[99]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[50]: ok
DATA[50]: byteOffset: 0 | byteLength: 1
START[100]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[101]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[51]: ok
DATA[51]: byteOffset: 0 | byteLength: 2
START[102]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[103]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[52]: ok
DATA[52]: byteOffset: 0 | byteLength: 1
START[104]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[105]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[53]: ok
DATA[53]: byteOffset: 0 | byteLength: 1
START[106]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[107]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[54]: ok
DATA[54]: byteOffset: 0 | byteLength: 3
START[108]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[109]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[55]: ok
DATA[55]: byteOffset: 0 | byteLength: 2
START[110]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[111]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[56]: ok
DATA[56]: byteOffset: 0 | byteLength: 2
START[112]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[113]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
RESULT[57]: ok
DATA[57]: byteOffset: 0 | byteLength: 2
START[114]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2
START[115]: chunkSize: 5056 -- this.endpoint_.endpointNumber: 2

As you can see it looks like there are more than one entry into the transferIn function...
Not sure what the next steps should be...

karelv commented

Hey reillyeon, Many thanks for your help!
I have tried to isolate the issue by making a small web-page demonstrator for web-serial (both -api and -polyfill).
Have a look here: https://github.com/karelv/web-serial-example and here https://karelv.github.io/web-serial-example/
It seems to keep working, meaning I need to review my more complex project, I guess.
Note: when use the serial.js with my_counter & local_counter, it still indicates there are several transferIn called at the same time.

karelv commented

Actually it still happens... (it takes just a whole lot longer!)

karelv commented

Let's close this one, I will make another ticket, as the example script is clear:
https://github.com/karelv/web-serial-example