Skip to content

Use UDP to talk to NTP servers #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 19 additions & 36 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 60 additions & 38 deletions adafruit_ntp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT

Expand All @@ -8,7 +8,7 @@

Network Time Protocol (NTP) helper for CircuitPython

* Author(s): Brent Rubell
* Author(s): Scott Shawcroft

Implementation Notes
--------------------
Expand All @@ -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
)
20 changes: 19 additions & 1 deletion docs/examples.rst
Original file line number Diff line number Diff line change
@@ -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:
16 changes: 16 additions & 0 deletions examples/ntp_cpython.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions examples/ntp_set_rtc.py
Original file line number Diff line number Diff line change
@@ -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)
76 changes: 24 additions & 52 deletions examples/ntp_simpletest.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

Adafruit-Blinka
adafruit-circuitpython-esp32spi
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
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.
url="https://github.com/adafruit/Adafruit_CircuitPython_NTP",
# Author details
author="Adafruit Industries",
author_email="[email protected]",
install_requires=["Adafruit-Blinka", "adafruit-circuitpython-esp32spi"],
install_requires=[],
# Choose your license
license="MIT",
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
Expand Down