Skip to content
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

maintain time.monotonic precision by using adafruit_ticks #210

Merged
merged 28 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
70f963b
maintain time.monotonic precision by using ns integer timestamps
kevin-tritz Mar 22, 2024
89bf045
formatted with Black
kevin-tritz Mar 22, 2024
639160a
added _ns suffix to vars and func
kevin-tritz Mar 22, 2024
9af0e3f
reverted func name back to get_monotonic_time to pass build check
kevin-tritz Mar 22, 2024
120d8a9
retain imprecise get_monotonic_time, add precision get_monotonic_time…
kevin-tritz Mar 25, 2024
5378f8f
reverted timestamps back to using ticks_ms from adafruit_ticks library
kevin-tritz Mar 25, 2024
51b0550
fix ping_timeout
kevin-tritz Mar 25, 2024
5b0c8cc
add adafruit_ticks to requirements.txt
kevin-tritz Mar 26, 2024
5ad6abb
revert
kevin-tritz Mar 26, 2024
2e3caae
Merge branch 'timestamp_ns' of https://github.com/kevin-tritz/Adafrui…
kevin-tritz Mar 26, 2024
bce70b2
add adafruit_ticks to requirements.txt
kevin-tritz Mar 26, 2024
92035c0
try lower case in requirements.txt
kevin-tritz Mar 26, 2024
1dd4652
ok, adafruit-circuitpython-ticks
kevin-tritz Mar 26, 2024
d5626ec
fix end of file error?
kevin-tritz Mar 26, 2024
128e3f0
purge last remnants of time.monotonic(), get rid of imprecise_time ar…
kevin-tritz May 6, 2024
830af78
remove test_loop dependence on mqtt.get_monotonic_time function
kevin-tritz May 6, 2024
82d6f9d
timestamp uses ticks_ms
kevin-tritz May 6, 2024
a1eec26
fix ticks_ms ref
kevin-tritz May 6, 2024
e873f43
modify ping_timeout test for 3 res
kevin-tritz May 6, 2024
804d7f7
black formatting
kevin-tritz May 6, 2024
b569ce7
Remove micropython from docs/conf.py
kevin-tritz May 7, 2024
89bd1e1
make loop ping timeout test more robust
kevin-tritz May 9, 2024
ee32e5e
cleaned up test_loop
kevin-tritz May 9, 2024
93b7b3e
fixed up ping_timeout
kevin-tritz May 12, 2024
4272252
ok, reverted all of the black reformats and just formatted minimqtt.py
kevin-tritz May 12, 2024
4486578
Merge branch 'main' into timestamp_ns
kevin-tritz Jun 4, 2024
1ffbe5e
fix for timeout test
FoamyGuy Jun 17, 2024
2d562af
tolerance value instead of rounding
FoamyGuy Jul 5, 2024
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
74 changes: 23 additions & 51 deletions adafruit_minimqtt/adafruit_minimqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from random import randint

from adafruit_connection_manager import get_connection_manager
from adafruit_ticks import ticks_ms, ticks_diff

try:
from typing import List, Optional, Tuple, Type, Union
Expand Down Expand Up @@ -123,8 +124,6 @@ class MQTT:
This works with all callbacks but the "on_message" and those added via add_topic_callback();
for those, to get access to the user_data use the 'user_data' member of the MQTT object
passed as 1st argument.
:param bool use_imprecise_time: on boards without time.monotonic_ns() one has to set
this to True in order to operate correctly over more than 24 days or so

"""

Expand All @@ -146,7 +145,6 @@ def __init__(
socket_timeout: int = 1,
connect_retries: int = 5,
user_data=None,
use_imprecise_time: Optional[bool] = None,
) -> None:
self._connection_manager = get_connection_manager(socket_pool)
self._socket_pool = socket_pool
Expand All @@ -155,20 +153,6 @@ def __init__(
self._backwards_compatible_sock = False
self._use_binary_mode = use_binary_mode

self.use_monotonic_ns = False
try:
time.monotonic_ns()
self.use_monotonic_ns = True
except AttributeError:
if use_imprecise_time:
self.use_monotonic_ns = False
else:
raise MMQTTException( # pylint: disable=raise-missing-from
"time.monotonic_ns() is not available. "
"Will use imprecise time however only if the"
"use_imprecise_time argument is set to True."
)

if recv_timeout <= socket_timeout:
raise MMQTTException(
"recv_timeout must be strictly greater than socket_timeout"
Expand All @@ -181,7 +165,7 @@ def __init__(
self._is_connected = False
self._msg_size_lim = MQTT_MSG_SZ_LIM
self._pid = 0
self._last_msg_sent_timestamp: float = 0
self._last_msg_sent_timestamp: int = 0
self.logger = NullLogger()
"""An optional logging attribute that can be set with with a Logger
to enable debug logging."""
Expand Down Expand Up @@ -220,7 +204,7 @@ def __init__(
self.client_id = client_id
else:
# assign a unique client_id
time_int = int(self.get_monotonic_time() * 100) % 1000
time_int = int(ticks_ms() / 10) % 1000
self.client_id = f"cpy{randint(0, time_int)}{randint(0, 99)}"
# generated client_id's enforce spec.'s length rules
if len(self.client_id.encode("utf-8")) > 23 or not self.client_id:
Expand All @@ -244,17 +228,6 @@ def __init__(
self.on_subscribe = None
self.on_unsubscribe = None

def get_monotonic_time(self) -> float:
"""
Provide monotonic time in seconds. Based on underlying implementation
this might result in imprecise time, that will result in the library
not being able to operate if running contiguously for more than 24 days or so.
"""
if self.use_monotonic_ns:
return time.monotonic_ns() / 1000000000

return time.monotonic()

def __enter__(self):
return self

Expand Down Expand Up @@ -526,9 +499,9 @@ def _connect(
if self._username is not None:
self._send_str(self._username)
self._send_str(self._password)
self._last_msg_sent_timestamp = self.get_monotonic_time()
self._last_msg_sent_timestamp = ticks_ms()
self.logger.debug("Receiving CONNACK packet from broker")
stamp = self.get_monotonic_time()
stamp = ticks_ms()
while True:
op = self._wait_for_msg()
if op == 32:
Expand All @@ -544,7 +517,7 @@ def _connect(
return result

if op is None:
if self.get_monotonic_time() - stamp > self._recv_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > self._recv_timeout:
raise MMQTTException(
f"No data received from broker for {self._recv_timeout} seconds."
)
Expand Down Expand Up @@ -594,14 +567,14 @@ def ping(self) -> list[int]:
self.logger.debug("Sending PINGREQ")
self._sock.send(MQTT_PINGREQ)
ping_timeout = self.keep_alive
stamp = self.get_monotonic_time()
stamp = ticks_ms()
self._last_msg_sent_timestamp = stamp
rc, rcs = None, []
while rc != MQTT_PINGRESP:
rc = self._wait_for_msg()
if rc:
rcs.append(rc)
if self.get_monotonic_time() - stamp > ping_timeout:
if ticks_diff(ticks_ms(), stamp) > ping_timeout * 1000:
dhalbert marked this conversation as resolved.
Show resolved Hide resolved
raise MMQTTException("PINGRESP not returned from broker.")
dhalbert marked this conversation as resolved.
Show resolved Hide resolved
return rcs

Expand Down Expand Up @@ -670,11 +643,11 @@ def publish(
self._sock.send(pub_hdr_fixed)
self._sock.send(pub_hdr_var)
self._sock.send(msg)
self._last_msg_sent_timestamp = self.get_monotonic_time()
self._last_msg_sent_timestamp = ticks_ms()
if qos == 0 and self.on_publish is not None:
self.on_publish(self, self.user_data, topic, self._pid)
if qos == 1:
stamp = self.get_monotonic_time()
stamp = ticks_ms()
while True:
op = self._wait_for_msg()
if op == 0x40:
Expand All @@ -688,7 +661,7 @@ def publish(
return

if op is None:
if self.get_monotonic_time() - stamp > self._recv_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > self._recv_timeout:
raise MMQTTException(
f"No data received from broker for {self._recv_timeout} seconds."
)
Expand Down Expand Up @@ -747,12 +720,12 @@ def subscribe(self, topic: Optional[Union[tuple, str, list]], qos: int = 0) -> N
self.logger.debug(f"SUBSCRIBING to topic {t} with QoS {q}")
self.logger.debug(f"payload: {payload}")
self._sock.send(payload)
stamp = self.get_monotonic_time()
stamp = ticks_ms()
self._last_msg_sent_timestamp = stamp
while True:
op = self._wait_for_msg()
if op is None:
if self.get_monotonic_time() - stamp > self._recv_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > self._recv_timeout:
raise MMQTTException(
f"No data received from broker for {self._recv_timeout} seconds."
)
Expand Down Expand Up @@ -824,13 +797,13 @@ def unsubscribe(self, topic: Optional[Union[str, list]]) -> None:
for t in topics:
self.logger.debug(f"UNSUBSCRIBING from topic {t}")
self._sock.send(payload)
self._last_msg_sent_timestamp = self.get_monotonic_time()
self._last_msg_sent_timestamp = ticks_ms()
self.logger.debug("Waiting for UNSUBACK...")
while True:
stamp = self.get_monotonic_time()
stamp = ticks_ms()
op = self._wait_for_msg()
if op is None:
if self.get_monotonic_time() - stamp > self._recv_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > self._recv_timeout:
raise MMQTTException(
f"No data received from broker for {self._recv_timeout} seconds."
)
Expand Down Expand Up @@ -930,12 +903,12 @@ def loop(self, timeout: float = 0) -> Optional[list[int]]:
self._connected()
self.logger.debug(f"waiting for messages for {timeout} seconds")

stamp = self.get_monotonic_time()
stamp = ticks_ms()
rcs = []

while True:
if (
self.get_monotonic_time() - self._last_msg_sent_timestamp
ticks_diff(ticks_ms(), self._last_msg_sent_timestamp) / 1000
>= self.keep_alive
):
# Handle KeepAlive by expecting a PINGREQ/PINGRESP from the server
Expand All @@ -945,22 +918,21 @@ def loop(self, timeout: float = 0) -> Optional[list[int]]:
rcs.extend(self.ping())
# ping() itself contains a _wait_for_msg() loop which might have taken a while,
# so check here as well.
if self.get_monotonic_time() - stamp > timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > timeout:
self.logger.debug(f"Loop timed out after {timeout} seconds")
break

rc = self._wait_for_msg()
if rc is not None:
rcs.append(rc)
if self.get_monotonic_time() - stamp > timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > timeout:
self.logger.debug(f"Loop timed out after {timeout} seconds")
break

return rcs if rcs else None

def _wait_for_msg(self, timeout: Optional[float] = None) -> Optional[int]:
# pylint: disable = too-many-return-statements

"""Reads and processes network events.
Return the packet type or None if there is nothing to be received.

Expand Down Expand Up @@ -1059,7 +1031,7 @@ def _sock_exact_recv(
:param float timeout: timeout, in seconds. Defaults to keep_alive
:return: byte array
"""
stamp = self.get_monotonic_time()
stamp = ticks_ms()
if not self._backwards_compatible_sock:
# CPython/Socketpool Impl.
rc = bytearray(bufsize)
Expand All @@ -1074,7 +1046,7 @@ def _sock_exact_recv(
recv_len = self._sock.recv_into(mv, to_read)
to_read -= recv_len
mv = mv[recv_len:]
if self.get_monotonic_time() - stamp > read_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > read_timeout:
raise MMQTTException(
f"Unable to receive {to_read} bytes within {read_timeout} seconds."
)
Expand All @@ -1094,7 +1066,7 @@ def _sock_exact_recv(
recv = self._sock.recv(to_read)
to_read -= len(recv)
rc += recv
if self.get_monotonic_time() - stamp > read_timeout:
if ticks_diff(ticks_ms(), stamp) / 1000 > read_timeout:
raise MMQTTException(
f"Unable to receive {to_read} bytes within {read_timeout} seconds."
)
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# Uncomment the below if you use native CircuitPython modules such as
# digitalio, micropython and busio. List the modules you use. Without it, the
# autodoc module docs will fail to generate with a warning.
autodoc_mock_imports = ["micropython", "microcontroller", "random"]
autodoc_mock_imports = ["microcontroller", "random"]


intersphinx_mapping = {
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

Adafruit-Blinka
Adafruit-Circuitpython-ConnectionManager
adafruit-circuitpython-ticks
6 changes: 3 additions & 3 deletions tests/test_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def test_loop_basic(self) -> None:
time_before = time.monotonic()
timeout = random.randint(3, 8)
# pylint: disable=protected-access
mqtt_client._last_msg_sent_timestamp = mqtt_client.get_monotonic_time()
mqtt_client._last_msg_sent_timestamp = MQTT.ticks_ms()
rcs = mqtt_client.loop(timeout=timeout)
time_after = time.monotonic()

Expand Down Expand Up @@ -220,10 +220,10 @@ def test_loop_ping_timeout(self):
mqtt_client._sock = mocket

start = time.monotonic()
res = mqtt_client.loop(timeout=2 * keep_alive_timeout)
res = mqtt_client.loop(timeout=2 * keep_alive_timeout + recv_timeout)
assert time.monotonic() - start >= 2 * keep_alive_timeout
assert len(mocket.sent) > 0
assert len(res) == 2
assert len(res) == 3
assert set(res) == {int(0xD0)}

# pylint: disable=no-self-use
Expand Down
Loading