diff --git a/README.rst b/README.rst index 078319e..f1ce534 100644 --- a/README.rst +++ b/README.rst @@ -55,43 +55,26 @@ Usage Example .. code-block:: python + import adafruit_ntp + import socketpool import time - import board - import busio - from digitalio import DigitalInOut - from adafruit_esp32spi import adafruit_esp32spi - from adafruit_ntp import NTP - - # If you are using a board with pre-defined ESP32 Pins: - esp32_cs = DigitalInOut(board.ESP_CS) - esp32_ready = DigitalInOut(board.ESP_BUSY) - esp32_reset = DigitalInOut(board.ESP_RESET) - - # If you have an externally connected ESP32: - # esp32_cs = DigitalInOut(board.D9) - # esp32_ready = DigitalInOut(board.D10) - # esp32_reset = DigitalInOut(board.D5) - - spi = busio.SPI(board.SCK, board.MOSI, board.MISO) - esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) - - print("Connecting to AP...") - while not esp.is_connected: - try: - esp.connect_AP(b"WIFI_SSID", b"WIFI_PASS") - except RuntimeError as e: - print("could not connect to AP, retrying: ", e) - continue - - # Initialize the NTP object - ntp = NTP(esp) - - # Fetch and set the microcontroller's current UTC time - ntp.set_time() - - # Get the current time in seconds since Jan 1, 1970 - current_time = time.time() - print("Seconds since Jan 1, 1970: {} seconds".format(current_time)) + import wifi + + # Get wifi details and more from a secrets.py file + try: + from secrets import secrets + except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + + wifi.radio.connect(secrets["ssid"], secrets["password"]) + + pool = socketpool.SocketPool(wifi.radio) + ntp = adafruit_ntp.NTP(pool, tz_offset=0) + + while True: + print(ntp.datetime) + time.sleep(1) Documentation diff --git a/adafruit_ntp.py b/adafruit_ntp.py index 1f3ac83..59d5aec 100644 --- a/adafruit_ntp.py +++ b/adafruit_ntp.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -8,7 +8,7 @@ Network Time Protocol (NTP) helper for CircuitPython - * Author(s): Brent Rubell + * Author(s): Scott Shawcroft Implementation Notes -------------------- @@ -19,52 +19,74 @@ https://github.com/adafruit/circuitpython/releases """ +import struct import time -import rtc - -try: - # Used only for typing - import typing # pylint: disable=unused-import - from adafruit_esp32spi.adafruit_esp32spi import ESP_SPIcontrol -except ImportError: - pass __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NTP.git" +NTP_TO_UNIX_EPOCH = 2208988800 # 1970-01-01 00:00:00 + class NTP: """Network Time Protocol (NTP) helper module for CircuitPython. - This module does not handle daylight savings or local time. - - :param adafruit_esp32spi esp: ESP32SPI object. - :param bool debug: Set to True to output set_time() failures to console + This module does not handle daylight savings or local time. It simply requests + UTC from a NTP server. """ - def __init__(self, esp: ESP_SPIcontrol, debug: bool = False) -> None: - # Verify ESP32SPI module - if "ESP_SPIcontrol" in str(type(esp)): - self._esp = esp - else: - raise TypeError("Provided object is not an ESP_SPIcontrol object.") - self.valid_time = False - self.debug = debug + def __init__( + self, + socketpool, + *, + server: str = "0.adafruit.pool.ntp.org", + port: int = 123, + tz_offset: int = 0, + ) -> None: + """ + :param object socketpool: A socket provider such as CPython's `socket` module. + :param str server: The domain of the ntp server to query. + :param int port: The port of the ntp server to query. + :param float tz_offset: Timezone offset in hours from UTC. Only useful for timezone ignorant + CircuitPython. CPython will determine timezone automatically and adjust (so don't use + this.) For example, Pacific daylight savings time is -7. + """ + self._pool = socketpool + self._server = server + self._port = port + self._packet = bytearray(48) + self._tz_offset = tz_offset * 60 * 60 - def set_time(self, tz_offset: int = 0) -> None: - """Fetches and sets the microcontroller's current time - in seconds since since Jan 1, 1970. + # This is our estimated start time for the monotonic clock. We adjust it based on the ntp + # responses. + self._monotonic_start = 0 - :param int tz_offset: The offset of the local timezone, - in seconds west of UTC (negative in most of Western Europe, - positive in the US, zero in the UK). - """ + self.next_sync = 0 + + @property + def datetime(self) -> time.struct_time: + """Current time from NTP server.""" + if time.monotonic_ns() > self.next_sync: + self._packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode + for i in range(1, len(self._packet)): + self._packet[i] = 0 + with self._pool.socket(self._pool.AF_INET, self._pool.SOCK_DGRAM) as sock: + sock.sendto(self._packet, (self._server, self._port)) + sock.recvfrom_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] + self.next_sync = destination + (2**poll) * 1_000_000_000 + seconds = struct.unpack_from( + "!I", self._packet, offset=len(self._packet) - 8 + )[0] + self._monotonic_start = ( + seconds + + self._tz_offset + - NTP_TO_UNIX_EPOCH + - (destination // 1_000_000_000) + ) - try: - now = self._esp.get_time() - now = time.localtime(now[0] + tz_offset) - rtc.RTC().datetime = now - self.valid_time = True - except ValueError as error: - if self.debug: - print(str(error)) - return + return time.localtime( + time.monotonic_ns() // 1_000_000_000 + self._monotonic_start + ) diff --git a/docs/examples.rst b/docs/examples.rst index 7493fa0..053a48a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,8 +1,26 @@ Simple test ------------ -Ensure your device works with this simple test. +Ensure your device works with this simple test that prints out the time from NTP. .. literalinclude:: ../examples/ntp_simpletest.py :caption: examples/ntp_simpletest.py :linenos: + +Set RTC +------------ + +Sync your CircuitPython board's realtime clock (RTC) with time from NTP. + +.. literalinclude:: ../examples/ntp_set_rtc.py + :caption: examples/ntp_set_rtc.py + :linenos: + +Simple test with CPython +------------------------ + +Test the library in CPython using ``socket``. + +.. literalinclude:: ../examples/ntp_cpython.py + :caption: examples/ntp_cpython.py + :linenos: diff --git a/examples/ntp_cpython.py b/examples/ntp_cpython.py new file mode 100644 index 0000000..76d5735 --- /dev/null +++ b/examples/ntp_cpython.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries +# SPDX-License-Identifier: MIT + +"""Tests NTP with CPython socket""" + +import socket +import time + +import adafruit_ntp + +# Don't use tz_offset kwarg with CPython because it will adjust automatically. +ntp = adafruit_ntp.NTP(socket) + +while True: + print(ntp.datetime) + time.sleep(1) diff --git a/examples/ntp_set_rtc.py b/examples/ntp_set_rtc.py new file mode 100644 index 0000000..363c8b4 --- /dev/null +++ b/examples/ntp_set_rtc.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries +# SPDX-License-Identifier: MIT + +"""Example demonstrating how to set the realtime clock (RTC) based on NTP time.""" + +import time + +import rtc +import socketpool +import wifi + +import adafruit_ntp + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +wifi.radio.connect(secrets["ssid"], secrets["password"]) + +pool = socketpool.SocketPool(wifi.radio) +ntp = adafruit_ntp.NTP(pool, tz_offset=0) + +# NOTE: This changes the system time so make sure you aren't assuming that time +# doesn't jump. +rtc.RTC().datetime = ntp.datetime + +while True: + print(time.localtime()) + time.sleep(1) diff --git a/examples/ntp_simpletest.py b/examples/ntp_simpletest.py index 9db4ab0..0258e16 100644 --- a/examples/ntp_simpletest.py +++ b/examples/ntp_simpletest.py @@ -1,55 +1,27 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries # SPDX-License-Identifier: MIT +"""Print out time based on NTP.""" + import time -import board -import busio -from digitalio import DigitalInOut -from adafruit_esp32spi import adafruit_esp32spi -from adafruit_ntp import NTP - -# If you are using a board with pre-defined ESP32 Pins: -esp32_cs = DigitalInOut(board.ESP_CS) -esp32_ready = DigitalInOut(board.ESP_BUSY) -esp32_reset = DigitalInOut(board.ESP_RESET) - -# If you have an externally connected ESP32: -# esp32_cs = DigitalInOut(board.D9) -# esp32_ready = DigitalInOut(board.D10) -# esp32_reset = DigitalInOut(board.D5) - -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) - -print("Connecting to AP...") -while not esp.is_connected: - try: - esp.connect_AP(b"WIFI_SSID", b"WIFI_PASS") - except RuntimeError as e: - print("could not connect to AP, retrying: ", e) - continue - -# Initialize the NTP object -ntp = NTP(esp) - -# Fetch and set the microcontroller's current UTC time -# keep retrying until a valid time is returned -while not ntp.valid_time: - ntp.set_time() - print("Failed to obtain time, retrying in 5 seconds...") - time.sleep(5) - -# Get the current time in seconds since Jan 1, 1970 -current_time = time.time() -print("Seconds since Jan 1, 1970: {} seconds".format(current_time)) - -# Convert the current time in seconds since Jan 1, 1970 to a struct_time -now = time.localtime(current_time) -print(now) - -# Pretty-parse the struct_time -print( - "It is currently {}/{}/{} at {}:{}:{} UTC".format( - now.tm_mon, now.tm_mday, now.tm_year, now.tm_hour, now.tm_min, now.tm_sec - ) -) + +import socketpool +import wifi + +import adafruit_ntp + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +wifi.radio.connect(secrets["ssid"], secrets["password"]) + +pool = socketpool.SocketPool(wifi.radio) +ntp = adafruit_ntp.NTP(pool, tz_offset=0) + +while True: + print(ntp.datetime) + time.sleep(1) diff --git a/requirements.txt b/requirements.txt index c5f36e9..c0ada24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # # SPDX-License-Identifier: Unlicense - -Adafruit-Blinka -adafruit-circuitpython-esp32spi diff --git a/setup.py b/setup.py index 56f78bc..5ce60c7 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ name="adafruit-circuitpython-ntp", use_scm_version=True, setup_requires=["setuptools_scm"], - description="Network Time Protocol (NTP) helper for CircuitPython", + description="Network Time Protocol (NTP) helper for Python", long_description=long_description, long_description_content_type="text/x-rst", # The project's main homepage. @@ -33,7 +33,7 @@ # Author details author="Adafruit Industries", author_email="circuitpython@adafruit.com", - install_requires=["Adafruit-Blinka", "adafruit-circuitpython-esp32spi"], + install_requires=[], # Choose your license license="MIT", # See https://pypi.python.org/pypi?%3Aaction=list_classifiers