Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions adafruit_fruitjam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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!

Expand Down
136 changes: 136 additions & 0 deletions adafruit_fruitjam/network.py
Original file line number Diff line number Diff line change
@@ -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
"""
Expand All @@ -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,
Expand Down Expand Up @@ -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")
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"audiocore",
"storage",
"terminalio",
"adafruit_connection_manager",
"adafruit_ntp",
"rtc",
]

autodoc_preserve_defaults = True
Expand Down
33 changes: 33 additions & 0 deletions examples/fruitjam_ntp_settings.toml
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions examples/fruitjam_time_sync.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ adafruit-circuitpython-requests
adafruit-circuitpython-bitmap-font
adafruit-circuitpython-display-text
adafruit-circuitpython-sd
adafruit-circuitpython-ntp
adafruit-circuitpython-connectionmanager