--{{0}}--
Connect the LiaScript code editor with the WebSerial API in Chrome. This allows for example to send Python code to a MicroPython device (like ESP32 or Raspberry Pi Pico) and receive output in real-time. The WebSerial API is implemented in Chrome and Edge, but not in Firefox or Safari.
Try it on LiaScript:
See the project on Github:
https://github.com/liaTemplates/webserial
--{{1}}--
Like with other LiaScript templates, there are three ways to integrate WebSerial, but the easiest way is to copy the import statement into your project. See the implementation in Sec. Implementation.
{{1}}
-
Load the latest macros via (this might cause breaking changes)
import: https://raw.githubusercontent.com/liaTemplates/webserial/main/README.mdor the current version 0.0.1 via:
import: https://raw.githubusercontent.com/LiaTemplates/webserial/0.0.1/README.md -
Copy the definitions into your Project
-
Clone this repository on GitHub
--{{0}}--
The @WebSerial macro attached to a LiaScript code-block will pass the code via the WebSerial API to a connected device.
Additionally, it will create a terminal for the device output and input.
``` python
for i in range(10):
print("Hello World", i)
```
@WebSerialResult:
for i in range(10):
print("Hello World", i)@WebSerial
@WebSerial
<script>
(async function() {
// Check if the Web Serial API is supported.
if (!("serial" in navigator)) {
setTimeout(() => {
console.error("Web Serial API is not supported in this browser, try Chrome.");
}, 100)
send.lia("LIA: stop")
return;
}
// Declare connection-related variables for later cleanup.
let port = null;
let reader = null;
try {
// Request and open the serial port.
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
// Create a TextEncoder instance.
const encoder = new TextEncoder();
// Function to stop any currently running code by sending Ctrl-C.
async function stopCurrentProgram() {
try {
const writer = port.writable.getWriter();
// Send Ctrl-C (ASCII 0x03) to interrupt any running code.
await writer.write(encoder.encode("\x03"));
// Wait briefly to allow the interrupt to be processed.
await new Promise(resolve => setTimeout(resolve, 100));
// Send a second Ctrl-C in case the first one was missed.
await writer.write(encoder.encode("\x03"));
writer.releaseLock();
} catch (e) {
console.error("Error sending Ctrl-C:", e);
}
}
// Stop any running code before sending new code.
await stopCurrentProgram();
// Retrieve the entire Python code from the liascript input.
const pythonCode = `@input(0)`;
// Function to send code using MicroPython's paste mode.
// In paste mode, the REPL buffers all lines until Ctrl‑D is received,
// then it compiles and executes the entire code block at once.
async function sendCodeInPasteMode(code) {
const writer = port.writable.getWriter();
// Enter paste mode (Ctrl‑E, ASCII 0x05).
await writer.write(encoder.encode("\x05"));
// Wait briefly for paste mode to be activated.
await new Promise(resolve => setTimeout(resolve, 100));
// Split the code into lines, preserving all indentation.
const codeLines = code.split(/\r?\n/);
for (const line of codeLines) {
// Send each line exactly as-is, with CR+LF.
await writer.write(encoder.encode(line + "\r\n"));
}
// Exit paste mode by sending Ctrl‑D (ASCII 0x04).
await writer.write(encoder.encode("\x04"));
writer.releaseLock();
send.lia("LIA: terminal");
}
// Function that sends the code and reads output until the REPL prompt (">>>") is detected.
// This ensures the entire block is executed before further input is allowed.
async function sendCodeAndWaitForPrompt(code) {
await sendCodeInPasteMode(code);
let outputBuffer = "";
const tempReader = port.readable.getReader();
const decoder = new TextDecoder();
let promptFound = false;
while (!promptFound) {
const { value, done } = await tempReader.read();
if (done) break;
if (value) {
const text = decoder.decode(value);
outputBuffer += text;
console.stream(text);
// Look for the REPL prompt (adjust if your prompt differs).
if (outputBuffer.includes(">>>")) {
promptFound = true;
}
}
}
await tempReader.releaseLock();
return outputBuffer;
}
// Send the Python code and wait until the prompt is detected.
await sendCodeAndWaitForPrompt(pythonCode);
console.log("Python code executed and prompt detected.");
// Now that execution is complete, enable terminal input.
send.lia("LIA: terminal");
// Start a global read loop to capture and display subsequent output.
reader = port.readable.getReader();
const globalDecoder = new TextDecoder();
(async function readLoop() {
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
console.debug("Stream closed");
send.lia("LIA: stop");
break;
}
if (value) {
console.stream(globalDecoder.decode(value));
}
}
} catch (error) {
console.error("Read error:", error);
} finally {
try { reader.releaseLock(); } catch (e) { /* ignore */ }
}
})();
// Handler to send terminal input lines to MicroPython.
send.handle("input", input => {
(async function() {
try {
const writer = port.writable.getWriter();
// Send the terminal input (preserving any whitespace) with CR+LF.
await writer.write(encoder.encode(input + "\r\n"));
writer.releaseLock();
} catch (e) {
console.error("Error sending input to MicroPython:", e);
}
})();
});
// Handler to clean up all connections and variables when a "stop" command is received.
send.handle("stop", async () => {
console.log("Cleaning up connections and stopping execution.");
// Cancel the reader if it exists.
if (reader) {
try {
await reader.cancel();
} catch (e) {
console.error("Error canceling reader:", e);
}
try { reader.releaseLock(); } catch (e) { /* ignore */ }
}
// Close the serial port if it's open.
if (port) {
try {
await port.close();
} catch (e) {
console.error("Error closing port:", e);
}
}
// Reset connection variables.
port = null;
reader = null;
console.log("Cleanup complete.");
});
} catch (error) {
console.error("Error connecting to the MicroPython device:", error);
send.lia("LIA: stop");
}
})();
"LIA: wait"
</script>
@end