Skip to content

Commit 6b74263

Browse files
committed
uvc: Experimental module for USB video
This allows the CircuitPython device to act as a UVC video source.
1 parent ccc85f8 commit 6b74263

14 files changed

Lines changed: 711 additions & 0 deletions

File tree

ports/espressif/boards/adafruit_qtpy_esp32s3_4mbflash_2mbpsram/mpconfigboard.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ CIRCUITPY_ESP_PSRAM_FREQ = 80m
1515

1616
OPTIMIZATION_FLAGS = -Os
1717
CIRCUITPY_ESPCAMERA = 0
18+
CIRCUITPY_USB_UVC = 1

ports/raspberrypi/mpconfigport.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ CIRCUITPY_ROTARYIO ?= 1
2323
CIRCUITPY_ROTARYIO_SOFTENCODER = 1
2424
CIRCUITPY_SYNTHIO_MAX_CHANNELS = 12
2525
CIRCUITPY_USB_HOST ?= 1
26+
CIRCUITPY_USB_UVC ?= 1
2627

2728
# Things that need to be implemented.
2829
CIRCUITPY_FREQUENCYIO = 0

py/circuitpy_defns.mk

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,9 @@ endif
393393
ifeq ($(CIRCUITPY_USB_HID),1)
394394
SRC_PATTERNS += usb_hid/%
395395
endif
396+
ifeq ($(CIRCUITPY_USB_UVC),1)
397+
SRC_PATTERNS += uvc/%
398+
endif
396399
ifeq ($(CIRCUITPY_USB_HOST),1)
397400
SRC_PATTERNS += usb_host/% usb/%
398401
endif

py/circuitpy_mpconfig.mk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,11 @@ CFLAGS += -DCIRCUITPY_USB_HID=$(CIRCUITPY_USB_HID)
567567
CIRCUITPY_USB_HID_ENABLED_DEFAULT ?= $(USB_NUM_ENDPOINT_PAIRS_5_OR_GREATER)
568568
CFLAGS += -DCIRCUITPY_USB_HID_ENABLED_DEFAULT=$(CIRCUITPY_USB_HID_ENABLED_DEFAULT)
569569

570+
CIRCUITPY_USB_UVC ?= 0
571+
CIRCUITPY_USB_UVC_ENABLED_DEFAULT ?= 0
572+
CFLAGS += -DCIRCUITPY_USB_UVC_ENABLED_DEFAULT=$(CIRCUITPY_USB_UVC_ENABLED_DEFAULT)
573+
CFLAGS += -DCIRCUITPY_USB_UVC=$(CIRCUITPY_USB_UVC)
574+
570575
CIRCUITPY_USB_HOST ?= 0
571576
CFLAGS += -DCIRCUITPY_USB_HOST=$(CIRCUITPY_USB_HOST)
572577

shared-bindings/uvc/__init__.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2024 Jeff Epler for Adafruit Industries
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#include "py/obj.h"
28+
#include "py/runtime.h"
29+
30+
#include "shared-bindings/uvc/__init__.h"
31+
#include "shared-module/uvc/__init__.h"
32+
33+
//| """Allows streaming bitmaps to a host computer via USB UVC
34+
//|
35+
//| This interface is experimental and may change without notice even in stable
36+
//| versions of CircuitPython."""
37+
//|
38+
//| bitmap: displayio.Bitmap
39+
//| """The content of this bitmap image is streamed to the host computer. To change the
40+
//| displayed image, modify the bitmap contents.
41+
//|
42+
//| The bitmap content is in RGB565_SWAPPED format. It is converted to "YUYV" format before
43+
//| transmission to the host computer. The "YUYV" format stores a "Y" (luminance) sample
44+
//| for every pixel, but a chromonance (UV) value only for each pair of pixels; due to this,
45+
//| the color of adjacent pixels cannot be fully controlled. For instance, there is no way
46+
//| for the first pixel to be red and the second pixel to be green. However, they can both
47+
//| be different shades of red.
48+
//| """
49+
50+
static const mp_map_elem_t uvc_module_globals_table[] = {
51+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_uvc) },
52+
{ MP_ROM_QSTR(MP_QSTR_bitmap), MP_ROM_PTR(&uvc_bitmap_obj) },
53+
};
54+
55+
static MP_DEFINE_CONST_DICT(uvc_module_globals, uvc_module_globals_table);
56+
57+
const mp_obj_module_t uvc_module = {
58+
.base = { &mp_type_module },
59+
.globals = (mp_obj_dict_t *)&uvc_module_globals,
60+
};
61+
62+
MP_REGISTER_MODULE(MP_QSTR_uvc, uvc_module);

shared-bindings/uvc/__init__.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2024 Jeff Epler for Adafruit Industries
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#pragma once
28+
29+
#include "shared-module/displayio/Bitmap.h"
30+
extern displayio_bitmap_t uvc_bitmap_obj;

shared-module/uvc/__init__.c

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#include "py/runtime.h"
2+
#include <stdint.h>
3+
#include "shared-module/uvc/__init__.h"
4+
#include "shared-module/uvc/uvc_usb_descriptors.h"
5+
#include "shared-bindings/displayio/Bitmap.h"
6+
#include "shared-module/bitmapfilter/macros.h"
7+
#include "class/video/video_device.h"
8+
#include "supervisor/shared/tick.h"
9+
#include "supervisor/background_callback.h"
10+
11+
#if CFG_TUD_VIDEO_STREAMING_BULK
12+
static const uint8_t usb_uvc_descriptor_template[] = {
13+
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR_BULK(0xaa, 0x80, FRAME_WIDTH, FRAME_HEIGHT, 10, 64)
14+
};
15+
16+
// Use the "descriptor.c" file to generate/verify these offsets
17+
const uint8_t interface_indices[] = {2, 10, 29, 59};
18+
const uint8_t string_indices[] = {7, 16, 65};
19+
const uint8_t endpoint_indices[] = {72, 153};
20+
21+
#else
22+
static const uint8_t usb_uvc_descriptor_template[] = {
23+
TUD_VIDEO_CAPTURE_DESCRIPTOR_UNCOMPR(0xaa, 0x80, FRAME_WIDTH, FRAME_HEIGHT, 10, CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE)
24+
};
25+
26+
// Use the "descriptor.c" file to generate/verify these offsets
27+
const uint8_t interface_indices[] = {2, 10, 29, 59, 153};
28+
const uint8_t string_indices[] = {7, 16, 65, 159};
29+
const uint8_t endpoint_indices[] = {72, 162};
30+
#endif
31+
32+
static unsigned frame_num = 0;
33+
static unsigned tx_busy = 0;
34+
static unsigned interval_ms = 1000 / FRAME_RATE;
35+
36+
// TODO must dynamically allocate this, otherwise everyone pays for it
37+
static uint8_t frame_buffer_yuyv[FRAME_WIDTH * FRAME_HEIGHT * 16 / 8];
38+
static uint16_t frame_buffer_rgb565[FRAME_WIDTH * FRAME_HEIGHT];
39+
40+
displayio_bitmap_t uvc_bitmap_obj = {
41+
.base = {.type = &displayio_bitmap_type },
42+
.width = FRAME_WIDTH,
43+
.height = FRAME_HEIGHT,
44+
.data = (uint32_t *)frame_buffer_rgb565,
45+
.stride = FRAME_WIDTH,
46+
.bits_per_value = 16,
47+
.x_shift = 1,
48+
.x_mask = 1,
49+
.bitmask = 0xffff,
50+
};
51+
52+
bool usb_uvc_enabled(void) {
53+
return true; // TODO
54+
}
55+
56+
size_t usb_uvc_descriptor_length(void) {
57+
return sizeof(usb_uvc_descriptor_template);
58+
}
59+
60+
static void convert_framebuffer(void) {
61+
uint8_t *dest = frame_buffer_yuyv;
62+
uint16_t *src = frame_buffer_rgb565;
63+
64+
for (int i = 0; i < FRAME_WIDTH * FRAME_HEIGHT / 2; i++) {
65+
uint16_t p1 = IMAGE_GET_RGB565_PIXEL_FAST(src, 0);
66+
uint16_t p2 = IMAGE_GET_RGB565_PIXEL_FAST(src, 1);
67+
src += 2;
68+
69+
int y1 = COLOR_RGB565_TO_Y(p1);
70+
int u = COLOR_RGB565_TO_U(p1) + 128; // openmv UV are signed in the range [-127,128]
71+
int v = COLOR_RGB565_TO_V(p1) + 128;
72+
int y2 = COLOR_RGB565_TO_Y(p2);
73+
74+
*dest++ = y1;
75+
*dest++ = u;
76+
*dest++ = y2;
77+
*dest++ = v;
78+
}
79+
}
80+
81+
size_t usb_uvc_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *descriptor_counts, uint8_t *current_interface_string) {
82+
usb_add_interface_string(*current_interface_string, "CircuitPython UVC");
83+
84+
memcpy(descriptor_buf, usb_uvc_descriptor_template, sizeof(usb_uvc_descriptor_template));
85+
86+
for (size_t i = 0; i < TU_ARRAY_SIZE(string_indices); i++) {
87+
descriptor_buf[string_indices[i]] = *current_interface_string;
88+
}
89+
90+
(*current_interface_string)++;
91+
92+
for (size_t i = 0; i < TU_ARRAY_SIZE(interface_indices); i++) {
93+
descriptor_buf[interface_indices[i]] += descriptor_counts->current_interface;
94+
}
95+
descriptor_counts->current_interface += 2;
96+
97+
for (size_t i = 0; i < TU_ARRAY_SIZE(endpoint_indices); i++) {
98+
// the only endpoint is an IN endpoint so set the 0x80 bit accordingly
99+
descriptor_buf[endpoint_indices[i]] = descriptor_counts->current_endpoint | 0x80;
100+
}
101+
descriptor_counts->num_out_endpoints++;
102+
descriptor_counts->current_endpoint++;
103+
descriptor_counts->current_interface++;
104+
105+
supervisor_enable_tick();
106+
107+
return sizeof(usb_uvc_descriptor_template);
108+
}
109+
110+
background_callback_t uvc_cb;
111+
112+
#define printf(...) mp_printf(&mp_plat_print, __VA_ARGS__)
113+
114+
STATIC void uvc_cb_fun(void *unused) {
115+
(void)unused;
116+
117+
static unsigned start_ms = 0;
118+
static unsigned already_sent = 0;
119+
120+
{
121+
static int i;
122+
if (i++ % 100 == 0) {
123+
// printf("usb_uvc_task i=%d, tud_video_n_streaming=%d already_sent=%d start_ms=%d\n", i, tud_video_n_streaming(0, 0), already_sent, start_ms);
124+
}
125+
}
126+
127+
if (!tud_video_n_streaming(0, 0)) {
128+
already_sent = 0;
129+
frame_num = 0;
130+
// printf("not already streaming\n");
131+
return;
132+
}
133+
134+
if (!already_sent) {
135+
already_sent = 1;
136+
start_ms = supervisor_ticks_ms32();
137+
convert_framebuffer();
138+
bool result = tud_video_n_frame_xfer(0, 0, (void *)frame_buffer_yuyv, FRAME_WIDTH * FRAME_HEIGHT * 16 / 8);
139+
(void)result;
140+
// printf("(!already_sent) frame_xfer -> %d\n", (int)result);
141+
}
142+
143+
unsigned cur = supervisor_ticks_ms32();
144+
if (cur - start_ms < interval_ms) {
145+
background_callback_add(&uvc_cb, uvc_cb_fun, NULL); // re-queue
146+
return; // not enough time
147+
}
148+
if (tx_busy) {
149+
background_callback_add(&uvc_cb, uvc_cb_fun, NULL); // re-queue
150+
return;
151+
}
152+
start_ms += interval_ms;
153+
154+
convert_framebuffer();
155+
bool result = tud_video_n_frame_xfer(0, 0, (void *)frame_buffer_yuyv, FRAME_WIDTH * FRAME_HEIGHT * 16 / 8);
156+
(void)result;
157+
// printf("frame_xfer -> %d\n", (int)result);
158+
}
159+
160+
void usb_uvc_task(void) {
161+
background_callback_add(&uvc_cb, uvc_cb_fun, NULL);
162+
}
163+
164+
void tud_video_frame_xfer_complete_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx) {
165+
(void)ctl_idx;
166+
(void)stm_idx;
167+
usb_uvc_task();
168+
tx_busy = 0;
169+
/* flip buffer */
170+
++frame_num;
171+
}
172+
173+
int tud_video_commit_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx,
174+
video_probe_and_commit_control_t const *parameters) {
175+
(void)ctl_idx;
176+
(void)stm_idx;
177+
/* convert unit to ms from 100 ns */
178+
interval_ms = parameters->dwFrameInterval / 10000;
179+
return VIDEO_ERROR_NONE;
180+
}

shared-module/uvc/__init__.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2023 Jeff Epler for Adafruit Industries
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#pragma once
28+
29+
#include <stdbool.h>
30+
#include <stddef.h>
31+
#include <stdint.h>
32+
#include "shared-module/storage/__init__.h"
33+
#include "shared-module/displayio/Bitmap.h"
34+
35+
bool usb_uvc_enabled(void);
36+
size_t usb_uvc_descriptor_length(void);
37+
size_t usb_uvc_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *descriptor_counts, uint8_t *current_interface_string);
38+
void usb_uvc_task(void);

0 commit comments

Comments
 (0)