Working MQTT back !
This commit is contained in:
+96
-35
@@ -11,13 +11,10 @@ try:
|
||||
except ImportError:
|
||||
try:
|
||||
from umqtt.simple import MQTTClient as _MQTTClient
|
||||
import _thread
|
||||
import gc
|
||||
BACKEND_NAME = "umqtt.simple"
|
||||
IS_MICROPYTHON = True
|
||||
# except ImportError:
|
||||
# try:
|
||||
# from umqtt.robust import MQTTClient as _MQTTClient
|
||||
# BACKEND_NAME = "umqtt.robust"
|
||||
# IS_MICROPYTHON = True
|
||||
except ImportError as exc:
|
||||
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
|
||||
|
||||
@@ -79,6 +76,11 @@ class BrokerClient:
|
||||
self._client = None
|
||||
self._callback = None
|
||||
self._messages = []
|
||||
self._cadata = None # Cache cert bytes to prevent heap fragmentation
|
||||
|
||||
# Thread safety lock for MicroPython socket reads/writes
|
||||
if IS_MICROPYTHON:
|
||||
self._lock = _thread.allocate_lock()
|
||||
|
||||
def set_callback(self, callback):
|
||||
self._callback = callback
|
||||
@@ -104,16 +106,24 @@ class BrokerClient:
|
||||
return self._client
|
||||
|
||||
if IS_MICROPYTHON:
|
||||
gc.collect() # Clean Python heap before importing/allocating SSL
|
||||
import ssl
|
||||
ssl_params = self.ssl_params
|
||||
|
||||
if self.use_tls and ssl_params is None:
|
||||
# MicroPython uses context-less structures.
|
||||
# If your CA is self-signed, validation can fail without a valid hostname match.
|
||||
# OPTION A: If broker uses 'require_certificate false' and self-signed certs:
|
||||
# Do NOT pass cadata when cert_reqs is CERT_NONE to save ~20KB of C-DRAM
|
||||
ssl_params = {
|
||||
"cert_reqs": ssl.CERT_NONE, # Temporarily change to NONE to test if validation is the culprit
|
||||
"cadata": _read_file_bytes(self.cafile)
|
||||
"cert_reqs": ssl.CERT_NONE,
|
||||
"server_hostname": self.host
|
||||
}
|
||||
|
||||
# OPTION B: If strict CA validation IS required, load cadata ONLY with CERT_REQUIRED:
|
||||
# ssl_params = {
|
||||
# "cert_reqs": ssl.CERT_REQUIRED,
|
||||
# "cadata": _read_file_bytes(self.cafile),
|
||||
# "server_hostname": self.host
|
||||
# }
|
||||
|
||||
client = _MQTTClient(
|
||||
self.client_id or "smartWave-client",
|
||||
@@ -150,19 +160,35 @@ class BrokerClient:
|
||||
return self._client
|
||||
|
||||
def connect(self):
|
||||
client = self.open()
|
||||
if IS_MICROPYTHON:
|
||||
client.connect()
|
||||
return client
|
||||
gc.collect() # Force C & Python memory cleanup right before TLS handshake
|
||||
|
||||
client.connect(self.host, self.port, self.keepalive)
|
||||
return client
|
||||
if self._client is not None:
|
||||
self.close()
|
||||
|
||||
client = self.open()
|
||||
|
||||
try:
|
||||
if IS_MICROPYTHON:
|
||||
gc.collect() # Sweep memory right before umqtt calls ssl.wrap_socket()
|
||||
with self._lock:
|
||||
client.connect()
|
||||
return client
|
||||
|
||||
client.connect(self.host, self.port, self.keepalive)
|
||||
return client
|
||||
except Exception as e:
|
||||
print("MQTT connection failed, closing client and releasing memory.")
|
||||
print("Exception:", e)
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def publish(self, topic, payload, qos=2, retain=False):
|
||||
client = self.open()
|
||||
payload_bytes = _ensure_bytes(payload)
|
||||
if IS_MICROPYTHON:
|
||||
return client.publish(topic, payload_bytes, retain=retain, qos=qos)
|
||||
with self._lock:
|
||||
return client.publish(topic, payload_bytes, retain=retain, qos=qos)
|
||||
|
||||
if isinstance(topic, bytes):
|
||||
topic = topic.decode('utf-8')
|
||||
@@ -172,8 +198,9 @@ class BrokerClient:
|
||||
def subscribe(self, topic, qos=2):
|
||||
client = self.open()
|
||||
if IS_MICROPYTHON:
|
||||
client.set_callback(self._on_micropython_message)
|
||||
return client.subscribe(topic, qos=qos)
|
||||
with self._lock:
|
||||
client.set_callback(self._on_micropython_message)
|
||||
return client.subscribe(topic, qos=qos)
|
||||
|
||||
if isinstance(topic, bytes):
|
||||
topic = topic.decode('utf-8')
|
||||
@@ -184,27 +211,43 @@ class BrokerClient:
|
||||
client = self.open()
|
||||
if IS_MICROPYTHON:
|
||||
import struct
|
||||
# Ensure the topic is bytes for writing to the socket
|
||||
import time
|
||||
topic_bytes = topic if isinstance(topic, bytes) else topic.encode('utf-8')
|
||||
|
||||
# 1. Build the MQTT unsubscribe packet header
|
||||
# 1. Increment and lock the PID for THIS specific request
|
||||
client.pid = (client.pid % 65535) + 1
|
||||
sent_pid = client.pid # <-- Store local copy
|
||||
|
||||
# 2. Construct UNSUBSCRIBE packet
|
||||
rem_len = 2 + 2 + len(topic_bytes)
|
||||
pkt = bytearray(b"\xa2\0\0\0")
|
||||
client.pid += 1
|
||||
struct.pack_into("!BH", pkt, 1, rem_len, sent_pid)
|
||||
|
||||
# Packet length is: 2 bytes (PID) + 2 bytes (topic length indicator) + topic string length
|
||||
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic_bytes), client.pid)
|
||||
|
||||
# 2. Write the packet to the socket
|
||||
# 3. Write packet to socket
|
||||
client.sock.write(pkt)
|
||||
client._send_str(topic_bytes)
|
||||
|
||||
# 3. Wait for the UNSUBACK confirmation frame (0xB0) from the broker
|
||||
while True:
|
||||
# 4. Wait for UNSUBACK (0xB0)
|
||||
start = time.time()
|
||||
while time.time() - start < 3:
|
||||
op = client.wait_msg()
|
||||
if op == 0xB0:
|
||||
resp = client.sock.read(3)
|
||||
assert resp[1] == pkt[2] and resp[2] == pkt[3]
|
||||
resp = bytearray(3)
|
||||
read_bytes = 0
|
||||
while read_bytes < 3:
|
||||
chunk = client.sock.read(3 - read_bytes)
|
||||
if chunk:
|
||||
resp[read_bytes:read_bytes + len(chunk)] = chunk
|
||||
read_bytes += len(chunk)
|
||||
else:
|
||||
time.sleep_ms(10)
|
||||
|
||||
# Compare against sent_pid instead of client.pid
|
||||
resp_pid = (resp[1] << 8) | resp[2]
|
||||
if resp_pid != sent_pid:
|
||||
print(f"[MQTT] UNSUBACK PID mismatch (expected {sent_pid}, got {resp_pid})")
|
||||
return client
|
||||
|
||||
return client
|
||||
|
||||
if isinstance(topic, bytes):
|
||||
@@ -219,14 +262,16 @@ class BrokerClient:
|
||||
if self._client is None:
|
||||
return None
|
||||
if IS_MICROPYTHON:
|
||||
return self._client.check_msg()
|
||||
with self._lock:
|
||||
return self._client.check_msg()
|
||||
return self._client.loop(timeout=timeout)
|
||||
|
||||
def wait(self):
|
||||
if self._client is None:
|
||||
return None
|
||||
if IS_MICROPYTHON:
|
||||
return self._client.wait_msg()
|
||||
with self._lock:
|
||||
return self._client.wait_msg()
|
||||
return self._client.loop_forever()
|
||||
|
||||
def get_message(self):
|
||||
@@ -235,13 +280,29 @@ class BrokerClient:
|
||||
return self._messages.pop(0)
|
||||
|
||||
def close(self):
|
||||
"""Safely clean up socket context without causing ESP32 C panics."""
|
||||
if self._client is None:
|
||||
return
|
||||
try:
|
||||
self._client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._client = None
|
||||
|
||||
if IS_MICROPYTHON:
|
||||
with self._lock:
|
||||
try:
|
||||
if hasattr(self._client, "sock") and self._client.sock:
|
||||
self._client.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if hasattr(self._client, "sock"):
|
||||
self._client.sock = None
|
||||
self._client = None
|
||||
gc.collect() # Immediately reclaim freed socket & mbedTLS RAM
|
||||
else:
|
||||
try:
|
||||
self._client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._client = None
|
||||
|
||||
def __enter__(self):
|
||||
self.connect()
|
||||
|
||||
Reference in New Issue
Block a user