Use ES6 modules
parent
200fc7629b
commit
4e41910dc1
@ -0,0 +1,10 @@
|
|||||||
|
import * as globals from './globals.js';
|
||||||
|
import { setupEventListeners } from './eventListeners.js';
|
||||||
|
import { updateSerialSelect } from './uiHelpers.js';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const ports = await navigator.serial.getPorts();
|
||||||
|
globals.setSerialPorts(ports);
|
||||||
|
updateSerialSelect(ports);
|
||||||
|
setupEventListeners();
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import * as globals from './globals.js';
|
||||||
|
import { connectToSerialPort, transmitContents } from './serialCommunication.js';
|
||||||
|
import { scrollToBottom, updateSerialSelect, onPortDisconnect } from './uiHelpers.js';
|
||||||
|
|
||||||
|
const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3]);
|
||||||
|
|
||||||
|
export function setupEventListeners() {
|
||||||
|
globals.addPort.addEventListener('click', async () => {
|
||||||
|
const port = await navigator.serial.requestPort();
|
||||||
|
globals.serialPorts.push(port);
|
||||||
|
updateSerialSelect(globals.serialPorts);
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.autoscrollCheckbox.addEventListener('change', (e) => {
|
||||||
|
globals.setAutoscroll(e.target.checked);
|
||||||
|
if (globals.autoscroll) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.clearButton.addEventListener('click', () => {
|
||||||
|
while (globals.scrollableElement.firstChild) {
|
||||||
|
globals.scrollableElement.removeChild(globals.scrollableElement.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.connectButton.addEventListener('click', async () => {
|
||||||
|
console.log(globals.select)
|
||||||
|
const selectedPort = globals.serialPorts[globals.select.selectedIndex];
|
||||||
|
console.log("selected port: ", selectedPort)
|
||||||
|
const baudRate = Math.round(globals.baud.value);
|
||||||
|
console.log("Baud Rate: ", baudRate)
|
||||||
|
if (!globals.isPortConnected) {
|
||||||
|
await connectToSerialPort(selectedPort, baudRate);
|
||||||
|
} else {
|
||||||
|
globals.setPortConnected(false);
|
||||||
|
onPortDisconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.refreshPorts.addEventListener('click', async () => {
|
||||||
|
const ports = await navigator.serial.getPorts();
|
||||||
|
console.log(ports)
|
||||||
|
globals.setSerialPorts(ports)
|
||||||
|
updateSerialSelect(globals.serialPorts)
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.transmitButton.addEventListener('click', async (event) => {
|
||||||
|
transmitContents(globals.transmitInput.innerText)
|
||||||
|
globals.transmitInput.innerHTML = ''
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.transmitInput.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'c' && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("Sending interrupt ", uPyKeyboardInterrupt)
|
||||||
|
globals.writer.write(uPyKeyboardInterrupt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globals.transmitInput.addEventListener('keyup', (event) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
transmitContents(globals.transmitInput.innerText);
|
||||||
|
globals.transmitInput.innerHTML = ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
export const addDeviceMessage = `Add a device... `;
|
||||||
|
export const addPort = document.getElementById('add-port');
|
||||||
|
export const autoscrollCheckbox = document.getElementById('autoscroll-checkbox');
|
||||||
|
export const baud = document.getElementById('baud');
|
||||||
|
export const clearButton = document.getElementById('clear-button');
|
||||||
|
export const connectButton = document.getElementById('connect-button');
|
||||||
|
export const refreshPorts = document.getElementById('refresh-ports');
|
||||||
|
export const scrollableElement = document.getElementById('scrollable-element');
|
||||||
|
export const select = document.getElementById('serial-select');
|
||||||
|
export const transmitInput = document.querySelector('div[contenteditable="true"]');
|
||||||
|
export const transmitButton = document.getElementById('transmit-button');
|
||||||
|
export const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3]);
|
||||||
|
|
||||||
|
export let isPortConnected = false;
|
||||||
|
export let autoscroll = true;
|
||||||
|
export let serialPorts = [];
|
||||||
|
export let encoder;
|
||||||
|
export let reader;
|
||||||
|
export let writer;
|
||||||
|
|
||||||
|
export function setPortConnected(isConnected) {
|
||||||
|
isPortConnected = isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAutoscroll(isSet) {
|
||||||
|
autoscroll = isSet;
|
||||||
|
console.log('Set autoscroll: ', autoscroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSerialPorts(ports) {
|
||||||
|
serialPorts = ports
|
||||||
|
console.log('Set ports: ', serialPorts)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEncoder() {
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEncoder(newEncoder) {
|
||||||
|
encoder = newEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReader() {
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setReader(newReader) {
|
||||||
|
reader = newReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWriter() {
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWriter(newWriter) {
|
||||||
|
writer = newWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose some globals for debuggin in webconsole
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.wsc = {
|
||||||
|
isPortConnected,
|
||||||
|
autoscroll,
|
||||||
|
serialPorts,
|
||||||
|
};
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
// Globals
|
|
||||||
const addDeviceMessage = `Add a device... `
|
|
||||||
const addPort = document.getElementById('add-port')
|
|
||||||
const autoscrollCheckbox = document.getElementById('autoscroll-checkbox');
|
|
||||||
const baud = document.getElementById('baud');
|
|
||||||
const clearButton = document.getElementById('clear-button');
|
|
||||||
const connectButton = document.getElementById('connect-button')
|
|
||||||
const refreshPorts = document.getElementById('refresh-ports');
|
|
||||||
const scrollableElement = document.getElementById('scrollable-element');
|
|
||||||
const select = document.getElementById('serial-select');
|
|
||||||
const transmitInput = document.querySelector('div[contenteditable="true"]');
|
|
||||||
const transmitButton = document.getElementById('transmit-button');
|
|
||||||
const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3])
|
|
||||||
|
|
||||||
let isPortConnected = false;
|
|
||||||
let controller;
|
|
||||||
let autoscroll = true;
|
|
||||||
let serialPorts = [];
|
|
||||||
|
|
||||||
let encoder;
|
|
||||||
let reader;
|
|
||||||
let writer;
|
|
||||||
|
|
||||||
// Event Listeners
|
|
||||||
addPort.addEventListener('click', async () => {
|
|
||||||
const port = await navigator.serial.requestPort();
|
|
||||||
serialPorts.push(port);
|
|
||||||
updateSerialSelect(serialPorts);
|
|
||||||
});
|
|
||||||
|
|
||||||
autoscrollCheckbox.addEventListener('change', (e) => {
|
|
||||||
autoscroll = e.target.checked;
|
|
||||||
if (autoscroll) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearButton.addEventListener('click', () => {
|
|
||||||
while (scrollableElement.firstChild) {
|
|
||||||
scrollableElement.removeChild(scrollableElement.firstChild);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connectButton.addEventListener('click', async () => {
|
|
||||||
const selectedPort = serialPorts[select.selectedIndex]
|
|
||||||
const baudRate = Math.round(baud.value)
|
|
||||||
if (!isPortConnected) {
|
|
||||||
await connectToSerialPort(selectedPort, baudRate);
|
|
||||||
} else {
|
|
||||||
isPortConnected = false;
|
|
||||||
onPortDisconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshPorts.addEventListener('click', async () => {
|
|
||||||
ports = await navigator.serial.getPorts();
|
|
||||||
serialPorts = ports
|
|
||||||
updateSerialSelect(ports)
|
|
||||||
});
|
|
||||||
|
|
||||||
transmitButton.addEventListener('click', async (event) => {
|
|
||||||
transmitContents(transmitInput.innerText)
|
|
||||||
transmitInput.innerHTML = ''
|
|
||||||
});
|
|
||||||
|
|
||||||
transmitInput.addEventListener('keydown', (event) => {
|
|
||||||
if (event.key === 'c' && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
console.log("Sending interrupt ", uPyKeyboardInterrupt)
|
|
||||||
writer.write(uPyKeyboardInterrupt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
transmitInput.addEventListener('keyup', (event) => {
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
|
||||||
transmitContents(transmitInput.innerText);
|
|
||||||
transmitInput.innerHTML = ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
function transmitContents(input) {
|
|
||||||
encoded_string = encoder.encode(input + '\r')
|
|
||||||
console.log("Binary Contents: ", encoded_string)
|
|
||||||
writer.write(encoded_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onPortConnect() {
|
|
||||||
connectButton.classList.remove('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
|
|
||||||
connectButton.classList.add('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
|
|
||||||
connectButton.textContent = 'Disconnect';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPortDisconnect() {
|
|
||||||
connectButton.classList.remove('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
|
|
||||||
connectButton.classList.add('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
|
|
||||||
connectButton.textContent = 'Connect';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function buildPortOption(port) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const info = port.getInfo();
|
|
||||||
if (info && 'usbVendorId' in info && 'usbProductId' in info) {
|
|
||||||
const { usbVendorId, usbProductId } = info;
|
|
||||||
option.text = `Device ${usbVendorId}:${usbProductId}`;
|
|
||||||
} else {
|
|
||||||
console.error('getInfo() did not return expected properties:', info);
|
|
||||||
option.text = 'Unknown Device';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error retrieving port information:', error);
|
|
||||||
option.text = 'Unknown Device';
|
|
||||||
}
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addText(text) {
|
|
||||||
const newText = document.createElement('p');
|
|
||||||
newText.textContent = `${new Date().toLocaleTimeString()} ${text}`;
|
|
||||||
scrollableElement.appendChild(newText);
|
|
||||||
|
|
||||||
if (autoscroll) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToBottom() {
|
|
||||||
scrollableElement.scrollTop = scrollableElement.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async Functions
|
|
||||||
async function connectToSerialPort(port, baud) {
|
|
||||||
await port.open({ baudRate: baud });
|
|
||||||
onPortConnect()
|
|
||||||
|
|
||||||
let buffer = ''
|
|
||||||
// The TextDecoderStream interface of the Encoding API converts a stream of text in a binary encoding,
|
|
||||||
// such as UTF-8 etc., to a stream of strings
|
|
||||||
// const textEncoder = new TextEncoderStream();
|
|
||||||
// const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
|
||||||
// writer = textEncoder.writable.getWriter();
|
|
||||||
|
|
||||||
encoder = new TextEncoder()
|
|
||||||
writer = port.writable.getWriter();
|
|
||||||
|
|
||||||
const textDecoder = new TextDecoderStream();
|
|
||||||
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
|
|
||||||
reader = textDecoder.readable.getReader();
|
|
||||||
|
|
||||||
controller = new AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
try {
|
|
||||||
isPortConnected = true;
|
|
||||||
while (isPortConnected) {
|
|
||||||
const { value, done } = await reader.read({ signal });
|
|
||||||
if (done || !port.readable) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer += value;
|
|
||||||
|
|
||||||
while (buffer.includes('\n')) {
|
|
||||||
const newlineIndex = buffer.indexOf('\n');
|
|
||||||
const line = buffer.slice(0, newlineIndex);
|
|
||||||
buffer = buffer.slice(newlineIndex + 1);
|
|
||||||
|
|
||||||
addText(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading data from serial port:', error);
|
|
||||||
} finally {
|
|
||||||
// Port cleanup
|
|
||||||
controller.abort();
|
|
||||||
writer.releaseLock();
|
|
||||||
reader.releaseLock();
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// writer.close();
|
|
||||||
// await writableStreamClosed;
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error encountered in writeableSteamClosed:", error)
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
|
||||||
reader.cancel();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error encountered in readableStreamClosed:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await readableStreamClosed;
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await readableStreamClosed;
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await port.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error encountered closing port:", error);
|
|
||||||
}
|
|
||||||
onPortDisconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateSerialSelect(ports) {
|
|
||||||
if (ports.length < 1) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.text = addDeviceMessage;
|
|
||||||
select.innerHTML = ''
|
|
||||||
select.appendChild(option)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
select.innerHTML = ''
|
|
||||||
ports.forEach(port => {
|
|
||||||
const option = buildPortOption(port)
|
|
||||||
select.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
@ -0,0 +1,73 @@
|
|||||||
|
import * as globals from './globals.js';
|
||||||
|
import { onPortConnect, onPortDisconnect, addText } from './uiHelpers.js';
|
||||||
|
|
||||||
|
export async function connectToSerialPort(port, baud) {
|
||||||
|
console.log("Connecting to port: ", port);
|
||||||
|
await port.open({ baudRate: baud });
|
||||||
|
onPortConnect()
|
||||||
|
|
||||||
|
let buffer = ''
|
||||||
|
globals.setEncoder(new TextEncoder());
|
||||||
|
globals.setWriter(port.writable.getWriter());
|
||||||
|
|
||||||
|
const textDecoder = new TextDecoderStream();
|
||||||
|
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
|
||||||
|
globals.setReader(textDecoder.readable.getReader());
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
try {
|
||||||
|
globals.setPortConnected(true);
|
||||||
|
while (globals.isPortConnected) {
|
||||||
|
const { value, done } = await globals.reader.read({ signal });
|
||||||
|
if (done || !port.readable) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer += value;
|
||||||
|
|
||||||
|
while (buffer.includes('\n')) {
|
||||||
|
const newlineIndex = buffer.indexOf('\n');
|
||||||
|
const line = buffer.slice(0, newlineIndex);
|
||||||
|
buffer = buffer.slice(newlineIndex + 1);
|
||||||
|
|
||||||
|
addText(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading data from serial port:', error);
|
||||||
|
} finally {
|
||||||
|
// Port cleanup
|
||||||
|
controller.abort();
|
||||||
|
globals.writer.releaseLock();
|
||||||
|
globals.reader.releaseLock();
|
||||||
|
|
||||||
|
try {
|
||||||
|
globals.reader.cancel();
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readableStreamClosed;
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readableStreamClosed;
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await port.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error encountered closing port:", error);
|
||||||
|
}
|
||||||
|
onPortDisconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transmitContents(input) {
|
||||||
|
const encoded_string = globals.encoder.encode(input + '\r')
|
||||||
|
console.log("Binary Contents: ", encoded_string)
|
||||||
|
globals.writer.write(encoded_string)
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import * as globals from './globals.js';
|
||||||
|
|
||||||
|
export function onPortConnect() {
|
||||||
|
globals.connectButton.classList.remove('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
|
||||||
|
globals.connectButton.classList.add('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
|
||||||
|
globals.connectButton.textContent = 'Disconnect';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPortDisconnect() {
|
||||||
|
globals.connectButton.classList.remove('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
|
||||||
|
globals.connectButton.classList.add('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
|
||||||
|
globals.connectButton.textContent = 'Connect';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPortOption(port) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = port;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = port.getInfo();
|
||||||
|
if (info && 'usbVendorId' in info && 'usbProductId' in info) {
|
||||||
|
const { usbVendorId, usbProductId } = info;
|
||||||
|
option.text = `Device ${usbVendorId}:${usbProductId}`;
|
||||||
|
} else {
|
||||||
|
console.error('getInfo() did not return expected properties:', info);
|
||||||
|
option.text = 'Unknown Device';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving port information:', error);
|
||||||
|
option.text = 'Unknown Device';
|
||||||
|
}
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addText(text) {
|
||||||
|
const newText = document.createElement('p');
|
||||||
|
newText.textContent = `${new Date().toLocaleTimeString()} ${text}`;
|
||||||
|
globals.scrollableElement.appendChild(newText);
|
||||||
|
|
||||||
|
if (globals.autoscroll) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollToBottom() {
|
||||||
|
globals.scrollableElement.scrollTop = globals.scrollableElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSerialSelect(ports) {
|
||||||
|
if (ports.length < 1) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.text = addDeviceMessage;
|
||||||
|
globals.select.innerHTML = ''
|
||||||
|
globals.select.appendChild(option)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globals.select.innerHTML = ''
|
||||||
|
ports.forEach(port => {
|
||||||
|
const option = buildPortOption(port)
|
||||||
|
globals.select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue