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

Switch to using ConnectionManager #197

Merged
merged 14 commits into from Feb 29, 2024
Merged
6 changes: 5 additions & 1 deletion .gitignore
Expand Up @@ -47,5 +47,9 @@ _build
.vscode
*~

# tox local cache
# tox-specific files
.tox
build

# coverage-specific files
.coverage
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Expand Up @@ -4,7 +4,7 @@

repos:
- repo: https://github.com/python/black
rev: 23.3.0
rev: 24.2.0
hooks:
- id: black
- repo: https://github.com/fsfe/reuse-tool
Expand Down Expand Up @@ -32,11 +32,11 @@ repos:
types: [python]
files: "^examples/"
args:
- --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code
- --disable=consider-using-f-string,duplicate-code,missing-docstring,invalid-name
- id: pylint
name: pylint (test code)
description: Run pylint rules on "tests/*.py" files
types: [python]
files: "^tests/"
args:
- --disable=missing-docstring,consider-using-f-string,duplicate-code
- --disable=consider-using-f-string,duplicate-code,missing-docstring,invalid-name,protected-access
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -24,6 +24,7 @@ Dependencies
This driver depends on:

* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
* `Adafruit CircuitPython ConnectionManager <https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager/>`_

Please ensure all dependencies are available on the CircuitPython filesystem.
This is easily achieved by downloading
Expand Down
142 changes: 17 additions & 125 deletions adafruit_minimqtt/adafruit_minimqtt.py
Expand Up @@ -26,12 +26,17 @@
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases

* Adafruit's Connection Manager library:
https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager

"""
import errno
import struct
import time
from random import randint

from adafruit_connection_manager import get_connection_manager

try:
from typing import List, Optional, Tuple, Type, Union
except ImportError:
Expand Down Expand Up @@ -82,64 +87,13 @@
class MMQTTException(Exception):
"""MiniMQTT Exception class."""

# pylint: disable=unnecessary-pass
# pass


class TemporaryError(Exception):
"""Temporary error class used for handling reconnects."""


# Legacy ESP32SPI Socket API
def set_socket(sock, iface=None) -> None:
"""Legacy API for setting the socket and network interface.

:param sock: socket object.
:param iface: internet interface object

"""
global _default_sock # pylint: disable=invalid-name, global-statement
global _fake_context # pylint: disable=invalid-name, global-statement
_default_sock = sock
if iface:
_default_sock.set_interface(iface)
_fake_context = _FakeSSLContext(iface)


class _FakeSSLSocket:
def __init__(self, socket, tls_mode) -> None:
self._socket = socket
self._mode = tls_mode
self.settimeout = socket.settimeout
self.send = socket.send
self.recv = socket.recv
self.close = socket.close

def connect(self, address):
"""connect wrapper to add non-standard mode parameter"""
try:
return self._socket.connect(address, self._mode)
except RuntimeError as error:
raise OSError(errno.ENOMEM) from error


class _FakeSSLContext:
def __init__(self, iface) -> None:
self._iface = iface

def wrap_socket(self, socket, server_hostname=None) -> _FakeSSLSocket:
"""Return the same socket"""
# pylint: disable=unused-argument
return _FakeSSLSocket(socket, self._iface.TLS_MODE)


class NullLogger:
"""Fake logger class that does not do anything"""

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

def __init__(self) -> None:
for log_level in ["debug", "info", "warning", "error", "critical"]:
Expand Down Expand Up @@ -194,6 +148,7 @@ def __init__(
user_data=None,
use_imprecise_time: Optional[bool] = None,
) -> None:
self._connection_manager = get_connection_manager(socket_pool)
self._socket_pool = socket_pool
self._ssl_context = ssl_context
self._sock = None
Expand Down Expand Up @@ -300,75 +255,6 @@ def get_monotonic_time(self) -> float:

return time.monotonic()

# pylint: disable=too-many-branches
def _get_connect_socket(self, host: str, port: int, *, timeout: int = 1):
"""Obtains a new socket and connects to a broker.

:param str host: Desired broker hostname
:param int port: Desired broker port
:param int timeout: Desired socket timeout, in seconds
"""
# For reconnections - check if we're using a socket already and close it
if self._sock:
self._sock.close()
self._sock = None

# Legacy API - use the interface's socket instead of a passed socket pool
if self._socket_pool is None:
self._socket_pool = _default_sock

# Legacy API - fake the ssl context
if self._ssl_context is None:
self._ssl_context = _fake_context

if not isinstance(port, int):
raise RuntimeError("Port must be an integer")

if self._is_ssl and not self._ssl_context:
raise RuntimeError(
"ssl_context must be set before using adafruit_mqtt for secure MQTT."
)

if self._is_ssl:
self.logger.info(f"Establishing a SECURE SSL connection to {host}:{port}")
else:
self.logger.info(f"Establishing an INSECURE connection to {host}:{port}")

addr_info = self._socket_pool.getaddrinfo(
host, port, 0, self._socket_pool.SOCK_STREAM
)[0]

try:
sock = self._socket_pool.socket(addr_info[0], addr_info[1])
except OSError as exc:
# Do not consider this for back-off.
self.logger.warning(
f"Failed to create socket for host {addr_info[0]} and port {addr_info[1]}"
)
raise TemporaryError from exc

connect_host = addr_info[-1][0]
if self._is_ssl:
sock = self._ssl_context.wrap_socket(sock, server_hostname=host)
connect_host = host
sock.settimeout(timeout)

try:
sock.connect((connect_host, port))
except MemoryError as exc:
sock.close()
self.logger.warning(f"Failed to allocate memory for connect: {exc}")
# Do not consider this for back-off.
raise TemporaryError from exc
except OSError as exc:
sock.close()
self.logger.warning(f"Failed to connect: {exc}")
# Do not consider this for back-off.
raise TemporaryError from exc

self._backwards_compatible_sock = not hasattr(sock, "recv_into")
return sock

def __enter__(self):
return self

Expand Down Expand Up @@ -538,8 +424,8 @@ def connect(
)
self._reset_reconnect_backoff()
return ret
except TemporaryError as e:
self.logger.warning(f"temporary error when connecting: {e}")
except RuntimeError as e:
self.logger.warning(f"Socket error when connecting: {e}")
backoff = False
except MMQTTException as e:
last_exception = e
Expand Down Expand Up @@ -587,9 +473,15 @@ def _connect(
time.sleep(self._reconnect_timeout)

# Get a new socket
self._sock = self._get_connect_socket(
self.broker, self.port, timeout=self._socket_timeout
self._sock = self._connection_manager.get_socket(
self.broker,
self.port,
proto="mqtt:",
timeout=self._socket_timeout,
is_ssl=self._is_ssl,
ssl_context=self._ssl_context,
)
self._backwards_compatible_sock = not hasattr(self._sock, "recv_into")

fixed_header = bytearray([0x10])

Expand Down Expand Up @@ -686,7 +578,7 @@ def disconnect(self) -> None:
except RuntimeError as e:
self.logger.warning(f"Unable to send DISCONNECT packet: {e}")
self.logger.debug("Closing socket")
self._sock.close()
self._connection_manager.free_socket(self._sock)
self._is_connected = False
self._subscribed_topics = []
self._last_msg_sent_timestamp = 0
Expand Down
17 changes: 17 additions & 0 deletions conftest.py
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2023 Justin Myers for Adafruit Industries
brentru marked this conversation as resolved.
Show resolved Hide resolved
#
# SPDX-License-Identifier: Unlicense

""" PyTest Setup """

import pytest
import adafruit_connection_manager


@pytest.fixture(autouse=True)
def reset_connection_manager(monkeypatch):
"""Reset the ConnectionManager, since it's a singlton and will hold data"""
monkeypatch.setattr(
"adafruit_minimqtt.adafruit_minimqtt.get_connection_manager",
adafruit_connection_manager.ConnectionManager,
)
31 changes: 17 additions & 14 deletions examples/cellular/minimqtt_adafruitio_cellular.py
@@ -1,22 +1,24 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT

import os
import time
import board
import busio
import digitalio
import adafruit_connection_manager
from adafruit_fona.adafruit_fona import FONA
import adafruit_fona.adafruit_fona_network as network
import adafruit_fona.adafruit_fona_socket as socket
import adafruit_fona.adafruit_fona_socket as pool

import adafruit_minimqtt.adafruit_minimqtt as MQTT

# Get Adafruit IO details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("GPRS secrets are kept in secrets.py, please add them there!")
raise
# Add settings.toml to your filesystem CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD keys
# with your GPRS credentials. Add your Adafruit IO username and key as well.
# DO NOT share that file or commit it into Git or other source control.

aio_username = os.getenv("aio_username")
aio_key = os.getenv("aio_key")

### Cellular ###

Expand All @@ -29,10 +31,10 @@
### Feeds ###

# Setup a feed named 'photocell' for publishing to a feed
photocell_feed = secrets["aio_username"] + "/feeds/photocell"
photocell_feed = aio_username + "/feeds/photocell"

# Setup a feed named 'onoff' for subscribing to changes
onoff_feed = secrets["aio_username"] + "/feeds/onoff"
onoff_feed = aio_username + "/feeds/onoff"

### Code ###

Expand Down Expand Up @@ -60,7 +62,7 @@ def message(client, topic, message):

# Initialize cellular data network
network = network.CELLULAR(
fona, (secrets["apn"], secrets["apn_username"], secrets["apn_password"])
fona, (os.getenv("apn"), os.getenv("apn_username"), os.getenv("apn_password"))
)

while not network.is_attached:
Expand All @@ -74,16 +76,17 @@ def message(client, topic, message):
time.sleep(0.5)
print("Network Connected!")

# Initialize MQTT interface with the cellular interface
MQTT.set_socket(socket, fona)
ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, fona)

# Set up a MiniMQTT Client
# NOTE: We'll need to connect insecurely for ethernet configurations.
mqtt_client = MQTT.MQTT(
broker="io.adafruit.com",
username=secrets["aio_username"],
password=secrets["aio_key"],
username=aio_username,
password=aio_key,
is_ssl=False,
socket_pool=pool,
ssl_context=ssl_context,
)

# Setup the callback methods above
Expand Down