Hello everyone. I welcome you to my 6th blog as part of Experimenting with Gesture Sensors Design Challenge. In this blog post I will make one step further from theory and evaluation kit description and I start implementing simple code which will act as a base for my first gesture-controlled game. In this blog I will describe my first experiments with Web Serial API which is JavaScript API used for interfacing Serial (UART) devices from web browser. At the end I will show my first web application which prints firmware version and also show heat map similarly like Maxim’s PC program does.
Web Serial
Web Serial is term for JavaScript API used for communicating with UART devices connected to the computer. Historically web browsers did not allow you to access any HW using web page but in last years this started changing and JavaScript was extended by set of APIs like Web USB, Web Bluetooth or Web Serial which I will show today.
Experiment
For my first experiment with this technology, I decided to make simple page which
- Connects to the MAX25405EVKIT Serial port
- Executes “ver” command and prints output
- Starts timer which will periodically read RAW ADC accumulators and draw heat map similarly to Maxim’s GUI program.
TypeScript
Instead of writing JavaScript I decided to use TypeScript. I did this because TypeScript adds type information and some syntax sugar to the JavaScript, so IDE (Visual Studio) can determine more information needed for functional Intellisense (Intellisense is name for code autocomplete in Visual Studio). At the end it is transpiled to JavaScript. Because it is new API there are no type definition for this API in standard TypeScript definitions, but you can download required definition from DefinelyTyped Github and include it in the project.
In this blog post I do not describe all details like HTML code and basic JavaScript/TypeScript constructions because this blog is not designed as HTML/JavaScript tutorial and this information have no relation to MAX25405. Source codes are available on Github here, so you can simply open them if you are interested in these details which I omit. Also note that in code examples I omitted almost all error checks which are present in source code. I did this for simplifying code examples. Error checks usually add many additional lines.
Requesting access to Serial devices
Before we can open serial port, we need to request permission from user. We can do this using requestPort function. All function related to Web Serial API are available in navigator.serial object. We can call this function as is, but we can specify optional parameter with some requirements. For example, we can in case of serial ports connected to computer over USB limit devices offered to user by USB Vendor Id (VID) and Product Id (PID). Because MAX25405 is this kind of device with emulated UART as USB CDC, I decided to do so. Advantage is that user can’t select wrong device because browser will not offer other COM ports to you. Function (and most other Web Serial functions) returns Promise because this function does not complete immediately. Instead, it completes sometime in the future (after user confirm his selection or reject the request). You can use standard .then() syntax but because I use TypeScript I used await keyword. There are two restrictions about calling this function. It must be called from handler which was triggered by user action (for example button click). Calling it in non-user triggered context (for example in onload handler, handler of setInterval, …) will result to fail. Other similar limitation is that webpage must run over encrypted HTTPS or run from localhost. Otherwise, you get error.
await navigator.serial.requestPort({ filters: [ { usbVendorId: 0x0B6A, // Maxim Integrated usbProductId: 0x4360 // MAX32620 MCU with MAX25405EVKIT firmware } ] });
Following popup appears after executing:
Search and open port
After gaining permissions you can enumerate serial ports and open it. Open method accepts (optional) parameters. Parameters are passed using object and you can for example specify baudrate, parity, number of stop bits, …. For writing and reading port we need to get instances of streams. We can get them using getWriter() and getReader methods from writable and readable objects on serial port object.
var serialPorts = await navigator.serial.getPorts(); for (var i = 0; i < serialPorts.length; i++) { await serialPorts[i].open({ baudRate: 115200 }); var writer = serialPorts[i].writable.getWriter(); var reader = serialPorts[i].readable.getReader(); }
Writing to serial port
For writing we can use write method of writer which we get in previous step. It has write method. This method does not expect string but rather expects array of 8-bit unsigned numbers. For converting string to array of numbers we can use TextEncoder class.
await writer.write(new TextEncoder().encode("ver\n"));
Reading from serial port
Reading we can do similarly but it has one issue. It reads very small fragments, frequently only single character. For this reason, we need to implement loop which will read whole line. It is slightly more complicated because we need to check for errors. Reader class do not return char directly, but it returns object which has something like status field which in case of occasional closing port while receiving data contains done property set to true (I guess that this can happen for example when disconnecting device from PC at the time of waiting for data). After receiving we can use TextDecoder to convert received array of bytes to natural string.
var message = ""; var completed = false; while (!completed && message.indexOf("\n") == -1) { var output = await reader.read(); if (output.done) { completed = true; } else { var received = new TextDecoder().decode(output.value); message = message + received; } }
After receiving message, I printed output on page.
After this I started implement heat map. Sending and receiving data is very similar.
Parsing big-endian numbers
As I stated in previous blog post registers containing ADC accumulator values are in big-endian order. For this reason processing output from “read reg 0x10 120” requires additional processing stuff. I need to combine two values by shifting and ORing them and I also need to handle sign because sensor can also return negative values (reason for negative values I described in third blog). Because values are encoded in two’s-complement I used construction with inverting all bits and adding hot one for converting negative unsigned number to signed. After processing pixel value, I implement threshold for values slower then 0 and higher than 4000. Note this algorithm is not the same as algorithm for computing pixel colors of heat map in Maxim’s GUI. Maxim implemented more advanced algorithm which handles underrange and overrange conditions by adjusting boundaries for color calculation. I implemented it simpler. At the end, pixel intensity is formed to RGB color and set to background color of the related table cell.
await writer.write(new TextEncoder().encode("reg read 0x10 120\n")); var output = await serialReadLine(reader); var bytes = output.split(" ").map((x) => Number.parseInt(x, 16)); for (var y = 0; y < 6; y++) { for (var x = 0; x < 10; x++) { // values are encoded as signed 16-bit integer encoded using BIG endian. var byte1 = bytes[2 * (y * 10 + x)]; var byte2 = bytes[2 * (y * 10 + x) + 1]; var valueRaw = (byte1 << 8) | byte2; var value = valueRaw; if (byte1 & 0x80) { value = ~valueRaw + 1; } if (value < 0) { value = 0; } if (value > 4000) { value = 4000; } var pixelValue = 255 * value / 4000; var cell = table.children[0].children[y].children[x] as HTMLTableCellElement; cell.style.backgroundColor = "rgb(0, " + Math.round(pixelValue) + ", 0)"; } }
See it in the action
You can see it in the action on the following video. After connecting I placed hand before sensor and did several gestures including shaking hand.
Try it yourself
Because it is website you do not need to download anything. I published this application to my website, so you can try it yourself very easily. Just connect your MAX25405 EVKIT to the computer and open following link. Note that currently only Google Chrome and Microsoft Edge support this feature.
https://misaz.cz/Public/MAX25405_WebSerialDemo/
Source codes
As I mentioned above all source codes are on Github. Note that according to best practices, Github repository do not contains compiled outputs (they are gitignored). If you want to see compiled outputs without transpiling them manually, you can see them directly on my website. I transpiled them without any optimizations, so they are pretty readable.
https://github.com/misaz/MAX25405EVKIT-WebSerialDemo
https://misaz.cz/Public/MAX25405_WebSerialDemo/MAX25405_ConnectionTest.js
What’s next?
Currently I am working on my first game using approach described in this blog. So most probably my next blog will be about my first gesture-controlled game. I already ordered and paid domain for it (http://gesture-tetris.fun/), but I did not configure web server and SSL certificate yet. In the meantime, I try to help other challengers on forum (and private messages) with compiling, flashing and debugging firmware provided by Maxim. Forum thread has over 70 replies and we resolved most issues, but some still remains. Most probably after resolving I will write some blog post or tutorial for saving time other challengers.
Next blog: Blog #7: Debugging Maxim’s Firmware Framework Crash