diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index 89bb283..718d082 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -63,14 +63,16 @@ _AW_CARDDET = const(8) _AW_SDPWR = const(9) _AW_DOWN = const(15) -_AW_LEFT = const(14) +_AW_RIGHT = const(14) _AW_UP = const(13) -_AW_RIGHT = const(12) +_AW_LEFT = const(12) _AW_OK = const(11) _NVM_RESOLUTION = const(1) _NVM_EFFECT = const(2) _NVM_MODE = const(3) +_NVM_TIMELAPSE_RATE = const(4) +_NVM_TIMELAPSE_SUBMODE = const(5) 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 "Sepia", "Solarize", ) - modes = ("JPEG", "GIF", "GBOY", "STOP") + + timelapse_rates = ( + 5, + 10, + 20, + 30, + 60, + 90, + 60 * 2, + 60 * 3, + 60 * 4, + 60 * 5, + 60 * 10, + 60 * 15, + 60 * 30, + 60 * 60, + ) + + timelapse_submodes = ("HiPwr", "MedPwr", "LowPwr") + + modes = ("JPEG", "GIF", "GBOY", "STOP", "LAPS") _INIT_SEQUENCE = ( b"\x01\x80\x78" # _SWRESET and Delay 120ms @@ -187,6 +209,11 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._timestamp = time.monotonic() self._bigbuf = None self._botbar = None + self._timelapsebar = None + self.timelapse_rate_label = None + self._timelapsestatus = None + self.timelapsestatus_label = None + self.timelapse_submode_label = None self._camera_device = None self._display_bus = None self._effect_label = None @@ -249,13 +276,13 @@ def make_debounced_expander_pin(pin_no): def make_camera_ui(self): """Create displayio widgets for the standard camera UI""" self._sd_label = label.Label( - terminalio.FONT, text="SD ??", color=0x0, x=150, y=10, scale=2 + terminalio.FONT, text="SD ??", color=0x0, x=170, y=10, scale=2 ) self._effect_label = label.Label( terminalio.FONT, text="EFFECT", color=0xFFFFFF, x=4, y=10, scale=2 ) self._mode_label = label.Label( - terminalio.FONT, text="MODE", color=0xFFFFFF, x=150, y=10, scale=2 + terminalio.FONT, text="MODE", color=0xFFFFFF, x=170, y=10, scale=2 ) self._topbar = displayio.Group() self._res_label = label.Label( @@ -268,8 +295,23 @@ def make_camera_ui(self): self._botbar.append(self._effect_label) self._botbar.append(self._mode_label) + self._timelapsebar = displayio.Group(x=0, y=180) + self.timelapse_submode_label = label.Label( + terminalio.FONT, text="SubM", color=0xFFFFFF, x=160, y=10, scale=2 + ) + self.timelapse_rate_label = label.Label( + terminalio.FONT, text="Time", color=0xFFFFFF, x=90, y=10, scale=2 + ) + self.timelapsestatus_label = label.Label( + terminalio.FONT, text="Status", color=0xFFFFFF, x=0, y=10, scale=2 + ) + self._timelapsebar.append(self.timelapse_rate_label) + self._timelapsebar.append(self.timelapsestatus_label) + self._timelapsebar.append(self.timelapse_submode_label) + self.splash.append(self._topbar) self.splash.append(self._botbar) + self.splash.append(self._timelapsebar) def init_accelerometer(self): """Initialize the accelerometer""" @@ -338,6 +380,8 @@ def init_camera(self, init_autofocus=True) -> None: self.camera.saturation = 3 self.resolution = microcontroller.nvm[_NVM_RESOLUTION] self.mode = microcontroller.nvm[_NVM_MODE] + self.timelapse_rate = microcontroller.nvm[_NVM_TIMELAPSE_RATE] + self.timelapse_submode = microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] if init_autofocus: self.autofocus_init() @@ -461,6 +505,9 @@ def select_setting(self, setting_name): self._res_label.text = self.resolutions[self._resolution] self._mode_label.color = 0xFFFFFF self._mode_label.background_color = 0x0 + self.timelapse_rate_label.color = 0xFFFFFF + self.timelapse_rate_label.background_color = None + if setting_name == "effect": self._effect_label.color = 0x0 self._effect_label.background_color = 0xFFFFFF @@ -478,6 +525,13 @@ def select_setting(self, setting_name): self._res_label.text = "LED CLR" self._res_label.color = 0x0 self._res_label.background_color = 0xFFFFFF + elif setting_name == "led_color": + self._res_label.text = "LED CLR" + self._res_label.color = 0x0 + self._res_label.background_color = 0xFFFFFF + elif setting_name == "timelapse_rate": + self.timelapse_rate_label.color = 0x0 + self.timelapse_rate_label.background_color = 0xFFFFFF self.display.refresh() @property @@ -538,6 +592,40 @@ def resolution(self, res): self._res_label.text = self.resolutions[res] self.display.refresh() + @property + def timelapse_rate(self): + """Get or set the amount of time between timelapse shots""" + return self._timelapse_rate + + @timelapse_rate.setter + def timelapse_rate(self, setting): + setting = (setting + len(self.timelapse_rates)) % len(self.timelapse_rates) + self._timelapse_rate = setting + if self.timelapse_rates[setting] < 60: + self.timelapse_rate_label.text = "%d S" % self.timelapse_rates[setting] + else: + self.timelapse_rate_label.text = "%d M" % ( + self.timelapse_rates[setting] / 60 + ) + microcontroller.nvm[_NVM_TIMELAPSE_RATE] = setting + self.display.refresh() + + @property + def timelapse_submode(self): + """Get or set the power mode for timelapsing""" + return self._timelapse_submode + + @timelapse_submode.setter + def timelapse_submode(self, setting): + setting = (setting + len(self.timelapse_submodes)) % len( + self.timelapse_submodes + ) + self._timelapse_submode = setting + self.timelapse_submode_label.text = self.timelapse_submodes[ + self._timelapse_submode + ] + microcontroller.nvm[_NVM_TIMELAPSE_SUBMODE] = setting + def init_display(self): """Initialize the TFT display""" # construct displayio by hand @@ -799,6 +887,80 @@ def led_color(self, new_color): else: self.pixels[:] = colors + def get_camera_autosettings(self): + """Collect all the settings related to exposure and white balance""" + exposure = ( + (self.read_camera_register(0x3500) << 12) + + (self.read_camera_register(0x3501) << 4) + + (self.read_camera_register(0x3502) >> 4) + ) + white_balance = [ + self.read_camera_register(x) + for x in (0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405) + ] + + settings = { + "gain": self.read_camera_register(0x350B), + "exposure": exposure, + "wb": white_balance, + } + return settings + + def set_camera_wb(self, wb_register_values=None): + """Set the camera white balance. + + The argument of `None` selects auto white balance, while + a list of 6 numbers sets a specific white balance. + + The numbers can come from the datasheet or from + ``get_camera_autosettings()["wb"]``.""" + if wb_register_values is None: + # just set to auto balance + self.camera.whitebal = True + return + + if len(wb_register_values) != 6: + raise RuntimeError("Please pass in 0x3400~0x3405 inclusive!") + + self.write_camera_register(0x3212, 0x03) + self.write_camera_register(0x3406, 0x01) + for i, reg_val in enumerate(wb_register_values): + self.write_camera_register(0x3400 + i, reg_val) + self.write_camera_register(0x3212, 0x13) + self.write_camera_register(0x3212, 0xA3) + + def set_camera_exposure(self, new_exposure=None): + """Set the camera's exposure values + + The argument of `None` selects auto exposure. + + Otherwise, the new exposure data should come from + ``get_camera_autosettings()["exposure"]``.""" + if new_exposure is None: + # just set auto expose + self.camera.exposure_ctrl = True + return + self.camera.exposure_ctrl = False + + self.write_camera_register(0x3500, (new_exposure >> 12) & 0xFF) + self.write_camera_register(0x3501, (new_exposure >> 4) & 0xFF) + self.write_camera_register(0x3502, (new_exposure << 4) & 0xFF) + + def set_camera_gain(self, new_gain=None): + """Set the camera's exposure values + + The argument of `None` selects auto gain control. + + Otherwise, the new exposure data should come from + ``get_camera_autosettings()["gain"]``.""" + if new_gain is None: + # just set auto expose + self.camera.gain_ctrl = True + return + + self.camera.gain_ctrl = False + self.write_camera_register(0x350B, new_gain) + class PyCamera(PyCameraBase): """Wrapper class for the PyCamera hardware""" diff --git a/examples/camera/code.py b/examples/camera/code.py index ddb0d5e..4f8c0a5 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Unlicense import time - import bitmaptools import displayio import gifio @@ -14,13 +13,24 @@ pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() -settings = (None, "resolution", "effect", "mode", "led_level", "led_color") +settings = ( + None, + "resolution", + "effect", + "mode", + "led_level", + "led_color", + "timelapse_rate", +) curr_setting = 0 print("Starting!") # pycam.tone(200, 0.1) last_frame = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) onionskin = displayio.Bitmap(pycam.camera.width, pycam.camera.height, 65535) +timelapse_remaining = None +timelapse_timestamp = None + while True: if pycam.mode_text == "STOP" and pycam.stop_motion_frame != 0: # alpha blend @@ -34,6 +44,49 @@ last_frame, pycam.continuous_capture(), displayio.Colorspace.RGB565_SWAPPED ) pycam.blit(last_frame) + elif pycam.mode_text == "LAPS": + if timelapse_remaining is None: + pycam.timelapsestatus_label.text = "STOP" + else: + timelapse_remaining = timelapse_timestamp - time.time() + pycam.timelapsestatus_label.text = f"{timelapse_remaining}s / " + # Manually updating the label text a second time ensures that the label + # is re-painted over the blitted preview. + pycam.timelapse_rate_label.text = pycam.timelapse_rate_label.text + pycam.timelapse_submode_label.text = pycam.timelapse_submode_label.text + + # only in high power mode do we continuously preview + if (timelapse_remaining is None) or ( + pycam.timelapse_submode_label.text == "HiPwr" + ): + pycam.blit(pycam.continuous_capture()) + if pycam.timelapse_submode_label.text == "LowPwr" and ( + timelapse_remaining is not None + ): + pycam.display.brightness = 0.05 + else: + pycam.display.brightness = 1 + pycam.display.refresh() + + if timelapse_remaining is not None and timelapse_remaining <= 0: + # no matter what, show what was just on the camera + pycam.blit(pycam.continuous_capture()) + # pycam.tone(200, 0.1) # uncomment to add a beep when a photo is taken + try: + pycam.display_message("Snap!", color=0x0000FF) + pycam.capture_jpeg() + except TypeError as e: + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + except RuntimeError as e: + pycam.display_message("Error\nNo SD Card", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + pycam.display.refresh() + pycam.blit(pycam.continuous_capture()) + timelapse_timestamp = ( + time.time() + pycam.timelapse_rates[pycam.timelapse_rate] + 1 + ) else: pycam.blit(pycam.continuous_capture()) # print("\t\t", capture_time, blit_time) @@ -127,6 +180,7 @@ except RuntimeError as e: pycam.display_message("Error\nNo SD Card", color=0xFF0000) time.sleep(0.5) + if pycam.card_detect.fell: print("SD card removed") pycam.unmount_sd_card() @@ -152,27 +206,54 @@ print("UP") key = settings[curr_setting] if key: + print("getting", key, getattr(pycam, key)) setattr(pycam, key, getattr(pycam, key) + 1) if pycam.down.fell: print("DN") key = settings[curr_setting] if key: setattr(pycam, key, getattr(pycam, key) - 1) - if pycam.left.fell: - print("LF") + if pycam.right.fell: + print("RT") curr_setting = (curr_setting + 1) % len(settings) + if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelapse_rate": + curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) # new_res = min(len(pycam.resolutions)-1, pycam.get_resolution()+1) # pycam.set_resolution(pycam.resolutions[new_res]) pycam.select_setting(settings[curr_setting]) - if pycam.right.fell: - print("RT") + if pycam.left.fell: + print("LF") curr_setting = (curr_setting - 1 + len(settings)) % len(settings) + if pycam.mode_text != "LAPS" and settings[curr_setting] == "timelaps_rate": + curr_setting = (curr_setting + 1) % len(settings) print(settings[curr_setting]) pycam.select_setting(settings[curr_setting]) # new_res = max(1, pycam.get_resolution()-1) # pycam.set_resolution(pycam.resolutions[new_res]) if pycam.select.fell: print("SEL") + if pycam.mode_text == "LAPS": + pycam.timelapse_submode += 1 + pycam.display.refresh() if pycam.ok.fell: print("OK") + if pycam.mode_text == "LAPS": + if timelapse_remaining is None: # stopped + print("Starting timelapse") + timelapse_remaining = pycam.timelapse_rates[pycam.timelapse_rate] + timelapse_timestamp = time.time() + timelapse_remaining + 1 + # dont let the camera take over auto-settings + saved_settings = pycam.get_camera_autosettings() + # print(f"Current exposure {saved_settings=}") + pycam.set_camera_exposure(saved_settings["exposure"]) + pycam.set_camera_gain(saved_settings["gain"]) + pycam.set_camera_wb(saved_settings["wb"]) + else: # is running, turn off + print("Stopping timelapse") + + timelapse_remaining = None + pycam.camera.exposure_ctrl = True + pycam.set_camera_gain(None) # go back to autogain + pycam.set_camera_wb(None) # go back to autobalance + pycam.set_camera_exposure(None) # go back to auto shutter