micrOSDevToolKit 2.13.1__py3-none-any.whl → 2.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of micrOSDevToolKit might be problematic. Click here for more details.
- micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +125 -121
- micrOS/source/Common.py +48 -26
- micrOS/source/Config.py +13 -5
- micrOS/source/Espnow.py +100 -58
- micrOS/source/Files.py +77 -41
- micrOS/source/Hooks.py +18 -34
- micrOS/source/Logger.py +2 -7
- micrOS/source/Network.py +36 -16
- micrOS/source/Server.py +22 -8
- micrOS/source/Shell.py +9 -6
- micrOS/source/Tasks.py +34 -13
- micrOS/source/Web.py +69 -31
- micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Files.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Server.cpython-312.pyc +0 -0
- micrOS/source/config/_git.keep +0 -0
- micrOS/source/micrOS.py +7 -0
- micrOS/source/micrOSloader.py +2 -10
- micrOS/source/microIO.py +2 -2
- micrOS/source/modules/IO_esp32c6.py +38 -0
- micrOS/source/modules/LM_L298N.py +161 -0
- micrOS/source/modules/LM_cluster.py +250 -0
- {toolkit/workspace/precompiled → micrOS/source/modules}/LM_esp32.py +5 -0
- micrOS/source/modules/LM_espnow.py +36 -0
- micrOS/source/{LM_i2c.py → modules/LM_i2c.py} +1 -1
- micrOS/source/{LM_light_sensor.py → modules/LM_light_sensor.py} +2 -2
- micrOS/source/modules/LM_mqtt_client.py +246 -0
- micrOS/source/{LM_neoeffects.py → modules/LM_neoeffects.py} +14 -4
- micrOS/source/{LM_neomatrix.py → modules/LM_neomatrix.py} +140 -38
- micrOS/source/{LM_oled_ui.py → modules/LM_oled_ui.py} +2 -2
- micrOS/source/{LM_oledui.py → modules/LM_oledui.py} +2 -2
- micrOS/source/{LM_pacman.py → modules/LM_pacman.py} +74 -29
- micrOS/source/{LM_presence.py → modules/LM_presence.py} +2 -2
- micrOS/source/{LM_robustness.py → modules/LM_robustness.py} +49 -2
- micrOS/source/{LM_tcs3472.py → modules/LM_tcs3472.py} +4 -6
- micrOS/source/web/dashboard.html +2 -0
- micrOS/source/web/matrix_draw.html +390 -0
- micrOS/source/web/uapi.js +9 -6
- {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.0.dist-info}/METADATA +30 -37
- {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.0.dist-info}/RECORD +200 -190
- toolkit/DevEnvCompile.py +21 -12
- toolkit/DevEnvOTA.py +27 -16
- toolkit/DevEnvUSB.py +35 -21
- toolkit/LM_to_compile.dat +3 -1
- toolkit/MicrOSDevEnv.py +37 -21
- toolkit/dashboard_apps/QMI8685_GYRO.py +1 -1
- toolkit/dashboard_apps/SystemTest.py +8 -5
- toolkit/{MicrosFiles.py → lib/MicrosFiles.py} +24 -4
- toolkit/micrOSdashboard.py +2 -2
- toolkit/micrOSlint.py +17 -7
- toolkit/simulator_lib/__pycache__/simulator.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
- toolkit/simulator_lib/mqtt_as/Note.md +15 -0
- toolkit/simulator_lib/mqtt_as/__init__.py +950 -0
- toolkit/simulator_lib/mqtt_as/__pycache__/__init__.cpython-312.pyc +0 -0
- toolkit/simulator_lib/mqtt_as/clean.py +69 -0
- toolkit/simulator_lib/mqtt_as/mqtt_v5_properties.py +239 -0
- toolkit/simulator_lib/mqtt_as/range.py +90 -0
- toolkit/simulator_lib/mqtt_as/range_ex.py +119 -0
- toolkit/simulator_lib/simulator.py +14 -1
- toolkit/simulator_lib/uos.py +2 -0
- toolkit/workspace/precompiled/Common.mpy +0 -0
- toolkit/workspace/precompiled/Config.mpy +0 -0
- toolkit/workspace/precompiled/Espnow.mpy +0 -0
- toolkit/workspace/precompiled/Files.mpy +0 -0
- toolkit/workspace/precompiled/Hooks.mpy +0 -0
- toolkit/workspace/precompiled/Logger.mpy +0 -0
- toolkit/workspace/precompiled/Network.mpy +0 -0
- toolkit/workspace/precompiled/Server.mpy +0 -0
- toolkit/workspace/precompiled/Shell.mpy +0 -0
- toolkit/workspace/precompiled/Tasks.mpy +0 -0
- toolkit/workspace/precompiled/Web.mpy +0 -0
- toolkit/workspace/precompiled/config/_git.keep +0 -0
- toolkit/workspace/precompiled/micrOS.mpy +0 -0
- toolkit/workspace/precompiled/micrOSloader.mpy +0 -0
- toolkit/workspace/precompiled/microIO.mpy +0 -0
- toolkit/workspace/precompiled/{IO_esp32.mpy → modules/IO_esp32.mpy} +0 -0
- toolkit/workspace/precompiled/{IO_esp32c3.mpy → modules/IO_esp32c3.mpy} +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32c6.mpy +0 -0
- toolkit/workspace/precompiled/{IO_esp32s2.mpy → modules/IO_esp32s2.mpy} +0 -0
- toolkit/workspace/precompiled/{IO_esp32s3.mpy → modules/IO_esp32s3.mpy} +0 -0
- toolkit/workspace/precompiled/{IO_m5stamp.mpy → modules/IO_m5stamp.mpy} +0 -0
- toolkit/workspace/precompiled/{IO_qtpy.mpy → modules/IO_qtpy.mpy} +0 -0
- toolkit/workspace/precompiled/modules/IO_rp2.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_s3matrix.mpy +0 -0
- toolkit/workspace/precompiled/{IO_tinypico.mpy → modules/IO_tinypico.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_L298N.mpy +0 -0
- toolkit/workspace/precompiled/{LM_OV2640.mpy → modules/LM_OV2640.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_aht10.mpy → modules/LM_aht10.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_bme280.mpy → modules/LM_bme280.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_buzzer.mpy → modules/LM_buzzer.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_cct.mpy → modules/LM_cct.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_cluster.mpy +0 -0
- toolkit/workspace/precompiled/{LM_co2.mpy → modules/LM_co2.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_dht11.mpy → modules/LM_dht11.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_dht22.mpy → modules/LM_dht22.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_dimmer.mpy → modules/LM_dimmer.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_distance.mpy → modules/LM_distance.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_ds18.mpy → modules/LM_ds18.mpy} +0 -0
- {micrOS/source → toolkit/workspace/precompiled/modules}/LM_esp32.py +5 -0
- toolkit/workspace/precompiled/modules/LM_espnow.py +36 -0
- toolkit/workspace/precompiled/{LM_gameOfLife.mpy → modules/LM_gameOfLife.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_genIO.mpy → modules/LM_genIO.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_haptic.mpy → modules/LM_haptic.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_i2c.py → modules/LM_i2c.py} +1 -1
- toolkit/workspace/precompiled/{LM_i2s_mic.mpy → modules/LM_i2s_mic.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_keychain.mpy → modules/LM_keychain.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_ld2410.mpy → modules/LM_ld2410.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_mqtt_client.mpy +0 -0
- toolkit/workspace/precompiled/{LM_neoeffects.mpy → modules/LM_neoeffects.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_neomatrix.mpy +0 -0
- toolkit/workspace/precompiled/{LM_neopixel.mpy → modules/LM_neopixel.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_oled.mpy → modules/LM_oled.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_oled_sh1106.mpy → modules/LM_oled_sh1106.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
- toolkit/workspace/precompiled/{LM_rest.mpy → modules/LM_rest.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_rgb.mpy → modules/LM_rgb.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_rgbcct.mpy → modules/LM_rgbcct.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_roboarm.mpy → modules/LM_roboarm.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_robustness.py → modules/LM_robustness.py} +49 -2
- toolkit/workspace/precompiled/{LM_servo.mpy → modules/LM_servo.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_sound_event.mpy → modules/LM_sound_event.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_stepper.mpy → modules/LM_stepper.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_switch.mpy → modules/LM_switch.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_system.mpy → modules/LM_system.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_tcs3472.py → modules/LM_tcs3472.py} +4 -6
- toolkit/workspace/precompiled/{LM_telegram.mpy → modules/LM_telegram.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_tinyrgb.mpy → modules/LM_tinyrgb.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_trackball.mpy → modules/LM_trackball.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_veml7700.mpy → modules/LM_veml7700.mpy} +0 -0
- toolkit/workspace/precompiled/web/dashboard.html +2 -0
- toolkit/workspace/precompiled/web/matrix_draw.html +390 -0
- toolkit/workspace/precompiled/web/uapi.js +9 -6
- micrOS/source/IO_esp32c6.py +0 -16
- micrOS/source/LM_L298N_DCmotor.py +0 -86
- micrOS/source/LM_espnow.py +0 -57
- micrOS/source/LM_mqtt_pro.py +0 -211
- toolkit/lib/file_extensions.py +0 -22
- toolkit/workspace/precompiled/Common.cpython-312.pyc +0 -0
- toolkit/workspace/precompiled/IO_esp32c6.mpy +0 -0
- toolkit/workspace/precompiled/IO_rp2.mpy +0 -0
- toolkit/workspace/precompiled/IO_s3matrix.mpy +0 -0
- toolkit/workspace/precompiled/LM_L298N_DCmotor.mpy +0 -0
- toolkit/workspace/precompiled/LM_espnow.py +0 -57
- toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/LM_mqtt_pro.py +0 -211
- toolkit/workspace/precompiled/LM_neomatrix.mpy +0 -0
- toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
- toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
- toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
- toolkit/workspace/precompiled/LM_presence.mpy +0 -0
- toolkit/workspace/precompiled/Logger.cpython-312.pyc +0 -0
- toolkit/workspace/precompiled/Server.cpython-312.pyc +0 -0
- /micrOS/source/{IO_esp32.py → modules/IO_esp32.py} +0 -0
- /micrOS/source/{IO_esp32c3.py → modules/IO_esp32c3.py} +0 -0
- /micrOS/source/{IO_esp32s2.py → modules/IO_esp32s2.py} +0 -0
- /micrOS/source/{IO_esp32s3.py → modules/IO_esp32s3.py} +0 -0
- /micrOS/source/{IO_m5stamp.py → modules/IO_m5stamp.py} +0 -0
- /micrOS/source/{IO_qtpy.py → modules/IO_qtpy.py} +0 -0
- /micrOS/source/{IO_rp2.py → modules/IO_rp2.py} +0 -0
- /micrOS/source/{IO_s3matrix.py → modules/IO_s3matrix.py} +0 -0
- /micrOS/source/{IO_tinypico.py → modules/IO_tinypico.py} +0 -0
- /micrOS/source/{LM_L9110_DCmotor.py → modules/LM_L9110_DCmotor.py} +0 -0
- /micrOS/source/{LM_OV2640.py → modules/LM_OV2640.py} +0 -0
- /micrOS/source/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +0 -0
- /micrOS/source/{LM_aht10.py → modules/LM_aht10.py} +0 -0
- /micrOS/source/{LM_bme280.py → modules/LM_bme280.py} +0 -0
- /micrOS/source/{LM_buzzer.py → modules/LM_buzzer.py} +0 -0
- /micrOS/source/{LM_cct.py → modules/LM_cct.py} +0 -0
- /micrOS/source/{LM_co2.py → modules/LM_co2.py} +0 -0
- /micrOS/source/{LM_dashboard_be.py → modules/LM_dashboard_be.py} +0 -0
- /micrOS/source/{LM_dht11.py → modules/LM_dht11.py} +0 -0
- /micrOS/source/{LM_dht22.py → modules/LM_dht22.py} +0 -0
- /micrOS/source/{LM_dimmer.py → modules/LM_dimmer.py} +0 -0
- /micrOS/source/{LM_distance.py → modules/LM_distance.py} +0 -0
- /micrOS/source/{LM_ds18.py → modules/LM_ds18.py} +0 -0
- /micrOS/source/{LM_gameOfLife.py → modules/LM_gameOfLife.py} +0 -0
- /micrOS/source/{LM_genIO.py → modules/LM_genIO.py} +0 -0
- /micrOS/source/{LM_haptic.py → modules/LM_haptic.py} +0 -0
- /micrOS/source/{LM_i2s_mic.py → modules/LM_i2s_mic.py} +0 -0
- /micrOS/source/{LM_keychain.py → modules/LM_keychain.py} +0 -0
- /micrOS/source/{LM_ld2410.py → modules/LM_ld2410.py} +0 -0
- /micrOS/source/{LM_neopixel.py → modules/LM_neopixel.py} +0 -0
- /micrOS/source/{LM_oled.py → modules/LM_oled.py} +0 -0
- /micrOS/source/{LM_oled_sh1106.py → modules/LM_oled_sh1106.py} +0 -0
- /micrOS/source/{LM_pet_feeder.py → modules/LM_pet_feeder.py} +0 -0
- /micrOS/source/{LM_qmi8658.py → modules/LM_qmi8658.py} +0 -0
- /micrOS/source/{LM_rencoder.py → modules/LM_rencoder.py} +0 -0
- /micrOS/source/{LM_rest.py → modules/LM_rest.py} +0 -0
- /micrOS/source/{LM_rgb.py → modules/LM_rgb.py} +0 -0
- /micrOS/source/{LM_rgbcct.py → modules/LM_rgbcct.py} +0 -0
- /micrOS/source/{LM_roboarm.py → modules/LM_roboarm.py} +0 -0
- /micrOS/source/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
- /micrOS/source/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
- /micrOS/source/{LM_servo.py → modules/LM_servo.py} +0 -0
- /micrOS/source/{LM_sound_event.py → modules/LM_sound_event.py} +0 -0
- /micrOS/source/{LM_stepper.py → modules/LM_stepper.py} +0 -0
- /micrOS/source/{LM_switch.py → modules/LM_switch.py} +0 -0
- /micrOS/source/{LM_system.py → modules/LM_system.py} +0 -0
- /micrOS/source/{LM_telegram.py → modules/LM_telegram.py} +0 -0
- /micrOS/source/{LM_tinyrgb.py → modules/LM_tinyrgb.py} +0 -0
- /micrOS/source/{LM_trackball.py → modules/LM_trackball.py} +0 -0
- /micrOS/source/{LM_veml7700.py → modules/LM_veml7700.py} +0 -0
- {microsdevtoolkit-2.13.1.data → microsdevtoolkit-2.17.0.data}/scripts/devToolKit.py +0 -0
- {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.0.dist-info}/WHEEL +0 -0
- {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.0.dist-info}/licenses/LICENSE +0 -0
- {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.0.dist-info}/top_level.txt +0 -0
- /toolkit/workspace/precompiled/{LM_L9110_DCmotor.py → modules/LM_L9110_DCmotor.py} +0 -0
- /toolkit/workspace/precompiled/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +0 -0
- /toolkit/workspace/precompiled/{LM_dashboard_be.py → modules/LM_dashboard_be.py} +0 -0
- /toolkit/workspace/precompiled/{LM_pet_feeder.py → modules/LM_pet_feeder.py} +0 -0
- /toolkit/workspace/precompiled/{LM_qmi8658.py → modules/LM_qmi8658.py} +0 -0
- /toolkit/workspace/precompiled/{LM_rencoder.py → modules/LM_rencoder.py} +0 -0
- /toolkit/workspace/precompiled/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
- /toolkit/workspace/precompiled/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
# mqtt_as.py Asynchronous version of umqtt.robust
|
|
2
|
+
# (C) Copyright Peter Hinch 2017-2025.
|
|
3
|
+
# Released under the MIT licence.
|
|
4
|
+
|
|
5
|
+
# Pyboard D support added also RP2/default
|
|
6
|
+
# Various improvements contributed by Kevin Köck
|
|
7
|
+
# V5 support added by Bob Veringa.
|
|
8
|
+
# Also other contributors.
|
|
9
|
+
|
|
10
|
+
import gc
|
|
11
|
+
import socket
|
|
12
|
+
import struct
|
|
13
|
+
|
|
14
|
+
gc.collect()
|
|
15
|
+
from binascii import hexlify
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
gc.collect()
|
|
19
|
+
from utime import ticks_ms, ticks_diff, sleep
|
|
20
|
+
from errno import EINPROGRESS, ETIMEDOUT
|
|
21
|
+
|
|
22
|
+
gc.collect()
|
|
23
|
+
from micropython import const
|
|
24
|
+
from machine import unique_id
|
|
25
|
+
import network
|
|
26
|
+
|
|
27
|
+
gc.collect()
|
|
28
|
+
from sys import platform
|
|
29
|
+
|
|
30
|
+
VERSION = (0, 8, 4)
|
|
31
|
+
# Default initial size for input messge buffer. Increase this if large messages
|
|
32
|
+
# are expected, but rarely, to avoid big runtime allocations
|
|
33
|
+
IBUFSIZE = 50
|
|
34
|
+
# By default the callback interface returns and incoming message as bytes.
|
|
35
|
+
# For performance reasons with large messages it may return a memoryview.
|
|
36
|
+
MSG_BYTES = True
|
|
37
|
+
|
|
38
|
+
# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection().
|
|
39
|
+
ESP32 = platform == "esp32"
|
|
40
|
+
RP2 = platform == "rp2"
|
|
41
|
+
if ESP32:
|
|
42
|
+
# https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942
|
|
43
|
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors
|
|
44
|
+
elif RP2:
|
|
45
|
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110]
|
|
46
|
+
else:
|
|
47
|
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT]
|
|
48
|
+
|
|
49
|
+
ESP8266 = platform == "esp8266"
|
|
50
|
+
PYBOARD = platform == "pyboard"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Default "do little" coro for optional user replacement
|
|
54
|
+
async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program
|
|
55
|
+
await asyncio.sleep_ms(0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MsgQueue:
|
|
59
|
+
def __init__(self, size):
|
|
60
|
+
self._q = [0 for _ in range(max(size, 4))]
|
|
61
|
+
self._size = size
|
|
62
|
+
self._wi = 0
|
|
63
|
+
self._ri = 0
|
|
64
|
+
self._evt = asyncio.Event()
|
|
65
|
+
self.discards = 0
|
|
66
|
+
|
|
67
|
+
def put(self, *v):
|
|
68
|
+
self._q[self._wi] = v
|
|
69
|
+
self._evt.set()
|
|
70
|
+
self._wi = (self._wi + 1) % self._size
|
|
71
|
+
if self._wi == self._ri: # Would indicate empty
|
|
72
|
+
self._ri = (self._ri + 1) % self._size # Discard a message
|
|
73
|
+
self.discards += 1
|
|
74
|
+
|
|
75
|
+
def __aiter__(self):
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
async def __anext__(self):
|
|
79
|
+
if self._ri == self._wi: # Empty
|
|
80
|
+
self._evt.clear()
|
|
81
|
+
await self._evt.wait()
|
|
82
|
+
r = self._q[self._ri]
|
|
83
|
+
self._ri = (self._ri + 1) % self._size
|
|
84
|
+
return r
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
config = {
|
|
88
|
+
"client_id": hexlify(unique_id()),
|
|
89
|
+
"server": None,
|
|
90
|
+
"port": 0,
|
|
91
|
+
"user": "",
|
|
92
|
+
"password": "",
|
|
93
|
+
"keepalive": 60,
|
|
94
|
+
"ping_interval": 0,
|
|
95
|
+
"ssl": False,
|
|
96
|
+
"ssl_params": {},
|
|
97
|
+
"response_time": 10,
|
|
98
|
+
"clean_init": True,
|
|
99
|
+
"clean": True,
|
|
100
|
+
"max_repubs": 4,
|
|
101
|
+
"will": None,
|
|
102
|
+
"subs_cb": lambda *_: None,
|
|
103
|
+
"wifi_coro": eliza,
|
|
104
|
+
"connect_coro": eliza,
|
|
105
|
+
"ssid": None,
|
|
106
|
+
"wifi_pw": None,
|
|
107
|
+
"queue_len": 0,
|
|
108
|
+
"gateway": False,
|
|
109
|
+
"mqttv5": False,
|
|
110
|
+
"mqttv5_con_props": None,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MQTTException(Exception):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def pid_gen():
|
|
119
|
+
pid = 0
|
|
120
|
+
while True:
|
|
121
|
+
pid = pid + 1 if pid < 65535 else 1
|
|
122
|
+
yield pid
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def qos_check(qos):
|
|
126
|
+
if not (qos == 0 or qos == 1):
|
|
127
|
+
raise ValueError("Only qos 0 and 1 are supported.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Populate a byte array with a variable byte integer. Args: buf the bytearray,
|
|
131
|
+
# offs: start offset. x the value. Returns the end offset.
|
|
132
|
+
# 1-4 bytes allowed, encoding up to 268,435,455 (V3.1.1 table 2.4). No point trapping this.
|
|
133
|
+
def vbi(buf: bytearray, offs: int, x: int):
|
|
134
|
+
buf[offs] = x & 0x7F
|
|
135
|
+
if x := x >> 7:
|
|
136
|
+
buf[offs] |= 0x80
|
|
137
|
+
return vbi(buf, offs + 1, x) if x else (offs + 1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
encode_properties = None
|
|
141
|
+
decode_properties = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MQTT_base:
|
|
145
|
+
REPUB_COUNT = 0 # TEST
|
|
146
|
+
DEBUG = False
|
|
147
|
+
|
|
148
|
+
def __init__(self, config):
|
|
149
|
+
self._events = config["queue_len"] > 0
|
|
150
|
+
# MQTT config
|
|
151
|
+
self._client_id = config["client_id"]
|
|
152
|
+
self._user = config["user"]
|
|
153
|
+
self._pswd = config["password"]
|
|
154
|
+
self._keepalive = config["keepalive"]
|
|
155
|
+
if self._keepalive >= 65536:
|
|
156
|
+
raise ValueError("invalid keepalive time")
|
|
157
|
+
self._response_time = config["response_time"] * 1000 # Repub if no PUBACK received (ms).
|
|
158
|
+
self._max_repubs = config["max_repubs"]
|
|
159
|
+
self._clean_init = config["clean_init"] # clean_session state on first connection
|
|
160
|
+
self._clean = config["clean"] # clean_session state on reconnect
|
|
161
|
+
will = config["will"]
|
|
162
|
+
if will is None:
|
|
163
|
+
self._lw_topic = False
|
|
164
|
+
else:
|
|
165
|
+
self._set_last_will(*will)
|
|
166
|
+
# WiFi config
|
|
167
|
+
self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266
|
|
168
|
+
self._wifi_pw = config["wifi_pw"]
|
|
169
|
+
self._ssl = config["ssl"]
|
|
170
|
+
self._ssl_params = config["ssl_params"]
|
|
171
|
+
# Callbacks and coros
|
|
172
|
+
if self._events:
|
|
173
|
+
self.up = asyncio.Event()
|
|
174
|
+
self.down = asyncio.Event()
|
|
175
|
+
self.queue = MsgQueue(config["queue_len"])
|
|
176
|
+
self._cb = self.queue.put
|
|
177
|
+
else: # Callbacks
|
|
178
|
+
self._cb = config["subs_cb"]
|
|
179
|
+
self._wifi_handler = config["wifi_coro"]
|
|
180
|
+
self._connect_handler = config["connect_coro"]
|
|
181
|
+
# Network
|
|
182
|
+
self.port = config["port"]
|
|
183
|
+
if self.port == 0:
|
|
184
|
+
self.port = 8883 if self._ssl else 1883
|
|
185
|
+
self.server = config["server"]
|
|
186
|
+
if self.server is None:
|
|
187
|
+
raise ValueError("no server specified.")
|
|
188
|
+
self._sock = None
|
|
189
|
+
self._sta_if = network.WLAN(network.STA_IF)
|
|
190
|
+
self._sta_if.active(True)
|
|
191
|
+
if config["gateway"]: # Called from gateway (hence ESP32).
|
|
192
|
+
import aioespnow # Set up ESPNOW
|
|
193
|
+
|
|
194
|
+
while not (sta := self._sta_if).active():
|
|
195
|
+
sleep(0.1)
|
|
196
|
+
sta.config(pm=sta.PM_NONE) # No power management
|
|
197
|
+
sta.active(True)
|
|
198
|
+
self._espnow = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
|
|
199
|
+
self._espnow.active(True)
|
|
200
|
+
|
|
201
|
+
self.newpid = pid_gen()
|
|
202
|
+
self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response
|
|
203
|
+
self.last_rx = ticks_ms() # Time of last communication from broker
|
|
204
|
+
self.lock = asyncio.Lock()
|
|
205
|
+
self._ibuf = bytearray(IBUFSIZE)
|
|
206
|
+
self._mvbuf = memoryview(self._ibuf)
|
|
207
|
+
|
|
208
|
+
self.mqttv5 = config.get("mqttv5")
|
|
209
|
+
self.mqttv5_con_props = config.get("mqttv5_con_props")
|
|
210
|
+
self.topic_alias_maximum = 0
|
|
211
|
+
|
|
212
|
+
if self.mqttv5:
|
|
213
|
+
global encode_properties, decode_properties
|
|
214
|
+
from .mqtt_v5_properties import encode_properties, decode_properties # noqa
|
|
215
|
+
|
|
216
|
+
def _set_last_will(self, topic, msg, retain=False, qos=0):
|
|
217
|
+
qos_check(qos)
|
|
218
|
+
if not topic:
|
|
219
|
+
raise ValueError("Empty topic.")
|
|
220
|
+
self._lw_topic = topic
|
|
221
|
+
self._lw_msg = msg
|
|
222
|
+
self._lw_qos = qos
|
|
223
|
+
self._lw_retain = retain
|
|
224
|
+
|
|
225
|
+
def dprint(self, msg, *args):
|
|
226
|
+
if self.DEBUG:
|
|
227
|
+
print(msg % args)
|
|
228
|
+
|
|
229
|
+
def _timeout(self, t):
|
|
230
|
+
return ticks_diff(ticks_ms(), t) > self._response_time
|
|
231
|
+
|
|
232
|
+
async def _as_read(self, n, sock=None): # OSError caught by superclass
|
|
233
|
+
if sock is None:
|
|
234
|
+
sock = self._sock
|
|
235
|
+
# Ensure input buffer is big enough to hold data. It keeps the new size
|
|
236
|
+
oflow = n - len(self._ibuf)
|
|
237
|
+
if oflow > 0: # Grow the buffer and re-create the memoryview
|
|
238
|
+
# Avoid too frequent small allocations by adding some extra bytes
|
|
239
|
+
self._ibuf.extend(bytearray(oflow + 50))
|
|
240
|
+
self._mvbuf = memoryview(self._ibuf)
|
|
241
|
+
buffer = self._mvbuf
|
|
242
|
+
size = 0
|
|
243
|
+
t = ticks_ms()
|
|
244
|
+
while size < n:
|
|
245
|
+
if self._timeout(t) or not self.isconnected():
|
|
246
|
+
raise OSError(-1, "Timeout on socket read")
|
|
247
|
+
try:
|
|
248
|
+
msg_size = sock.readinto(buffer[size:], n - size)
|
|
249
|
+
except OSError as e: # ESP32 issues weird 119 errors here
|
|
250
|
+
msg_size = None
|
|
251
|
+
if e.args[0] not in BUSY_ERRORS:
|
|
252
|
+
raise
|
|
253
|
+
if msg_size == 0: # Connection closed by host
|
|
254
|
+
raise OSError(-1, "Connection closed by host")
|
|
255
|
+
if msg_size is not None: # data received
|
|
256
|
+
size += msg_size
|
|
257
|
+
t = ticks_ms()
|
|
258
|
+
self.last_rx = ticks_ms()
|
|
259
|
+
await asyncio.sleep_ms(0)
|
|
260
|
+
return buffer[:n]
|
|
261
|
+
|
|
262
|
+
async def _as_write(self, bytes_wr, length=0, sock=None):
|
|
263
|
+
if sock is None:
|
|
264
|
+
sock = self._sock
|
|
265
|
+
|
|
266
|
+
# Wrap bytes in memoryview to avoid copying during slicing
|
|
267
|
+
bytes_wr = memoryview(bytes_wr)
|
|
268
|
+
if length:
|
|
269
|
+
bytes_wr = bytes_wr[:length]
|
|
270
|
+
t = ticks_ms()
|
|
271
|
+
while bytes_wr:
|
|
272
|
+
if self._timeout(t) or not self.isconnected():
|
|
273
|
+
raise OSError(-1, "Timeout on socket write")
|
|
274
|
+
try:
|
|
275
|
+
n = sock.write(bytes_wr)
|
|
276
|
+
except OSError as e: # ESP32 issues weird 119 errors here
|
|
277
|
+
n = 0
|
|
278
|
+
if e.args[0] not in BUSY_ERRORS:
|
|
279
|
+
raise
|
|
280
|
+
if n:
|
|
281
|
+
t = ticks_ms()
|
|
282
|
+
bytes_wr = bytes_wr[n:]
|
|
283
|
+
await asyncio.sleep_ms(0)
|
|
284
|
+
|
|
285
|
+
async def _send_str(self, s):
|
|
286
|
+
await self._as_write(struct.pack("!H", len(s)))
|
|
287
|
+
await self._as_write(s)
|
|
288
|
+
|
|
289
|
+
# Receive a Variable Byte Integer and decode.
|
|
290
|
+
async def _recv_len(self, d=0, i=0):
|
|
291
|
+
s = (await self._as_read(1))[0]
|
|
292
|
+
d |= (s & 0x7F) << (i * 7)
|
|
293
|
+
return await self._recv_len(d, i + 1) if (s & 0x80) else (d, i + 1)
|
|
294
|
+
|
|
295
|
+
async def _connect(self, clean):
|
|
296
|
+
mqttv5 = self.mqttv5 # Cache local
|
|
297
|
+
self._sock = socket.socket()
|
|
298
|
+
self._sock.setblocking(False)
|
|
299
|
+
try:
|
|
300
|
+
self._sock.connect(self._addr)
|
|
301
|
+
except OSError as e:
|
|
302
|
+
if e.args[0] not in BUSY_ERRORS:
|
|
303
|
+
raise
|
|
304
|
+
await asyncio.sleep_ms(0)
|
|
305
|
+
self.dprint("Connecting to broker.")
|
|
306
|
+
if self._ssl:
|
|
307
|
+
try:
|
|
308
|
+
import ssl
|
|
309
|
+
except ImportError:
|
|
310
|
+
import ussl as ssl
|
|
311
|
+
|
|
312
|
+
self._sock = ssl.wrap_socket(self._sock, **self._ssl_params)
|
|
313
|
+
premsg = bytearray(b"\x10\0\0\0\0\0")
|
|
314
|
+
msg = bytearray(b"\x04MQTT\x00\0\0\0")
|
|
315
|
+
msg[5] = 0x05 if mqttv5 else 0x04
|
|
316
|
+
|
|
317
|
+
sz = 10 + 2 + len(self._client_id)
|
|
318
|
+
msg[6] = clean << 1
|
|
319
|
+
if self._user:
|
|
320
|
+
sz += 2 + len(self._user) + 2 + len(self._pswd)
|
|
321
|
+
msg[6] |= 0xC0
|
|
322
|
+
if self._keepalive:
|
|
323
|
+
msg[7] |= self._keepalive >> 8
|
|
324
|
+
msg[8] |= self._keepalive & 0x00FF
|
|
325
|
+
if self._lw_topic:
|
|
326
|
+
sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg)
|
|
327
|
+
if mqttv5:
|
|
328
|
+
# Extra for the will properties
|
|
329
|
+
sz += 1
|
|
330
|
+
msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3
|
|
331
|
+
msg[6] |= self._lw_retain << 5
|
|
332
|
+
|
|
333
|
+
if mqttv5:
|
|
334
|
+
properties = encode_properties(self.mqttv5_con_props)
|
|
335
|
+
sz += len(properties)
|
|
336
|
+
|
|
337
|
+
i = vbi(premsg, 1, sz) # sz -> Variable Byte Integer
|
|
338
|
+
await self._as_write(premsg, i + 1)
|
|
339
|
+
await self._as_write(msg)
|
|
340
|
+
if mqttv5:
|
|
341
|
+
await self._as_write(properties)
|
|
342
|
+
|
|
343
|
+
await self._send_str(self._client_id)
|
|
344
|
+
if self._lw_topic:
|
|
345
|
+
if mqttv5:
|
|
346
|
+
# We don't support will properties, so we send 0x00 for properties length
|
|
347
|
+
await self._as_write(b"\x00")
|
|
348
|
+
await self._send_str(self._lw_topic)
|
|
349
|
+
await self._send_str(self._lw_msg)
|
|
350
|
+
if self._user:
|
|
351
|
+
await self._send_str(self._user)
|
|
352
|
+
await self._send_str(self._pswd)
|
|
353
|
+
# Await CONNACK
|
|
354
|
+
# read causes ECONNABORTED if broker is out; triggers a reconnect.
|
|
355
|
+
del premsg, msg
|
|
356
|
+
packet_type = await self._as_read(1)
|
|
357
|
+
if packet_type[0] != 0x20:
|
|
358
|
+
raise OSError(-1, "CONNACK not received")
|
|
359
|
+
# The connect packet has changed, so size might be different now. But
|
|
360
|
+
# we can still handle it the same for 3.1.1 and v5
|
|
361
|
+
sz, _ = await self._recv_len()
|
|
362
|
+
if not mqttv5 and sz != 2:
|
|
363
|
+
raise OSError(-1, "Invalid CONNACK packet")
|
|
364
|
+
|
|
365
|
+
# Only read the first 2 bytes, as properties have their own length
|
|
366
|
+
connack_resp = await self._as_read(2)
|
|
367
|
+
|
|
368
|
+
# Connect ack flags
|
|
369
|
+
if connack_resp[0] != 0:
|
|
370
|
+
raise OSError(-1, "CONNACK flags not 0")
|
|
371
|
+
# Reason code
|
|
372
|
+
if connack_resp[1] != 0:
|
|
373
|
+
# On MQTTv5 Reason codes below 128 may need to be handled
|
|
374
|
+
# differently. For now, we just raise an error. Spec is a bit weird
|
|
375
|
+
# on this.
|
|
376
|
+
raise OSError(-1, "CONNACK reason code 0x%x" % connack_resp[1])
|
|
377
|
+
|
|
378
|
+
del connack_resp
|
|
379
|
+
if not mqttv5:
|
|
380
|
+
# If we are not on MQTTv5 we can stop here
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
connack_props_length, _ = await self._recv_len()
|
|
384
|
+
if connack_props_length > 0:
|
|
385
|
+
connack_props = await self._as_read(connack_props_length)
|
|
386
|
+
decoded_props = decode_properties(connack_props, connack_props_length)
|
|
387
|
+
self.dprint("CONNACK properties: %s", decoded_props)
|
|
388
|
+
self.topic_alias_maximum = decoded_props.get(0x22, 0)
|
|
389
|
+
|
|
390
|
+
async def _ping(self):
|
|
391
|
+
async with self.lock:
|
|
392
|
+
await self._as_write(b"\xc0\0")
|
|
393
|
+
|
|
394
|
+
# Check internet connectivity by sending DNS lookup to Google's 8.8.8.8
|
|
395
|
+
async def wan_ok(
|
|
396
|
+
self,
|
|
397
|
+
packet=b"$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01",
|
|
398
|
+
):
|
|
399
|
+
if not self.isconnected(): # WiFi is down
|
|
400
|
+
return False
|
|
401
|
+
length = 32 # DNS query and response packet size
|
|
402
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
403
|
+
s.setblocking(False)
|
|
404
|
+
s.connect(("8.8.8.8", 53))
|
|
405
|
+
await asyncio.sleep(1)
|
|
406
|
+
async with self.lock:
|
|
407
|
+
try:
|
|
408
|
+
await self._as_write(packet, sock=s)
|
|
409
|
+
await asyncio.sleep(2)
|
|
410
|
+
res = await self._as_read(length, s)
|
|
411
|
+
if len(res) == length:
|
|
412
|
+
return True # DNS response size OK
|
|
413
|
+
except OSError: # Timeout on read: no connectivity.
|
|
414
|
+
return False
|
|
415
|
+
finally:
|
|
416
|
+
s.close()
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
async def broker_up(self): # Test broker connectivity
|
|
420
|
+
if not self.isconnected():
|
|
421
|
+
return False
|
|
422
|
+
tlast = self.last_rx
|
|
423
|
+
if ticks_diff(ticks_ms(), tlast) < 1000:
|
|
424
|
+
return True
|
|
425
|
+
try:
|
|
426
|
+
await self._ping()
|
|
427
|
+
except OSError:
|
|
428
|
+
return False
|
|
429
|
+
t = ticks_ms()
|
|
430
|
+
while not self._timeout(t):
|
|
431
|
+
await asyncio.sleep_ms(100)
|
|
432
|
+
if ticks_diff(self.last_rx, tlast) > 0: # Response received
|
|
433
|
+
return True
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
async def disconnect(self):
|
|
437
|
+
if self._sock is not None:
|
|
438
|
+
await self._kill_tasks(False) # Keep socket open
|
|
439
|
+
try:
|
|
440
|
+
async with self.lock:
|
|
441
|
+
self._sock.write(b"\xe0\0") # Close broker connection
|
|
442
|
+
await asyncio.sleep_ms(100)
|
|
443
|
+
except OSError:
|
|
444
|
+
pass
|
|
445
|
+
self._close()
|
|
446
|
+
self._has_connected = False
|
|
447
|
+
|
|
448
|
+
def _close(self):
|
|
449
|
+
if self._sock is not None:
|
|
450
|
+
self._sock.close()
|
|
451
|
+
|
|
452
|
+
def close(self): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60
|
|
453
|
+
self._close()
|
|
454
|
+
try:
|
|
455
|
+
self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors
|
|
456
|
+
except OSError:
|
|
457
|
+
self.dprint("Wi-Fi not started, unable to disconnect interface")
|
|
458
|
+
self._sta_if.active(False)
|
|
459
|
+
|
|
460
|
+
async def _await_pid(self, pid):
|
|
461
|
+
t = ticks_ms()
|
|
462
|
+
while pid in self.rcv_pids: # local copy
|
|
463
|
+
if self._timeout(t) or not self.isconnected():
|
|
464
|
+
break # Must repub or bail out
|
|
465
|
+
await asyncio.sleep_ms(100)
|
|
466
|
+
else:
|
|
467
|
+
return True # PID received. All done.
|
|
468
|
+
return False
|
|
469
|
+
|
|
470
|
+
# qos == 1: coro blocks until wait_msg gets correct PID.
|
|
471
|
+
# If WiFi fails completely subclass re-publishes with new PID.
|
|
472
|
+
async def publish(self, topic, msg, retain, qos, properties=None):
|
|
473
|
+
pid = next(self.newpid)
|
|
474
|
+
if qos:
|
|
475
|
+
self.rcv_pids.add(pid)
|
|
476
|
+
async with self.lock:
|
|
477
|
+
await self._publish(topic, msg, retain, qos, 0, pid, properties)
|
|
478
|
+
if qos == 0:
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
count = 0
|
|
482
|
+
while 1: # Await PUBACK, republish on timeout
|
|
483
|
+
if await self._await_pid(pid):
|
|
484
|
+
return
|
|
485
|
+
# No match
|
|
486
|
+
if count >= self._max_repubs or not self.isconnected():
|
|
487
|
+
raise OSError(-1) # Subclass to re-publish with new PID
|
|
488
|
+
async with self.lock:
|
|
489
|
+
# Add pid
|
|
490
|
+
await self._publish(topic, msg, retain, qos, dup=1, pid=pid, properties=properties)
|
|
491
|
+
count += 1
|
|
492
|
+
self.REPUB_COUNT += 1
|
|
493
|
+
|
|
494
|
+
async def _publish(self, topic, msg, retain, qos, dup, pid, properties=None):
|
|
495
|
+
pkt = bytearray(b"\x30\0\0\0")
|
|
496
|
+
pkt[0] |= qos << 1 | retain | dup << 3
|
|
497
|
+
sz = 2 + len(topic) + len(msg)
|
|
498
|
+
if qos > 0:
|
|
499
|
+
sz += 2
|
|
500
|
+
|
|
501
|
+
if self.mqttv5:
|
|
502
|
+
properties = encode_properties(properties)
|
|
503
|
+
sz += len(properties)
|
|
504
|
+
|
|
505
|
+
await self._as_write(pkt, vbi(pkt, 1, sz)) # Encode size as VBI
|
|
506
|
+
await self._send_str(topic)
|
|
507
|
+
if qos > 0:
|
|
508
|
+
struct.pack_into("!H", pkt, 0, pid)
|
|
509
|
+
await self._as_write(pkt, 2)
|
|
510
|
+
if self.mqttv5:
|
|
511
|
+
await self._as_write(properties)
|
|
512
|
+
await self._as_write(msg)
|
|
513
|
+
|
|
514
|
+
async def subscribe(self, topic, qos, properties=None):
|
|
515
|
+
await self._usub(topic, qos, properties)
|
|
516
|
+
|
|
517
|
+
async def unsubscribe(self, topic, properties=None):
|
|
518
|
+
await self._usub(topic, None, properties)
|
|
519
|
+
|
|
520
|
+
# Subscribe/unsubscribe
|
|
521
|
+
# Can raise OSError if WiFi fails. Subclass traps.
|
|
522
|
+
async def _usub(self, topic, qos, properties):
|
|
523
|
+
sub = qos is not None
|
|
524
|
+
pkt = bytearray(7)
|
|
525
|
+
pkt[0] = 0x82 if sub else 0xA2
|
|
526
|
+
pid = next(self.newpid)
|
|
527
|
+
self.rcv_pids.add(pid)
|
|
528
|
+
# 2 bytes of PID + 2 bytes of topic length + len(topic)
|
|
529
|
+
sz = 2 + 2 + len(topic) + (1 if sub else 0)
|
|
530
|
+
if self.mqttv5:
|
|
531
|
+
# Return length as VBI followed by properties or b'\0'
|
|
532
|
+
properties = encode_properties(properties)
|
|
533
|
+
sz += len(properties)
|
|
534
|
+
offs = vbi(pkt, 1, sz) # Store size as variable byte integer
|
|
535
|
+
struct.pack_into("!H", pkt, offs, pid)
|
|
536
|
+
|
|
537
|
+
async with self.lock:
|
|
538
|
+
await self._as_write(pkt, offs + 2)
|
|
539
|
+
if self.mqttv5:
|
|
540
|
+
await self._as_write(properties)
|
|
541
|
+
await self._send_str(topic)
|
|
542
|
+
if sub:
|
|
543
|
+
# Only QoS is supported other features such as:
|
|
544
|
+
# (NL) No Local, (RAP) Retain As Published and Retain Handling.
|
|
545
|
+
# Are not supported.
|
|
546
|
+
await self._as_write(qos.to_bytes(1, "little"))
|
|
547
|
+
|
|
548
|
+
if not await self._await_pid(pid):
|
|
549
|
+
raise OSError(-1)
|
|
550
|
+
|
|
551
|
+
# Remove a pending pid after a successful receive.
|
|
552
|
+
def kill_pid(self, pid, msg):
|
|
553
|
+
if pid in self.rcv_pids:
|
|
554
|
+
self.rcv_pids.discard(pid)
|
|
555
|
+
else:
|
|
556
|
+
raise OSError(-1, f"Invalid pid in {msg} packet")
|
|
557
|
+
|
|
558
|
+
# Wait for a single incoming MQTT message and process it.
|
|
559
|
+
# Subscribed messages are delivered to a callback previously
|
|
560
|
+
# set by .setup() method. Other (internal) MQTT
|
|
561
|
+
# messages processed internally.
|
|
562
|
+
# Immediate return if no data available. Called from ._handle_msg().
|
|
563
|
+
async def wait_msg(self):
|
|
564
|
+
mqttv5 = self.mqttv5 # Cache local
|
|
565
|
+
try:
|
|
566
|
+
res = self._sock.read(1) # Throws OSError on WiFi fail
|
|
567
|
+
except OSError as e:
|
|
568
|
+
if e.args[0] in BUSY_ERRORS: # Needed by RP2
|
|
569
|
+
await asyncio.sleep_ms(0)
|
|
570
|
+
return
|
|
571
|
+
raise
|
|
572
|
+
|
|
573
|
+
if res is None:
|
|
574
|
+
return
|
|
575
|
+
if res == b"":
|
|
576
|
+
raise OSError(-1, "Empty response") # Can happen on broker fail
|
|
577
|
+
|
|
578
|
+
if res == b"\xd0": # PINGRESP
|
|
579
|
+
await self._as_read(1) # Update .last_rx time
|
|
580
|
+
return
|
|
581
|
+
op = res[0]
|
|
582
|
+
|
|
583
|
+
if op == 0x40: # PUBACK
|
|
584
|
+
sz, _ = await self._recv_len()
|
|
585
|
+
if not mqttv5 and sz != 2:
|
|
586
|
+
raise OSError(-1, "Invalid PUBACK packet")
|
|
587
|
+
rcv_pid = await self._as_read(2)
|
|
588
|
+
pid = rcv_pid[0] << 8 | rcv_pid[1]
|
|
589
|
+
# For some reason even on MQTTv5 reason code is optional
|
|
590
|
+
if sz != 2:
|
|
591
|
+
reason_code = await self._as_read(1)
|
|
592
|
+
reason_code = reason_code[0]
|
|
593
|
+
if reason_code >= 0x80:
|
|
594
|
+
raise OSError(-1, "PUBACK reason code 0x%x" % reason_code)
|
|
595
|
+
if sz > 3:
|
|
596
|
+
puback_props_sz, _ = await self._recv_len()
|
|
597
|
+
if puback_props_sz > 0:
|
|
598
|
+
puback_props = await self._as_read(puback_props_sz)
|
|
599
|
+
decoded_props = decode_properties(puback_props, puback_props_sz)
|
|
600
|
+
self.dprint("PUBACK properties %s", decoded_props)
|
|
601
|
+
# No exception thrown: PUBACK successfuly received. Remove pending PID
|
|
602
|
+
self.kill_pid(pid, "PUBACK")
|
|
603
|
+
|
|
604
|
+
if op == 0x90 or op == 0xB0: # [UN]SUBACK
|
|
605
|
+
un = "UN" if op == 0xB0 else ""
|
|
606
|
+
suback = op == 0x90
|
|
607
|
+
sz, _ = await self._recv_len()
|
|
608
|
+
rcv_pid = await self._as_read(2)
|
|
609
|
+
pid = rcv_pid[0] << 8 | rcv_pid[1]
|
|
610
|
+
sz -= 2
|
|
611
|
+
# Handle properties
|
|
612
|
+
if mqttv5:
|
|
613
|
+
suback_props_sz, sz_len = await self._recv_len()
|
|
614
|
+
sz -= sz_len
|
|
615
|
+
sz -= suback_props_sz
|
|
616
|
+
if suback_props_sz > 0:
|
|
617
|
+
suback_props = await self._as_read(suback_props_sz)
|
|
618
|
+
decoded_props = decode_properties(suback_props, suback_props_sz)
|
|
619
|
+
self.dprint("[UN] SUBACK properties %s", decoded_props)
|
|
620
|
+
|
|
621
|
+
if sz > 1:
|
|
622
|
+
raise OSError(-1, "Got too many bytes")
|
|
623
|
+
if suback or mqttv5:
|
|
624
|
+
reason_code = await self._as_read(sz)
|
|
625
|
+
reason_code = reason_code[0]
|
|
626
|
+
if reason_code >= 0x80:
|
|
627
|
+
raise OSError(-1, f"{un}SUBACK reason code 0x{reason_code:x}")
|
|
628
|
+
self.kill_pid(pid, f"{un}SUBACK")
|
|
629
|
+
|
|
630
|
+
if op == 0xE0: # DISCONNECT
|
|
631
|
+
if mqttv5:
|
|
632
|
+
sz, _ = await self._recv_len()
|
|
633
|
+
reason_code = await self._as_read(1)
|
|
634
|
+
reason_code = reason_code[0]
|
|
635
|
+
|
|
636
|
+
sz -= 1
|
|
637
|
+
if sz > 0:
|
|
638
|
+
dis_props_sz, dis_len = await self._recv_len()
|
|
639
|
+
sz -= dis_len
|
|
640
|
+
disconnect_props = await self._as_read(dis_props_sz)
|
|
641
|
+
decoded_props = decode_properties(disconnect_props, dis_props_sz)
|
|
642
|
+
self.dprint("DISCONNECT properties %s", decoded_props)
|
|
643
|
+
|
|
644
|
+
if reason_code >= 0x80:
|
|
645
|
+
raise OSError(-1, "DISCONNECT reason code 0x%x" % reason_code)
|
|
646
|
+
|
|
647
|
+
if op & 0xF0 != 0x30:
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
sz, _ = await self._recv_len()
|
|
651
|
+
topic_len = await self._as_read(2)
|
|
652
|
+
topic_len = (topic_len[0] << 8) | topic_len[1]
|
|
653
|
+
topic = await self._as_read(topic_len)
|
|
654
|
+
topic = bytes(topic) # Copy before re-using the read buffer
|
|
655
|
+
sz -= topic_len + 2
|
|
656
|
+
# MQTT V3.1.1 section 2.3.1 non-normative comment. Get server PID.
|
|
657
|
+
if op & 6: # This is distinct from client PIDs.
|
|
658
|
+
pid = await self._as_read(2)
|
|
659
|
+
pid = pid[0] << 8 | pid[1]
|
|
660
|
+
sz -= 2
|
|
661
|
+
|
|
662
|
+
decoded_props = None
|
|
663
|
+
if mqttv5:
|
|
664
|
+
pub_props_sz, pub_props_sz_len = await self._recv_len()
|
|
665
|
+
sz -= pub_props_sz_len
|
|
666
|
+
sz -= pub_props_sz
|
|
667
|
+
if pub_props_sz > 0:
|
|
668
|
+
pub_props = await self._as_read(pub_props_sz)
|
|
669
|
+
decoded_props = decode_properties(pub_props, pub_props_sz)
|
|
670
|
+
|
|
671
|
+
msg = await self._as_read(sz)
|
|
672
|
+
# In event mode we must copy the message otherwise .queue contents will be wrong:
|
|
673
|
+
# every entry would contain the same message.
|
|
674
|
+
# In callback mode not copying the message is OK so long as the callback is purely
|
|
675
|
+
# synchronous. Overruns can't occur because of the lock.
|
|
676
|
+
if self._events or MSG_BYTES:
|
|
677
|
+
msg = bytes(msg)
|
|
678
|
+
retained = op & 0x01
|
|
679
|
+
args = [topic, msg, bool(retained)]
|
|
680
|
+
if mqttv5:
|
|
681
|
+
args.append(decoded_props)
|
|
682
|
+
self._cb(*args)
|
|
683
|
+
|
|
684
|
+
if op & 6 == 2: # qos 1
|
|
685
|
+
pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK
|
|
686
|
+
struct.pack_into("!H", pkt, 2, pid)
|
|
687
|
+
await self._as_write(pkt)
|
|
688
|
+
elif op & 6 == 4: # qos 2 not supported
|
|
689
|
+
raise OSError(-1, "QoS 2 not supported")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# MQTTClient class. Handles issues relating to connectivity.
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class MQTTClient(MQTT_base):
|
|
696
|
+
def __init__(self, config):
|
|
697
|
+
super().__init__(config)
|
|
698
|
+
self._isconnected = False # Current connection state
|
|
699
|
+
keepalive = 1000 * self._keepalive # ms
|
|
700
|
+
self._ping_interval = keepalive // 4 if keepalive else 20000
|
|
701
|
+
p_i = config["ping_interval"] * 1000 # Can specify shorter e.g. for subscribe-only
|
|
702
|
+
if p_i and p_i < self._ping_interval:
|
|
703
|
+
self._ping_interval = p_i
|
|
704
|
+
self._in_connect = False
|
|
705
|
+
self._has_connected = False # Define 'Clean Session' value to use.
|
|
706
|
+
self._tasks = []
|
|
707
|
+
if ESP8266:
|
|
708
|
+
import esp
|
|
709
|
+
|
|
710
|
+
esp.sleep_type(0) # Improve connection integrity at cost of power consumption.
|
|
711
|
+
|
|
712
|
+
async def wifi_connect(self, quick=False):
|
|
713
|
+
s = self._sta_if
|
|
714
|
+
if ESP8266:
|
|
715
|
+
if s.isconnected(): # 1st attempt, already connected.
|
|
716
|
+
return
|
|
717
|
+
s.active(True)
|
|
718
|
+
s.connect() # ESP8266 remembers connection.
|
|
719
|
+
for _ in range(60):
|
|
720
|
+
# Break out on fail or success. Check once per sec.
|
|
721
|
+
if s.status() != network.STAT_CONNECTING:
|
|
722
|
+
break
|
|
723
|
+
await asyncio.sleep(1)
|
|
724
|
+
# might hang forever awaiting dhcp lease renewal or something else
|
|
725
|
+
if s.status() == network.STAT_CONNECTING:
|
|
726
|
+
s.disconnect()
|
|
727
|
+
await asyncio.sleep(1)
|
|
728
|
+
if not s.isconnected() and self._ssid is not None and self._wifi_pw is not None:
|
|
729
|
+
s.connect(self._ssid, self._wifi_pw)
|
|
730
|
+
# Break out on fail or success. Check once per sec.
|
|
731
|
+
while s.status() == network.STAT_CONNECTING:
|
|
732
|
+
await asyncio.sleep(1)
|
|
733
|
+
else:
|
|
734
|
+
s.active(True)
|
|
735
|
+
if RP2: # Disable auto-sleep.
|
|
736
|
+
# https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
|
|
737
|
+
# para 3.6.3
|
|
738
|
+
s.config(pm=0xA11140)
|
|
739
|
+
s.connect(self._ssid, self._wifi_pw)
|
|
740
|
+
for _ in range(60): # Break out on fail or success. Check once per sec.
|
|
741
|
+
await asyncio.sleep(1)
|
|
742
|
+
# Loop while connecting or no IP
|
|
743
|
+
if s.isconnected():
|
|
744
|
+
break
|
|
745
|
+
if ESP32:
|
|
746
|
+
# Status values >= STAT_IDLE can occur during connect:
|
|
747
|
+
# STAT_IDLE 1000, STAT_CONNECTING 1001, STAT_GOT_IP 1010
|
|
748
|
+
# Error statuses are in range 200..204
|
|
749
|
+
if s.status() < network.STAT_IDLE:
|
|
750
|
+
# pause as workaround to avoid persistent reconnect failures
|
|
751
|
+
# see https://github.com/peterhinch/micropython-mqtt/issues/132 for details
|
|
752
|
+
await asyncio.sleep(1)
|
|
753
|
+
break
|
|
754
|
+
elif PYBOARD: # No symbolic constants in network
|
|
755
|
+
if not 1 <= s.status() <= 2:
|
|
756
|
+
break
|
|
757
|
+
elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?)
|
|
758
|
+
if not 1 <= s.status() <= 2:
|
|
759
|
+
break
|
|
760
|
+
else: # Timeout: still in connecting state
|
|
761
|
+
s.disconnect()
|
|
762
|
+
await asyncio.sleep(1)
|
|
763
|
+
|
|
764
|
+
if not s.isconnected(): # Timed out
|
|
765
|
+
raise OSError("Wi-Fi connect timed out")
|
|
766
|
+
if not quick: # Skip on first connection only if power saving
|
|
767
|
+
# Ensure connection stays up for a few secs.
|
|
768
|
+
self.dprint("Checking WiFi integrity.")
|
|
769
|
+
for _ in range(5):
|
|
770
|
+
if not s.isconnected():
|
|
771
|
+
raise OSError("Connection Unstable") # in 1st 5 secs
|
|
772
|
+
await asyncio.sleep(1)
|
|
773
|
+
self.dprint("Got reliable connection")
|
|
774
|
+
|
|
775
|
+
async def connect(self, *, quick=False): # Quick initial connect option for battery apps
|
|
776
|
+
if not self._has_connected:
|
|
777
|
+
await self.wifi_connect(quick) # On 1st call, caller handles error
|
|
778
|
+
# Note this blocks if DNS lookup occurs. Do it once to prevent
|
|
779
|
+
# blocking during later internet outage:
|
|
780
|
+
self._addr = socket.getaddrinfo(self.server, self.port)[0][-1]
|
|
781
|
+
self._in_connect = True # Disable low level ._isconnected check
|
|
782
|
+
try:
|
|
783
|
+
is_clean = self._clean
|
|
784
|
+
if not self._has_connected and self._clean_init and not self._clean:
|
|
785
|
+
if self.mqttv5:
|
|
786
|
+
is_clean = True
|
|
787
|
+
else:
|
|
788
|
+
# Power up. Clear previous session data but subsequently save it.
|
|
789
|
+
# Issue #40
|
|
790
|
+
await self._connect(True) # Connect with clean session
|
|
791
|
+
try:
|
|
792
|
+
async with self.lock:
|
|
793
|
+
self._sock.write(b"\xe0\0") # Force disconnect but keep socket open
|
|
794
|
+
except OSError:
|
|
795
|
+
pass
|
|
796
|
+
self.dprint("Waiting for disconnect")
|
|
797
|
+
await asyncio.sleep(2) # Wait for broker to disconnect
|
|
798
|
+
self.dprint("About to reconnect with unclean session.")
|
|
799
|
+
await self._connect(is_clean)
|
|
800
|
+
except Exception:
|
|
801
|
+
self._close()
|
|
802
|
+
self._in_connect = False # Caller may run .isconnected()
|
|
803
|
+
raise
|
|
804
|
+
self.rcv_pids.clear()
|
|
805
|
+
# If we get here without error broker/LAN must be up.
|
|
806
|
+
self._isconnected = True
|
|
807
|
+
self._in_connect = False # Low level code can now check connectivity.
|
|
808
|
+
if not self._events:
|
|
809
|
+
asyncio.create_task(self._wifi_handler(True)) # User handler.
|
|
810
|
+
if not self._has_connected:
|
|
811
|
+
self._has_connected = True # Use normal clean flag on reconnect.
|
|
812
|
+
asyncio.create_task(self._keep_connected())
|
|
813
|
+
# Runs forever unless user issues .disconnect()
|
|
814
|
+
|
|
815
|
+
asyncio.create_task(self._handle_msg()) # Task quits on connection fail.
|
|
816
|
+
self._tasks.append(asyncio.create_task(self._keep_alive()))
|
|
817
|
+
if self.DEBUG:
|
|
818
|
+
self._tasks.append(asyncio.create_task(self._memory()))
|
|
819
|
+
if self._events:
|
|
820
|
+
self.up.set() # Connectivity is up
|
|
821
|
+
else:
|
|
822
|
+
asyncio.create_task(self._connect_handler(self)) # User handler.
|
|
823
|
+
|
|
824
|
+
# Launched by .connect(). Runs until connectivity fails. Checks for and
|
|
825
|
+
# handles incoming messages.
|
|
826
|
+
async def _handle_msg(self):
|
|
827
|
+
try:
|
|
828
|
+
while self.isconnected():
|
|
829
|
+
async with self.lock:
|
|
830
|
+
await self.wait_msg() # Immediate return if no message
|
|
831
|
+
# https://github.com/peterhinch/micropython-mqtt/issues/166
|
|
832
|
+
# A delay > 0 is necessary for webrepl compatibility.
|
|
833
|
+
await asyncio.sleep_ms(5) # Let other tasks get lock
|
|
834
|
+
|
|
835
|
+
except OSError:
|
|
836
|
+
pass
|
|
837
|
+
self._reconnect() # Broker or WiFi fail.
|
|
838
|
+
|
|
839
|
+
# Keep broker alive MQTT spec 3.1.2.10 Keep Alive.
|
|
840
|
+
# Runs until ping failure or no response in keepalive period.
|
|
841
|
+
async def _keep_alive(self):
|
|
842
|
+
while self.isconnected():
|
|
843
|
+
pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval
|
|
844
|
+
if pings_due >= 4:
|
|
845
|
+
self.dprint("Reconnect: broker fail.")
|
|
846
|
+
break
|
|
847
|
+
await asyncio.sleep_ms(self._ping_interval)
|
|
848
|
+
try:
|
|
849
|
+
await self._ping()
|
|
850
|
+
except OSError:
|
|
851
|
+
break
|
|
852
|
+
self._reconnect() # Broker or WiFi fail.
|
|
853
|
+
|
|
854
|
+
async def _kill_tasks(self, kill_skt): # Cancel running tasks
|
|
855
|
+
for task in self._tasks:
|
|
856
|
+
task.cancel()
|
|
857
|
+
self._tasks.clear()
|
|
858
|
+
await asyncio.sleep_ms(0) # Ensure cancellation complete
|
|
859
|
+
if kill_skt: # Close socket
|
|
860
|
+
self._close()
|
|
861
|
+
|
|
862
|
+
# DEBUG: show RAM messages.
|
|
863
|
+
async def _memory(self):
|
|
864
|
+
while True:
|
|
865
|
+
await asyncio.sleep(20)
|
|
866
|
+
gc.collect()
|
|
867
|
+
self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc())
|
|
868
|
+
|
|
869
|
+
def isconnected(self):
|
|
870
|
+
if self._in_connect: # Disable low-level check during .connect()
|
|
871
|
+
return True
|
|
872
|
+
|
|
873
|
+
if self._isconnected and not self._sta_if.isconnected(): # It's going down.
|
|
874
|
+
self._reconnect()
|
|
875
|
+
return self._isconnected
|
|
876
|
+
|
|
877
|
+
def _reconnect(self): # Schedule a reconnection if not underway.
|
|
878
|
+
if self._isconnected:
|
|
879
|
+
self._isconnected = False
|
|
880
|
+
asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket
|
|
881
|
+
if self._events: # Signal an outage
|
|
882
|
+
self.down.set()
|
|
883
|
+
else:
|
|
884
|
+
asyncio.create_task(self._wifi_handler(False)) # User handler.
|
|
885
|
+
|
|
886
|
+
# Await broker connection.
|
|
887
|
+
async def _connection(self):
|
|
888
|
+
while not self._isconnected:
|
|
889
|
+
await asyncio.sleep(1)
|
|
890
|
+
|
|
891
|
+
# Scheduled on 1st successful connection. Runs forever maintaining wifi and
|
|
892
|
+
# broker connection. Must handle conditions at edge of WiFi range.
|
|
893
|
+
async def _keep_connected(self):
|
|
894
|
+
while self._has_connected:
|
|
895
|
+
if self.isconnected(): # Pause for 1 second
|
|
896
|
+
await asyncio.sleep(1)
|
|
897
|
+
gc.collect()
|
|
898
|
+
else: # Link is down, socket is closed, tasks are killed
|
|
899
|
+
try:
|
|
900
|
+
self._sta_if.disconnect()
|
|
901
|
+
except OSError:
|
|
902
|
+
self.dprint("Wi-Fi not started, unable to disconnect interface")
|
|
903
|
+
await asyncio.sleep(1)
|
|
904
|
+
try:
|
|
905
|
+
await self.wifi_connect()
|
|
906
|
+
except OSError:
|
|
907
|
+
continue
|
|
908
|
+
if not self._has_connected: # User has issued the terminal .disconnect()
|
|
909
|
+
self.dprint("Disconnected, exiting _keep_connected")
|
|
910
|
+
break
|
|
911
|
+
try:
|
|
912
|
+
await self.connect()
|
|
913
|
+
# Now has set ._isconnected and scheduled _connect_handler().
|
|
914
|
+
self.dprint("Reconnect OK!")
|
|
915
|
+
except OSError as e:
|
|
916
|
+
self.dprint("Error in reconnect. %s", e)
|
|
917
|
+
# Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received.
|
|
918
|
+
self._close() # Disconnect and try again.
|
|
919
|
+
self._in_connect = False
|
|
920
|
+
self._isconnected = False
|
|
921
|
+
self.dprint("Disconnected, exited _keep_connected")
|
|
922
|
+
|
|
923
|
+
async def subscribe(self, topic, qos=0, properties=None):
|
|
924
|
+
qos_check(qos)
|
|
925
|
+
while 1:
|
|
926
|
+
await self._connection()
|
|
927
|
+
try:
|
|
928
|
+
return await super().subscribe(topic, qos, properties)
|
|
929
|
+
except OSError:
|
|
930
|
+
pass
|
|
931
|
+
self._reconnect() # Broker or WiFi fail.
|
|
932
|
+
|
|
933
|
+
async def unsubscribe(self, topic, properties=None):
|
|
934
|
+
while 1:
|
|
935
|
+
await self._connection()
|
|
936
|
+
try:
|
|
937
|
+
return await super().unsubscribe(topic, properties)
|
|
938
|
+
except OSError:
|
|
939
|
+
pass
|
|
940
|
+
self._reconnect() # Broker or WiFi fail.
|
|
941
|
+
|
|
942
|
+
async def publish(self, topic, msg, retain=False, qos=0, properties=None):
|
|
943
|
+
qos_check(qos)
|
|
944
|
+
while 1:
|
|
945
|
+
await self._connection()
|
|
946
|
+
try:
|
|
947
|
+
return await super().publish(topic, msg, retain, qos, properties)
|
|
948
|
+
except OSError:
|
|
949
|
+
pass
|
|
950
|
+
self._reconnect() # Broker or WiFi fail.
|