Chapter 30 Camera Web Server

In this section, we’ll use ESP32’s video function as an example to study.

Project 30.1 Camera Web Server

Connect ESP32 using USB and check its IP address through serial monitor. Use web page to access IP address to obtain video and image data.

Component List

ESP32-WROVER x1

Chapter01_00

Micro USB Wire x1

Chapter08_00

Circuit

Connect Freenove ESP32 to the computer using the USB cable.

../../../_images/Chapter32_01.png

Code

Move the program folder “ Freenove_Ultimate_Starter_Kit_for_ESP32/Python/Python_Codes “ to disk(D) in advance with the path of “ D:/Micropython_Codes “.

Since Micropython does not provide firmware including camera module, in this chapter, we will use the camera based on the firmware in lemariva’s Github project, micropython-camera-driver.

Project link: https://github.com/lemariva/micropython-camera-driver

Before starting the project, we need to re-upload the firmware with the camera module via steps below.

Open Thonny, click “run” and select “Select interpreter…””

../../../_images/Chapter34_09.png

Select “Micropython (ESP32)”, select “USB-SERIAL CH340 (COM4)”, and then click the long button under “Firmware”.

../../../_images/Chapter34_10.png

In the new popup window, select corresponding “USB-SERIAL CH340 (COM3)” for port. Click “Browse”, select “ 30.1_Camera_WebServerfirmwaremicropython_camera_feeeb5ea3_esp32_idf4_4.bin “.

Select “Erase…” and click “Install”。

../../../_images/Chapter34_11.png

Wait for completion.

Open “Thonny”, click “This computer” -> “D:” -> “Micropython_Codes” -> “30.1_Camera_WebServer”. Select folder “lib”, right click your mouse to select “Upload to /”, wait for “lib” to be uploaded to ESP32-WROVER and then double click “picoweb_video.py”.

30.1_Camera_WebServer

../../../_images/Chapter34_12.png

Before running the program, please modify your router’s name and password in the box shown in the illustration above to make sure that your code can compile and work successfully.

Click “run” to run the code “picoweb_video.py”, then you can see the following content in the shell area.

../../../_images/Chapter34_13.png

If your ESP32 has been in the process of connecting to router, but the information above has not been printed out, please re-check whether the router name and password have been entered correctly and press the reset key on ESP32-WROVER to wait for a successful connection prompt.

Open a web browser, enter the IP address printed by the serial monitor in the address bar, and access it.

Taking the Google browser as an example, here’s what the browser prints out after successful access to ESP32’s IP.

../../../_images/Chapter34_14.png

The effect is shown in the image below.

../../../_images/Chapter34_15.png

Note

If the shell area prompts an error when you click to run the code, please press the rst button on the esp32, wait for the system reset to complete, and then re-run the code.

The following is the program code.

  1import network
  2import socket
  3import time
  4import gc
  5from camera import Camera, FrameSize, PixelFormat
  6
  7cam = Camera(
  8    frame_size=FrameSize.QQVGA,
  9    pixel_format=PixelFormat.RGB565,
 10    xclk_freq=20000000,
 11    init=True
 12)
 13
 14ssid = '********'
 15password = '********'
 16
 17station = network.WLAN(network.STA_IF)
 18station.active(True)
 19station.connect(ssid, password)
 20
 21while not station.isconnected():
 22    time.sleep(1)
 23
 24ip = station.ifconfig()[0]
 25print(f'Connected! Open in browser: http://{ip}')
 26
 27addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
 28s = socket.socket()
 29s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 30s.bind(addr)
 31s.listen(5)
 32
 33print('Server running on:', addr)
 34
 35html = """<!DOCTYPE html>
 36<html>
 37<head>
 38    <meta charset="UTF-8">
 39    <title>ESP32 Camera Stream</title>
 40    <style>
 41        body {
 42            display: flex;
 43            justify-content: center;
 44            align-items: center;
 45            height: 100vh;
 46            margin: 0;
 47            background-color: #f0f0f0;
 48            font-family: Arial, sans-serif;
 49        }
 50        .video-container {
 51            text-align: center;
 52            width: 100%;
 53            max-width: 1000px;
 54        }
 55        canvas {
 56            width: auto;
 57            height: auto;
 58            image-rendering: pixelated;
 59            border: 2px solid #ccc;
 60            background-color: #000;
 61        }
 62        #status {
 63            margin-top: 10px;
 64            font-size: 16px;
 65            color: #333;
 66        }
 67    </style>
 68</head>
 69<body>
 70    <div class="video-container">
 71        <h1>ESP32 Camera Stream</h1>
 72        <canvas id="videoCanvas" width="640" height="480"></canvas>
 73        <div id="status">Connecting to camera...</div>
 74    </div>
 75
 76    <script>
 77        const canvas = document.getElementById('videoCanvas');
 78        const ctx = canvas.getContext('2d');
 79        const statusDiv = document.getElementById('status');
 80
 81        function fetchImage() {
 82            fetch('/frame')
 83                .then(response => {
 84                    if (!response.ok) throw new Error('Network request error');
 85                    return response.arrayBuffer();
 86                })
 87                .then(data => {
 88                    if (!data || data.byteLength === 0) {
 89                        statusDiv.textContent = 'Error: Empty data received';
 90                        setTimeout(fetchImage, 500);
 91                        return;
 92                    }
 93
 94                    let uint8Array = new Uint8Array(data);
 95                    if (data.byteLength % 2 !== 0) {
 96                        uint8Array = new Uint8Array(data.slice(0, data.byteLength - 1));
 97                    }
 98
 99                    const swappedArray = swapRgb565Bytes(uint8Array);
100                    const rgb565Array = new Uint16Array(swappedArray.buffer);
101                    const rgbaArray = convertRGB565ToRGBA(rgb565Array);
102
103                    const actualPixelCount = rgb565Array.length;
104
105                    let width, height;
106                    if (actualPixelCount === 640 * 480) {
107                        width = 640; height = 480;
108                    } else if (actualPixelCount === 320 * 240) {
109                        width = 320; height = 240;
110                    } else if (actualPixelCount === 160 * 120) {
111                        width = 160; height = 120;
112                    } else {
113                        width = Math.floor(Math.sqrt(actualPixelCount));
114                        while (actualPixelCount % width !== 0 && width > 1) { width--; }
115                        height = actualPixelCount / width;
116                    }
117
118                    if (canvas.width !== width || canvas.height !== height) {

Import picoweb、utime、camera、gc modules.

1import time
2import gc
3from camera import Camera, FrameSize, PixelFormat
4

Before running the code, please modify the WiFi name and password in the code to ensure that the ESP32 can connect to the network.

1frame_size=FrameSize.QQVGA,
2pixel_format=PixelFormat.RGB565,

Define the WiFi connection function, set the ESP32 to STA mode, and let the ESP32 connect to the nearby WiFi. If the connection is successful, the WiFi configuration information of the ESP32 will be printed; if the connection fails, the connection timeout will be printed.

 1)
 2
 3ssid = '********'
 4password = '********'
 5
 6station = network.WLAN(network.STA_IF)
 7station.active(True)
 8station.connect(ssid, password)
 9
10while not station.isconnected():
11    time.sleep(1)
12
13ip = station.ifconfig()[0]
14print(f'Connected! Open in browser: http://{ip}')
15

The deinit() is used to disable the configuration of the camera to prevent the previous configuration from interfering with the following configuration.

The init() is used to configure the camera’s pin driver, image data format, resolution and other information. By default, please do not modify this function, otherwise the camera initialization fails and the image cannot be obtained.

1s.listen(5)
2
3print('Server running on:', addr)
4
5html = """<!DOCTYPE html>
6<html>
7<head>

This function can set the resolution of the camera individually, you can refer to the notes below to select the appropriate resolution size.

1<title>ESP32 Camera Stream</title>
2<style>
3    body {
4        display: flex;
5        justify-content: center;
6        align-items: center;
7        height: 100vh;

The following functions can modify the image information obtained by the camera.

 1    background-color: #f0f0f0;
 2    font-family: Arial, sans-serif;
 3}
 4.video-container {
 5    text-align: center;
 6    width: 100%;
 7    max-width: 1000px;
 8}
 9canvas {
10    width: auto;
11    height: auto;
12    image-rendering: pixelated;

This is the code for a simple web interface, used here as an example.

 1        }
 2        #status {
 3            margin-top: 10px;
 4            font-size: 16px;
 5            color: #333;
 6        }
 7    </style>
 8</head>
 9<body>
10    <div class="video-container">
11        <h1>ESP32 Camera Stream</h1>
12        <canvas id="videoCanvas" width="640" height="480"></canvas>

Web page response function. When a user visits the webpage “/” built by ESP32, ESP32 calls this function, allowing the user to observe a display interface in the browser.

1
2<script>
3    const canvas = document.getElementById('videoCanvas');

send_frame() can send the image obtained by ESP32 in web page format. When someone visits the webpage “/video” built by the ESP32, the video(req, resp) function is used to continuously fetch images and send them to the browser.

 1const statusDiv = document.getElementById('status');
 2
 3function fetchImage() {
 4    fetch('/frame')
 5        .then(response => {
 6            if (!response.ok) throw new Error('Network request error');
 7            return response.arrayBuffer();
 8        })
 9        .then(data => {
10            if (!data || data.byteLength === 0) {
11                statusDiv.textContent = 'Error: Empty data received';
12                setTimeout(fetchImage, 500);
13                return;
14            }
15

Create two route decorators and declare their listening strings and corresponding response handlers respectively.

1    uint8Array = new Uint8Array(data.slice(0, data.byteLength - 1));
2}
3
4const swappedArray = swapRgb565Bytes(uint8Array);
5const rgb565Array = new Uint16Array(swappedArray.buffer);

This is the main part of the program. First initialize the ESP32 camera, and then configure WiFi to connect the ESP32 to the network. Call the picoweb library, build a webserver, and run it.

Reference

Image resolution

Sharpness

Image resolution

Sharpness

FRAMESIZE_96x96

96x96

FRAMESIZE_HVGA

480x320

FRAMESIZE_QQVGA

160x120

FRAMESIZE_VGA

640x480

FRAMESIZE_QCIF

176x144

FRAMESIZE_SVGA

800x600

FRAMESIZE_HQVGA

240x176

FRAMESIZE_XGA

1024x768

FRAMESIZE_240x240

240x240

FRAMESIZE_HD

1280x720

FRAMESIZE_QVGA

320x240

FRAMESIZE_SXGA

1280x1024

FRAMESIZE_CIF

400x296

FRAMESIZE_UXGA

1600x1200

We recommend that the resolution not exceed VGA(640x480).