Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebSerial Camera Preview #822

Merged
merged 38 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
229f167
Send bytes in chunks
sebromero May 23, 2023
ea9c545
Add web serial camera app
sebromero May 23, 2023
940cf82
Catch edge case
sebromero May 23, 2023
70f6b23
Remove unused code
sebromero May 24, 2023
d54dba8
Working version for RGB
sebromero May 24, 2023
3d0fb5e
Working version for both
sebromero May 24, 2023
599e764
Add image processor
sebromero May 24, 2023
66f1a02
Add save function
sebromero May 24, 2023
5ced73c
Implement connection handler
sebromero May 24, 2023
e2789f4
Add callbacks for connect / disconnect
sebromero May 24, 2023
a85053c
Add documentation to serial connection handler
sebromero May 24, 2023
fe1cef1
Better error reporting
sebromero May 26, 2023
a7a35a0
Change order of config bytes
sebromero Jun 30, 2023
2313636
Add auto config for camera
sebromero Jun 30, 2023
fb8a54f
Better error handling
sebromero Dec 29, 2023
7149a1b
Extract magic strings
sebromero Dec 29, 2023
e2e949b
Add references
sebromero Dec 29, 2023
815151d
Stream works, disconnect broken
sebromero Dec 29, 2023
084393b
Fix deadlock
sebromero Dec 29, 2023
302c8a6
Add documentation to image processor
sebromero Dec 29, 2023
8d7a3fa
Add docs to config file
sebromero Dec 29, 2023
1e5cdc8
Use default serial values
sebromero Jan 8, 2024
8a3ca3b
Remove dependency on 2D Context
sebromero Jan 8, 2024
29adcd8
Working with new transformer
sebromero Jan 8, 2024
d7d0819
Add documentation to app file
sebromero Jan 8, 2024
c656793
Add more documentation
sebromero Jan 8, 2024
28d4e37
Add dedicated sketch for WebSerial
sebromero Jan 9, 2024
b6d20c4
Add filters
sebromero Jan 9, 2024
41fbc7a
Fix incorrect variable name
sebromero Jan 9, 2024
e343588
Add documentation
sebromero Jan 9, 2024
354a32b
Merge branch 'main' into sebromero/web-camera
sebromero Jan 9, 2024
4c58a74
Restore original sketch
sebromero Jan 9, 2024
e9aa024
Add setter for connection timeout
sebromero Jan 17, 2024
c9299d4
Add start stop sequence transformer
sebromero Jan 17, 2024
4f72204
Better handle data in transformer
sebromero Jan 17, 2024
5aa18d7
Refactor example sketch
sebromero Jan 17, 2024
a6ea68f
Use red LED to indicate errors
sebromero Jan 17, 2024
b696b59
Clarify sketch instructions
sebromero Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* This example shows how to capture images from the camera and send them over Web Serial.
*
* There is a companion web app that receives the images and displays them in a canvas.
* It can be found in the "extras" folder of this library.
* The on-board LED lights up while the image is being sent over serial.
*
* Instructions:
* 1. Make sure the correct camera is selected in the #include section below by uncommenting the correct line.
* 2. Upload this sketch to your camera-equipped board.
* 3. Open the web app in a browser (Chrome or Edge) by opening the index.html file
* in the "WebSerialCamera" folder which is located in the "extras" folder.
*
* Initial author: Sebastian Romero @sebromero
*/

#include "camera.h"

#ifdef ARDUINO_NICLA_VISION
#include "gc2145.h"
GC2145 galaxyCore;
Camera cam(galaxyCore);
#define IMAGE_MODE CAMERA_RGB565
#elif defined(ARDUINO_PORTENTA_H7_M7)
// uncomment the correct camera in use
#include "hm0360.h"
HM0360 himax;
// #include "himax.h";
// HM01B0 himax;
Camera cam(himax);
#define IMAGE_MODE CAMERA_GRAYSCALE
#elif defined(ARDUINO_GIGA)
#include "ov767x.h"
// uncomment the correct camera in use
OV7670 ov767x;
// OV7675 ov767x;
Camera cam(ov767x);
#define IMAGE_MODE CAMERA_RGB565
#else
#error "This board is unsupported."
#endif
sebromero marked this conversation as resolved.
Show resolved Hide resolved

/*
Other buffer instantiation options:
FrameBuffer fb(0x30000000);
FrameBuffer fb(320,240,2);

If resolution higher than 320x240 is required, please use external RAM via
#include "SDRAM.h"
FrameBuffer fb(SDRAM_START_ADDRESS);
...
// and adding in setup()
SDRAM.begin();
*/
constexpr uint16_t CHUNK_SIZE = 512; // Size of chunks in bytes
constexpr uint8_t RESOLUTION = CAMERA_R320x240; // CAMERA_R160x120
constexpr uint8_t CONFIG_SEND_REQUEST = 2;
constexpr uint8_t IMAGE_SEND_REQUEST = 1;

uint8_t START_SEQUENCE[4] = { 0xfa, 0xce, 0xfe, 0xed };
uint8_t STOP_SEQUENCE[4] = { 0xda, 0xbb, 0xad, 0x00 };
FrameBuffer fb;

/**
* Blinks the LED a specified number of times.
* @param ledPin The pin number of the LED.
* @param count The number of times to blink the LED. Default is 0xFFFFFFFF.
*/
void blinkLED(int ledPin, uint32_t count = 0xFFFFFFFF) {
while (count--) {
digitalWrite(ledPin, LOW); // turn the LED on (HIGH is the voltage level)
delay(50); // wait for a second
digitalWrite(ledPin, HIGH); // turn the LED off by making the voltage LOW
delay(50); // wait for a second
}
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LEDR, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(LEDR, HIGH);

// Init the cam QVGA, 30FPS
if (!cam.begin(RESOLUTION, IMAGE_MODE, 30)) {
blinkLED(LEDR);
}

blinkLED(LED_BUILTIN, 5);
}

/**
* Sends a chunk of data over a serial connection.
*
* @param buffer The buffer containing the data to be sent.
* @param bufferSize The size of the buffer.
*/
void sendChunk(uint8_t* buffer, size_t bufferSize){
Serial.write(buffer, bufferSize);
Serial.flush();
delay(1); // Optional: Add a small delay to allow the receiver to process the chunk
}

/**
* Sends a frame of camera image data over a serial connection.
*/
void sendFrame(){
// Grab frame and write to serial
if (cam.grabFrame(fb, 3000) == 0) {
byte* buffer = fb.getBuffer();
size_t bufferSize = cam.frameSize();
digitalWrite(LED_BUILTIN, LOW);

sendChunk(START_SEQUENCE, sizeof(START_SEQUENCE));

// Split buffer into chunks
for(size_t i = 0; i < bufferSize; i += CHUNK_SIZE) {
size_t chunkSize = min(bufferSize - i, CHUNK_SIZE);
sendChunk(buffer + i, chunkSize);
}

sendChunk(STOP_SEQUENCE, sizeof(STOP_SEQUENCE));

digitalWrite(LED_BUILTIN, HIGH);
} else {
blinkLED(20);
}
}

/**
* Sends the camera configuration over a serial connection.
* This is used to configure the web app to display the image correctly.
*/
void sendCameraConfig(){
Serial.write(IMAGE_MODE);
Serial.write(RESOLUTION);
Serial.flush();
delay(1);
}

void loop() {
if(!Serial) {
Serial.begin(115200);
while(!Serial);
}

if(!Serial.available()) return;

byte request = Serial.read();

switch(request){
case IMAGE_SEND_REQUEST:
sendFrame();
break;
case CONFIG_SEND_REQUEST:
sendCameraConfig();
break;
}

}
12 changes: 12 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 📹 WebSerial Camera Stream

This folder contains a web application that provides a camera stream over WebSerial.
This is an experimental feature not supported in all browsers. It's recommended to use Google Chrome.
See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility)

## Instructions

1. Upload the companion [Arduino sketch](../../examples/CameraCaptureWebSerial/CameraCaptureWebSerial.ino) to your board.
2. Open the web app either by directly opening the index.html file or serving it via a webserver and opening the URL provided by the webserver.
3. Click "Connect". Your board's serial port should show up in the popup. Select it. Click once again "Connect". The camera feed should start. If the board has been previously connected to the browser, it will connect automatically.
4. (Optional) click "Save Image" if you want to save individual camera frames to your computer.
135 changes: 135 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @fileoverview This file contains the main application logic.
*
* The application uses the Web Serial API to connect to the serial port.
* Check the following links for more information on the Web Serial API:
* https://developer.chrome.com/articles/serial/
* https://wicg.github.io/serial/
*
* The flow of the application is as follows:
* 1. The user clicks the "Connect" button or the browser automatically connects
* to the serial port if it has been previously connected.
* 2. The application requests the camera configuration (mode and resolution) from the board.
* 3. The application starts reading the image data stream from the serial port.
* It waits until the expected amount of bytes have been read and then processes the data.
* 4. The processed image data is rendered on the canvas.
*
* @author Sebastian Romero
*/

const connectButton = document.getElementById('connect');
const refreshButton = document.getElementById('refresh');
const startButton = document.getElementById('start');
const saveImageButton = document.getElementById('save-image');
const canvas = document.getElementById('bitmapCanvas');
const ctx = canvas.getContext('2d');

const imageDataTransfomer = new ImageDataTransformer(ctx);
imageDataTransfomer.setStartSequence([0xfa, 0xce, 0xfe, 0xed]);
imageDataTransfomer.setStopSequence([0xda, 0xbb, 0xad, 0x00]);

// 🐣 Uncomment one of the following lines to apply a filter to the image data
// imageDataTransfomer.filter = new GrayScaleFilter();
// imageDataTransfomer.filter = new BlackAndWhiteFilter();
// imageDataTransfomer.filter = new SepiaColorFilter();
// imageDataTransfomer.filter = new PixelateFilter(8);
// imageDataTransfomer.filter = new BlurFilter(8);
const connectionHandler = new SerialConnectionHandler();


// Connection handler event listeners

connectionHandler.onConnect = async () => {
connectButton.textContent = 'Disconnect';
cameraConfig = await connectionHandler.getConfig();
if(!cameraConfig){
console.error('🚫 Could not read camera configuration. Aborting...');
return;
}
const imageMode = CAMERA_MODES[cameraConfig[0]];
const imageResolution = CAMERA_RESOLUTIONS[cameraConfig[1]];
if(!imageMode || !imageResolution){
console.error(`🚫 Invalid camera configuration: ${cameraConfig[0]}, ${cameraConfig[1]}. Aborting...`);
return;
}
imageDataTransfomer.setImageMode(imageMode);
imageDataTransfomer.setResolution(imageResolution.width, imageResolution.height);
renderStream();
};

connectionHandler.onDisconnect = () => {
connectButton.textContent = 'Connect';
imageDataTransfomer.reset();
};


// Rendering logic

async function renderStream(){
while(connectionHandler.isConnected()){
if(imageDataTransfomer.isConfigured()) await renderFrame();
}
}

/**
* Renders the image data for one frame from the board and renders it.
* @returns {Promise<boolean>} True if a frame was rendered, false otherwise.
*/
async function renderFrame(){
if(!connectionHandler.isConnected()) return;
const imageData = await connectionHandler.getFrame(imageDataTransfomer);
if(!imageData) return false; // Nothing to render
if(!(imageData instanceof ImageData)) throw new Error('🚫 Image data is not of type ImageData');
renderBitmap(ctx, imageData);
return true;
}

/**
* Renders the image data on the canvas.
* @param {CanvasRenderingContext2D} context The canvas context to render on.
* @param {ImageData} imageData The image data to render.
*/
function renderBitmap(context, imageData) {
context.canvas.width = imageData.width;
context.canvas.height = imageData.height;
context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(imageData, 0, 0);
}


// UI Event listeners

startButton.addEventListener('click', renderStream);

connectButton.addEventListener('click', async () => {
if(connectionHandler.isConnected()){
connectionHandler.disconnectSerial();
} else {
await connectionHandler.requestSerialPort();
await connectionHandler.connectSerial();
}
});

refreshButton.addEventListener('click', () => {
if(imageDataTransfomer.isConfigured()) renderFrame();
});

saveImageButton.addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'image.png';
link.href = canvas.toDataURL();
link.click();
link.remove();
});

// On page load event, try to connect to the serial port
window.addEventListener('load', async () => {
console.log('🚀 Page loaded. Trying to connect to serial port...');
setTimeout(() => {
connectionHandler.autoConnect();
}, 1000);
});

if (!("serial" in navigator)) {
alert("The Web Serial API is not supported in your browser.");
}
51 changes: 51 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/cameraConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @fileoverview This file contains the configuration for the camera.
* @author Sebastian Romero
*/

/**
* The available camera (color) modes.
* The Arduino sketch uses the same values to communicate which mode should be used.
**/
const CAMERA_MODES = {
0: "GRAYSCALE",
1: "BAYER",
2: "RGB565"
};

/**
* The available camera resolutions.
* The Arduino sketch uses the same values to communicate which resolution should be used.
*/
const CAMERA_RESOLUTIONS = {
0: {
"name": "QQVGA",
"width": 160,
"height": 120
},
1: {
"name": "QVGA",
"width": 320,
"height": 240
},
2: {
"name": "320x320",
"width": 320,
"height": 320
},
3: {
"name": "VGA",
"width": 640,
"height": 480
},
5: {
"name": "SVGA",
"width": 800,
"height": 600
},
6: {
"name": "UXGA",
"width": 1600,
"height": 1200
}
};
Loading
Loading