diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70ade69..374676d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pycqa/pylint - rev: v2.17.4 + rev: v3.3.1 hooks: - id: pylint name: pylint (library code) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b79ec5b..fe4faae 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,9 @@ # Required version: 2 +sphinx: + configuration: docs/conf.py + build: os: ubuntu-20.04 tools: diff --git a/README.rst b/README.rst index af6a199..48e9ca9 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Introduction ============ -.. image:: https://readthedocs.org/projects/adafruit-circuitpython-pycamera/badge/?version=latest +.. image:: https://readthedocs.org/projects/circuitpython-pycamera/badge/?version=latest :target: https://docs.circuitpython.org/projects/pycamera/en/latest/ :alt: Documentation Status diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py index d019cf5..1ef4c94 100644 --- a/adafruit_pycamera/__init__.py +++ b/adafruit_pycamera/__init__.py @@ -81,9 +81,10 @@ class PyCameraBase: # pylint: disable=too-many-instance-attributes,too-many-public-methods - """Base class for PyCamera hardware""" + """Base class for PyCamera hardware - """Wrapper class for the PyCamera hardware with lots of smarts""" + Wrapper class for the PyCamera hardware with lots of smarts + """ _finalize_firmware_load = ( 0x3022, @@ -238,6 +239,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.overlay_transparency_color = None self.overlay_bmp = None self.combined_bmp = None + self.preview_scale = None + self.overlay_position = [None, None] + self.overlay_scale = 1.0 self.splash = displayio.Group() # Reset display and I/O expander @@ -250,9 +254,6 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self.shutter_button.switch_to_input(Pull.UP) self.shutter = Button(self.shutter_button) - self._cam_reset = DigitalInOut(board.CAMERA_RESET) - self._cam_pwdn = DigitalInOut(board.CAMERA_PWDN) - # AW9523 GPIO expander self._aw = adafruit_aw9523.AW9523(self._i2c, address=0x58) print("Found AW9523") @@ -371,14 +372,6 @@ def init_neopixel(self): def init_camera(self, init_autofocus=True) -> None: """Initialize the camera, by default including autofocus""" - print("reset camera") - self._cam_reset.switch_to_output(False) - self._cam_pwdn.switch_to_output(True) - time.sleep(0.01) - self._cam_pwdn.switch_to_output(False) - time.sleep(0.01) - self._cam_reset.switch_to_output(True) - time.sleep(0.01) print("Initializing camera") self.camera = espcamera.Camera( @@ -387,6 +380,8 @@ def init_camera(self, init_autofocus=True) -> None: pixel_clock_pin=board.CAMERA_PCLK, vsync_pin=board.CAMERA_VSYNC, href_pin=board.CAMERA_HREF, + powerdown_pin=board.CAMERA_PWDN, + reset_pin=board.CAMERA_RESET, pixel_format=espcamera.PixelFormat.RGB565, frame_size=espcamera.FrameSize.HQVGA, i2c=board.I2C(), @@ -452,13 +447,13 @@ def write_camera_list(self, reg_list: Sequence[int]) -> None: def read_camera_register(self, reg: int) -> int: """Read a 1-byte camera register""" - b = bytearray(2) - b[0] = reg >> 8 - b[1] = reg & 0xFF + b_out = bytearray(2) + b_out[0] = reg >> 8 + b_out[1] = reg & 0xFF + b_in = bytearray(1) with self._camera_device as i2c: - i2c.write(b) - i2c.readinto(b, end=1) - return b[0] + i2c.write_then_readinto(b_out, b_in) + return b_in[0] def autofocus_init_from_bitstream(self, firmware: bytes): """Initialize the autofocus engine from a bytestring""" @@ -645,6 +640,8 @@ def resolution(self, res): microcontroller.nvm[_NVM_RESOLUTION] = res self._resolution = res self._res_label.text = self.resolutions[res] + _width = int(self.resolutions[self.resolution].split("x")[0]) + self.preview_scale = 240 / _width self.display.refresh() @property @@ -820,14 +817,14 @@ def live_preview_mode(self): # self.effect = self._effect self.continuous_capture_start() - def open_next_image(self, extension="jpg"): + def open_next_image(self, extension="jpg", filename_prefix="img"): """Return an opened numbered file on the sdcard, such as "img01234.jpg".""" try: os.stat("/sd") except OSError as exc: # no SD card! raise RuntimeError("No SD card mounted") from exc while True: - filename = "/sd/img%04d.%s" % (self._image_counter, extension) + filename = f"/sd/{filename_prefix}{self._image_counter:04d}.{extension}" self._image_counter += 1 try: os.stat(filename) @@ -837,7 +834,7 @@ def open_next_image(self, extension="jpg"): print("Writing to", filename) return open(filename, "wb") - def capture_jpeg(self): + def capture_jpeg(self, filename_prefix="img"): """Capture a jpeg file and save it to the SD card""" try: os.stat("/sd") @@ -855,7 +852,7 @@ def capture_jpeg(self): print(f"Captured {len(jpeg)} bytes of jpeg data") print("Resolution %d x %d" % (self.camera.width, self.camera.height)) - with self.open_next_image() as dest: + with self.open_next_image(filename_prefix=filename_prefix) as dest: chunksize = 16384 for offset in range(0, len(jpeg), chunksize): dest.write(jpeg[offset : offset + chunksize]) @@ -925,13 +922,15 @@ def blit_overlay_into_last_capture(self): self.decoder.decode(photo_bitmap, scale=0, x=0, y=0) - bitmaptools.blit( + bitmaptools.rotozoom( photo_bitmap, self.overlay_bmp, - 0, - 0, - skip_source_index=self.overlay_transparency_color, - skip_dest_index=None, + ox=self.overlay_position[0] if self.overlay_position[0] is not None else 0, + oy=self.overlay_position[1] if self.overlay_position[1] is not None else 0, + px=0 if self.overlay_position[0] is not None else None, + py=0 if self.overlay_position[1] is not None else None, + skip_index=self.overlay_transparency_color, + scale=self.overlay_scale, ) cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED) @@ -947,6 +946,13 @@ def blit_overlay_into_last_capture(self): del cc565_swapped gc.collect() + @property + def last_saved_filename(self) -> str: + """ + The filename of the last image saved. + """ + return self._last_saved_image_filename + def continuous_capture_start(self): """Switch the camera to continuous-capture mode""" pass # pylint: disable=unnecessary-pass @@ -1003,8 +1009,16 @@ def blit(self, bitmap, x_offset=0, y_offset=32): bitmaptools.rotozoom( self.combined_bmp, self.overlay_bmp, - scale=0.75, + scale=self.preview_scale * self.overlay_scale, skip_index=self.overlay_transparency_color, + ox=int(self.overlay_position[0] * self.preview_scale) + if self.overlay_position[0] is not None + else None, + oy=int(self.overlay_position[1] * self.preview_scale) + if self.overlay_position[1] is not None + else None, + px=0 if self.overlay_position[0] is not None else None, + py=0 if self.overlay_position[1] is not None else None, ) bitmap = self.combined_bmp diff --git a/docs/conf.py b/docs/conf.py index cbf1452..9993665 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -137,7 +137,6 @@ import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/examples/camera/code.py b/examples/camera/code.py index 553c035..a03ea64 100644 --- a/examples/camera/code.py +++ b/examples/camera/code.py @@ -2,8 +2,14 @@ # SPDX-FileCopyrightText: 2023 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: Unlicense - +import ssl +import os import time +import socketpool +import adafruit_requests +import rtc +import adafruit_ntp +import wifi import bitmaptools import displayio import gifio @@ -11,6 +17,46 @@ import adafruit_pycamera +# Wifi details are in settings.toml file, also, +# timezone info should be included to allow local time and DST adjustments +# # UTC_OFFSET, if present, will override TZ and DST and no API query will be done +# UTC_OFFSET=-25200 +# # TZ="America/Phoenix" + +UTC_OFFSET = os.getenv("UTC_OFFSET") +TZ = os.getenv("TZ") + +print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}") +SSID = os.getenv("CIRCUITPY_WIFI_SSID") +PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +if SSID and PASSWORD: + wifi.radio.connect( + os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD") + ) + if wifi.radio.connected: + print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!") + print("My IP address is", wifi.radio.ipv4_address) + pool = socketpool.SocketPool(wifi.radio) + + if UTC_OFFSET is None: + requests = adafruit_requests.Session(pool, ssl.create_default_context()) + response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ) + response_as_json = response.json() + UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"] + print(f"UTC_OFFSET: {UTC_OFFSET}") + + ntp = adafruit_ntp.NTP( + pool, server="pool.ntp.org", tz_offset=UTC_OFFSET // 3600 + ) + + print(f"ntp time: {ntp.datetime}") + rtc.RTC().datetime = ntp.datetime + else: + print("Wifi failed to connect. Time not set.") +else: + print("Wifi config not found in settintgs.toml. Time not set.") + pycam = adafruit_pycamera.PyCamera() # pycam.live_preview_mode() @@ -163,7 +209,7 @@ t0 = t1 pycam._mode_label.text = "GIF" # pylint: disable=protected-access print(f"\nfinal size {f.tell()} for {i} frames") - print(f"average framerate {i/(t1-t00)}fps") + print(f"average framerate {i / (t1 - t00)}fps") print(f"best {max(ft)} worst {min(ft)} std. deviation {np.std(ft)}") f.close() pycam.display.refresh() diff --git a/examples/filter/code.py b/examples/filter/code.py index 8d4598c..207b55f 100644 --- a/examples/filter/code.py +++ b/examples/filter/code.py @@ -139,8 +139,7 @@ def sketch(b): def cycle(seq): while True: - for s in seq: - yield s + yield from seq effects_cycle = iter(cycle(effects)) diff --git a/examples/overlay/blinka_emoji_rgb888.bmp b/examples/overlay/blinka_emoji_rgb888.bmp new file mode 100644 index 0000000..6a26a1f Binary files /dev/null and b/examples/overlay/blinka_emoji_rgb888.bmp differ diff --git a/examples/overlay/blinka_emoji_rgb888.bmp.license b/examples/overlay/blinka_emoji_rgb888.bmp.license new file mode 100644 index 0000000..831eb5c --- /dev/null +++ b/examples/overlay/blinka_emoji_rgb888.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py index 480675e..665d4e2 100644 --- a/examples/overlay/code_select.py +++ b/examples/overlay/code_select.py @@ -11,6 +11,11 @@ import traceback import adafruit_pycamera # pylint: disable=import-error +MODE_POSITION = 0 +MODE_SCALE = 1 +CURRENT_MODE = 0 + +int_scale = 100 pycam = adafruit_pycamera.PyCamera() pycam.mode = 0 # only mode 0 (JPEG) will work in this example @@ -34,6 +39,7 @@ pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" pycam.overlay_transparency_color = 0xE007 +pycam.overlay_position = [0, 0] overlay_files = os.listdir("/sd/overlays/") cur_overlay_idx = 0 @@ -49,6 +55,34 @@ print(f"changing overlay to {overlay_files[cur_overlay_idx]}") pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}" + if CURRENT_MODE == MODE_POSITION: + if not pycam.down.value: + pycam.overlay_position[1] += 1 * ( + int(pycam.down.current_duration / 0.3) + 1 + ) + if not pycam.up.value: + pycam.overlay_position[1] -= 1 * (int(pycam.up.current_duration / 0.3) + 1) + if not pycam.left.value: + pycam.overlay_position[0] -= 1 * ( + int(pycam.left.current_duration / 0.3) + 1 + ) + if not pycam.right.value: + pycam.overlay_position[0] += 1 * ( + int(pycam.right.current_duration / 0.3) + 1 + ) + if CURRENT_MODE == MODE_SCALE: + if pycam.down.fell: + int_scale -= 10 + pycam.overlay_scale = int_scale / 100 + print(pycam.overlay_scale) + if pycam.up.fell: + int_scale += 10 + pycam.overlay_scale = int_scale / 100 + print(pycam.overlay_scale) + + if pycam.ok.fell: + CURRENT_MODE = MODE_POSITION if CURRENT_MODE == MODE_SCALE else MODE_SCALE + print(f"Changing mode to: {CURRENT_MODE}") if pycam.shutter.short_count: print("Shutter released") pycam.tone(1200, 0.05) diff --git a/examples/timestamp_filename/code.py b/examples/timestamp_filename/code.py new file mode 100644 index 0000000..05c2a2e --- /dev/null +++ b/examples/timestamp_filename/code.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" simple point-and-shoot camera example. With NTP and internal RTC to + add timestamp to photo filenames. Must install adafruit_ntp library! + Example code assumes WIFI credentials are properly setup and web workflow + enabled in settings.toml. If not, you'll need to add code to manually connect + to your network.""" + +import time +import wifi +import socketpool +import rtc +import adafruit_ntp +import adafruit_pycamera # pylint: disable=import-error + +pool = socketpool.SocketPool(wifi.radio) +ntp = adafruit_ntp.NTP(pool, tz_offset=0) +rtc.RTC().datetime = ntp.datetime + +pycam = adafruit_pycamera.PyCamera() +pycam.mode = 0 # only mode 0 (JPEG) will work in this example + +# User settings - try changing these: +pycam.resolution = 2 # 0-12 preset resolutions: +# 0: 240x240, 1: 320x240, 2: 640x480, 3: 800x600, 4: 1024x768, +# 5: 1280x720, 6: 1280x1024, 7: 1600x1200, 8: 1920x1080, 9: 2048x1536, +# 10: 2560x1440, 11: 2560x1600, 12: 2560x1920 +pycam.led_level = 1 # 0-4 preset brightness levels +pycam.led_color = 0 # 0-7 preset colors: 0: white, 1: green, 2: yellow, 3: red, +# 4: pink, 5: blue, 6: teal, 7: rainbow +pycam.effect = 0 # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red, +# 4: green, 5: blue, 6: sepia, 7: solarize + +print("Simple camera ready.") +pycam.tone(800, 0.1) +pycam.tone(1200, 0.05) + +while True: + pycam.blit(pycam.continuous_capture()) + pycam.keys_debounce() + + if pycam.shutter.short_count: + print("Shutter released") + pycam.tone(1200, 0.05) + pycam.tone(1600, 0.05) + try: + pycam.display_message("snap", color=0x00DD00) + timestamp = "img_{}-{}-{}_{:02}-{:02}-{:02}_".format( + time.localtime().tm_year, + time.localtime().tm_mon, + time.localtime().tm_mday, + time.localtime().tm_hour, + time.localtime().tm_min, + time.localtime().tm_sec, + ) + pycam.capture_jpeg(filename_prefix=timestamp) + pycam.live_preview_mode() + except TypeError as exception: + pycam.display_message("Failed", color=0xFF0000) + time.sleep(0.5) + pycam.live_preview_mode() + except RuntimeError as exception: + 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() + pycam.display.refresh() + + if pycam.card_detect.rose: + print("SD card inserted") + pycam.display_message("Mounting\nSD Card", color=0xFFFFFF) + for _ in range(3): + try: + print("Mounting card") + pycam.mount_sd_card() + print("Success!") + break + except OSError as exception: + print("Retrying!", exception) + time.sleep(0.5) + else: + pycam.display_message("SD Card\nFailed!", color=0xFF0000) + time.sleep(0.5) + pycam.display.refresh()