|
32 | 32 | PACKET_SIZE = const(48)
|
33 | 33 |
|
34 | 34 |
|
35 |
| -class NTP: |
| 35 | +class NTP: # pylint:disable=too-many-instance-attributes |
36 | 36 | """Network Time Protocol (NTP) helper module for CircuitPython.
|
37 | 37 | This module does not handle daylight savings or local time. It simply requests
|
38 | 38 | UTC from a NTP server.
|
39 | 39 | """
|
40 | 40 |
|
41 |
| - def __init__( |
| 41 | + def __init__( # pylint:disable=too-many-arguments |
42 | 42 | self,
|
43 | 43 | socketpool,
|
44 | 44 | *,
|
@@ -70,45 +70,76 @@ def __init__(
|
70 | 70 |
|
71 | 71 | # This is our estimated start time for the monotonic clock. We adjust it based on the ntp
|
72 | 72 | # responses.
|
73 |
| - self._monotonic_start = 0 |
| 73 | + self._monotonic_start_ns = 0 |
74 | 74 |
|
75 | 75 | self.next_sync = 0
|
76 | 76 |
|
| 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 | + |
77 | 122 | @property
|
78 | 123 | def datetime(self) -> time.struct_time:
|
79 | 124 | """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.""" |
83 | 126 | 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 |
0 commit comments