Skip to content

Commit 5eabd3d

Browse files
committed
Merge branch 'refs/heads/main' into volume_api
# Conflicts: # requirements.txt
2 parents 3f33c18 + 9259a09 commit 5eabd3d

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

adafruit_fruitjam/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in f
247247

248248
gc.collect()
249249

250+
def sync_time(self, **kwargs):
251+
"""Set the system RTC via NTP using this FruitJam's Network.
252+
253+
This is a convenience wrapper for ``self.network.sync_time(...)``.
254+
255+
:param str server: Override NTP host (defaults to ``NTP_SERVER`` or
256+
``"pool.ntp.org"`` if unset). (Pass via ``server=...`` in kwargs.)
257+
:param float tz_offset: Override hours from UTC (defaults to ``NTP_TZ``;
258+
``NTP_DST`` is still added). (Pass via ``tz_offset=...``.)
259+
:param dict tuning: Advanced options dict (optional). Supported keys:
260+
``timeout`` (float, socket timeout seconds; defaults to ``NTP_TIMEOUT`` or 5.0),
261+
``cache_seconds`` (int; defaults to ``NTP_CACHE_SECONDS`` or 0),
262+
``require_year`` (int; defaults to ``NTP_REQUIRE_YEAR`` or 2022).
263+
(Pass via ``tuning={...}``.)
264+
265+
:returns: Synced time
266+
:rtype: time.struct_time
267+
"""
268+
return self.network.sync_time(**kwargs)
269+
250270
def set_caption(self, caption_text, caption_position, caption_color):
251271
"""A caption. Requires setting ``caption_font`` in init!
252272

adafruit_fruitjam/network.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries
22
# SPDX-FileCopyrightText: 2025 Tim Cocks, written for Adafruit Industries
3+
# SPDX-FileCopyrightText: 2025 Mikey Sklar, written for Adafruit Industries
34
#
45
# SPDX-License-Identifier: Unlicense
56
"""
@@ -25,9 +26,14 @@
2526
"""
2627

2728
import gc
29+
import os
30+
import time
2831

32+
import adafruit_connection_manager as acm
33+
import adafruit_ntp
2934
import microcontroller
3035
import neopixel
36+
import rtc
3137
from adafruit_portalbase.network import (
3238
CONTENT_IMAGE,
3339
CONTENT_JSON,
@@ -209,3 +215,133 @@ def process_image(self, json_data, sd_card=False): # noqa: PLR0912 Too many bra
209215
gc.collect()
210216

211217
return filename, position
218+
219+
def sync_time(self, server=None, tz_offset=None, tuning=None):
220+
"""
221+
Set the system RTC via NTP using this Network's Wi-Fi connection.
222+
223+
Reads optional settings from settings.toml:
224+
225+
NTP_SERVER – NTP host (default: "pool.ntp.org")
226+
NTP_TZ – timezone offset in hours (float, default: 0)
227+
NTP_DST – extra offset for daylight saving (0=no, 1=yes; default: 0)
228+
NTP_INTERVAL – re-sync interval in seconds (default: 3600, not used internally)
229+
230+
NTP_TIMEOUT – socket timeout per attempt (seconds, default: 5.0)
231+
NTP_CACHE_SECONDS – cache results, 0 = always fetch fresh (default: 0)
232+
NTP_REQUIRE_YEAR – minimum acceptable year (default: 2022)
233+
234+
NTP_RETRIES – number of NTP fetch attempts on timeout (default: 8)
235+
NTP_DELAY_S – delay between retries in seconds (default: 1.0)
236+
237+
Keyword args:
238+
server (str) – override NTP_SERVER
239+
tz_offset (float) – override NTP_TZ (+ NTP_DST still applied)
240+
tuning (dict) – override tuning knobs, e.g.:
241+
{
242+
"timeout": 5.0,
243+
"cache_seconds": 0,
244+
"require_year": 2022,
245+
"retries": 8,
246+
"retry_delay": 1.0,
247+
}
248+
249+
Returns:
250+
time.struct_time
251+
"""
252+
# Ensure Wi-Fi up
253+
self.connect()
254+
255+
# Socket pool
256+
pool = acm.get_radio_socketpool(self._wifi.esp)
257+
258+
# Settings & overrides
259+
server = server or os.getenv("NTP_SERVER") or "pool.ntp.org"
260+
tz = tz_offset if tz_offset is not None else _combined_tz_offset(0.0)
261+
t = tuning or {}
262+
263+
timeout = float(t.get("timeout", _get_float_env("NTP_TIMEOUT", 5.0)))
264+
cache_seconds = int(t.get("cache_seconds", _get_int_env("NTP_CACHE_SECONDS", 0)))
265+
require_year = int(t.get("require_year", _get_int_env("NTP_REQUIRE_YEAR", 2022)))
266+
ntp_retries = int(t.get("retries", _get_int_env("NTP_RETRIES", 8)))
267+
ntp_delay_s = float(t.get("retry_delay", _get_float_env("NTP_DELAY_S", 1.0)))
268+
269+
# NTP client
270+
ntp = adafruit_ntp.NTP(
271+
pool,
272+
server=server,
273+
tz_offset=tz,
274+
socket_timeout=timeout,
275+
cache_seconds=cache_seconds,
276+
)
277+
278+
# Attempt fetch (retries on timeout)
279+
now = _ntp_get_datetime(
280+
ntp,
281+
connect_cb=self.connect,
282+
retries=ntp_retries,
283+
delay_s=ntp_delay_s,
284+
debug=getattr(self, "_debug", False),
285+
)
286+
287+
# Sanity check & commit
288+
if now.tm_year < require_year:
289+
raise RuntimeError("NTP returned an unexpected year; not setting RTC")
290+
291+
rtc.RTC().datetime = now
292+
return now
293+
294+
295+
# ---- Internal helpers to keep sync_time() small and Ruff-friendly ----
296+
297+
298+
def _get_float_env(name, default):
299+
v = os.getenv(name)
300+
try:
301+
return float(v) if v not in {None, ""} else float(default)
302+
except Exception:
303+
return float(default)
304+
305+
306+
def _get_int_env(name, default):
307+
v = os.getenv(name)
308+
if v in {None, ""}:
309+
return int(default)
310+
try:
311+
return int(v)
312+
except Exception:
313+
try:
314+
return int(float(v)) # tolerate "5.0"
315+
except Exception:
316+
return int(default)
317+
318+
319+
def _combined_tz_offset(base_default):
320+
"""Return tz offset hours including DST via env (NTP_TZ + NTP_DST)."""
321+
tz = _get_float_env("NTP_TZ", base_default)
322+
dst = _get_float_env("NTP_DST", 0)
323+
return tz + dst
324+
325+
326+
def _ntp_get_datetime(ntp, connect_cb, retries, delay_s, debug=False):
327+
"""Fetch ntp.datetime with limited retries on timeout; re-connect between tries."""
328+
for i in range(retries):
329+
last_exc = None
330+
try:
331+
return ntp.datetime # struct_time
332+
except OSError as e:
333+
last_exc = e
334+
is_timeout = (getattr(e, "errno", None) == 116) or ("ETIMEDOUT" in str(e))
335+
if not is_timeout:
336+
break
337+
if debug:
338+
print(f"NTP timeout, attempt {i + 1}/{retries}")
339+
connect_cb() # re-assert Wi-Fi using existing policy
340+
time.sleep(delay_s)
341+
continue
342+
except Exception as e:
343+
last_exc = e
344+
break
345+
if last_exc:
346+
raise last_exc
347+
raise RuntimeError("NTP sync failed")

docs/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"audiocore",
3434
"storage",
3535
"terminalio",
36+
"adafruit_connection_manager",
37+
"adafruit_ntp",
38+
"rtc",
3639
]
3740

3841
autodoc_preserve_defaults = True
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Mikey Sklar for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# Wi-Fi credentials
5+
CIRCUITPY_WIFI_SSID = "YourSSID"
6+
CIRCUITPY_WIFI_PASSWORD = "YourPassword"
7+
8+
# NTP settings
9+
# Common UTC offsets (hours):
10+
# 0 UTC / Zulu
11+
# 1 CET (Central Europe)
12+
# 2 EET (Eastern Europe)
13+
# 3 FET (Further Eastern Europe)
14+
# -5 EST (Eastern US)
15+
# -6 CST (Central US)
16+
# -7 MST (Mountain US)
17+
# -8 PST (Pacific US)
18+
# -9 AKST (Alaska)
19+
# -10 HST (Hawaii, no DST)
20+
21+
NTP_SERVER = "pool.ntp.org" # NTP host (default pool.ntp.org)
22+
NTP_TZ = -5 # timezone offset in hours
23+
NTP_DST = 1 # daylight saving (0=no, 1=yes)
24+
NTP_INTERVAL = 3600 # re-sync interval (seconds)
25+
26+
# Optional tuning
27+
NTP_TIMEOUT = "1.0" # socket timeout in seconds
28+
NTP_CACHE_SECONDS = 0 # cache results (0 = always fetch)
29+
NTP_REQUIRE_YEAR = 2022 # sanity check minimum year
30+
31+
# Retries
32+
NTP_RETRIES = 8 # number of NTP fetch attempts
33+
NTP_DELAY_S = "1.5" # delay between attempts (seconds)

examples/fruitjam_time_sync.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Mikey Sklar for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import time
5+
6+
from adafruit_fruitjam import FruitJam
7+
8+
fj = FruitJam()
9+
now = fj.sync_time()
10+
print("RTC set:", now)
11+
print("Localtime:", time.localtime())

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ adafruit-circuitpython-requests
1313
adafruit-circuitpython-bitmap-font
1414
adafruit-circuitpython-display-text
1515
adafruit-circuitpython-sd
16+
adafruit-circuitpython-ntp
17+
adafruit-circuitpython-connectionmanager
1618
adafruit-circuitpython-simpleio

0 commit comments

Comments
 (0)