diff --git a/adafruit_ntp.py b/adafruit_ntp.py index 6297ebf..a8755c2 100644 --- a/adafruit_ntp.py +++ b/adafruit_ntp.py @@ -36,42 +36,50 @@ https://github.com/adafruit/circuitpython/releases """ +import struct import time -import rtc __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. + This module does not handle daylight savings or local time. It simply requests + UTC from a NTP server. - :param adafruit_esp32spi esp: ESP32SPI object. + :param object socketpool: A socket provider such as CPython's `socket` module. """ - def __init__(self, esp): - # 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 + def __init__(self, socketpool, *, server="pool.ntp.org", port=123): + self._pool = socketpool + self._server = server + self._port = port + self._packet = bytearray(48) - def set_time(self, tz_offset=0): - """Fetches and sets the microcontroller's current time - in seconds since since Jan 1, 1970. - :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). - """ + # This is our estimated start time for the monotonic clock. + # We adjust it based on the ntp responses. + self._monotonic_start = 0 - 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: - print(str(error)) - return + self.next_sync = 0 + + @property + def datetime(self): + 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)) + size, address = 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 - NTP_TO_UNIX_EPOCH - (destination // 1_000_000_000) + + return time.localtime(time.monotonic_ns() // 1_000_000_000 + self._monotonic_start) diff --git a/examples/ntp_esp32spi.py b/examples/ntp_esp32spi.py new file mode 100644 index 0000000..7b0598f --- /dev/null +++ b/examples/ntp_esp32spi.py @@ -0,0 +1,52 @@ +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 + ) +) diff --git a/examples/ntp_simpletest.py b/examples/ntp_simpletest.py index 7b0598f..a3b3d2f 100644 --- a/examples/ntp_simpletest.py +++ b/examples/ntp_simpletest.py @@ -1,52 +1,26 @@ +import socketpool import time -import board -import busio -from digitalio import DigitalInOut -from adafruit_esp32spi import adafruit_esp32spi -from adafruit_ntp import NTP +import wifi -# 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) +import adafruit_ntp -# 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) +# Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and +# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other +# source control. +# pylint: disable=no-name-in-module,wrong-import-order +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise -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 +wifi.radio.connect(secrets["ssid"], secrets["password"]) -# Initialize the NTP object -ntp = NTP(esp) +pool = socketpool.SocketPool(wifi.radio) +ntp = adafruit_ntp.NTP(pool) -# 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 - ) -) +while True: + print(ntp.datetime) + time.sleep(1) + \ No newline at end of file diff --git a/examples/ntp_simpletest_cpython.py b/examples/ntp_simpletest_cpython.py new file mode 100644 index 0000000..23557d8 --- /dev/null +++ b/examples/ntp_simpletest_cpython.py @@ -0,0 +1,9 @@ +import adafruit_ntp +import socket +import time + +ntp = adafruit_ntp.NTP(socket) + +while True: + print(ntp.datetime) + time.sleep(1)