156 lines
4.6 KiB
JavaScript
156 lines
4.6 KiB
JavaScript
|
|
/** Video container element */
|
||
|
|
const videoRoot = /** @type {HTMLElement} */
|
||
|
|
(document.getElementById('video-root'));
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A map from tag id to video element
|
||
|
|
* {[$tagId]: <video><source src="$src"/></video>}
|
||
|
|
*/
|
||
|
|
const videos = Object.fromEntries(Object.entries(tags).map(([tagId, src]) => {
|
||
|
|
const video = document.createElement('video');
|
||
|
|
const source = document.createElement('source');
|
||
|
|
source.setAttribute('src', src);
|
||
|
|
video.appendChild(source);
|
||
|
|
videoRoot.appendChild(video);
|
||
|
|
return [tagId, video];
|
||
|
|
}));
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Current playing video element
|
||
|
|
* @type {HTMLVideoElement|null}
|
||
|
|
*/
|
||
|
|
let currentVideo = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Callback function when a tag is read
|
||
|
|
* @param {string} tagId
|
||
|
|
*/
|
||
|
|
function onTag(tagId) {
|
||
|
|
const nextVideo = videos[tagId];
|
||
|
|
// ignore unknown tag
|
||
|
|
if (nextVideo == null) {
|
||
|
|
console.log('unknown tag', tagId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// ignore if the same tag is used
|
||
|
|
if (nextVideo === currentVideo) return;
|
||
|
|
// play the next video from the beginning
|
||
|
|
nextVideo.currentTime = 0;
|
||
|
|
nextVideo.play();
|
||
|
|
nextVideo.classList.add('playing');
|
||
|
|
// stop the current video
|
||
|
|
currentVideo?.pause();
|
||
|
|
currentVideo?.classList.remove('playing');
|
||
|
|
// update video reference
|
||
|
|
currentVideo = nextVideo;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Open and read the serial port
|
||
|
|
* @param {SerialPort} port
|
||
|
|
*/
|
||
|
|
async function openPort(port, baudRate=9600) {
|
||
|
|
// open the serial port
|
||
|
|
await port.open({baudRate});
|
||
|
|
// reader of byte stream from the serial port
|
||
|
|
const reader = port.readable.getReader({mode: 'byob'});
|
||
|
|
|
||
|
|
// buffer for reading the serial port
|
||
|
|
const bufferSize = 16;
|
||
|
|
let buffer = new ArrayBuffer(bufferSize);
|
||
|
|
let offset = 0;
|
||
|
|
try {
|
||
|
|
while (true) {
|
||
|
|
// read bytes from the serial port to buffer
|
||
|
|
const {value, done} = await reader.read(
|
||
|
|
new Uint8Array(buffer, offset, bufferSize - offset),
|
||
|
|
);
|
||
|
|
|
||
|
|
// no more message
|
||
|
|
if (done) {
|
||
|
|
showMessage('The serial port is closed');
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// update buffer and offset
|
||
|
|
buffer = value.buffer;
|
||
|
|
offset += value.byteLength;
|
||
|
|
|
||
|
|
// message format:
|
||
|
|
// - (u8) N = tag id length
|
||
|
|
// - (u8[N]) tag id
|
||
|
|
/** pointer (array index) to the message in buffer */
|
||
|
|
let ptr = 0;
|
||
|
|
// if tag id length of the next message presents
|
||
|
|
while (offset > ptr) {
|
||
|
|
const tagIdLength = new DataView(buffer).getUint8(0);
|
||
|
|
// break if the message is not completely read
|
||
|
|
const ptrNext = ptr + 1 + tagIdLength;
|
||
|
|
if (offset < ptrNext) break;
|
||
|
|
// convert tag id bytes to HEX string
|
||
|
|
const tagId = Array.from(
|
||
|
|
new Uint8Array(buffer, ptr + 1, tagIdLength),
|
||
|
|
// %02X
|
||
|
|
x => x.toString(16).toUpperCase().padStart(2, '0'),
|
||
|
|
).join('');
|
||
|
|
// handle the tag
|
||
|
|
onTag(tagId);
|
||
|
|
// try to read the next message
|
||
|
|
ptr = ptrNext;
|
||
|
|
}
|
||
|
|
// move the remaining bytes in the buffer to the beginning
|
||
|
|
// if any message is read
|
||
|
|
if (ptr > 0) {
|
||
|
|
// remaining byte count
|
||
|
|
const remainingLength = offset - ptr;
|
||
|
|
// copy bytes
|
||
|
|
if (remainingLength > 0) {
|
||
|
|
new Uint8Array(buffer)
|
||
|
|
.set(new Uint8Array(buffer, ptr, remainingLength));
|
||
|
|
}
|
||
|
|
// update offset
|
||
|
|
offset = remainingLength;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
showMessage(
|
||
|
|
'Error occurred while reading serial port:\n'
|
||
|
|
+ (err?.toString() ?? ''),
|
||
|
|
);
|
||
|
|
console.error(err);
|
||
|
|
} finally {
|
||
|
|
reader.releaseLock();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** HTML element for showing message */
|
||
|
|
const msgElm = /** @type {HTMLElement} */ (document.getElementById('message'));
|
||
|
|
/** Show message on page */
|
||
|
|
const showMessage = (/** @type {string} */ msg) => msgElm.textContent = msg;
|
||
|
|
|
||
|
|
// check if this browser supports Web Serial API
|
||
|
|
if (navigator.serial == null) {
|
||
|
|
showMessage('Your browser does not support Web Serial API!');
|
||
|
|
} else {
|
||
|
|
// click body to select serial port to listen
|
||
|
|
// this is required since videos cannot be played without user's interaction
|
||
|
|
document.body.addEventListener('click', async function onBodyClicked() {
|
||
|
|
// get serial port
|
||
|
|
try {
|
||
|
|
// try to get serial ports
|
||
|
|
const ports = await navigator.serial.getPorts();
|
||
|
|
// choose the first port (FIXME) if availble port presents
|
||
|
|
// or request user to select a port
|
||
|
|
const port = ports[0] ?? await navigator.serial.requestPort();
|
||
|
|
// unregister body onClick listener
|
||
|
|
document.body.removeEventListener('click', onBodyClicked);
|
||
|
|
// remove message from page
|
||
|
|
showMessage('');
|
||
|
|
// start handling the messages from the serial port
|
||
|
|
openPort(port);
|
||
|
|
} catch(err) {
|
||
|
|
showMessage('Failed to access serial port:\n' + (err?.toString() ?? ''));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|