web: implement synchronous IO with web and service workers

This commit is contained in:
Fischer Moseley 2024-04-18 11:21:51 -07:00
parent d485971a1c
commit f4c24976c6
7 changed files with 147 additions and 148 deletions

12
doc/assets/main.py Normal file
View File

@ -0,0 +1,12 @@
from js import workerReadSerial, workerWriteSerial
def capture(foo):
print(f"(Python): capture() has been called with args: {foo}")
workerWriteSerial("W00000001\r\n")
workerWriteSerial("W00010001\r\n")
workerWriteSerial("R0000\r\n")
foo = workerReadSerial()
print(f"(Python): capture() has completed and returned value: ", {foo})
return foo
print("(Python) Load Complete!")

View File

@ -1,43 +0,0 @@
let port;
async function getPort() {
if (port) return port;
try {
[port] = await navigator.serial.getPorts();
await port.open({ baudRate: 115200 });
console.log('Connected to serial device:', port);
return port;
} catch (error) {
console.error('Error connecting to serial device:', error);
}
}
async function write(data) {
const port = await getPort();
const writer = port.writable.getWriter();
await writer.write(new TextEncoder().encode(data));
await writer.releaseLock();
}
async function read() {
const port = await getPort();
const reader = port.readable.getReader();
const {value, done} = await reader.read();
reader.releaseLock();
if (!done) {
return new TextDecoder().decode(value);
}
}
self.addEventListener('message', async function(event) {
await write(event.data);
});
async function readWrapper() {
if (port) {
self.postMessage(await read()); // this is a hack but i'm curious
}
}
setInterval(readWrapper, 500);

View File

@ -0,0 +1,23 @@
addEventListener('install', () => {
self.skipWaiting();
});
addEventListener('activate', () => {
self.clients.claim();
});
let resolver;
console.log("(Service Worker): Loaded!");
addEventListener('message', event => {
resolver(new Response(event.data,{status:200}));
console.log("(Service Worker): fufilling fetch, sending: ", event.data);
});
addEventListener('fetch', e => {
const u = new URL(e.request.url);
console.log("(Service Worker): Got fetch to: ", u);
if (u.pathname === '/read_serial/' || u.pathname === '/write_serial/') {
e.respondWith(new Promise(r => resolver = r));
}
});

View File

@ -1,42 +0,0 @@
let data;
let fileUploaded = false;
// Map the fake upload button to the real (hidden) upload button
document.addEventListener("DOMContentLoaded", function() {
const uploadButton = document.getElementById("uploadButton");
const fileInput = document.getElementById("fileInput");
const filePathDisplay = document.getElementById("filePath");
uploadButton.addEventListener("click", function() {
fileInput.click();
});
fileInput.addEventListener("change", function() {
const filePath = fileInput.value; // Get the file path from the file input
filePathDisplay.textContent = "Loaded " + filePath; // Display the file path
});
});
// Update fileUploaded flag when a file gets uploaded
document.getElementById('fileInput').addEventListener('change', function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
data = e.target.result;
fileUploaded = true;
};
reader.readAsText(file);
});
// Continuously poll to see if the file has been uploaded
function runAfterUpload() {
if (!fileUploaded) {
setTimeout(runAfterUpload, 100); // Check again after 100ms
return;
}
// Additional JavaScript code to run after file upload
// WebTerminal(data);
}
runAfterUpload();

View File

@ -0,0 +1,40 @@
let pyodide;
importScripts("https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js");
(async function() {
pyodide = await loadPyodide({ indexURL : "https://cdn.jsdelivr.net/pyodide/v0.25.1/full/" });
const response = await fetch('../assets/main.py');
pyodide.runPython(await response.text());
console.log("(Web Worker) Python loaded!");
})()
addEventListener('message', function(e) {
console.log("(Web Worker): Message received from main thread: ", e.data);
const output = pyodide.globals.get("capture")(e.data);
});
function blockingRequestToURL(url) {
const request = new XMLHttpRequest();
// `false` makes the request synchronous
request.open('GET', url, false);
request.send(null);
console.log('status', request.status);
return request.responseText;
}
function workerReadSerial() {
console.log("(Web Worker): workerReadSerial called");
postMessage({awaitingRead: true});
data = blockingRequestToURL('/read_serial/');
postMessage({awaitingRead: false});
return data;
}
function workerWriteSerial(data) {
console.log("(Web Worker): workerWriteSerial called with: ", data);
postMessage({awaitingWrite: true, writeData: data});
blockingRequestToURL('/write_serial/');
postMessage({awaitingWrite: false, writeData: undefined});
}

View File

@ -1,53 +1,75 @@
document.getElementById('connectButton').addEventListener('click', selectPort);
document.getElementById('runButton').addEventListener('click', webTerminal);
// serial
let port;
async function selectPort(){
await navigator.serial.requestPort();
async function connect() {
try {
const serialPort = await navigator.serial.requestPort();
await serialPort.open({ baudRate: 115200 });
port = serialPort;
console.log('Connected to serial device:', port);
} catch (error) {
console.error('Error connecting to serial device:', error);
}
}
// Start Web Worker for Serial API
globalThis.receiveBuffer = [];
const serialWorker = new Worker("../javascripts/serial.js");
function sendToSerialWorker(data){
console.log("Main -> Worker: ", data);
serialWorker.postMessage(data);
async function write(data) {
console.log("(Main) write() called with: ", data);
const writer = port.writable.getWriter();
await writer.write(new TextEncoder().encode(data));
await writer.releaseLock();
}
serialWorker.onmessage = (e) => {
console.log("Worker -> Main: ", e.data);
receiveBuffer.push(e.data);
async function read() {
console.log("(Main) read() called");
const reader = port.readable.getReader();
const {value, done} = await reader.read();
reader.releaseLock();
if (!done) {
return new TextDecoder().decode(value);
}
}
// workers
let serviceWorker;
navigator.serviceWorker.register('../javascripts/service-worker.js').then(function(registration) {
serviceWorker = registration.active;
if (!serviceWorker) {
location.reload();
}
});
// main loop
let awaitingRead = false;
let awaitingWrite = false;
let writeData;
const worker = new Worker('../javascripts/web-worker.js');
worker.onmessage = function(e) {
console.log("(Main) received message from web worker: ", e.data);
if ("awaitingRead" in e.data) {awaitingRead = e.data.awaitingRead};
if ("awaitingWrite" in e.data) {awaitingWrite = e.data.awaitingWrite};
if ("writeData" in e.data) {writeData = e.data.writeData};
};
// Main function for the Web Terminal
async function webTerminal(){
let pyodide = await loadPyodide();
// Load Manta.yaml into pyodide's file system
// pyodide.FS.writeFile("/manta.yaml", data, { encoding: "utf8" });
// Load micropip, setuptools, manta
await pyodide.loadPackage("micropip");
await pyodide.loadPackage("setuptools");
const micropip = pyodide.pyimport("micropip");
await micropip.install('../assets/manta-1.0.0-py3-none-any.whl');
pyodide.runPythonAsync(`
import asyncio
from js import sendToSerialWorker
from js import receiveBuffer
from manta import Manta
#m = Manta("/manta.yaml")
print("Sending read request")
sendToSerialWorker("R0000\\r\\n")
for _ in range(10):
print(len(receiveBuffer))
print(receiveBuffer)
await asyncio.sleep(0.1)
print("Python Complete")
`);
async function capture() {
console.log("(Main): Sending message to Web Worker: ", "");
worker.postMessage("");
}
async function checkForAwaitingInput() {
if (awaitingRead) {
let out = await read();
console.log("(Main): Sending message to Service Worker: ", out);
serviceWorker.postMessage(out);
}
if (awaitingWrite) {
let out = await write(writeData);
console.log("(Main): Sending message to Service Worker: ", out);
serviceWorker.postMessage(out);
}
}
setInterval(checkForAwaitingInput, 100);

View File

@ -1,20 +1,7 @@
## Open Serial Port
<button class="md-button" id="connectButton">Connect</button>
## Load Manta Configuration File
<button class="md-button" id="uploadButton">Upload File</button>
<input type="file" id="fileInput" accept=".yaml,.yml" style="display: none">
<div id="filePath"></div>
## Run Main Thread
<button class="md-button" id="runButton">Run!</button>
<!-- Load Javascript -->
<script src="../javascripts/webTerminal.js"></script>
<script src="../javascripts/serial.js"></script>
<!-- <script src="../javascripts/upload.js"></script> -->
<script src="https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js"></script>
<button onclick='connect()' class="md-button">Connect</button>
<button onclick='console.log("File Upload Callback")' class="md-button">Upload Configuration File</button>
<button onclick='capture()' class="md-button">Capture</button>
<input type="file" id="fileInput" accept=".yaml,.yml,.json" style="display: none">
<script src="../javascripts/webTerminal.js" type="text/javascript"></script>
<!-- <iframe src="https://app.surfer-project.org/?load_url=https://app.surfer-project.org/picorv32.vcd&amp;startup_commands=show_quick_start;module_add%20testbench.top;toggle_menu" ,="" style="width: 100%; height: 500px"></iframe> -->