8000 Add web serial camera app · arduino/ArduinoCore-mbed@ea9c545 · GitHub
[go: up one dir, main page]

Skip to content

Commit ea9c545

Browse files
committed
Add web serial camera app
1 parent 229f167 commit ea9c545

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
const connectButton = document.getElementById('connect');
2+
const refreshButton = document.getElementById('refresh');
3+
const startButton = document.getElementById('start');
4+
const disconnectButton = document.getElementById('disconnect');
5+
const canvas = document.getElementById('bitmapCanvas');
6+
const ctx = canvas.getContext('2d');
7+
8+
const UserActionAbortError = 8;
9+
const ArduinoUSBVendorId = 0x2341;
10+
11+
const imageWidth = 320; // Adjust this value based on your bitmap width
12+
const imageHeight = 240; // Adjust this value based on your bitmap height
13+
const bytesPerPixel = 1; // Adjust this value based on your bitmap format
14+
// const mode = 'RGB565'; // Adjust this value based on your bitmap format
15+
const totalBytes = imageWidth * imageHeight * bytesPerPixel;
16+
17+
// Set the buffer size to the total bytes. This allows to read the entire bitmap in one go.
18+
const bufferSize = Math.min(totalBytes, 16 * 1024 * 1024); // Max buffer size is 16MB
19+
const baudRate = 115200; // Adjust this value based on your device's baud rate
20+
const dataBits = 8; // Adjust this value based on your device's data bits
21+
const stopBits = 2; // Adjust this value based on your device's stop bits
22+
23+
let currentPort, currentReader;
24+
25+
async function requestSerialPort(){
26+
try {
27+
// Request a serial port
28+
const port = await navigator.serial.requestPort({ filters: [{ usbVendorId: ArduinoUSBVendorId }] });
29+
currentPort = port;
30+
return port;
31+
} catch (error) {
32+
if(error.code != UserActionAbortError){
33+
console.log(error);
34+
}
35+
return null;
36+
}
37+
}
38+
39+
async function autoConnect(){
40+
if(currentPort){
41+
console.log('🔌 Already connected to a serial port.');
42+
return false;
43+
}
44+
45+
// Get all serial ports the user has previously granted the website access to.
46+
const ports = await navigator.serial.getPorts();
47+
48+
for (const port of ports) {
49+
console.log('👀 Serial port found with VID: 0x' + port.getInfo().usbVendorId.toString(16));
50+
if(port.getInfo().usbVendorId === ArduinoUSBVendorId){
51+
currentPort = port;
52+
return await connectSerial(currentPort);
53+
}
54+
}
55+
return false;
56+
}
57+
58+
async function connectSerial(port, baudRate = 115200, dataBits = 8, stopBits = 2, bufferSize = 4096) {
59+
try {
60+
// If the port is already open, close it
61+
if (port.readable) await port.close();
62+
await port.open({ baudRate: baudRate, parity: "even", dataBits: dataBits, stopBits: stopBits, bufferSize: bufferSize });
63+
console.log('✅ Connected to serial port.');
64+
return true;
65+
} catch (error) {
66+
return false;
67+
}
68+
}
69+
70+
71+
async function readBytes(port, numBytes, timeout = null){
72+
if(port.readable.locked){
73+
console.log('🔒 Stream is already locked. Ignoring request...');
74+
return null;
75+
}
76+
77+
const bytesRead = new Uint8Array(numBytes);
78+
let bytesReadIdx = 0;
79+
let keepReading = true;
80+
81+
// As long as the errors are non-fatal, a new ReadableStream is created automatically and hence port.readable is non-null.
82+
// If a fatal error occurs, such as the serial device being removed, then port.readable becomes null.
83+
84+
while (port.readable && keepReading) {
85+
const reader = port.readable.getReader();
86+
currentReader = reader;
87+
let timeoutID = null;
88+
let count = 0;
89+
90+
try {
91+
while (bytesReadIdx < numBytes) {
92+
if(timeout){
93+
timeoutID = setTimeout(() => {
94+
console.log('⌛️ Timeout occurred while reading.');
95+
if(port.readable) reader?.cancel();
96+
}, timeout);
97+
}
98+
99+
const { value, done } = await reader.read();
100+
if(timeoutID) clearTimeout(timeoutID);
101+
102+
if(value){
103+
for (const byte of value) {
104+
bytesRead[bytesReadIdx++] = byte;
105+
}
106+
// count += value.byteLength;
107+
// console.log(`Read ${value.byteLength} (Total: ${count}) out of ${numBytes} bytes.}`);
108+
}
109+
110+
if (done) {
111+
// |reader| has been canceled.
112+
console.log('🚫 Reader has been canceled');
113+
break;
114+
}
115+
}
116+
117+
} catch (error) {
118+
// Handle |error|...
119+
console.log('💣 Error occurred while reading: ');
120+
console.log(error);
121+
} finally {
122+
keepReading = false;
123+
// console.log('🔓 Releasing reader lock...');
124+
reader?.releaseLock();
125+
currentReader = null;
126+
}
127+
}
128+
return bytesRead;
129+
}
130+
131+
function renderBitmap(bytes, width, height) {
132+
canvas.width = width;
133+
canvas.height = height;
134+
const BYTES_PER_ROW = width * bytesPerPixel;
135+
const BYTES_PER_COL = height * bytesPerPixel;
136+
137+
const imageData = ctx.createImageData(canvas.width, canvas.height);
138+
const data = imageData.data;
139+
140+
for (let row = 0; row < BYTES_PER_ROW; row++) {
141+
for (let col = 0; col < BYTES_PER_COL; col++) {
142+
const byte = bytes[row * BYTES_PER_COL + col];
143+
const grayscaleValue = byte;
144+
145+
const idx = (row * BYTES_PER_COL + col) * 4;
146+
data[idx] = grayscaleValue; // Red channel
147+
data[idx + 1] = grayscaleValue; // Green channel
148+
data[idx + 2] = grayscaleValue; // Blue channel
149+
data[idx + 3] = 255; // Alpha channel (opacity)
150+
}
151+
}
152+
ctx.clearRect(0, 0, canvas.width, canvas.height);
153+
ctx.putImageData(imageData, 0, 0);
154+
}
155+
156+
async function requestFrame(port){
157+
if(!port?.writable) {
158+
console.log('🚫 Port is not writable. Ignoring request...');
159+
return;
160+
}
161+
// console.log('Writing 1 to the serial port...');
162+
// Write a 1 to the serial port
163+
const writer = port.writable.getWriter();
164+
await writer.write(new Uint8Array([1]));
165+
await writer.close();
166+
}
167+
async function renderStream(){
168+
while(true && currentPort){
169+
await renderFrame(currentPort);
170+
}
171+
}
172+
173+
async function renderFrame(port){
174+
if(!port) return;
175+
const bytes = await getFrame(port);
176+
if(!bytes) return false; // Nothing to render
177+
// console.log(`Reading done ✅. Rendering image...`);
178+
// Render the bytes as a grayscale bitmap
179+
renderBitmap(bytes, imageWidth, imageHeight);
180+
return true;
181+
}
182+
183+
async function getFrame(port) {
184+
if(!port) return;
185+
186+
await requestFrame(port);
187+
// console.log(`Trying to read ${totalBytes} bytes...`);
188+
// Read the given amount of bytes
189+
return await readBytes(port, totalBytes, 2000);
190+
}
191+
192+
async function disconnectSerial(port) {
193+
if(!port) return;
194+
try {
195+
currentPort = null;
196+
await currentReader?.cancel();
197+
await port.close();
198+
console.log('🔌 Disconnected from serial port.');
199+
} catch (error) {
200+
console.error('💣 Error occurred while disconnecting: ' + error.message);
201+
};
202+
}
203+
204+
startButton.addEventListener('click', renderStream);
205+
connectButton.addEventListener('click', async () => {
206+
currentPort = await requestSerialPort();
207+
if(await connectSerial(currentPort, baudRate, dataBits, stopBits, bufferSize)){
208+
renderStream();
209+
}
210+
});
211+
disconnectButton.addEventListener('click', () => disconnectSerial(currentPort));
212+
refreshButton.addEventListener('click', () => {
213+
renderFrame(currentPort);
214+
});
215+
216+
navigator.serial.addEventListener("connect", (e) => {
217+
// Connect to `e.target` or add it to a list of available ports.
218+
console.log('🔌 Serial port became available. VID: 0x' + e.target.getInfo().usbVendorId.toString(16));
219+
autoConnect().then((connected) => {
220+
if(connected){
221+
renderStream();
222+
};
223+
});
224+
});
225+
226+
navigator.serial.addEventListener("disconnect", (e) => {
227+
// Remove `e.target` from the list of available ports.
228+
console.log('❌ Serial port lost. VID: 0x' + e.target.getInfo().usbVendorId.toString(16));
229+
currentPort = null;
230+
});
231+
232+
// On page load event, try to connect to the serial port
233+
window.addEventListener('load', async () => {
234+
console.log('🚀 Page loaded. Trying to connect to serial port...');
235+
setTimeout(() => {
236+
autoConnect().then((connected) => {
237+
if (connected) {
238+
renderStream();
239+
};
240+
});
241+
}, 1000);
242+
});
243+
244+
if (!("serial" in navigator)) {
245+
alert("The Web Serial API is not supported in your browser.");
246+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function convertRGB565ToRGB888(pixelValue) {
2+
// RGB565
3+
let r = (pixelValue >> (6 + 5)) & 0x1F;
4+
let g = (pixelValue >> 5) & 0x3F;
5+
let b = pixelValue & 0x1F;
6+
// RGB888 - amplify
7+
r <<= 3;
8+
g <<= 2;
9+
b <<= 3;
10+
return [r, g, b];
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Web Serial Bitmap Reader</title>
7+
<style>
8+
#main-container {
9+
display: flex;
10+
flex-direction: column;
11+
align-items: center;
12+
gap: 1rem;
13+
}
14+
</style>
15+
</head>
16+
<body>
17+
<div id="main-container">
18+
<canvas id="bitmapCanvas"></canvas>
19+
<div id="controls">
20+
<button id="connect">Connect</button>
21+
<button id="disconnect">Disconnect</button>
22+
<button id="refresh">Refresh</button>
23+
<button id="start">Start</button>
24+
</div>
25+
</div>
26+
<script src="app.js"></script>
27+
</body>
28+
</html>

0 commit comments

Comments
 (0)
0