Compare commits

...

49 commits
0.4.0 ... main

Author SHA1 Message Date
Scott Shawcroft
e67239b37b
Merge pull request #15 from FoamyGuy/play_mp3_file
Some checks failed
Build CI / test (push) Has been cancelled
play_mp3_file() function
2025-08-28 15:43:20 -07:00
foamyguy
b4f18f7d8f use MP3Decoder.open() 2025-08-28 12:59:41 -05:00
foamyguy
e8a2a77ac6 Merge branch 'refs/heads/main' into play_mp3_file 2025-08-28 12:51:35 -05:00
foamyguy
8ee337c686
Merge pull request #13 from FoamyGuy/volume_api
Some checks failed
Build CI / test (push) Has been cancelled
Volume and output interface API
2025-08-28 12:13:07 -05:00
foamyguy
7682a56c51 clarify wording in the volume warning 2025-08-28 08:44:48 -05:00
foamyguy
6f0204bf34 Merge remote-tracking branch 'foamyguy/volume_api' into volume_api 2025-08-28 08:40:59 -05:00
foamyguy
733ef1b925 use adafruit_simplemath instead of simpleio. Use multi-line string for warning message 2025-08-28 08:40:51 -05:00
foamyguy
45d7cc1232
simplified condition logic
Co-authored-by: Dan Halbert <halbert@halwitz.org>
2025-08-27 18:09:26 -05:00
foamyguy
c41e65bdeb lower default volume limit, update examples, change warning message 2025-08-27 10:46:26 -05:00
foamyguy
f2325f3811 lower default volume 2025-08-27 10:42:40 -05:00
foamyguy
5eabd3d955 Merge branch 'refs/heads/main' into volume_api
# Conflicts:
#	requirements.txt
2025-08-27 10:33:27 -05:00
foamyguy
3f33c18ddf fix _audio_output initialization error 2025-08-27 10:32:40 -05:00
foamyguy
9259a09330
Merge pull request #14 from mikeysklar/ntp-helper
Some checks failed
Build CI / test (push) Has been cancelled
Fruit Jam NTP module
2025-08-25 18:57:36 -05:00
foamyguy
1f46569c1d play_mp3_file() function 2025-08-25 18:50:43 -05:00
Mikey Sklar
cb5d1db622 settings.toml float quote, attribution, duplicate last_exc setting
settings.toml example with double quotes for floating point values

mikeysklar attribution

drop duplicate last_exc
2025-08-25 16:06:37 -07:00
Mikey Sklar
d2ae05eaed helpers for sync_time
ruff was getting upset about too many branches
2025-08-25 14:47:56 -07:00
Mikey Sklar
2e19356d9a ntp constructor dup / retry logic
ntp constructor removed.

dup rtc import had already been removed.

new NTP_RETRIES and NTP_DELAY_S user configurable settings.

```
NTP_RETRIES   = 8     # number of NTP fetch attempts
NTP_DELAY_S   = 1.0   # delay between attempts (seconds)
```

updated settings.toml example

updated docstring for sync_time with new NTP* configurables
2025-08-25 13:19:56 -07:00
Mikey Sklar
96e4575d2e retry logic for ETIMEOUT 2025-08-23 18:15:57 -07:00
Mikey Sklar
75e5fb7ac2 Update __init__.py
blank line missing.
2025-08-22 18:55:00 -07:00
Mikey Sklar
6b86bc95a6 rtc lib mock add 2025-08-22 18:49:03 -07:00
Mikey Sklar
fc388c5ccb mock imports / requirements
ntp and connection_manager libs
2025-08-22 18:37:32 -07:00
Mikey Sklar
d1d0bb0932 refactored to use fruitjam network module
Refactored and "works for me". I'll ask @b-blake to confirm functionality as well.

	•	Added Network.sync_time() method in network.py
	•	Uses adafruit_ntp + adafruit_connection_manager.
	•	Reads optional NTP_* keys from settings.toml.
	•	Sets rtc.RTC().datetime
	•	Added example examples/fruitjam_time_sync.py (sync once, print localtime).
	•	Added example examples/fruitjam_ntp_settings.toml
2025-08-22 18:30:11 -07:00
Mikey Sklar
544adaa447 ntp module
helper code to get time sync with minimal fuss on the Fruit Jam.
2025-08-21 17:41:36 -07:00
foamyguy
eb35d69e1b update printed volume label 2025-08-20 07:54:10 -05:00
foamyguy
4998356604 add synthio example. 2025-08-19 16:31:11 -05:00
foamyguy
a722ba1835 convenience accessors for volume and audio_output properties on main fruit_jam object. 2025-08-19 16:10:41 -05:00
foamyguy
9825076bcf update audio examples to use new volume API. 2025-08-19 16:07:54 -05:00
foamyguy
c332f74892 add simpleio to reqs 2025-08-19 11:32:11 -05:00
foamyguy
f35068b0d3 audio volume and interface APIs 2025-08-19 11:31:14 -05:00
foamyguy
a0327ac8e6
Merge pull request #12 from mikeysklar/headphone-speaker
Some checks failed
Build CI / test (push) Has been cancelled
headphone vs speaker usage
2025-08-14 16:55:05 -05:00
foamyguy
b8b1770da5 use .license files 2025-08-14 16:50:14 -05:00
Mikey Sklar
09af721cf7 ruff whitespace 2025-08-14 13:06:25 -07:00
Mikey Sklar
456851aabf wav file reuse 2025-08-14 13:04:19 -07:00
Mikey Sklar
e95edb3e3d license files for wavs 2025-08-14 12:55:17 -07:00
Mikey Sklar
0e0ab5431f headphone vs speaker usage
Short examples showing how to set volume and play WAV files for both headphone 3.5mm jack and mini speaker.
2025-08-14 12:32:14 -07:00
foamyguy
3aebe8dc9d
Merge pull request #11 from adafruit/TheKitty-patch-1
Some checks failed
Build CI / test (push) Has been cancelled
Update network.py for Issue #7
2025-08-14 10:46:16 -05:00
Anne Barela
d883e687bb
Update network.py
Delete blank line
2025-08-14 11:11:08 -04:00
Anne Barela
f3e13ff5c7
Update network.py per comment
Remove debug print from development
2025-08-14 11:04:54 -04:00
Anne Barela
153b177a3f
Update network.py for Issue #7
Code prints the status_neopixel to stdout when it should only, perhaps, do that on debug.  Fixes #7
2025-08-14 09:39:11 -04:00
foamyguy
9175c448f6
Merge pull request #9 from relic-se/request_display_config-default
Some checks failed
Build CI / test (push) Has been cancelled
Automatically configure display size based on `CIRCUITPY_DISPLAY_WIDTH`
2025-08-12 15:42:27 -05:00
Cooper Dalrymple
6224c6510d Update request_display_config docstring 2025-08-12 14:44:18 -05:00
foamyguy
71dbc2ae38
Merge pull request #8 from relic-se/any_button_pressed-fix
Some checks failed
Build CI / test (push) Has been cancelled
Invert `any_button_pressed` logic for pull-up
2025-08-12 14:12:23 -05:00
Cooper Dalrymple
3cd3c6e596 Fix formatting 2025-08-12 14:08:31 -05:00
Cooper Dalrymple
bfc6140e6d Optimize display height assignment 2025-08-12 13:59:59 -05:00
Cooper Dalrymple
f5b3367c2b Automatically configure display size based on CIRCUITPY_DISPLAY_WIDTH 2025-08-12 13:50:01 -05:00
Cooper Dalrymple
153e078870 Invert any_button_pressed logic for pull-up 2025-08-06 14:49:02 -05:00
foamyguy
17d3868ed6
Merge pull request #6 from FoamyGuy/more_portalbase_stuff
Some checks failed
Build CI / test (push) Has been cancelled
More portalbase APIs, fixes for status_led and caption text
2025-07-24 11:24:41 -05:00
foamyguy
2a5d7e81e7 fix docs build 2025-07-24 10:05:15 -05:00
foamyguy
5932bf058c add sd_check(), play_file(), stop_play(), show_QR(), hide_QR() portalbase APIs. Fix for status_led pin already in use. Fix for caption text is None 2025-07-24 09:01:55 -05:00
17 changed files with 496 additions and 18 deletions

View file

@ -162,7 +162,7 @@ class FruitJam(PortalBase):
network = Network(
status_neopixel=self.peripherals.neopixels
if status_neopixel is None
if status_neopixel is None or status_neopixel == board.NEOPIXEL
else status_neopixel,
esp=esp,
external_spi=spi,
@ -191,14 +191,17 @@ class FruitJam(PortalBase):
# Convenience Shortcuts for compatibility
# self.sd_check = self.peripherals.sd_check
# self.play_file = self.peripherals.play_file
# self.stop_play = self.peripherals.stop_play
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
# self.show_QR = self.graphics.qrcode
# self.hide_QR = self.graphics.hide_QR
self.show_QR = self.graphics.qrcode
self.hide_QR = self.graphics.hide_QR
if default_bg is not None:
self.graphics.set_background(default_bg)
@ -207,7 +210,8 @@ class FruitJam(PortalBase):
print("Init caption")
if caption_font:
self._caption_font = self._load_font(caption_font)
self.set_caption(caption_text, caption_position, caption_color)
if caption_text is not None:
self.set_caption(caption_text, caption_position, caption_color)
if text_font:
if text_position is not None and isinstance(text_position[0], (list, tuple)):
@ -244,6 +248,26 @@ class FruitJam(PortalBase):
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!

View file

@ -66,3 +66,34 @@ class Graphics(GraphicsBase):
if supervisor.runtime.display is None:
request_display_config(640, 480)
super().__init__(supervisor.runtime.display, default_bg=default_bg, debug=debug)
def qrcode(self, qr_data, *, qr_size=1, x=0, y=0, hide_background=False): # noqa: PLR0913 Too many arguments in function definition
"""Display a QR code
:param qr_data: The data for the QR code.
:param int qr_size: The scale of the QR code.
:param x: The x position of upper left corner of the QR code on the display.
:param y: The y position of upper left corner of the QR code on the display.
:param hide_background: Hide the background while showing the QR code.
"""
super().qrcode(
qr_data,
qr_size=qr_size,
x=x,
y=y,
)
if hide_background:
self.display.root_group = self._qr_group
self._qr_only = hide_background
def hide_QR(self):
"""Clear any QR codes that are currently on the screen"""
if self._qr_only:
self.display.root_group = self.root_group
else:
try:
self._qr_group.pop()
except (IndexError, AttributeError): # later test if empty
pass

View file

@ -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 @@ Implementation Notes
"""
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,
@ -88,7 +94,6 @@ class Network(NetworkBase):
image_position=None,
image_dim_json_path=None,
):
print(f"status_neopixel", status_neopixel)
if isinstance(status_neopixel, microcontroller.Pin):
status_led = neopixel.NeoPixel(status_neopixel, 1, brightness=0.2)
elif isinstance(status_neopixel, neopixel.NeoPixel):
@ -210,3 +215,133 @@ class Network(NetworkBase):
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")

View file

@ -26,13 +26,21 @@ Implementation Notes
"""
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 adafruit_simplemath import map_range
from digitalio import DigitalInOut, Direction, Pull
from neopixel import NeoPixel
@ -48,7 +56,7 @@ COLOR_DEPTH_LUT = {
}
def request_display_config(width, height, color_depth=None):
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
@ -56,8 +64,11 @@ def request_display_config(width, height, color_depth=None):
This function will set the initialized display to ``supervisor.runtime.display``
:param width: The width of the display in pixels.
:param height: The height of the display in pixels.
: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
@ -65,9 +76,20 @@ def request_display_config(width, height, color_depth=None):
is 16.
:return: None
"""
if (width, height) not in VALID_DISPLAY_SIZES:
# 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
@ -112,13 +134,16 @@ 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 15
Using higher values can damage some speakers, change at your own risk.
Attributes:
neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board.
See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html
"""
def __init__(self):
def __init__(self, audio_output="headphone", safe_volume_limit=12):
self.neopixels = NeoPixel(board.NEOPIXEL, 5)
self._buttons = []
@ -134,11 +159,44 @@ class Peripherals:
# 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_output = audio_output
self.audio_output = audio_output
self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN)
if safe_volume_limit < 1 or safe_volume_limit > 20:
raise ValueError("safe_volume_limit must be between 1 and 20")
self.safe_volume_limit = safe_volume_limit
self._volume = 7
self._apply_volume()
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:
@ -166,7 +224,7 @@ class Peripherals:
"""
Return whether any button is pressed
"""
return True in [button.value for (i, button) in enumerate(self._buttons)]
return True in [not button.value for (i, button) in enumerate(self._buttons)]
@property
def dac(self):
@ -175,3 +233,100 @@ class Peripherals:
@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):
if self._mp3_decoder is None:
from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level
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()
@property
def volume(self) -> int:
"""
The volume level of the Fruit Jam audio output. Valid values are 1-20.
"""
return self._volume
@volume.setter
def volume(self, volume_level: int) -> None:
"""
:param volume_level: new volume level 1-20
:return: None
"""
if not (1 <= volume_level <= 20):
raise ValueError("Volume level must be between 1 and 20")
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._volume = volume_level
self._apply_volume()
@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 == "headphone":
self._dac.headphone_output = True
self._dac.speaker_output = False
elif audio_output == "speaker":
self._dac.headphone_output = False
self._dac.speaker_output = True
else:
raise ValueError("audio_output must be either 'headphone' or 'speaker'")
def _apply_volume(self) -> None:
"""
Map the basic volume level to a db value and set it on the DAC.
"""
db_val = map_range(self._volume, 1, 20, -63, 23)
self._dac.dac_volume = db_val

View file

@ -30,7 +30,12 @@ autodoc_mock_imports = [
"framebufferio",
"picodvi",
"audiobusio",
"audiocore",
"storage",
"terminalio",
"adafruit_connection_manager",
"adafruit_ntp",
"rtc",
]
autodoc_preserve_defaults = True

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import adafruit_fruitjam
pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone")
FILES = ["beep.wav", "dip.wav", "rise.wav"]
VOLUMES = [5, 7, 10, 11, 12]
while True:
print("\n=== Headphones Test ===")
for vol in VOLUMES:
pobj.volume = vol
print(f"Headphones volume: {vol}")
for f in FILES:
print(f" -> {f}")
pobj.play_file(f)
time.sleep(0.2)
time.sleep(1.0)

View file

@ -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)

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import adafruit_fruitjam
pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="speaker")
FILES = ["beep.wav", "dip.wav", "rise.wav"]
VOLUMES = [5, 7, 10, 11, 12]
while True:
print("\n=== Speaker Test ===")
for vol in VOLUMES:
pobj.volume = vol
print(f"Speaker volume: {vol}")
for f in FILES:
print(f" -> {f}")
pobj.play_file(f)
time.sleep(0.2)
time.sleep(1.0)

View file

@ -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 = [5, 7, 10, 11, 12]
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)

View file

@ -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())

BIN
examples/wav/beep.wav Executable file

Binary file not shown.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries
#
# SPDX-License-Identifier: CC-BY-4.0

BIN
examples/wav/dip.wav Executable file

Binary file not shown.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries
#
# SPDX-License-Identifier: CC-BY-4.0

BIN
examples/wav/rise.wav Executable file

Binary file not shown.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Adafruit Industries
#
# SPDX-License-Identifier: CC-BY-4.0

View file

@ -12,3 +12,7 @@ adafruit-circuitpython-esp32spi
adafruit-circuitpython-requests
adafruit-circuitpython-bitmap-font
adafruit-circuitpython-display-text
adafruit-circuitpython-sd
adafruit-circuitpython-ntp
adafruit-circuitpython-connectionmanager
adafruit-circuitpython-simplemath