# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT """ `adafruit_fruitjam.peripherals` ================================================================================ Hardware peripherals for Adafruit Fruit Jam * Author(s): Tim Cocks Implementation Notes -------------------- **Hardware:** * `Adafruit Fruit Jam `_ **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads # * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice """ import os import adafruit_sdcard import adafruit_tlv320 import audiobusio import audiocore import board import busio import digitalio import displayio import framebufferio import picodvi import storage import supervisor from digitalio import DigitalInOut, Direction, Pull from neopixel import NeoPixel __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_FruitJam.git" VALID_DISPLAY_SIZES = {(360, 200), (720, 400), (320, 240), (640, 480)} COLOR_DEPTH_LUT = { 360: 16, 320: 16, 720: 8, 640: 8, } def request_display_config(width=None, height=None, color_depth=None): """ Request a display size configuration. If the display is un-initialized, or is currently using a different configuration it will be initialized to the requested width and height. This function will set the initialized display to ``supervisor.runtime.display`` :param width: The width of the display in pixels. Leave unspecified to default to the ``CIRCUITPY_DISPLAY_WIDTH`` environmental variable if provided. Otherwise, a ``ValueError`` exception will be thrown. :param height: The height of the display in pixels. Leave unspecified to default to the appropriate height for the provided width. :param color_depth: The color depth of the display in bits. Valid values are 1, 2, 4, 8, 16, 32. Larger resolutions must use smaller color_depths due to RAM limitations. Default color_depth for 720 and 640 width is 8, and default color_depth for 320 and 360 width is 16. :return: None """ # if user does not specify width, use default configuration if width is None and (width := os.getenv("CIRCUITPY_DISPLAY_WIDTH")) is None: raise ValueError("No CIRCUITPY_DISPLAY_WIDTH specified in settings.toml.") # check that we have a valid display size if (height is not None and (width, height) not in VALID_DISPLAY_SIZES) or ( height is None and width not in [size[0] for size in VALID_DISPLAY_SIZES] ): raise ValueError(f"Invalid display size. Must be one of: {VALID_DISPLAY_SIZES}") # if user does not specify height, use matching height if height is None: height = next((h for w, h in VALID_DISPLAY_SIZES if width == w)) # if user does not specify a requested color_depth if color_depth is None: # use the maximum color depth for given width color_depth = COLOR_DEPTH_LUT[width] requested_config = (width, height, color_depth) if requested_config != get_display_config(): displayio.release_displays() fb = picodvi.Framebuffer( width, height, clk_dp=board.CKP, clk_dn=board.CKN, red_dp=board.D0P, red_dn=board.D0N, green_dp=board.D1P, green_dn=board.D1N, blue_dp=board.D2P, blue_dn=board.D2N, color_depth=color_depth, ) supervisor.runtime.display = framebufferio.FramebufferDisplay(fb) def get_display_config(): """ Get the current display size configuration. :return: display_config: Tuple containing the width, height, and color_depth of the display in pixels and bits respectively. """ display = supervisor.runtime.display if display is not None: display_config = (display.width, display.height, display.framebuffer.color_depth) return display_config else: return (None, None, None) class Peripherals: """Peripherals Helper Class for the FruitJam Library Attributes: neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board. See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ def __init__(self): self.neopixels = NeoPixel(board.NEOPIXEL, 5) self._buttons = [] for pin in (board.BUTTON1, board.BUTTON2, board.BUTTON3): switch = DigitalInOut(pin) switch.direction = Direction.INPUT switch.pull = Pull.UP self._buttons.append(switch) i2c = board.I2C() self._dac = adafruit_tlv320.TLV320DAC3100(i2c) # set sample rate & bit depth self._dac.configure_clocks(sample_rate=11030, bit_depth=16) # use headphones self._dac.headphone_output = True self._dac.headphone_volume = -15 # dB self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) self._sd_mounted = False sd_pins_in_use = False SD_CS = board.SD_CS # try to Connect to the sdcard card and mount the filesystem. try: # initialze CS pin cs = digitalio.DigitalInOut(SD_CS) except ValueError: # likely the SDCard was auto-initialized by the core sd_pins_in_use = True # if placeholder.txt file does not exist if "placeholder.txt" not in os.listdir("/sd/"): self._sd_mounted = True if not sd_pins_in_use: try: # if sd CS pin was not in use # try to initialize and mount the SDCard sdcard = adafruit_sdcard.SDCard( busio.SPI(board.SD_SCK, board.SD_MOSI, board.SD_MISO), cs ) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd") self._sd_mounted = True except OSError: # sdcard init or mounting failed self._sd_mounted = False self._mp3_decoder = None @property def button1(self) -> bool: """ Return whether Button 1 is pressed """ return not self._buttons[0].value @property def button2(self) -> bool: """ Return whether Button 2 is pressed """ return not self._buttons[1].value @property def button3(self) -> bool: """ Return whether Button 3 is pressed """ return not self._buttons[2].value @property def any_button_pressed(self) -> bool: """ Return whether any button is pressed """ return True in [not button.value for (i, button) in enumerate(self._buttons)] @property def dac(self): return self._dac @property def audio(self): return self._audio def sd_check(self): return self._sd_mounted def play_file(self, file_name, wait_to_finish=True): """Play a wav file. :param str file_name: The name of the wav file to play on the speaker. :param bool wait_to_finish: flag to determine if this is a blocking call """ # can't use `with` because we need wavefile to remain open after return self.wavfile = open(file_name, "rb") wavedata = audiocore.WaveFile(self.wavfile) self.audio.play(wavedata) if not wait_to_finish: return while self.audio.playing: pass self.wavfile.close() def play_mp3_file(self, filename): with open(filename, "rb") as f: if self._mp3_decoder is None: from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level self._mp3_decoder = MP3Decoder(f) else: self._mp3_decoder.file = f self.audio.play(self._mp3_decoder) while self.audio.playing: pass def stop_play(self): """Stops playing a wav file.""" self.audio.stop() if self.wavfile is not None: self.wavfile.close()