Webserial in PyScript

Published February 8, 2023

Tags: python pyscript serial webserial

A user came along the PyScipt GitHub Discussions the other day with an interesting question - can you use PySerial (or similar) in PyScript? That got my wheels a turning; this post is the answer to that question.

The short answer is no, PySerial doesn't work in PyScript - PySerial and other serial libraries rely on low-level features of their host operating systems which just aren't present in the browser window.

But just because PySerial doesn't work, doesn't mean that serial connections can't work. Using the new-ish WebSerial Browser API, we can ask web users for permission to access their local serial devices. If it's granted, we can access those devices via a serial connection. And if you'd like to try it out live in your browser, hit the load PyScript button below:

This isn't a particularly full-featured demo. As you'll see in the code below, it doesn't contain much provision for error handling, and only the barest of UI. But it does work!

Below are three source files for a working demo using WebSerial in PyScript. The first (webPageSerial.html) is a (minimally formatted) HTML page with two buttons - "Open a Serial Port" and "Write to the Serial Port" - as well as an input box. Clicking the "Open" button prompts the user (if their browser supports WebSerial) to select an available serial port, connects to it, and begins listening for incoming bytes on that port. Once the port is open, when the user clicks the "write" button, the contents of the text box are written to the open serial port.

webserialPage.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSerial Demo</title>
    <script defer src="https://pyscript.net/releases/2022.12.1/pyscript.js"></script>
    <link rel="stylesheet" href="https://pyscript.net/releases/2022.12.1/pyscript.css">
</head>
<body>
    <py-script src="webserialdemo.py"></py-script>
    <button py-click="sm.askForSerial()" id="open">Open a Serial Port</button>
    <br><button py-click ="sendValueFromInputBox(sm)" id="write">Write to the serial port:</button>
    <input type="text" id="text">
</body>
</html>

The second file (webSerialDemo.py) contains the actual Python/PyScript code that makes this demo work. It wraps the WebSerial API in a new class, SerialManager, for the purpose of managing the state of the serial connection. It also creates an instance of this class, called sm, which is referenced by the py-click attributes in the above HTML document.

Finally, a single helper function sendValueFromInputBox() is defined, which is used by the "Write" button - it fetches the contents of the input box, asks the SerialManager to write that value to the serial port, then clears the input box.

webSerialDemo.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from js import navigator
from pyodide.ffi import to_js
from pyodide.ffi.wrappers import add_event_listener

#Utility function for converting py dicts to JS objects
def j(obj):
    return to_js(obj, dict_converter=js.Object.fromEntries)

class SerialManager():
    '''
    Class for managing reads and writes to/from a serial port
    Not very clean! No error handling, no way to stop listening etc.
    '''
    async def askForSerial(self):
        '''
        Request that the user select a serial port, and initialize
        the reader/writer streams with it
        '''
        if not hasattr(navigator, 'serial'):
            warning = "This browser does not support WebSerial; see https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility for a list of compatible browsers."
            print(warning)
            raise NotImplementedError(warning)
        
        self.port = await navigator.serial.requestPort()
        await self.port.open(j({"baudRate": 9600}))
        js.console.log("OPENED PORT")

        # Set up encoder to write to port
        self.encoder = js.TextEncoderStream.new()
        outputDone = self.encoder.readable.pipeTo(self.port.writable)

        # Set up listening for incoming bytes
        self.decoder = js.TextDecoderStream.new()
        inputDone = self.port.readable.pipeTo(self.decoder.writable)
        inputStream = self.decoder.readable

        self.reader = inputStream.getReader();
        await self.listenAndEcho()

    async def writeToSerial(self, data):
        '''Write to the serial port'''
        outputWriter = self.encoder.writable.getWriter()
        outputWriter.write(data + '\n')
        outputWriter.releaseLock()
        js.console.log(f"Wrote to stream: {data}")

    async def listenAndEcho(self):
        '''Loop forever, echoing values received on the serial port to the JS console'''
        receivedValues = []
        while (True):
            response = await self.reader.read()
            value, done = response.value, response.done
            if ('\r' in value or '\n' in value):
                #Output whole line and clear buffer when a newline is received
                print(f"Received from Serial: {''.join(receivedValues)}")
                receivedValues = []
            elif (value):
                #Output individual characters as they come in
                print(f"Received Char: {value}")
                receivedValues.append(value)

#Create an instance of the SerialManager class when this script runs
sm = SerialManager()

#A helper function - to point the py-click attribute of one of our buttons to
async def sendValueFromInputBox(sm: SerialManager):
    '''
    Get the value of the input box and write it to serial
    Also clears the input box
    '''
    textInput = js.document.getElementById("text")
    value = textInput.value
    textInput.value = ''
    print(f"Writing to Serial Port: {value}")

    await sm.writeToSerial(value)

Scroll to see complete code

Finally, because a serial demo isn't all that exciting without something to actually communicate with, the final bit of code is an Arduino Sketch. When run on an Arduino Uno or similar, the code simply echos back what it receives on its serial port, with a slight delay.

arduinoSerialEcho.ino

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Echos back whatever is written to the serial port, with a small delay
void setup() {
  Serial.begin(9600);
}

void loop() {
  if (Serial.available() > 0){
    int incomingByte = Serial.read();
    delay(100);
    Serial.print(char(incomingByte));
  }
}