micrOSDevToolKit 2.13.1__py3-none-any.whl → 2.17.1__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.

Files changed (220) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +125 -121
  2. micrOS/source/Common.py +48 -26
  3. micrOS/source/Config.py +13 -5
  4. micrOS/source/Espnow.py +100 -58
  5. micrOS/source/Files.py +77 -41
  6. micrOS/source/Hooks.py +18 -34
  7. micrOS/source/Logger.py +2 -7
  8. micrOS/source/Network.py +36 -16
  9. micrOS/source/Server.py +22 -8
  10. micrOS/source/Shell.py +9 -6
  11. micrOS/source/Tasks.py +34 -13
  12. micrOS/source/Web.py +69 -31
  13. micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
  14. micrOS/source/__pycache__/Files.cpython-312.pyc +0 -0
  15. micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
  16. micrOS/source/__pycache__/Server.cpython-312.pyc +0 -0
  17. micrOS/source/config/_git.keep +0 -0
  18. micrOS/source/micrOS.py +7 -0
  19. micrOS/source/micrOSloader.py +4 -10
  20. micrOS/source/microIO.py +2 -2
  21. micrOS/source/modules/IO_esp32c6.py +38 -0
  22. micrOS/source/modules/LM_L298N.py +161 -0
  23. micrOS/source/modules/LM_cluster.py +255 -0
  24. {toolkit/workspace/precompiled → micrOS/source/modules}/LM_esp32.py +5 -0
  25. micrOS/source/modules/LM_espnow.py +36 -0
  26. micrOS/source/{LM_i2c.py → modules/LM_i2c.py} +1 -1
  27. micrOS/source/{LM_light_sensor.py → modules/LM_light_sensor.py} +2 -2
  28. micrOS/source/modules/LM_mqtt_client.py +246 -0
  29. micrOS/source/{LM_neoeffects.py → modules/LM_neoeffects.py} +14 -4
  30. micrOS/source/{LM_neomatrix.py → modules/LM_neomatrix.py} +140 -38
  31. micrOS/source/{LM_oled_ui.py → modules/LM_oled_ui.py} +2 -2
  32. micrOS/source/{LM_oledui.py → modules/LM_oledui.py} +2 -2
  33. micrOS/source/{LM_pacman.py → modules/LM_pacman.py} +74 -29
  34. micrOS/source/{LM_presence.py → modules/LM_presence.py} +2 -2
  35. micrOS/source/{LM_robustness.py → modules/LM_robustness.py} +49 -2
  36. micrOS/source/{LM_tcs3472.py → modules/LM_tcs3472.py} +4 -6
  37. micrOS/source/web/dashboard.html +2 -0
  38. micrOS/source/web/matrix_draw.html +390 -0
  39. micrOS/source/web/uapi.js +9 -6
  40. {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.1.dist-info}/METADATA +30 -37
  41. {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.1.dist-info}/RECORD +200 -190
  42. toolkit/DevEnvCompile.py +21 -12
  43. toolkit/DevEnvOTA.py +27 -16
  44. toolkit/DevEnvUSB.py +35 -21
  45. toolkit/LM_to_compile.dat +3 -1
  46. toolkit/MicrOSDevEnv.py +37 -21
  47. toolkit/dashboard_apps/QMI8685_GYRO.py +1 -1
  48. toolkit/dashboard_apps/SystemTest.py +8 -5
  49. toolkit/{MicrosFiles.py → lib/MicrosFiles.py} +24 -4
  50. toolkit/micrOSdashboard.py +2 -2
  51. toolkit/micrOSlint.py +17 -7
  52. toolkit/simulator_lib/__pycache__/simulator.cpython-312.pyc +0 -0
  53. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  54. toolkit/simulator_lib/mqtt_as/Note.md +15 -0
  55. toolkit/simulator_lib/mqtt_as/__init__.py +950 -0
  56. toolkit/simulator_lib/mqtt_as/__pycache__/__init__.cpython-312.pyc +0 -0
  57. toolkit/simulator_lib/mqtt_as/clean.py +69 -0
  58. toolkit/simulator_lib/mqtt_as/mqtt_v5_properties.py +239 -0
  59. toolkit/simulator_lib/mqtt_as/range.py +90 -0
  60. toolkit/simulator_lib/mqtt_as/range_ex.py +119 -0
  61. toolkit/simulator_lib/simulator.py +14 -1
  62. toolkit/simulator_lib/uos.py +2 -0
  63. toolkit/workspace/precompiled/Common.mpy +0 -0
  64. toolkit/workspace/precompiled/Config.mpy +0 -0
  65. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  66. toolkit/workspace/precompiled/Files.mpy +0 -0
  67. toolkit/workspace/precompiled/Hooks.mpy +0 -0
  68. toolkit/workspace/precompiled/Logger.mpy +0 -0
  69. toolkit/workspace/precompiled/Network.mpy +0 -0
  70. toolkit/workspace/precompiled/Server.mpy +0 -0
  71. toolkit/workspace/precompiled/Shell.mpy +0 -0
  72. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  73. toolkit/workspace/precompiled/Web.mpy +0 -0
  74. toolkit/workspace/precompiled/config/_git.keep +0 -0
  75. toolkit/workspace/precompiled/micrOS.mpy +0 -0
  76. toolkit/workspace/precompiled/micrOSloader.mpy +0 -0
  77. toolkit/workspace/precompiled/microIO.mpy +0 -0
  78. toolkit/workspace/precompiled/{IO_esp32.mpy → modules/IO_esp32.mpy} +0 -0
  79. toolkit/workspace/precompiled/{IO_esp32c3.mpy → modules/IO_esp32c3.mpy} +0 -0
  80. toolkit/workspace/precompiled/modules/IO_esp32c6.mpy +0 -0
  81. toolkit/workspace/precompiled/{IO_esp32s2.mpy → modules/IO_esp32s2.mpy} +0 -0
  82. toolkit/workspace/precompiled/{IO_esp32s3.mpy → modules/IO_esp32s3.mpy} +0 -0
  83. toolkit/workspace/precompiled/{IO_m5stamp.mpy → modules/IO_m5stamp.mpy} +0 -0
  84. toolkit/workspace/precompiled/{IO_qtpy.mpy → modules/IO_qtpy.mpy} +0 -0
  85. toolkit/workspace/precompiled/modules/IO_rp2.mpy +0 -0
  86. toolkit/workspace/precompiled/modules/IO_s3matrix.mpy +0 -0
  87. toolkit/workspace/precompiled/{IO_tinypico.mpy → modules/IO_tinypico.mpy} +0 -0
  88. toolkit/workspace/precompiled/modules/LM_L298N.mpy +0 -0
  89. toolkit/workspace/precompiled/{LM_OV2640.mpy → modules/LM_OV2640.mpy} +0 -0
  90. toolkit/workspace/precompiled/{LM_aht10.mpy → modules/LM_aht10.mpy} +0 -0
  91. toolkit/workspace/precompiled/{LM_bme280.mpy → modules/LM_bme280.mpy} +0 -0
  92. toolkit/workspace/precompiled/{LM_buzzer.mpy → modules/LM_buzzer.mpy} +0 -0
  93. toolkit/workspace/precompiled/{LM_cct.mpy → modules/LM_cct.mpy} +0 -0
  94. toolkit/workspace/precompiled/modules/LM_cluster.mpy +0 -0
  95. toolkit/workspace/precompiled/{LM_co2.mpy → modules/LM_co2.mpy} +0 -0
  96. toolkit/workspace/precompiled/{LM_dht11.mpy → modules/LM_dht11.mpy} +0 -0
  97. toolkit/workspace/precompiled/{LM_dht22.mpy → modules/LM_dht22.mpy} +0 -0
  98. toolkit/workspace/precompiled/{LM_dimmer.mpy → modules/LM_dimmer.mpy} +0 -0
  99. toolkit/workspace/precompiled/{LM_distance.mpy → modules/LM_distance.mpy} +0 -0
  100. toolkit/workspace/precompiled/{LM_ds18.mpy → modules/LM_ds18.mpy} +0 -0
  101. {micrOS/source → toolkit/workspace/precompiled/modules}/LM_esp32.py +5 -0
  102. toolkit/workspace/precompiled/modules/LM_espnow.py +36 -0
  103. toolkit/workspace/precompiled/{LM_gameOfLife.mpy → modules/LM_gameOfLife.mpy} +0 -0
  104. toolkit/workspace/precompiled/{LM_genIO.mpy → modules/LM_genIO.mpy} +0 -0
  105. toolkit/workspace/precompiled/{LM_haptic.mpy → modules/LM_haptic.mpy} +0 -0
  106. toolkit/workspace/precompiled/{LM_i2c.py → modules/LM_i2c.py} +1 -1
  107. toolkit/workspace/precompiled/{LM_i2s_mic.mpy → modules/LM_i2s_mic.mpy} +0 -0
  108. toolkit/workspace/precompiled/{LM_keychain.mpy → modules/LM_keychain.mpy} +0 -0
  109. toolkit/workspace/precompiled/{LM_ld2410.mpy → modules/LM_ld2410.mpy} +0 -0
  110. toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
  111. toolkit/workspace/precompiled/modules/LM_mqtt_client.mpy +0 -0
  112. toolkit/workspace/precompiled/{LM_neoeffects.mpy → modules/LM_neoeffects.mpy} +0 -0
  113. toolkit/workspace/precompiled/modules/LM_neomatrix.mpy +0 -0
  114. toolkit/workspace/precompiled/{LM_neopixel.mpy → modules/LM_neopixel.mpy} +0 -0
  115. toolkit/workspace/precompiled/{LM_oled.mpy → modules/LM_oled.mpy} +0 -0
  116. toolkit/workspace/precompiled/{LM_oled_sh1106.mpy → modules/LM_oled_sh1106.mpy} +0 -0
  117. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  118. toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
  119. toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
  120. toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
  121. toolkit/workspace/precompiled/{LM_rest.mpy → modules/LM_rest.mpy} +0 -0
  122. toolkit/workspace/precompiled/{LM_rgb.mpy → modules/LM_rgb.mpy} +0 -0
  123. toolkit/workspace/precompiled/{LM_rgbcct.mpy → modules/LM_rgbcct.mpy} +0 -0
  124. toolkit/workspace/precompiled/{LM_roboarm.mpy → modules/LM_roboarm.mpy} +0 -0
  125. toolkit/workspace/precompiled/{LM_robustness.py → modules/LM_robustness.py} +49 -2
  126. toolkit/workspace/precompiled/{LM_servo.mpy → modules/LM_servo.mpy} +0 -0
  127. toolkit/workspace/precompiled/{LM_sound_event.mpy → modules/LM_sound_event.mpy} +0 -0
  128. toolkit/workspace/precompiled/{LM_stepper.mpy → modules/LM_stepper.mpy} +0 -0
  129. toolkit/workspace/precompiled/{LM_switch.mpy → modules/LM_switch.mpy} +0 -0
  130. toolkit/workspace/precompiled/{LM_system.mpy → modules/LM_system.mpy} +0 -0
  131. toolkit/workspace/precompiled/{LM_tcs3472.py → modules/LM_tcs3472.py} +4 -6
  132. toolkit/workspace/precompiled/{LM_telegram.mpy → modules/LM_telegram.mpy} +0 -0
  133. toolkit/workspace/precompiled/{LM_tinyrgb.mpy → modules/LM_tinyrgb.mpy} +0 -0
  134. toolkit/workspace/precompiled/{LM_trackball.mpy → modules/LM_trackball.mpy} +0 -0
  135. toolkit/workspace/precompiled/{LM_veml7700.mpy → modules/LM_veml7700.mpy} +0 -0
  136. toolkit/workspace/precompiled/web/dashboard.html +2 -0
  137. toolkit/workspace/precompiled/web/matrix_draw.html +390 -0
  138. toolkit/workspace/precompiled/web/uapi.js +9 -6
  139. micrOS/source/IO_esp32c6.py +0 -16
  140. micrOS/source/LM_L298N_DCmotor.py +0 -86
  141. micrOS/source/LM_espnow.py +0 -57
  142. micrOS/source/LM_mqtt_pro.py +0 -211
  143. toolkit/lib/file_extensions.py +0 -22
  144. toolkit/workspace/precompiled/Common.cpython-312.pyc +0 -0
  145. toolkit/workspace/precompiled/IO_esp32c6.mpy +0 -0
  146. toolkit/workspace/precompiled/IO_rp2.mpy +0 -0
  147. toolkit/workspace/precompiled/IO_s3matrix.mpy +0 -0
  148. toolkit/workspace/precompiled/LM_L298N_DCmotor.mpy +0 -0
  149. toolkit/workspace/precompiled/LM_espnow.py +0 -57
  150. toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
  151. toolkit/workspace/precompiled/LM_mqtt_pro.py +0 -211
  152. toolkit/workspace/precompiled/LM_neomatrix.mpy +0 -0
  153. toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
  154. toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
  155. toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
  156. toolkit/workspace/precompiled/LM_presence.mpy +0 -0
  157. toolkit/workspace/precompiled/Logger.cpython-312.pyc +0 -0
  158. toolkit/workspace/precompiled/Server.cpython-312.pyc +0 -0
  159. /micrOS/source/{IO_esp32.py → modules/IO_esp32.py} +0 -0
  160. /micrOS/source/{IO_esp32c3.py → modules/IO_esp32c3.py} +0 -0
  161. /micrOS/source/{IO_esp32s2.py → modules/IO_esp32s2.py} +0 -0
  162. /micrOS/source/{IO_esp32s3.py → modules/IO_esp32s3.py} +0 -0
  163. /micrOS/source/{IO_m5stamp.py → modules/IO_m5stamp.py} +0 -0
  164. /micrOS/source/{IO_qtpy.py → modules/IO_qtpy.py} +0 -0
  165. /micrOS/source/{IO_rp2.py → modules/IO_rp2.py} +0 -0
  166. /micrOS/source/{IO_s3matrix.py → modules/IO_s3matrix.py} +0 -0
  167. /micrOS/source/{IO_tinypico.py → modules/IO_tinypico.py} +0 -0
  168. /micrOS/source/{LM_L9110_DCmotor.py → modules/LM_L9110_DCmotor.py} +0 -0
  169. /micrOS/source/{LM_OV2640.py → modules/LM_OV2640.py} +0 -0
  170. /micrOS/source/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +0 -0
  171. /micrOS/source/{LM_aht10.py → modules/LM_aht10.py} +0 -0
  172. /micrOS/source/{LM_bme280.py → modules/LM_bme280.py} +0 -0
  173. /micrOS/source/{LM_buzzer.py → modules/LM_buzzer.py} +0 -0
  174. /micrOS/source/{LM_cct.py → modules/LM_cct.py} +0 -0
  175. /micrOS/source/{LM_co2.py → modules/LM_co2.py} +0 -0
  176. /micrOS/source/{LM_dashboard_be.py → modules/LM_dashboard_be.py} +0 -0
  177. /micrOS/source/{LM_dht11.py → modules/LM_dht11.py} +0 -0
  178. /micrOS/source/{LM_dht22.py → modules/LM_dht22.py} +0 -0
  179. /micrOS/source/{LM_dimmer.py → modules/LM_dimmer.py} +0 -0
  180. /micrOS/source/{LM_distance.py → modules/LM_distance.py} +0 -0
  181. /micrOS/source/{LM_ds18.py → modules/LM_ds18.py} +0 -0
  182. /micrOS/source/{LM_gameOfLife.py → modules/LM_gameOfLife.py} +0 -0
  183. /micrOS/source/{LM_genIO.py → modules/LM_genIO.py} +0 -0
  184. /micrOS/source/{LM_haptic.py → modules/LM_haptic.py} +0 -0
  185. /micrOS/source/{LM_i2s_mic.py → modules/LM_i2s_mic.py} +0 -0
  186. /micrOS/source/{LM_keychain.py → modules/LM_keychain.py} +0 -0
  187. /micrOS/source/{LM_ld2410.py → modules/LM_ld2410.py} +0 -0
  188. /micrOS/source/{LM_neopixel.py → modules/LM_neopixel.py} +0 -0
  189. /micrOS/source/{LM_oled.py → modules/LM_oled.py} +0 -0
  190. /micrOS/source/{LM_oled_sh1106.py → modules/LM_oled_sh1106.py} +0 -0
  191. /micrOS/source/{LM_pet_feeder.py → modules/LM_pet_feeder.py} +0 -0
  192. /micrOS/source/{LM_qmi8658.py → modules/LM_qmi8658.py} +0 -0
  193. /micrOS/source/{LM_rencoder.py → modules/LM_rencoder.py} +0 -0
  194. /micrOS/source/{LM_rest.py → modules/LM_rest.py} +0 -0
  195. /micrOS/source/{LM_rgb.py → modules/LM_rgb.py} +0 -0
  196. /micrOS/source/{LM_rgbcct.py → modules/LM_rgbcct.py} +0 -0
  197. /micrOS/source/{LM_roboarm.py → modules/LM_roboarm.py} +0 -0
  198. /micrOS/source/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
  199. /micrOS/source/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
  200. /micrOS/source/{LM_servo.py → modules/LM_servo.py} +0 -0
  201. /micrOS/source/{LM_sound_event.py → modules/LM_sound_event.py} +0 -0
  202. /micrOS/source/{LM_stepper.py → modules/LM_stepper.py} +0 -0
  203. /micrOS/source/{LM_switch.py → modules/LM_switch.py} +0 -0
  204. /micrOS/source/{LM_system.py → modules/LM_system.py} +0 -0
  205. /micrOS/source/{LM_telegram.py → modules/LM_telegram.py} +0 -0
  206. /micrOS/source/{LM_tinyrgb.py → modules/LM_tinyrgb.py} +0 -0
  207. /micrOS/source/{LM_trackball.py → modules/LM_trackball.py} +0 -0
  208. /micrOS/source/{LM_veml7700.py → modules/LM_veml7700.py} +0 -0
  209. {microsdevtoolkit-2.13.1.data → microsdevtoolkit-2.17.1.data}/scripts/devToolKit.py +0 -0
  210. {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.1.dist-info}/WHEEL +0 -0
  211. {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.1.dist-info}/licenses/LICENSE +0 -0
  212. {microsdevtoolkit-2.13.1.dist-info → microsdevtoolkit-2.17.1.dist-info}/top_level.txt +0 -0
  213. /toolkit/workspace/precompiled/{LM_L9110_DCmotor.py → modules/LM_L9110_DCmotor.py} +0 -0
  214. /toolkit/workspace/precompiled/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +0 -0
  215. /toolkit/workspace/precompiled/{LM_dashboard_be.py → modules/LM_dashboard_be.py} +0 -0
  216. /toolkit/workspace/precompiled/{LM_pet_feeder.py → modules/LM_pet_feeder.py} +0 -0
  217. /toolkit/workspace/precompiled/{LM_qmi8658.py → modules/LM_qmi8658.py} +0 -0
  218. /toolkit/workspace/precompiled/{LM_rencoder.py → modules/LM_rencoder.py} +0 -0
  219. /toolkit/workspace/precompiled/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
  220. /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.