A repository for MicroPython code that communicates with and relays information from the Texas Instruments TMP117/119 temperature sensor.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

803 lines
29 KiB

2 months ago
  1. # mqtt_as.py Asynchronous version of umqtt.robust
  2. # (C) Copyright Peter Hinch 2017-2023.
  3. # Released under the MIT licence.
  4. # Pyboard D support added also RP2/default
  5. # Various improvements contributed by Kevin Köck.
  6. import gc
  7. import usocket as socket
  8. import ustruct as struct
  9. gc.collect()
  10. from ubinascii import hexlify
  11. import uasyncio as asyncio
  12. gc.collect()
  13. from utime import ticks_ms, ticks_diff
  14. from uerrno import EINPROGRESS, ETIMEDOUT
  15. gc.collect()
  16. from micropython import const
  17. from machine import unique_id
  18. import network
  19. gc.collect()
  20. from sys import platform
  21. VERSION = (0, 7, 2)
  22. # Default short delay for good SynCom throughput (avoid sleep(0) with SynCom).
  23. _DEFAULT_MS = const(20)
  24. _SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency
  25. # Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection().
  26. ESP32 = platform == "esp32"
  27. RP2 = platform == "rp2"
  28. if ESP32:
  29. # https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942
  30. BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors
  31. elif RP2:
  32. BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110]
  33. else:
  34. BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT]
  35. ESP8266 = platform == "esp8266"
  36. PYBOARD = platform == "pyboard"
  37. # Default "do little" coro for optional user replacement
  38. async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program
  39. await asyncio.sleep_ms(_DEFAULT_MS)
  40. class MsgQueue:
  41. def __init__(self, size):
  42. self._q = [0 for _ in range(max(size, 4))]
  43. self._size = size
  44. self._wi = 0
  45. self._ri = 0
  46. self._evt = asyncio.Event()
  47. self.discards = 0
  48. def put(self, *v):
  49. self._q[self._wi] = v
  50. self._evt.set()
  51. self._wi = (self._wi + 1) % self._size
  52. if self._wi == self._ri: # Would indicate empty
  53. self._ri = (self._ri + 1) % self._size # Discard a message
  54. self.discards += 1
  55. def __aiter__(self):
  56. return self
  57. async def __anext__(self):
  58. if self._ri == self._wi: # Empty
  59. self._evt.clear()
  60. await self._evt.wait()
  61. r = self._q[self._ri]
  62. self._ri = (self._ri + 1) % self._size
  63. return r
  64. config = {
  65. "client_id": hexlify(unique_id()),
  66. "server": None,
  67. "port": 0,
  68. "user": "",
  69. "password": "",
  70. "keepalive": 60,
  71. "ping_interval": 0,
  72. "ssl": False,
  73. "ssl_params": {},
  74. "response_time": 10,
  75. "clean_init": True,
  76. "clean": True,
  77. "max_repubs": 4,
  78. "will": None,
  79. "subs_cb": lambda *_: None,
  80. "wifi_coro": eliza,
  81. "connect_coro": eliza,
  82. "ssid": None,
  83. "wifi_pw": None,
  84. "queue_len": 0,
  85. "gateway": False,
  86. }
  87. class MQTTException(Exception):
  88. pass
  89. def pid_gen():
  90. pid = 0
  91. while True:
  92. pid = pid + 1 if pid < 65535 else 1
  93. yield pid
  94. def qos_check(qos):
  95. if not (qos == 0 or qos == 1):
  96. raise ValueError("Only qos 0 and 1 are supported.")
  97. # MQTT_base class. Handles MQTT protocol on the basis of a good connection.
  98. # Exceptions from connectivity failures are handled by MQTTClient subclass.
  99. class MQTT_base:
  100. REPUB_COUNT = 0 # TEST
  101. DEBUG = False
  102. def __init__(self, config):
  103. self._events = config["queue_len"] > 0
  104. # MQTT config
  105. self._client_id = config["client_id"]
  106. self._user = config["user"]
  107. self._pswd = config["password"]
  108. self._keepalive = config["keepalive"]
  109. if self._keepalive >= 65536:
  110. raise ValueError("invalid keepalive time")
  111. self._response_time = config["response_time"] * 1000 # Repub if no PUBACK received (ms).
  112. self._max_repubs = config["max_repubs"]
  113. self._clean_init = config["clean_init"] # clean_session state on first connection
  114. self._clean = config["clean"] # clean_session state on reconnect
  115. will = config["will"]
  116. if will is None:
  117. self._lw_topic = False
  118. else:
  119. self._set_last_will(*will)
  120. # WiFi config
  121. self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266
  122. self._wifi_pw = config["wifi_pw"]
  123. self._ssl = config["ssl"]
  124. self._ssl_params = config["ssl_params"]
  125. # Callbacks and coros
  126. if self._events:
  127. self.up = asyncio.Event()
  128. self.down = asyncio.Event()
  129. self.queue = MsgQueue(config["queue_len"])
  130. else: # Callbacks
  131. self._cb = config["subs_cb"]
  132. self._wifi_handler = config["wifi_coro"]
  133. self._connect_handler = config["connect_coro"]
  134. # Network
  135. self.port = config["port"]
  136. if self.port == 0:
  137. self.port = 8883 if self._ssl else 1883
  138. self.server = config["server"]
  139. if self.server is None:
  140. raise ValueError("no server specified.")
  141. self._sock = None
  142. self._sta_if = network.WLAN(network.STA_IF)
  143. self._sta_if.active(True)
  144. if config["gateway"]: # Called from gateway (hence ESP32).
  145. import aioespnow # Set up ESPNOW
  146. while not (sta := self._sta_if).active():
  147. time.sleep(0.1)
  148. sta.config(pm=sta.PM_NONE) # No power management
  149. sta.active(True)
  150. self._espnow = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
  151. self._espnow.active(True)
  152. self.newpid = pid_gen()
  153. self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response
  154. self.last_rx = ticks_ms() # Time of last communication from broker
  155. self.lock = asyncio.Lock()
  156. def _set_last_will(self, topic, msg, retain=False, qos=0):
  157. qos_check(qos)
  158. if not topic:
  159. raise ValueError("Empty topic.")
  160. self._lw_topic = topic
  161. self._lw_msg = msg
  162. self._lw_qos = qos
  163. self._lw_retain = retain
  164. def dprint(self, msg, *args):
  165. if self.DEBUG:
  166. print(msg % args)
  167. def _timeout(self, t):
  168. return ticks_diff(ticks_ms(), t) > self._response_time
  169. async def _as_read(self, n, sock=None): # OSError caught by superclass
  170. if sock is None:
  171. sock = self._sock
  172. # Declare a byte array of size n. That space is needed anyway, better
  173. # to just 'allocate' it in one go instead of appending to an
  174. # existing object, this prevents reallocation and fragmentation.
  175. data = bytearray(n)
  176. buffer = memoryview(data)
  177. size = 0
  178. t = ticks_ms()
  179. while size < n:
  180. if self._timeout(t) or not self.isconnected():
  181. raise OSError(-1, "Timeout on socket read")
  182. try:
  183. msg_size = sock.readinto(buffer[size:], n - size)
  184. except OSError as e: # ESP32 issues weird 119 errors here
  185. msg_size = None
  186. if e.args[0] not in BUSY_ERRORS:
  187. raise
  188. if msg_size == 0: # Connection closed by host
  189. raise OSError(-1, "Connection closed by host")
  190. if msg_size is not None: # data received
  191. size += msg_size
  192. t = ticks_ms()
  193. self.last_rx = ticks_ms()
  194. await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
  195. return data
  196. async def _as_write(self, bytes_wr, length=0, sock=None):
  197. if sock is None:
  198. sock = self._sock
  199. # Wrap bytes in memoryview to avoid copying during slicing
  200. bytes_wr = memoryview(bytes_wr)
  201. if length:
  202. bytes_wr = bytes_wr[:length]
  203. t = ticks_ms()
  204. while bytes_wr:
  205. if self._timeout(t) or not self.isconnected():
  206. raise OSError(-1, "Timeout on socket write")
  207. try:
  208. n = sock.write(bytes_wr)
  209. except OSError as e: # ESP32 issues weird 119 errors here
  210. n = 0
  211. if e.args[0] not in BUSY_ERRORS:
  212. raise
  213. if n:
  214. t = ticks_ms()
  215. bytes_wr = bytes_wr[n:]
  216. await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
  217. async def _send_str(self, s):
  218. await self._as_write(struct.pack("!H", len(s)))
  219. await self._as_write(s)
  220. async def _recv_len(self):
  221. n = 0
  222. sh = 0
  223. while 1:
  224. res = await self._as_read(1)
  225. b = res[0]
  226. n |= (b & 0x7F) << sh
  227. if not b & 0x80:
  228. return n
  229. sh += 7
  230. async def _connect(self, clean):
  231. self._sock = socket.socket()
  232. self._sock.setblocking(False)
  233. try:
  234. self._sock.connect(self._addr)
  235. except OSError as e:
  236. if e.args[0] not in BUSY_ERRORS:
  237. raise
  238. await asyncio.sleep_ms(_DEFAULT_MS)
  239. self.dprint("Connecting to broker.")
  240. if self._ssl:
  241. try:
  242. import ssl
  243. except ImportError:
  244. import ussl as ssl
  245. self._sock = ssl.wrap_socket(self._sock, **self._ssl_params)
  246. premsg = bytearray(b"\x10\0\0\0\0\0")
  247. msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1
  248. sz = 10 + 2 + len(self._client_id)
  249. msg[6] = clean << 1
  250. if self._user:
  251. sz += 2 + len(self._user) + 2 + len(self._pswd)
  252. msg[6] |= 0xC0
  253. if self._keepalive:
  254. msg[7] |= self._keepalive >> 8
  255. msg[8] |= self._keepalive & 0x00FF
  256. if self._lw_topic:
  257. sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg)
  258. msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3
  259. msg[6] |= self._lw_retain << 5
  260. i = 1
  261. while sz > 0x7F:
  262. premsg[i] = (sz & 0x7F) | 0x80
  263. sz >>= 7
  264. i += 1
  265. premsg[i] = sz
  266. await self._as_write(premsg, i + 2)
  267. await self._as_write(msg)
  268. await self._send_str(self._client_id)
  269. if self._lw_topic:
  270. await self._send_str(self._lw_topic)
  271. await self._send_str(self._lw_msg)
  272. if self._user:
  273. await self._send_str(self._user)
  274. await self._send_str(self._pswd)
  275. # Await CONNACK
  276. # read causes ECONNABORTED if broker is out; triggers a reconnect.
  277. resp = await self._as_read(4)
  278. self.dprint("Connected to broker.") # Got CONNACK
  279. if resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02:
  280. # Bad CONNACK e.g. authentication fail.
  281. raise OSError(
  282. -1, f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)"
  283. )
  284. async def _ping(self):
  285. async with self.lock:
  286. await self._as_write(b"\xc0\0")
  287. # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8
  288. async def wan_ok(
  289. self,
  290. packet=b"$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01",
  291. ):
  292. if not self.isconnected(): # WiFi is down
  293. return False
  294. length = 32 # DNS query and response packet size
  295. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  296. s.setblocking(False)
  297. s.connect(("8.8.8.8", 53))
  298. await asyncio.sleep(1)
  299. try:
  300. await self._as_write(packet, sock=s)
  301. await asyncio.sleep(2)
  302. res = await self._as_read(length, s)
  303. if len(res) == length:
  304. return True # DNS response size OK
  305. except OSError: # Timeout on read: no connectivity.
  306. return False
  307. finally:
  308. s.close()
  309. return False
  310. async def broker_up(self): # Test broker connectivity
  311. if not self.isconnected():
  312. return False
  313. tlast = self.last_rx
  314. if ticks_diff(ticks_ms(), tlast) < 1000:
  315. return True
  316. try:
  317. await self._ping()
  318. except OSError:
  319. return False
  320. t = ticks_ms()
  321. while not self._timeout(t):
  322. await asyncio.sleep_ms(100)
  323. if ticks_diff(self.last_rx, tlast) > 0: # Response received
  324. return True
  325. return False
  326. async def disconnect(self):
  327. if self._sock is not None:
  328. await self._kill_tasks(False) # Keep socket open
  329. try:
  330. async with self.lock:
  331. self._sock.write(b"\xe0\0") # Close broker connection
  332. await asyncio.sleep_ms(100)
  333. except OSError:
  334. pass
  335. self._close()
  336. self._has_connected = False
  337. def _close(self):
  338. if self._sock is not None:
  339. self._sock.close()
  340. def close(self): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60
  341. self._close()
  342. try:
  343. self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors
  344. except OSError:
  345. self.dprint("Wi-Fi not started, unable to disconnect interface")
  346. self._sta_if.active(False)
  347. async def _await_pid(self, pid):
  348. t = ticks_ms()
  349. while pid in self.rcv_pids: # local copy
  350. if self._timeout(t) or not self.isconnected():
  351. break # Must repub or bail out
  352. await asyncio.sleep_ms(100)
  353. else:
  354. return True # PID received. All done.
  355. return False
  356. # qos == 1: coro blocks until wait_msg gets correct PID.
  357. # If WiFi fails completely subclass re-publishes with new PID.
  358. async def publish(self, topic, msg, retain, qos):
  359. pid = next(self.newpid)
  360. if qos:
  361. self.rcv_pids.add(pid)
  362. async with self.lock:
  363. await self._publish(topic, msg, retain, qos, 0, pid)
  364. if qos == 0:
  365. return
  366. count = 0
  367. while 1: # Await PUBACK, republish on timeout
  368. if await self._await_pid(pid):
  369. return
  370. # No match
  371. if count >= self._max_repubs or not self.isconnected():
  372. raise OSError(-1) # Subclass to re-publish with new PID
  373. async with self.lock:
  374. await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid
  375. count += 1
  376. self.REPUB_COUNT += 1
  377. async def _publish(self, topic, msg, retain, qos, dup, pid):
  378. pkt = bytearray(b"\x30\0\0\0")
  379. pkt[0] |= qos << 1 | retain | dup << 3
  380. sz = 2 + len(topic) + len(msg)
  381. if qos > 0:
  382. sz += 2
  383. if sz >= 2097152:
  384. raise MQTTException("Strings too long.")
  385. i = 1
  386. while sz > 0x7F:
  387. pkt[i] = (sz & 0x7F) | 0x80
  388. sz >>= 7
  389. i += 1
  390. pkt[i] = sz
  391. await self._as_write(pkt, i + 1)
  392. await self._send_str(topic)
  393. if qos > 0:
  394. struct.pack_into("!H", pkt, 0, pid)
  395. await self._as_write(pkt, 2)
  396. await self._as_write(msg)
  397. # Can raise OSError if WiFi fails. Subclass traps.
  398. async def subscribe(self, topic, qos):
  399. pkt = bytearray(b"\x82\0\0\0")
  400. pid = next(self.newpid)
  401. self.rcv_pids.add(pid)
  402. struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid)
  403. async with self.lock:
  404. await self._as_write(pkt)
  405. await self._send_str(topic)
  406. await self._as_write(qos.to_bytes(1, "little"))
  407. if not await self._await_pid(pid):
  408. raise OSError(-1)
  409. # Can raise OSError if WiFi fails. Subclass traps.
  410. async def unsubscribe(self, topic):
  411. pkt = bytearray(b"\xa2\0\0\0")
  412. pid = next(self.newpid)
  413. self.rcv_pids.add(pid)
  414. struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid)
  415. async with self.lock:
  416. await self._as_write(pkt)
  417. await self._send_str(topic)
  418. if not await self._await_pid(pid):
  419. raise OSError(-1)
  420. # Wait for a single incoming MQTT message and process it.
  421. # Subscribed messages are delivered to a callback previously
  422. # set by .setup() method. Other (internal) MQTT
  423. # messages processed internally.
  424. # Immediate return if no data available. Called from ._handle_msg().
  425. async def wait_msg(self):
  426. try:
  427. res = self._sock.read(1) # Throws OSError on WiFi fail
  428. except OSError as e:
  429. if e.args[0] in BUSY_ERRORS: # Needed by RP2
  430. await asyncio.sleep_ms(0)
  431. return
  432. raise
  433. if res is None:
  434. return
  435. if res == b"":
  436. raise OSError(-1, "Empty response")
  437. if res == b"\xd0": # PINGRESP
  438. await self._as_read(1) # Update .last_rx time
  439. return
  440. op = res[0]
  441. if op == 0x40: # PUBACK: save pid
  442. sz = await self._as_read(1)
  443. if sz != b"\x02":
  444. raise OSError(-1, "Invalid PUBACK packet")
  445. rcv_pid = await self._as_read(2)
  446. pid = rcv_pid[0] << 8 | rcv_pid[1]
  447. if pid in self.rcv_pids:
  448. self.rcv_pids.discard(pid)
  449. else:
  450. raise OSError(-1, "Invalid pid in PUBACK packet")
  451. if op == 0x90: # SUBACK
  452. resp = await self._as_read(4)
  453. if resp[3] == 0x80:
  454. raise OSError(-1, "Invalid SUBACK packet")
  455. pid = resp[2] | (resp[1] << 8)
  456. if pid in self.rcv_pids:
  457. self.rcv_pids.discard(pid)
  458. else:
  459. raise OSError(-1, "Invalid pid in SUBACK packet")
  460. if op == 0xB0: # UNSUBACK
  461. resp = await self._as_read(3)
  462. pid = resp[2] | (resp[1] << 8)
  463. if pid in self.rcv_pids:
  464. self.rcv_pids.discard(pid)
  465. else:
  466. raise OSError(-1)
  467. if op & 0xF0 != 0x30:
  468. return
  469. sz = await self._recv_len()
  470. topic_len = await self._as_read(2)
  471. topic_len = (topic_len[0] << 8) | topic_len[1]
  472. topic = await self._as_read(topic_len)
  473. sz -= topic_len + 2
  474. if op & 6:
  475. pid = await self._as_read(2)
  476. pid = pid[0] << 8 | pid[1]
  477. sz -= 2
  478. msg = await self._as_read(sz)
  479. retained = op & 0x01
  480. if self._events:
  481. self.queue.put(topic, msg, bool(retained))
  482. else:
  483. self._cb(topic, msg, bool(retained))
  484. if op & 6 == 2: # qos 1
  485. pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK
  486. struct.pack_into("!H", pkt, 2, pid)
  487. await self._as_write(pkt)
  488. elif op & 6 == 4: # qos 2 not supported
  489. raise OSError(-1, "QoS 2 not supported")
  490. # MQTTClient class. Handles issues relating to connectivity.
  491. class MQTTClient(MQTT_base):
  492. def __init__(self, config):
  493. super().__init__(config)
  494. self._isconnected = False # Current connection state
  495. keepalive = 1000 * self._keepalive # ms
  496. self._ping_interval = keepalive // 4 if keepalive else 20000
  497. p_i = config["ping_interval"] * 1000 # Can specify shorter e.g. for subscribe-only
  498. if p_i and p_i < self._ping_interval:
  499. self._ping_interval = p_i
  500. self._in_connect = False
  501. self._has_connected = False # Define 'Clean Session' value to use.
  502. self._tasks = []
  503. if ESP8266:
  504. import esp
  505. esp.sleep_type(0) # Improve connection integrity at cost of power consumption.
  506. async def wifi_connect(self, quick=False):
  507. s = self._sta_if
  508. if ESP8266:
  509. if s.isconnected(): # 1st attempt, already connected.
  510. return
  511. s.active(True)
  512. s.connect() # ESP8266 remembers connection.
  513. for _ in range(60):
  514. if (
  515. s.status() != network.STAT_CONNECTING
  516. ): # Break out on fail or success. Check once per sec.
  517. break
  518. await asyncio.sleep(1)
  519. if (
  520. s.status() == network.STAT_CONNECTING
  521. ): # might hang forever awaiting dhcp lease renewal or something else
  522. s.disconnect()
  523. await asyncio.sleep(1)
  524. if not s.isconnected() and self._ssid is not None and self._wifi_pw is not None:
  525. s.connect(self._ssid, self._wifi_pw)
  526. while (
  527. s.status() == network.STAT_CONNECTING
  528. ): # Break out on fail or success. Check once per sec.
  529. await asyncio.sleep(1)
  530. else:
  531. s.active(True)
  532. if RP2: # Disable auto-sleep.
  533. # https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
  534. # para 3.6.3
  535. s.config(pm=0xA11140)
  536. s.connect(self._ssid, self._wifi_pw)
  537. for _ in range(60): # Break out on fail or success. Check once per sec.
  538. await asyncio.sleep(1)
  539. # Loop while connecting or no IP
  540. if s.isconnected():
  541. break
  542. if ESP32:
  543. # Status values >= STAT_IDLE can occur during connect:
  544. # STAT_IDLE 1000, STAT_CONNECTING 1001, STAT_GOT_IP 1010
  545. if s.status() < network.STAT_IDLE: # Error statuses
  546. break # are in range 200..204
  547. elif PYBOARD: # No symbolic constants in network
  548. if not 1 <= s.status() <= 2:
  549. break
  550. elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?)
  551. if not 1 <= s.status() <= 2:
  552. break
  553. else: # Timeout: still in connecting state
  554. s.disconnect()
  555. await asyncio.sleep(1)
  556. if not s.isconnected(): # Timed out
  557. raise OSError("Wi-Fi connect timed out")
  558. if not quick: # Skip on first connection only if power saving
  559. # Ensure connection stays up for a few secs.
  560. self.dprint("Checking WiFi integrity.")
  561. for _ in range(5):
  562. if not s.isconnected():
  563. raise OSError("Connection Unstable") # in 1st 5 secs
  564. await asyncio.sleep(1)
  565. self.dprint("Got reliable connection")
  566. async def connect(self, *, quick=False): # Quick initial connect option for battery apps
  567. if not self._has_connected:
  568. await self.wifi_connect(quick) # On 1st call, caller handles error
  569. # Note this blocks if DNS lookup occurs. Do it once to prevent
  570. # blocking during later internet outage:
  571. self._addr = socket.getaddrinfo(self.server, self.port)[0][-1]
  572. self._in_connect = True # Disable low level ._isconnected check
  573. try:
  574. if not self._has_connected and self._clean_init and not self._clean:
  575. # Power up. Clear previous session data but subsequently save it.
  576. # Issue #40
  577. await self._connect(True) # Connect with clean session
  578. try:
  579. async with self.lock:
  580. self._sock.write(b"\xe0\0") # Force disconnect but keep socket open
  581. except OSError:
  582. pass
  583. self.dprint("Waiting for disconnect")
  584. await asyncio.sleep(2) # Wait for broker to disconnect
  585. self.dprint("About to reconnect with unclean session.")
  586. await self._connect(self._clean)
  587. except Exception:
  588. self._close()
  589. self._in_connect = False # Caller may run .isconnected()
  590. raise
  591. self.rcv_pids.clear()
  592. # If we get here without error broker/LAN must be up.
  593. self._isconnected = True
  594. self._in_connect = False # Low level code can now check connectivity.
  595. if not self._events:
  596. asyncio.create_task(self._wifi_handler(True)) # User handler.
  597. if not self._has_connected:
  598. self._has_connected = True # Use normal clean flag on reconnect.
  599. asyncio.create_task(self._keep_connected())
  600. # Runs forever unless user issues .disconnect()
  601. asyncio.create_task(self._handle_msg()) # Task quits on connection fail.
  602. self._tasks.append(asyncio.create_task(self._keep_alive()))
  603. if self.DEBUG:
  604. self._tasks.append(asyncio.create_task(self._memory()))
  605. if self._events:
  606. self.up.set() # Connectivity is up
  607. else:
  608. asyncio.create_task(self._connect_handler(self)) # User handler.
  609. # Launched by .connect(). Runs until connectivity fails. Checks for and
  610. # handles incoming messages.
  611. async def _handle_msg(self):
  612. try:
  613. while self.isconnected():
  614. async with self.lock:
  615. await self.wait_msg() # Immediate return if no message
  616. await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock
  617. except OSError:
  618. pass
  619. self._reconnect() # Broker or WiFi fail.
  620. # Keep broker alive MQTT spec 3.1.2.10 Keep Alive.
  621. # Runs until ping failure or no response in keepalive period.
  622. async def _keep_alive(self):
  623. while self.isconnected():
  624. pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval
  625. if pings_due >= 4:
  626. self.dprint("Reconnect: broker fail.")
  627. break
  628. await asyncio.sleep_ms(self._ping_interval)
  629. try:
  630. await self._ping()
  631. except OSError:
  632. break
  633. self._reconnect() # Broker or WiFi fail.
  634. async def _kill_tasks(self, kill_skt): # Cancel running tasks
  635. for task in self._tasks:
  636. task.cancel()
  637. self._tasks.clear()
  638. await asyncio.sleep_ms(0) # Ensure cancellation complete
  639. if kill_skt: # Close socket
  640. self._close()
  641. # DEBUG: show RAM messages.
  642. async def _memory(self):
  643. while True:
  644. await asyncio.sleep(20)
  645. gc.collect()
  646. self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc())
  647. def isconnected(self):
  648. if self._in_connect: # Disable low-level check during .connect()
  649. return True
  650. if self._isconnected and not self._sta_if.isconnected(): # It's going down.
  651. self._reconnect()
  652. return self._isconnected
  653. def _reconnect(self): # Schedule a reconnection if not underway.
  654. if self._isconnected:
  655. self._isconnected = False
  656. asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket
  657. if self._events: # Signal an outage
  658. self.down.set()
  659. else:
  660. asyncio.create_task(self._wifi_handler(False)) # User handler.
  661. # Await broker connection.
  662. async def _connection(self):
  663. while not self._isconnected:
  664. await asyncio.sleep(1)
  665. # Scheduled on 1st successful connection. Runs forever maintaining wifi and
  666. # broker connection. Must handle conditions at edge of WiFi range.
  667. async def _keep_connected(self):
  668. while self._has_connected:
  669. if self.isconnected(): # Pause for 1 second
  670. await asyncio.sleep(1)
  671. gc.collect()
  672. else: # Link is down, socket is closed, tasks are killed
  673. try:
  674. self._sta_if.disconnect()
  675. except OSError:
  676. self.dprint("Wi-Fi not started, unable to disconnect interface")
  677. await asyncio.sleep(1)
  678. try:
  679. await self.wifi_connect()
  680. except OSError:
  681. continue
  682. if not self._has_connected: # User has issued the terminal .disconnect()
  683. self.dprint("Disconnected, exiting _keep_connected")
  684. break
  685. try:
  686. await self.connect()
  687. # Now has set ._isconnected and scheduled _connect_handler().
  688. self.dprint("Reconnect OK!")
  689. except OSError as e:
  690. self.dprint("Error in reconnect. %s", e)
  691. # Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received.
  692. self._close() # Disconnect and try again.
  693. self._in_connect = False
  694. self._isconnected = False
  695. self.dprint("Disconnected, exited _keep_connected")
  696. async def subscribe(self, topic, qos=0):
  697. qos_check(qos)
  698. while 1:
  699. await self._connection()
  700. try:
  701. return await super().subscribe(topic, qos)
  702. except OSError:
  703. pass
  704. self._reconnect() # Broker or WiFi fail.
  705. async def unsubscribe(self, topic):
  706. while 1:
  707. await self._connection()
  708. try:
  709. return await super().unsubscribe(topic)
  710. except OSError:
  711. pass
  712. self._reconnect() # Broker or WiFi fail.
  713. async def publish(self, topic, msg, retain=False, qos=0):
  714. qos_check(qos)
  715. while 1:
  716. await self._connection()
  717. try:
  718. return await super().publish(topic, msg, retain, qos)
  719. except OSError:
  720. pass
  721. self._reconnect() # Broker or WiFi fail.