Skip to content

Commit c656793

Browse files
committed
Add more documentation
1 parent d7d0819 commit c656793

File tree

3 files changed

+151
-58
lines changed

3 files changed

+151
-58
lines changed

libraries/Camera/extras/WebSerialCamera/imageDataProcessor.js

+3-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/**
22
* Represents an image data processor that converts raw image data to a specified pixel format.
3-
* This could be turned into a transform stream and be used in the serial connection handler.
4-
* See example here: https://github.com/mdn/dom-examples/blob/main/streams/png-transform-stream/png-transform-stream.js
53
*
64
* @author Sebastian Romero
75
*/
@@ -27,14 +25,13 @@ class ImageDataProcessor {
2725

2826
/**
2927
* Creates a new instance of the imageDataProcessor class.
30-
* @param {CanvasRenderingContext2D} context - The 2D rendering context of the canvas.
3128
* @param {string|null} mode - The image mode of the image data processor. (Optional)
3229
* Possible values: RGB565, GRAYSCALE, RGB888, BAYER
3330
* @param {number|null} width - The width of the image data processor. (Optional)
3431
* @param {number|null} height - The height of the image data processor. (Optional)
3532
*/
3633
constructor(mode = null, width = null, height = null) {
37-
if(mode) this.setMode(mode);
34+
if(mode) this.setImageMode(mode);
3835
if(width && height) this.setResolution(width, height);
3936
}
4037

@@ -44,7 +41,7 @@ class ImageDataProcessor {
4441
*
4542
* @param {string} mode - The image mode of the image data processor.
4643
*/
47-
setMode(mode) {
44+
setImageMode(mode) {
4845
this.mode = mode;
4946
this.bytesPerPixel = this.pixelFormatInfo[mode].bytesPerPixel;
5047
}
@@ -69,15 +66,6 @@ class ImageDataProcessor {
6966
return this.width * this.height * this.bytesPerPixel;
7067
}
7168

72-
/**
73-
* Checks if the image data processor is configured.
74-
* This is true if the image mode and resolution are set.
75-
* @returns {boolean} True if the image data processor is configured, false otherwise.
76-
*/
77-
isConfigured() {
78-
return this.mode && this.width && this.height;
79-
}
80-
8169
/**
8270
* Resets the state of the imageDataProcessor.
8371
* This resets the image mode, resolution, and bytes per pixel.
@@ -152,7 +140,7 @@ class ImageDataProcessor {
152140
* @param {Uint8Array} bytes - The raw byte array containing the image data.
153141
* @returns {Uint8ClampedArray} The image data as a Uint8ClampedArray containing RGBA values.
154142
*/
155-
getImageData(bytes) {
143+
convertToPixelData(bytes) {
156144
const BYTES_PER_ROW = this.width * this.bytesPerPixel;
157145
const dataContainer = new Uint8ClampedArray(this.width * this.height * 4); // 4 channels: R, G, B, A
158146

libraries/Camera/extras/WebSerialCamera/serialConnectionHandler.js

+56-24
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1+
/**
2+
* @fileoverview This file contains the SerialConnectionHandler class.
3+
* It handles the connection between the browser and the Arduino board via Web Serial.
4+
* @author Sebastian Romero
5+
*/
6+
17
const ArduinoUSBVendorId = 0x2341;
28
const UserActionAbortError = 8;
39

410
/**
511
* Handles the connection between the browser and the Arduino board via Web Serial.
12+
* Please note that for board with software serial over USB, the baud rate and other serial settings have no effect.
613
*/
714
class SerialConnectionHandler {
15+
/**
16+
* Represents a serial connection handler.
17+
* @constructor
18+
* @param {number} [baudRate=115200] - The baud rate of the serial connection.
19+
* @param {number} [dataBits=8] - The number of data bits.
20+
* @param {number} [stopBits=1] - The number of stop bits.
21+
* @param {string} [parity="none"] - The parity setting.
22+
* @param {string} [flowControl="none"] - The flow control setting.
23+
* @param {number} [bufferSize=2097152] - The size of the buffer in bytes. Max buffer size is 16MB
24+
* @param {number} [timeout=2000] - The connection timeout value in milliseconds.
25+
*/
826
constructor(baudRate = 115200, dataBits = 8, stopBits = 1, parity = "none", flowControl = "none", bufferSize = 2 * 1024 * 1024, timeout = 2000) {
927
this.baudRate = baudRate;
1028
this.dataBits = dataBits;
1129
this.stopBits = stopBits;
1230
this.flowControl = flowControl;
13-
// Max buffer size is 16MB
1431
this.bufferSize = bufferSize;
1532
this.parity = parity;
1633
this.timeout = timeout;
1734
this.currentPort = null;
1835
this.currentReader = null;
36+
this.currentTransformer = null;
1937
this.readableStreamClosed = null;
20-
this.transformer = new BytesWaitTransformer();
2138
this.registerEvents();
2239
}
2340

@@ -38,14 +55,6 @@ class SerialConnectionHandler {
3855
}
3956
}
4057

41-
/**
42-
* Sets the transformer that is used to convert bytes into higher-level data types.
43-
* @param {*} transformer
44-
*/
45-
setTransformer(transformer) {
46-
this.transformer = transformer;
47-
}
48-
4958
/**
5059
* Checks if the browser is connected to a serial port.
5160
* @returns {boolean} True if the browser is connected, false otherwise.
@@ -93,7 +102,7 @@ class SerialConnectionHandler {
93102
this.currentPort = null;
94103
await this.currentReader?.cancel();
95104
await this.readableStreamClosed.catch(() => { }); // Ignores the error
96-
this.transformer.flush();
105+
this.currentTransformer?.flush();
97106
await port.close();
98107
console.log('🔌 Disconnected from serial port.');
99108
if(this.onDisconnect) this.onDisconnect();
@@ -126,21 +135,31 @@ class SerialConnectionHandler {
126135
return false;
127136
}
128137

138+
139+
/**
140+
* Reads a specified number of bytes from the serial connection.
141+
* @param {number} numBytes - The number of bytes to read.
142+
* @returns {Promise<Uint8Array>} - A promise that resolves to a Uint8Array containing the read bytes.
143+
*/
144+
async readBytes(numBytes) {
145+
return await this.readData(new BytesWaitTransformer(numBytes));
146+
}
147+
129148
/**
130149
* Reads the specified number of bytes from the serial port.
131-
* @param {number} numBytes The number of bytes to read.
132-
* @param {number} timeout The timeout in milliseconds.
150+
* @param {Transformer} transformer The transformer that is used to process the bytes.
133151
* If the timeout is reached, the reader will be canceled and the read lock will be released.
134152
*/
135-
async readBytes(numBytes, timeout = null) {
153+
async readData(transformer) {
154+
if(!transformer) throw new Error('Transformer is null');
136155
if(!this.currentPort) return null;
137156
if(this.currentPort.readable.locked) {
138157
console.log('🔒 Stream is already locked. Ignoring request...');
139158
return null;
140159
}
141160

142-
this.transformer.setBytesToWait(numBytes);
143-
const transformStream = new TransformStream(this.transformer);
161+
const transformStream = new TransformStream(transformer);
162+
this.currentTransformer = transformer;
144163
// pipeThrough() cannot be used because we need a promise that resolves when the stream is closed
145164
// to be able to close the port. pipeTo() returns such a promise.
146165
// SEE: https://stackoverflow.com/questions/71262432/how-can-i-close-a-web-serial-port-that-ive-piped-through-a-transformstream
@@ -150,12 +169,12 @@ class SerialConnectionHandler {
150169
let timeoutID = null;
151170

152171
try {
153-
if (timeout) {
172+
if (this.timeout) {
154173
timeoutID = setTimeout(() => {
155174
console.log('⌛️ Timeout occurred while reading.');
156175
if (this.currentPort?.readable) reader?.cancel();
157176
this.transformer.flush();
158-
}, timeout);
177+
}, this.timeout);
159178
}
160179
const { value, done } = await reader.read();
161180
if (timeoutID) clearTimeout(timeoutID);
@@ -173,9 +192,16 @@ class SerialConnectionHandler {
173192
await this.readableStreamClosed.catch(() => { }); // Ignores the error
174193
reader?.releaseLock();
175194
this.currentReader = null;
195+
this.currentTransformer = null;
176196
}
177197
}
178198

199+
/**
200+
* Sends the provided byte array data through the current serial port.
201+
*
202+
* @param {ArrayBuffer} byteArray - The byte array data to send.
203+
* @returns {Promise<void>} - A promise that resolves when the data has been sent.
204+
*/
179205
async sendData(byteArray) {
180206
if (!this.currentPort?.writable) {
181207
console.log('🚫 Port is not writable. Ignoring request...');
@@ -196,10 +222,19 @@ class SerialConnectionHandler {
196222
return this.sendData([1]);
197223
}
198224

225+
/**
226+
* Requests the camera configuration from the board by writing a 2 to the serial port.
227+
* @returns {Promise} A promise that resolves with the configuration data.
228+
*/
199229
async requestConfig() {
200230
return this.sendData([2]);
201231
}
202232

233+
/**
234+
* Requests the camera resolution from the board and reads it back from the serial port.
235+
* The configuration simply consists of two bytes: the mode and the resolution.
236+
* @returns {Promise<ArrayBuffer>} The raw configuration data as an ArrayBuffer.
237+
*/
203238
async getConfig() {
204239
if (!this.currentPort) return;
205240

@@ -211,15 +246,12 @@ class SerialConnectionHandler {
211246
/**
212247
* Requests a frame from the Arduino board and reads the specified number of bytes from the serial port afterwards.
213248
* Times out after the timeout in milliseconds specified in the constructor.
214-
* @param {number} totalBytes The number of bytes to read.
249+
* @param {Transformer} transformer The transformer that is used to process the bytes.
215250
*/
216-
async getFrame(totalBytes) {
251+
async getFrame(transformer) {
217252
if (!this.currentPort) return;
218-
219253
await this.requestFrame();
220-
// console.log(`Trying to read ${totalBytes} bytes...`);
221-
// Read the given amount of bytes
222-
return await this.readBytes(totalBytes, this.timeout);
254+
return await this.readData(transformer, this.timeout);
223255
}
224256

225257
/**
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
/**
2+
* A transformer class that waits for a specific number of bytes before processing them.
3+
*/
14
class BytesWaitTransformer {
25
constructor(waitBytes = 1) {
36
this.waitBytes = waitBytes;
47
this.buffer = new Uint8Array(0);
58
this.controller = undefined;
69
}
710

11+
/**
12+
* Sets the number of bytes to wait before processing the data.
13+
* @param {number} waitBytes - The number of bytes to wait.
14+
*/
815
setBytesToWait(waitBytes) {
916
this.waitBytes = waitBytes;
1017
}
@@ -19,7 +26,13 @@ class BytesWaitTransformer {
1926
return bytes;
2027
}
2128

22-
29+
/**
30+
* Transforms the incoming chunk of data and enqueues the processed bytes to the controller.
31+
* It does so when the buffer contains at least the specified number of bytes.
32+
* @param {Uint8Array} chunk - The incoming chunk of data.
33+
* @param {TransformStreamDefaultController} controller - The controller for enqueuing processed bytes.
34+
* @returns {Promise<void>} - A promise that resolves when the transformation is complete.
35+
*/
2336
async transform(chunk, controller) {
2437
this.controller = controller;
2538

@@ -38,47 +51,107 @@ class BytesWaitTransformer {
3851
}
3952
}
4053

54+
/**
55+
* Flushes the buffer and processes any remaining bytes when the stream is closed.
56+
*
57+
* @param {WritableStreamDefaultController} controller - The controller for the writable stream.
58+
*/
4159
flush(controller) {
4260
if (this.buffer.length > 0) {
4361
// Handle remaining bytes (if any) when the stream is closed
4462
const remainingBytes = this.buffer.slice();
4563
console.log("Remaining bytes:", remainingBytes);
4664

4765
// Notify the controller that remaining bytes have been processed
48-
controller.enqueue(remainingBytes);
66+
controller?.enqueue(remainingBytes);
4967
}
5068
}
5169
}
5270

71+
72+
/**
73+
* Represents an Image Data Transformer that converts bytes into image data.
74+
* See other example for PNGs here: https://github.com/mdn/dom-examples/blob/main/streams/png-transform-stream/png-transform-stream.js
75+
* @extends BytesWaitTransformer
76+
*/
5377
class ImageDataTransformer extends BytesWaitTransformer {
54-
constructor(context, width, height, imageMode) {
55-
super(1);
56-
this.width = width;
57-
this.height = height;
78+
/**
79+
* Creates a new instance of the Transformer class.
80+
* @param {CanvasRenderingContext2D} context - The canvas rendering context.
81+
* @param {number} [width=null] - The width of the image.
82+
* @param {number} [height=null] - The height of the image.
83+
* @param {string} [imageMode=null] - The image mode.
84+
*/
85+
constructor(context, width = null, height = null, imageMode = null) {
86+
super();
87+
this.context = context;
88+
this.imageDataProcessor = new ImageDataProcessor();
89+
if (width && height){
90+
this.setResolution(width, height);
91+
}
92+
if (imageMode){
93+
this.setImageMode(imageMode);
94+
}
5895
}
5996

97+
/**
98+
* Sets the resolution of the camera image that is being processed.
99+
*
100+
* @param {number} width - The width of the resolution.
101+
* @param {number} height - The height of the resolution.
102+
*/
60103
setResolution(width, height) {
61104
this.width = width;
62105
this.height = height;
106+
this.imageDataProcessor.setResolution(width, height);
107+
if(this.isConfigured()){
108+
this.setBytesToWait(this.imageDataProcessor.getTotalBytes());
109+
}
63110
}
64111

112+
/**
113+
* Sets the image mode of the camera image that is being processed.
114+
* Possible values: RGB565, GRAYSCALE, RGB888, BAYER
115+
*
116+
* @param {string} imageMode - The image mode to set.
117+
*/
65118
setImageMode(imageMode) {
66119
this.imageMode = imageMode;
120+
this.imageDataProcessor.setImageMode(imageMode);
121+
if(this.isConfigured()){
122+
this.setBytesToWait(this.imageDataProcessor.getTotalBytes());
123+
}
67124
}
68125

69-
convertBytes(bytes) {
70-
console.log("Converting bytes");
71-
let a = new Uint8Array(bytes);
72-
// Iterate over UInt8Array
73-
for (let i = 0; i < a.length; i++) {
74-
a[i] = a[i] * 2;
75-
}
126+
/**
127+
* Checks if the image data processor is configured.
128+
* This is true if the image mode and resolution are set.
129+
* @returns {boolean} True if the image data processor is configured, false otherwise.
130+
*/
131+
isConfigured() {
132+
return this.imageMode && this.width && this.height;
133+
}
76134

77-
// const imageData = new ImageData(this.width, this.height);
78-
// for (let i = 0; i < bytes.length; i++) {
79-
// imageData.data[i] = bytes[i];
80-
// }
81-
// return imageData;
82-
return bytes;
135+
/**
136+
* Resets the state of the transformer.
137+
*/
138+
reset() {
139+
this.imageMode = null;
140+
this.width = null;
141+
this.height = null;
142+
this.imageDataProcessor.reset();
143+
}
144+
145+
/**
146+
* Converts the given raw bytes into an ImageData object by using the ImageDataProcessor.
147+
*
148+
* @param {Uint8Array} bytes - The bytes to convert.
149+
* @returns {ImageData} The converted ImageData object.
150+
*/
151+
convertBytes(bytes) {
152+
const pixelData = this.imageDataProcessor.convertToPixelData(bytes);
153+
const imageData = this.context.createImageData(imageDataTransfomer.width, imageDataTransfomer.height);
154+
imageData.data.set(pixelData);
155+
return imageData;
83156
}
84157
}

0 commit comments

Comments
 (0)