Skip to content

Commit 2d9562c

Browse files
authored
Merge pull request #39 from mMerlin/main
refactor to extract ntp update logic
2 parents 18c60dc + 98fd191 commit 2d9562c

File tree

3 files changed

+90
-49
lines changed

3 files changed

+90
-49
lines changed

adafruit_ntp.py

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

examples/ntp_set_rtc.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

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

6+
import os
67
import time
78

89
import rtc
@@ -11,15 +12,19 @@
1112

1213
import adafruit_ntp
1314

14-
# Get wifi details and more from a secrets.py file
15+
# Get wifi AP credentials from a settings.toml file
16+
wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID")
17+
wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
18+
if wifi_ssid is None:
19+
print("WiFi credentials are kept in settings.toml, please add them there!")
20+
raise ValueError("SSID not found in environment variables")
21+
1522
try:
16-
from secrets import secrets
17-
except ImportError:
18-
print("WiFi secrets are kept in secrets.py, please add them there!")
23+
wifi.radio.connect(wifi_ssid, wifi_password)
24+
except ConnectionError:
25+
print("Failed to connect to WiFi with provided credentials")
1926
raise
2027

21-
wifi.radio.connect(secrets["ssid"], secrets["password"])
22-
2328
pool = socketpool.SocketPool(wifi.radio)
2429
ntp = adafruit_ntp.NTP(pool, tz_offset=0, cache_seconds=3600)
2530

examples/ntp_simpletest.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@
33

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

6+
import os
67
import time
78

89
import socketpool
910
import wifi
1011

1112
import adafruit_ntp
1213

13-
# Get wifi details and more from a secrets.py file
14+
# Get wifi AP credentials from a settings.toml file
15+
wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID")
16+
wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
17+
if wifi_ssid is None:
18+
print("WiFi credentials are kept in settings.toml, please add them there!")
19+
raise ValueError("SSID not found in environment variables")
20+
1421
try:
15-
from secrets import secrets
16-
except ImportError:
17-
print("WiFi secrets are kept in secrets.py, please add them there!")
22+
wifi.radio.connect(wifi_ssid, wifi_password)
23+
except ConnectionError:
24+
print("Failed to connect to WiFi with provided credentials")
1825
raise
1926

20-
wifi.radio.connect(secrets["ssid"], secrets["password"])
21-
2227
pool = socketpool.SocketPool(wifi.radio)
2328
ntp = adafruit_ntp.NTP(pool, tz_offset=0, cache_seconds=3600)
2429

0 commit comments

Comments
 (0)