Skip to content

Commit 56cec23

Browse files
committed
Switch to using ConnectionManager
1 parent 4a52082 commit 56cec23

File tree

6 files changed

+79
-131
lines changed

6 files changed

+79
-131
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,9 @@ _build
4747
.vscode
4848
*~
4949

50-
# tox local cache
50+
# tox-specific files
5151
.tox
52+
build
53+
54+
# coverage-specific files
55+
.coverage

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ repos:
3939
types: [python]
4040
files: "^tests/"
4141
args:
42-
- --disable=missing-docstring,consider-using-f-string,duplicate-code
42+
- --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code

adafruit_minimqtt/adafruit_minimqtt.py

Lines changed: 22 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@
2626
* Adafruit CircuitPython firmware for the supported boards:
2727
https://github.com/adafruit/circuitpython/releases
2828
29+
* Adafruit's Connection Manager library:
30+
https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager
31+
2932
"""
3033
import errno
3134
import struct
3235
import time
3336
from random import randint
3437

38+
from adafruit_connectionmanager import (
39+
get_connection_manager,
40+
SocketGetOSError,
41+
SocketConnectMemoryError,
42+
)
43+
3544
try:
3645
from typing import List, Optional, Tuple, Type, Union
3746
except ImportError:
@@ -78,68 +87,19 @@
7887
_default_sock = None # pylint: disable=invalid-name
7988
_fake_context = None # pylint: disable=invalid-name
8089

90+
TemporaryError = (SocketGetOSError, SocketConnectMemoryError)
91+
8192

8293
class MMQTTException(Exception):
8394
"""MiniMQTT Exception class."""
8495

85-
# pylint: disable=unnecessary-pass
86-
# pass
87-
88-
89-
class TemporaryError(Exception):
90-
"""Temporary error class used for handling reconnects."""
91-
92-
93-
# Legacy ESP32SPI Socket API
94-
def set_socket(sock, iface=None) -> None:
95-
"""Legacy API for setting the socket and network interface.
96-
97-
:param sock: socket object.
98-
:param iface: internet interface object
99-
100-
"""
101-
global _default_sock # pylint: disable=invalid-name, global-statement
102-
global _fake_context # pylint: disable=invalid-name, global-statement
103-
_default_sock = sock
104-
if iface:
105-
_default_sock.set_interface(iface)
106-
_fake_context = _FakeSSLContext(iface)
107-
108-
109-
class _FakeSSLSocket:
110-
def __init__(self, socket, tls_mode) -> None:
111-
self._socket = socket
112-
self._mode = tls_mode
113-
self.settimeout = socket.settimeout
114-
self.send = socket.send
115-
self.recv = socket.recv
116-
self.close = socket.close
117-
118-
def connect(self, address):
119-
"""connect wrapper to add non-standard mode parameter"""
120-
try:
121-
return self._socket.connect(address, self._mode)
122-
except RuntimeError as error:
123-
raise OSError(errno.ENOMEM) from error
124-
125-
126-
class _FakeSSLContext:
127-
def __init__(self, iface) -> None:
128-
self._iface = iface
129-
130-
def wrap_socket(self, socket, server_hostname=None) -> _FakeSSLSocket:
131-
"""Return the same socket"""
132-
# pylint: disable=unused-argument
133-
return _FakeSSLSocket(socket, self._iface.TLS_MODE)
134-
13596

13697
class NullLogger:
13798
"""Fake logger class that does not do anything"""
13899

139100
# pylint: disable=unused-argument
140101
def nothing(self, msg: str, *args) -> None:
141102
"""no action"""
142-
pass
143103

144104
def __init__(self) -> None:
145105
for log_level in ["debug", "info", "warning", "error", "critical"]:
@@ -194,6 +154,7 @@ def __init__(
194154
user_data=None,
195155
use_imprecise_time: Optional[bool] = None,
196156
) -> None:
157+
self._connection_manager = get_connection_manager(socket_pool)
197158
self._socket_pool = socket_pool
198159
self._ssl_context = ssl_context
199160
self._sock = None
@@ -300,77 +261,6 @@ def get_monotonic_time(self) -> float:
300261

301262
return time.monotonic()
302263

303-
# pylint: disable=too-many-branches
304-
def _get_connect_socket(self, host: str, port: int, *, timeout: int = 1):
305-
"""Obtains a new socket and connects to a broker.
306-
307-
:param str host: Desired broker hostname
308-
:param int port: Desired broker port
309-
:param int timeout: Desired socket timeout, in seconds
310-
"""
311-
# For reconnections - check if we're using a socket already and close it
312-
if self._sock:
313-
self._sock.close()
314-
self._sock = None
315-
316-
# Legacy API - use the interface's socket instead of a passed socket pool
317-
if self._socket_pool is None:
318-
self._socket_pool = _default_sock
319-
320-
# Legacy API - fake the ssl context
321-
if self._ssl_context is None:
322-
self._ssl_context = _fake_context
323-
324-
if not isinstance(port, int):
325-
raise RuntimeError("Port must be an integer")
326-
327-
if self._is_ssl and not self._ssl_context:
328-
raise RuntimeError(
329-
"ssl_context must be set before using adafruit_mqtt for secure MQTT."
330-
)
331-
332-
if self._is_ssl:
333-
self.logger.info(f"Establishing a SECURE SSL connection to {host}:{port}")
334-
else:
335-
self.logger.info(f"Establishing an INSECURE connection to {host}:{port}")
336-
337-
addr_info = self._socket_pool.getaddrinfo(
338-
host, port, 0, self._socket_pool.SOCK_STREAM
339-
)[0]
340-
341-
try:
342-
sock = self._socket_pool.socket(addr_info[0], addr_info[1])
343-
except OSError as exc:
344-
# Do not consider this for back-off.
345-
self.logger.warning(
346-
f"Failed to create socket for host {addr_info[0]} and port {addr_info[1]}"
347-
)
348-
raise TemporaryError from exc
349-
350-
connect_host = addr_info[-1][0]
351-
if self._is_ssl:
352-
sock = self._ssl_context.wrap_socket(sock, server_hostname=host)
353-
connect_host = host
354-
sock.settimeout(timeout)
355-
356-
last_exception = None
357-
try:
358-
sock.connect((connect_host, port))
359-
except MemoryError as exc:
360-
sock.close()
361-
self.logger.warning(f"Failed to allocate memory for connect: {exc}")
362-
# Do not consider this for back-off.
363-
raise TemporaryError from exc
364-
except OSError as exc:
365-
sock.close()
366-
last_exception = exc
367-
368-
if last_exception:
369-
raise last_exception
370-
371-
self._backwards_compatible_sock = not hasattr(sock, "recv_into")
372-
return sock
373-
374264
def __enter__(self):
375265
return self
376266

@@ -593,8 +483,15 @@ def _connect(
593483
time.sleep(self._reconnect_timeout)
594484

595485
# Get a new socket
596-
self._sock = self._get_connect_socket(
597-
self.broker, self.port, timeout=self._socket_timeout
486+
self._sock = self._connection_manager.get_socket(
487+
self.broker,
488+
self.port,
489+
"mqtt:",
490+
timeout=self._socket_timeout,
491+
is_ssl=self._is_ssl,
492+
ssl_context=self._ssl_context,
493+
max_retries=1, # setting to 1 since we want to handle backoff internally
494+
exception_passthrough=True,
598495
)
599496

600497
# Fixed Header
@@ -689,7 +586,7 @@ def disconnect(self) -> None:
689586
except RuntimeError as e:
690587
self.logger.warning(f"Unable to send DISCONNECT packet: {e}")
691588
self.logger.debug("Closing socket")
692-
self._sock.close()
589+
self._connection_manager.free_socket(self._sock)
693590
self._is_connected = False
694591
self._subscribed_topics = []
695592
if self.on_disconnect is not None:

conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-FileCopyrightText: 2023 Justin Myers for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
""" PyTest Setup """
6+
7+
import pytest
8+
import adafruit_connectionmanager
9+
10+
11+
@pytest.fixture(autouse=True)
12+
def reset_connection_manager(monkeypatch):
13+
"""Reset the ConnectionManager, since it's a singlton and will hold data"""
14+
monkeypatch.setattr(
15+
"adafruit_minimqtt.adafruit_minimqtt.get_connection_manager",
16+
adafruit_connectionmanager.ConnectionManager,
17+
)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
# SPDX-License-Identifier: Unlicense
44

55
Adafruit-Blinka
6+
Adafruit-Circuitpython-ConnectionManager@git+https://github.com/justmobilize/Adafruit_CircuitPython_ConnectionManager

tox.ini

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,38 @@
33
# SPDX-License-Identifier: MIT
44

55
[tox]
6-
envlist = py39
6+
envlist = py311
77

88
[testenv]
9-
changedir = {toxinidir}/tests
10-
deps = pytest==6.2.5
11-
commands = pytest -v
9+
description = run tests
10+
deps =
11+
pytest==7.4.3
12+
pytest-subtests==0.11.0
13+
commands = pytest
14+
15+
[testenv:coverage]
16+
description = run coverage
17+
deps =
18+
pytest==7.4.3
19+
pytest-cov==4.1.0
20+
pytest-subtests==0.11.0
21+
package = editable
22+
commands =
23+
coverage run --source=. --omit=tests/* --branch {posargs} -m pytest
24+
coverage report
25+
coverage html
26+
27+
[testenv:lint]
28+
description = run linters
29+
deps =
30+
pre-commit==3.6.0
31+
skip_install = true
32+
commands = pre-commit run {posargs}
33+
34+
[testenv:docs]
35+
description = build docs
36+
deps =
37+
-r requirements.txt
38+
-r docs/requirements.txt
39+
skip_install = true
40+
commands = sphinx-build -W -b html docs/ _build/

0 commit comments

Comments
 (0)