As of CircuitPython 9, a mount point (folder) named /sd is required on the CIRCUITPY drive. Make sure to create that directory after upgrading CircuitPython.

Install & Use the Demo

For this demo, you'll need to attach an SD card breakout to your Kaluga using the following connections:

  • 5V to 5V, or 3V to 3V3 (depends on breakout)
  • GND to GND
  • CLK to GPIO18
  • MOSI to GPIO14
  • MISO to GPIO10
  • CS to GPIO12

Now, insert a CircuitPython-supported, formatted SD card. Supported formats include FAT16 and FAT32, but not exFAT.

Next, copy the correct bundle to your device. It will automatically reload and start displaying the image from the camera on the LCD.

Click the boot button to capture an image in JPEG format to the SD card.

Click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, and copy the entire lib folder and the code.py file to your CIRCUITPY drive.

# SPDX-FileCopyrightText: Copyright (c) 2023 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

"""
This demo is designed for the Kaluga development kit version 1.3 with the
ILI9341 display. It requires CircuitPython 8.

This demo needs reserved psram properly configured in settings.toml:
CIRCUITPY_RESERVED_PSRAM=1048576

This example also requires an SD card breakout wired as follows:
 * IO18: SD Clock Input
 * IO17: SD Serial Output (MISO)
 * IO14: SD Serial Input (MOSI)
 * IO12: SD Chip Select

Insert a CircuitPython-compatible SD card before powering on the Kaluga.
Press the "BOOT" button to take a photo in BMP format.
"""

import os
import struct

import board
import busio
import displayio
import espcamera
import espidf
import keypad
import sdcardio
import storage

print("Initializing display")
displayio.release_displays()
spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK)
display_bus = displayio.FourWire(
    spi,
    command=board.LCD_D_C,
    chip_select=board.LCD_CS,
    reset=board.LCD_RST,
    baudrate=80_000_000,
)
_INIT_SEQUENCE = (
    b"\x01\x80\x80"  # Software reset then delay 0x80 (128ms)
    b"\xEF\x03\x03\x80\x02"
    b"\xCF\x03\x00\xC1\x30"
    b"\xED\x04\x64\x03\x12\x81"
    b"\xE8\x03\x85\x00\x78"
    b"\xCB\x05\x39\x2C\x00\x34\x02"
    b"\xF7\x01\x20"
    b"\xEA\x02\x00\x00"
    b"\xc0\x01\x23"  # Power control VRH[5:0]
    b"\xc1\x01\x10"  # Power control SAP[2:0];BT[3:0]
    b"\xc5\x02\x3e\x28"  # VCM control
    b"\xc7\x01\x86"  # VCM control2
    b"\x36\x01\x40"  # Memory Access Control
    b"\x37\x01\x00"  # Vertical scroll zero
    b"\x3a\x01\x55"  # COLMOD: Pixel Format Set
    b"\xb1\x02\x00\x18"  # Frame Rate Control (In Normal Mode/Full Colors)
    b"\xb6\x03\x08\x82\x27"  # Display Function Control
    b"\xF2\x01\x00"  # 3Gamma Function Disable
    b"\x26\x01\x01"  # Gamma curve selected
    b"\xe0\x0f\x0F\x31\x2B\x0C\x0E\x08\x4E\xF1\x37\x07\x10\x03\x0E\x09\x00"  # Set Gamma
    b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"  # Set Gamma
    b"\x11\x80\x78"  # Exit Sleep then delay 0x78 (120ms)
    b"\x29\x80\x78"  # Display on then delay 0x78 (120ms)
)

display = displayio.Display(display_bus, _INIT_SEQUENCE, width=320, height=240)

if espidf.get_reserved_psram() < 1047586:
    print("""Place the following line in CIRCUITPY/settings.toml, then hard-reset the board:
CIRCUITPY_RESERVED_PSRAM=1048576
""")
    raise SystemExit

print("Initializing SD card")
sd_spi = busio.SPI(clock=board.IO18, MOSI=board.IO14, MISO=board.IO17)
sd_cs = board.IO12
sdcard = sdcardio.SDCard(sd_spi, sd_cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")

print("Initializing camera")
cam = espcamera.Camera(
    data_pins=board.CAMERA_DATA,
    external_clock_pin=board.CAMERA_XCLK,
    pixel_clock_pin=board.CAMERA_PCLK,
    vsync_pin=board.CAMERA_VSYNC,
    href_pin=board.CAMERA_HREF,
    pixel_format=espcamera.PixelFormat.RGB565,
    frame_size=espcamera.FrameSize.QVGA,
    i2c=board.I2C(),
    external_clock_frequency=20_000_000,
    framebuffer_count=1)
print("initialized")
display.auto_refresh = False

def exists(filename):
    try:
        os.stat(filename)
        return True
    except OSError:
        return False


_image_counter = 0


def open_next_image(extension="jpg"):
    global _image_counter  # pylint: disable=global-statement
    while True:
        filename = f"/sd/img{_image_counter:04d}.{extension}"
        _image_counter += 1
        if exists(filename):
            continue
        print("#", filename)
        return open(filename, "wb")

ow = (display.width - cam.width) // 2
oh = (display.height - cam.height) // 2

k = keypad.Keys([board.IO0], value_when_pressed=False)

while True:
    frame = cam.take(1)
    display_bus.send(42, struct.pack(">hh", ow, cam.width + ow - 1))
    display_bus.send(43, struct.pack(">hh", oh, cam.height + ow - 1))
    display_bus.send(44, frame)
    if (e := k.events.get()) is not None and e.pressed:
        cam.reconfigure(
            pixel_format=espcamera.PixelFormat.JPEG,
            frame_size=espcamera.FrameSize.SVGA,
        )
        frame = cam.take(1)
        if isinstance(frame, memoryview):
            jpeg = frame
            print(f"Captured {len(jpeg)} bytes of jpeg data")

            with open_next_image() as f:
                f.write(jpeg)
        cam.reconfigure(
            pixel_format=espcamera.PixelFormat.RGB565,
            frame_size=espcamera.FrameSize.QVGA,
        )
Folder

Code walkthrough

These functions help with opening the next numbered image in a sequence, so that files are named img0000 img0001 etc.

def exists(filename):
    try:
        os.stat(filename)
        return True
    except OSError:
        return False


_image_counter = 0


def open_next_image(extension="jpg"):
    global _image_counter  # pylint: disable=global-statement
    while True:
        filename = f"/sd/img{_image_counter:04d}.{extension}"
        _image_counter += 1
        if exists(filename):
            continue
        print("#", filename)
        return open(filename, "wb")

Inside the forever loop, when the shutter button is pressed, these lines change the camera to JPEG mode, capture an image to the SD card, and then set the camera back to RGB mode:

if (e := k.events.get()) is not None and e.pressed:
        cam.reconfigure(
            pixel_format=espcamera.PixelFormat.JPEG,
            frame_size=espcamera.FrameSize.SVGA,
        )
        frame = cam.take(1)
        if isinstance(frame, memoryview):
            jpeg = frame
            print(f"Captured {len(jpeg)} bytes of jpeg data")

            with open_next_image() as f:
                f.write(jpeg)
        cam.reconfigure(
            pixel_format=espcamera.PixelFormat.RGB565,
            frame_size=espcamera.FrameSize.QVGA,
        )
USB Type A Plug Breakout Cable with Premium Female Jumpers
If you'd like to connect a USB-capable chip to your USB host, this cable will make the task very simple. There is no converter chip in this cable! Its basically a...
$1.95
In Stock
USB Type A Extension Cable
This handy USB extension cable will make it easy for you to extend your USB cable when it won't reach. The connectors are gold plated for years of reliability. We use these handy...
$6.95
In Stock

This guide was first published on Feb 21, 2023. It was last updated on May 24, 2024.

This page (JPEG Capture Demo) was last updated on May 24, 2024.

Text editor powered by tinymce.