Skip to content

Commit a125f4d

Browse files
authored
Merge pull request #20 from tannewt/raw_ntp
Use UDP to talk to NTP servers
2 parents e346174 + c4ec11f commit a125f4d

File tree

8 files changed

+172
-132
lines changed

8 files changed

+172
-132
lines changed

README.rst

+19-36
Original file line numberDiff line numberDiff line change
@@ -55,43 +55,26 @@ Usage Example
5555

5656
.. code-block:: python
5757
58+
import adafruit_ntp
59+
import socketpool
5860
import time
59-
import board
60-
import busio
61-
from digitalio import DigitalInOut
62-
from adafruit_esp32spi import adafruit_esp32spi
63-
from adafruit_ntp import NTP
64-
65-
# If you are using a board with pre-defined ESP32 Pins:
66-
esp32_cs = DigitalInOut(board.ESP_CS)
67-
esp32_ready = DigitalInOut(board.ESP_BUSY)
68-
esp32_reset = DigitalInOut(board.ESP_RESET)
69-
70-
# If you have an externally connected ESP32:
71-
# esp32_cs = DigitalInOut(board.D9)
72-
# esp32_ready = DigitalInOut(board.D10)
73-
# esp32_reset = DigitalInOut(board.D5)
74-
75-
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
76-
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
77-
78-
print("Connecting to AP...")
79-
while not esp.is_connected:
80-
try:
81-
esp.connect_AP(b"WIFI_SSID", b"WIFI_PASS")
82-
except RuntimeError as e:
83-
print("could not connect to AP, retrying: ", e)
84-
continue
85-
86-
# Initialize the NTP object
87-
ntp = NTP(esp)
88-
89-
# Fetch and set the microcontroller's current UTC time
90-
ntp.set_time()
91-
92-
# Get the current time in seconds since Jan 1, 1970
93-
current_time = time.time()
94-
print("Seconds since Jan 1, 1970: {} seconds".format(current_time))
61+
import wifi
62+
63+
# Get wifi details and more from a secrets.py file
64+
try:
65+
from secrets import secrets
66+
except ImportError:
67+
print("WiFi secrets are kept in secrets.py, please add them there!")
68+
raise
69+
70+
wifi.radio.connect(secrets["ssid"], secrets["password"])
71+
72+
pool = socketpool.SocketPool(wifi.radio)
73+
ntp = adafruit_ntp.NTP(pool, tz_offset=0)
74+
75+
while True:
76+
print(ntp.datetime)
77+
time.sleep(1)
9578
9679
9780
Documentation

adafruit_ntp.py

+60-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
1+
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
22
#
33
# SPDX-License-Identifier: MIT
44

@@ -8,7 +8,7 @@
88
99
Network Time Protocol (NTP) helper for CircuitPython
1010
11-
* Author(s): Brent Rubell
11+
* Author(s): Scott Shawcroft
1212
1313
Implementation Notes
1414
--------------------
@@ -19,52 +19,74 @@
1919
https://github.com/adafruit/circuitpython/releases
2020
2121
"""
22+
import struct
2223
import time
23-
import rtc
24-
25-
try:
26-
# Used only for typing
27-
import typing # pylint: disable=unused-import
28-
from adafruit_esp32spi.adafruit_esp32spi import ESP_SPIcontrol
29-
except ImportError:
30-
pass
3124

3225
__version__ = "0.0.0-auto.0"
3326
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NTP.git"
3427

28+
NTP_TO_UNIX_EPOCH = 2208988800 # 1970-01-01 00:00:00
29+
3530

3631
class NTP:
3732
"""Network Time Protocol (NTP) helper module for CircuitPython.
38-
This module does not handle daylight savings or local time.
39-
40-
:param adafruit_esp32spi esp: ESP32SPI object.
41-
:param bool debug: Set to True to output set_time() failures to console
33+
This module does not handle daylight savings or local time. It simply requests
34+
UTC from a NTP server.
4235
"""
4336

44-
def __init__(self, esp: ESP_SPIcontrol, debug: bool = False) -> None:
45-
# Verify ESP32SPI module
46-
if "ESP_SPIcontrol" in str(type(esp)):
47-
self._esp = esp
48-
else:
49-
raise TypeError("Provided object is not an ESP_SPIcontrol object.")
50-
self.valid_time = False
51-
self.debug = debug
37+
def __init__(
38+
self,
39+
socketpool,
40+
*,
41+
server: str = "0.adafruit.pool.ntp.org",
42+
port: int = 123,
43+
tz_offset: int = 0,
44+
) -> None:
45+
"""
46+
:param object socketpool: A socket provider such as CPython's `socket` module.
47+
:param str server: The domain of the ntp server to query.
48+
:param int port: The port of the ntp server to query.
49+
:param float tz_offset: Timezone offset in hours from UTC. Only useful for timezone ignorant
50+
CircuitPython. CPython will determine timezone automatically and adjust (so don't use
51+
this.) For example, Pacific daylight savings time is -7.
52+
"""
53+
self._pool = socketpool
54+
self._server = server
55+
self._port = port
56+
self._packet = bytearray(48)
57+
self._tz_offset = tz_offset * 60 * 60
5258

53-
def set_time(self, tz_offset: int = 0) -> None:
54-
"""Fetches and sets the microcontroller's current time
55-
in seconds since since Jan 1, 1970.
59+
# This is our estimated start time for the monotonic clock. We adjust it based on the ntp
60+
# responses.
61+
self._monotonic_start = 0
5662

57-
:param int tz_offset: The offset of the local timezone,
58-
in seconds west of UTC (negative in most of Western Europe,
59-
positive in the US, zero in the UK).
60-
"""
63+
self.next_sync = 0
64+
65+
@property
66+
def datetime(self) -> time.struct_time:
67+
"""Current time from NTP server."""
68+
if time.monotonic_ns() > self.next_sync:
69+
self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode
70+
for i in range(1, len(self._packet)):
71+
self._packet[i] = 0
72+
with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock:
73+
sock.sendto(self._packet, (self._server, self._port))
74+
sock.recvfrom_into(self._packet)
75+
# Get the time in the context to minimize the difference between it and receiving
76+
# the packet.
77+
destination = time.monotonic_ns()
78+
poll = struct.unpack_from("!B", self._packet, offset=2)[0]
79+
self.next_sync = destination + (2**poll) * 1_000_000_000
80+
seconds = struct.unpack_from(
81+
"!I", self._packet, offset=len(self._packet) - 8
82+
)[0]
83+
self._monotonic_start = (
84+
seconds
85+
+ self._tz_offset
86+
- NTP_TO_UNIX_EPOCH
87+
- (destination // 1_000_000_000)
88+
)
6189

62-
try:
63-
now = self._esp.get_time()
64-
now = time.localtime(now[0] + tz_offset)
65-
rtc.RTC().datetime = now
66-
self.valid_time = True
67-
except ValueError as error:
68-
if self.debug:
69-
print(str(error))
70-
return
90+
return time.localtime(
91+
time.monotonic_ns() // 1_000_000_000 + self._monotonic_start
92+
)

docs/examples.rst

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
Simple test
22
------------
33

4-
Ensure your device works with this simple test.
4+
Ensure your device works with this simple test that prints out the time from NTP.
55

66
.. literalinclude:: ../examples/ntp_simpletest.py
77
:caption: examples/ntp_simpletest.py
88
:linenos:
9+
10+
Set RTC
11+
------------
12+
13+
Sync your CircuitPython board's realtime clock (RTC) with time from NTP.
14+
15+
.. literalinclude:: ../examples/ntp_set_rtc.py
16+
:caption: examples/ntp_set_rtc.py
17+
:linenos:
18+
19+
Simple test with CPython
20+
------------------------
21+
22+
Test the library in CPython using ``socket``.
23+
24+
.. literalinclude:: ../examples/ntp_cpython.py
25+
:caption: examples/ntp_cpython.py
26+
:linenos:

examples/ntp_cpython.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""Tests NTP with CPython socket"""
5+
6+
import socket
7+
import time
8+
9+
import adafruit_ntp
10+
11+
# Don't use tz_offset kwarg with CPython because it will adjust automatically.
12+
ntp = adafruit_ntp.NTP(socket)
13+
14+
while True:
15+
print(ntp.datetime)
16+
time.sleep(1)

examples/ntp_set_rtc.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""Example demonstrating how to set the realtime clock (RTC) based on NTP time."""
5+
6+
import time
7+
8+
import rtc
9+
import socketpool
10+
import wifi
11+
12+
import adafruit_ntp
13+
14+
# Get wifi details and more from a secrets.py file
15+
try:
16+
from secrets import secrets
17+
except ImportError:
18+
print("WiFi secrets are kept in secrets.py, please add them there!")
19+
raise
20+
21+
wifi.radio.connect(secrets["ssid"], secrets["password"])
22+
23+
pool = socketpool.SocketPool(wifi.radio)
24+
ntp = adafruit_ntp.NTP(pool, tz_offset=0)
25+
26+
# NOTE: This changes the system time so make sure you aren't assuming that time
27+
# doesn't jump.
28+
rtc.RTC().datetime = ntp.datetime
29+
30+
while True:
31+
print(time.localtime())
32+
time.sleep(1)

examples/ntp_simpletest.py

+24-52
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,27 @@
1-
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
1+
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
22
# SPDX-License-Identifier: MIT
33

4+
"""Print out time based on NTP."""
5+
46
import time
5-
import board
6-
import busio
7-
from digitalio import DigitalInOut
8-
from adafruit_esp32spi import adafruit_esp32spi
9-
from adafruit_ntp import NTP
10-
11-
# If you are using a board with pre-defined ESP32 Pins:
12-
esp32_cs = DigitalInOut(board.ESP_CS)
13-
esp32_ready = DigitalInOut(board.ESP_BUSY)
14-
esp32_reset = DigitalInOut(board.ESP_RESET)
15-
16-
# If you have an externally connected ESP32:
17-
# esp32_cs = DigitalInOut(board.D9)
18-
# esp32_ready = DigitalInOut(board.D10)
19-
# esp32_reset = DigitalInOut(board.D5)
20-
21-
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
22-
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
23-
24-
print("Connecting to AP...")
25-
while not esp.is_connected:
26-
try:
27-
esp.connect_AP(b"WIFI_SSID", b"WIFI_PASS")
28-
except RuntimeError as e:
29-
print("could not connect to AP, retrying: ", e)
30-
continue
31-
32-
# Initialize the NTP object
33-
ntp = NTP(esp)
34-
35-
# Fetch and set the microcontroller's current UTC time
36-
# keep retrying until a valid time is returned
37-
while not ntp.valid_time:
38-
ntp.set_time()
39-
print("Failed to obtain time, retrying in 5 seconds...")
40-
time.sleep(5)
41-
42-
# Get the current time in seconds since Jan 1, 1970
43-
current_time = time.time()
44-
print("Seconds since Jan 1, 1970: {} seconds".format(current_time))
45-
46-
# Convert the current time in seconds since Jan 1, 1970 to a struct_time
47-
now = time.localtime(current_time)
48-
print(now)
49-
50-
# Pretty-parse the struct_time
51-
print(
52-
"It is currently {}/{}/{} at {}:{}:{} UTC".format(
53-
now.tm_mon, now.tm_mday, now.tm_year, now.tm_hour, now.tm_min, now.tm_sec
54-
)
55-
)
7+
8+
import socketpool
9+
import wifi
10+
11+
import adafruit_ntp
12+
13+
# Get wifi details and more from a secrets.py file
14+
try:
15+
from secrets import secrets
16+
except ImportError:
17+
print("WiFi secrets are kept in secrets.py, please add them there!")
18+
raise
19+
20+
wifi.radio.connect(secrets["ssid"], secrets["password"])
21+
22+
pool = socketpool.SocketPool(wifi.radio)
23+
ntp = adafruit_ntp.NTP(pool, tz_offset=0)
24+
25+
while True:
26+
print(ntp.datetime)
27+
time.sleep(1)

requirements.txt

-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
22
#
33
# SPDX-License-Identifier: Unlicense
4-
5-
Adafruit-Blinka
6-
adafruit-circuitpython-esp32spi

setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
name="adafruit-circuitpython-ntp",
2626
use_scm_version=True,
2727
setup_requires=["setuptools_scm"],
28-
description="Network Time Protocol (NTP) helper for CircuitPython",
28+
description="Network Time Protocol (NTP) helper for Python",
2929
long_description=long_description,
3030
long_description_content_type="text/x-rst",
3131
# The project's main homepage.
3232
url="https://github.com/adafruit/Adafruit_CircuitPython_NTP",
3333
# Author details
3434
author="Adafruit Industries",
3535
author_email="[email protected]",
36-
install_requires=["Adafruit-Blinka", "adafruit-circuitpython-esp32spi"],
36+
install_requires=[],
3737
# Choose your license
3838
license="MIT",
3939
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers

0 commit comments

Comments
 (0)