├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── README.rst ├── compile.sh ├── example_sub_robust.py ├── setup.py ├── src └── umqtt │ ├── __init__.py │ └── robust2.py ├── src_minimized └── umqtt │ ├── __init__.py │ └── robust2.py └── tests.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | For example a piece of code that can be pasted into a micropython console. 16 | 17 | **Details (please complete the following information):** 18 | - Device board: [e.g. ESP32] 19 | - Micropython version: [e.g. 1.12, name or URL fork] 20 | - Library version [e.g. 0.1] 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | build/ 10 | build_app/ 11 | build_port/ 12 | develop-eggs/ 13 | dist/ 14 | eggs/ 15 | sdist/ 16 | tmp/ 17 | *.egg-info/ 18 | .installed.cfg 19 | *.egg 20 | .eggs 21 | __dist/ 22 | .idea/ 23 | MANIFEST 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | ## generic files to ignore 30 | *~ 31 | *.lock 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Wojciech Banaś 4 | Copyright (c) 2014-2018 Paul Sokolovsky 5 | Copyright (c) 2013-2018 micropython-lib contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. role:: bash(code) 2 | :language: bash 3 | 4 | .. role:: python(code) 5 | :language: python 6 | 7 | umqtt.robust2 8 | ============= 9 | 10 | umqtt.robust2 is a MQTT client for MicroPython. (Note that it uses some 11 | MicroPython shortcuts and doesn't work with CPython). It consists of 12 | two submodules: umqtt.simple2_ and umqtt.robust2_. umqtt.robust2_ is built 13 | on top of umqtt.simple2_. 14 | 15 | Features of this library 16 | ------------------------ 17 | * allows you to resume connection with the MQTT broker 18 | * allows you to send unsent messages when you resume a connection 19 | * allows you to subscribe to all previously subscribed topics once the connection is resumed 20 | * is resistant to errors in connections 21 | * you have clear information about the type of errors 22 | 23 | Differences between umqtt.robust and umqtt.robust2 24 | -------------------------------------------------- 25 | 26 | * allows for easy programming of different strategies to maintain communication with the MQTT broker 27 | * works without blocking app 28 | * in case of network problems, it can send the data itself at a later time 29 | * we have the ability to track down errors 30 | * is larger than the previous one, so I recommend compiling this 31 | library to MPY files (especially for esp8266) 32 | 33 | Problems and solutions 34 | ---------------------- 35 | * ImportError: no module named 'umqtt.robust2' 36 | 37 | Versions of micropython from http://micropython.org/download/ since version 1.12 include 38 | the umqtt library, which conflicts with the current library. 39 | To avoid conflicts, you need to change the order of importing libraries. 40 | You need to import the '/lib' libraries first and then the system libraries. 41 | Just add the following lines of code to the boot.py file: 42 | 43 | .. code-block:: python 44 | 45 | import sys 46 | sys.path.reverse() 47 | 48 | How and where to install this code? 49 | ----------------------------------- 50 | This library requires the umqtt.simple2_ library. 51 | Therefore, please read this required library first, 52 | and then you can install this one. 53 | 54 | You can install using the upip: 55 | 56 | .. code-block:: python 57 | 58 | import upip 59 | upip.install("micropython-umqtt.robust2") 60 | 61 | or 62 | 63 | .. code-block:: bash 64 | 65 | micropython -m upip install -p modules micropython-umqtt.robust2 66 | 67 | 68 | You can also clone this repository, and install it manually: 69 | 70 | .. code-block:: bash 71 | 72 | git clone https://github.com/fizista/micropython-umqtt.robust2.git 73 | 74 | Manual installation gives you more possibilities: 75 | 76 | * You can compile this library into MPY files using the :bash:`compile.sh` script. 77 | * You can remove comments from the code with the command: :bash:`python setup.py minify` 78 | * You can of course copy the code as it is, if you don't mind. 79 | 80 | **Please note that the PyPi repositories contain optimized code (no comments).** 81 | 82 | **For more detailed information about API please see the source code 83 | (which is quite short and easy to review) and provided examples.** 84 | 85 | What does it mean to be "robust" ? 86 | ---------------------------------- 87 | 88 | Modern computing systems are sufficiently complex and have multiple 89 | points of failure. Consider for example that nothing will work if 90 | there's no power (mains outage or battery ran out). As you may imagine, 91 | umqtt.robust2 won't help you with your flat battery. Most computing 92 | systems are now networked, and communication is another weak link. 93 | This is especially true for wireless communications. If two of your 94 | systems can't connect reliably communicate via WiFi, umqtt.robust2 95 | can't magically resolve that (but it may help with intermittent 96 | WiFi issues). 97 | 98 | What umqtt.robust2 tries to do is very simple - if while trying to 99 | perform some operation, it detects that connection to MQTT breaks, 100 | it tries to reconnect to it. That's good direction towards "robustness", 101 | but the problem that there is no single definition of what "robust" 102 | is. Let's consider following usecase: 103 | 104 | 1. A temperature reading gets transmitted once a minute. Then the 105 | best option in case of a transmission error might be not doing 106 | anything at all - in a minute, another reading will be transmitted, 107 | and for slowly-changing parameter like a temperature, a one-minute 108 | lost reading is not a problem. Actually, if the sending device is 109 | battery-powered, any connection retries will just drain battery and 110 | make device "less robust" (it will run out of juice sooner and more 111 | unexpectedly, which may be a criteria for "robustness"). 112 | 113 | We can also cache some of the results, as far as memory allows, 114 | until we try to connect again. This will increase the reliability 115 | of data delivery. 116 | 117 | 2. If there's a button, which communicates its press event, then 118 | perhaps it's really worth to retry to deliver this event (a user 119 | expects something to happen when they press the button, right?). 120 | But if a button is battery-power, unconstrained retries won't do 121 | much good still. Consider mains power outage for several hours, 122 | MQTT server down all this time, and battery-powered button trying 123 | to re-publish event every second. It will likely drain battery 124 | during this time, which is very non-robust. Perhaps, if a press 125 | isn't delivered in 15 seconds, it's no longer relevant (depending 126 | on what press does, the above may be good for a button turning 127 | on lights, but not for something else!) 128 | 129 | 3. Finally, let's consider security sensors, like a window broken 130 | sensor. That's the hardest case. Apparently, those events are 131 | important enough to be delivered no matter what. But if done with 132 | short, dumb retries, it will only lead to quick battery drain. So, 133 | a robust device would retry, but in smart manner, to let battery 134 | run for as long as possible, to maximize the chance of the message 135 | being delivered. 136 | 137 | Let's sum it up: 138 | 139 | a) There's no single definition of what "robust" is. It depends on 140 | a particular application. 141 | b) Robustness is a complex measure, it doesn't depend on one single 142 | feature, but rather many different features working together. 143 | Consider for example that to make button from the case 2 above 144 | work better, it would help to add a visual feedback, so a user 145 | knew what happens. 146 | 147 | As you may imagine, umqtt.robust2 doesn't, and can't, cover all possible 148 | "robustness" scenarios, nor it alone can make your MQTT application 149 | "robust". Rather, it's a barebones example of how to reconnect to an 150 | MQTT server in case of a connection error. As such, it's just one 151 | of many steps required to make your app robust, and majority of those 152 | steps lie on *your application* side. With that in mind, any realistic 153 | application would subclass umqtt.robust2.MQTTClient class and override 154 | add_msg_to_send() and reconnect() methods and will use the 155 | socket_timeout/message_timeout parameters to suit particular usage scenario. 156 | It may even happen that umqtt.robust2 won't even suit your needs, and you 157 | will need to implement your "robust" handling from scratch. 158 | 159 | 160 | Persistent and non-persistent MQTT servers 161 | ------------------------------------------ 162 | 163 | Consider an example: you subscribed to some MQTT topics, then connection 164 | went down. If we talk "robust", then once you reconnect, you want any 165 | messages which arrived when the connection was down, to be still delivered 166 | to you. That requires retainment and persistency enabled on MQTT server. 167 | As umqtt.robust2 tries to achieve as much "robustness" as possible, it 168 | makes a requirement that the MQTT server it communicates to has persistency 169 | enabled. This include persistent sessions, meaning that any client 170 | subscriptions are retained across disconnect, and if you subscribed once, 171 | you no longer need to resubscribe again on next connection(s). This makes 172 | it more robust, minimizing amount of traffic to transfer on each connection 173 | (the more you transfer, the higher probability of error), and also saves 174 | battery power. 175 | 176 | However, not all broker offer true, persistent MQTT support: 177 | 178 | * If you use self-hosted broker, you may need to configure it for 179 | persistency. E.g., a popular open-source broker Mosquitto requires 180 | following line:: 181 | 182 | persistence true 183 | 184 | to be added to ``mosquitto.conf``. Please consult documentation of 185 | your broker. 186 | 187 | * Many so-called "cloud providers" offer very limited subset of MQTT for 188 | their free/inexpensive tiers. Persistence and QoS are features usually 189 | not supported. It's hard to achieve any true robustness with these 190 | demo-like offerings, and umqtt.robust2 isn't designed to work with them. 191 | 192 | 193 | .. _umqtt.simple2: https://github.com/fizista/micropython-umqtt.simple2 194 | .. _umqtt.robust2: https://github.com/fizista/micropython-umqtt.robust2 -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OUTPUT=$(realpath ./build_port) 4 | 5 | if [ -d "$OUTPUT" ]; then 6 | rm -r "$OUTPUT" 7 | fi 8 | 9 | mkdir "$OUTPUT" 10 | 11 | function compile() { 12 | FILE_PATH="$1" 13 | 14 | file_name=$(basename $FILE_PATH) 15 | file_name="${file_name%.*}.mpy" 16 | file_dir=$(dirname $FILE_PATH) 17 | 18 | 19 | out_dir="${OUTPUT}/${file_dir}" 20 | out_path="$out_dir/$file_name" 21 | mp_path="${FILE_PATH%.*}.mpy" 22 | mp_path="${FILE_PATH:2}" 23 | 24 | if [ ! -d "$out_dir" ]; then 25 | mkdir "$out_dir" 26 | fi 27 | 28 | echo "Compile $FILE_PATH => $out_path" 29 | 30 | MP_FILE_PATH="${FILE_PATH%.*}.mpy" 31 | 32 | #mpy-cross -mno-unicode -o "$out_path" -s "$mp_path" "$FILE_PATH" 33 | #mpy-cross -march=xtensa -X emit=native "$FILE_PATH" &>/dev/null || mpy-cross -march=xtensa "$FILE_PATH" 34 | mpy-cross "$FILE_PATH" 35 | mv "$MP_FILE_PATH" "$out_dir" 36 | } 37 | 38 | cd ./src 39 | for file_path in ./umqtt/*.py 40 | do 41 | compile "$file_path" 42 | done 43 | 44 | cd .. 45 | compile "./tests.py" 46 | -------------------------------------------------------------------------------- /example_sub_robust.py: -------------------------------------------------------------------------------- 1 | import utime 2 | from umqtt.robust2 import MQTTClient 3 | 4 | 5 | def sub_cb(topic, msg, retained, duplicate): 6 | print((topic, msg, retained, duplicate)) 7 | 8 | 9 | c = MQTTClient("umqtt_client", "localhost") 10 | # Print diagnostic messages when retries/reconnects happens 11 | c.DEBUG = True 12 | # Information whether we store unsent messages with the flag QoS==0 in the queue. 13 | c.KEEP_QOS0 = False 14 | # Option, limits the possibility of only one unique message being queued. 15 | c.NO_QUEUE_DUPS = True 16 | # Limit the number of unsent messages in the queue. 17 | c.MSG_QUEUE_MAX = 2 18 | 19 | last_will_topic = '/lw/topic' 20 | 21 | c.set_last_will(last_will_topic, 'Disconnected', retain=True) 22 | 23 | c.set_callback(sub_cb) 24 | # Connect to server, requesting not to clean session for this 25 | # client. If there was no existing session (False return value 26 | # from connect() method), we perform the initial setup of client 27 | # session - subscribe to needed topics. Afterwards, these 28 | # subscriptions will be stored server-side, and will be persistent, 29 | # (as we use clean_session=False). 30 | # 31 | # TODO: Still exists??? 32 | # There can be a problem when a session for a given client exists, 33 | # but doesn't have subscriptions a particular application expects. 34 | # In this case, a session needs to be cleaned first. See 35 | # example_reset_session.py for an obvious way how to do that. 36 | # 37 | # In an actual application, it's up to its developer how to 38 | # manage these issues. One extreme is to have external "provisioning" 39 | # phase, where initial session setup, and any further management of 40 | # a session, is done by external tools. This allows to save resources 41 | # on a small embedded device. Another extreme is to have an application 42 | # to perform auto-setup (e.g., clean session, then re-create session 43 | # on each restart). This example shows mid-line between these 2 44 | # approaches, where initial setup of session is done by application, 45 | # but if anything goes wrong, there's an external tool to clean session. 46 | if not c.connect(clean_session=False): 47 | print("New session being set up") 48 | c.subscribe(b"foo_topic") 49 | 50 | while 1: 51 | utime.sleep_ms(500) 52 | 53 | # At this point in the code you must consider how to handle 54 | # connection errors. And how often to resume the connection. 55 | if c.is_conn_issue(): 56 | while c.is_conn_issue(): 57 | # If the connection is successful, the is_conn_issue 58 | # method will not return a connection error. 59 | c.reconnect() 60 | c.publish(last_will_topic, 'Connected', retain=True) 61 | else: 62 | c.resubscribe() 63 | 64 | c.publish('/hello/topic', 'online', qos=1) 65 | 66 | # WARNING!!! 67 | # The below functions should be run as often as possible. 68 | # There may be a problem with the connection. (MQTTException(7,), 9) 69 | # In the following way, we clear the queue. 70 | for _ in range(500): 71 | c.check_msg() # needed when publish(qos=1), ping(), subscribe() 72 | c.send_queue() # needed when using the caching capabilities for unsent messages 73 | if not c.things_to_do(): 74 | break 75 | utime.sleep_ms(1) 76 | 77 | c.disconnect() 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | from pathlib import Path 5 | from filecmp import dircmp 6 | from glob import glob 7 | from os.path import basename, splitext, join, dirname 8 | 9 | __dir__ = Path(__file__).absolute().parent 10 | # Remove current dir from sys.path, otherwise setuptools will peek up our 11 | # module instead of system's. 12 | sys.path.pop(0) 13 | import setuptools 14 | 15 | sys.path.append("..") 16 | import sdist_upip 17 | 18 | 19 | def read(file_relative): 20 | file = __dir__ / file_relative 21 | with open(str(file)) as f: 22 | return f.read() 23 | 24 | 25 | MINIFIED_DIR = Path('build_app') 26 | 27 | 28 | class PythonMinifier(setuptools.Command): 29 | """A custom command to run Python Minifier""" 30 | 31 | description = 'run Python Minifier on Python source files' 32 | user_options = [ 33 | ('minified-dir=', 'm', 'Path where the result is to be saved.'), 34 | ] 35 | 36 | def initialize_options(self): 37 | """Set default values for options.""" 38 | self.minified_dir = str(MINIFIED_DIR) 39 | 40 | def finalize_options(self): 41 | """Post-process options.""" 42 | if self.minified_dir == str(MINIFIED_DIR): 43 | if not os.path.exists(str(MINIFIED_DIR)): 44 | os.mkdir(str(MINIFIED_DIR)) 45 | 46 | if not os.path.isdir(self.minified_dir): 47 | raise Exception("Directory does not exist: {0}".format(self.minified_dir)) 48 | 49 | if os.path.exists(str(Path(self.minified_dir) / 'umqtt')): 50 | shutil.rmtree(str(Path(self.minified_dir) / 'umqtt')) 51 | os.makedirs(str(Path(self.minified_dir) / 'umqtt'), exist_ok=True) 52 | print('OUT directory: ', self.minified_dir) 53 | 54 | def run(self): 55 | """Run command.""" 56 | 57 | import python_minifier 58 | 59 | for file in os.listdir(str(__dir__ / 'src' / 'umqtt')): 60 | with open(str(__dir__ / 'src' / 'umqtt' / file)) as f: 61 | print('Minify: %s' % file) 62 | source = f.read() 63 | filename = file.split('/')[-1] 64 | out = python_minifier.minify( 65 | source, 66 | filename, 67 | remove_annotations=True, 68 | remove_pass=True, 69 | remove_literal_statements=True, 70 | combine_imports=False, 71 | hoist_literals=False, 72 | rename_locals=True, 73 | preserve_locals=None, 74 | rename_globals=False, 75 | preserve_globals=None, 76 | remove_object_base=False, 77 | convert_posargs_to_args=False 78 | ) 79 | with open(str(Path(self.minified_dir) / 'umqtt' / file), 'w') as f: 80 | f.write(out) 81 | 82 | 83 | from distutils import dir_util, dep_util, file_util, archive_util 84 | from distutils import log 85 | 86 | 87 | class SDistCommand(sdist_upip.sdist): 88 | """Custom build command.""" 89 | 90 | def make_release_tree(self, base_dir, files): 91 | """Create the directory tree that will become the source 92 | distribution archive. All directories implied by the filenames in 93 | 'files' are created under 'base_dir', and then we hard link or copy 94 | (if hard linking is unavailable) those files into place. 95 | Essentially, this duplicates the developer's source tree, but in a 96 | directory named after the distribution, containing only the files 97 | to be distributed. 98 | """ 99 | # Create all the directories under 'base_dir' necessary to 100 | # put 'files' there; the 'mkpath()' is just so we don't die 101 | # if the manifest happens to be empty. 102 | self.mkpath(base_dir) 103 | files_tree_fix = [f.replace(str(MINIFIED_DIR) + '/', '') for f in files] 104 | dir_util.create_tree(base_dir, files_tree_fix, dry_run=self.dry_run) 105 | 106 | # And walk over the list of files, either making a hard link (if 107 | # os.link exists) to each one that doesn't already exist in its 108 | # corresponding location under 'base_dir', or copying each file 109 | # that's out-of-date in 'base_dir'. (Usually, all files will be 110 | # out-of-date, because by default we blow away 'base_dir' when 111 | # we're done making the distribution archives.) 112 | 113 | if hasattr(os, 'link'): # can make hard links on this system 114 | link = 'hard' 115 | msg = "making hard links in %s..." % base_dir 116 | else: # nope, have to copy 117 | link = None 118 | msg = "copying files to %s..." % base_dir 119 | 120 | if not files: 121 | log.warn("no files to distribute -- empty manifest?") 122 | else: 123 | log.info(msg) 124 | for file in files: 125 | if not os.path.isfile(file): 126 | log.warn("'%s' not a regular file -- skipping" % file) 127 | else: 128 | file_fix = file.replace(str(MINIFIED_DIR) + '/', '') 129 | dest = os.path.join(base_dir, file_fix) 130 | self.copy_file(file, dest, link=link) 131 | 132 | self.distribution.metadata.write_pkg_info(base_dir) 133 | 134 | def run(self): 135 | self.run_command('minify') 136 | 137 | def print_diff_files(dcmp): 138 | is_diff = False 139 | for name in dcmp.diff_files: 140 | #print("diff_file %s found in %s and %s" % (name, dcmp.left, dcmp.right)) 141 | is_diff = True 142 | for sub_dcmp in dcmp.subdirs.values(): 143 | is_diff = is_diff or print_diff_files(sub_dcmp) 144 | return is_diff 145 | 146 | dcmp = dircmp(str(MINIFIED_DIR), 'src_minimized') 147 | if print_diff_files(dcmp): 148 | raise Exception('There are differences in minimized files. ' 149 | 'Compare %s and %s directories. They must be identical.' % 150 | (str(MINIFIED_DIR), 'src_minimized')) 151 | 152 | super(SDistCommand, self).run() 153 | 154 | 155 | setuptools.setup( 156 | name='micropython-umqtt.robust2', 157 | version='2.2.0', 158 | description='MQTT client for MicroPython ("robust" version).', 159 | long_description=read('README.rst'), 160 | long_description_content_type="text/x-rst", 161 | url='https://github.com/fizista/micropython-umqtt.robust2', 162 | author='Wojciech Banaś', 163 | author_email='fizista@gmail.com', 164 | maintainer='Wojciech Banaś', 165 | maintainer_email='fizista+umqtt.robust2@gmail.com', 166 | license='MIT', 167 | classifiers=[ 168 | 'Programming Language :: Python :: Implementation :: MicroPython', 169 | ], 170 | keywords='mqtt micropython', 171 | cmdclass={'sdist': SDistCommand, 'minify': PythonMinifier}, 172 | setup_requires=['python_minifier'], 173 | packages=setuptools.find_packages('src'), 174 | package_dir={'': str(MINIFIED_DIR)}, 175 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 176 | project_urls={ 177 | 'Bug Reports': 'https://github.com/fizista/micropython-umqtt.robust2/issues', 178 | 'Source': 'https://github.com/fizista/micropython-umqtt.robust2', 179 | }, 180 | ) 181 | -------------------------------------------------------------------------------- /src/umqtt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/micropython-umqtt.robust2/c12c06414e2dcc19aa9d3244429423836bb9bb3b/src/umqtt/__init__.py -------------------------------------------------------------------------------- /src/umqtt/robust2.py: -------------------------------------------------------------------------------- 1 | from utime import ticks_ms, ticks_diff 2 | from . import simple2 3 | 4 | 5 | class MQTTClient(simple2.MQTTClient): 6 | DEBUG = False 7 | 8 | # Information whether we store unsent messages with the flag QoS==0 in the queue. 9 | KEEP_QOS0 = True 10 | # Option, limits the possibility of only one unique message being queued. 11 | NO_QUEUE_DUPS = True 12 | # Limit the number of unsent messages in the queue. 13 | MSG_QUEUE_MAX = 5 14 | # How many PIDs we store for a sent message 15 | CONFIRM_QUEUE_MAX = 10 16 | # When you reconnect, all existing subscriptions are renewed. 17 | RESUBSCRIBE = True 18 | 19 | def __init__(self, *args, **kwargs): 20 | """ 21 | See documentation for `umqtt.simple2.MQTTClient.__init__()` 22 | """ 23 | super().__init__(*args, **kwargs) 24 | self.subs = [] # List of stored subscriptions [ (topic, qos), ...] 25 | # Queue with list of messages to send 26 | self.msg_to_send = [] # [(topic, msg, retain, qos), (topic, msg, retain, qos), ... ] 27 | # Queue with list of subscriptions to send 28 | self.sub_to_send = [] # [(topic, qos), ...] 29 | # Queue with a list of messages waiting for the server to confirm of the message. 30 | self.msg_to_confirm = {} # {(topic, msg, retain, qos): [pid, pid, ...] 31 | # Queue with a subscription list waiting for the server to confirm of the subscription 32 | self.sub_to_confirm = {} # {(topic, qos): [pid, pid, ...]} 33 | self.conn_issue = None # We store here if there is a connection problem. 34 | 35 | def is_keepalive(self): 36 | """ 37 | It checks if the connection is active. If the connection is not active at the specified time, 38 | saves an error message and returns False. 39 | 40 | :return: If the connection is not active at the specified time returns False otherwise True. 41 | """ 42 | time_from__last_cpackage = ticks_diff(ticks_ms(), self.last_cpacket) // 1000 43 | if 0 < self.keepalive < time_from__last_cpackage: 44 | self.conn_issue = (simple2.MQTTException(7), 9) 45 | return False 46 | return True 47 | 48 | def set_callback_status(self, f): 49 | """ 50 | See documentation for `umqtt.simple2.MQTTClient.set_callback_status()` 51 | """ 52 | self._cbstat = f 53 | 54 | def cbstat(self, pid, stat): 55 | """ 56 | Captured message statuses affect the queue here. 57 | 58 | stat == 0 - the message goes back to the message queue to be sent 59 | stat == 1 or 2 - the message is removed from the queue 60 | """ 61 | try: 62 | self._cbstat(pid, stat) 63 | except AttributeError: 64 | pass 65 | 66 | for data, pids in self.msg_to_confirm.items(): 67 | if pid in pids: 68 | if stat == 0: 69 | if data not in self.msg_to_send: 70 | self.msg_to_send.insert(0, data) 71 | pids.remove(pid) 72 | if not pids: 73 | self.msg_to_confirm.pop(data) 74 | elif stat in (1, 2): 75 | # A message has been delivered at least once, so we are not waiting for other confirmations 76 | self.msg_to_confirm.pop(data) 77 | return 78 | 79 | for data, pids in self.sub_to_confirm.items(): 80 | if pid in pids: 81 | if stat == 0: 82 | if data not in self.sub_to_send: 83 | self.sub_to_send.append(data) 84 | pids.remove(pid) 85 | if not pids: 86 | self.sub_to_confirm.pop(data) 87 | elif stat in (1, 2): 88 | # A message has been delivered at least once, so we are not waiting for other confirmations 89 | self.sub_to_confirm.pop(data) 90 | 91 | def connect(self, clean_session=True): 92 | """ 93 | See documentation for `umqtt.simple2.MQTTClient.connect()`. 94 | 95 | If clean_session==True, then the queues are cleared. 96 | 97 | Connection problems are captured and handled by `is_conn_issue()` 98 | """ 99 | if clean_session: 100 | self.msg_to_send[:] = [] 101 | self.msg_to_confirm.clear() 102 | try: 103 | out = super().connect(clean_session) 104 | self.conn_issue = None 105 | return out 106 | except (OSError, simple2.MQTTException) as e: 107 | self.conn_issue = (e, 1) 108 | 109 | def log(self): 110 | if self.DEBUG: 111 | if type(self.conn_issue) is tuple: 112 | conn_issue, issue_place = self.conn_issue 113 | else: 114 | conn_issue = self.conn_issue 115 | issue_place = 0 116 | place_str = ('?', 'connect', 'publish', 'subscribe', 117 | 'reconnect', 'sendqueue', 'disconnect', 'ping', 'wait_msg', 'keepalive', 'check_msg') 118 | print("MQTT (%s): %r" % (place_str[issue_place], conn_issue)) 119 | 120 | def reconnect(self): 121 | """ 122 | The function tries to resume the connection. 123 | 124 | Connection problems are captured and handled by `is_conn_issue()` 125 | """ 126 | out = self.connect(False) 127 | if self.conn_issue: 128 | super().disconnect() 129 | return out 130 | 131 | def resubscribe(self): 132 | """ 133 | Function from previously registered subscriptions, sends them again to the server. 134 | 135 | :return: 136 | """ 137 | for topic, qos in self.subs: 138 | self.subscribe(topic, qos, False) 139 | 140 | def things_to_do(self): 141 | """ 142 | The sum of all actions in the queues. 143 | 144 | When the value equals 0, it means that the library has sent and confirms the sending: 145 | * all messages 146 | * all subscriptions 147 | 148 | When the value equals 0, it means that the device can go into hibernation mode, 149 | assuming that it has not subscribed to some topics. 150 | 151 | :return: 0 (nothing to do) or int (number of things to do) 152 | """ 153 | return len(self.msg_to_send) + \ 154 | len(self.sub_to_send) + \ 155 | sum([len(a) for a in self.msg_to_confirm.values()]) + \ 156 | sum([len(a) for a in self.sub_to_confirm.values()]) 157 | 158 | def add_msg_to_send(self, data): 159 | """ 160 | By overwriting this method, you can control the amount of stored data in the queue. 161 | This is important because we do not have an infinite amount of memory in the devices. 162 | 163 | Currently, this method limits the queue length to MSG_QUEUE_MAX messages. 164 | 165 | The number of active messages is the sum of messages to be sent with messages awaiting confirmation. 166 | 167 | :param data: 168 | :return: 169 | """ 170 | # Before we add data to the queue, it is necessary to release the memory first. 171 | # Otherwise, we may fall into an infinite loop due to a lack of available memory. 172 | messages_count = len(self.msg_to_send) 173 | messages_count += sum(map(len, self.msg_to_confirm.values())) 174 | 175 | while messages_count >= self.MSG_QUEUE_MAX: 176 | min_msg_to_confirm = min(map(lambda x: x[0] if x else 65535, self.msg_to_confirm.values()), default=0) 177 | if 0 < min_msg_to_confirm < 65535: 178 | key_to_check = None 179 | for k, v in self.msg_to_confirm.items(): 180 | if v and v[0] == min_msg_to_confirm: 181 | del v[0] 182 | key_to_check = k 183 | break 184 | if key_to_check and key_to_check in self.msg_to_confirm and not self.msg_to_confirm[key_to_check]: 185 | self.msg_to_confirm.pop(key_to_check) 186 | else: 187 | self.msg_to_send.pop(0) 188 | messages_count -= 1 189 | 190 | self.msg_to_send.append(data) 191 | 192 | def disconnect(self): 193 | """ 194 | See documentation for `umqtt.simple2.MQTTClient.disconnect()` 195 | 196 | Connection problems are captured and handled by `is_conn_issue()` 197 | """ 198 | try: 199 | return super().disconnect() 200 | except (OSError, simple2.MQTTException) as e: 201 | self.conn_issue = (e, 6) 202 | 203 | def ping(self): 204 | """ 205 | See documentation for `umqtt.simple2.MQTTClient.ping()` 206 | 207 | Connection problems are captured and handled by `is_conn_issue()` 208 | """ 209 | if not self.is_keepalive(): 210 | return 211 | try: 212 | return super().ping() 213 | except (OSError, simple2.MQTTException) as e: 214 | self.conn_issue = (e, 7) 215 | 216 | def publish(self, topic, msg, retain=False, qos=0): 217 | """ 218 | See documentation for `umqtt.simple2.MQTTClient.publish()` 219 | 220 | The function tries to send a message. If it fails, the message goes to the message queue for sending. 221 | 222 | The function does not support the `dup` parameter! 223 | 224 | When we have messages with the retain flag set, only one last message with that flag is sent! 225 | 226 | Connection problems are captured and handled by `is_conn_issue()` 227 | 228 | :return: None od PID for QoS==1 (only if the message is sent immediately, otherwise it returns None) 229 | """ 230 | data = (topic, msg, retain, qos) 231 | if retain: 232 | # We delete all previous messages for this topic with the retain flag set to True. 233 | # Only the last message with this flag is relevant. 234 | self.msg_to_send[:] = [m for m in self.msg_to_send if not (topic == m[0] and retain == m[2])] 235 | try: 236 | out = super().publish(topic, msg, retain, qos, False) 237 | if qos == 1: 238 | # We postpone the message in case it is not delivered to the server. 239 | # We will delete it when we receive a receipt. 240 | self.msg_to_confirm.setdefault(data, []).append(out) 241 | if len(self.msg_to_confirm[data]) > self.CONFIRM_QUEUE_MAX: 242 | self.msg_to_confirm.pop(0) 243 | 244 | return out 245 | except (OSError, simple2.MQTTException) as e: 246 | self.conn_issue = (e, 2) 247 | # If the message cannot be sent, we put it in the queue to try to resend it. 248 | if self.NO_QUEUE_DUPS: 249 | if data in self.msg_to_send: 250 | return 251 | if self.KEEP_QOS0 and qos == 0: 252 | self.add_msg_to_send(data) 253 | elif qos == 1: 254 | self.add_msg_to_send(data) 255 | 256 | def subscribe(self, topic, qos=0, resubscribe=True): 257 | """ 258 | See documentation for `umqtt.simple2.MQTTClient.subscribe()` 259 | 260 | The function tries to subscribe to the topic. If it fails, 261 | the topic subscription goes into the subscription queue. 262 | 263 | Connection problems are captured and handled by `is_conn_issue()` 264 | 265 | """ 266 | data = (topic, qos) 267 | 268 | if self.RESUBSCRIBE and resubscribe: 269 | if topic not in dict(self.subs): 270 | self.subs.append(data) 271 | 272 | # We delete all previous subscriptions for the same topic from the queue. 273 | # The most important is the last subscription. 274 | self.sub_to_send[:] = [s for s in self.sub_to_send if topic != s[0]] 275 | try: 276 | out = super().subscribe(topic, qos) 277 | self.sub_to_confirm.setdefault(data, []).append(out) 278 | if len(self.sub_to_confirm[data]) > self.CONFIRM_QUEUE_MAX: 279 | self.sub_to_confirm.pop(0) 280 | return out 281 | except (OSError, simple2.MQTTException) as e: 282 | self.conn_issue = (e, 3) 283 | if self.NO_QUEUE_DUPS: 284 | if data in self.sub_to_send: 285 | return 286 | self.sub_to_send.append(data) 287 | 288 | def send_queue(self): 289 | """ 290 | The function tries to send all messages and subscribe to all topics that are in the queue to send. 291 | 292 | :return: True if the queue's empty. 293 | :rtype: bool 294 | """ 295 | msg_to_del = [] 296 | for data in self.msg_to_send: 297 | topic, msg, retain, qos = data 298 | try: 299 | out = super().publish(topic, msg, retain, qos, False) 300 | if qos == 1: 301 | # We postpone the message in case it is not delivered to the server. 302 | # We will delete it when we receive a receipt. 303 | self.msg_to_confirm.setdefault(data, []).append(out) 304 | msg_to_del.append(data) 305 | except (OSError, simple2.MQTTException) as e: 306 | self.conn_issue = (e, 5) 307 | return False 308 | self.msg_to_send[:] = [m for m in self.msg_to_send if m not in msg_to_del] 309 | del msg_to_del 310 | 311 | sub_to_del = [] 312 | for data in self.sub_to_send: 313 | topic, qos = data 314 | try: 315 | out = super().subscribe(topic, qos) 316 | self.sub_to_confirm.setdefault(data, []).append(out) 317 | sub_to_del.append(data) 318 | except (OSError, simple2.MQTTException) as e: 319 | self.conn_issue = (e, 5) 320 | return False 321 | self.sub_to_send[:] = [s for s in self.sub_to_send if s not in sub_to_del] 322 | 323 | return True 324 | 325 | def is_conn_issue(self): 326 | """ 327 | With this function we can check if there is any connection problem. 328 | 329 | It is best to use this function with the reconnect() method to resume the connection when it is broken. 330 | 331 | You can also check the result of methods such as this: 332 | `connect()`, `publish()`, `subscribe()`, `reconnect()`, `send_queue()`, `disconnect()`, `ping()`, `wait_msg()`, 333 | `check_msg()`, `is_keepalive()`. 334 | 335 | The value of the last error is stored in self.conn_issue. 336 | 337 | :return: Connection problem 338 | :rtype: bool 339 | """ 340 | self.is_keepalive() 341 | 342 | if self.conn_issue: 343 | self.log() 344 | return bool(self.conn_issue) 345 | 346 | def wait_msg(self): 347 | """ 348 | See documentation for `umqtt.simple2.MQTTClient.wait_msg()` 349 | 350 | Connection problems are captured and handled by `is_conn_issue()` 351 | """ 352 | self.is_keepalive() 353 | try: 354 | return super().wait_msg() 355 | except (OSError, simple2.MQTTException) as e: 356 | self.conn_issue = (e, 8) 357 | 358 | def check_msg(self): 359 | """ 360 | See documentation for `umqtt.simple2.MQTTClient.check_msg()` 361 | 362 | Connection problems are captured and handled by `is_conn_issue()` 363 | """ 364 | self.is_keepalive() 365 | try: 366 | return super().check_msg() 367 | except (OSError, simple2.MQTTException) as e: 368 | self.conn_issue = (e, 10) 369 | -------------------------------------------------------------------------------- /src_minimized/umqtt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/micropython-umqtt.robust2/c12c06414e2dcc19aa9d3244429423836bb9bb3b/src_minimized/umqtt/__init__.py -------------------------------------------------------------------------------- /src_minimized/umqtt/robust2.py: -------------------------------------------------------------------------------- 1 | from utime import ticks_ms,ticks_diff 2 | from . import simple2 3 | class MQTTClient(simple2.MQTTClient): 4 | DEBUG=False;KEEP_QOS0=True;NO_QUEUE_DUPS=True;MSG_QUEUE_MAX=5;CONFIRM_QUEUE_MAX=10;RESUBSCRIBE=True 5 | def __init__(A,*B,**C):super().__init__(*B,**C);A.subs=[];A.msg_to_send=[];A.sub_to_send=[];A.msg_to_confirm={};A.sub_to_confirm={};A.conn_issue=None 6 | def is_keepalive(A): 7 | B=ticks_diff(ticks_ms(),A.last_cpacket)//1000 8 | if 0=A.MSG_QUEUE_MAX: 50 | E=min(map(lambda x:x[0]if x else 65535,A.msg_to_confirm.values()),default=0) 51 | if 0A.CONFIRM_QUEUE_MAX:A.msg_to_confirm.pop(0) 74 | return F 75 | except (OSError,simple2.MQTTException)as G: 76 | A.conn_issue=G,2 77 | if A.NO_QUEUE_DUPS: 78 | if C in A.msg_to_send:return 79 | if A.KEEP_QOS0 and B==0:A.add_msg_to_send(C) 80 | elif B==1:A.add_msg_to_send(C) 81 | def subscribe(A,topic,qos=0,resubscribe=True): 82 | C=topic;B=C,qos 83 | if A.RESUBSCRIBE and resubscribe: 84 | if C not in dict(A.subs):A.subs.append(B) 85 | A.sub_to_send[:]=[B for B in A.sub_to_send if C!=B[0]] 86 | try: 87 | D=super().subscribe(C,qos);A.sub_to_confirm.setdefault(B,[]).append(D) 88 | if len(A.sub_to_confirm[B])>A.CONFIRM_QUEUE_MAX:A.sub_to_confirm.pop(0) 89 | return D 90 | except (OSError,simple2.MQTTException)as E: 91 | A.conn_issue=E,3 92 | if A.NO_QUEUE_DUPS: 93 | if B in A.sub_to_send:return 94 | A.sub_to_send.append(B) 95 | def send_queue(A): 96 | D=[] 97 | for B in A.msg_to_send: 98 | E,I,J,C=B 99 | try: 100 | F=super().publish(E,I,J,C,False) 101 | if C==1:A.msg_to_confirm.setdefault(B,[]).append(F) 102 | D.append(B) 103 | except (OSError,simple2.MQTTException)as G:A.conn_issue=G,5;return False 104 | A.msg_to_send[:]=[B for B in A.msg_to_send if B not in D];del D;H=[] 105 | for B in A.sub_to_send: 106 | E,C=B 107 | try:F=super().subscribe(E,C);A.sub_to_confirm.setdefault(B,[]).append(F);H.append(B) 108 | except (OSError,simple2.MQTTException)as G:A.conn_issue=G,5;return False 109 | A.sub_to_send[:]=[B for B in A.sub_to_send if B not in H];return True 110 | def is_conn_issue(A): 111 | A.is_keepalive() 112 | if A.conn_issue:A.log() 113 | return bool(A.conn_issue) 114 | def wait_msg(A): 115 | A.is_keepalive() 116 | try:return super().wait_msg() 117 | except (OSError,simple2.MQTTException)as B:A.conn_issue=B,8 118 | def check_msg(A): 119 | A.is_keepalive() 120 | try:return super().check_msg() 121 | except (OSError,simple2.MQTTException)as B:A.conn_issue=B,10 -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import utime 2 | from ubinascii import hexlify 3 | from umqtt.robust2 import MQTTClient as _MQTTClient 4 | 5 | 6 | def debug_print(data): 7 | print('HEX: %s STR: /*' % hexlify(data).decode('ascii'), end='') 8 | for i, d in enumerate(data): 9 | if type(d) == str: 10 | d = ord(d) 11 | if 31 < d < 127: 12 | print(chr(d), end='') 13 | else: 14 | print('.', end='') 15 | print('*/') 16 | 17 | 18 | def debug_func_name(f_name): 19 | print('FUNC: %s' % f_name) 20 | 21 | 22 | class MQTTClient(_MQTTClient): 23 | DEBUG = 1 24 | MAX_DBG_LEN = 80 25 | MSG_QUEUE_MAX = 3 26 | 27 | def _read(self, n): 28 | out = super()._read(n) 29 | if type(out) == bytes: 30 | print(' R(%3d) - ' % len(out), end='') 31 | debug_print(out[:self.MAX_DBG_LEN]) 32 | else: 33 | print(' R(---) - %s' % out) 34 | return out 35 | 36 | def _write(self, bytes_wr, *args, **kwargs): 37 | print(' W(%3d,%3s) - ' % (len(bytes_wr), args[0] if args else '---'), end='') 38 | debug_print(bytes_wr[:self.MAX_DBG_LEN]) 39 | return super()._write(bytes_wr, *args, **kwargs) 40 | 41 | def connect(self, *a, **k): 42 | debug_func_name('connect') 43 | return super().connect(*a, **k) 44 | 45 | def disconnect(self, *a, **k): 46 | debug_func_name('disconnect') 47 | return super().disconnect(*a, **k) 48 | 49 | def ping(self, *a, **k): 50 | debug_func_name('ping') 51 | return super().ping(*a, **k) 52 | 53 | def publish(self, *a, **k): 54 | debug_func_name('publish') 55 | return super().publish(*a, **k) 56 | 57 | def subscribe(self, *a, **k): 58 | debug_func_name('subscribe') 59 | return super().subscribe(*a, **k) 60 | 61 | def wait_msg(self, *a, **k): 62 | debug_func_name('wait_msg') 63 | return super().wait_msg(*a, **k) 64 | 65 | def send_queue(self, *a, **k): 66 | debug_func_name('send_queue') 67 | out = super().send_queue(*a, **k) 68 | self.is_conn_issue() 69 | return out 70 | 71 | def connect(self, *a, **k): 72 | debug_func_name('connect') 73 | out = super().connect(*a, **k) 74 | self.is_conn_issue() 75 | return out 76 | 77 | def reconnect(self, *a, **k): 78 | debug_func_name('reconnect') 79 | out = super().reconnect(*a, **k) 80 | self.is_conn_issue() 81 | return out 82 | 83 | 84 | class TestMQTT: 85 | def __init__(self, *args, **kwargs): 86 | self.mqtt_client_args = (args, kwargs) 87 | self.msg_id = args[0] 88 | self.subsctiption_out = {} 89 | self.status_out = {} 90 | self.clients = {} 91 | self.client = None 92 | 93 | def init_mqtt_client(self, clientid_postfix='_1', mqtt_kwargs=None): 94 | args = list(self.mqtt_client_args[0][:]) 95 | kwargs = self.mqtt_client_args[1].copy() 96 | if mqtt_kwargs: 97 | kwargs.update(mqtt_kwargs) 98 | if len(args) > 0: 99 | args[0] += clientid_postfix 100 | if 'client_id' in kwargs: 101 | kwargs['client_id'] += clientid_postfix 102 | print('MQTT connection args:', args, kwargs) 103 | client = MQTTClient(*args, **kwargs) 104 | client.set_callback(self.sub_cb_gen(clientid_postfix)) 105 | client.set_callback_status(self.stat_cb_gen(clientid_postfix)) 106 | self.subsctiption_out[clientid_postfix] = None 107 | self.status_out[clientid_postfix] = None 108 | self.clients[clientid_postfix] = client 109 | return client 110 | 111 | def sub_cb_gen(self, clientid_postfix='_1'): 112 | def sub_cb(topic, msg, retained, dup): 113 | print('TOPIC: %s MSG: %s R: %s D: %s' % (topic, msg, retained, dup)) 114 | self.subsctiption_out[clientid_postfix] = (topic, msg, retained) 115 | 116 | return sub_cb 117 | 118 | def stat_cb_gen(self, clientid_postfix='_1'): 119 | def stat_cb(pid, status): 120 | print('PID: %s STATUS: %d' % (pid, status)) 121 | self.status_out[clientid_postfix] = (pid, status) 122 | 123 | return stat_cb 124 | 125 | def get_subscription_out(self, timeout=5, clientid_postfix='_1'): 126 | print('WAIT SUB: timeout=%d' % (timeout,)) 127 | client = self.clients[clientid_postfix] 128 | for i in range(timeout): 129 | client.check_msg() 130 | if clientid_postfix in self.subsctiption_out and self.subsctiption_out[clientid_postfix] is not None: 131 | o = self.subsctiption_out[clientid_postfix] 132 | self.subsctiption_out[clientid_postfix] = None 133 | return o 134 | utime.sleep(1) 135 | raise Exception('timeout') 136 | 137 | def get_status_out(self, timeout=5, pid=None, clientid_postfix='_1'): 138 | print('WAIT STAT: timeout=%d pid=%s' % (timeout, pid)) 139 | client = self.clients[clientid_postfix] 140 | for i in range(timeout + 1): 141 | utime.sleep(1) 142 | client.check_msg() 143 | if clientid_postfix in self.status_out and self.status_out[clientid_postfix] is not None: 144 | o = self.status_out[clientid_postfix] 145 | self.status_out[clientid_postfix] = None 146 | if pid: 147 | if pid != o[0]: 148 | continue 149 | return o 150 | raise Exception('timeout') 151 | 152 | def disable_net(self): 153 | raise RuntimeError('Not implemented method') 154 | 155 | def enable_net(self): 156 | raise RuntimeError('Not implemented method') 157 | 158 | def get_topic(self, test_name): 159 | return '%s/umqtt.robust2/%s/' % (self.msg_id, test_name) 160 | 161 | def run(self): 162 | test_fails = [] 163 | tests = [ 164 | 'test_cbstat', 165 | 'test_publish_qos_0', 166 | 'test_publish_qos_1', 167 | 'test_subscribe', 168 | 'test_keepalive', 169 | 'test_queue_max' 170 | ] 171 | for test_name in tests: 172 | if not self.run_test(test_name): 173 | test_fails.append(test_name) 174 | if test_fails: 175 | print('\nTests ok: %s\n' % ', '.join(t for t in tests if t not in test_fails)) 176 | print('\nTests fails: %s\n' % ', '.join(test_fails)) 177 | else: 178 | print('All the tests were finished successfully!') 179 | 180 | def run_test(self, test_name): 181 | try: 182 | if self.client: 183 | self.client.disconnect() 184 | except: 185 | pass 186 | self.client = self.init_mqtt_client() 187 | self.subsctiption_out = {} 188 | self.status_out = {} 189 | test = getattr(self, test_name) 190 | print('RUN [%s]' % test_name) 191 | test_pass = True 192 | self.enable_net() 193 | try: 194 | test(self.get_topic(test_name)) 195 | except Exception as e: 196 | from sys import print_exception 197 | print_exception(e) 198 | test_pass = False 199 | print('END [%s] %s\n' % (test_name, 'succes' if test_pass else 'FAIL')) 200 | return test_pass 201 | 202 | def test_cbstat(self, topic): 203 | 204 | self.client.msg_to_confirm = {('top1', 'msg11', False, 0): [1, 2, 3]} 205 | self.client.msg_to_send = [] 206 | self.client.cbstat(1, 0) 207 | assert self.client.msg_to_send == [('top1', 'msg11', False, 0)] 208 | assert self.client.msg_to_confirm == {('top1', 'msg11', False, 0): [2, 3]} 209 | self.client.cbstat(2, 0) 210 | assert self.client.msg_to_send == [('top1', 'msg11', False, 0)] 211 | assert self.client.msg_to_confirm == {('top1', 'msg11', False, 0): [3]} 212 | self.client.cbstat(3, 0) 213 | assert self.client.msg_to_send == [('top1', 'msg11', False, 0)] 214 | assert self.client.msg_to_confirm == {} 215 | 216 | self.client.msg_to_confirm = {('top1', 'msg11', False, 0): [1, 2, 3]} 217 | self.client.msg_to_send = [] 218 | self.client.cbstat(2, 1) 219 | assert self.client.msg_to_send == [] 220 | assert self.client.msg_to_confirm == {} 221 | 222 | self.client.sub_to_send = [] 223 | self.client.sub_to_confirm = {('stop1', 0): [10, 11, 12]} 224 | self.client.cbstat(10, 0) 225 | assert self.client.sub_to_send == [('stop1', 0)] 226 | assert self.client.sub_to_confirm == {('stop1', 0): [11, 12]} 227 | self.client.cbstat(11, 0) 228 | assert self.client.sub_to_send == [('stop1', 0)] 229 | assert self.client.sub_to_confirm == {('stop1', 0): [12]} 230 | self.client.cbstat(12, 0) 231 | assert self.client.sub_to_send == [('stop1', 0)] 232 | assert self.client.sub_to_confirm == {} 233 | 234 | self.client.sub_to_confirm = {('stop1', 0): [10, 11, 12]} 235 | self.client.sub_to_send = [] 236 | self.client.cbstat(11, 1) 237 | assert self.client.sub_to_send == [] 238 | assert self.client.sub_to_confirm == {} 239 | 240 | self.client.disconnect() 241 | 242 | def test_publish_qos_0(self, topic): 243 | self.client.connect() 244 | 245 | self.client.sock.close() 246 | self.client.publish(topic, 'test QoS 0') 247 | if self.client.is_conn_issue(): 248 | self.client.reconnect() 249 | self.client.check_msg() 250 | self.client.subscribe(topic + '#') 251 | 252 | assert self.client.send_queue() 253 | t, m, r = self.get_subscription_out() 254 | assert t.decode('ascii') == topic 255 | assert m.decode('ascii') == 'test QoS 0' 256 | 257 | self.client.disconnect() 258 | 259 | def test_publish_qos_1(self, topic): 260 | self.client.connect() 261 | 262 | self.client.sock.close() 263 | pid = self.client.publish(topic, 'test QoS 1', qos=1) 264 | assert pid is None 265 | 266 | print('msg_to_:', self.client.msg_to_send, self.client.msg_to_confirm, self.client.rcv_pids) 267 | assert self.client.msg_to_send == [(topic, 'test QoS 1', False, 1)] 268 | assert self.client.msg_to_confirm == {} 269 | 270 | if self.client.is_conn_issue(): 271 | self.client.reconnect() 272 | 273 | assert self.client.send_queue() 274 | print('msg_to_:', self.client.msg_to_send, self.client.msg_to_confirm, self.client.rcv_pids) 275 | pid = list(self.client.rcv_pids.keys())[0] 276 | assert self.client.msg_to_send == [] 277 | assert self.client.msg_to_confirm == {(topic, 'test QoS 1', False, 1): [pid]} 278 | 279 | self.client.check_msg() # get a confirmation 280 | out_pid, status = self.get_status_out(pid=pid) 281 | assert status == 1 282 | 283 | print('msg_to_:', self.client.msg_to_send, self.client.msg_to_confirm, self.client.rcv_pids) 284 | assert self.client.msg_to_send == [] 285 | assert self.client.msg_to_confirm == {} 286 | 287 | self.client.disconnect() 288 | 289 | def test_subscribe(self, topic): 290 | self.client.connect() 291 | 292 | self.client.sock.close() 293 | pid = self.client.subscribe(topic + '#') 294 | assert pid is None 295 | 296 | print(self.client.sub_to_send, self.client.sub_to_confirm, self.client.rcv_pids, 2) 297 | assert self.client.sub_to_send == [(topic + '#', 0)] 298 | assert self.client.sub_to_confirm == {} 299 | 300 | if self.client.is_conn_issue(): 301 | self.client.reconnect() 302 | 303 | print(self.client.sub_to_send, self.client.sub_to_confirm, self.client.rcv_pids, 3) 304 | assert self.client.sub_to_send == [(topic + '#', 0)] 305 | assert self.client.sub_to_confirm == {} 306 | 307 | assert self.client.send_queue() # resubscribe 308 | 309 | print(self.client.sub_to_send, self.client.sub_to_confirm, self.client.rcv_pids, 4) 310 | pid = list(self.client.rcv_pids.keys())[0] 311 | assert self.client.sub_to_send == [] 312 | assert self.client.sub_to_confirm == {(topic + '#', 0): [pid]} 313 | 314 | out_pid, status = self.get_status_out(pid=pid) 315 | 316 | print(self.client.sub_to_send, self.client.sub_to_confirm, self.client.rcv_pids, 5) 317 | assert status == 1 318 | assert self.client.sub_to_send == [] 319 | assert self.client.sub_to_confirm == {} 320 | 321 | self.client.disconnect() 322 | 323 | def test_resubscribe(self, topic): 324 | # TODO: Test re-subscription . This is currently not working. 325 | pass 326 | 327 | def test_keepalive(self, topic): 328 | c = self.init_mqtt_client(mqtt_kwargs={'keepalive': 3}) 329 | c.connect() # 3 sec 330 | assert not c.is_conn_issue() 331 | utime.sleep(2) 332 | c.ping() # 3 sec 333 | utime.sleep(1) 334 | c.check_msg() 335 | assert not c.is_conn_issue() 336 | utime.sleep(1) 337 | assert not c.is_conn_issue() 338 | utime.sleep(3) 339 | assert c.is_conn_issue() 340 | c.disconnect() 341 | 342 | def test_queue_max(self, topic): 343 | c = self.init_mqtt_client() 344 | c.add_msg_to_send((1, 'x', 'y')) 345 | c.add_msg_to_send((2, 'x', 'y')) 346 | c.add_msg_to_send((3, 'x', 'y')) 347 | c.add_msg_to_send((4, 'x', 'y')) 348 | print(c.msg_to_send) 349 | assert len(c.msg_to_send) == 3 350 | assert c.msg_to_send[-1] == (4, 'x', 'y') 351 | c.msg_to_confirm = { 352 | (2, 'x', 'y'): [1, 2] 353 | } 354 | c.add_msg_to_send((5, 'x', 'y')) 355 | assert len(c.msg_to_send) == 3 356 | assert c.msg_to_send[-3] == (3, 'x', 'y') 357 | assert c.msg_to_send[-2] == (4, 'x', 'y') 358 | assert c.msg_to_send[-1] == (5, 'x', 'y') 359 | assert len(c.msg_to_confirm) == 0 360 | c.msg_to_confirm = { 361 | (3, 'x', 'y'): [3], 362 | (4, 'x', 'y'): [2] 363 | } 364 | c.msg_to_send.pop(0) 365 | c.msg_to_send.pop(0) 366 | c.add_msg_to_send((6, 'x', 'y')) 367 | assert len(c.msg_to_send) == 2 368 | assert c.msg_to_send[-2] == (5, 'x', 'y') 369 | assert c.msg_to_send[-1] == (6, 'x', 'y') 370 | assert len(c.msg_to_confirm) == 1 371 | assert c.msg_to_confirm[(3, 'x', 'y')] == [3] 372 | --------------------------------------------------------------------------------