├── examples └── basic-example.py ├── .gitignore ├── LICENSE ├── README.md └── src └── mqtt.py /examples/basic-example.py: -------------------------------------------------------------------------------- 1 | import micropython 2 | from libs.mqtt import MQTTClient 3 | 4 | 5 | def puback_cb(msg_id): 6 | print('PUBACK ID = %r' % msg_id) 7 | 8 | def suback_cb(msg_id, qos): 9 | print('SUBACK ID = %r, Accepted QOS = %r' % (msg_id, qos)) 10 | 11 | def con_cb(connected): 12 | if connected: 13 | client.subscribe('subscribe/topic') 14 | 15 | def msg_cb(topic, pay): 16 | print('Received %s: %s' % (topic.decode("utf-8"), pay.decode("utf-8"))) 17 | 18 | 19 | client = MQTTClient('192.168.1.1', port=1883) 20 | 21 | client.set_connected_callback(con_cb) 22 | client.set_puback_callback(puback_cb) 23 | client.set_suback_callback(suback_cb) 24 | client.set_message_callback(msg_cb) 25 | 26 | client.connect('my_client_id') 27 | 28 | while True: 29 | if client.isconnected(): 30 | try: 31 | pub_id = client.publish('publish/topic', 'payload', False) 32 | except Exception as e: 33 | print(e) 34 | 35 | utime.sleep_ms(1000) 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, IAconnects Technology Limited 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors may be 17 | used to endorse or promote products derived from this software without specific prior 18 | written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 21 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 23 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 24 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | Portions of this code are based on the micropython-lib umqtt.simple library released under the MIT License - Copyright (c) 2013, 2014 micropython-lib contributors 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micropython-mqtt 2 | Async MQTT client library with auto reconnect for MicroPython devices such as the ESP32 or Pycom devices 3 | 4 | #### Installation 5 | - Download the **mqtt.py** file and add it to your /libs folder 6 | 7 | #### Features 8 | The **MQTTClient** class is a simple lightweight MQTT client for basic MQTT pub / sub functionality. It has the following features: 9 | - Auto reconnect to the broker when the connection is lost 10 | - Supports QOS 0 and 1 for publish and subscribe 11 | - Publish, subscribe and receipt of published messages are all async 12 | - Automatically handles ping requests if no messages are sent for a period of time 13 | 14 | #### Usage 15 | See the **basic-example.py** file in the **examples** folder for a simple example of using the **MQTTClient** class 16 | 17 | ##### `from libs.mqtt import MQTTClient` 18 | Import the MQTTClient class 19 | 20 | ##### `client = MQTTClient('192.168.1.1', port=1883)` 21 | Construct a new instance of the **MQTTClient** class 22 | - Parameter: **server** - the address of the MQTT broker 23 | - Type: string 24 | - Parameter: **port** - the TCP/IP port to connect to 25 | - Type: number 26 | - Optional Parameter: **reconnect_retry_time** - the time in seconds between reconnect retry attempts 27 | - Type: number 28 | - Default: 10 29 | - Optional Parameter: **keep_alive** - the time in seconds of no activity before a ping is sent to the broker 30 | - Type: number 31 | - Default: 15 32 | - Optional Parameter: **ssl** - set True to use SSL 33 | - Type: boolean 34 | - Default: False 35 | - Optional Parameter: **ssl_params** - the SSL parameters to use if using SSL 36 | - Type: object 37 | 38 | ##### `set_connected_callback(cb)` 39 | Set the callback method which is fired when the **connected** status changes. This callback is optional 40 | - Parameter: **cb** - the callback method 41 | - Type: method name 42 | - Callback Signature: `def connected_cb(status)` where **status** is True if the client is connected or False if disconnected 43 | 44 | ##### `set_message_callback(cb)` 45 | Set the callback method which is fired when a new MQTT message arrives. This callback is optional 46 | - Parameter: **cb** - the callback method 47 | - Type: method name 48 | - Callback Signature: `def message_cb(topic, payload)` where **topic** is the message topic and **payload** is the message payload 49 | 50 | ##### `set_puback_callback(cb)` 51 | Set the callback method which is fired when a publish command succeeds. This callback is optional 52 | - Parameter: **cb** - the callback method 53 | - Type: method name 54 | - Callback Signature: `def puback_cb(msg_id)` where **msg_id** is a message ID. This can be matched with a message ID returned from the **publish** method to confirm that a publish has succeeded 55 | 56 | ##### `set_suback_callback(cb)` 57 | Set the callback method which is fired when a subscribe command succeeds. This callback is optional 58 | - Parameter: **cb** - the callback method 59 | - Type: method name 60 | - Callback Signature: `def suback_cb(msg_id)` where **msg_id** is a message ID. This can be matched with a message ID returned from the **subscribe** method to confirm that a subscribe has succeeded 61 | 62 | ##### `set_unsuback_callback(cb)` 63 | Set the callback method which is fired when an ubsubscribe command succeeds. This callback is optional 64 | - Parameter: **cb** - the callback method 65 | - Type: method name 66 | - Callback Signature: `def unsuback_cb(msg_id)` where **msg_id** is a message ID. This can be matched with a message ID returned from the **unsubscribe** method to confirm that an unsubscribe has succeeded 67 | 68 | ##### `connect(client_id, user=None, password=None, clean_session=True, will_topic=None, will_qos=0, will_retain=False, will_payload=None)` 69 | Connect the MQTT client to the broker. This method is non-blocking and returns before the connection has completed. 70 | - Parameter: **client_id** - the ID of this MQTT client 71 | - Type: string 72 | - Optional Parameter: **user** - the username used to authenticate with the broker 73 | - Type: string 74 | - Default: None 75 | - Optional Parameter: **password** - the password used to authenticate with the broker 76 | - Type: string 77 | - Default: None 78 | - Optional Parameter: **clean_session** - set true to start a clean session with the broker 79 | - Type: boolean 80 | - Default: True 81 | - Optional Parameter: **will_topic** - the last will and testament topic. 82 | - Type: string 83 | - Default: None 84 | - Optional Parameter: **will_qos** - the last will and testament QOS level. QOS values of 0, 1 and 2 are valid 85 | - Type: number 86 | - Default: 0 87 | - Optional Parameter: **will_retain** - the last will and testament message retain flag 88 | - Type: boolean 89 | - Default: False 90 | - Optional Parameter: **will_payload** - the last will and testament message payload 91 | - Type: string 92 | - Default: None 93 | 94 | ##### `disconnect()` 95 | Disconnect the MQTT client from the broker 96 | 97 | ##### `isconnected()` 98 | Get the status of the connection between the MQTT client and broker 99 | - Returns: True if the client is connected, else False 100 | 101 | ##### `publish(topic, payload, retain=False, qos=0)` 102 | Publish an MQTT message to the broker 103 | - Parameter: **topic** - the message topic 104 | - Type: string 105 | - Parameter: **payload** - the message payload 106 | - Type: string 107 | - Optional Parameter: **retain** - the message retain flag 108 | - Type: boolean 109 | - Default: False 110 | - Optional Parameter: **qos** - the message QOS level. QOS values of 0 and 1 are valid. This library does not currently support QOS 2 111 | - Type: number 112 | - Default: 0 113 | - Returns: QOS = 0, None. QOS = 1, the publish message ID. This can be used with the *set_puback_callback* to verify that a message has been published 114 | - Raises: MQTTException if the message cannot be published 115 | 116 | ##### `subscribe(topic, qos=0)` 117 | Subscribe to an MQTT topic 118 | - Parameter: **topic** - the topic to subscribe to. MQTT wildcards can be used 119 | - Type: string 120 | - Optional Parameter: **qos** - the subscription QOS level. QOS values of 0 and 1 are valid. This library does not currently support QOS 2 121 | - Type: boolean 122 | - Default: False 123 | - Returns: the subscribe message ID. This can be used with the *set_suback_callback* to verify that a subscription was successful 124 | - Raises: MQTTException if the subscribe message cannot be sent 125 | 126 | ##### `unsubscribe(topic)` 127 | Unsubscribe from an MQTT topic 128 | - Parameter: **topic** - the topic to unsubscribe from. MQTT wildcards can be used 129 | - Type: string 130 | - Returns: the unsubscribe message ID. This can be used with the *set_unsuback_callback* to verify that an unsubscription was successful 131 | - Raises: MQTTException if the unsubscribe message cannot be sent -------------------------------------------------------------------------------- /src/mqtt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, IAconnects Technology Limited 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without modification, 5 | # are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation and/or 12 | # other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be 15 | # used to endorse or promote products derived from this software without specific prior 16 | # written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 20 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 21 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 22 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | # Portions of this code are based on the micropython-lib umqtt.simple library released under the MIT License 28 | # Copyright (c) 2013, 2014 micropython-lib contributors 29 | 30 | from micropython import const 31 | import usocket as socket 32 | import ustruct as struct 33 | from ubinascii import hexlify 34 | import _thread 35 | import utime 36 | 37 | TRACE = const(1) 38 | DEBUG = const(2) 39 | INFO = const(3) 40 | WARN = const(4) 41 | ERROR = const(5) 42 | DEBUG_LEVEL = const(DEBUG) 43 | 44 | class MQTTException(Exception): 45 | pass 46 | 47 | class MQTTClient: 48 | 49 | def __init__(self, server, port, reconnect_retry_time=10, keep_alive=5, ssl=False, ssl_params={}): 50 | self._client_id = None 51 | self._sock = None 52 | self._addr = socket.getaddrinfo(server, port)[0][-1] 53 | self._ssl = ssl 54 | self._ssl_params = ssl_params 55 | self._pub_id = 0 56 | self._user = None 57 | self._pswd = None 58 | self._keep_alive = keep_alive 59 | self._clean_session = True 60 | self._lw_topic = None 61 | self._lw_msg = None 62 | self._lw_qos = 0 63 | self._lw_retain = False 64 | self._message_callback = None 65 | self._connected_callback = None 66 | self._puback_callback = None 67 | self._suback_callback = None 68 | self._unsuback_callback = None 69 | self._isconnected = False 70 | self._reconnect_retry_time = reconnect_retry_time 71 | self._last_msg_sent_time = 0 72 | self._connect_thread_running = False 73 | self._read_thread_running = False 74 | 75 | def __del__(): 76 | self._connect_thread_running = False 77 | self._read_thread_running = False 78 | 79 | def set_connected_callback(self, cb): 80 | self._connected_callback = cb 81 | 82 | def set_message_callback(self, cb): 83 | self._message_callback = cb 84 | 85 | def set_puback_callback(self, cb): 86 | self._puback_callback = cb 87 | 88 | def set_suback_callback(self, cb): 89 | self._suback_callback = cb 90 | 91 | def set_unsuback_callback(self, cb): 92 | self._unsuback_callback = cb 93 | 94 | def connect(self, client_id, user=None, password=None, clean_session=True, will_topic=None, will_qos=0, will_retain=False, will_payload=None): 95 | self._client_id = client_id 96 | self._user = user 97 | self._pswd = password 98 | self._clean_session = clean_session 99 | self._lw_topic = will_topic 100 | self._lw_qos = will_qos 101 | self._lw_retain = will_retain 102 | self._lw_msg = will_payload 103 | self._connect_thread_running = True 104 | _thread.start_new_thread(self._connect_loop, ()) 105 | 106 | def disconnect(self): 107 | self._connect_thread_running = False 108 | if self._sock is not None: 109 | try: 110 | self._sock.write(b"\xe0\0") 111 | self._destroy_socket() 112 | except Exception as e: 113 | self._log(DEBUG, 'Exception caught closing MQTT socket', e) 114 | self._log(INFO, 'MQTT socket is disconnecting') 115 | 116 | def isconnected(self): 117 | return self._isconnected 118 | 119 | def publish(self, topic, payload, retain=False, qos=0): 120 | if qos < 0 or qos > 1: 121 | raise MQTTException('QOS must be 0 or 1') 122 | try: 123 | pkt = bytearray(b"\x30\0\0\0") 124 | pkt[0] |= qos << 1 | retain 125 | sz = 2 + len(topic) + len(payload) 126 | if qos > 0: 127 | sz += 2 128 | assert sz < 2097152 129 | i = 1 130 | while sz > 0x7f: 131 | pkt[i] = (sz & 0x7f) | 0x80 132 | sz >>= 7 133 | i += 1 134 | pkt[i] = sz 135 | assert self._send_packet(pkt, i + 1) 136 | assert self._send_str(topic) 137 | if qos > 0: 138 | self._pub_id += 1 139 | struct.pack_into("!H", pkt, 0, self._pub_id) 140 | assert self._send_packet(pkt, 2) 141 | assert self._send_packet(payload) 142 | return self._pub_id 143 | except Exception as e: 144 | self._log(ERROR, 'Exception caught publishing MQTT message', e) 145 | self._destroy_socket() 146 | raise MQTTException('Failed to publish message to topic %s' % topic) 147 | 148 | def subscribe(self, topic, qos=0): 149 | if qos < 0 or qos > 1: 150 | raise MQTTException('QOS must be 0 or 1') 151 | try: 152 | pkt = bytearray(b"\x82\0\0\0") 153 | self._pub_id += 1 154 | struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pub_id) 155 | assert self._send_packet(pkt) 156 | assert self._send_str(topic) 157 | assert self._send_packet(qos.to_bytes(1, 'little')) 158 | return self._pub_id 159 | except Exception as e: 160 | self._log(ERROR, 'Exception caught subscribing to MQTT topic', e) 161 | self._destroy_socket() 162 | raise MQTTException('Failed to subscribe to topic %s' % topic) 163 | 164 | def unsubscribe(self, topic): 165 | try: 166 | pkt = bytearray(b"\xA2\0\0\0") 167 | self._pub_id += 1 168 | struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), self._pub_id) 169 | assert self._send_packet(pkt) 170 | assert self._send_str(topic) 171 | return self._pub_id 172 | except Exception as e: 173 | self._log(ERROR, 'Exception caught unsubscribing from MQTT topic', e) 174 | self._destroy_socket() 175 | raise MQTTException('Failed to unsubscribe from topic %s' % topic) 176 | 177 | def _connect_loop(self): 178 | last_reconnect_attempt_time = self._reconnect_retry_time * -1 179 | while self._connect_thread_running: 180 | try: 181 | if not self._isconnected: 182 | if utime.time() > last_reconnect_attempt_time + self._reconnect_retry_time: 183 | last_reconnect_attempt_time = utime.time() 184 | self._log(INFO, 'Connecting to MQTT broker....') 185 | self._isconnected = self._reconnect() 186 | assert self._isconnected 187 | if self._connected_callback is not None: 188 | self._connected_callback(self._isconnected) 189 | self._read_thread_running = True 190 | _thread.start_new_thread(self._read_socket_loop, ()) 191 | else: 192 | if utime.time() > self._last_msg_sent_time + self._keep_alive: 193 | assert self._send_ping() 194 | except Exception as e: 195 | self._destroy_socket() 196 | finally: 197 | utime.sleep(1) 198 | 199 | def _log(self, level, message, e=None): 200 | if level >= DEBUG_LEVEL: 201 | if e is None: 202 | print('%s' % (message)) 203 | else: 204 | print('%s: %r' % (message, e)) 205 | 206 | def _reconnect(self): 207 | # create and open a socket and wrap with SSL if required 208 | try: 209 | self._sock = socket.socket() 210 | self._sock.connect(self._addr) 211 | self._sock.setblocking(True) 212 | except OSError as e: 213 | self._log(DEBUG, 'Exception caught opening MQTT socket', e) 214 | return False 215 | try: 216 | if self._ssl: 217 | import ussl 218 | self._sock = ussl.wrap_socket(self._sock, **self._ssl_params) 219 | except OSError as e: 220 | self._log(DEBUG, 'Exception caught wrapping MQTT socket in SSL', e) 221 | return False 222 | try: 223 | # build the connect message 224 | msg = bytearray(b"\x10\0\0\x04MQTT\x04\x02\0\0") 225 | msg[1] = 10 + 2 + len(self._client_id) 226 | msg[9] = self._clean_session << 1 227 | if self._user is not None: 228 | msg[1] += 2 + len(self._user) + 2 + len(self._pswd) 229 | msg[9] |= 0xC0 230 | if self._keep_alive: 231 | msg[10] |= self._keep_alive >> 8 232 | msg[11] |= self._keep_alive & 0x00FF 233 | if self._lw_topic: 234 | msg[1] += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) 235 | msg[9] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 236 | msg[9] |= self._lw_retain << 5 237 | # send the connect packet 238 | assert self._send_packet(msg) 239 | # send the client ID 240 | assert self._send_str(self._client_id) 241 | # send the last will packet 242 | if self._lw_topic: 243 | assert self._send_str(self._lw_topic) 244 | assert self._send_str(self._lw_msg) 245 | if self._user is not None: 246 | assert self._send_str(self._user) 247 | assert self._send_str(self._pswd) 248 | # check the connection response 249 | resp = self._sock.read(4) 250 | assert resp[0] == 0x20 and resp[1] == 0x02 251 | if resp[3] == 1: 252 | self._log(WARN, 'Connection refused, wrong protocol version') 253 | elif resp[3] == 2: 254 | self._log(WARN, 'Connection refused, client ID rejected') 255 | elif resp[3] == 3: 256 | self._log(WARN, 'Connection refused, server unavailable') 257 | elif resp[3] == 4: 258 | self._log(WARN, 'Connection refused, bad username or password') 259 | elif resp[3] == 5: 260 | self._log(WARN, 'Connection refused, not authorised') 261 | assert resp[3] == 0 262 | self._log(INFO, 'Connected to MQTT broker') 263 | return True 264 | except AssertionError as e: 265 | self._log(ERROR, 'Exception caught sending MQTT connection packet', e) 266 | return False 267 | 268 | def _destroy_socket(self): 269 | try: 270 | self._sock.close() 271 | except Exception as e: 272 | self._log(DEBUG, 'Exception caught closing MQTT socket', e) 273 | try: 274 | del self._sock 275 | except Exception as e: 276 | self._log(DEBUG, 'Exception caught destroying MQTT socket', e) 277 | finally: 278 | self._read_thread_running = False 279 | was_connected = self._isconnected 280 | self._isconnected = False 281 | if was_connected: 282 | self._log(INFO, 'MQTT socket destroyed') 283 | if self._connected_callback is not None: 284 | self._connected_callback(self._isconnected) 285 | 286 | def _send_packet(self, packet, l=0): 287 | try: 288 | if l == 0: 289 | self._sock.write(packet) 290 | else: 291 | self._sock.write(packet, l) 292 | self._last_msg_sent_time = utime.time() 293 | return True 294 | except Exception as e: 295 | self._log(ERROR, 'Exception caught sending MQTT packet', e) 296 | return False 297 | 298 | def _send_str(self, s): 299 | if self._send_packet(struct.pack("!H", len(s))): 300 | return self._send_packet(s) 301 | else: 302 | return False 303 | 304 | def _send_ping(self): 305 | self._log(TRACE, 'Sending ping message') 306 | return self._send_packet(b"\xc0\0") 307 | 308 | def _recv_len(self, start): 309 | if not start & 0x80: 310 | return start 311 | n = start 312 | sh = 0 313 | while 1: 314 | b = self._sock.read(1)[0] 315 | n |= (b & 0x7f) << sh 316 | assert n < 0x7fffffff 317 | if not b & 0x80: 318 | return n 319 | sh += 7 320 | 321 | def _read_socket_loop(self): 322 | self._sock.setblocking(True) 323 | while self._read_thread_running: 324 | try: 325 | hdr = self._sock.read(2) 326 | if hdr != None and hdr != b'': 327 | if hdr[0] & 0xf0 != 0x30: 328 | if hdr[0] == 0xd0 and hdr[1] == 0x00: # PING response 329 | self._log(TRACE, "Received MQTT ping response") 330 | if hdr[0] == 0x40 and hdr[1] == 0x02: # PUBACK response 331 | msg_data = self._sock.read(2) 332 | if self._puback_callback is not None: 333 | self._puback_callback(msg_data[0] << 8 | msg_data[1]) 334 | if hdr[0] == 0x90 and hdr[1] == 0x03: # SUBACK response 335 | msg_data = self._sock.read(3) 336 | if self._suback_callback is not None: 337 | self._suback_callback(msg_data[0] << 8 | msg_data[1], msg_data[2]) 338 | if hdr[0] == 0xB0 and hdr[1] == 0x02: # UNSUBACK response 339 | msg_data = self._sock.read(2) 340 | if self._unsuback_callback is not None: 341 | self._unsuback_callback(msg_data[0] << 8 | msg_data[1]) 342 | elif hdr[0] & 0xf0 == 0x30: # PUBLISH message 343 | sz = self._recv_len(hdr[1]) 344 | topic_len = self._sock.read(2) 345 | topic_len = (topic_len[0] << 8) | topic_len[1] 346 | topic = self._sock.read(topic_len) 347 | sz -= topic_len + 2 348 | if hdr[0] & 6: 349 | pid = self._sock.read(2) 350 | pid = pid[0] << 8 | pid[1] 351 | sz -= 2 352 | pay = self._sock.read(sz) 353 | if self._message_callback is not None: 354 | self._message_callback(topic, pay) 355 | if hdr[0] & 6 == 2: 356 | pkt = bytearray(b"\x40\x02\0\0") 357 | struct.pack_into("!H", pkt, 2, pid) 358 | self._send_packet(pkt) 359 | except Exception as e: 360 | self._log(DEBUG, 'Exception caught processing received message', e) 361 | 362 | utime.sleep_ms(50) 363 | --------------------------------------------------------------------------------