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
Changes from 1 commit
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
98 changes: 61 additions & 37 deletions adafruit_ntp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries

Check failure on line 1 in adafruit_ntp.py

View workflow job for this annotation

GitHub Actions / test

reformatted
#
# SPDX-License-Identifier: MIT

Expand Down 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,69 @@

# 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
Loading