diff --git a/adafruit_fruitjam/__init__.py b/adafruit_fruitjam/__init__.py index 57240b9..35bde3e 100644 --- a/adafruit_fruitjam/__init__.py +++ b/adafruit_fruitjam/__init__.py @@ -193,7 +193,10 @@ def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in f self.sd_check = self.peripherals.sd_check self.play_file = self.peripherals.play_file + self.play_mp3_file = self.peripherals.play_mp3_file self.stop_play = self.peripherals.stop_play + self.volume = self.peripherals.volume + self.audio_output = self.peripherals.audio_output self.image_converter_url = self.network.image_converter_url self.wget = self.network.wget @@ -245,6 +248,26 @@ def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in f gc.collect() + def sync_time(self, **kwargs): + """Set the system RTC via NTP using this FruitJam's Network. + + This is a convenience wrapper for ``self.network.sync_time(...)``. + + :param str server: Override NTP host (defaults to ``NTP_SERVER`` or + ``"pool.ntp.org"`` if unset). (Pass via ``server=...`` in kwargs.) + :param float tz_offset: Override hours from UTC (defaults to ``NTP_TZ``; + ``NTP_DST`` is still added). (Pass via ``tz_offset=...``.) + :param dict tuning: Advanced options dict (optional). Supported keys: + ``timeout`` (float, socket timeout seconds; defaults to ``NTP_TIMEOUT`` or 5.0), + ``cache_seconds`` (int; defaults to ``NTP_CACHE_SECONDS`` or 0), + ``require_year`` (int; defaults to ``NTP_REQUIRE_YEAR`` or 2022). + (Pass via ``tuning={...}``.) + + :returns: Synced time + :rtype: time.struct_time + """ + return self.network.sync_time(**kwargs) + def set_caption(self, caption_text, caption_position, caption_color): """A caption. Requires setting ``caption_font`` in init! diff --git a/adafruit_fruitjam/network.py b/adafruit_fruitjam/network.py index 8c73b98..fc349ac 100644 --- a/adafruit_fruitjam/network.py +++ b/adafruit_fruitjam/network.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries # SPDX-FileCopyrightText: 2025 Tim Cocks, written for Adafruit Industries +# SPDX-FileCopyrightText: 2025 Mikey Sklar, written for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ @@ -25,9 +26,14 @@ """ import gc +import os +import time +import adafruit_connection_manager as acm +import adafruit_ntp import microcontroller import neopixel +import rtc from adafruit_portalbase.network import ( CONTENT_IMAGE, CONTENT_JSON, @@ -209,3 +215,133 @@ def process_image(self, json_data, sd_card=False): # noqa: PLR0912 Too many bra gc.collect() return filename, position + + def sync_time(self, server=None, tz_offset=None, tuning=None): + """ + Set the system RTC via NTP using this Network's Wi-Fi connection. + + Reads optional settings from settings.toml: + + NTP_SERVER – NTP host (default: "pool.ntp.org") + NTP_TZ – timezone offset in hours (float, default: 0) + NTP_DST – extra offset for daylight saving (0=no, 1=yes; default: 0) + NTP_INTERVAL – re-sync interval in seconds (default: 3600, not used internally) + + NTP_TIMEOUT – socket timeout per attempt (seconds, default: 5.0) + NTP_CACHE_SECONDS – cache results, 0 = always fetch fresh (default: 0) + NTP_REQUIRE_YEAR – minimum acceptable year (default: 2022) + + NTP_RETRIES – number of NTP fetch attempts on timeout (default: 8) + NTP_DELAY_S – delay between retries in seconds (default: 1.0) + + Keyword args: + server (str) – override NTP_SERVER + tz_offset (float) – override NTP_TZ (+ NTP_DST still applied) + tuning (dict) – override tuning knobs, e.g.: + { + "timeout": 5.0, + "cache_seconds": 0, + "require_year": 2022, + "retries": 8, + "retry_delay": 1.0, + } + + Returns: + time.struct_time + """ + # Ensure Wi-Fi up + self.connect() + + # Socket pool + pool = acm.get_radio_socketpool(self._wifi.esp) + + # Settings & overrides + server = server or os.getenv("NTP_SERVER") or "pool.ntp.org" + tz = tz_offset if tz_offset is not None else _combined_tz_offset(0.0) + t = tuning or {} + + timeout = float(t.get("timeout", _get_float_env("NTP_TIMEOUT", 5.0))) + cache_seconds = int(t.get("cache_seconds", _get_int_env("NTP_CACHE_SECONDS", 0))) + require_year = int(t.get("require_year", _get_int_env("NTP_REQUIRE_YEAR", 2022))) + ntp_retries = int(t.get("retries", _get_int_env("NTP_RETRIES", 8))) + ntp_delay_s = float(t.get("retry_delay", _get_float_env("NTP_DELAY_S", 1.0))) + + # NTP client + ntp = adafruit_ntp.NTP( + pool, + server=server, + tz_offset=tz, + socket_timeout=timeout, + cache_seconds=cache_seconds, + ) + + # Attempt fetch (retries on timeout) + now = _ntp_get_datetime( + ntp, + connect_cb=self.connect, + retries=ntp_retries, + delay_s=ntp_delay_s, + debug=getattr(self, "_debug", False), + ) + + # Sanity check & commit + if now.tm_year < require_year: + raise RuntimeError("NTP returned an unexpected year; not setting RTC") + + rtc.RTC().datetime = now + return now + + +# ---- Internal helpers to keep sync_time() small and Ruff-friendly ---- + + +def _get_float_env(name, default): + v = os.getenv(name) + try: + return float(v) if v not in {None, ""} else float(default) + except Exception: + return float(default) + + +def _get_int_env(name, default): + v = os.getenv(name) + if v in {None, ""}: + return int(default) + try: + return int(v) + except Exception: + try: + return int(float(v)) # tolerate "5.0" + except Exception: + return int(default) + + +def _combined_tz_offset(base_default): + """Return tz offset hours including DST via env (NTP_TZ + NTP_DST).""" + tz = _get_float_env("NTP_TZ", base_default) + dst = _get_float_env("NTP_DST", 0) + return tz + dst + + +def _ntp_get_datetime(ntp, connect_cb, retries, delay_s, debug=False): + """Fetch ntp.datetime with limited retries on timeout; re-connect between tries.""" + for i in range(retries): + last_exc = None + try: + return ntp.datetime # struct_time + except OSError as e: + last_exc = e + is_timeout = (getattr(e, "errno", None) == 116) or ("ETIMEDOUT" in str(e)) + if not is_timeout: + break + if debug: + print(f"NTP timeout, attempt {i + 1}/{retries}") + connect_cb() # re-assert Wi-Fi using existing policy + time.sleep(delay_s) + continue + except Exception as e: + last_exc = e + break + if last_exc: + raise last_exc + raise RuntimeError("NTP sync failed") diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 461bbcf..096565b 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -27,6 +27,7 @@ """ import os +import time import adafruit_sdcard import adafruit_tlv320 @@ -40,6 +41,7 @@ import picodvi import storage import supervisor +from adafruit_simplemath import map_range from digitalio import DigitalInOut, Direction, Pull from neopixel import NeoPixel @@ -133,33 +135,66 @@ def get_display_config(): class Peripherals: """Peripherals Helper Class for the FruitJam Library + :param audio_output: The audio output interface to use 'speaker' or 'headphone' + :param safe_volume_limit: The maximum volume allowed for the audio output. Default is 0.75. + Using higher values can damage some speakers, change at your own risk. + :param sample_rate: The sample rate to play back audio data in hertz. Default is 11025. + :param bit_depth: The bits per sample of the audio data. Supports 8 and 16 bits. Default is 16. + :param i2c: The I2C bus the audio DAC is connected to. Set as False to disable audio. 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) + def __init__( # noqa: PLR0913, PLR0912 + self, + audio_output: str = "headphone", + safe_volume_limit: float = 0.75, + sample_rate: int = 11025, + bit_depth: int = 16, + i2c: busio.I2C = None, + ): + self.neopixels = NeoPixel(board.NEOPIXEL, 5) if "NEOPIXEL" in dir(board) else None + + self._buttons = None + if "BUTTON1" in dir(board) and "BUTTON2" in dir(board) and "BUTTON3" in dir(board): + self._buttons = [ + DigitalInOut(pin) for pin in (board.BUTTON1, board.BUTTON2, board.BUTTON3) + ] + for switch in self._buttons: + switch.direction = Direction.INPUT + switch.pull = Pull.UP + + if i2c is None: + i2c = board.I2C() + if i2c is False: + self._dac = None + else: + while not i2c.try_lock(): + time.sleep(0.01) + dac_present = 0x18 in i2c.scan() + i2c.unlock() + + if dac_present: + self._dac = adafruit_tlv320.TLV320DAC3100(i2c) + self._dac.configure_clocks( # set sample rate & bit depth + sample_rate=sample_rate, bit_depth=bit_depth + ) + else: + self._dac = None - # set sample rate & bit depth - self._dac.configure_clocks(sample_rate=11030, bit_depth=16) + if "I2S_BCLK" in dir(board) and "I2S_WS" in dir(board) and "I2S_DIN" in dir(board): + self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + else: + self._audio = None - # use headphones - self._dac.headphone_output = True - self._dac.headphone_volume = -15 # dB + if not (0.0 <= safe_volume_limit <= 1.0): + raise ValueError("safe_volume_limit must be between 0.0 and 1.0") + self.safe_volume_limit = safe_volume_limit - self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + self.audio_output = audio_output + self._apply_volume(0.35) self._sd_mounted = False sd_pins_in_use = False @@ -190,65 +225,205 @@ def __init__(self): # sdcard init or mounting failed self._sd_mounted = False + self._mp3_decoder = None + self.wavfile = None + @property def button1(self) -> bool: """ Return whether Button 1 is pressed """ - return not self._buttons[0].value + return self._buttons is not None and not self._buttons[0].value @property def button2(self) -> bool: """ Return whether Button 2 is pressed """ - return not self._buttons[1].value + return self._buttons is not None and not self._buttons[1].value @property def button3(self) -> bool: """ Return whether Button 3 is pressed """ - return not self._buttons[2].value + return self._buttons is not None and 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)] + return self._buttons is not None and True in [not button.value for button in self._buttons] @property - def dac(self): + def dac(self) -> adafruit_tlv320.TLV320DAC3100: + """ + The instance of the ``adafruit_tlv320.TLV320DAC3100`` driver class. + Can be used for lower level DAC control. + """ return self._dac + @dac.setter + def dac(self, value: adafruit_tlv320.TLV320DAC3100) -> None: + if self._dac is not None: + self._dac.reset() + del self._dac + self._dac = value + self._apply_audio_output() + self._apply_volume() + @property - def audio(self): + def audio(self) -> audiobusio.I2SOut: + """ + Instance of ``audiobusio.I2SOut`` ready to play audio through the TLV320 DAC. + """ return self._audio - def sd_check(self): + @audio.setter + def audio(self, value: audiobusio.I2SOut) -> None: + if self._audio is not None: + self._audio.stop() + self._audio.deinit() + del self._audio + self._audio = value + + def sd_check(self) -> bool: + """ + Whether the SD card is mounted. + :return: True if SD is mounted, False otherwise + """ 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 str file_name: The name of the wav file to play. :param bool wait_to_finish: flag to determine if this is a blocking call """ + if self._audio is not None: + # 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: str): + """ + Play a mp3 audio file. + + :param str filename: The name of the mp3 file to play. + """ + if self._audio is not None: + if self._mp3_decoder is None: + from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level - # 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() + self._mp3_decoder = MP3Decoder(filename) + else: + self._mp3_decoder.open(filename) + + 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() + if self._audio is not None: + self._audio.stop() + if self.wavfile is not None: + self.wavfile.close() + + @property + def volume(self) -> float: + """ + The volume level of the Fruit Jam audio output. Valid values are 0.0 - 1.0. + """ + return self._volume + + @volume.setter + def volume(self, volume_level: float) -> None: + """ + :param volume_level: new volume level 0.0 - 1.0 + :return: None + """ + if not (0.0 <= volume_level <= 1.0): + raise ValueError("Volume level must be between 0.0 and 1.0") + + if volume_level > self.safe_volume_limit: + raise ValueError( + f"""Volume level must be less than or equal to +safe_volume_limit: {self.safe_volume_limit}. Using higher values could damage speakers. +To override this limitation set a larger value than {self.safe_volume_limit} +for the safe_volume_limit with the constructor or property.""" + ) + + self._apply_volume(volume_level) + + @property + def audio_output(self) -> str: + """ + The audio output interface. 'speaker' or 'headphone' + :return: + """ + return self._audio_output + + @audio_output.setter + def audio_output(self, audio_output: str) -> None: + """ + :param audio_output: The audio interface to use 'speaker' or 'headphone'. + :return: None + """ + if audio_output not in {"headphone", "speaker"}: + raise ValueError("audio_output must be either 'headphone' or 'speaker'") + self._apply_audio_output(audio_output) + + def _apply_audio_output(self, audio_output: str = None) -> None: + """ + Assign the output of the dac based on the desired setting. + """ + if audio_output is not None: + self._audio_output = audio_output + if self._dac is not None: + self._dac.headphone_output = self._audio_output == "headphone" + self._dac.speaker_output = self._audio_output == "speaker" + + def _apply_volume(self, volume_level: float = None) -> None: + """ + Map the basic volume level to a db value and set it on the DAC. + """ + if volume_level is not None: + self._volume = volume_level + if self._dac is not None: + db_val = map_range(self._volume, 0.0, 1.0, -63, 23) + self._dac.dac_volume = db_val + + def deinit(self) -> None: + """ + Deinitializes Peripherals and releases any hardware resources for reuse. + """ + if self.neopixels is not None: + self.neopixels.deinit() + self.neopixels = None + + if self._buttons is not None: + for button in self._buttons: + button.deinit() + self._buttons = None + + if self._audio is not None: + self._audio.stop() + self._audio.deinit() + self._audio = None + + if self._dac is not None: + self._dac.reset() + self._dac = None + + if self._mp3_decoder is not None: + self._mp3_decoder.deinit() + self._mp3_decoder = None diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..d60cf4b --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,8 @@ +/* SPDX-FileCopyrightText: 2025 Sam Blenny + * SPDX-License-Identifier: MIT + */ + +/* Monkey patch the rtd theme to prevent horizontal stacking of short items + * see https://github.com/readthedocs/sphinx_rtd_theme/issues/1301 + */ +.py.property{display: block !important;} diff --git a/docs/conf.py b/docs/conf.py index 5afc3cc..879bff7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,9 @@ "audiocore", "storage", "terminalio", + "adafruit_connection_manager", + "adafruit_ntp", + "rtc", ] autodoc_preserve_defaults = True @@ -125,6 +128,9 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +# Include extra css to work around rtd theme glitches +html_css_files = ["custom.css"] + # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. diff --git a/examples/fruitjam_headphone.py b/examples/fruitjam_headphone.py index c5d3a94..0c3fb51 100644 --- a/examples/fruitjam_headphone.py +++ b/examples/fruitjam_headphone.py @@ -5,21 +5,16 @@ import adafruit_fruitjam -pobj = adafruit_fruitjam.peripherals.Peripherals() -dac = pobj.dac # use Fruit Jam's codec +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone") -# Route once for headphones -dac.headphone_output = True -dac.speaker_output = False - -FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES_DB = [12, 6, 0, -6, -12] +FILES = ["wav/beep.wav", "wav/dip.wav", "wav/rise.wav"] +VOLUMES = [0.25, 0.35, 0.50, 0.55, 0.60] while True: print("\n=== Headphones Test ===") - for vol in VOLUMES_DB: - dac.dac_volume = vol - print(f"Headphones volume: {vol} dB") + for vol in VOLUMES: + pobj.volume = vol + print(f"Headphones volume: {vol}") for f in FILES: print(f" -> {f}") pobj.play_file(f) diff --git a/examples/fruitjam_neopixels.py b/examples/fruitjam_neopixels.py new file mode 100644 index 0000000..1e250bb --- /dev/null +++ b/examples/fruitjam_neopixels.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +import time + +from adafruit_fruitjam.peripherals import Peripherals + +colors = [0xFF00FF, 0xFFFF00, 0x00FF00] + +fruitjam = Peripherals() +fruitjam.neopixels.brightness = 0.1 + +while True: + fruitjam.neopixels.fill(colors[0]) + time.sleep(0.3) + fruitjam.neopixels.fill(colors[1]) + time.sleep(0.3) + fruitjam.neopixels.fill(colors[2]) + time.sleep(0.3) + fruitjam.neopixels.fill(0x000000) + + for i in range(5): + fruitjam.neopixels[i] = colors[i % len(colors)] + time.sleep(0.1) diff --git a/examples/fruitjam_ntp_settings.toml b/examples/fruitjam_ntp_settings.toml new file mode 100644 index 0000000..4cd2313 --- /dev/null +++ b/examples/fruitjam_ntp_settings.toml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Mikey Sklar for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# Wi-Fi credentials +CIRCUITPY_WIFI_SSID = "YourSSID" +CIRCUITPY_WIFI_PASSWORD = "YourPassword" + +# NTP settings +# Common UTC offsets (hours): +# 0 UTC / Zulu +# 1 CET (Central Europe) +# 2 EET (Eastern Europe) +# 3 FET (Further Eastern Europe) +# -5 EST (Eastern US) +# -6 CST (Central US) +# -7 MST (Mountain US) +# -8 PST (Pacific US) +# -9 AKST (Alaska) +# -10 HST (Hawaii, no DST) + +NTP_SERVER = "pool.ntp.org" # NTP host (default pool.ntp.org) +NTP_TZ = -5 # timezone offset in hours +NTP_DST = 1 # daylight saving (0=no, 1=yes) +NTP_INTERVAL = 3600 # re-sync interval (seconds) + +# Optional tuning +NTP_TIMEOUT = "1.0" # socket timeout in seconds +NTP_CACHE_SECONDS = 0 # cache results (0 = always fetch) +NTP_REQUIRE_YEAR = 2022 # sanity check minimum year + +# Retries +NTP_RETRIES = 8 # number of NTP fetch attempts +NTP_DELAY_S = "1.5" # delay between attempts (seconds) diff --git a/examples/fruitjam_peripherals.py b/examples/fruitjam_peripherals.py index f22dbf8..4decfc5 100644 --- a/examples/fruitjam_peripherals.py +++ b/examples/fruitjam_peripherals.py @@ -7,18 +7,18 @@ import supervisor from audiocore import WaveFile -from adafruit_fruitjam import FruitJam +from adafruit_fruitjam.peripherals import Peripherals colors = [0xFF00FF, 0xFFFF00, 0x00FF00] -fruitjam = FruitJam() +fruitjam = Peripherals() fruitjam.neopixels.brightness = 0.1 fruitjam.neopixels.fill(0xFF00FF) time.sleep(2) fruitjam.neopixels.fill(0x000000) - -wave_file = open("/boot_animation/ada_fruitjam_boot_jingle.wav", "rb") +fruitjam.volume = 0.65 +wave_file = open("/wav/ada_fruitjam_boot_jingle.wav", "rb") wave = WaveFile(wave_file) fruitjam.audio.play(wave) diff --git a/examples/fruitjam_speaker.py b/examples/fruitjam_speaker.py index 02e75fa..e4cb1b3 100644 --- a/examples/fruitjam_speaker.py +++ b/examples/fruitjam_speaker.py @@ -5,21 +5,16 @@ import adafruit_fruitjam -pobj = adafruit_fruitjam.peripherals.Peripherals() -dac = pobj.dac # use Fruit Jam's codec +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="speaker") -# Route once for speaker -dac.headphone_output = False -dac.speaker_output = True - -FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES_DB = [12, 6, 0, -6, -12] +FILES = ["wav/beep.wav", "wav/dip.wav", "wav/rise.wav"] +VOLUMES = [0.25, 0.35, 0.50, 0.55, 0.60] while True: print("\n=== Speaker Test ===") - for vol in VOLUMES_DB: - dac.dac_volume = vol - print(f"Speaker volume: {vol} dB") + for vol in VOLUMES: + pobj.volume = vol + print(f"Speaker volume: {vol}") for f in FILES: print(f" -> {f}") pobj.play_file(f) diff --git a/examples/fruitjam_synthio_headphone.py b/examples/fruitjam_synthio_headphone.py new file mode 100644 index 0000000..2da3782 --- /dev/null +++ b/examples/fruitjam_synthio_headphone.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +import time + +import synthio + +import adafruit_fruitjam + +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone") + +synth = synthio.Synthesizer(sample_rate=44100) +pobj.audio.play(synth) +VOLUMES = [0.25, 0.35, 0.50, 0.55, 0.60] +C_major_scale = [60, 62, 64, 65, 67, 69, 71, 72, 71, 69, 67, 65, 64, 62, 60] +while True: + print("\n=== Synthio Test ===") + for vol in VOLUMES: + pobj.volume = vol + print(f"Volume: {vol}") + for note in C_major_scale: + synth.press(note) + time.sleep(0.1) + synth.release(note) + time.sleep(0.05) + + time.sleep(1.0) diff --git a/examples/fruitjam_time_sync.py b/examples/fruitjam_time_sync.py new file mode 100644 index 0000000..08a6af5 --- /dev/null +++ b/examples/fruitjam_time_sync.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Mikey Sklar for Adafruit Industries +# +# SPDX-License-Identifier: MIT +import time + +from adafruit_fruitjam import FruitJam + +fj = FruitJam() +now = fj.sync_time() +print("RTC set:", now) +print("Localtime:", time.localtime()) diff --git a/examples/wav/ada_fruitjam_boot_jingle.wav b/examples/wav/ada_fruitjam_boot_jingle.wav new file mode 100644 index 0000000..9ce581c Binary files /dev/null and b/examples/wav/ada_fruitjam_boot_jingle.wav differ diff --git a/examples/wav/ada_fruitjam_boot_jingle.wav.license b/examples/wav/ada_fruitjam_boot_jingle.wav.license new file mode 100644 index 0000000..e840764 --- /dev/null +++ b/examples/wav/ada_fruitjam_boot_jingle.wav.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries +# +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/requirements.txt b/requirements.txt index d26fdd0..bd4358c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,6 @@ adafruit-circuitpython-requests adafruit-circuitpython-bitmap-font adafruit-circuitpython-display-text adafruit-circuitpython-sd +adafruit-circuitpython-ntp +adafruit-circuitpython-connectionmanager +adafruit-circuitpython-simplemath diff --git a/ruff.toml b/ruff.toml index 0f3565b..7bd00c3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -17,7 +17,6 @@ extend-select = [ "PLC2401", # non-ascii-name "PLC2801", # unnecessary-dunder-call "PLC3002", # unnecessary-direct-lambda-call - "E999", # syntax-error "PLE0101", # return-in-init "F706", # return-outside-function "F704", # yield-outside-function