Skip to content

Commit 544adaa

Browse files
committed
ntp module
helper code to get time sync with minimal fuss on the Fruit Jam.
1 parent a0327ac commit 544adaa

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

adafruit_fruitjam/ntp.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
Fruit Jam NTP helper (one-shot)
6+
- Reads Wi-Fi creds (CIRCUITPY_WIFI_SSID/PASSWORD)
7+
- Reads optional NTP_* settings (server, tz, dst, interval, timeout, etc.)
8+
- Connects AirLift, queries NTP, sets rtc.RTC().datetime
9+
- Returns (now, next_sync) where next_sync is None if NTP_INTERVAL is 0/absent
10+
"""
11+
12+
import os
13+
import time
14+
15+
import adafruit_connection_manager as acm
16+
import adafruit_ntp
17+
import board
18+
import rtc
19+
from adafruit_esp32spi import adafruit_esp32spi
20+
from digitalio import DigitalInOut
21+
22+
23+
class _State:
24+
"""Mutable holder to avoid module-level 'global' updates (ruff PLW0603)."""
25+
26+
def __init__(self):
27+
self.spi = None
28+
self.cs = None
29+
self.rdy = None
30+
self.rst = None
31+
self.esp = None
32+
self.pool = None
33+
34+
35+
_state = _State()
36+
37+
38+
def _ensure_radio():
39+
if _state.esp and _state.pool:
40+
return _state.esp, _state.pool
41+
42+
if _state.spi is None:
43+
_state.spi = board.SPI()
44+
45+
if _state.cs is None:
46+
_state.cs = DigitalInOut(board.ESP_CS)
47+
if _state.rdy is None:
48+
_state.rdy = DigitalInOut(board.ESP_BUSY)
49+
if _state.rst is None:
50+
_state.rst = DigitalInOut(board.ESP_RESET)
51+
52+
if _state.esp is None:
53+
_state.esp = adafruit_esp32spi.ESP_SPIcontrol(_state.spi, _state.cs, _state.rdy, _state.rst)
54+
55+
if _state.pool is None:
56+
_state.pool = acm.get_radio_socketpool(_state.esp)
57+
58+
return _state.esp, _state.pool
59+
60+
61+
def _env_float(name, default):
62+
try:
63+
v = os.getenv(name)
64+
return float(v) if v not in {None, ""} else float(default)
65+
except Exception:
66+
return float(default)
67+
68+
69+
def _env_int(name, default):
70+
try:
71+
v = os.getenv(name)
72+
return int(v) if v not in {None, ""} else int(default)
73+
except Exception:
74+
return int(default)
75+
76+
77+
def sync_time(*, server=None, tz_offset=None, tuning=None):
78+
"""
79+
One-call NTP sync. Small public API to satisfy ruff PLR0913.
80+
server: override NTP_SERVER
81+
tz_offset: override NTP_TZ (+ NTP_DST is still applied)
82+
tuning: optional dict to override timeouts/retries/cache/year check, e.g.:
83+
{"timeout": 5.0, "retries": 2, "retry_delay": 1.0,
84+
"cache_seconds": 0, "require_year": 2022}
85+
86+
Returns (now, next_sync). next_sync is None if NTP_INTERVAL is disabled.
87+
"""
88+
# Wi-Fi creds (required)
89+
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
90+
pw = os.getenv("CIRCUITPY_WIFI_PASSWORD")
91+
if not ssid or not pw:
92+
raise RuntimeError("Add CIRCUITPY_WIFI_SSID/PASSWORD to settings.toml")
93+
94+
# NTP config (env defaults, overridable by parameters)
95+
server = server or os.getenv("NTP_SERVER") or "pool.ntp.org"
96+
if tz_offset is None:
97+
tz_offset = _env_float("NTP_TZ", 0.0)
98+
tz_offset += _env_float("NTP_DST", 0.0)
99+
100+
# Tuning knobs
101+
t = tuning or {}
102+
timeout = float(t.get("timeout", _env_float("NTP_TIMEOUT", 5.0)))
103+
retries = int(t.get("retries", _env_int("NTP_RETRIES", 2)))
104+
retry_delay = float(t.get("retry_delay", _env_float("NTP_DELAY_S", 1.0)))
105+
cache_seconds = int(t.get("cache_seconds", _env_int("NTP_CACHE_SECONDS", 0)))
106+
require_year = int(t.get("require_year", 2022))
107+
interval = _env_int("NTP_INTERVAL", 0)
108+
109+
esp, pool = _ensure_radio()
110+
111+
# Connect with light retries
112+
for attempt in range(retries + 1):
113+
try:
114+
if not esp.is_connected:
115+
esp.connect_AP(ssid, pw)
116+
break
117+
except Exception:
118+
if attempt >= retries:
119+
raise
120+
try:
121+
esp.reset()
122+
except Exception:
123+
pass
124+
time.sleep(retry_delay)
125+
126+
ntp = adafruit_ntp.NTP(
127+
pool,
128+
tz_offset=tz_offset,
129+
server=server,
130+
socket_timeout=timeout,
131+
cache_seconds=cache_seconds,
132+
)
133+
134+
now = ntp.datetime
135+
if now.tm_year < require_year:
136+
raise RuntimeError("NTP returned an unexpected year; not setting RTC")
137+
138+
rtc.RTC().datetime = now
139+
next_sync = time.time() + interval if interval > 0 else None
140+
return now, next_sync
141+
142+
143+
def release_pins():
144+
"""Free pins if hot-reloading during development."""
145+
try:
146+
for pin in (_state.cs, _state.rdy, _state.rst):
147+
if pin:
148+
pin.deinit()
149+
finally:
150+
_state.spi = _state.cs = _state.rdy = _state.rst = _state.esp = _state.pool = None
151+
152+
153+
def setup_ntp():
154+
"""Retry wrapper that prints status; useful while developing."""
155+
print("Fetching time via NTP.")
156+
while True:
157+
try:
158+
now, next_sync = sync_time()
159+
break
160+
except Exception as ex:
161+
print("Exception:", ex)
162+
time.sleep(1)
163+
print("NTP OK, localtime:", time.localtime())
164+
return now, next_sync

examples/fruitjam_ntp.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
#
5+
# see examples/settings.toml for NTP_ options
6+
#
7+
from adafruit_fruitjam.ntp import sync_time
8+
9+
now, next_sync = sync_time()
10+
print("RTC set:", now)

examples/settings.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# Wi-Fi settings (required)
5+
CIRCUITPY_WIFI_SSID = "YourSSID"
6+
CIRCUITPY_WIFI_PASSWORD = "YourPassword"
7+
8+
# Time zone offset in hours relative to UTC (default 0 if not set)
9+
# Examples:
10+
# 0 = UTC (Zulu)
11+
# 1 = CET (Central European Time)
12+
# 2 = EET (Eastern European Time)
13+
# 3 = FET (Further Eastern European Time)
14+
# -5 = EST (Eastern Standard Time)
15+
# -6 = CST (Central Standard Time)
16+
# -7 = MST (Mountain Standard Time)
17+
# -8 = PST (Pacific Standard Time)
18+
# -9 = AKST (Alaska Standard Time)
19+
# -10 = HST (Hawaii Standard Time, no DST)
20+
NTP_SERVER = "pool.ntp.org"
21+
NTP_TZ = -5
22+
NTP_DST = 1
23+
NTP_INTERVAL = 3600

0 commit comments

Comments
 (0)