Skip to content

NTP time sync helper for Airlift boards #10572

@mikeysklar

Description

@mikeysklar

Ideally setting up NTP on an integrated AirLift enabled board like Fruit Jam, PyPortal or MatrixPortal M4 would be as easy as :

import adafruit_airlift_ntp as ntp
ntp.sync()  # done

It actually takes quite a bit of setup currently. @b-blake pointed this out and we put together a little helper library. Would it make sense for me to test and submit an adafruit_airlift_ntp library to the bundle or is there a better way to do this?

working example tested on Fruit Jam only.

settings.toml

# Wi-Fi credentials
CIRCUITPY_WIFI_SSID = "YourNetworkName"
CIRCUITPY_WIFI_PASSWORD = "YourPassword"

# NTP settings
# Host to query (default pool.ntp.org if not set)
CIRCUITPY_NTP_HOST = "pool.ntp.org"

# Time zone offset in hours relative to UTC
# Use -8 for Pacific Standard Time, -7 for Pacific Daylight Time, etc.
CIRCUITPY_NTP_TZ_OFFSET = -7

# How often to re-sync, in seconds (example: every 6 hours)
CIRCUITPY_NTP_INTERVAL = 21600

code.py

import time
import fruitjam_ntp

tz, host = fruitjam_ntp.sync()
print("NTP OK:", host, "tz:", tz, "->", time.localtime())

fruitjam_ntp.release()

lib/fruitjam_ntp.py

# fruitjam_ntp.py — robust NTP sync for Fruit Jam (ESP32SPI/AirLift)
# - Creates DigitalInOut pins ONCE and reuses them (avoids "ESP_CS in use")
# - Reads Wi-Fi + NTP prefs from settings.toml
# - Adds simple retry / reset on transient ESP errors

import os, time, rtc, board
from digitalio import DigitalInOut
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_connection_manager
import adafruit_ntp

# Module-singletons so multiple sync() calls don't re-grab pins
_SPI = None
_CS = _RDY = _RST = None
_ESP = None
_POOL = None

def _ensure_radio():
    """Create and cache SPI + ESP32SPI/SocketPool once."""
    global _SPI, _CS, _RDY, _RST, _ESP, _POOL

    if _ESP and _POOL:
        return _ESP, _POOL

    if _SPI is None:
        _SPI = board.SPI()  # shared board SPI is fine here

    if _CS is None:
        _CS = DigitalInOut(board.ESP_CS)
    if _RDY is None:
        _RDY = DigitalInOut(board.ESP_BUSY)
    if _RST is None:
        _RST = DigitalInOut(board.ESP_RESET)

    if _ESP is None:
        _ESP = adafruit_esp32spi.ESP_SPIcontrol(_SPI, _CS, _RDY, _RST)

    if _POOL is None:
        _POOL = adafruit_connection_manager.get_radio_socketpool(_ESP)

    return _ESP, _POOL

def sync(default_tz=0, retries=2, retry_delay=1.0):
    """
    Sync RTC from NTP using settings.toml.
    Returns (tz, host).
    """
    ssid = os.getenv("CIRCUITPY_WIFI_SSID")
    pwd  = os.getenv("CIRCUITPY_WIFI_PASSWORD")
    if not ssid or not pwd:
        raise RuntimeError("Add CIRCUITPY_WIFI_SSID/PASSWORD to settings.toml")

    tz   = int(os.getenv("CIRCUITPY_NTP_TZ_OFFSET", str(default_tz)))
    host = os.getenv("CIRCUITPY_NTP_HOST", "pool.ntp.org")

    esp, pool = _ensure_radio()

    # Connect / reconnect with light retries
    for attempt in range(retries + 1):
        try:
            if not esp.is_connected:
                esp.connect_AP(ssid, pwd)
            break
        except Exception as e:
            if attempt >= retries:
                raise
            try:
                esp.reset()  # hardware toggle via RST pin
            except Exception:
                pass
            time.sleep(retry_delay)

    ntp = adafruit_ntp.NTP(pool, tz_offset=tz, server=host, cache_seconds=3600)
    rtc.RTC().datetime = ntp.datetime
    return tz, host

def release():
    """
    Explicitly release pins if you want to force-free them before a soft reload.
    Not required for normal operation, but handy if you’re experimenting.
    """
    global _SPI, _CS, _RDY, _RST, _ESP, _POOL
    try:
        # ESP_SPIcontrol doesn't expose deinit; releasing pins is enough.
        if _CS:
            _CS.deinit()
        if _RDY:
            _RDY.deinit()
        if _RST:
            _RST.deinit()
    finally:
        _SPI = _CS = _RDY = _RST = _ESP = _POOL = None

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions