Skip to content

Commit 7dd0d35

Browse files
authored
Merge pull request #22 from adafruit/timelapse
Timelapse mode
2 parents 3945865 + 5b02b8f commit 7dd0d35

File tree

2 files changed

+248
-5
lines changed

2 files changed

+248
-5
lines changed

adafruit_pycamera/__init__.py

+165-3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
_NVM_RESOLUTION = const(1)
7272
_NVM_EFFECT = const(2)
7373
_NVM_MODE = const(3)
74+
_NVM_TIMELAPSE_RATE = const(4)
75+
_NVM_TIMELAPSE_SUBMODE = const(5)
7476

7577

7678
class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods
@@ -168,7 +170,27 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-pub
168170
"Sepia",
169171
"Solarize",
170172
)
171-
modes = ("JPEG", "GIF", "GBOY", "STOP")
173+
174+
timelapse_rates = (
175+
5,
176+
10,
177+
20,
178+
30,
179+
60,
180+
90,
181+
60 * 2,
182+
60 * 3,
183+
60 * 4,
184+
60 * 5,
185+
60 * 10,
186+
60 * 15,
187+
60 * 30,
188+
60 * 60,
189+
)
190+
191+
timelapse_submodes = ("HiPwr", "MedPwr", "LowPwr")
192+
193+
modes = ("JPEG", "GIF", "GBOY", "STOP", "LAPS")
172194

173195
_INIT_SEQUENCE = (
174196
b"\x01\x80\x78" # _SWRESET and Delay 120ms
@@ -187,6 +209,11 @@ def __init__(self) -> None: # pylint: disable=too-many-statements
187209
self._timestamp = time.monotonic()
188210
self._bigbuf = None
189211
self._botbar = None
212+
self._timelapsebar = None
213+
self.timelapse_rate_label = None
214+
self._timelapsestatus = None
215+
self.timelapsestatus_label = None
216+
self.timelapse_submode_label = None
190217
self._camera_device = None
191218
self._display_bus = None
192219
self._effect_label = None
@@ -249,13 +276,13 @@ def make_debounced_expander_pin(pin_no):
249276
def make_camera_ui(self):
250277
"""Create displayio widgets for the standard camera UI"""
251278
self._sd_label = label.Label(
252-
terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2
279+
terminalio.FONT, text="SD ??", color=0x0, x=170, y=10, scale=2
253280
)
254281
self._effect_label = label.Label(
255282
terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2
256283
)
257284
self._mode_label = label.Label(
258-
terminalio.FONT, text="MODE", color=0xFFFFFF, x=150, y=10, scale=2
285+
terminalio.FONT, text="MODE", color=0xFFFFFF, x=170, y=10, scale=2
259286
)
260287
self._topbar = displayio.Group()
261288
self._res_label = label.Label(
@@ -268,8 +295,23 @@ def make_camera_ui(self):
268295
self._botbar.append(self._effect_label)
269296
self._botbar.append(self._mode_label)
270297

298+
self._timelapsebar = displayio.Group(x=0, y=180)
299+
self.timelapse_submode_label = label.Label(
300+
terminalio.FONT, text="SubM", color=0xFFFFFF, x=160, y=10, scale=2
301+
)
302+
self.timelapse_rate_label = label.Label(
303+
terminalio.FONT, text="Time", color=0xFFFFFF, x=90, y=10, scale=2
304+
)
305+
self.timelapsestatus_label = label.Label(
306+
terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2
307+
)
308+
self._timelapsebar.append(self.timelapse_rate_label)
309+
self._timelapsebar.append(self.timelapsestatus_label)
310+
self._timelapsebar.append(self.timelapse_submode_label)
311+
271312
self.splash.append(self._topbar)
272313
self.splash.append(self._botbar)
314+
self.splash.append(self._timelapsebar)
273315

274316
def init_accelerometer(self):
275317
"""Initialize the accelerometer"""
@@ -338,6 +380,8 @@ def init_camera(self, init_autofocus=True) -> None:
338380
self.camera.saturation = 3
339381
self.resolution = microcontroller.nvm[_NVM_RESOLUTION]
340382
self.mode = microcontroller.nvm[_NVM_MODE]
383+
self.timelapse_rate = microcontroller.nvm[_NVM_TIMELAPSE_RATE]
384+
self.timelapse_submode = microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE]
341385

342386
if init_autofocus:
343387
self.autofocus_init()
@@ -461,6 +505,9 @@ def select_setting(self, setting_name):
461505
self._res_label.text = self.resolutions[self._resolution]
462506
self._mode_label.color = 0xFFFFFF
463507
self._mode_label.background_color = 0x0
508+
self.timelapse_rate_label.color = 0xFFFFFF
509+
self.timelapse_rate_label.background_color = None
510+
464511
if setting_name == "effect":
465512
self._effect_label.color = 0x0
466513
self._effect_label.background_color = 0xFFFFFF
@@ -478,6 +525,13 @@ def select_setting(self, setting_name):
478525
self._res_label.text = "LED CLR"
479526
self._res_label.color = 0x0
480527
self._res_label.background_color = 0xFFFFFF
528+
elif setting_name == "led_color":
529+
self._res_label.text = "LED CLR"
530+
self._res_label.color = 0x0
531+
self._res_label.background_color = 0xFFFFFF
532+
elif setting_name == "timelapse_rate":
533+
self.timelapse_rate_label.color = 0x0
534+
self.timelapse_rate_label.background_color = 0xFFFFFF
481535
self.display.refresh()
482536

483537
@property
@@ -538,6 +592,40 @@ def resolution(self, res):
538592
self._res_label.text = self.resolutions[res]
539593
self.display.refresh()
540594

595+
@property
596+
def timelapse_rate(self):
597+
"""Get or set the amount of time between timelapse shots"""
598+
return self._timelapse_rate
599+
600+
@timelapse_rate.setter
601+
def timelapse_rate(self, setting):
602+
setting = (setting + len(self.timelapse_rates)) % len(self.timelapse_rates)
603+
self._timelapse_rate = setting
604+
if self.timelapse_rates[setting] < 60:
605+
self.timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting]
606+
else:
607+
self.timelapse_rate_label.text = "%d M" % (
608+
self.timelapse_rates[setting] / 60
609+
)
610+
microcontroller.nvm[_NVM_TIMELAPSE_RATE] = setting
611+
self.display.refresh()
612+
613+
@property
614+
def timelapse_submode(self):
615+
"""Get or set the power mode for timelapsing"""
616+
return self._timelapse_submode
617+
618+
@timelapse_submode.setter
619+
def timelapse_submode(self, setting):
620+
setting = (setting + len(self.timelapse_submodes)) % len(
621+
self.timelapse_submodes
622+
)
623+
self._timelapse_submode = setting
624+
self.timelapse_submode_label.text = self.timelapse_submodes[
625+
self._timelapse_submode
626+
]
627+
microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] = setting
628+
541629
def init_display(self):
542630
"""Initialize the TFT display"""
543631
# construct displayio by hand
@@ -799,6 +887,80 @@ def led_color(self, new_color):
799887
else:
800888
self.pixels[:] = colors
801889

890+
def get_camera_autosettings(self):
891+
"""Collect all the settings related to exposure and white balance"""
892+
exposure = (
893+
(self.read_camera_register(0x3500) << 12)
894+
+ (self.read_camera_register(0x3501) << 4)
895+
+ (self.read_camera_register(0x3502) >> 4)
896+
)
897+
white_balance = [
898+
self.read_camera_register(x)
899+
for x in (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405)
900+
]
901+
902+
settings = {
903+
"gain": self.read_camera_register(0x350B),
904+
"exposure": exposure,
905+
"wb": white_balance,
906+
}
907+
return settings
908+
909+
def set_camera_wb(self, wb_register_values=None):
910+
"""Set the camera white balance.
911+
912+
The argument of `None` selects auto white balance, while
913+
a list of 6 numbers sets a specific white balance.
914+
915+
The numbers can come from the datasheet or from
916+
``get_camera_autosettings()["wb"]``."""
917+
if wb_register_values is None:
918+
# just set to auto balance
919+
self.camera.whitebal = True
920+
return
921+
922+
if len(wb_register_values) != 6:
923+
raise RuntimeError("Please pass in 0x3400~0x3405 inclusive!")
924+
925+
self.write_camera_register(0x3212, 0x03)
926+
self.write_camera_register(0x3406, 0x01)
927+
for i, reg_val in enumerate(wb_register_values):
928+
self.write_camera_register(0x3400 + i, reg_val)
929+
self.write_camera_register(0x3212, 0x13)
930+
self.write_camera_register(0x3212, 0xA3)
931+
932+
def set_camera_exposure(self, new_exposure=None):
933+
"""Set the camera's exposure values
934+
935+
The argument of `None` selects auto exposure.
936+
937+
Otherwise, the new exposure data should come from
938+
``get_camera_autosettings()["exposure"]``."""
939+
if new_exposure is None:
940+
# just set auto expose
941+
self.camera.exposure_ctrl = True
942+
return
943+
self.camera.exposure_ctrl = False
944+
945+
self.write_camera_register(0x3500, (new_exposure >> 12) & 0xFF)
946+
self.write_camera_register(0x3501, (new_exposure >> 4) & 0xFF)
947+
self.write_camera_register(0x3502, (new_exposure << 4) & 0xFF)
948+
949+
def set_camera_gain(self, new_gain=None):
950+
"""Set the camera's exposure values
951+
952+
The argument of `None` selects auto gain control.
953+
954+
Otherwise, the new exposure data should come from
955+
``get_camera_autosettings()["gain"]``."""
956+
if new_gain is None:
957+
# just set auto expose
958+
self.camera.gain_ctrl = True
959+
return
960+
961+
self.camera.gain_ctrl = False
962+
self.write_camera_register(0x350B, new_gain)
963+
802964

803965
class PyCamera(PyCameraBase):
804966
"""Wrapper class for the PyCamera hardware"""

examples/camera/code.py

+83-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# SPDX-License-Identifier: Unlicense
44

55
import time
6-
76
import bitmaptools
87
import displayio
98
import gifio
@@ -14,13 +13,24 @@
1413
pycam = adafruit_pycamera.PyCamera()
1514
# pycam.live_preview_mode()
1615

17-
settings = (None, "resolution", "effect", "mode", "led_level", "led_color")
16+
settings = (
17+
None,
18+
"resolution",
19+
"effect",
20+
"mode",
21+
"led_level",
22+
"led_color",
23+
"timelapse_rate",
24+
)
1825
curr_setting = 0
1926

2027
print("Starting!")
2128
# pycam.tone(200, 0.1)
2229
last_frame = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535)
2330
onionskin = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535)
31+
timelapse_remaining = None
32+
timelapse_timestamp = None
33+
2434
while True:
2535
if pycam.mode_text == "STOP" and pycam.stop_motion_frame != 0:
2636
# alpha blend
@@ -34,6 +44,49 @@
3444
last_frame, pycam.continuous_capture(), displayio.Colorspace.RGB565_SWAPPED
3545
)
3646
pycam.blit(last_frame)
47+
elif pycam.mode_text == "LAPS":
48+
if timelapse_remaining is None:
49+
pycam.timelapsestatus_label.text = "STOP"
50+
else:
51+
timelapse_remaining = timelapse_timestamp - time.time()
52+
pycam.timelapsestatus_label.text = f"{timelapse_remaining}s / "
53+
# Manually updating the label text a second time ensures that the label
54+
# is re-painted over the blitted preview.
55+
pycam.timelapse_rate_label.text = pycam.timelapse_rate_label.text
56+
pycam.timelapse_submode_label.text = pycam.timelapse_submode_label.text
57+
58+
# only in high power mode do we continuously preview
59+
if (timelapse_remaining is None) or (
60+
pycam.timelapse_submode_label.text == "HiPwr"
61+
):
62+
pycam.blit(pycam.continuous_capture())
63+
if pycam.timelapse_submode_label.text == "LowPwr" and (
64+
timelapse_remaining is not None
65+
):
66+
pycam.display.brightness = 0.05
67+
else:
68+
pycam.display.brightness = 1
69+
pycam.display.refresh()
70+
71+
if timelapse_remaining is not None and timelapse_remaining <= 0:
72+
# no matter what, show what was just on the camera
73+
pycam.blit(pycam.continuous_capture())
74+
# pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken
75+
try:
76+
pycam.display_message("Snap!", color=0x0000FF)
77+
pycam.capture_jpeg()
78+
except TypeError as e:
79+
pycam.display_message("Failed", color=0xFF0000)
80+
time.sleep(0.5)
81+
except RuntimeError as e:
82+
pycam.display_message("Error\nNo SD Card", color=0xFF0000)
83+
time.sleep(0.5)
84+
pycam.live_preview_mode()
85+
pycam.display.refresh()
86+
pycam.blit(pycam.continuous_capture())
87+
timelapse_timestamp = (
88+
time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1
89+
)
3790
else:
3891
pycam.blit(pycam.continuous_capture())
3992
# print("\t\t", capture_time, blit_time)
@@ -127,6 +180,7 @@
127180
except RuntimeError as e:
128181
pycam.display_message("Error\nNo SD Card", color=0xFF0000)
129182
time.sleep(0.5)
183+
130184
if pycam.card_detect.fell:
131185
print("SD card removed")
132186
pycam.unmount_sd_card()
@@ -152,6 +206,7 @@
152206
print("UP")
153207
key = settings[curr_setting]
154208
if key:
209+
print("getting", key, getattr(pycam, key))
155210
setattr(pycam, key, getattr(pycam, key) + 1)
156211
if pycam.down.fell:
157212
print("DN")
@@ -161,18 +216,44 @@
161216
if pycam.right.fell:
162217
print("RT")
163218
curr_setting = (curr_setting + 1) % len(settings)
219+
if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelapse_rate":
220+
curr_setting = (curr_setting + 1) % len(settings)
164221
print(settings[curr_setting])
165222
# new_res = min(len(pycam.resolutions)-1, pycam.get_resolution()+1)
166223
# pycam.set_resolution(pycam.resolutions[new_res])
167224
pycam.select_setting(settings[curr_setting])
168225
if pycam.left.fell:
169226
print("LF")
170227
curr_setting = (curr_setting - 1 + len(settings)) % len(settings)
228+
if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelaps_rate":
229+
curr_setting = (curr_setting + 1) % len(settings)
171230
print(settings[curr_setting])
172231
pycam.select_setting(settings[curr_setting])
173232
# new_res = max(1, pycam.get_resolution()-1)
174233
# pycam.set_resolution(pycam.resolutions[new_res])
175234
if pycam.select.fell:
176235
print("SEL")
236+
if pycam.mode_text == "LAPS":
237+
pycam.timelapse_submode += 1
238+
pycam.display.refresh()
177239
if pycam.ok.fell:
178240
print("OK")
241+
if pycam.mode_text == "LAPS":
242+
if timelapse_remaining is None: # stopped
243+
print("Starting timelapse")
244+
timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate]
245+
timelapse_timestamp = time.time() + timelapse_remaining + 1
246+
# dont let the camera take over auto-settings
247+
saved_settings = pycam.get_camera_autosettings()
248+
# print(f"Current exposure {saved_settings=}")
249+
pycam.set_camera_exposure(saved_settings["exposure"])
250+
pycam.set_camera_gain(saved_settings["gain"])
251+
pycam.set_camera_wb(saved_settings["wb"])
252+
else: # is running, turn off
253+
print("Stopping timelapse")
254+
255+
timelapse_remaining = None
256+
pycam.camera.exposure_ctrl = True
257+
pycam.set_camera_gain(None) # go back to autogain
258+
pycam.set_camera_wb(None) # go back to autobalance
259+
pycam.set_camera_exposure(None) # go back to auto shutter

0 commit comments

Comments
 (0)