diff --git a/adafruit_ntp.py b/adafruit_ntp.py index bc6db0d..9dc7476 100644 --- a/adafruit_ntp.py +++ b/adafruit_ntp.py @@ -32,13 +32,13 @@ PACKET_SIZE = const(48) -class NTP: +class NTP: # pylint:disable=too-many-instance-attributes """Network Time Protocol (NTP) helper module for CircuitPython. This module does not handle daylight savings or local time. It simply requests UTC from a NTP server. """ - def __init__( + def __init__( # pylint:disable=too-many-arguments self, socketpool, *, @@ -70,45 +70,76 @@ def __init__( # This is our estimated start time for the monotonic clock. We adjust it based on the ntp # responses. - self._monotonic_start = 0 + self._monotonic_start_ns = 0 self.next_sync = 0 + def _update_time_sync(self) -> None: + """Update the time sync value. Raises OSError exception if no response + is received within socket_timeout seconds, ArithmeticError for substantially incorrect + NTP results.""" + if self._socket_address is None: + self._socket_address = self._pool.getaddrinfo(self._server, self._port)[0][ + 4 + ] + + self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode + for i in range(1, PACKET_SIZE): + self._packet[i] = 0 + with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock: + sock.settimeout(self._socket_timeout) + local_send_ns = time.monotonic_ns() # expanded + sock.sendto(self._packet, self._socket_address) + sock.recv_into(self._packet) + # Get the time in the context to minimize the difference between it and receiving + # the packet. + local_recv_ns = time.monotonic_ns() # was destination + + poll = struct.unpack_from("!B", self._packet, offset=2)[0] + + cache_offset_s = max(2**poll, self._cache_seconds) + self.next_sync = local_recv_ns + cache_offset_s * 1_000_000_000 + + srv_recv_s, srv_recv_f = struct.unpack_from("!II", self._packet, offset=32) + srv_send_s, srv_send_f = struct.unpack_from("!II", self._packet, offset=40) + + # Convert the server times from NTP to UTC for local use + srv_recv_ns = (srv_recv_s - NTP_TO_UNIX_EPOCH) * 1_000_000_000 + ( + srv_recv_f * 1_000_000_000 // 2**32 + ) + srv_send_ns = (srv_send_s - NTP_TO_UNIX_EPOCH) * 1_000_000_000 + ( + srv_send_f * 1_000_000_000 // 2**32 + ) + + # _round_trip_delay = (local_recv_ns - local_send_ns) - (srv_send_ns - srv_recv_ns) + # Calculate (best estimate) offset between server UTC and board monotonic_ns time + clock_offset = ( + (srv_recv_ns - local_send_ns) + (srv_send_ns - local_recv_ns) + ) // 2 + + self._monotonic_start_ns = clock_offset + self._tz_offset * 1_000_000_000 + @property def datetime(self) -> time.struct_time: """Current time from NTP server. Accessing this property causes the NTP time request, - unless there has already been a recent request. Raises OSError exception if no response - is received within socket_timeout seconds, ArithmeticError for substantially incorrect - NTP results.""" + unless there has already been a recent request.""" if time.monotonic_ns() > self.next_sync: - if self._socket_address is None: - self._socket_address = self._pool.getaddrinfo(self._server, self._port)[ - 0 - ][4] - - self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode - for i in range(1, PACKET_SIZE): - self._packet[i] = 0 - with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock: - sock.settimeout(self._socket_timeout) - sock.sendto(self._packet, self._socket_address) - sock.recv_into(self._packet) - # Get the time in the context to minimize the difference between it and receiving - # the packet. - destination = time.monotonic_ns() - poll = struct.unpack_from("!B", self._packet, offset=2)[0] - - cache_offset = max(2**poll, self._cache_seconds) - self.next_sync = destination + cache_offset * 1_000_000_000 - seconds = struct.unpack_from("!I", self._packet, offset=PACKET_SIZE - 8)[0] - - self._monotonic_start = ( - seconds - + self._tz_offset - - NTP_TO_UNIX_EPOCH - - (destination // 1_000_000_000) - ) - - return time.localtime( - time.monotonic_ns() // 1_000_000_000 + self._monotonic_start - ) + self._update_time_sync() + + # Calculate the current time based on the current and start monotonic times + current_time_s = ( + time.monotonic_ns() + self._monotonic_start_ns + ) // 1_000_000_000 + + return time.localtime(current_time_s) + + @property + def utc_ns(self) -> int: + """UTC (unix epoch) time in nanoseconds. Accessing this property causes the NTP time + request, unless there has already been a recent request. Raises OSError exception if + no response is received within socket_timeout seconds, ArithmeticError for substantially + incorrect NTP results.""" + if time.monotonic_ns() > self.next_sync: + self._update_time_sync() + + return time.monotonic_ns() + self._monotonic_start_ns diff --git a/examples/ntp_set_rtc.py b/examples/ntp_set_rtc.py index e4f331b..e0be6f9 100644 --- a/examples/ntp_set_rtc.py +++ b/examples/ntp_set_rtc.py @@ -3,6 +3,7 @@ """Example demonstrating how to set the realtime clock (RTC) based on NTP time.""" +import os import time import rtc @@ -11,15 +12,19 @@ import adafruit_ntp -# Get wifi details and more from a secrets.py file +# Get wifi AP credentials from a settings.toml file +wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID") +wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +if wifi_ssid is None: + print("WiFi credentials are kept in settings.toml, please add them there!") + raise ValueError("SSID not found in environment variables") + try: - from secrets import secrets -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") + wifi.radio.connect(wifi_ssid, wifi_password) +except ConnectionError: + print("Failed to connect to WiFi with provided credentials") raise -wifi.radio.connect(secrets["ssid"], secrets["password"]) - pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=0, cache_seconds=3600) diff --git a/examples/ntp_simpletest.py b/examples/ntp_simpletest.py index a3f1b70..59b88bb 100644 --- a/examples/ntp_simpletest.py +++ b/examples/ntp_simpletest.py @@ -3,6 +3,7 @@ """Print out time based on NTP.""" +import os import time import socketpool @@ -10,15 +11,19 @@ import adafruit_ntp -# Get wifi details and more from a secrets.py file +# Get wifi AP credentials from a settings.toml file +wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID") +wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +if wifi_ssid is None: + print("WiFi credentials are kept in settings.toml, please add them there!") + raise ValueError("SSID not found in environment variables") + try: - from secrets import secrets -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") + wifi.radio.connect(wifi_ssid, wifi_password) +except ConnectionError: + print("Failed to connect to WiFi with provided credentials") raise -wifi.radio.connect(secrets["ssid"], secrets["password"]) - pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=0, cache_seconds=3600)