Merge branch 'feature/usb_host_uvc_component' into 'master'

USB host UVC example

See merge request espressif/esp-idf!19397
This commit is contained in:
Martin Válik 2022-09-06 20:30:11 +08:00
commit f1611841a3
10 changed files with 772 additions and 0 deletions

View File

@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(usb-uvc)

View File

@ -0,0 +1,199 @@
| Supported Targets | ESP32-S2 | ESP32-S3 |
| ----------------- | -------- | -------- |
# USB Camera Example
## Overview
This example demonstrates how to:
- Capture video from a USB camera using the `libuvc` library.
- Stream the video over WiFi by hosting a TCP server.
The example enumerates connected camera, negotiates selected resolution together with `FPS` and starts capturing video.
`frame_callback` function is then invoked after receiving each frame. User can process received frame according to his needs.
Optionally, captured video can be visualized on computer with help of `player.py` script located in this example.
After setting `Enable streaming` menuconfig option, example will create TCP server upon start, and wait until `player.py` connects to server.
Once connection is established, example streams each received frame to computer for visualization.
**Notice** that `libuvc` selects highest possible `dwMaxPayloadTransferSize` by default, so example has to manually overwrite this value to 512 bytes (maximum transfer size supported by ESP32-S2/S3).
(See the README.md file in the upper level 'examples' directory for more information about examples.)
## How to Use Example
### Hardware Required
This example requires any ESP32-S2 or ESP32-S3 with external PSRAM and exposed USB connector attached to USB camera.
*ESP module without external PSRAM will fail to initialize*
### Configure the project
Following configuration is needed for streaming video:
Open the project configuration menu (`idf.py menuconfig`).
In the `Example Connection Configuration` menu:
* Set the Wi-Fi configuration.
* Set `WiFi SSID`.
* Set `WiFi Password`.
In the `Example Configuration` menu:
* Set the Example configuration
* `Enable streaming`
Optional: If you need, change the other options according to your requirements.
Additionally, `player.py` python script makes use of `opencv-python` and `numpy` packages,
not included in `idf-env` environment. Run following commands to install:
* `pip install opencv-python`
* `pip install numpy`
### Build and Flash
Build the project and flash it to the board, then run the monitor tool to view the serial output:
Run `idf.py set-target esp32s2` to set target chip.
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for all the steps to configure and use the ESP-IDF to build projects.
## Known limitations
Having only Full Speed USB peripheral and hardware limited MPS (maximum packet size) to 512 bytes, ESP32-S2/S3 is capable of reading about 0.5 MB of data per second. When connected to Full Speed USB host, cameras normally provide resolution no larger than 640x480 pixels.
Following two supported formats are the most common (both encoded in MJPEG):
* 320x240 30 FPS
* 640x480 15 FPS
## Tested cameras
* Logitech C980
* CANYON CNE-CWC2
## Example Output
```
...
Waiting for device
Device found
DEVICE CONFIGURATION (0c45:6340/ S) ---
Status: idle
VideoControl:
bcdUVC: 0x0100
VideoStreaming(1):
bEndpointAddress: 129
Formats:
MJPEGFormat(1)
bits per pixel: 0
GUID: 4d4a5047000000000000000000000000 (MJPG)
default frame: 1
aspect ratio: 0x0
interlace flags: 00
copy protect: 00
FrameDescriptor(1)
capabilities: 00
size: 640x480
bit rate: 24576000-147456000
max frame size: 614400
default interval: 1/30
interval[0]: 1/30
interval[1]: 1/25
interval[2]: 1/20
interval[3]: 1/15
interval[4]: 1/10
interval[5]: 1/5
FrameDescriptor(2)
capabilities: 00
size: 352x288
bit rate: 8110080-48660480
max frame size: 202752
default interval: 1/30
interval[0]: 1/30
interval[1]: 1/25
interval[2]: 1/20
interval[3]: 1/15
interval[4]: 1/10
interval[5]: 1/5
FrameDescriptor(3)
capabilities: 00
size: 320x240
bit rate: 6144000-36864000
max frame size: 153600
default interval: 1/30
interval[0]: 1/30
interval[1]: 1/25
interval[2]: 1/20
interval[3]: 1/15
interval[4]: 1/10
interval[5]: 1/5
FrameDescriptor(4)
capabilities: 00
size: 176x144
bit rate: 2027520-12165120
max frame size: 50688
default interval: 1/30
interval[0]: 1/30
interval[1]: 1/25
interval[2]: 1/20
interval[3]: 1/15
interval[4]: 1/10
interval[5]: 1/5
FrameDescriptor(5)
capabilities: 00
size: 160x120
bit rate: 1536000-9216000
max frame size: 38400
default interval: 1/30
interval[0]: 1/30
interval[1]: 1/25
interval[2]: 1/20
interval[3]: 1/15
interval[4]: 1/10
interval[5]: 1/5
StillFrameDescriptor
bEndPointAddress: 00
wWidth(1) = 640
wHeight(1) = 480
wWidth(2) = 352
wHeight(2) = 288
wWidth(3) = 320
wHeight(3) = 240
wWidth(4) = 176
wHeight(4) = 144
wWidth(5) = 160
wHeight(5) = 120
END DEVICE CONFIGURATION
bmHint: 0001
bFormatIndex: 1
bFrameIndex: 3
dwFrameInterval: 333333
wKeyFrameRate: 0
wPFrameRate: 0
wCompQuality: 0
wCompWindowSize: 0
wDelay: 20905
dwMaxVideoFrameSize: 153600
dwMaxPayloadTransferSize: 512
bInterfaceNumber: 1
Streaming...
I (4801) example: fps: 35, bytes per second: 170480
I (5821) example: fps: 34, bytes per second: 172448
I (6841) example: fps: 34, bytes per second: 172448
I (7871) example: fps: 34, bytes per second: 172448
I (8891) example: fps: 34, bytes per second: 172448
I (9921) example: fps: 35, bytes per second: 177520
I (10941) example: fps: 34, bytes per second: 172448
I (11961) example: fps: 34, bytes per second: 172448
I (12991) example: fps: 34, bytes per second: 172448
I (14011) example: fps: 34, bytes per second: 172448
I (15041) example: fps: 34, bytes per second: 172448
I (16061) example: fps: 34, bytes per second: 172448
I (17081) example: fps: 34, bytes per second: 172448
Done streaming.
UVC exited
```

View File

@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c" "tcp_server.c"
INCLUDE_DIRS ""
REQUIRES protocol_examples_common nvs_flash usb mdns esp_ringbuf esp_timer esp_wifi driver)

View File

@ -0,0 +1,7 @@
menu "Example Configuration"
config EXAMPLE_ENABLE_STREAMING
bool "Enable streaming"
default n
help
Enables streaming of captured video
endmenu

View File

@ -0,0 +1,7 @@
## IDF Component Manager Manifest File
dependencies:
idf: ">=4.4"
usb_host_uvc: "1.0.0"
mdns:
rules:
- if: "idf_version >= 5.0"

View File

@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <unistd.h>
#include "esp_log.h"
#include "tcp_server.h"
#include "libuvc/libuvc.h"
#include "libuvc_helper.h"
#include "libuvc_adapter.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "usb/usb_host.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
static const char *TAG = "example";
#define USB_DISCONNECT_PIN GPIO_NUM_0
#define FPS 30
#define WIDTH 320
#define HEIGHT 240
#define FORMAT UVC_COLOR_FORMAT_MJPEG // UVC_COLOR_FORMAT_YUYV
// Attached camera can be filtered out based on (non-zero value of) PID, VID, SERIAL_NUMBER
#define PID 0
#define VID 0
#define SERIAL_NUMBER NULL
#define UVC_CHECK(exp) do { \
uvc_error_t _err_ = (exp); \
if(_err_ < 0) { \
ESP_LOGE(TAG, "UVC error: %s", \
uvc_error_string(_err_)); \
assert(0); \
} \
} while(0)
static SemaphoreHandle_t ready_to_uninstall_usb;
static EventGroupHandle_t app_flags;
// Handles common USB host library events
static void usb_lib_handler_task(void *args)
{
while (1) {
uint32_t event_flags;
usb_host_lib_handle_events(portMAX_DELAY, &event_flags);
// Release devices once all clients has deregistered
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) {
usb_host_device_free_all();
}
// Give ready_to_uninstall_usb semaphore to indicate that USB Host library
// can be deinitialized, and terminate this task.
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) {
xSemaphoreGive(ready_to_uninstall_usb);
}
}
vTaskDelete(NULL);
}
static esp_err_t initialize_usb_host_lib(void)
{
TaskHandle_t task_handle = NULL;
const usb_host_config_t host_config = {
.intr_flags = ESP_INTR_FLAG_LEVEL1
};
esp_err_t err = usb_host_install(&host_config);
if (err != ESP_OK) {
return err;
}
ready_to_uninstall_usb = xSemaphoreCreateBinary();
if (ready_to_uninstall_usb == NULL) {
usb_host_uninstall();
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(usb_lib_handler_task, "usb_events", 4096, NULL, 2, &task_handle) != pdPASS) {
vSemaphoreDelete(ready_to_uninstall_usb);
usb_host_uninstall();
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
static void uninitialize_usb_host_lib(void)
{
xSemaphoreTake(ready_to_uninstall_usb, portMAX_DELAY);
vSemaphoreDelete(ready_to_uninstall_usb);
if ( usb_host_uninstall() != ESP_OK) {
ESP_LOGE(TAG, "Failed to uninstall usb_host");
}
}
/* This callback function runs once per frame. Use it to perform any
* quick processing you need, or have it put the frame into your application's
* input queue. If this function takes too long, you'll start losing frames. */
void frame_callback(uvc_frame_t *frame, void *ptr)
{
static size_t fps;
static size_t bytes_per_second;
static int64_t start_time;
int64_t current_time = esp_timer_get_time();
bytes_per_second += frame->data_bytes;
fps++;
if (!start_time) {
start_time = current_time;
}
if (current_time > start_time + 1000000) {
ESP_LOGI(TAG, "fps: %u, bytes per second: %u", fps, bytes_per_second);
start_time = current_time;
bytes_per_second = 0;
fps = 0;
}
// Stream received frame to client, if enabled
tcp_server_send(frame->data, frame->data_bytes);
}
void button_callback(int button, int state, void *user_ptr)
{
printf("button %d state %d\n", button, state);
}
static void libuvc_adapter_cb(libuvc_adapter_event_t event)
{
xEventGroupSetBits(app_flags, event);
}
static EventBits_t wait_for_event(EventBits_t event)
{
return xEventGroupWaitBits(app_flags, event, pdTRUE, pdFALSE, portMAX_DELAY) & event;
}
int app_main(int argc, char **argv)
{
uvc_context_t *ctx;
uvc_device_t *dev;
uvc_device_handle_t *devh;
uvc_stream_ctrl_t ctrl;
uvc_error_t res;
app_flags = xEventGroupCreate();
assert(app_flags);
const gpio_config_t input_pin = {
.pin_bit_mask = BIT64(USB_DISCONNECT_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
ESP_ERROR_CHECK( gpio_config(&input_pin) );
ESP_ERROR_CHECK( initialize_usb_host_lib() );
libuvc_adapter_config_t config = {
.create_background_task = true,
.task_priority = 5,
.stack_size = 4096,
.callback = libuvc_adapter_cb
};
libuvc_adapter_set_config(&config);
UVC_CHECK( uvc_init(&ctx, NULL) );
// Streaming takes place only when enabled in menuconfig
ESP_ERROR_CHECK( tcp_server_wait_for_connection() );
do {
printf("Waiting for device\n");
wait_for_event(UVC_DEVICE_CONNECTED);
UVC_CHECK( uvc_find_device(ctx, &dev, PID, VID, SERIAL_NUMBER) );
puts("Device found");
UVC_CHECK( uvc_open(dev, &devh) );
// Uncomment to print configuration descriptor
// libuvc_adapter_print_descriptors(devh);
uvc_set_button_callback(devh, button_callback, NULL);
// Print known device information
uvc_print_diag(devh, stderr);
// Negotiate stream profile
res = uvc_get_stream_ctrl_format_size(devh, &ctrl, FORMAT, WIDTH, HEIGHT, FPS );
while (res != UVC_SUCCESS) {
printf("Negotiating streaming format failed, trying again...\n");
res = uvc_get_stream_ctrl_format_size(devh, &ctrl, FORMAT, WIDTH, HEIGHT, FPS );
sleep(1);
}
// dwMaxPayloadTransferSize has to be overwritten to MPS (maximum packet size)
// supported by ESP32-S2(S3), as libuvc selects the highest possible MPS by default.
ctrl.dwMaxPayloadTransferSize = 512;
uvc_print_stream_ctrl(&ctrl, stderr);
UVC_CHECK( uvc_start_streaming(devh, &ctrl, frame_callback, NULL, 0) );
puts("Streaming...");
wait_for_event(UVC_DEVICE_DISCONNECTED);
uvc_stop_streaming(devh);
puts("Done streaming.");
uvc_close(devh);
} while (gpio_get_level(USB_DISCONNECT_PIN) != 0);
tcp_server_close_when_done();
uvc_exit(ctx);
puts("UVC exited");
uninitialize_usb_host_lib();
return 0;
}

View File

@ -0,0 +1,227 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "addr_from_stdin.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "tcp_server.h"
#include "mdns.h"
#define TAG "tcp_server"
#define PORT 2222
typedef struct {
int sock;
int listen_sock;
RingbufHandle_t buffer;
volatile bool close_request;
bool is_active;
} tcp_server_t;
#ifdef CONFIG_EXAMPLE_ENABLE_STREAMING
static tcp_server_t *s_server;
void socket_close(tcp_server_t *server)
{
ESP_LOGI(TAG, "Closing socket");
shutdown(server->sock, 0);
close(server->sock);
close(server->listen_sock);
}
static void sender_task(void *arg)
{
tcp_server_t *server = (tcp_server_t *)arg;
server->is_active = true;
while (1) {
size_t bytes_received = 0;
char *payload = (char *)xRingbufferReceiveUpTo(
server->buffer, &bytes_received, pdMS_TO_TICKS(2500), 20000);
if (payload != NULL && server->is_active) {
int sent = send(server->sock, payload, bytes_received, 0);
if (sent < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d, \
Shutting down tcp server...", errno);
server->is_active = false;
}
vRingbufferReturnItem(server->buffer, (void *)payload);
}
if (server->close_request) {
socket_close(server);
vRingbufferDelete(server->buffer);
vTaskDelete(NULL);
s_server = NULL;
free(server);
return;
}
}
}
esp_err_t tcp_server_send(uint8_t *payload, size_t size)
{
if (!s_server || !s_server->is_active) {
return ESP_OK;
}
if ( xRingbufferSend(s_server->buffer, payload, size, pdMS_TO_TICKS(1)) != pdTRUE ) {
ESP_LOGW(TAG, "Failed to send frame to ring buffer.");
return ESP_FAIL;
}
return ESP_OK;
}
static esp_err_t start_mdns_service(void)
{
esp_err_t err = mdns_init();
if (err) {
printf("MDNS Init failed: %d\n", err);
return ESP_FAIL;
}
mdns_hostname_set("esp-cam");
return ESP_OK;
}
static esp_err_t create_server(tcp_server_t *server)
{
char addr_str[128];
int ip_protocol = 0;
int addr_family = AF_INET;
struct sockaddr_storage dest_addr;
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = addr_family;
dest_addr_ip4->sin_port = htons(PORT);
ip_protocol = IPPROTO_IP;
server->listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (server->listen_sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
return ESP_FAIL;
}
int opt = 1;
setsockopt(server->listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ESP_LOGI(TAG, "Socket created");
int err = bind(server->listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
ESP_LOGE(TAG, "IPPROTO: %d", addr_family);
close(server->listen_sock);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Socket bound, port %d", PORT);
err = listen(server->listen_sock, 1);
if (err != 0) {
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
close(server->listen_sock);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Socket listening...");
ESP_LOGI(TAG, "Execute player.py script");
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
server->sock = accept(server->listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (server->sock < 0) {
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
close(server->listen_sock);
return ESP_FAIL;
}
// Convert ip address to string
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}
ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);
return ESP_OK;
}
esp_err_t tcp_server_wait_for_connection(void)
{
TaskHandle_t task_handle = NULL;
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(start_mdns_service());
ESP_ERROR_CHECK(example_connect());
tcp_server_t *server = calloc(1, sizeof(tcp_server_t));
if (server == NULL) {
return ESP_ERR_NO_MEM;
}
server->buffer = xRingbufferCreate(100000, RINGBUF_TYPE_BYTEBUF);
if ( server->buffer == NULL) {
free(server);
return ESP_ERR_NO_MEM;;
}
if ( create_server(server) != ESP_OK) {
vRingbufferDelete(server->buffer);
free(server);
return ESP_FAIL;
}
BaseType_t task_created = xTaskCreate(sender_task, "sender_task", 4096, server, 10, &task_handle);
if (!task_created) {
socket_close(server);
vRingbufferDelete(server->buffer);
free(server);
return ESP_ERR_NO_MEM;
}
s_server = server;
return ESP_OK;
}
void tcp_server_close_when_done(void)
{
if (s_server) {
s_server->close_request = true;
}
}
#else
esp_err_t tcp_server_wait_for_connection(void)
{
return ESP_OK;
}
esp_err_t tcp_server_send(uint8_t *payload, size_t size)
{
return ESP_OK;
}
void tcp_server_close_when_done(void) { }
#endif

View File

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t tcp_server_wait_for_connection(void);
esp_err_t tcp_server_send(uint8_t *payload, size_t size);
void tcp_server_close_when_done(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import socket
import cv2
import numpy as np
frame_count = 0
stream = bytearray()
print('Connecting to server...')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect(('esp-cam.local', 2222))
print('Receiving data ')
while True:
data = sock.recv(4096)
if not data:
break
stream += data
print('.', end='', flush=True)
a = stream.find(b'\xff\xd8')
b = stream.find(b'\xff\xd9', a)
if a != -1 and b != -1:
jpg = stream[a:b + 2]
stream = stream[b + 2:]
buffer = np.frombuffer(jpg, dtype=np.uint8)
image = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
cv2.imshow('Stream', image)
if cv2.waitKey(10) == 27:
exit(0)
frame_count += 1
print('\nFrames received ', frame_count)

View File

@ -0,0 +1,26 @@
#
# SPIRAM
#
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=150000
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
#
# USB
#
CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=1024
CONFIG_USB_HOST_HW_BUFFER_BIAS_IN=y
#
# WIFI
#
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM=16
CONFIG_ESP32_WIFI_RX_BA_WIN=8
#
# LWIP
#
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=15000