Skip to content

Commit 642091d

Browse files
authored
Merge pull request #2246 from jepler/esp32-camera
Examples for new esp32_camera module in CircuitPython 8
2 parents 6577afe + 7e38a51 commit 642091d

File tree

5 files changed

+475
-0
lines changed
  • CircuitPython_ESP32_Camera

5 files changed

+475
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Jeff Epler for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
Take a 10-frame stop motion GIF image.
7+
8+
This example requires:
9+
* `Espressif Kaluga v1.3 <https://www.adafruit.com/product/4729>`_ with compatible LCD display
10+
* `MicroSD card breakout board + <https://www.adafruit.com/product/254>`_ connected as follows:
11+
* CLK to board.IO18
12+
* DI to board.IO14
13+
* DO to board.IO17
14+
* CS to IO12
15+
* GND to GND
16+
* 5V to 5V
17+
* A compatible SD card inserted in the SD card slot
18+
* A compatible camera module (such as OV5640) connected to the camera header
19+
20+
To use:
21+
22+
Insert an SD card and power on.
23+
24+
Set up the first frame using the viewfinder. Click the REC button to take a frame.
25+
26+
Set up the next frame using the viewfinder. The previous and current frames are
27+
blended together on the display, which is called an "onionskin". Click the REC
28+
button to take the next frame.
29+
30+
After 10 frames are recorded, the GIF is complete and you can begin recording another.
31+
32+
33+
About the Kaluga development kit:
34+
35+
The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is
36+
tested on v1.3.
37+
38+
The audio board must be mounted between the Kaluga and the LCD, it provides the
39+
I2C pull-ups(!)
40+
41+
The v1.3 development kit's LCD can have one of two chips, the ili9341 or
42+
st7789. Furthermore, there are at least 2 ILI9341 variants, which differ
43+
by rotation. This example is written for one if the ILI9341 variants,
44+
the one which usually uses rotation=90 to get a landscape display.
45+
"""
46+
47+
import os
48+
import struct
49+
50+
import esp32_camera
51+
import analogio
52+
import board
53+
import busio
54+
import bitmaptools
55+
import displayio
56+
import sdcardio
57+
import storage
58+
import gifio
59+
60+
V_RECORD = int(2.41 * 65536 / 3.3)
61+
V_FUZZ = 2000
62+
63+
a = analogio.AnalogIn(board.IO6)
64+
65+
def record_pressed():
66+
value = a.value
67+
return abs(value - V_RECORD) < V_FUZZ
68+
69+
displayio.release_displays()
70+
spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK)
71+
display_bus = displayio.FourWire(
72+
spi,
73+
command=board.LCD_D_C,
74+
chip_select=board.LCD_CS,
75+
reset=board.LCD_RST,
76+
baudrate=80_000_000,
77+
)
78+
_INIT_SEQUENCE = (
79+
b"\x01\x80\x80" # Software reset then delay 0x80 (128ms)
80+
b"\xEF\x03\x03\x80\x02"
81+
b"\xCF\x03\x00\xC1\x30"
82+
b"\xED\x04\x64\x03\x12\x81"
83+
b"\xE8\x03\x85\x00\x78"
84+
b"\xCB\x05\x39\x2C\x00\x34\x02"
85+
b"\xF7\x01\x20"
86+
b"\xEA\x02\x00\x00"
87+
b"\xc0\x01\x23" # Power control VRH[5:0]
88+
b"\xc1\x01\x10" # Power control SAP[2:0];BT[3:0]
89+
b"\xc5\x02\x3e\x28" # VCM control
90+
b"\xc7\x01\x86" # VCM control2
91+
b"\x36\x01\x40" # Memory Access Control
92+
b"\x37\x01\x00" # Vertical scroll zero
93+
b"\x3a\x01\x55" # COLMOD: Pixel Format Set
94+
b"\xb1\x02\x00\x18" # Frame Rate Control (In Normal Mode/Full Colors)
95+
b"\xb6\x03\x08\x82\x27" # Display Function Control
96+
b"\xF2\x01\x00" # 3Gamma Function Disable
97+
b"\x26\x01\x01" # Gamma curve selected
98+
b"\xe0\x0f\x0F\x31\x2B\x0C\x0E\x08\x4E\xF1\x37\x07\x10\x03\x0E\x09\x00" # Set Gamma
99+
b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
100+
b"\x11\x80\x78" # Exit Sleep then delay 0x78 (120ms)
101+
b"\x29\x80\x78" # Display on then delay 0x78 (120ms)
102+
)
103+
104+
display = displayio.Display(display_bus, _INIT_SEQUENCE, width=320, height=240)
105+
106+
sd_spi = busio.SPI(clock=board.IO18, MOSI=board.IO14, MISO=board.IO17)
107+
sd_cs = board.IO12
108+
sdcard = sdcardio.SDCard(sd_spi, sd_cs, baudrate=24_000_000)
109+
vfs = storage.VfsFat(sdcard)
110+
storage.mount(vfs, "/sd")
111+
112+
cam = esp32_camera.Camera(
113+
data_pins=board.CAMERA_DATA,
114+
external_clock_pin=board.CAMERA_XCLK,
115+
pixel_clock_pin=board.CAMERA_PCLK,
116+
vsync_pin=board.CAMERA_VSYNC,
117+
href_pin=board.CAMERA_HREF,
118+
pixel_format=esp32_camera.PixelFormat.RGB565,
119+
frame_size=esp32_camera.FrameSize.QVGA,
120+
i2c=board.I2C(),
121+
external_clock_frequency=20_000_000,
122+
framebuffer_count=2)
123+
124+
def exists(filename):
125+
try:
126+
os.stat(filename)
127+
return True
128+
except OSError as _:
129+
return False
130+
131+
132+
_image_counter = 0
133+
134+
def next_filename(extension="jpg"):
135+
global _image_counter # pylint: disable=global-statement
136+
while True:
137+
filename = f"/sd/img{_image_counter:04d}.{extension}"
138+
if exists(filename):
139+
print(f"File exists: {filename}", end='\r')
140+
_image_counter += 1
141+
continue
142+
print()
143+
return filename
144+
145+
# Pre-cache the next image number
146+
next_filename("gif")
147+
148+
# Blank the whole display
149+
g = displayio.Group()
150+
display.show(g)
151+
display.auto_refresh = False
152+
display.refresh()
153+
154+
def open_next_image(extension="jpg"):
155+
global _image_counter # pylint: disable=global-statement
156+
while True:
157+
filename = next_filename(extension)
158+
print("# writing to", filename)
159+
return open(filename, "wb")
160+
161+
cam.saturation = 3
162+
163+
old_frame = displayio.Bitmap(cam.width, cam.height, 65536)
164+
# Displayed (onion skinned) frame here
165+
onionskin = displayio.Bitmap(cam.width, cam.height, 65536)
166+
167+
ow = (display.width - onionskin.width) // 2
168+
oh = (display.height - onionskin.height) // 2
169+
display_bus.send(42, struct.pack(">hh", ow, onionskin.width + ow - 1))
170+
display_bus.send(43, struct.pack(">hh", oh, onionskin.height + ow - 1))
171+
172+
def wait_record_pressed_update_display(first_frame, camera):
173+
while record_pressed():
174+
pass
175+
while True:
176+
frame = camera.take(1)
177+
print(type(frame))
178+
if record_pressed():
179+
return frame
180+
181+
if first_frame:
182+
# First frame -- display as-is
183+
display_bus.send(44, frame)
184+
else:
185+
bitmaptools.alphablend(onionskin, old_frame, frame, displayio.Colorspace.RGB565_SWAPPED)
186+
display_bus.send(44, onionskin)
187+
188+
def take_stop_motion_gif(n_frames=10, replay_frame_time=.3):
189+
print(f"0/{n_frames}")
190+
frame = wait_record_pressed_update_display(True, cam)
191+
with open_next_image("gif") as f, gifio.GifWriter(f, cam.width, cam.height,
192+
displayio.Colorspace.RGB565_SWAPPED, dither=True) as writer:
193+
writer.add_frame(frame, replay_frame_time)
194+
for i in range(1, n_frames):
195+
print(f"{i}/{n_frames}")
196+
old_frame.blit(0, 0, frame, x1=0, y1=0, x2=cam.width, y2=cam.height)
197+
frame = wait_record_pressed_update_display(False, cam)
198+
writer.add_frame(frame, replay_frame_time)
199+
print("done")
200+
201+
while True:
202+
take_stop_motion_gif()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Jeff Epler for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
Show the live camera image on the viewfinder, then upload to adafruit IO when
7+
the 'BOOT' button is pressed.
8+
"""
9+
10+
import binascii
11+
import ssl
12+
import struct
13+
14+
from adafruit_io.adafruit_io import IO_MQTT
15+
import adafruit_minimqtt.adafruit_minimqtt as MQTT
16+
import board
17+
import dotenv
18+
import esp32_camera
19+
import keypad
20+
import socketpool
21+
import wifi
22+
23+
shutter_button = keypad.Keys((board.BOOT,), value_when_pressed=False)
24+
25+
aio_username = dotenv.get_key("/.env", "AIO_USERNAME")
26+
aio_key = dotenv.get_key("/.env", "AIO_KEY")
27+
28+
image_feed = "image"
29+
30+
pool = socketpool.SocketPool(wifi.radio)
31+
32+
print("Connecting to Adafruit IO")
33+
mqtt_client = MQTT.MQTT(
34+
broker="io.adafruit.com",
35+
username=aio_username,
36+
password=aio_key,
37+
socket_pool=pool,
38+
ssl_context=ssl.create_default_context(),
39+
)
40+
mqtt_client.connect()
41+
io = IO_MQTT(mqtt_client)
42+
43+
44+
print("Initializing camera")
45+
cam = esp32_camera.Camera(
46+
data_pins=board.CAMERA_DATA,
47+
external_clock_pin=board.CAMERA_XCLK,
48+
pixel_clock_pin=board.CAMERA_PCLK,
49+
vsync_pin=board.CAMERA_VSYNC,
50+
href_pin=board.CAMERA_HREF,
51+
pixel_format=esp32_camera.PixelFormat.RGB565,
52+
frame_size=esp32_camera.FrameSize.R240X240,
53+
i2c=board.I2C(),
54+
external_clock_frequency=20_000_000,
55+
framebuffer_count=2,
56+
)
57+
cam.vflip = True
58+
cam.hmirror = True
59+
60+
board.DISPLAY.auto_refresh = False
61+
display_bus = board.DISPLAY.bus
62+
63+
print(cam.width, cam.height)
64+
65+
ow = (board.DISPLAY.width - cam.width) // 2
66+
oh = (board.DISPLAY.height - cam.height) // 2
67+
display_bus.send(42, struct.pack(">hh", ow, cam.width + ow - 1))
68+
display_bus.send(43, struct.pack(">hh", oh, cam.height + ow - 1))
69+
70+
while True:
71+
frame = cam.take(1)
72+
display_bus.send(44, frame)
73+
if (ev := shutter_button.events.get()) and ev.pressed:
74+
cam.reconfigure(
75+
pixel_format=esp32_camera.PixelFormat.JPEG,
76+
frame_size=esp32_camera.FrameSize.SVGA,
77+
)
78+
frame = cam.take(1)
79+
if isinstance(frame, memoryview):
80+
jpeg = frame
81+
print(f"Captured {len(jpeg)} bytes of jpeg data")
82+
83+
# b2a_base64() appends a trailing newline, which IO does not like
84+
encoded_data = binascii.b2a_base64(jpeg).strip()
85+
print(f"Expanded to {len(encoded_data)} for IO upload")
86+
87+
io.publish("image", encoded_data)
88+
cam.reconfigure(
89+
pixel_format=esp32_camera.PixelFormat.RGB565,
90+
frame_size=esp32_camera.FrameSize.R240X240,
91+
)
92+
print(end=".")
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Jeff Epler for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
Upload a jpeg image to Adafruit IO at regular intervals
7+
8+
This example requires:
9+
* ESP32-S3-EYE development kit from Espressif
10+
11+
To use:
12+
* On io.adafruit.com, create a feed named "image" and turn OFF history
13+
* On io.adafruit.com, create a dashboard and add an "image" block
14+
using the feed "image" as its data
15+
* Set up CIRCUITPY/.env with WiFI and Adafruit IO credentials
16+
* Copy the project bundle to CIRCUITPY
17+
"""
18+
19+
import binascii
20+
import io
21+
import ssl
22+
import time
23+
import adafruit_minimqtt.adafruit_minimqtt as MQTT
24+
from adafruit_io.adafruit_io import IO_MQTT
25+
import board
26+
import dotenv
27+
import esp32_camera
28+
import socketpool
29+
import wifi
30+
31+
aio_username = dotenv.get_key('/.env', 'AIO_USERNAME')
32+
aio_key = dotenv.get_key('/.env', 'AIO_KEY')
33+
34+
image_feed = "image"
35+
36+
cam = esp32_camera.Camera(
37+
data_pins=board.CAMERA_DATA,
38+
external_clock_pin=board.CAMERA_XCLK,
39+
pixel_clock_pin=board.CAMERA_PCLK,
40+
vsync_pin=board.CAMERA_VSYNC,
41+
href_pin=board.CAMERA_HREF,
42+
pixel_format=esp32_camera.PixelFormat.JPEG,
43+
frame_size=esp32_camera.FrameSize.SVGA,
44+
i2c=board.I2C(),
45+
external_clock_frequency=20_000_000,
46+
grab_mode=esp32_camera.GrabMode.WHEN_EMPTY)
47+
cam.vflip = True
48+
49+
50+
pool = socketpool.SocketPool(wifi.radio)
51+
52+
print("Connecting to Adafruit IO")
53+
mqtt_client = MQTT.MQTT(
54+
broker="io.adafruit.com",
55+
username=aio_username,
56+
password=aio_key,
57+
socket_pool=pool,
58+
ssl_context=ssl.create_default_context(),
59+
)
60+
mqtt_client.connect()
61+
io = IO_MQTT(mqtt_client)
62+
63+
while True:
64+
frame = cam.take(1)
65+
if isinstance(frame, memoryview):
66+
jpeg = frame
67+
print(f"Captured {len(jpeg)} bytes of jpeg data")
68+
69+
# b2a_base64() appends a trailing newline, which IO does not like
70+
encoded_data = binascii.b2a_base64(jpeg).strip()
71+
print(f"Expanded to {len(encoded_data)} for IO upload")
72+
73+
io.publish("image", encoded_data)
74+
75+
time.sleep(10)

0 commit comments

Comments
 (0)