feat: read serial port and play video
This commit is contained in:
commit
ad839bbdd4
9 changed files with 243 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# config
|
||||
/tags.js
|
||||
/video/*
|
||||
!/video/README.md
|
||||
|
||||
# dev
|
||||
node_modules/
|
||||
7
README.md
Normal file
7
README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# usr-tag-interaction
|
||||
## Usage
|
||||
1. Put video files in `video/` directory
|
||||
2. Copy `tags.example.js` to `tags.js` and edit it
|
||||
to configure the tag ids and the corresponding video urls
|
||||
3. Open `index.html` with [browser that supports Web Serial API](https://caniuse.com/?search=serial)
|
||||
(Chrome, Edge, Opera as of 2024/08/03)
|
||||
29
index.html
Normal file
29
index.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0 auto;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-size: 48px;
|
||||
}
|
||||
video {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
video.playing {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message">Click me!</div>
|
||||
<section id="video-root" />
|
||||
<script src="tags.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
155
index.js
Normal file
155
index.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/** 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() ?? ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
7
jsconfig.json
Normal file
7
jsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es2020"],
|
||||
"checkJs": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
22
package-lock.json
generated
Normal file
22
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "usr-tag-interaction",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "usr-tag-interaction",
|
||||
"devDependencies": {
|
||||
"@types/dom-serial": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@types+dom-serial@1.0.6/node_modules/@types/dom-serial": {
|
||||
"version": "1.0.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dom-serial": {
|
||||
"resolved": "node_modules/.pnpm/@types+dom-serial@1.0.6/node_modules/@types/dom-serial",
|
||||
"link": true
|
||||
}
|
||||
}
|
||||
}
|
||||
6
package.json
Normal file
6
package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "usr-tag-interaction",
|
||||
"devDependencies": {
|
||||
"@types/dom-serial": "^1.0.6"
|
||||
}
|
||||
}
|
||||
9
tags.example.js
Normal file
9
tags.example.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* A map from tag id (HEX string) to url of the corresponding video
|
||||
* @type {{[tagId: string]: string}}
|
||||
*/
|
||||
var tags = {
|
||||
'3E033E03': 'video/CAT1.mp4',
|
||||
'ABCD1234': 'video/CAT2.mp4',
|
||||
'BEEFCAFE': 'video/CAT3.mp4',
|
||||
};
|
||||
1
video/README.md
Normal file
1
video/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Put your video files here!
|
||||
Loading…
Add table
Add a link
Reference in a new issue