Skip to content

refactor to extract ntp update logic #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 68 additions & 37 deletions adafruit_ntp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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
17 changes: 11 additions & 6 deletions examples/ntp_set_rtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

"""Example demonstrating how to set the realtime clock (RTC) based on NTP time."""

import os
import time

import rtc
Expand All @@ -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)

Expand Down
17 changes: 11 additions & 6 deletions examples/ntp_simpletest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@

"""Print out time based on NTP."""

import os
import time

import socketpool
import wifi

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)

Expand Down
Loading