|
1 | 1 | # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries
|
2 | 2 | # SPDX-FileCopyrightText: 2025 Tim Cocks, written for Adafruit Industries
|
| 3 | +# SPDX-FileCopyrightText: 2025 Mikey Sklar, written for Adafruit Industries |
3 | 4 | #
|
4 | 5 | # SPDX-License-Identifier: Unlicense
|
5 | 6 | """
|
|
25 | 26 | """
|
26 | 27 |
|
27 | 28 | import gc
|
| 29 | +import os |
| 30 | +import time |
28 | 31 |
|
| 32 | +import adafruit_connection_manager as acm |
| 33 | +import adafruit_ntp |
29 | 34 | import microcontroller
|
30 | 35 | import neopixel
|
| 36 | +import rtc |
31 | 37 | from adafruit_portalbase.network import (
|
32 | 38 | CONTENT_IMAGE,
|
33 | 39 | CONTENT_JSON,
|
@@ -209,3 +215,133 @@ def process_image(self, json_data, sd_card=False): # noqa: PLR0912 Too many bra
|
209 | 215 | gc.collect()
|
210 | 216 |
|
211 | 217 | 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") |
0 commit comments