Skip to content

Commit 784a8b4

Browse files
committed
refactor to extract ntp update logic
use nanoseconds start reference
1 parent 18c60dc commit 784a8b4

File tree

1 file changed

+61
-37
lines changed

1 file changed

+61
-37
lines changed

adafruit_ntp.py

+61-37
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
PACKET_SIZE = const(48)
3333

3434

35-
class NTP:
35+
class NTP: # pylint:disable=too-many-instance-attributes
3636
"""Network Time Protocol (NTP) helper module for CircuitPython.
3737
This module does not handle daylight savings or local time. It simply requests
3838
UTC from a NTP server.
3939
"""
4040

41-
def __init__(
41+
def __init__( # pylint:disable=too-many-arguments
4242
self,
4343
socketpool,
4444
*,
@@ -70,45 +70,69 @@ def __init__(
7070

7171
# This is our estimated start time for the monotonic clock. We adjust it based on the ntp
7272
# responses.
73-
self._monotonic_start = 0
73+
self._monotonic_start_ns = 0
7474

7575
self.next_sync = 0
7676

77+
def _update_time_sync(self) -> None:
78+
"""Update the time sync value. Raises OSError exception if no response
79+
is received within socket_timeout seconds, ArithmeticError for substantially incorrect
80+
NTP results."""
81+
if self._socket_address is None:
82+
self._socket_address = self._pool.getaddrinfo(
83+
self._server, self._port)[0][4]
84+
85+
self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode
86+
for i in range(1, PACKET_SIZE):
87+
self._packet[i] = 0
88+
with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock:
89+
sock.settimeout(self._socket_timeout)
90+
local_send_ns = time.monotonic_ns() # expanded
91+
sock.sendto(self._packet, self._socket_address)
92+
sock.recv_into(self._packet)
93+
# Get the time in the context to minimize the difference between it and receiving
94+
# the packet.
95+
local_recv_ns = time.monotonic_ns() # was destination
96+
97+
poll = struct.unpack_from("!B", self._packet, offset=2)[0]
98+
99+
cache_offset_s = max(2**poll, self._cache_seconds)
100+
self.next_sync = local_recv_ns + cache_offset_s * 1_000_000_000
101+
102+
srv_recv_s, srv_recv_f = struct.unpack_from("!II", self._packet, offset=32)
103+
srv_send_s, srv_send_f = struct.unpack_from("!II", self._packet, offset=40)
104+
105+
# Convert the server times from NTP to UTC for local use
106+
srv_recv_ns = (srv_recv_s - NTP_TO_UNIX_EPOCH) * 1_000_000_000 + \
107+
(srv_recv_f * 1_000_000_000 // 2**32)
108+
srv_send_ns = (srv_send_s - NTP_TO_UNIX_EPOCH) * 1_000_000_000 + \
109+
(srv_send_f * 1_000_000_000 // 2**32)
110+
111+
# _round_trip_delay = (local_recv_ns - local_send_ns) - (srv_send_ns - srv_recv_ns)
112+
# Calculate (best estimate) offset between server UTC and board monotonic_ns time
113+
clock_offset = ((srv_recv_ns - local_send_ns) + (srv_send_ns - local_recv_ns)) // 2
114+
115+
self._monotonic_start_ns = clock_offset + self._tz_offset * 1_000_000_000
116+
77117
@property
78118
def datetime(self) -> time.struct_time:
79119
"""Current time from NTP server. Accessing this property causes the NTP time request,
80-
unless there has already been a recent request. Raises OSError exception if no response
81-
is received within socket_timeout seconds, ArithmeticError for substantially incorrect
82-
NTP results."""
120+
unless there has already been a recent request."""
83121
if time.monotonic_ns() > self.next_sync:
84-
if self._socket_address is None:
85-
self._socket_address = self._pool.getaddrinfo(self._server, self._port)[
86-
0
87-
][4]
88-
89-
self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode
90-
for i in range(1, PACKET_SIZE):
91-
self._packet[i] = 0
92-
with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock:
93-
sock.settimeout(self._socket_timeout)
94-
sock.sendto(self._packet, self._socket_address)
95-
sock.recv_into(self._packet)
96-
# Get the time in the context to minimize the difference between it and receiving
97-
# the packet.
98-
destination = time.monotonic_ns()
99-
poll = struct.unpack_from("!B", self._packet, offset=2)[0]
100-
101-
cache_offset = max(2**poll, self._cache_seconds)
102-
self.next_sync = destination + cache_offset * 1_000_000_000
103-
seconds = struct.unpack_from("!I", self._packet, offset=PACKET_SIZE - 8)[0]
104-
105-
self._monotonic_start = (
106-
seconds
107-
+ self._tz_offset
108-
- NTP_TO_UNIX_EPOCH
109-
- (destination // 1_000_000_000)
110-
)
111-
112-
return time.localtime(
113-
time.monotonic_ns() // 1_000_000_000 + self._monotonic_start
114-
)
122+
self._update_time_sync()
123+
124+
# Calculate the current time based on the current and start monotonic times
125+
current_time_s = (time.monotonic_ns() + self._monotonic_start_ns) // 1_000_000_000
126+
127+
return time.localtime(current_time_s)
128+
129+
@property
130+
def utc_ns(self) -> int:
131+
"""UTC (unix epoch) time in nanoseconds. Accessing this property causes the NTP time
132+
request, unless there has already been a recent request. Raises OSError exception if
133+
no response is received within socket_timeout seconds, ArithmeticError for substantially
134+
incorrect NTP results."""
135+
if time.monotonic_ns() > self.next_sync:
136+
self._update_time_sync()
137+
138+
return time.monotonic_ns() + self._monotonic_start_ns

0 commit comments

Comments
 (0)