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
This commit is contained in:
parent
544adaa447
commit
d1d0bb0932
6 changed files with 139 additions and 192 deletions
|
|
@ -245,6 +245,23 @@ class FruitJam(PortalBase):
|
||||||
|
|
||||||
gc.collect()
|
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(...)`` so
|
||||||
|
user code can simply call ``fruitjam.sync_time()``.
|
||||||
|
|
||||||
|
Keyword args are passed through to ``Network.sync_time``:
|
||||||
|
- server (str) : NTP host (default from NTP_SERVER or pool.ntp.org)
|
||||||
|
- tz_offset (float) : hours from UTC (default from NTP_TZ + NTP_DST)
|
||||||
|
- timeout (float) : socket timeout seconds (default from NTP_TIMEOUT or 5.0)
|
||||||
|
- cache_seconds (int) : NTP cache seconds (default from NTP_CACHE_SECONDS or 0)
|
||||||
|
- require_year (int) : sanity check lower bound (default 2022)
|
||||||
|
Returns:
|
||||||
|
time.struct_time
|
||||||
|
"""
|
||||||
|
return self.network.sync_time(**kwargs)
|
||||||
|
|
||||||
def set_caption(self, caption_text, caption_position, caption_color):
|
def set_caption(self, caption_text, caption_position, caption_color):
|
||||||
"""A caption. Requires setting ``caption_font`` in init!
|
"""A caption. Requires setting ``caption_font`` in init!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,13 @@ Implementation Notes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
|
|
||||||
|
import adafruit_connection_manager as acm
|
||||||
|
import adafruit_ntp
|
||||||
import microcontroller
|
import microcontroller
|
||||||
import neopixel
|
import neopixel
|
||||||
|
import rtc
|
||||||
from adafruit_portalbase.network import (
|
from adafruit_portalbase.network import (
|
||||||
CONTENT_IMAGE,
|
CONTENT_IMAGE,
|
||||||
CONTENT_JSON,
|
CONTENT_JSON,
|
||||||
|
|
@ -209,3 +213,86 @@ class Network(NetworkBase):
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
return filename, position
|
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 (default "pool.ntp.org")
|
||||||
|
NTP_TZ (float hours from UTC, default 0)
|
||||||
|
NTP_DST (additional offset, usually 0 or 1)
|
||||||
|
NTP_TIMEOUT (seconds, default 5.0)
|
||||||
|
NTP_CACHE_SECONDS (default 0 = always fetch fresh)
|
||||||
|
NTP_REQUIRE_YEAR (minimum acceptable year, default 2022)
|
||||||
|
|
||||||
|
Keyword args:
|
||||||
|
server (str) – override NTP_SERVER
|
||||||
|
tz_offset (float) – override NTP_TZ (+ NTP_DST still applied)
|
||||||
|
tuning (dict) – override other knobs:
|
||||||
|
{"timeout": 5.0,
|
||||||
|
"cache_seconds": 0,
|
||||||
|
"require_year": 2022}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
time.struct_time
|
||||||
|
"""
|
||||||
|
# Bring up Wi-Fi using the existing flow.
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
# Build a socket pool from the existing ESP interface.
|
||||||
|
pool = acm.get_radio_socketpool(self._wifi.esp)
|
||||||
|
|
||||||
|
# Settings with environment fallbacks.
|
||||||
|
server = server or os.getenv("NTP_SERVER") or "pool.ntp.org"
|
||||||
|
|
||||||
|
if tz_offset is None:
|
||||||
|
tz_env = os.getenv("NTP_TZ")
|
||||||
|
try:
|
||||||
|
tz_offset = float(tz_env) if tz_env not in {None, ""} else 0.0
|
||||||
|
except Exception:
|
||||||
|
tz_offset = 0.0
|
||||||
|
|
||||||
|
# Simple DST additive offset (no IANA time zone logic).
|
||||||
|
try:
|
||||||
|
dst = float(os.getenv("NTP_DST") or 0)
|
||||||
|
except Exception:
|
||||||
|
dst = 0.0
|
||||||
|
tz_offset += dst
|
||||||
|
|
||||||
|
# Optional tuning (env can override passed defaults).
|
||||||
|
t = tuning or {}
|
||||||
|
|
||||||
|
def _f(name, default):
|
||||||
|
v = os.getenv(name)
|
||||||
|
try:
|
||||||
|
return float(v) if v not in {None, ""} else float(default)
|
||||||
|
except Exception:
|
||||||
|
return float(default)
|
||||||
|
|
||||||
|
def _i(name, default):
|
||||||
|
v = os.getenv(name)
|
||||||
|
try:
|
||||||
|
return int(v) if v not in {None, ""} else int(default)
|
||||||
|
except Exception:
|
||||||
|
return int(default)
|
||||||
|
|
||||||
|
timeout = float(t.get("timeout", _f("NTP_TIMEOUT", 5.0)))
|
||||||
|
cache_seconds = int(t.get("cache_seconds", _i("NTP_CACHE_SECONDS", 0)))
|
||||||
|
require_year = int(t.get("require_year", _i("NTP_REQUIRE_YEAR", 2022)))
|
||||||
|
|
||||||
|
# Query NTP and set the system RTC.
|
||||||
|
ntp = adafruit_ntp.NTP(
|
||||||
|
pool,
|
||||||
|
server=server,
|
||||||
|
tz_offset=tz_offset,
|
||||||
|
socket_timeout=timeout,
|
||||||
|
cache_seconds=cache_seconds,
|
||||||
|
)
|
||||||
|
now = ntp.datetime # struct_time
|
||||||
|
|
||||||
|
if now.tm_year < require_year:
|
||||||
|
raise RuntimeError("NTP returned an unexpected year; not setting RTC")
|
||||||
|
|
||||||
|
rtc.RTC().datetime = now
|
||||||
|
return now
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""
|
|
||||||
Fruit Jam NTP helper (one-shot)
|
|
||||||
- Reads Wi-Fi creds (CIRCUITPY_WIFI_SSID/PASSWORD)
|
|
||||||
- Reads optional NTP_* settings (server, tz, dst, interval, timeout, etc.)
|
|
||||||
- Connects AirLift, queries NTP, sets rtc.RTC().datetime
|
|
||||||
- Returns (now, next_sync) where next_sync is None if NTP_INTERVAL is 0/absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
import adafruit_connection_manager as acm
|
|
||||||
import adafruit_ntp
|
|
||||||
import board
|
|
||||||
import rtc
|
|
||||||
from adafruit_esp32spi import adafruit_esp32spi
|
|
||||||
from digitalio import DigitalInOut
|
|
||||||
|
|
||||||
|
|
||||||
class _State:
|
|
||||||
"""Mutable holder to avoid module-level 'global' updates (ruff PLW0603)."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.spi = None
|
|
||||||
self.cs = None
|
|
||||||
self.rdy = None
|
|
||||||
self.rst = None
|
|
||||||
self.esp = None
|
|
||||||
self.pool = None
|
|
||||||
|
|
||||||
|
|
||||||
_state = _State()
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_radio():
|
|
||||||
if _state.esp and _state.pool:
|
|
||||||
return _state.esp, _state.pool
|
|
||||||
|
|
||||||
if _state.spi is None:
|
|
||||||
_state.spi = board.SPI()
|
|
||||||
|
|
||||||
if _state.cs is None:
|
|
||||||
_state.cs = DigitalInOut(board.ESP_CS)
|
|
||||||
if _state.rdy is None:
|
|
||||||
_state.rdy = DigitalInOut(board.ESP_BUSY)
|
|
||||||
if _state.rst is None:
|
|
||||||
_state.rst = DigitalInOut(board.ESP_RESET)
|
|
||||||
|
|
||||||
if _state.esp is None:
|
|
||||||
_state.esp = adafruit_esp32spi.ESP_SPIcontrol(_state.spi, _state.cs, _state.rdy, _state.rst)
|
|
||||||
|
|
||||||
if _state.pool is None:
|
|
||||||
_state.pool = acm.get_radio_socketpool(_state.esp)
|
|
||||||
|
|
||||||
return _state.esp, _state.pool
|
|
||||||
|
|
||||||
|
|
||||||
def _env_float(name, default):
|
|
||||||
try:
|
|
||||||
v = os.getenv(name)
|
|
||||||
return float(v) if v not in {None, ""} else float(default)
|
|
||||||
except Exception:
|
|
||||||
return float(default)
|
|
||||||
|
|
||||||
|
|
||||||
def _env_int(name, default):
|
|
||||||
try:
|
|
||||||
v = os.getenv(name)
|
|
||||||
return int(v) if v not in {None, ""} else int(default)
|
|
||||||
except Exception:
|
|
||||||
return int(default)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_time(*, server=None, tz_offset=None, tuning=None):
|
|
||||||
"""
|
|
||||||
One-call NTP sync. Small public API to satisfy ruff PLR0913.
|
|
||||||
server: override NTP_SERVER
|
|
||||||
tz_offset: override NTP_TZ (+ NTP_DST is still applied)
|
|
||||||
tuning: optional dict to override timeouts/retries/cache/year check, e.g.:
|
|
||||||
{"timeout": 5.0, "retries": 2, "retry_delay": 1.0,
|
|
||||||
"cache_seconds": 0, "require_year": 2022}
|
|
||||||
|
|
||||||
Returns (now, next_sync). next_sync is None if NTP_INTERVAL is disabled.
|
|
||||||
"""
|
|
||||||
# Wi-Fi creds (required)
|
|
||||||
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
|
|
||||||
pw = os.getenv("CIRCUITPY_WIFI_PASSWORD")
|
|
||||||
if not ssid or not pw:
|
|
||||||
raise RuntimeError("Add CIRCUITPY_WIFI_SSID/PASSWORD to settings.toml")
|
|
||||||
|
|
||||||
# NTP config (env defaults, overridable by parameters)
|
|
||||||
server = server or os.getenv("NTP_SERVER") or "pool.ntp.org"
|
|
||||||
if tz_offset is None:
|
|
||||||
tz_offset = _env_float("NTP_TZ", 0.0)
|
|
||||||
tz_offset += _env_float("NTP_DST", 0.0)
|
|
||||||
|
|
||||||
# Tuning knobs
|
|
||||||
t = tuning or {}
|
|
||||||
timeout = float(t.get("timeout", _env_float("NTP_TIMEOUT", 5.0)))
|
|
||||||
retries = int(t.get("retries", _env_int("NTP_RETRIES", 2)))
|
|
||||||
retry_delay = float(t.get("retry_delay", _env_float("NTP_DELAY_S", 1.0)))
|
|
||||||
cache_seconds = int(t.get("cache_seconds", _env_int("NTP_CACHE_SECONDS", 0)))
|
|
||||||
require_year = int(t.get("require_year", 2022))
|
|
||||||
interval = _env_int("NTP_INTERVAL", 0)
|
|
||||||
|
|
||||||
esp, pool = _ensure_radio()
|
|
||||||
|
|
||||||
# Connect with light retries
|
|
||||||
for attempt in range(retries + 1):
|
|
||||||
try:
|
|
||||||
if not esp.is_connected:
|
|
||||||
esp.connect_AP(ssid, pw)
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
if attempt >= retries:
|
|
||||||
raise
|
|
||||||
try:
|
|
||||||
esp.reset()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
|
|
||||||
ntp = adafruit_ntp.NTP(
|
|
||||||
pool,
|
|
||||||
tz_offset=tz_offset,
|
|
||||||
server=server,
|
|
||||||
socket_timeout=timeout,
|
|
||||||
cache_seconds=cache_seconds,
|
|
||||||
)
|
|
||||||
|
|
||||||
now = ntp.datetime
|
|
||||||
if now.tm_year < require_year:
|
|
||||||
raise RuntimeError("NTP returned an unexpected year; not setting RTC")
|
|
||||||
|
|
||||||
rtc.RTC().datetime = now
|
|
||||||
next_sync = time.time() + interval if interval > 0 else None
|
|
||||||
return now, next_sync
|
|
||||||
|
|
||||||
|
|
||||||
def release_pins():
|
|
||||||
"""Free pins if hot-reloading during development."""
|
|
||||||
try:
|
|
||||||
for pin in (_state.cs, _state.rdy, _state.rst):
|
|
||||||
if pin:
|
|
||||||
pin.deinit()
|
|
||||||
finally:
|
|
||||||
_state.spi = _state.cs = _state.rdy = _state.rst = _state.esp = _state.pool = None
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ntp():
|
|
||||||
"""Retry wrapper that prints status; useful while developing."""
|
|
||||||
print("Fetching time via NTP.")
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
now, next_sync = sync_time()
|
|
||||||
break
|
|
||||||
except Exception as ex:
|
|
||||||
print("Exception:", ex)
|
|
||||||
time.sleep(1)
|
|
||||||
print("NTP OK, localtime:", time.localtime())
|
|
||||||
return now, next_sync
|
|
||||||
29
examples/fruitjam_ntp_settings.toml
Normal file
29
examples/fruitjam_ntp_settings.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks 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 = 5 # socket timeout in seconds
|
||||||
|
NTP_CACHE_SECONDS = 0 # cache results (0 = always fetch)
|
||||||
|
NTP_REQUIRE_YEAR = 2022 # sanity check minimum year
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
#
|
import time
|
||||||
# see examples/settings.toml for NTP_ options
|
|
||||||
#
|
|
||||||
from adafruit_fruitjam.ntp import sync_time
|
|
||||||
|
|
||||||
now, next_sync = sync_time()
|
from adafruit_fruitjam import FruitJam
|
||||||
|
|
||||||
|
fj = FruitJam()
|
||||||
|
now = fj.sync_time()
|
||||||
print("RTC set:", now)
|
print("RTC set:", now)
|
||||||
|
print("Localtime:", time.localtime())
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# Wi-Fi settings (required)
|
|
||||||
CIRCUITPY_WIFI_SSID = "YourSSID"
|
|
||||||
CIRCUITPY_WIFI_PASSWORD = "YourPassword"
|
|
||||||
|
|
||||||
# Time zone offset in hours relative to UTC (default 0 if not set)
|
|
||||||
# Examples:
|
|
||||||
# 0 = UTC (Zulu)
|
|
||||||
# 1 = CET (Central European Time)
|
|
||||||
# 2 = EET (Eastern European Time)
|
|
||||||
# 3 = FET (Further Eastern European Time)
|
|
||||||
# -5 = EST (Eastern Standard Time)
|
|
||||||
# -6 = CST (Central Standard Time)
|
|
||||||
# -7 = MST (Mountain Standard Time)
|
|
||||||
# -8 = PST (Pacific Standard Time)
|
|
||||||
# -9 = AKST (Alaska Standard Time)
|
|
||||||
# -10 = HST (Hawaii Standard Time, no DST)
|
|
||||||
NTP_SERVER = "pool.ntp.org"
|
|
||||||
NTP_TZ = -5
|
|
||||||
NTP_DST = 1
|
|
||||||
NTP_INTERVAL = 3600
|
|
||||||
Loading…
Reference in a new issue