From 76c28391bc999fdee7bfe58a5d574e7ff6c413e5 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 12:18:38 -0400 Subject: [PATCH 001/148] cookiecutter init! --- .gitignore | 12 + .pylintrc | 433 ++++++++++++++++++++++++++++++++ .readthedocs.yml | 3 + .travis.yml | 48 ++++ CODE_OF_CONDUCT.md | 127 ++++++++++ LICENSE | 4 +- README.md | 2 - README.rst | 119 +++++++++ adafruit_minimqtt.py | 53 ++++ docs/_static/favicon.ico | Bin 0 -> 4414 bytes docs/api.rst | 8 + docs/conf.py | 160 ++++++++++++ docs/examples.rst | 8 + docs/index.rst | 51 ++++ examples/minimqtt_simpletest.py | 0 requirements.txt | 1 + setup.py | 63 +++++ 17 files changed, 1088 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 .readthedocs.yml create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 README.md create mode 100644 README.rst create mode 100644 adafruit_minimqtt.py create mode 100644 docs/_static/favicon.ico create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/examples.rst create mode 100644 docs/index.rst create mode 100644 examples/minimqtt_simpletest.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55f127b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.mpy +.idea +__pycache__ +_build +*.pyc +.env +build* +bundles +*.DS_Store +.eggs +dist +**/*.egg-info \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..039eaec --- /dev/null +++ b/.pylintrc @@ -0,0 +1,433 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +# jobs=1 +jobs=2 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes=FIXME,XXX,TODO +notes=FIXME,XXX + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=board + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format= +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +# class-name-hint=[A-Z_][a-zA-Z0-9]+$ +class-name-hint=[A-Z_][a-zA-Z0-9_]+$ + +# Regular expression matching correct class names +# class-rgx=[A-Z_][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9_]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +# good-names=i,j,k,ex,Run,_ +good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +# max-attributes=7 +max-attributes=11 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..f4243ad --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,3 @@ +python: + version: 3 +requirements_file: requirements.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5f1eaee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +# This is a common .travis.yml for generating library release zip files for +# CircuitPython library releases using circuitpython-build-tools. +# See https://github.com/adafruit/circuitpython-build-tools for detailed setup +# instructions. + +dist: xenial +language: python +python: + - "3.6" + +cache: + pip: true + +# TODO: if deployment to PyPi is desired, change 'DEPLOY_PYPI' to "true", +# or remove the env block entirely and remove the condition in the +# deploy block. +env: + - DEPLOY_PYPI="false" + +deploy: + - provider: releases + api_key: "$GITHUB_TOKEN" + file_glob: true + file: "$TRAVIS_BUILD_DIR/bundles/*" + skip_cleanup: true + overwrite: true + on: + tags: true + # TODO: Use 'travis encrypt --com -r adafruit/' to generate + # the encrypted password for adafruit-travis. Paste result below. + - provider: pypi + user: adafruit-travis + password: + secure: #-- PASTE ENCRYPTED PASSWORD HERE --# + on: + tags: true + condition: $DEPLOY_PYPI = "true" + +install: + - pip install -r requirements.txt + - pip install circuitpython-build-tools Sphinx sphinx-rtd-theme + - pip install --force-reinstall pylint==1.9.2 + +script: + - pylint adafruit_minimqtt.py + - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) + - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-minimqtt --library_location . + - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7ca3a1d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Adafruit Community Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and leaders pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level or type of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +We are committed to providing a friendly, safe and welcoming environment for +all. + +Examples of behavior that contributes to creating a positive environment +include: + +* Be kind and courteous to others +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Collaborating with other community members +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and sexual attention or advances +* The use of inappropriate images, including in a community member's avatar +* The use of inappropriate language, including in a community member's nickname +* Any spamming, flaming, baiting or other attention-stealing behavior +* Excessive or unwelcome helping; answering outside the scope of the question + asked +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate + +The goal of the standards and moderation guidelines outlined here is to build +and maintain a respectful community. We ask that you don’t just aim to be +"technically unimpeachable", but rather try to be your best self. + +We value many things beyond technical expertise, including collaboration and +supporting others within our community. Providing a positive experience for +other community members can have a much more significant impact than simply +providing the correct answer. + +## Our Responsibilities + +Project leaders are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project leaders have the right and responsibility to remove, edit, or +reject messages, comments, commits, code, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any community member for other behaviors that they deem +inappropriate, threatening, offensive, or harmful. + +## Moderation + +Instances of behaviors that violate the Adafruit Community Code of Conduct +may be reported by any member of the community. Community members are +encouraged to report these situations, including situations they witness +involving other community members. + +You may report in the following ways: + +In any situation, you may send an email to . + +On the Adafruit Discord, you may send an open message from any channel +to all Community Helpers by tagging @community moderators. You may also send an +open message from any channel, or a direct message to @kattni#1507, +@tannewt#4653, @Dan Halbert#1614, @cater#2442, @sommersoft#0222, or +@Andon#8175. + +Email and direct message reports will be kept confidential. + +In situations on Discord where the issue is particularly egregious, possibly +illegal, requires immediate action, or violates the Discord terms of service, +you should also report the message directly to Discord. + +These are the steps for upholding our community’s standards of conduct. + +1. Any member of the community may report any situation that violates the +Adafruit Community Code of Conduct. All reports will be reviewed and +investigated. +2. If the behavior is an egregious violation, the community member who +committed the violation may be banned immediately, without warning. +3. Otherwise, moderators will first respond to such behavior with a warning. +4. Moderators follow a soft "three strikes" policy - the community member may +be given another chance, if they are receptive to the warning and change their +behavior. +5. If the community member is unreceptive or unreasonable when warned by a +moderator, or the warning goes unheeded, they may be banned for a first or +second offense. Repeated offenses will result in the community member being +banned. + +## Scope + +This Code of Conduct and the enforcement policies listed above apply to all +Adafruit Community venues. This includes but is not limited to any community +spaces (both public and private), the entire Adafruit Discord server, and +Adafruit GitHub repositories. Examples of Adafruit Community spaces include +but are not limited to meet-ups, audio chats on the Adafruit Discord, or +interaction at a conference. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. As a community +member, you are representing our community, and are expected to behave +accordingly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +, +and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +For other projects adopting the Adafruit Community Code of +Conduct, please contact the maintainers of those projects for enforcement. +If you wish to use this code of conduct for your own project, consider +explicitly mentioning your moderation policy or making a copy with your +own moderation policy so as to avoid confusion. diff --git a/LICENSE b/LICENSE index 5739aaa..d4fbf1d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2019 Adafruit Industries +Copyright (c) 2019 Brent Rubell for Adafruit Industries Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md deleted file mode 100644 index 650a9ea..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Adafruit_CircuitPython_MiniMQTT -MQTT for CircuitPython diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..6fd6066 --- /dev/null +++ b/README.rst @@ -0,0 +1,119 @@ +Introduction +============ + +.. image:: https://readthedocs.org/projects/adafruit-circuitpython-minimqtt/badge/?version=latest + :target: https://circuitpython.readthedocs.io/projects/minimqtt/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://discord.gg/nBQh6qu + :alt: Discord + +.. image:: https://travis-ci.com/adafruit/Adafruit_CircuitPython_MiniMQTT.svg?branch=master + :target: https://travis-ci.com/adafruit/Adafruit_CircuitPython_MiniMQTT + :alt: Build Status + +MQTT client library for CircuitPython + + +Dependencies +============= +This driver depends on: + +* `Adafruit CircuitPython `_ + +Please ensure all dependencies are available on the CircuitPython filesystem. +This is easily achieved by downloading +`the Adafruit library and driver bundle `_. + +Installing from PyPI +===================== +.. note:: This library is not available on PyPI yet. Install documentation is included + as a standard element. Stay tuned for PyPI availability! + +.. todo:: Remove the above note if PyPI version is/will be available at time of release. + If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section. + +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. To install for current user: + +.. code-block:: shell + + pip3 install adafruit-circuitpython-minimqtt + +To install system-wide (this may be required in some cases): + +.. code-block:: shell + + sudo pip3 install adafruit-circuitpython-minimqtt + +To install in a virtual environment in your current project: + +.. code-block:: shell + + mkdir project-name && cd project-name + python3 -m venv .env + source .env/bin/activate + pip3 install adafruit-circuitpython-minimqtt + +Usage Example +============= + +.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst. + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. + +Building locally +================ + +Zip release files +----------------- + +To build this library locally you'll need to install the +`circuitpython-build-tools `_ package. + +.. code-block:: shell + + python3 -m venv .env + source .env/bin/activate + pip install circuitpython-build-tools + +Once installed, make sure you are in the virtual environment: + +.. code-block:: shell + + source .env/bin/activate + +Then run the build: + +.. code-block:: shell + + circuitpython-build-bundles --filename_prefix adafruit-circuitpython-minimqtt --library_location . + +Sphinx documentation +----------------------- + +Sphinx is used to build the documentation based on rST files and comments in the code. First, +install dependencies (feel free to reuse the virtual environment from above): + +.. code-block:: shell + + python3 -m venv .env + source .env/bin/activate + pip install Sphinx sphinx-rtd-theme + +Now, once you have the virtual environment activated: + +.. code-block:: shell + + cd docs + sphinx-build -E -W -b html . _build/html + +This will output the documentation to ``docs/_build/html``. Open the index.html in your browser to +view them. It will also (due to -W) error out on any warning like Travis will. This is a good way to +locally verify it will pass. diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py new file mode 100644 index 0000000..4f5e7a2 --- /dev/null +++ b/adafruit_minimqtt.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Brent Rubell for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_minimqtt` +================================================================================ + +MQTT client library for CircuitPython + + +* Author(s): Brent Rubell + +Implementation Notes +-------------------- + +**Hardware:** + +.. todo:: Add links to any specific hardware product page(s), or category page(s). Use unordered list & hyperlink rST + inline format: "* `Link Text `_" + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies based on the library's use of either. + +# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +""" + +# imports + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5aca98376a1f7e593ebd9cf41a808512c2135635 GIT binary patch literal 4414 zcmd^BX;4#F6n=SG-XmlONeGrD5E6J{RVh+e928U#MG!$jWvO+UsvWh`x&VqGNx*en zx=qox7Dqv{kPwo%fZC$dDwVpRtz{HzTkSs8QhG0)%Y=-3@Kt!4ag|JcIo?$-F|?bXVS9UDUyev>MVZQ(H8K4#;BQW-t2CPorj8^KJrMX}QK zp+e<;4ldpXz~=)2GxNy811&)gt-}Q*yVQpsxr@VMoA##{)$1~=bZ1MmjeFw?uT(`8 z^g=09<=zW%r%buwN%iHtuKSg|+r7HkT0PYN*_u9k1;^Ss-Z!RBfJ?Un4w(awqp2b3 z%+myoFis_lTlCrGx2z$0BQdh+7?!JK#9K9@Z!VrG zNj6gK5r(b4?YDOLw|DPRoN7bdP{(>GEG41YcN~4r_SUHU2hgVtUwZG@s%edC;k7Sn zC)RvEnlq~raE2mY2ko64^m1KQL}3riixh?#J{o)IT+K-RdHae2eRX91-+g!y`8^># z-zI0ir>P%Xon)!@xp-BK2bDYUB9k613NRrY6%lVjbFcQc*pRqiK~8xtkNPLxt}e?&QsTB}^!39t_%Qb)~Ukn0O%iC;zt z<&A-y;3h++)>c1br`5VFM~5(83!HKx$L+my8sW_c#@x*|*vB1yU)_dt3vH;2hqPWx zAl^6@?ipx&U7pf`a*>Yq6C85nb+B=Fnn+(id$W#WB^uHAcZVG`qg;rWB}ubvi(Y>D z$ei>REw$#xp0SHAd^|1hq&9HJ=jKK8^zTH~nk)G?yUcmTh9vUM6Y0LMw4(gYVY$D$ zGl&WY&H<)BbJ&3sYbKjx1j^=3-0Q#f^}(aP1?8^`&FUWMp|rmtpK)bLQ1Zo?^s4jqK=Lfg*9&geMGVQ z#^-*!V`fG@;H&{M9S8%+;|h&Qrxym0Ar>WT4BCVLR8cGXF=JmEYN(sNT(9vl+S|%g z8r7nXQ(95i^`=+XHo|){$vf2$?=`F$^&wFlYXyXg$B{a>$-Fp+V}+D;9k=~Xl~?C4 zAB-;RKXdUzBJE{V&d&%R>aEfFe;vxqI$0@hwVM}gFeQR@j}a>DDxR+n+-*6|_)k%% z*mSpDV|=5I9!&VC&9tD%fcVygWZV!iIo2qFtm#!*(s|@ZT33*Ad;+<|3^+yrp*;oH zBSYLV(H1zTU?2WjrCQoQW)Z>J2a=dTriuvezBmu16`tM2fm7Q@d4^iqII-xFpwHGI zn9CL}QE*1vdj2PX{PIuqOe5dracsciH6OlAZATvE8rj6ykqdIjal2 z0S0S~PwHb-5?OQ-tU-^KTG@XNrEVSvo|HIP?H;7ZhYeZkhSqh-{reE!5di;1zk$#Y zCe7rOnlzFYJ6Z#Hm$GoidKB=2HBCwm`BbZVeZY4ukmG%1uz7p2URs6c9j-Gjj^oQV zsdDb3@k2e`C$1I5ML5U0Qs0C1GAp^?!*`=|Nm(vWz3j*j*8ucum2;r0^-6Aca=Gv) zc%}&;!+_*S2tlnnJnz0EKeRmw-Y!@9ob!XQBwiv}^u9MkaXHvM=!<3YX;+2#5Cj5pp?FEK750S3BgeSDtaE^ zXUM@xoV6yBFKfzvY20V&Lr0yC + CircuitPython Reference Documentation + CircuitPython Support Forum + Discord Chat + Adafruit Learning System + Adafruit Blog + Adafruit Store + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..edf9394 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Adafruit-Blinka diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c0efb71 --- /dev/null +++ b/setup.py @@ -0,0 +1,63 @@ +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='adafruit-circuitpython-minimqtt', + + use_scm_version=True, + setup_requires=['setuptools_scm'], + + description='MQTT client library for CircuitPython', + long_description=long_description, + long_description_content_type='text/x-rst', + + # The project's main homepage. + url='https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT', + + # Author details + author='Adafruit Industries', + author_email='circuitpython@adafruit.com', + + install_requires=[ + 'Adafruit-Blinka' + ], + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries', + 'Topic :: System :: Hardware', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + # What does your project relate to? + keywords='adafruit blinka circuitpython micropython minimqtt mqtt, client, socket', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, + # CHANGE `py_modules=['...']` TO `packages=['...']` + py_modules=['adafruit_minimqtt'], +) From 6863435b8415e2d0c34ebb8446adc3a0b8f8babc Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 13:15:36 -0400 Subject: [PATCH 002/148] add init, starting connect... --- adafruit_minimqtt.py | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4f5e7a2..8d46572 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -2,6 +2,9 @@ # # Copyright (c) 2019 Brent Rubell for Adafruit Industries # +# Original Work Copyright (c) 2016 Paul Sokolovsky, uMQTT +# Modified Work Copyright (c) 2019 Bradley Beach, esp32spi_mqtt +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights @@ -31,11 +34,6 @@ Implementation Notes -------------------- -**Hardware:** - -.. todo:: Add links to any specific hardware product page(s), or category page(s). Use unordered list & hyperlink rST - inline format: "* `Link Text `_" - **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: @@ -43,11 +41,49 @@ .. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies based on the library's use of either. -# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +* Adafruit's ESP32SPI library: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/ """ - -# imports +import time +import struct __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" + + +class MQTT: + """ + MQTT Client for CircuitPython. + """ + def __init__(self, esp, socket, wifimanager, client_id, server_address, port=1883, user=None, + password = None, is_ssl=False): + self._esp = esp + self._socket = socket + self._wifi_manager = wifimanager + if port == 0 and ssl: + port = 8883 + else: + port = 1883 + self.port = port + self.user = user + self._pass = password + self._client_id = client_id + self.server = server_address + self._is_ssl = is_ssl + self.packet_id = 0 + self._keep_alive = 0 + self._cb = None + self._lw_topic = None + self._lw_msg = None + self._lw_retain = False + + def connect(self, clean_session=True): + """Initiates connection with the MQTT Broker. + :param bool clean_session: Establishes a persistent session + with the broker. Defaults to a non-persistent session. + """ + # get a socket + if self._esp: + self._socket.set_interface(self._esp) + + + From 775adffa99539b7b65859bca53381f74b4e7c063 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 13:55:57 -0400 Subject: [PATCH 003/148] connects to IO mqtt broker via socket, still need to remove passing the socket lib --- adafruit_minimqtt.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8d46572..c01f327 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -81,9 +81,38 @@ def connect(self, clean_session=True): :param bool clean_session: Establishes a persistent session with the broker. Defaults to a non-persistent session. """ - # get a socket + #TODO: This might be approachable without passing in a socket if self._esp: self._socket.set_interface(self._esp) - - - + conn_type = 0 # TCP Mode + self._sock = self._socket.socket() + else: + raise TypeError('ESP32SPI interface required!') + addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] + print(addr) + self._sock.settimeout(10) + if self._is_ssl: + raise NotImplementedError('SSL not implemented yet!') + else: + self._sock.connect(addr, conn_type) + premsg = bytearray(b"\x10\0\0") + msg = bytearray(b"\x04MQTT\x04\x02\0\0") + msg[6] = clean_session << 1 + sz = 10 + 2 + len(self._client_id) + if self.user is not None: + sz += 2 + len(self.user) + 2 + len(self._pass) + msg[6] |= 0xC0 + if self._keep_alive: + assert self._keep_alive < 65536 + msg[7] |= self._keep_alive >> 8 + msg[8] |= self._keep_alive & 0x00FF + if self._lw_topic: + sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) + msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 + msg[6] |= self._lw_retain << 5 + i = 1 + while sz > 0x7f: + premsg[i] = (sz & 0x7f) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz \ No newline at end of file From ea99dbf5b5325d12981225828e5e42264dcf1e5d Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 14:52:16 -0400 Subject: [PATCH 004/148] connects to Adafruit IO with MQTTS --- adafruit_minimqtt.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c01f327..c22a038 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -59,10 +59,6 @@ def __init__(self, esp, socket, wifimanager, client_id, server_address, port=188 self._esp = esp self._socket = socket self._wifi_manager = wifimanager - if port == 0 and ssl: - port = 8883 - else: - port = 1883 self.port = port self.user = user self._pass = password @@ -84,17 +80,17 @@ def connect(self, clean_session=True): #TODO: This might be approachable without passing in a socket if self._esp: self._socket.set_interface(self._esp) - conn_type = 0 # TCP Mode + conn_type = 2 # TCP Mode self._sock = self._socket.socket() else: raise TypeError('ESP32SPI interface required!') - addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] - print(addr) + #addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] + #print(addr) self._sock.settimeout(10) if self._is_ssl: raise NotImplementedError('SSL not implemented yet!') else: - self._sock.connect(addr, conn_type) + self._sock.connect((self.server, self.port), conn_type) premsg = bytearray(b"\x10\0\0") msg = bytearray(b"\x04MQTT\x04\x02\0\0") msg[6] = clean_session << 1 @@ -115,4 +111,29 @@ def connect(self, clean_session=True): premsg[i] = (sz & 0x7f) | 0x80 sz >>= 7 i += 1 - premsg[i] = sz \ No newline at end of file + premsg[i] = sz + + self._sock.write(premsg) + self._sock.write(msg) + self._send_str(self._client_id) + if self._lw_topic: + self._send_str(self._lw_topic) + self._send_str(self._lw_msg) + if self.user is not None: + self._send_str(self.user) + self._send_str(self._pass) + resp = self._sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 + if resp[3] !=0: + raise TypeError(resp[3]) #todo: make this a mqttexception + return resp[2] & 1 + + def _send_str(self, string): + """Packs a string into a struct. and writes it to a socket. + :param str string: String to write to the socket. + """ + self._sock.write(struct.pack("!H", len(string))) + if type(string) == str: + self._sock.write(str.encode(string, 'utf-8')) + else: + self._sock.write(string) \ No newline at end of file From ed2b12b91cfe823a3b1ed30fbc732fba6a0ba7f8 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 15:22:48 -0400 Subject: [PATCH 005/148] generate a client identifier, add control flow for is_ssl boolean --- adafruit_minimqtt.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c22a038..5035af1 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -45,6 +45,8 @@ """ import time import struct +from micropython import const +import microcontroller __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" @@ -54,24 +56,35 @@ class MQTT: """ MQTT Client for CircuitPython. """ - def __init__(self, esp, socket, wifimanager, client_id, server_address, port=1883, user=None, - password = None, is_ssl=False): + TCP_MODE = const(0) + TLS_MODE = const(2) + def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=None, + password = None, client_id=None, is_ssl=False): self._esp = esp self._socket = socket self._wifi_manager = wifimanager self.port = port + if is_ssl: + self.port = 8883 self.user = user self._pass = password - self._client_id = client_id + if client_id is not None: + self._client_id = client_id + else: # randomize client identifier, prevent duplicate devices on broker + self._client_id = 'circuitpython-{0}'.format(int(microcontroller.cpu.temperature)) self.server = server_address - self._is_ssl = is_ssl self.packet_id = 0 self._keep_alive = 0 self._cb = None self._lw_topic = None self._lw_msg = None self._lw_retain = False - + self._is_connected = False + + def is_connected(self): + """Returns if there is an active MQTT connection.""" + return self._is_connected + def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session @@ -80,17 +93,15 @@ def connect(self, clean_session=True): #TODO: This might be approachable without passing in a socket if self._esp: self._socket.set_interface(self._esp) - conn_type = 2 # TCP Mode self._sock = self._socket.socket() else: raise TypeError('ESP32SPI interface required!') - #addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] - #print(addr) self._sock.settimeout(10) - if self._is_ssl: - raise NotImplementedError('SSL not implemented yet!') + if self.port == 8883: + self._sock.connect((self.server, self.port), TLS_MODE) else: - self._sock.connect((self.server, self.port), conn_type) + addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] + self._sock.connect(addr, TCP_MODE) premsg = bytearray(b"\x10\0\0") msg = bytearray(b"\x04MQTT\x04\x02\0\0") msg[6] = clean_session << 1 @@ -126,8 +137,16 @@ def connect(self, clean_session=True): assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] !=0: raise TypeError(resp[3]) #todo: make this a mqttexception + self._is_connected = True return resp[2] & 1 - + + def disconnect(self): + """Disconnects from the broker. + """ + self._sock.write(b"\xe0\0") + self._sock.close() + self._is_connected = False + def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. :param str string: String to write to the socket. From 6f3a57a6d18a94dd8da28a61d05ac3c7e8a5e1d1 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 16:02:36 -0400 Subject: [PATCH 006/148] generate a client_id based on cpu UUID --- adafruit_minimqtt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 5035af1..2bd305e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -46,6 +46,7 @@ import time import struct from micropython import const +from random import randint import microcontroller __version__ = "0.0.0-auto.0" @@ -71,7 +72,7 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non if client_id is not None: self._client_id = client_id else: # randomize client identifier, prevent duplicate devices on broker - self._client_id = 'circuitpython-{0}'.format(int(microcontroller.cpu.temperature)) + self._client_id = 'cpy-{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) self.server = server_address self.packet_id = 0 self._keep_alive = 0 From e14df6d3f0df2083cb375915bd0ccfb0acc8a773 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 16:19:57 -0400 Subject: [PATCH 007/148] add subscribe() method --- adafruit_minimqtt.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2bd305e..c493682 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -53,6 +53,9 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" +class MiniMQTTException(Exception): + pass + class MQTT: """ MQTT Client for CircuitPython. @@ -91,8 +94,7 @@ def connect(self, clean_session=True): :param bool clean_session: Establishes a persistent session with the broker. Defaults to a non-persistent session. """ - #TODO: This might be approachable without passing in a socket - if self._esp: + if self._esp: #TODO: This might be approachable without passing in a socket self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: @@ -148,6 +150,32 @@ def disconnect(self): self._sock.close() self._is_connected = False + def ping(self): + """Pings the broker. + """ + self._sock.write(b"\xc0\0") + + def subscribe(self, topic, qos=0): + """Sends a subscribe message to the broker. + :param str topic: Unique topic subscription identifier. + :param int qos: QoS level for the topic. + """ + assert self._cb is not None, "Subscribe callback is not set - set one up before calling subscribe()." + pkt = bytearray(b"\x82\0\0\0") + self.pid += 11 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + self._sock.write(pkt) + self._send_str(topic) + self.sock.write(qos.to_bytes(1, "little")) + while 1: + op = self.wait_msg() + if op == 0x90: + resp = self.sock.read(4) + assert resp[1] == pkt[2] and resp[2] == pkt[3] + if resp[3] == 0x80: + raise MQTTException(resp[3]) + return + def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. :param str string: String to write to the socket. From 789bf81c3df5ce80bb135067fd8d9c9b1bedf46b Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 17:12:38 -0400 Subject: [PATCH 008/148] add a reconnect method --- adafruit_minimqtt.py | 70 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c493682..8d2e304 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -84,6 +84,18 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self._lw_msg = None self._lw_retain = False self._is_connected = False + self._pid = 0 + + def reconnect(self): + """Attempts to reconnect the MQTT client.""" + attempts = 0 + while 1: #TODO: switch this to better logic + try: + self.connect(False) + except OSError as e: + print('Unable to connect, reconnecting...') + i+=1 + self.delay(i) def is_connected(self): """Returns if there is an active MQTT connection.""" @@ -162,20 +174,70 @@ def subscribe(self, topic, qos=0): """ assert self._cb is not None, "Subscribe callback is not set - set one up before calling subscribe()." pkt = bytearray(b"\x82\0\0\0") - self.pid += 11 - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + self._pid += 11 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) self._sock.write(pkt) self._send_str(topic) - self.sock.write(qos.to_bytes(1, "little")) + self._sock.write(qos.to_bytes(1, "little")) while 1: op = self.wait_msg() if op == 0x90: - resp = self.sock.read(4) + resp = self._sock.read(4) assert resp[1] == pkt[2] and resp[2] == pkt[3] if resp[3] == 0x80: raise MQTTException(resp[3]) return + def wait_msg(self, timeout=0): + """Waits for and processes an incoming MQTT message. + :param int timeout: Socket timeout. + """ + self._sock.settimeout(timeout) + res = self._sock.read(1) + if res in [None, b""]: + return None + if res == b"\xd0": # PINGRESP + sz = self._sock.read(1)[0] + assert sz == 0 + return None + op = res[0] + if op & 0xf0 != 0x30: + return op + sz = self._recv_len() + topic_len = self._sock.read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = self._sock.read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = self._sock.read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = self._sock.read(sz) + self._cb(topic, msg) + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") + struct.pack_into("!H", pkt, 2, pid) + self._sock.write(pkt) + elif op & 6 == 4: + assert 0 + + def _recv_len(self): + n = 0 + sh = 0 + while 1: + b = self._sock.read(1)[0] + n |= (b & 0x7f) << sh + if not b & 0x80: + return n + sh += 7 + + def set_callback(self, f): + """Sets a subscription callback function. + :param function f: User-defined function to receive a topic/message. + format: def function(topic, message) + """ + self._cb = f + def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. :param str string: String to write to the socket. From 51ed0ae568a53ca6f492710541834dfbfa3e354f Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 17:16:22 -0400 Subject: [PATCH 009/148] tie the reconnect into the code, hard reset the esp if it doesnt connect --- adafruit_minimqtt.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8d2e304..fb9e515 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -87,15 +87,34 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self._pid = 0 def reconnect(self): - """Attempts to reconnect the MQTT client.""" - attempts = 0 - while 1: #TODO: switch this to better logic + """Attempts to reconnect to the MQTT broker.""" + failure_count = 0 + while not self._is_connected: try: self.connect(False) except OSError as e: - print('Unable to connect, reconnecting...') - i+=1 - self.delay(i) + print('Failed to connect to the broker, retrying\n', e) + failure_count+=1 + if failure_count >= 30: + failure_count = 0 + self._wifi_manager.reset() + continue + + while not self._esp.is_connected: + try: + if self.debug: + print("Connecting to AP...") + self.pixel_status((100, 0, 0)) + self._esp.connect_AP(bytes(self.ssid, 'utf-8'), bytes(self.password, 'utf-8')) + failure_count = 0 + self.pixel_status((0, 100, 0)) + except (ValueError, RuntimeError) as error: + print("Failed to connect, retrying\n", error) + failure_count += 1 + if failure_count >= self.attempts: + failure_count = 0 + self.reset() + continue def is_connected(self): """Returns if there is an active MQTT connection.""" From 0d25d3febf1e9c9e9da660959e34841004089ca2 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 17:25:55 -0400 Subject: [PATCH 010/148] publish method works --- adafruit_minimqtt.py | 52 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index fb9e515..0c2ccdb 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -185,11 +185,55 @@ def ping(self): """Pings the broker. """ self._sock.write(b"\xc0\0") - + + + def publish(self, topic, msg, retain=False, qos=0): + """Publishes a message to the MQTT broker. + :param str topic: Unique topic identifier. + :param str msg: Data to send to the broker. + :param bool retain: Whether the message is saved by the broker. + :param int qos: Quality of Service level for the message. + """ + pkt = bytearray(b"\x30\0") + pkt[0] |= qos << 1 | retain + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert sz < 2097152 + i = 1 + while sz > 0x7f: + pkt[i] = (sz & 0x7f) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + self._sock.write(pkt) + self._send_str(topic) + if qos > 0: + self.pid += 1 + pid = self.pid + struct.pack_into("!H", pkt, 0, pid) + self._sock.write(pkt) + if type(msg) == str: + msg = str.encode(msg, 'utf-8') + self._sock.write(msg) + if qos == 1: + while 1: + op = self.wait_msg() + if op == 0x40: + sz = self._sock.read(1) + assert sz == b"\x02" + rcv_pid = self._sock.read(2) + rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid == rcv_pid: + return + elif qos == 2: + assert 0 + + def subscribe(self, topic, qos=0): - """Sends a subscribe message to the broker. - :param str topic: Unique topic subscription identifier. - :param int qos: QoS level for the topic. + """Sends a subscribe message to the MQTT broker. + :param str topic: Unique topic identifier. + :param int qos: Quality of Service level for the topic. """ assert self._cb is not None, "Subscribe callback is not set - set one up before calling subscribe()." pkt = bytearray(b"\x82\0\0\0") From 9dafe235e04afb56380c4efc666db675050af269 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 17:46:49 -0400 Subject: [PATCH 011/148] add pubsub example --- examples/minimqtt_simpletest.py | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index e69de29..948dab3 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -0,0 +1,78 @@ +import time +import board +import busio +from digitalio import DigitalInOut +import neopixel +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_esp32spi import adafruit_esp32spi_wifimanager + +from adafruit_minimqtt import MQTT + +print("CircuitPython MiniMQTT WiFi Test") + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +try: + esp32_cs = DigitalInOut(board.ESP_CS) + esp32_ready = DigitalInOut(board.ESP_BUSY) + esp32_reset = DigitalInOut(board.ESP_RESET) +except: + esp32_cs = DigitalInOut(board.D9) + esp32_ready = DigitalInOut(board.D10) + esp32_reset = DigitalInOut(board.D5) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED""" +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) + +# Create a WiFiManager +wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) + +# Instanciate a MQTT Client +mqtt_client = MQTT(esp, socket, wifi, secrets['aio_url'] + user=secrets['aio_user'], password=secrets['aio_password'], + is_ssl = True) + +print("Connecting to AP...") +while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue +print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) +print("My IP address is", esp.pretty_ip(esp.ip_address)) + +# Set up MQTT Client callback +# TODO: We could probably make this a kwarg... +mqtt_client.set_callback(mqtt_client.rcv_msg) +print('Connecting to {0}:{1}...'.format(mqtt_client.server, mqtt_client.port)) +mqtt_client.connect() + +# MQTT Feed (Adafruit IO Format!) +mqtt_feed = "brubell/feeds/temperature" + +print('Listening for a message on {0}...'.format(mqtt_feed)) +mqtt_client.subscribe(mqtt_feed) +while True: + print('Publishing a message...') + mqtt_client.publish(mqtt_feed, '42') + mqtt_client.wait_msg() + time.sleep(0.5) \ No newline at end of file From af70eb475e14c863309029a702a102c3be79b1ac Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 17:48:41 -0400 Subject: [PATCH 012/148] add a default callback method and set_callback --- adafruit_minimqtt.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 0c2ccdb..bfced62 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -100,22 +100,6 @@ def reconnect(self): self._wifi_manager.reset() continue - while not self._esp.is_connected: - try: - if self.debug: - print("Connecting to AP...") - self.pixel_status((100, 0, 0)) - self._esp.connect_AP(bytes(self.ssid, 'utf-8'), bytes(self.password, 'utf-8')) - failure_count = 0 - self.pixel_status((0, 100, 0)) - except (ValueError, RuntimeError) as error: - print("Failed to connect, retrying\n", error) - failure_count += 1 - if failure_count >= self.attempts: - failure_count = 0 - self.reset() - continue - def is_connected(self): """Returns if there is an active MQTT connection.""" return self._is_connected @@ -170,7 +154,7 @@ def connect(self, clean_session=True): resp = self._sock.read(4) assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] !=0: - raise TypeError(resp[3]) #todo: make this a mqttexception + raise MiniMQTTException(resp[3]) self._is_connected = True return resp[2] & 1 @@ -186,7 +170,6 @@ def ping(self): """ self._sock.write(b"\xc0\0") - def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. :param str topic: Unique topic identifier. @@ -227,8 +210,7 @@ def publish(self, topic, msg, retain=False, qos=0): if pid == rcv_pid: return elif qos == 2: - assert 0 - + assert 0 def subscribe(self, topic, qos=0): """Sends a subscribe message to the MQTT broker. @@ -285,6 +267,7 @@ def wait_msg(self, timeout=0): assert 0 def _recv_len(self): + """Receives the size of the topic length.""" n = 0 sh = 0 while 1: @@ -294,12 +277,16 @@ def _recv_len(self): return n sh += 7 - def set_callback(self, f): + def rcv_msg(self, topic, msg): + print('new message on {0}\n'.format(topic)) + print(msg) + + def set_callback(self, function): """Sets a subscription callback function. - :param function f: User-defined function to receive a topic/message. + :param function function: User-defined function to receive a topic/message. format: def function(topic, message) """ - self._cb = f + self._cb = function def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. From e296f113b19cc1827bbc2dfe4cd9f6206f7c3877 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 27 Jun 2019 18:18:08 -0400 Subject: [PATCH 013/148] add naive impl. of associating callback methods with subscription topics --- adafruit_minimqtt.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bfced62..2165f1d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -85,6 +85,7 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self._lw_retain = False self._is_connected = False self._pid = 0 + self._sub_callbacks = 0 def reconnect(self): """Attempts to reconnect to the MQTT broker.""" @@ -212,11 +213,17 @@ def publish(self, topic, msg, retain=False, qos=0): elif qos == 2: assert 0 - def subscribe(self, topic, qos=0): + def subscribe(self, topic, qos=0, callback_method = None): """Sends a subscribe message to the MQTT broker. :param str topic: Unique topic identifier. :param int qos: Quality of Service level for the topic. + :param method callback_method: User-defined callback method to attach + the subscription feed to. """ + # TODO: Implement a dictionary which holds the callback_method with the topic + # identifier + if callback_method is not None: + self._sub_callbacks[len(self._sub_callbacks)+1] = callback_method assert self._cb is not None, "Subscribe callback is not set - set one up before calling subscribe()." pkt = bytearray(b"\x82\0\0\0") self._pid += 11 @@ -258,6 +265,7 @@ def wait_msg(self, timeout=0): pid = pid[0] << 8 | pid[1] sz -= 2 msg = self._sock.read(sz) + # TODO: send to the correct callback method via the topic ID self._cb(topic, msg) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") @@ -278,11 +286,15 @@ def _recv_len(self): sh += 7 def rcv_msg(self, topic, msg): + """Default callback method if one is not externally defined. + :param string topic: MQTT Topic. + :param string msg: MQTT message content. + """ print('new message on {0}\n'.format(topic)) print(msg) - def set_callback(self, function): - """Sets a subscription callback function. + def on_message(self, function): + """Defines a function which is executed :param function function: User-defined function to receive a topic/message. format: def function(topic, message) """ From e5fa86b5e9927db2ddebb233a7c401c139f0a55c Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 10:34:04 -0400 Subject: [PATCH 014/148] start working towards custom callback method dictionary mapping based off of subscription topic --- adafruit_minimqtt.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2165f1d..7965e9c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -79,13 +79,14 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self._cb = None + #self._cb = None + self._callback_methods = dict() + #self._callback_methods = {} self._lw_topic = None self._lw_msg = None self._lw_retain = False self._is_connected = False self._pid = 0 - self._sub_callbacks = 0 def reconnect(self): """Attempts to reconnect to the MQTT broker.""" @@ -213,18 +214,19 @@ def publish(self, topic, msg, retain=False, qos=0): elif qos == 2: assert 0 - def subscribe(self, topic, qos=0, callback_method = None): + def subscribe(self, topic, callback_method=None, qos=0): """Sends a subscribe message to the MQTT broker. :param str topic: Unique topic identifier. + :param method callback_method: Callback method for subscription topic. Defaults to default_sub_callback if None. :param int qos: Quality of Service level for the topic. - :param method callback_method: User-defined callback method to attach - the subscription feed to. """ - # TODO: Implement a dictionary which holds the callback_method with the topic - # identifier - if callback_method is not None: - self._sub_callbacks[len(self._sub_callbacks)+1] = callback_method - assert self._cb is not None, "Subscribe callback is not set - set one up before calling subscribe()." + if callback_method is None: + print('setting default callback method...') + #self._callback_methods.update[{'i': 2}] + self._callback_methods.update( {topic : self.default_sub_callback} ) + print('cb methods: ', self._callback_methods) + else: + self._callback_methods.update({topic, custom_callback_method}) pkt = bytearray(b"\x82\0\0\0") self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) @@ -265,8 +267,10 @@ def wait_msg(self, timeout=0): pid = pid[0] << 8 | pid[1] sz -= 2 msg = self._sock.read(sz) - # TODO: send to the correct callback method via the topic ID - self._cb(topic, msg) + # call the topic's callback method + print('TOPIC: ', topic) + if topic in self._callback_methods: + print('topic found: ', topic) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) @@ -286,19 +290,15 @@ def _recv_len(self): sh += 7 def rcv_msg(self, topic, msg): - """Default callback method if one is not externally defined. - :param string topic: MQTT Topic. - :param string msg: MQTT message content. - """ print('new message on {0}\n'.format(topic)) print(msg) - def on_message(self, function): - """Defines a function which is executed - :param function function: User-defined function to receive a topic/message. - format: def function(topic, message) + def default_sub_callback(self, topic, msg): + """Default feed subscription callback method. + :param str topic: Subscription topic. + :param str msg: Payload content. """ - self._cb = function + print('New message on {0}: {1}\n'.format(topic, msg)) def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. From 4da85f797373252e611516f208ed4e2c37b238d4 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 10:51:52 -0400 Subject: [PATCH 015/148] tested default internal callback method, todo: test dict with a default and a custom callback --- adafruit_minimqtt.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 7965e9c..b64aae8 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -79,9 +79,7 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self.server = server_address self.packet_id = 0 self._keep_alive = 0 - #self._cb = None - self._callback_methods = dict() - #self._callback_methods = {} + self._callback_methods = {} self._lw_topic = None self._lw_msg = None self._lw_retain = False @@ -221,12 +219,9 @@ def subscribe(self, topic, callback_method=None, qos=0): :param int qos: Quality of Service level for the topic. """ if callback_method is None: - print('setting default callback method...') - #self._callback_methods.update[{'i': 2}] self._callback_methods.update( {topic : self.default_sub_callback} ) - print('cb methods: ', self._callback_methods) else: - self._callback_methods.update({topic, custom_callback_method}) + self._callback_methods.update( {topic : custom_callback_method} ) pkt = bytearray(b"\x82\0\0\0") self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) @@ -268,9 +263,10 @@ def wait_msg(self, timeout=0): sz -= 2 msg = self._sock.read(sz) # call the topic's callback method - print('TOPIC: ', topic) - if topic in self._callback_methods: - print('topic found: ', topic) + #topic = str(topic, 'utf-8') + if str(topic, 'utf-8') in self._callback_methods: + callback_method = self._callback_methods[str(topic, 'utf-8')] + callback_method(str(topic, 'utf-8'), str(topic, 'utf-8')) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) @@ -289,10 +285,6 @@ def _recv_len(self): return n sh += 7 - def rcv_msg(self, topic, msg): - print('new message on {0}\n'.format(topic)) - print(msg) - def default_sub_callback(self, topic, msg): """Default feed subscription callback method. :param str topic: Subscription topic. From 1642c59005341ca752d73bffa239c4d79fa55814 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 11:02:09 -0400 Subject: [PATCH 016/148] add better error handling defs --- adafruit_minimqtt.py | 49 +++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b64aae8..742689d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -53,6 +53,18 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" +# MQTT Connection Errors +MQTT_ERR_INCORRECT_SERVER = const(3) + +def handle_mqtt_error(mqtt_err): + """Returns string associated with MQTT error number. + :param int mqtt_err: MQTT error number. + """ + if mqtt_err == MQTT_ERR_INCORRECT_SERVER: + raise MiniMQTTException("Invalid server address defined.") + else: + raise MiniMQTTException("Unknown error!") + class MiniMQTTException(Exception): pass @@ -79,7 +91,7 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self._callback_methods = {} + self._handler_methods = {} self._lw_topic = None self._lw_msg = None self._lw_retain = False @@ -116,10 +128,16 @@ def connect(self, clean_session=True): raise TypeError('ESP32SPI interface required!') self._sock.settimeout(10) if self.port == 8883: - self._sock.connect((self.server, self.port), TLS_MODE) + try: + self._sock.connect((self.server, self.port), TLS_MODE) + except RuntimeError: + handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] - self._sock.connect(addr, TCP_MODE) + try: + self._sock.connect(addr, TCP_MODE) + except RuntimeError: + self.handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) premsg = bytearray(b"\x10\0\0") msg = bytearray(b"\x04MQTT\x04\x02\0\0") msg[6] = clean_session << 1 @@ -212,16 +230,16 @@ def publish(self, topic, msg, retain=False, qos=0): elif qos == 2: assert 0 - def subscribe(self, topic, callback_method=None, qos=0): + def subscribe(self, topic, handler_method=None, qos=0): """Sends a subscribe message to the MQTT broker. :param str topic: Unique topic identifier. - :param method callback_method: Callback method for subscription topic. Defaults to default_sub_callback if None. + :param method handler_method: handler method for subscription topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. """ - if callback_method is None: - self._callback_methods.update( {topic : self.default_sub_callback} ) + if handler_method is None: + self._handler_methods.update( {topic : self.default_sub_handler} ) else: - self._callback_methods.update( {topic : custom_callback_method} ) + self._handler_methods.update( {topic : custom_handler_method} ) pkt = bytearray(b"\x82\0\0\0") self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) @@ -262,11 +280,10 @@ def wait_msg(self, timeout=0): pid = pid[0] << 8 | pid[1] sz -= 2 msg = self._sock.read(sz) - # call the topic's callback method - #topic = str(topic, 'utf-8') - if str(topic, 'utf-8') in self._callback_methods: - callback_method = self._callback_methods[str(topic, 'utf-8')] - callback_method(str(topic, 'utf-8'), str(topic, 'utf-8')) + # call the topic's handler method + if str(topic, 'utf-8') in self._handler_methods: + handler_method = self._handler_methods[str(topic, 'utf-8')] + handler_method(str(topic, 'utf-8'), str(topic, 'utf-8')) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) @@ -285,12 +302,12 @@ def _recv_len(self): return n sh += 7 - def default_sub_callback(self, topic, msg): - """Default feed subscription callback method. + def default_sub_handler(self, topic, msg): + """Default feed subscription handler method. :param str topic: Subscription topic. :param str msg: Payload content. """ - print('New message on {0}: {1}\n'.format(topic, msg)) + print('New message on {0}: {1}'.format(topic, msg)) def _send_str(self, string): """Packs a string into a struct. and writes it to a socket. From d29982ca132ea46638feae8ffd141d4ac88b9d1f Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 11:22:21 -0400 Subject: [PATCH 017/148] add checks for invalid QOS, message length --- adafruit_minimqtt.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 742689d..8be184d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -138,6 +138,7 @@ def connect(self, clean_session=True): self._sock.connect(addr, TCP_MODE) except RuntimeError: self.handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) + premsg = bytearray(b"\x10\0\0") msg = bytearray(b"\x04MQTT\x04\x02\0\0") msg[6] = clean_session << 1 @@ -195,6 +196,12 @@ def publish(self, topic, msg, retain=False, qos=0): :param bool retain: Whether the message is saved by the broker. :param int qos: Quality of Service level for the message. """ + # TODO: handle if MQTT topic is a wild card, we cant publish! + if qos < 0 or qos > 2: + raise MQTTException('QoS must be between 0 and 2.') + # TODO: Message type conversions + if len(msg) > 268435455: #TODO: repl with user-defined + raise MQTTException('Message is larger than MQTT_MSG_SZ_LIMIT.') pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) From fe1d98f67a68fb80a656c58a8afe266c5085000f Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 12:18:05 -0400 Subject: [PATCH 018/148] add msg size limits, topic size limits, setter/getter for size limits --- adafruit_minimqtt.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8be184d..f3c3f9e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -53,6 +53,10 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" +# length of maximum mqtt message +MQTT_MSG_MAX_SZ = const(268435455) +MQTT_TOPIC_SZ_LIMIT = const(65536) + # MQTT Connection Errors MQTT_ERR_INCORRECT_SERVER = const(3) @@ -97,6 +101,7 @@ def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=Non self._lw_retain = False self._is_connected = False self._pid = 0 + self._msg_size_lim = const(10000000) def reconnect(self): """Attempts to reconnect to the MQTT broker.""" @@ -189,6 +194,20 @@ def ping(self): """ self._sock.write(b"\xc0\0") + + @property + def mqtt_msg(self): + """Returns maximum MQTT payload and topic size.""" + return self._msg_size_lim, MQTT_TOPIC_SZ_LIMIT + + @mqtt_msg.setter + def mqtt_msg(self, msg_size): + """Sets the maximum MQTT message payload size. + :param int msg_size: Maximum MQTT payload size. + """ + if msg_size < MQTT_MSG_MAX_SZ: + self.__msg_size_lim = msg_size + def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. :param str topic: Unique topic identifier. @@ -200,7 +219,7 @@ def publish(self, topic, msg, retain=False, qos=0): if qos < 0 or qos > 2: raise MQTTException('QoS must be between 0 and 2.') # TODO: Message type conversions - if len(msg) > 268435455: #TODO: repl with user-defined + if len(msg) > MQTT_MSG_SZ_LIMIT: raise MQTTException('Message is larger than MQTT_MSG_SZ_LIMIT.') pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain From e4db740362d164c2a0cab6d32131a5b6f3a50ca0 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 12:54:17 -0400 Subject: [PATCH 019/148] add checks to mqtt publish for topic, message, and quos. add type conversions --- adafruit_minimqtt.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f3c3f9e..bb6e823 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -26,8 +26,7 @@ `adafruit_minimqtt` ================================================================================ -MQTT client library for CircuitPython - +MQTT Library for CircuitPython. * Author(s): Brent Rubell @@ -39,9 +38,6 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases -.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies based on the library's use of either. - -* Adafruit's ESP32SPI library: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/ """ import time import struct @@ -59,6 +55,7 @@ # MQTT Connection Errors MQTT_ERR_INCORRECT_SERVER = const(3) +MQTT_ERR_INVALID = const(4) def handle_mqtt_error(mqtt_err): """Returns string associated with MQTT error number. @@ -66,6 +63,8 @@ def handle_mqtt_error(mqtt_err): """ if mqtt_err == MQTT_ERR_INCORRECT_SERVER: raise MiniMQTTException("Invalid server address defined.") + elif mqtt_err == MQTT_ERR_INVALID: + raise MiniMQTTException("Invalid method arguments provided.") else: raise MiniMQTTException("Unknown error!") @@ -215,12 +214,23 @@ def publish(self, topic, msg, retain=False, qos=0): :param bool retain: Whether the message is saved by the broker. :param int qos: Quality of Service level for the message. """ - # TODO: handle if MQTT topic is a wild card, we cant publish! - if qos < 0 or qos > 2: - raise MQTTException('QoS must be between 0 and 2.') - # TODO: Message type conversions + # check topic kwarg + if topic is None or len(topic) == 0: + raise MQTTException('Invalid topic.') + if b'+' in topic or b'#' in topic: + raise MQTTException('Topic can not contain wildcards.') + # check msg/qos args if len(msg) > MQTT_MSG_SZ_LIMIT: - raise MQTTException('Message is larger than MQTT_MSG_SZ_LIMIT.') + raise MQTTException('Message size larger than %db.'%MQTT_MSG_SZ_LIMIT) + if qos < 0 or qos > 2: + raise MQTTException('Invalid QoS, must be between 0 and 2.') + # msg kwarg type conversions + if msg is None: + raise MQTTException('Message can not be None.') + elif isinstance(msg, (int, float)): + msg = str(msg).encode('ascii') + elif isinstance(msg, unicode): + msg = str(msg).encode('utf-8') pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) From ef9ab3c85530e7ef3aa9fcd8259cb409b9a7bdc6 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 13:24:06 -0400 Subject: [PATCH 020/148] refactoring and fix publish message type conversions --- adafruit_minimqtt.py | 97 +++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bb6e823..0e31c63 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -57,6 +57,16 @@ MQTT_ERR_INCORRECT_SERVER = const(3) MQTT_ERR_INVALID = const(4) +# MQTT Spec. Commands +MQTT_TLS_PORT = const(8883) +MQTT_TCP_PORT = const(1883) +MQTT_PINGRESP = b'\xd0' +MQTT_SUB_PKT_TYPE = bytearray(b'\x82\0\0\0') +MQTT_DISCONNECT = b'\xe0\0' +MQTT_PING_REQ = b'\xc0\0' +MQTT_CON_PREMSG = bytearray(b"\x10\0\0") +MQTT_CON_MSG = bytearray(b"\x04MQTT\x04\x02\0\0") + def handle_mqtt_error(mqtt_err): """Returns string associated with MQTT error number. :param int mqtt_err: MQTT error number. @@ -74,45 +84,57 @@ class MiniMQTTException(Exception): class MQTT: """ MQTT Client for CircuitPython. + :param esp: ESP32SPI object. + :param socket: ESP32SPI Socket object. + :param wifimanager: WiFiManager object. + :param str server_address: Server URL or IP Address. + :param int port: Optional port definition, defaults to 8883. + :param str user: Username for broker authentication. + :param str password: Password for broker authentication. + :param str client_id: Optional client identifier, defaults to a randomly generated id. + :param bool is_ssl: Enables TCP mode if false (port 1883). Defaults to True (port 8883). """ TCP_MODE = const(0) TLS_MODE = const(2) - def __init__(self, esp, socket, wifimanager, server_address, port=1883, user=None, - password = None, client_id=None, is_ssl=False): + def __init__(self, esp, socket, wifimanager, server_address, port=8883, user=None, + password = None, client_id=None, is_ssl=True): self._esp = esp self._socket = socket self._wifi_manager = wifimanager self.port = port - if is_ssl: - self.port = 8883 + if not is_ssl: + self.port = MQTT_TCP_PORT self.user = user self._pass = password if client_id is not None: self._client_id = client_id - else: # randomize client identifier, prevent duplicate devices on broker + else: # randomize client identifier, prevents duplicate devices on broker self._client_id = 'cpy-{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self._handler_methods = {} self._lw_topic = None self._lw_msg = None self._lw_retain = False self._is_connected = False self._pid = 0 + # subscription method handler dictionary + self._handler_methods = {} self._msg_size_lim = const(10000000) - def reconnect(self): - """Attempts to reconnect to the MQTT broker.""" - failure_count = 0 + def reconnect(self, retries=30): + """Attempts to reconnect to the MQTT broker. + :param int retries: Amount of retries before resetting the ESP32 hardware. + """ + retries = 0 while not self._is_connected: try: self.connect(False) except OSError as e: print('Failed to connect to the broker, retrying\n', e) - failure_count+=1 - if failure_count >= 30: - failure_count = 0 + retries+=1 + if retries >= 30: + retries = 0 self._wifi_manager.reset() continue @@ -143,15 +165,15 @@ def connect(self, clean_session=True): except RuntimeError: self.handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) - premsg = bytearray(b"\x10\0\0") - msg = bytearray(b"\x04MQTT\x04\x02\0\0") + premsg = MQTT_CON_PREMSG + msg = MQTT_CON_MSG msg[6] = clean_session << 1 - sz = 10 + 2 + len(self._client_id) + sz = 12 + len(self._client_id) if self.user is not None: sz += 2 + len(self.user) + 2 + len(self._pass) msg[6] |= 0xC0 if self._keep_alive: - assert self._keep_alive < 65536 + assert self._keep_alive < MQTT_TOPIC_SZ_LIMIT msg[7] |= self._keep_alive >> 8 msg[8] |= self._keep_alive & 0x00FF if self._lw_topic: @@ -184,15 +206,14 @@ def connect(self, clean_session=True): def disconnect(self): """Disconnects from the broker. """ - self._sock.write(b"\xe0\0") + self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False def ping(self): """Pings the broker. """ - self._sock.write(b"\xc0\0") - + self._sock.write(MQTT_PING_REQ) @property def mqtt_msg(self): @@ -215,22 +236,24 @@ def publish(self, topic, msg, retain=False, qos=0): :param int qos: Quality of Service level for the message. """ # check topic kwarg - if topic is None or len(topic) == 0: + topic_str = str(topic).encode('utf-8') + if topic_str is None or len(topic_str) == 0: raise MQTTException('Invalid topic.') - if b'+' in topic or b'#' in topic: + if b'+' in topic_str or b'#' in topic_str: raise MQTTException('Topic can not contain wildcards.') - # check msg/qos args - if len(msg) > MQTT_MSG_SZ_LIMIT: - raise MQTTException('Message size larger than %db.'%MQTT_MSG_SZ_LIMIT) - if qos < 0 or qos > 2: - raise MQTTException('Invalid QoS, must be between 0 and 2.') - # msg kwarg type conversions + # check msg/qos kwargs if msg is None: raise MQTTException('Message can not be None.') elif isinstance(msg, (int, float)): msg = str(msg).encode('ascii') - elif isinstance(msg, unicode): + elif isinstance(msg, str): msg = str(msg).encode('utf-8') + else: + raise MQTTException('Invalid message data type.') + if len(msg) > MQTT_MSG_MAX_SZ: + raise MQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) + if qos < 0 or qos > 2: + raise MQTTException('Invalid QoS, must be between 0 and 2.') pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -239,7 +262,7 @@ def publish(self, topic, msg, retain=False, qos=0): assert sz < 2097152 i = 1 while sz > 0x7f: - pkt[i] = (sz & 0x7f) | 0x80 + pkt[i] = (sz & 0x7f) | const(0x80) sz >>= 7 i += 1 pkt[i] = sz @@ -250,13 +273,11 @@ def publish(self, topic, msg, retain=False, qos=0): pid = self.pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) - if type(msg) == str: - msg = str.encode(msg, 'utf-8') self._sock.write(msg) if qos == 1: while 1: op = self.wait_msg() - if op == 0x40: + if op == const(0x40): sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) @@ -276,7 +297,7 @@ def subscribe(self, topic, handler_method=None, qos=0): self._handler_methods.update( {topic : self.default_sub_handler} ) else: self._handler_methods.update( {topic : custom_handler_method} ) - pkt = bytearray(b"\x82\0\0\0") + pkt = MQTT_SUB_PKT_TYPE self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) self._sock.write(pkt) @@ -299,7 +320,7 @@ def wait_msg(self, timeout=0): res = self._sock.read(1) if res in [None, b""]: return None - if res == b"\xd0": # PINGRESP + if res == MQTT_PINGRESP: sz = self._sock.read(1)[0] assert sz == 0 return None @@ -310,6 +331,7 @@ def wait_msg(self, timeout=0): topic_len = self._sock.read(2) topic_len = (topic_len[0] << 8) | topic_len[1] topic = self._sock.read(topic_len) + topic = str(topic, 'utf-8') sz -= topic_len + 2 if op & 6: pid = self._sock.read(2) @@ -317,9 +339,9 @@ def wait_msg(self, timeout=0): sz -= 2 msg = self._sock.read(sz) # call the topic's handler method - if str(topic, 'utf-8') in self._handler_methods: - handler_method = self._handler_methods[str(topic, 'utf-8')] - handler_method(str(topic, 'utf-8'), str(topic, 'utf-8')) + if topic in self._handler_methods: + handler_method = self._handler_methods[topic] + handler_method(topic, str(msg, 'utf-8')) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) @@ -343,6 +365,7 @@ def default_sub_handler(self, topic, msg): :param str topic: Subscription topic. :param str msg: Payload content. """ + print(msg) print('New message on {0}: {1}'.format(topic, msg)) def _send_str(self, string): From 054f61387222d66b6d1a1426a65d07c9b1bcefcf Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 28 Jun 2019 15:26:38 -0400 Subject: [PATCH 021/148] add updated example --- examples/minimqtt_simpletest.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index 948dab3..6a36344 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -46,7 +46,7 @@ wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) # Instanciate a MQTT Client -mqtt_client = MQTT(esp, socket, wifi, secrets['aio_url'] +mqtt_client = MQTT(esp, socket, wifi, secrets['aio_url'], user=secrets['aio_user'], password=secrets['aio_password'], is_ssl = True) @@ -60,19 +60,18 @@ print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("My IP address is", esp.pretty_ip(esp.ip_address)) -# Set up MQTT Client callback -# TODO: We could probably make this a kwarg... -mqtt_client.set_callback(mqtt_client.rcv_msg) print('Connecting to {0}:{1}...'.format(mqtt_client.server, mqtt_client.port)) + +# Connect MQTT Client mqtt_client.connect() -# MQTT Feed (Adafruit IO Format!) -mqtt_feed = "brubell/feeds/temperature" +# Subscribe to MQTT feed +print('Subscribing to feed') +mqtt_client.subscribe('brubell/feeds/temperature') -print('Listening for a message on {0}...'.format(mqtt_feed)) -mqtt_client.subscribe(mqtt_feed) while True: - print('Publishing a message...') - mqtt_client.publish(mqtt_feed, '42') + print('Publishing msg to feed') + mqtt_client.publish('brubell/feeds/temperature', "cat") + print('Listening for msg...') mqtt_client.wait_msg() - time.sleep(0.5) \ No newline at end of file + time.sleep(5) \ No newline at end of file From 21db5ca518701b93961563a12f80ad0a062553be Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 11:46:43 -0400 Subject: [PATCH 022/148] enforce some of the mqtt client_id spec, check hardware objects before proceeding --- adafruit_minimqtt.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 0e31c63..c8ace10 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -98,9 +98,12 @@ class MQTT: TLS_MODE = const(2) def __init__(self, esp, socket, wifimanager, server_address, port=8883, user=None, password = None, client_id=None, is_ssl=True): - self._esp = esp - self._socket = socket - self._wifi_manager = wifimanager + if esp and socket and wifimanager is not None: + self._esp = esp + self._socket = socket + self._wifi_manager = wifimanager + else: + raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI connection.') self.port = port if not is_ssl: self.port = MQTT_TCP_PORT @@ -108,8 +111,13 @@ def __init__(self, esp, socket, wifimanager, server_address, port=8883, user=Non self._pass = password if client_id is not None: self._client_id = client_id - else: # randomize client identifier, prevents duplicate devices on broker - self._client_id = 'cpy-{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) + else: + self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) + # ._clientid MUST be a UTF-8 encoded string [MQTT-3.1.3-4]. + self._client_id = self._client_id.encode('utf-8') + # server must allow clientid btween 1 and 23 bytes [MQTT-3.1.3-5] + if len(self._client_id) > 23 or len(self._client_id) < 1: + raise ValueError('MQTT Client ID must be between 1 and 23 bytes') self.server = server_address self.packet_id = 0 self._keep_alive = 0 @@ -122,6 +130,19 @@ def __init__(self, esp, socket, wifimanager, server_address, port=8883, user=Non self._handler_methods = {} self._msg_size_lim = const(10000000) + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.deinit() + + def deinit(self): + """Disconnects the MQTT client from the broker and + deinitializes the defined hardware. + """ + self.disconnect() + #TODO: deinit the esp object if possible... + def reconnect(self, retries=30): """Attempts to reconnect to the MQTT broker. :param int retries: Amount of retries before resetting the ESP32 hardware. From 30b4a5f7bee0372b0a54cc08aca2052e08ffe2a5 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 11:50:23 -0400 Subject: [PATCH 023/148] drop wifimanager dependency, favor esp32spi --- adafruit_minimqtt.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c8ace10..e4c36dc 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -86,7 +86,6 @@ class MQTT: MQTT Client for CircuitPython. :param esp: ESP32SPI object. :param socket: ESP32SPI Socket object. - :param wifimanager: WiFiManager object. :param str server_address: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str user: Username for broker authentication. @@ -96,12 +95,11 @@ class MQTT: """ TCP_MODE = const(0) TLS_MODE = const(2) - def __init__(self, esp, socket, wifimanager, server_address, port=8883, user=None, + def __init__(self, esp, socket, server_address, port=8883, user=None, password = None, client_id=None, is_ssl=True): - if esp and socket and wifimanager is not None: + if esp and socket is not None: self._esp = esp self._socket = socket - self._wifi_manager = wifimanager else: raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI connection.') self.port = port @@ -156,7 +154,7 @@ def reconnect(self, retries=30): retries+=1 if retries >= 30: retries = 0 - self._wifi_manager.reset() + self._esp.reset() continue def is_connected(self): From a967eb9746d3c6e3de7ab103b21b88b354ecf4fe Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 12:12:58 -0400 Subject: [PATCH 024/148] only enforce alpanumeric/byte clientid limit on generated client id, lax on user-defined. set msg size lim to a constant --- adafruit_minimqtt.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index e4c36dc..533a41c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -51,6 +51,7 @@ # length of maximum mqtt message MQTT_MSG_MAX_SZ = const(268435455) +MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) # MQTT Connection Errors @@ -108,14 +109,19 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, self.user = user self._pass = password if client_id is not None: + # user-defined client_id MAY allow client_id's > 23 bytes or + # non-alpha-numeric characters self._client_id = client_id else: + # assign a unique client_id self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) - # ._clientid MUST be a UTF-8 encoded string [MQTT-3.1.3-4]. + # generated client_id's enforce length rules + if len(self._client_id) > 23 or len(self._client_id) < 1: + raise ValueError('MQTT Client ID must be between 1 and 23 bytes') + # client_id MUST be a UTF-8 encoded string [MQTT-3.1.3-4]. self._client_id = self._client_id.encode('utf-8') - # server must allow clientid btween 1 and 23 bytes [MQTT-3.1.3-5] - if len(self._client_id) > 23 or len(self._client_id) < 1: - raise ValueError('MQTT Client ID must be between 1 and 23 bytes') + # subscription method handler dictionary + self._handler_methods = {} self.server = server_address self.packet_id = 0 self._keep_alive = 0 @@ -124,9 +130,7 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, self._lw_retain = False self._is_connected = False self._pid = 0 - # subscription method handler dictionary - self._handler_methods = {} - self._msg_size_lim = const(10000000) + self._msg_size_lim = MQTT_MSG_SZ_LIM def __enter__(self): return self From 3db491145971684c206e5e0ce6a5300153b9a3e2 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 12:23:18 -0400 Subject: [PATCH 025/148] wait_for_message checks timeout value, allow more precise timeouts with a float instead of int --- adafruit_minimqtt.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 533a41c..70bedbd 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -139,11 +139,9 @@ def __exit__(self, exception_type, exception_value, traceback): self.deinit() def deinit(self): - """Disconnects the MQTT client from the broker and - deinitializes the defined hardware. + """Disconnects the MQTT client from the broker. """ self.disconnect() - #TODO: deinit the esp object if possible... def reconnect(self, retries=30): """Attempts to reconnect to the MQTT broker. @@ -299,7 +297,7 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(msg) if qos == 1: while 1: - op = self.wait_msg() + op = self.wait_for_msg() if op == const(0x40): sz = self._sock.read(1) assert sz == b"\x02" @@ -327,7 +325,7 @@ def subscribe(self, topic, handler_method=None, qos=0): self._send_str(topic) self._sock.write(qos.to_bytes(1, "little")) while 1: - op = self.wait_msg() + op = self.wait_for_msg() if op == 0x90: resp = self._sock.read(4) assert resp[1] == pkt[2] and resp[2] == pkt[3] @@ -335,10 +333,13 @@ def subscribe(self, topic, handler_method=None, qos=0): raise MQTTException(resp[3]) return - def wait_msg(self, timeout=0): - """Waits for and processes an incoming MQTT message. - :param int timeout: Socket timeout. + + def wait_for_msg(self, timeout = 1.0): + """Waits for and processes network events. + :param float timeout: The time in seconds to wait for network traffic before returning. """ + if timeout < 0.0: + raise ValueError('timeout must be > 0.0 seconds.') self._sock.settimeout(timeout) res = self._sock.read(1) if res in [None, b""]: From 6dc0dd8391d7b5e3cfcf70bd37341934a60c6738 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 12:35:37 -0400 Subject: [PATCH 026/148] add check on disconnect() for an empty socket before dc'ing --- adafruit_minimqtt.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 70bedbd..6b654b0 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -76,6 +76,8 @@ def handle_mqtt_error(mqtt_err): raise MiniMQTTException("Invalid server address defined.") elif mqtt_err == MQTT_ERR_INVALID: raise MiniMQTTException("Invalid method arguments provided.") + elif mqtt_err == MQTT_ERR_NO_CONN: + raise MiniMQTTException("MiniMQTT not connected.") else: raise MiniMQTTException("Unknown error!") @@ -106,8 +108,7 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, self.port = port if not is_ssl: self.port = MQTT_TCP_PORT - self.user = user - self._pass = password + self._set_username_password(user, password) if client_id is not None: # user-defined client_id MAY allow client_id's > 23 bytes or # non-alpha-numeric characters @@ -143,6 +144,17 @@ def deinit(self): """ self.disconnect() + def _set_username_password(self, username, password=None): + """Set up username and optional password for authentication. + :param str username: MQTT broker username + """ + if username is None: + self._user = None + else: + # Username must be a UTF-8 encoded string [MQTT-3.1.3-12] + self._user = username.encode('utf-8') + self._pass = password + def reconnect(self, retries=30): """Attempts to reconnect to the MQTT broker. :param int retries: Amount of retries before resetting the ESP32 hardware. @@ -190,8 +202,8 @@ def connect(self, clean_session=True): msg = MQTT_CON_MSG msg[6] = clean_session << 1 sz = 12 + len(self._client_id) - if self.user is not None: - sz += 2 + len(self.user) + 2 + len(self._pass) + if self._user is not None: + sz += 2 + len(self._user) + 2 + len(self._pass) msg[6] |= 0xC0 if self._keep_alive: assert self._keep_alive < MQTT_TOPIC_SZ_LIMIT @@ -214,8 +226,8 @@ def connect(self, clean_session=True): if self._lw_topic: self._send_str(self._lw_topic) self._send_str(self._lw_msg) - if self.user is not None: - self._send_str(self.user) + if self._user is not None: + self._send_str(self._user) self._send_str(self._pass) resp = self._sock.read(4) assert resp[0] == 0x20 and resp[1] == 0x02 @@ -227,6 +239,8 @@ def connect(self, clean_session=True): def disconnect(self): """Disconnects from the broker. """ + if self._sock is None: + self.handle_mqtt_error(MQTT_ERR_NO_CONN) self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False From e7f9c280655196b806f3cbb5fa1afa6274acea15 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 13:07:54 -0400 Subject: [PATCH 027/148] publish matches mqtt spec --- adafruit_minimqtt.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 6b654b0..4503b5e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -78,6 +78,10 @@ def handle_mqtt_error(mqtt_err): raise MiniMQTTException("Invalid method arguments provided.") elif mqtt_err == MQTT_ERR_NO_CONN: raise MiniMQTTException("MiniMQTT not connected.") + elif mqtt_err == MQTT_INVALID_TOPIC: + raise MiniMQTTException("Invalid MQTT Topic, must have length > 0.") + elif mqtt_err == MQTT_INVALID_QOS: + raise MiniMQTTException("Invalid QoS level, must be between 0 and 2.") else: raise MiniMQTTException("Unknown error!") @@ -273,7 +277,7 @@ def publish(self, topic, msg, retain=False, qos=0): # check topic kwarg topic_str = str(topic).encode('utf-8') if topic_str is None or len(topic_str) == 0: - raise MQTTException('Invalid topic.') + self.handle_mqtt_error(MQTT_INVALID_TOPIC) if b'+' in topic_str or b'#' in topic_str: raise MQTTException('Topic can not contain wildcards.') # check msg/qos kwargs @@ -288,7 +292,7 @@ def publish(self, topic, msg, retain=False, qos=0): if len(msg) > MQTT_MSG_MAX_SZ: raise MQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: - raise MQTTException('Invalid QoS, must be between 0 and 2.') + self.handle_mqtt_error(MQTT_INVALID_QOS) pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -325,9 +329,17 @@ def publish(self, topic, msg, retain=False, qos=0): def subscribe(self, topic, handler_method=None, qos=0): """Sends a subscribe message to the MQTT broker. :param str topic: Unique topic identifier. - :param method handler_method: handler method for subscription topic. Defaults to default_sub_handler if None. + :param method handler_method: Method for handling messages recieved from a + topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. """ + if qos < 0 or qos > 2: + raise MQTTException('QoS level must be between 1 and 2.') + if topic is None or len(topic) == 0: + self.handle_mqtt_error(MQTT_INVALID_TOPIC) + # [MQTT-3.8.3-1] + topic = topic.encode('utf-8') + # associate topic subscription with handler_method. if handler_method is None: self._handler_methods.update( {topic : self.default_sub_handler} ) else: From 8c372d66dd12ecd3e5dbde39498b0a3b8f0120fe Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 13:32:33 -0400 Subject: [PATCH 028/148] fix broken subscribe, _send_str already encodes the topic per spec. --- adafruit_minimqtt.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4503b5e..5858f3d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -54,9 +54,13 @@ MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) -# MQTT Connection Errors +# MQTT Errors (for handle_mqtt_error method) MQTT_ERR_INCORRECT_SERVER = const(3) MQTT_ERR_INVALID = const(4) +MQTT_ERR_NO_CONN = const(5) +MQTT_INVALID_TOPIC = const(6) +MQTT_INVALID_QOS = const(7) +MQTT_INVALID_WILDCARD = const(8) # MQTT Spec. Commands MQTT_TLS_PORT = const(8883) @@ -82,6 +86,8 @@ def handle_mqtt_error(mqtt_err): raise MiniMQTTException("Invalid MQTT Topic, must have length > 0.") elif mqtt_err == MQTT_INVALID_QOS: raise MiniMQTTException("Invalid QoS level, must be between 0 and 2.") + elif mqtt_err == MQTT_INVALID_WILDCARD: + raise MiniMQTTException("Invalid MQTT Wildcard - must be * or #.") else: raise MiniMQTTException("Unknown error!") @@ -200,7 +206,7 @@ def connect(self, clean_session=True): try: self._sock.connect(addr, TCP_MODE) except RuntimeError: - self.handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) + handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) premsg = MQTT_CON_PREMSG msg = MQTT_CON_MSG @@ -244,7 +250,7 @@ def disconnect(self): """Disconnects from the broker. """ if self._sock is None: - self.handle_mqtt_error(MQTT_ERR_NO_CONN) + handle_mqtt_error(MQTT_ERR_NO_CONN) self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False @@ -277,7 +283,7 @@ def publish(self, topic, msg, retain=False, qos=0): # check topic kwarg topic_str = str(topic).encode('utf-8') if topic_str is None or len(topic_str) == 0: - self.handle_mqtt_error(MQTT_INVALID_TOPIC) + handle_mqtt_error(MQTT_INVALID_TOPIC) if b'+' in topic_str or b'#' in topic_str: raise MQTTException('Topic can not contain wildcards.') # check msg/qos kwargs @@ -292,7 +298,7 @@ def publish(self, topic, msg, retain=False, qos=0): if len(msg) > MQTT_MSG_MAX_SZ: raise MQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: - self.handle_mqtt_error(MQTT_INVALID_QOS) + handle_mqtt_error(MQTT_INVALID_QOS) pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -336,18 +342,18 @@ def subscribe(self, topic, handler_method=None, qos=0): if qos < 0 or qos > 2: raise MQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: - self.handle_mqtt_error(MQTT_INVALID_TOPIC) - # [MQTT-3.8.3-1] - topic = topic.encode('utf-8') + handle_mqtt_error(MQTT_INVALID_TOPIC) # associate topic subscription with handler_method. if handler_method is None: self._handler_methods.update( {topic : self.default_sub_handler} ) else: self._handler_methods.update( {topic : custom_handler_method} ) + # TODO: Allow topic to be a tuple, multiple topic subscriptions pkt = MQTT_SUB_PKT_TYPE self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) self._sock.write(pkt) + # [MQTT-3.8.3-1] self._send_str(topic) self._sock.write(qos.to_bytes(1, "little")) while 1: @@ -419,7 +425,7 @@ def default_sub_handler(self, topic, msg): print('New message on {0}: {1}'.format(topic, msg)) def _send_str(self, string): - """Packs a string into a struct. and writes it to a socket. + """Packs a string into a struct, and writes it to a socket as a utf-8 encoded string. :param str string: String to write to the socket. """ self._sock.write(struct.pack("!H", len(string))) From 5386cd37ef96b93196dc2a02272f72a7072878ef Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 13:40:39 -0400 Subject: [PATCH 029/148] client_id is already encoded in connect, dont encode twice --- adafruit_minimqtt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 5858f3d..b59707a 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -129,8 +129,6 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, # generated client_id's enforce length rules if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') - # client_id MUST be a UTF-8 encoded string [MQTT-3.1.3-4]. - self._client_id = self._client_id.encode('utf-8') # subscription method handler dictionary self._handler_methods = {} self.server = server_address @@ -232,6 +230,7 @@ def connect(self, clean_session=True): self._sock.write(premsg) self._sock.write(msg) + # [MQTT-3.1.3-4] self._send_str(self._client_id) if self._lw_topic: self._send_str(self._lw_topic) @@ -335,10 +334,12 @@ def publish(self, topic, msg, retain=False, qos=0): def subscribe(self, topic, handler_method=None, qos=0): """Sends a subscribe message to the MQTT broker. :param str topic: Unique topic identifier. + :param :param method handler_method: Method for handling messages recieved from a topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. """ + if qos < 0 or qos > 2: raise MQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: From 0833fa155df6653db7fcef6108d4c04ae5a2e6dd Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 13:44:15 -0400 Subject: [PATCH 030/148] topic string should not be encoded twice, redundant code --- adafruit_minimqtt.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b59707a..30943af 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -279,11 +279,9 @@ def publish(self, topic, msg, retain=False, qos=0): :param bool retain: Whether the message is saved by the broker. :param int qos: Quality of Service level for the message. """ - # check topic kwarg - topic_str = str(topic).encode('utf-8') - if topic_str is None or len(topic_str) == 0: + if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) - if b'+' in topic_str or b'#' in topic_str: + if '+' in topic or '#' in topic: raise MQTTException('Topic can not contain wildcards.') # check msg/qos kwargs if msg is None: @@ -359,6 +357,7 @@ def subscribe(self, topic, handler_method=None, qos=0): self._sock.write(qos.to_bytes(1, "little")) while 1: op = self.wait_for_msg() + print('OP: ', op) if op == 0x90: resp = self._sock.read(4) assert resp[1] == pkt[2] and resp[2] == pkt[3] From af3cce89a0bea7315bf69d2592cdf406b9a57f08 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 16:44:03 -0400 Subject: [PATCH 031/148] add last_will method to allow user-defined LWT, incorp. into init. add PUBACK errors --- adafruit_minimqtt.py | 52 +++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 30943af..b5b1534 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -54,7 +54,7 @@ MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) -# MQTT Errors (for handle_mqtt_error method) +# General MQTT Errors MQTT_ERR_INCORRECT_SERVER = const(3) MQTT_ERR_INVALID = const(4) MQTT_ERR_NO_CONN = const(5) @@ -62,6 +62,17 @@ MQTT_INVALID_QOS = const(7) MQTT_INVALID_WILDCARD = const(8) +# PUBACK Errors +MQTT_PUBACK_OK = const(0x00) +MQTT_PUBACK_ERR_SUBS = const(0x10) +MQTT_PUBACK_ERR_UNSPECIFIED = const(0x80) +MQTT_PUBACK_ERR_IMPL = const(0x83) +MQTT_PUBACK_ERR_UNAUTHORIZED = const(0x87) +MQTT_PUBACK_ERR_INVALID_TOPIC = const(0x90) +MQTT_PUBACK_ERR_PACKETID = const(0x91) +MQTT_PUBACK_ERR_QUOTA = const(0x97) +MQTT_PUBACK_ERR_PAYLOAD = const(0x99) + # MQTT Spec. Commands MQTT_TLS_PORT = const(8883) MQTT_TCP_PORT = const(1883) @@ -101,14 +112,14 @@ class MQTT: :param socket: ESP32SPI Socket object. :param str server_address: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. - :param str user: Username for broker authentication. + :param str username: Username for broker authentication. :param str password: Password for broker authentication. :param str client_id: Optional client identifier, defaults to a randomly generated id. :param bool is_ssl: Enables TCP mode if false (port 1883). Defaults to True (port 8883). """ TCP_MODE = const(0) TLS_MODE = const(2) - def __init__(self, esp, socket, server_address, port=8883, user=None, + def __init__(self, esp, socket, server_address, port=8883, username=None, password = None, client_id=None, is_ssl=True): if esp and socket is not None: self._esp = esp @@ -118,7 +129,8 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, self.port = port if not is_ssl: self.port = MQTT_TCP_PORT - self._set_username_password(user, password) + self._user = username + self._pass = password if client_id is not None: # user-defined client_id MAY allow client_id's > 23 bytes or # non-alpha-numeric characters @@ -134,9 +146,7 @@ def __init__(self, esp, socket, server_address, port=8883, user=None, self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self._lw_topic = None - self._lw_msg = None - self._lw_retain = False + self.last_will(None, None, 0, False) self._is_connected = False self._pid = 0 self._msg_size_lim = MQTT_MSG_SZ_LIM @@ -152,16 +162,19 @@ def deinit(self): """ self.disconnect() - def _set_username_password(self, username, password=None): - """Set up username and optional password for authentication. - :param str username: MQTT broker username + def last_will(self, topic=None, message=None, qos=0, retain=False): + """Sets the last will and testament properties. MUST be called before connect(). + :param str topic: MQTT Broker topic. + :param str message: Last will disconnection message. + :param int qos: Quality of Service level. + :param bool retain: Specifies if the message is to be retained when it is published. """ - if username is None: - self._user = None - else: - # Username must be a UTF-8 encoded string [MQTT-3.1.3-12] - self._user = username.encode('utf-8') - self._pass = password + if qos < 0 or qos > 2: + handle_mqtt_error(MQTT_INVALID_QOS) + self._lw_qos = qos + self._lw_topic = topic + self._lw_msg = message + self._lw_retain = retain def reconnect(self, retries=30): """Attempts to reconnect to the MQTT broker. @@ -233,9 +246,12 @@ def connect(self, clean_session=True): # [MQTT-3.1.3-4] self._send_str(self._client_id) if self._lw_topic: + # [MQTT-3.1.3-11] self._send_str(self._lw_topic) self._send_str(self._lw_msg) - if self._user is not None: + if self._user is None: + self._user = None + else: self._send_str(self._user) self._send_str(self._pass) resp = self._sock.read(4) @@ -323,6 +339,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) + print('RCV PID: ', rcv_pid) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] if pid == rcv_pid: return @@ -365,6 +382,7 @@ def subscribe(self, topic, handler_method=None, qos=0): raise MQTTException(resp[3]) return + # TODO: Implement unsubscibe def wait_for_msg(self, timeout = 1.0): """Waits for and processes network events. From 6a0d96e4e116239f6ef668ed8a429b611b92d730 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 17:16:14 -0400 Subject: [PATCH 032/148] start work on unsubscribe --- adafruit_minimqtt.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b5b1534..86dfcc2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -382,15 +382,20 @@ def subscribe(self, topic, handler_method=None, qos=0): raise MQTTException(resp[3]) return - # TODO: Implement unsubscibe + def unsubscribe(self, topic): + """Unsubscribes the MQTT client from the MQTT Broker. + :param str topic: MQTT subscription topic. + """ + if not topic in self._handler_methods: + raise MiniMQTTException('Can not unsubscribe - topic was not subscribed to.') + - def wait_for_msg(self, timeout = 1.0): + def wait_for_msg(self, blocking=True): """Waits for and processes network events. + :param bool blocking: Set the blocking or non-blocking mode of the socket. :param float timeout: The time in seconds to wait for network traffic before returning. """ - if timeout < 0.0: - raise ValueError('timeout must be > 0.0 seconds.') - self._sock.settimeout(timeout) + self._sock.settimeout(0.0) res = self._sock.read(1) if res in [None, b""]: return None @@ -422,6 +427,11 @@ def wait_for_msg(self, timeout = 1.0): self._sock.write(pkt) elif op & 6 == 4: assert 0 + + def check_msg(self): + """Non-blocking version of wait_for_msg + """ + return self.wait_for_msg(False) def _recv_len(self): """Receives the size of the topic length.""" From 91648c4168e9d62e7889fc744fc8b035fb86427d Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 17:27:11 -0400 Subject: [PATCH 033/148] pop topic from method handler dictionary on unsubscribe. handle publish and subscribe cases where there is no socket initalized due to a possible deinit. --- adafruit_minimqtt.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 86dfcc2..92d323c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -312,6 +312,8 @@ def publish(self, topic, msg, retain=False, qos=0): raise MQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: handle_mqtt_error(MQTT_INVALID_QOS) + if self._sock is None: + handle_mqtt_error(MQTT_ERR_NO_CONN) pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -364,6 +366,8 @@ def subscribe(self, topic, handler_method=None, qos=0): self._handler_methods.update( {topic : self.default_sub_handler} ) else: self._handler_methods.update( {topic : custom_handler_method} ) + if self._sock is None: + handle_mqtt_error(MQTT_ERR_NO_CONN) # TODO: Allow topic to be a tuple, multiple topic subscriptions pkt = MQTT_SUB_PKT_TYPE self._pid += 11 @@ -388,6 +392,11 @@ def unsubscribe(self, topic): """ if not topic in self._handler_methods: raise MiniMQTTException('Can not unsubscribe - topic was not subscribed to.') + if topic is None or len(topic) == 0: + handle_mqtt_error(MQTT_INVALID_TOPIC) + # remove topic from handler methods dict. + self._handler_methods.pop(topic) + print(self._handler_methods) def wait_for_msg(self, blocking=True): From 07a349cbe2c8fb150bb1db3d5f7771a4a4e0be0e Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 17:40:11 -0400 Subject: [PATCH 034/148] added inline docstring examples to subscription method --- adafruit_minimqtt.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 92d323c..b001a4b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -349,14 +349,40 @@ def publish(self, topic, msg, retain=False, qos=0): assert 0 def subscribe(self, topic, handler_method=None, qos=0): - """Sends a subscribe message to the MQTT broker. + """Subscribes to a topic on the MQTT Broker. + This method can subscribe to one topics or multiple topics. :param str topic: Unique topic identifier. - :param - :param method handler_method: Method for handling messages recieved from a - topic. Defaults to default_sub_handler if None. + :param method handler_method: Predefined method for handling messages + recieved from a topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. - """ + Example of subscribing to one topic: + .. code-block:: python + mqtt_client.subscribe('topics/ledState') + + Example of subscribing to one topic and setting the Quality of Service level to 1: + .. code-block:: python + mqtt_client.subscribe('topics/ledState', 1) + + Example of subscribing to one topic and attaching a method handler: + .. code-block:: python + mqtt_client.subscribe('topics/ledState', led_setter) + + Example of subscribing to multiple topics: + .. code-block:: python + mqtt_client.subscribe([('brubell/feeds/ledState'), ('topics/ServoState')]) + """ + # TODO: Topic tuple list implementation + # multiple subscriptions to different topics, handler methods + if isinstance(topic, list): + print("Topics: ", topic) + for i in len(topic): + topics = topic[i][0] + method_handlers = topic[i][1] + if topic[i][2]: + qos = topic[i][2] + else: + qos = 2 if qos < 0 or qos > 2: raise MQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: @@ -368,7 +394,6 @@ def subscribe(self, topic, handler_method=None, qos=0): self._handler_methods.update( {topic : custom_handler_method} ) if self._sock is None: handle_mqtt_error(MQTT_ERR_NO_CONN) - # TODO: Allow topic to be a tuple, multiple topic subscriptions pkt = MQTT_SUB_PKT_TYPE self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) From 1efb73f04366d4fe37c9b8c097aa61f09e0ed6b4 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 18:01:05 -0400 Subject: [PATCH 035/148] Add publish_multiple method - publish to multiple topics at once! --- adafruit_minimqtt.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b001a4b..2755b33 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -288,6 +288,34 @@ def mqtt_msg(self, msg_size): if msg_size < MQTT_MSG_MAX_SZ: self.__msg_size_lim = msg_size + def publish_multiple(self, data, timeout=1.0): + """Publishes to multiple MQTT broker topics. + :param tuple data: A list of tuple format: + :param str topic: Unique topic identifier. + :param str msg: Data to send to the broker. + :param bool retain: Whether the message is saved by the broker. + :param int qos: Quality of Service level for the message. + :param float timeout: Timeout between calls to publish(). This value + is usually set by your MQTT broker. Defaults to 1.0 + """ + for i in range(len(data)): + topic = data[i][0] + msg = data[i][1] + try: + if data[i][2]: + retain = data[i][2] + except IndexError: + retain = False + pass + try: + if data[i][3]: + qos = data[i][3] + except IndexError: + qos = 0 + pass + self.publish(topic, msg, retain, qos) + time.sleep(timeout) + def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. :param str topic: Unique topic identifier. @@ -403,7 +431,6 @@ def subscribe(self, topic, handler_method=None, qos=0): self._sock.write(qos.to_bytes(1, "little")) while 1: op = self.wait_for_msg() - print('OP: ', op) if op == 0x90: resp = self._sock.read(4) assert resp[1] == pkt[2] and resp[2] == pkt[3] From 9081744be8bc75b114fbf91e6daaf9063782ef83 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 1 Jul 2019 18:08:39 -0400 Subject: [PATCH 036/148] add subscribe_multiple - not working yet! - subscribes to multiple broker topics --- adafruit_minimqtt.py | 69 ++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2755b33..f4d62b2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -142,7 +142,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') # subscription method handler dictionary - self._handler_methods = {} + self._method_handlers = {} self.server = server_address self.packet_id = 0 self._keep_alive = 0 @@ -376,11 +376,11 @@ def publish(self, topic, msg, retain=False, qos=0): elif qos == 2: assert 0 - def subscribe(self, topic, handler_method=None, qos=0): + def subscribe(self, topic, method_handler=None, qos=0): """Subscribes to a topic on the MQTT Broker. This method can subscribe to one topics or multiple topics. :param str topic: Unique topic identifier. - :param method handler_method: Predefined method for handling messages + :param method method_handler: Predefined method for handling messages recieved from a topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. @@ -395,31 +395,16 @@ def subscribe(self, topic, handler_method=None, qos=0): Example of subscribing to one topic and attaching a method handler: .. code-block:: python mqtt_client.subscribe('topics/ledState', led_setter) - - Example of subscribing to multiple topics: - .. code-block:: python - mqtt_client.subscribe([('brubell/feeds/ledState'), ('topics/ServoState')]) """ - # TODO: Topic tuple list implementation - # multiple subscriptions to different topics, handler methods - if isinstance(topic, list): - print("Topics: ", topic) - for i in len(topic): - topics = topic[i][0] - method_handlers = topic[i][1] - if topic[i][2]: - qos = topic[i][2] - else: - qos = 2 if qos < 0 or qos > 2: raise MQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) - # associate topic subscription with handler_method. - if handler_method is None: - self._handler_methods.update( {topic : self.default_sub_handler} ) + # associate topic subscription with method_handler. + if method_handler is None: + self._method_handlers.update( {topic : self.default_sub_handler} ) else: - self._handler_methods.update( {topic : custom_handler_method} ) + self._method_handlers.update( {topic : custom_method_handler} ) if self._sock is None: handle_mqtt_error(MQTT_ERR_NO_CONN) pkt = MQTT_SUB_PKT_TYPE @@ -438,17 +423,45 @@ def subscribe(self, topic, handler_method=None, qos=0): raise MQTTException(resp[3]) return + def subscribe_multiple(self, topic_info, timeout=1.0): + """Subscribes to multiple MQTT broker topics. + :param tuple topic_info: A list of tuple format: + :param str topic: Unique topic identifier. + :param method method_handler: Predefined method for handling messages + recieved from a topic. Defaults to default_sub_handler if None. + :param int qos: Quality of Service level for the topic. Defaults to 0. + :param float timeout: Timeout between calls to subscribe(). + """ + print('topics:', topic_info) + for i in range(len(topic_info)): + topic = topic_info[i][0] + try: + if topic_info[i][1]: + method_handler = topic_info[i][1] + except IndexError: + method_handler = None + pass + try: + if topic_info[i][2]: + qos = topic_info[i][2] + except IndexError: + qos = 0 + pass + print('Subscribing to:', topic, method_handler, qos) + self.subscribe(topic, method_handler, qos) + time.sleep(timeout) + def unsubscribe(self, topic): """Unsubscribes the MQTT client from the MQTT Broker. :param str topic: MQTT subscription topic. """ - if not topic in self._handler_methods: + if not topic in self._method_handlers: raise MiniMQTTException('Can not unsubscribe - topic was not subscribed to.') if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) # remove topic from handler methods dict. - self._handler_methods.pop(topic) - print(self._handler_methods) + self._method_handlers.pop(topic) + print(self._method_handlers) def wait_for_msg(self, blocking=True): @@ -479,9 +492,9 @@ def wait_for_msg(self, blocking=True): sz -= 2 msg = self._sock.read(sz) # call the topic's handler method - if topic in self._handler_methods: - handler_method = self._handler_methods[topic] - handler_method(topic, str(msg, 'utf-8')) + if topic in self._method_handlers: + method_handler = self._method_handlers[topic] + method_handler(topic, str(msg, 'utf-8')) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) From 62515361604da48e7c74e28452bfc7d51cf4379d Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 11:38:07 -0400 Subject: [PATCH 037/148] check for is_connected before will is set --- adafruit_minimqtt.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f4d62b2..7c8c74a 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -143,11 +143,11 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, raise ValueError('MQTT Client ID must be between 1 and 23 bytes') # subscription method handler dictionary self._method_handlers = {} + self._is_connected = False self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self.last_will(None, None, 0, False) - self._is_connected = False + self.last_will() self._pid = 0 self._msg_size_lim = MQTT_MSG_SZ_LIM @@ -169,6 +169,8 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): :param int qos: Quality of Service level. :param bool retain: Specifies if the message is to be retained when it is published. """ + if self._is_connected: + raise MiniMQTTException('Last Will should be defined BEFORE connect() is called.') if qos < 0 or qos > 2: handle_mqtt_error(MQTT_INVALID_QOS) self._lw_qos = qos @@ -191,6 +193,8 @@ def reconnect(self, retries=30): retries = 0 self._esp.reset() continue + # TODO: If we disconnected, we should re-subscribe to + # all the topics held in the dict! def is_connected(self): """Returns if there is an active MQTT connection.""" @@ -255,6 +259,7 @@ def connect(self, clean_session=True): self._send_str(self._user) self._send_str(self._pass) resp = self._sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] !=0: raise MiniMQTTException(resp[3]) @@ -501,11 +506,7 @@ def wait_for_msg(self, blocking=True): self._sock.write(pkt) elif op & 6 == 4: assert 0 - - def check_msg(self): - """Non-blocking version of wait_for_msg - """ - return self.wait_for_msg(False) + # TODO: return a value if successful, RETURN OP def _recv_len(self): """Receives the size of the topic length.""" From c9051fd6ee35c7236a6730fb3c99f53442a6bbe9 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 12:24:32 -0400 Subject: [PATCH 038/148] minimqttexception -> mmqtexception --- adafruit_minimqtt.py | 49 +++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 7c8c74a..96bd202 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -88,21 +88,21 @@ def handle_mqtt_error(mqtt_err): :param int mqtt_err: MQTT error number. """ if mqtt_err == MQTT_ERR_INCORRECT_SERVER: - raise MiniMQTTException("Invalid server address defined.") + raise MMQTTException("Invalid server address defined.") elif mqtt_err == MQTT_ERR_INVALID: - raise MiniMQTTException("Invalid method arguments provided.") + raise MMQTTException("Invalid method arguments provided.") elif mqtt_err == MQTT_ERR_NO_CONN: - raise MiniMQTTException("MiniMQTT not connected.") + raise MMQTTException("MiniMQTT not connected.") elif mqtt_err == MQTT_INVALID_TOPIC: - raise MiniMQTTException("Invalid MQTT Topic, must have length > 0.") + raise MMQTTException("Invalid MQTT Topic, must have length > 0.") elif mqtt_err == MQTT_INVALID_QOS: - raise MiniMQTTException("Invalid QoS level, must be between 0 and 2.") + raise MMQTTException("Invalid QoS level, must be between 0 and 2.") elif mqtt_err == MQTT_INVALID_WILDCARD: - raise MiniMQTTException("Invalid MQTT Wildcard - must be * or #.") + raise MMQTTException("Invalid MQTT Wildcard - must be * or #.") else: - raise MiniMQTTException("Unknown error!") + raise MMQTTException("Unknown error!") -class MiniMQTTException(Exception): +class MMQTTException(Exception): pass class MQTT: @@ -170,7 +170,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): :param bool retain: Specifies if the message is to be retained when it is published. """ if self._is_connected: - raise MiniMQTTException('Last Will should be defined BEFORE connect() is called.') + raise MMQTTException('Last Will should be defined BEFORE connect() is called.') if qos < 0 or qos > 2: handle_mqtt_error(MQTT_INVALID_QOS) self._lw_qos = qos @@ -262,7 +262,7 @@ def connect(self, clean_session=True): assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] !=0: - raise MiniMQTTException(resp[3]) + raise MMQTTException(resp[3]) self._is_connected = True return resp[2] & 1 @@ -331,18 +331,18 @@ def publish(self, topic, msg, retain=False, qos=0): if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) if '+' in topic or '#' in topic: - raise MQTTException('Topic can not contain wildcards.') + raise MMQTTException('Topic can not contain wildcards.') # check msg/qos kwargs if msg is None: - raise MQTTException('Message can not be None.') + raise MMQTTException('Message can not be None.') elif isinstance(msg, (int, float)): msg = str(msg).encode('ascii') elif isinstance(msg, str): msg = str(msg).encode('utf-8') else: - raise MQTTException('Invalid message data type.') + raise MMQTTException('Invalid message data type.') if len(msg) > MQTT_MSG_MAX_SZ: - raise MQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) + raise MMQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: handle_mqtt_error(MQTT_INVALID_QOS) if self._sock is None: @@ -402,9 +402,14 @@ def subscribe(self, topic, method_handler=None, qos=0): mqtt_client.subscribe('topics/ledState', led_setter) """ if qos < 0 or qos > 2: - raise MQTTException('QoS level must be between 1 and 2.') + raise MMQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) + try: + if self._method_handlers[topic]: + raise MMQTTException('Already subscribed to %s!'%topic) + except: + pass # associate topic subscription with method_handler. if method_handler is None: self._method_handlers.update( {topic : self.default_sub_handler} ) @@ -425,7 +430,7 @@ def subscribe(self, topic, method_handler=None, qos=0): resp = self._sock.read(4) assert resp[1] == pkt[2] and resp[2] == pkt[3] if resp[3] == 0x80: - raise MQTTException(resp[3]) + raise MMQTTException(resp[3]) return def subscribe_multiple(self, topic_info, timeout=1.0): @@ -456,18 +461,6 @@ def subscribe_multiple(self, topic_info, timeout=1.0): self.subscribe(topic, method_handler, qos) time.sleep(timeout) - def unsubscribe(self, topic): - """Unsubscribes the MQTT client from the MQTT Broker. - :param str topic: MQTT subscription topic. - """ - if not topic in self._method_handlers: - raise MiniMQTTException('Can not unsubscribe - topic was not subscribed to.') - if topic is None or len(topic) == 0: - handle_mqtt_error(MQTT_INVALID_TOPIC) - # remove topic from handler methods dict. - self._method_handlers.pop(topic) - print(self._method_handlers) - def wait_for_msg(self, blocking=True): """Waits for and processes network events. From 155c67977b32bdfa35bdbd1a430c78987bfa0c84 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 12:34:42 -0400 Subject: [PATCH 039/148] check if topic is already subscribed to before subscribing --- adafruit_minimqtt.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 96bd202..8c14058 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -405,11 +405,8 @@ def subscribe(self, topic, method_handler=None, qos=0): raise MMQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: handle_mqtt_error(MQTT_INVALID_TOPIC) - try: - if self._method_handlers[topic]: - raise MMQTTException('Already subscribed to %s!'%topic) - except: - pass + if topic in self._method_handlers: + raise MMQTTException('Already subscribed to topic.') # associate topic subscription with method_handler. if method_handler is None: self._method_handlers.update( {topic : self.default_sub_handler} ) From c6250f542d5a811dee0bf0671de0e3c064028343 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 12:52:02 -0400 Subject: [PATCH 040/148] associate return codes with their description --- adafruit_minimqtt.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8c14058..025722a 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -62,6 +62,8 @@ MQTT_INVALID_QOS = const(7) MQTT_INVALID_WILDCARD = const(8) +# TODO: Set Puback and Connack to dicionary items so they can be handled by handle_mqtt_error!! + # PUBACK Errors MQTT_PUBACK_OK = const(0x00) MQTT_PUBACK_ERR_SUBS = const(0x10) @@ -73,6 +75,17 @@ MQTT_PUBACK_ERR_QUOTA = const(0x97) MQTT_PUBACK_ERR_PAYLOAD = const(0x99) +# CONNACK Errors +CONACK_RESP = {const(0x00) : 'Connection Accepted!', + const(0x05) : 'Connection Refused - Incorrect Protocol Version'} + +MQTT_CONACK_ACCEPTED = const(0x00) +MQTT_CONACK_ERR_VER = const(0x01) +MQTT_CONACK_ERR_ID = const(0x02) +MQTT_CONACK_ERR_SERVER = const(0x03) +MQTT_CONACK_ERR_CREDS = const(0x04) +MQTT_CONACK_ERR_UNAUTHORIZED = const(0x05) + # MQTT Spec. Commands MQTT_TLS_PORT = const(8883) MQTT_TCP_PORT = const(1883) @@ -85,7 +98,7 @@ def handle_mqtt_error(mqtt_err): """Returns string associated with MQTT error number. - :param int mqtt_err: MQTT error number. + :param int mqtt_err: MQTT error number or type. """ if mqtt_err == MQTT_ERR_INCORRECT_SERVER: raise MMQTTException("Invalid server address defined.") @@ -262,7 +275,7 @@ def connect(self, clean_session=True): assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] !=0: - raise MMQTTException(resp[3]) + raise MMQTTException(CONACK_RESP[resp[3]]) self._is_connected = True return resp[2] & 1 @@ -370,11 +383,11 @@ def publish(self, topic, msg, retain=False, qos=0): if qos == 1: while 1: op = self.wait_for_msg() + print(op) if op == const(0x40): sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) - print('RCV PID: ', rcv_pid) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] if pid == rcv_pid: return From f74459944415ced35400211e9bf820e495ef0d57 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 13:21:52 -0400 Subject: [PATCH 041/148] removal of puback error codes, mqtt error handling in favor of cleaner mmqtt exception class --- adafruit_minimqtt.py | 97 ++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 71 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 025722a..d5e819b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -54,38 +54,6 @@ MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) -# General MQTT Errors -MQTT_ERR_INCORRECT_SERVER = const(3) -MQTT_ERR_INVALID = const(4) -MQTT_ERR_NO_CONN = const(5) -MQTT_INVALID_TOPIC = const(6) -MQTT_INVALID_QOS = const(7) -MQTT_INVALID_WILDCARD = const(8) - -# TODO: Set Puback and Connack to dicionary items so they can be handled by handle_mqtt_error!! - -# PUBACK Errors -MQTT_PUBACK_OK = const(0x00) -MQTT_PUBACK_ERR_SUBS = const(0x10) -MQTT_PUBACK_ERR_UNSPECIFIED = const(0x80) -MQTT_PUBACK_ERR_IMPL = const(0x83) -MQTT_PUBACK_ERR_UNAUTHORIZED = const(0x87) -MQTT_PUBACK_ERR_INVALID_TOPIC = const(0x90) -MQTT_PUBACK_ERR_PACKETID = const(0x91) -MQTT_PUBACK_ERR_QUOTA = const(0x97) -MQTT_PUBACK_ERR_PAYLOAD = const(0x99) - -# CONNACK Errors -CONACK_RESP = {const(0x00) : 'Connection Accepted!', - const(0x05) : 'Connection Refused - Incorrect Protocol Version'} - -MQTT_CONACK_ACCEPTED = const(0x00) -MQTT_CONACK_ERR_VER = const(0x01) -MQTT_CONACK_ERR_ID = const(0x02) -MQTT_CONACK_ERR_SERVER = const(0x03) -MQTT_CONACK_ERR_CREDS = const(0x04) -MQTT_CONACK_ERR_UNAUTHORIZED = const(0x05) - # MQTT Spec. Commands MQTT_TLS_PORT = const(8883) MQTT_TCP_PORT = const(1883) @@ -96,24 +64,11 @@ MQTT_CON_PREMSG = bytearray(b"\x10\0\0") MQTT_CON_MSG = bytearray(b"\x04MQTT\x04\x02\0\0") -def handle_mqtt_error(mqtt_err): - """Returns string associated with MQTT error number. - :param int mqtt_err: MQTT error number or type. - """ - if mqtt_err == MQTT_ERR_INCORRECT_SERVER: - raise MMQTTException("Invalid server address defined.") - elif mqtt_err == MQTT_ERR_INVALID: - raise MMQTTException("Invalid method arguments provided.") - elif mqtt_err == MQTT_ERR_NO_CONN: - raise MMQTTException("MiniMQTT not connected.") - elif mqtt_err == MQTT_INVALID_TOPIC: - raise MMQTTException("Invalid MQTT Topic, must have length > 0.") - elif mqtt_err == MQTT_INVALID_QOS: - raise MMQTTException("Invalid QoS level, must be between 0 and 2.") - elif mqtt_err == MQTT_INVALID_WILDCARD: - raise MMQTTException("Invalid MQTT Wildcard - must be * or #.") - else: - raise MMQTTException("Unknown error!") +CONNACK_ERRORS = {const(0x01) : 'Connection Refused - Incorrect Protocol Version', + const(0x02) : 'Connection Refused - ID Rejected', + const(0x03) : 'Connection Refused - Server unavailable', + const(0x04) : 'Connection Refused - Incorrect username/password', + const(0x05) : 'Connection Refused - Unauthorized'} class MMQTTException(Exception): pass @@ -157,12 +112,12 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, # subscription method handler dictionary self._method_handlers = {} self._is_connected = False + self._msg_size_lim = MQTT_MSG_SZ_LIM self.server = server_address self.packet_id = 0 self._keep_alive = 0 - self.last_will() self._pid = 0 - self._msg_size_lim = MQTT_MSG_SZ_LIM + self.last_will() def __enter__(self): return self @@ -185,7 +140,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): if self._is_connected: raise MMQTTException('Last Will should be defined BEFORE connect() is called.') if qos < 0 or qos > 2: - handle_mqtt_error(MQTT_INVALID_QOS) + raise MMQTTException("Invalid QoS level, must be between 0 and 2.") self._lw_qos = qos self._lw_topic = topic self._lw_msg = message @@ -218,7 +173,7 @@ def connect(self, clean_session=True): :param bool clean_session: Establishes a persistent session with the broker. Defaults to a non-persistent session. """ - if self._esp: #TODO: This might be approachable without passing in a socket + if self._esp: self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: @@ -228,13 +183,13 @@ def connect(self, clean_session=True): try: self._sock.connect((self.server, self.port), TLS_MODE) except RuntimeError: - handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) + raise MMQTTException("Invalid server address defined.") else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] try: self._sock.connect(addr, TCP_MODE) except RuntimeError: - handle_mqtt_error(MQTT_ERR_INCORRECT_SERVER) + raise MMQTTException("Invalid server address defined.") premsg = MQTT_CON_PREMSG msg = MQTT_CON_MSG @@ -271,19 +226,19 @@ def connect(self, clean_session=True): else: self._send_str(self._user) self._send_str(self._pass) - resp = self._sock.read(4) + rc = self._sock.read(4) - assert resp[0] == 0x20 and resp[1] == 0x02 - if resp[3] !=0: - raise MMQTTException(CONACK_RESP[resp[3]]) + assert rc[0] == 0x20 and rc[1] == 0x02 + if rc[3] !=0: + raise MMQTTException(CONNACK_ERRORS[rc[3]]) self._is_connected = True - return resp[2] & 1 + return rc[2] & 1 def disconnect(self): """Disconnects from the broker. """ if self._sock is None: - handle_mqtt_error(MQTT_ERR_NO_CONN) + raise MMQTTException("MiniMQTT not connected.") self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False @@ -342,7 +297,7 @@ def publish(self, topic, msg, retain=False, qos=0): :param int qos: Quality of Service level for the message. """ if topic is None or len(topic) == 0: - handle_mqtt_error(MQTT_INVALID_TOPIC) + raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if '+' in topic or '#' in topic: raise MMQTTException('Topic can not contain wildcards.') # check msg/qos kwargs @@ -357,9 +312,9 @@ def publish(self, topic, msg, retain=False, qos=0): if len(msg) > MQTT_MSG_MAX_SZ: raise MMQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: - handle_mqtt_error(MQTT_INVALID_QOS) + raise MMQTTException("Invalid QoS level, must be between 0 and 2.") if self._sock is None: - handle_mqtt_error(MQTT_ERR_NO_CONN) + raise MMQTTException("MiniMQTT not connected.") pkt = bytearray(b"\x30\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -417,7 +372,7 @@ def subscribe(self, topic, method_handler=None, qos=0): if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: - handle_mqtt_error(MQTT_INVALID_TOPIC) + raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if topic in self._method_handlers: raise MMQTTException('Already subscribed to topic.') # associate topic subscription with method_handler. @@ -426,7 +381,7 @@ def subscribe(self, topic, method_handler=None, qos=0): else: self._method_handlers.update( {topic : custom_method_handler} ) if self._sock is None: - handle_mqtt_error(MQTT_ERR_NO_CONN) + raise MMQTTException("MiniMQTT not connected.") pkt = MQTT_SUB_PKT_TYPE self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) @@ -437,10 +392,10 @@ def subscribe(self, topic, method_handler=None, qos=0): while 1: op = self.wait_for_msg() if op == 0x90: - resp = self._sock.read(4) - assert resp[1] == pkt[2] and resp[2] == pkt[3] - if resp[3] == 0x80: - raise MMQTTException(resp[3]) + rc = self._sock.read(4) + assert rc[1] == pkt[2] and rc[2] == pkt[3] + if rc[3] == 0x80: + raise MMQTTException(rc[3]) return def subscribe_multiple(self, topic_info, timeout=1.0): From 5a3b4afeddd4f1da26d54ab313c8369c5e01a047 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 13:41:01 -0400 Subject: [PATCH 042/148] ping method checks against pingresp, returns if true. raises if false --- adafruit_minimqtt.py | 103 +++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index d5e819b..0904cce 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -244,50 +244,17 @@ def disconnect(self): self._is_connected = False def ping(self): - """Pings the broker. + """Pings the MQTT Broker to confirm if the server is alive or + if the network connection is active. + Raises an error if server is not alive. + Returns PINGRESP if server is alive. """ + # note: sock.write handles the PINGRESP self._sock.write(MQTT_PING_REQ) - - @property - def mqtt_msg(self): - """Returns maximum MQTT payload and topic size.""" - return self._msg_size_lim, MQTT_TOPIC_SZ_LIMIT - - @mqtt_msg.setter - def mqtt_msg(self, msg_size): - """Sets the maximum MQTT message payload size. - :param int msg_size: Maximum MQTT payload size. - """ - if msg_size < MQTT_MSG_MAX_SZ: - self.__msg_size_lim = msg_size - - def publish_multiple(self, data, timeout=1.0): - """Publishes to multiple MQTT broker topics. - :param tuple data: A list of tuple format: - :param str topic: Unique topic identifier. - :param str msg: Data to send to the broker. - :param bool retain: Whether the message is saved by the broker. - :param int qos: Quality of Service level for the message. - :param float timeout: Timeout between calls to publish(). This value - is usually set by your MQTT broker. Defaults to 1.0 - """ - for i in range(len(data)): - topic = data[i][0] - msg = data[i][1] - try: - if data[i][2]: - retain = data[i][2] - except IndexError: - retain = False - pass - try: - if data[i][3]: - qos = data[i][3] - except IndexError: - qos = 0 - pass - self.publish(topic, msg, retain, qos) - time.sleep(timeout) + res = self._sock.read(1) + if res != MQTT_PINGRESP: + raise MMQTTException('PINGRESP was not received') + return res def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. @@ -395,9 +362,50 @@ def subscribe(self, topic, method_handler=None, qos=0): rc = self._sock.read(4) assert rc[1] == pkt[2] and rc[2] == pkt[3] if rc[3] == 0x80: - raise MMQTTException(rc[3]) + raise MMQTTException('SUBACK Failure!') return + @property + def mqtt_msg(self): + """Returns maximum MQTT payload and topic size.""" + return self._msg_size_lim, MQTT_TOPIC_SZ_LIMIT + + @mqtt_msg.setter + def mqtt_msg(self, msg_size): + """Sets the maximum MQTT message payload size. + :param int msg_size: Maximum MQTT payload size. + """ + if msg_size < MQTT_MSG_MAX_SZ: + self.__msg_size_lim = msg_size + + def publish_multiple(self, data, timeout=1.0): + """Publishes to multiple MQTT broker topics. + :param tuple data: A list of tuple format: + :param str topic: Unique topic identifier. + :param str msg: Data to send to the broker. + :param bool retain: Whether the message is saved by the broker. + :param int qos: Quality of Service level for the message. + :param float timeout: Timeout between calls to publish(). This value + is usually set by your MQTT broker. Defaults to 1.0 + """ + for i in range(len(data)): + topic = data[i][0] + msg = data[i][1] + try: + if data[i][2]: + retain = data[i][2] + except IndexError: + retain = False + pass + try: + if data[i][3]: + qos = data[i][3] + except IndexError: + qos = 0 + pass + self.publish(topic, msg, retain, qos) + time.sleep(timeout) + def subscribe_multiple(self, topic_info, timeout=1.0): """Subscribes to multiple MQTT broker topics. :param tuple topic_info: A list of tuple format: @@ -428,7 +436,7 @@ def subscribe_multiple(self, topic_info, timeout=1.0): def wait_for_msg(self, blocking=True): - """Waits for and processes network events. + """Waits for and processes network events. Returns if successful. :param bool blocking: Set the blocking or non-blocking mode of the socket. :param float timeout: The time in seconds to wait for network traffic before returning. """ @@ -464,7 +472,7 @@ def wait_for_msg(self, blocking=True): self._sock.write(pkt) elif op & 6 == 4: assert 0 - # TODO: return a value if successful, RETURN OP + return op def _recv_len(self): """Receives the size of the topic length.""" @@ -480,13 +488,12 @@ def _recv_len(self): def default_sub_handler(self, topic, msg): """Default feed subscription handler method. :param str topic: Subscription topic. - :param str msg: Payload content. + :param str msg: Message content. """ - print(msg) print('New message on {0}: {1}'.format(topic, msg)) def _send_str(self, string): - """Packs a string into a struct, and writes it to a socket as a utf-8 encoded string. + """Packs a string into a struct, and writes it to a socket as an utf-8 encoded string. :param str string: String to write to the socket. """ self._sock.write(struct.pack("!H", len(string))) From 1fe9cdaf8b7b979dfe2806524dfec57186ac3655 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 15:02:06 -0400 Subject: [PATCH 043/148] start sketching on on_connect for paho-style ack callbacks --- adafruit_minimqtt.py | 88 +++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 0904cce..390fd24 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -49,33 +49,34 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" -# length of maximum mqtt message +# Client-specific variables MQTT_MSG_MAX_SZ = const(268435455) MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) - -# MQTT Spec. Commands -MQTT_TLS_PORT = const(8883) MQTT_TCP_PORT = const(1883) + +# MQTT Commands +MQTT_PING_REQ = b'\xc0' MQTT_PINGRESP = b'\xd0' -MQTT_SUB_PKT_TYPE = bytearray(b'\x82\0\0\0') +MQTT_SUB = bytearray(b'\x82\0\0\0') +MQTT_PUB = bytearray(b'\x30\0') +MQTT_CON = bytearray(b'\x10\0\0') +# Variable header [MQTT 3.1.2] +MQTT_CON_HEADER = bytearray(b"\x04MQTT\x04\x02\0\0") MQTT_DISCONNECT = b'\xe0\0' -MQTT_PING_REQ = b'\xc0\0' -MQTT_CON_PREMSG = bytearray(b"\x10\0\0") -MQTT_CON_MSG = bytearray(b"\x04MQTT\x04\x02\0\0") CONNACK_ERRORS = {const(0x01) : 'Connection Refused - Incorrect Protocol Version', - const(0x02) : 'Connection Refused - ID Rejected', - const(0x03) : 'Connection Refused - Server unavailable', - const(0x04) : 'Connection Refused - Incorrect username/password', - const(0x05) : 'Connection Refused - Unauthorized'} + const(0x02) : 'Connection Refused - ID Rejected', + const(0x03) : 'Connection Refused - Server unavailable', + const(0x04) : 'Connection Refused - Incorrect username/password', + const(0x05) : 'Connection Refused - Unauthorized'} class MMQTTException(Exception): pass class MQTT: """ - MQTT Client for CircuitPython. + MQTT client interface for CircuitPython devices. :param esp: ESP32SPI object. :param socket: ESP32SPI Socket object. :param str server_address: Server URL or IP Address. @@ -117,6 +118,12 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self.packet_id = 0 self._keep_alive = 0 self._pid = 0 + # paho-style method callbacks + self._on_connect = None + self._on_disconnect = None + self._on_publish = None + self._on_subscribe = None + self._on_log = None self.last_will() def __enter__(self): @@ -138,7 +145,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): :param bool retain: Specifies if the message is to be retained when it is published. """ if self._is_connected: - raise MMQTTException('Last Will should be defined BEFORE connect() is called.') + raise MMQTTException('Last Will should be defined before connect() is called.') if qos < 0 or qos > 2: raise MMQTTException("Invalid QoS level, must be between 0 and 2.") self._lw_qos = qos @@ -146,9 +153,11 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): self._lw_msg = message self._lw_retain = retain - def reconnect(self, retries=30): + def reconnect(self, retries=30, resub_topics=False): """Attempts to reconnect to the MQTT broker. :param int retries: Amount of retries before resetting the ESP32 hardware. + :param bool resub_topics: Client resubscribes to previously subscribed topics upon + a successful reconnection. """ retries = 0 while not self._is_connected: @@ -161,11 +170,14 @@ def reconnect(self, retries=30): retries = 0 self._esp.reset() continue - # TODO: If we disconnected, we should re-subscribe to - # all the topics held in the dict! + self._is_connected = True + if resub_topics: + while len(self._method_handlers) > 0: + feed = self._method_handlers.popitem() + self.subscribe(feed) def is_connected(self): - """Returns if there is an active MQTT connection.""" + """Returns MQTT session status.""" return self._is_connected def connect(self, clean_session=True): @@ -190,9 +202,8 @@ def connect(self, clean_session=True): self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") - - premsg = MQTT_CON_PREMSG - msg = MQTT_CON_MSG + premsg = MQTT_CON + msg = MQTT_CON_HEADER msg[6] = clean_session << 1 sz = 12 + len(self._client_id) if self._user is not None: @@ -212,7 +223,6 @@ def connect(self, clean_session=True): sz >>= 7 i += 1 premsg[i] = sz - self._sock.write(premsg) self._sock.write(msg) # [MQTT-3.1.3-4] @@ -227,18 +237,24 @@ def connect(self, clean_session=True): self._send_str(self._user) self._send_str(self._pass) rc = self._sock.read(4) - - assert rc[0] == 0x20 and rc[1] == 0x02 + print('Packet: ', rc) + assert rc[0] == const(0x20) and rc[1] == const(0x02) if rc[3] !=0: raise MMQTTException(CONNACK_ERRORS[rc[3]]) + # connack rx'd + # TODO: figure out flag extracting + flags_dict = {} + #flags_dict['session present'] = + result = rc[2] & 1 + self._on_connect(self, rc, flags_dict, result) self._is_connected = True - return rc[2] & 1 + return result def disconnect(self): """Disconnects from the broker. """ if self._sock is None: - raise MMQTTException("MiniMQTT not connected.") + raise MMQTTException("MiniMQTT is not connected.") self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False @@ -282,7 +298,7 @@ def publish(self, topic, msg, retain=False, qos=0): raise MMQTTException("Invalid QoS level, must be between 0 and 2.") if self._sock is None: raise MMQTTException("MiniMQTT not connected.") - pkt = bytearray(b"\x30\0") + pkt = MQTT_PUB pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) if qos > 0: @@ -349,7 +365,7 @@ def subscribe(self, topic, method_handler=None, qos=0): self._method_handlers.update( {topic : custom_method_handler} ) if self._sock is None: raise MMQTTException("MiniMQTT not connected.") - pkt = MQTT_SUB_PKT_TYPE + pkt = MQTT_SUB self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) self._sock.write(pkt) @@ -434,7 +450,6 @@ def subscribe_multiple(self, topic_info, timeout=1.0): self.subscribe(topic, method_handler, qos) time.sleep(timeout) - def wait_for_msg(self, blocking=True): """Waits for and processes network events. Returns if successful. :param bool blocking: Set the blocking or non-blocking mode of the socket. @@ -500,4 +515,17 @@ def _send_str(self, string): if type(string) == str: self._sock.write(str.encode(string, 'utf-8')) else: - self._sock.write(string) \ No newline at end of file + self._sock.write(string) + + # Acknowledgement Callbacks + @property + def on_connect(self): + """Called when the MQTT broker responds to a connection request. + """ + return self._on_callback + + @on_connect.setter + def on_connect(self, rc, flags_dict, result): + """Sets the on_connect parameter + """ + self._on_connect = method \ No newline at end of file From e954938ba9b45a8f62901fb2c2c017a7f4f4976c Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 15:15:19 -0400 Subject: [PATCH 044/148] working on_connect callback, add a user_data property and setter to conform with paho's callback style --- adafruit_minimqtt.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 390fd24..24e8845 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -124,6 +124,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._on_publish = None self._on_subscribe = None self._on_log = None + self._user_data = None self.last_will() def __enter__(self): @@ -246,7 +247,7 @@ def connect(self, clean_session=True): flags_dict = {} #flags_dict['session present'] = result = rc[2] & 1 - self._on_connect(self, rc, flags_dict, result) + self._on_connect(self._user_data, rc, flags_dict, result) self._is_connected = True return result @@ -516,8 +517,25 @@ def _send_str(self, string): self._sock.write(str.encode(string, 'utf-8')) else: self._sock.write(string) + + ## Logging ## + # TODO: Set up Logging with the CircuitPython logger module. + + ## Acknowledgement Callbacks ## + + @property + def user_data(self): + """Returns the user_data variable passed to callbacks. + """ + return self._user_data - # Acknowledgement Callbacks + @user_data.setter + def user_data(self, data): + """Sets the private user_data variable passed to callbacks. + :param data: Any data type. + """ + self._user_data = data + @property def on_connect(self): """Called when the MQTT broker responds to a connection request. @@ -525,7 +543,16 @@ def on_connect(self): return self._on_callback @on_connect.setter - def on_connect(self, rc, flags_dict, result): - """Sets the on_connect parameter + def on_connect(self, method): + """Defines the method which will run when the client is connected. + :param unbound_method method: user-defined method for connections. + + The on_connect method signature takes the following format: + on_connect_method(client, userdata, flags, rc) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + :param flags: CONNACK flags. + :param int rc: Response code. """ self._on_connect = method \ No newline at end of file From f05955874d2411f571f4bbc47f8fc68e13f1b1c8 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 16:27:03 -0400 Subject: [PATCH 045/148] add on_disconnect, check for a previously defined on_ack before calling it to avoid errors --- adafruit_minimqtt.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 24e8845..3e85287 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -247,8 +247,9 @@ def connect(self, clean_session=True): flags_dict = {} #flags_dict['session present'] = result = rc[2] & 1 - self._on_connect(self._user_data, rc, flags_dict, result) self._is_connected = True + if self._on_connect is not None: + self._on_connect(self._user_data, rc, flags_dict, result) return result def disconnect(self): @@ -259,6 +260,8 @@ def disconnect(self): self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False + if self._on_disconnect is not None: + self._on_disconnect(self._user_data, 0) def ping(self): """Pings the MQTT Broker to confirm if the server is alive or @@ -544,8 +547,8 @@ def on_connect(self): @on_connect.setter def on_connect(self, method): - """Defines the method which will run when the client is connected. - :param unbound_method method: user-defined method for connections. + """Defines the method which runs when the client is connected. + :param unbound_method method: user-defined method for connection. The on_connect method signature takes the following format: on_connect_method(client, userdata, flags, rc) @@ -555,4 +558,27 @@ def on_connect(self, method): :param flags: CONNACK flags. :param int rc: Response code. """ - self._on_connect = method \ No newline at end of file + self._on_connect = method + + @property + def on_disconnect(self): + """Called when the MQTT broker responds to a disconnection request. + """ + return self._on_disconnect + + @on_disconnect.setter + def on_disconnect(self, method): + """Defines the method which runs when the client is disconnected. + :param unbound_method method: user-defined method for disconnection. + + The on_disconnect method signature takes the following format: + on_disconnect_method(client, userdata, rc) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + :param int rc: Response code. + """ + return self._on_disconnect + + + # TODO:, on_publish, on_subscribe, on_log \ No newline at end of file From 516db8d068ac47dbe595d4d427b6271eecf0b4aa Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 17:20:52 -0400 Subject: [PATCH 046/148] added on_subscribe getter/setter and handling code to subscribe method --- adafruit_minimqtt.py | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 3e85287..3cbd91d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -383,6 +383,8 @@ def subscribe(self, topic, method_handler=None, qos=0): assert rc[1] == pkt[2] and rc[2] == pkt[3] if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') + if self._on_subscribe is not None: + self.on_subscribe(self, self._user_data, rc[3]) return @property @@ -578,7 +580,47 @@ def on_disconnect(self, method): :param userdata: User data, previously set in the user_data method. :param int rc: Response code. """ - return self._on_disconnect + self._on_disconnect = method + @property + def on_publish(self): + """Called when the MQTT broker responds to a publish request. + """ + return self._on_publish + + @on_disconnect.setter + def on_disconnect(self, method): + """Defines the method which runs when the client is disconnected. + :param unbound_method method: user-defined method for disconnection. + + The on_publish method signature takes the following format: + on_publish(client, userdata, rc) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + :param int rc: Response code. + """ + self._on_publish = method + + @property + def on_subscribe(self): + """Called when the MQTT broker successfully subscribes to a feed. + """ + return self._on_subscribe + + @on_subscribe.setter + def on_subscribe(self, method): + """Defines the method which runs when a client subscribes to a feed. + + :param unbound_method method: user-defined method for disconnection. + + The on_subscribe method signature takes the following format: + on_subscribe(client, userdata, rc) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + :param int granted_qos: QoS level the broker has granted the subscription request.. + """ + self._on_subscribe = method - # TODO:, on_publish, on_subscribe, on_log \ No newline at end of file + # TODO:, on_publish, on_log \ No newline at end of file From d40288caddc7052b3376093d23cddecda514c24c Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 17:47:54 -0400 Subject: [PATCH 047/148] fix disconnect callback, publish not working --- adafruit_minimqtt.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 3cbd91d..c75a0db 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -261,7 +261,7 @@ def disconnect(self): self._sock.close() self._is_connected = False if self._on_disconnect is not None: - self._on_disconnect(self._user_data, 0) + self._on_disconnect(self, self._user_data, 0) def ping(self): """Pings the MQTT Broker to confirm if the server is alive or @@ -316,24 +316,32 @@ def publish(self, topic, msg, retain=False, qos=0): pkt[i] = sz self._sock.write(pkt) self._send_str(topic) + print('self: ', self._on_publish, qos) if qos > 0: self.pid += 1 pid = self.pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) + print('self: ', self._on_publish) + if self._on_publish is not None: + self._on_publish(self, self._user_data, pid) self._sock.write(msg) if qos == 1: while 1: op = self.wait_for_msg() - print(op) if op == const(0x40): sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + print('PUB: ', self._on_publish) + if self._on_publish is not None: + self._on_publish(self, self._user_data, rcv_pid) if pid == rcv_pid: return elif qos == 2: + if self._on_publish is not None: + raise NotImplementedError('on_publish callback not implemented for QoS > 1.') assert 0 def subscribe(self, topic, method_handler=None, qos=0): @@ -545,7 +553,7 @@ def user_data(self, data): def on_connect(self): """Called when the MQTT broker responds to a connection request. """ - return self._on_callback + return self._on_connect @on_connect.setter def on_connect(self, method): @@ -588,9 +596,9 @@ def on_publish(self): """ return self._on_publish - @on_disconnect.setter - def on_disconnect(self, method): - """Defines the method which runs when the client is disconnected. + @on_publish.setter + def on_publish(self, method): + """Defines the method which runs when the client publishes data to a feed. :param unbound_method method: user-defined method for disconnection. The on_publish method signature takes the following format: @@ -623,4 +631,25 @@ def on_subscribe(self, method): """ self._on_subscribe = method - # TODO:, on_publish, on_log \ No newline at end of file + @property + def on_publish(self): + """Called when the MQTT broker successfully publishes to a feed. + """ + return self._on_publish + + @on_publish.setter + def on_publish(self, method): + """Defines the method which runs when a client publishes to a feed. + + :param unbound_method method: user-defined method for disconnection. + + The on_publish method signature takes the following format: + on_publish(client, userdata, rc) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + :param int granted_qos: QoS level the broker has granted the subscription request.. + """ + self._on_publish = method + + # TODO: Implement on_log \ No newline at end of file From 5dff2fffdb74db2c4728679104b41be9d01ac8ea Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 17:51:29 -0400 Subject: [PATCH 048/148] add working publish for callbacks when qos = 0 --- adafruit_minimqtt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c75a0db..deeb018 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -316,13 +316,14 @@ def publish(self, topic, msg, retain=False, qos=0): pkt[i] = sz self._sock.write(pkt) self._send_str(topic) - print('self: ', self._on_publish, qos) + if qos == 0: + if self._on_publish is not None: + self._on_publish(self, self._user_data, self._pid) if qos > 0: self.pid += 1 pid = self.pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) - print('self: ', self._on_publish) if self._on_publish is not None: self._on_publish(self, self._user_data, pid) self._sock.write(msg) @@ -334,7 +335,6 @@ def publish(self, topic, msg, retain=False, qos=0): assert sz == b"\x02" rcv_pid = self._sock.read(2) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] - print('PUB: ', self._on_publish) if self._on_publish is not None: self._on_publish(self, self._user_data, rcv_pid) if pid == rcv_pid: From 3d4446109ae1ed9a77b16ec62b54cd9fa9e92cf0 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 18:11:08 -0400 Subject: [PATCH 049/148] add network loop methods, untested as of now --- adafruit_minimqtt.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index deeb018..240545b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -125,6 +125,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._on_subscribe = None self._on_log = None self._user_data = None + self._is_loop = False self.last_will() def __enter__(self): @@ -178,9 +179,11 @@ def reconnect(self, retries=30, resub_topics=False): self.subscribe(feed) def is_connected(self): - """Returns MQTT session status.""" + """Returns MQTT client session status.""" return self._is_connected + ### Core MQTT Methods ### + def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session @@ -418,6 +421,7 @@ def publish_multiple(self, data, timeout=1.0): :param float timeout: Timeout between calls to publish(). This value is usually set by your MQTT broker. Defaults to 1.0 """ + # TODO: Untested! for i in range(len(data)): topic = data[i][0] msg = data[i][1] @@ -445,6 +449,8 @@ def subscribe_multiple(self, topic_info, timeout=1.0): :param int qos: Quality of Service level for the topic. Defaults to 0. :param float timeout: Timeout between calls to subscribe(). """ + #TODO: This could be simplified + # 1 mqtt subscription call, multiple topics print('topics:', topic_info) for i in range(len(topic_info)): topic = topic_info[i][0] @@ -464,12 +470,12 @@ def subscribe_multiple(self, topic_info, timeout=1.0): self.subscribe(topic, method_handler, qos) time.sleep(timeout) - def wait_for_msg(self, blocking=True): + def wait_for_msg(self, timeout=0.0): """Waits for and processes network events. Returns if successful. :param bool blocking: Set the blocking or non-blocking mode of the socket. :param float timeout: The time in seconds to wait for network traffic before returning. """ - self._sock.settimeout(0.0) + self._sock.settimeout(timeout) res = self._sock.read(1) if res in [None, b""]: return None @@ -531,6 +537,25 @@ def _send_str(self, string): else: self._sock.write(string) + # Network Loop Methods + + def loop(self, timeout=1.0): + """Call regularly to process network events. + This function blocks for up to timeout seconds. + Timeout must not exceed the keepalive value for the client or + your client will be regularly disconnected by the broker. + :param float timeout: Blocks between calls to wait_for_msg() + """ + # TODO: Untested! + self.wait_for_msg(timeout) + + def loop_forever(self): + """Blocking network loop, will not return until disconnect() is called from + the client. Automatically handles the re-connection. + """ + # TODO! + return None + ## Logging ## # TODO: Set up Logging with the CircuitPython logger module. From 13010a4d3eaec1ba85c1a6c89dd04ed4a6feb838 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 2 Jul 2019 18:11:42 -0400 Subject: [PATCH 050/148] add larger example from pyportal --- examples/minimqtt_simpletest.py | 55 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index 6a36344..b0d3972 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -5,7 +5,6 @@ import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_esp32spi import adafruit_esp32spi_wifimanager from adafruit_minimqtt import MQTT @@ -42,12 +41,9 @@ # BLUE_LED = PWMOut.PWMOut(esp, 25) # status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) -# Create a WiFiManager -wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) - # Instanciate a MQTT Client -mqtt_client = MQTT(esp, socket, wifi, secrets['aio_url'], - user=secrets['aio_user'], password=secrets['aio_password'], +mqtt_client = MQTT(esp, socket, secrets['aio_url'], + username=secrets['aio_user'], password=secrets['aio_password'], is_ssl = True) print("Connecting to AP...") @@ -62,16 +58,47 @@ print('Connecting to {0}:{1}...'.format(mqtt_client.server, mqtt_client.port)) + +# ACK Callbacks for testing +def connect(client, userdata, flags, rc): + """.connect() called""" + print('Connected to MQTT Broker!') + print('Flags: {0}\n RC: {1}'.format(flags, rc)) + +def disconnect(client, userdata, rc): + """.disconnect() called""" + print('Disconnected from MQTT Broker!') + print('RC: {0}\n'.format(rc)) + +def subscribe(client, userdata, qos): + """.subscribe() called""" + print('Client successfully subscribed to feed with a QOS of %d'%qos) + +def publish(client, userdata, pid): + """.publish() called""" + print('Client successfully published to feed with a PID of %d'%pid) + +# Set the user_data to a generated client_id +mqtt_client.user_data = mqtt_client._client_id + +# define callbacks +mqtt_client.on_connect = connect +mqtt_client.on_subscribe = subscribe +mqtt_client.on_publish = publish +mqtt_client.on_disconnect = disconnect + # Connect MQTT Client +print('connecting...') mqtt_client.connect() -# Subscribe to MQTT feed -print('Subscribing to feed') +# Subscribe to feed +print('subscribing...') mqtt_client.subscribe('brubell/feeds/temperature') -while True: - print('Publishing msg to feed') - mqtt_client.publish('brubell/feeds/temperature', "cat") - print('Listening for msg...') - mqtt_client.wait_msg() - time.sleep(5) \ No newline at end of file +print('publishing...') +mqtt_client.publish('brubell/feeds/temperature', 50) + +# Disconnect from MQTT Client +print('disconnecting...') +mqtt_client.disconnect() + From 9acb8e1a26b37eb30bff9304090d1e5b185aa480 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 11:35:51 -0400 Subject: [PATCH 051/148] fix CONNACK on_connect response codes --- adafruit_minimqtt.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 240545b..8f4391b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -241,18 +241,13 @@ def connect(self, clean_session=True): self._send_str(self._user) self._send_str(self._pass) rc = self._sock.read(4) - print('Packet: ', rc) assert rc[0] == const(0x20) and rc[1] == const(0x02) if rc[3] !=0: raise MMQTTException(CONNACK_ERRORS[rc[3]]) - # connack rx'd - # TODO: figure out flag extracting - flags_dict = {} - #flags_dict['session present'] = - result = rc[2] & 1 self._is_connected = True + result = rc[2] & 1 if self._on_connect is not None: - self._on_connect(self._user_data, rc, flags_dict, result) + self._on_connect(self, self._user_data, result, rc[3]) return result def disconnect(self): From 67b4d75e8fbbe923af25171b1124fd80f541e2a2 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 12:34:09 -0400 Subject: [PATCH 052/148] add unsub...broken response --- adafruit_minimqtt.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8f4391b..3f190e3 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -274,6 +274,8 @@ def ping(self): raise MMQTTException('PINGRESP was not received') return res + + def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. :param str topic: Unique topic identifier. @@ -393,6 +395,19 @@ def subscribe(self, topic, method_handler=None, qos=0): self.on_subscribe(self, self._user_data, rc[3]) return + def unsubscribe(self, topic, qos=0): + """Unsubscribes from a MQTT topic. + """ + pkt = bytearray(b'\xA0\0\0\0') + self._pid += 11 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) + print(pkt) + self._sock.write(pkt) + while 1: + print('waiting for response...') + op = self.wait_for_msg() + print('op,', op) + @property def mqtt_msg(self): """Returns maximum MQTT payload and topic size.""" From 131621402b575b9620cae8316a8e65192e9a18de Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 12:44:42 -0400 Subject: [PATCH 053/148] increase timeout of wait_for_msg --- adafruit_minimqtt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 3f190e3..6817eca 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -480,9 +480,8 @@ def subscribe_multiple(self, topic_info, timeout=1.0): self.subscribe(topic, method_handler, qos) time.sleep(timeout) - def wait_for_msg(self, timeout=0.0): + def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. - :param bool blocking: Set the blocking or non-blocking mode of the socket. :param float timeout: The time in seconds to wait for network traffic before returning. """ self._sock.settimeout(timeout) From 51d0b7bae8023ff06da3813a0385b263d1e99c49 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 13:42:53 -0400 Subject: [PATCH 054/148] add working unsubscribe method, need stricter checking on the UNPUBACK --- adafruit_minimqtt.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 6817eca..bf5a20c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -347,7 +347,7 @@ def publish(self, topic, msg, retain=False, qos=0): def subscribe(self, topic, method_handler=None, qos=0): """Subscribes to a topic on the MQTT Broker. This method can subscribe to one topics or multiple topics. - :param str topic: Unique topic identifier. + :param str topic: Unique MQTT topic identifier. :param method method_handler: Predefined method for handling messages recieved from a topic. Defaults to default_sub_handler if None. :param int qos: Quality of Service level for the topic. @@ -395,18 +395,24 @@ def subscribe(self, topic, method_handler=None, qos=0): self.on_subscribe(self, self._user_data, rc[3]) return - def unsubscribe(self, topic, qos=0): + def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. + :param str topic: Unique MQTT topic identifier. """ - pkt = bytearray(b'\xA0\0\0\0') - self._pid += 11 - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) - print(pkt) + pkt = bytearray(b'\xA2\0\0\0') + self._pid+=1 + # variable header length + remaining_length = 2 + remaining_length += 2 + len(topic) + struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) self._sock.write(pkt) + self._send_str(topic) while 1: print('waiting for response...') op = self.wait_for_msg() - print('op,', op) + if op is not None: + print('OK!') + return @property def mqtt_msg(self): @@ -421,7 +427,7 @@ def mqtt_msg(self, msg_size): if msg_size < MQTT_MSG_MAX_SZ: self.__msg_size_lim = msg_size - def publish_multiple(self, data, timeout=1.0): + def publish_multiple(self, data, timeout=0.0): """Publishes to multiple MQTT broker topics. :param tuple data: A list of tuple format: :param str topic: Unique topic identifier. From f27e7e5c0e379722eda31986f5d4ccff55d85bc1 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 14:52:31 -0400 Subject: [PATCH 055/148] handle unsuback better with a variable socket timeout --- adafruit_minimqtt.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bf5a20c..b29bb94 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -59,6 +59,7 @@ MQTT_PING_REQ = b'\xc0' MQTT_PINGRESP = b'\xd0' MQTT_SUB = bytearray(b'\x82\0\0\0') +MQTT_UNSUB = (b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable header [MQTT 3.1.2] @@ -275,7 +276,6 @@ def ping(self): return res - def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. :param str topic: Unique topic identifier. @@ -396,23 +396,22 @@ def subscribe(self, topic, method_handler=None, qos=0): return def unsubscribe(self, topic): - """Unsubscribes from a MQTT topic. + """Unsubscribes from a MQTT topic. Topic must be previously subscribed to. :param str topic: Unique MQTT topic identifier. """ - pkt = bytearray(b'\xA2\0\0\0') - self._pid+=1 - # variable header length + pkt = MQTT_UNSUB + self._pid += 1 remaining_length = 2 - remaining_length += 2 + len(topic) - struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) + struct.pack_into("!BH", pkt, 1, remaining_length += 2 + len(topic), self._pid) self._sock.write(pkt) self._send_str(topic) while 1: - print('waiting for response...') - op = self.wait_for_msg() - if op is not None: - print('OK!') + try: + # Attempt to rx UNSUBACK + op = self.wait_for_msg(0.1) return + except RuntimeError: + raise MMQTTException('Could not unsubscribe from feed.') @property def mqtt_msg(self): From fba332cc015a8fc255c39856b263c92b752e6192 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 15:28:11 -0400 Subject: [PATCH 056/148] add on_unsubscribe ack handler --- adafruit_minimqtt.py | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b29bb94..43a1906 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -59,7 +59,7 @@ MQTT_PING_REQ = b'\xc0' MQTT_PINGRESP = b'\xd0' MQTT_SUB = bytearray(b'\x82\0\0\0') -MQTT_UNSUB = (b'\xA2\0\0\0') +MQTT_UNSUB = bytearray(b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable header [MQTT 3.1.2] @@ -124,6 +124,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._on_disconnect = None self._on_publish = None self._on_subscribe = None + self._on_unsubscribe = None self._on_log = None self._user_data = None self._is_loop = False @@ -395,20 +396,31 @@ def subscribe(self, topic, method_handler=None, qos=0): self.on_subscribe(self, self._user_data, rc[3]) return + def unsubscribe(self, topic): - """Unsubscribes from a MQTT topic. Topic must be previously subscribed to. + """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. """ + if topic is None or len(topic) == 0: + raise MMQTTException("Invalid MQTT topic - must have a length > 0.") + if topic not in self._method_handlers: + raise MMQTTException('Must be subscribed to %s before unsubscribing'%topic) pkt = MQTT_UNSUB - self._pid += 1 + self._pid+=1 + # variable header length remaining_length = 2 - struct.pack_into("!BH", pkt, 1, remaining_length += 2 + len(topic), self._pid) + remaining_length += 2 + len(topic) + struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) self._sock.write(pkt) self._send_str(topic) while 1: try: - # Attempt to rx UNSUBACK + # attempt to UNSUBACK op = self.wait_for_msg(0.1) + # remove topic and method from method_handlers + self._method_handlers.pop(topic) + if self._on_subscribe is not None: + self.on_subscribe(self, self._user_data) return except RuntimeError: raise MMQTTException('Could not unsubscribe from feed.') @@ -659,7 +671,7 @@ def on_subscribe(self): def on_subscribe(self, method): """Defines the method which runs when a client subscribes to a feed. - :param unbound_method method: user-defined method for disconnection. + :param unbound_method method: user-defined method for subscribing to a feed. The on_subscribe method signature takes the following format: on_subscribe(client, userdata, rc) @@ -670,6 +682,25 @@ def on_subscribe(self, method): """ self._on_subscribe = method + @property + def on_unsubscribe(self): + """Called when the MQTT broker responds to a call to unsubscribe(). + """ + return self._on_unsubscribe + + @on_unsubscribe.setter + def on_unsubscribe(self, method): + """Defines the method which runs when a client unsubscribes from a feed. + :param unbound_method method: user-defined method for unsubscribing from a feed. + + The on_unsubscribe signature takes the following format: + on_unsubscribe(client, userdata) + and expects the following parameters: + :param client: MiniMQTT Client Instance. + :param userdata: User data, previously set in the user_data method. + """ + self._on_unsubscribe = method + @property def on_publish(self): """Called when the MQTT broker successfully publishes to a feed. From 9b0635b6664bae19b999f35481dd71fb7d9a0c64 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 15:30:47 -0400 Subject: [PATCH 057/148] correct method handler call --- adafruit_minimqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 43a1906..971388f 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -419,8 +419,8 @@ def unsubscribe(self, topic): op = self.wait_for_msg(0.1) # remove topic and method from method_handlers self._method_handlers.pop(topic) - if self._on_subscribe is not None: - self.on_subscribe(self, self._user_data) + if self._on_unsubscribe is not None: + self._on_unsubscribe(self, self._user_data) return except RuntimeError: raise MMQTTException('Could not unsubscribe from feed.') From c2c4f9cef6c2eef262992dff6c4e2163901615f6 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 16:11:09 -0400 Subject: [PATCH 058/148] remove @setter/@getter callbacks, make on_ methods public to usercode --- adafruit_minimqtt.py | 190 ++++++------------------------------------- 1 file changed, 25 insertions(+), 165 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 971388f..861864a 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -62,7 +62,7 @@ MQTT_UNSUB = bytearray(b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') -# Variable header [MQTT 3.1.2] +# Variable CONNECT header [MQTT 3.1.2] MQTT_CON_HEADER = bytearray(b"\x04MQTT\x04\x02\0\0") MQTT_DISCONNECT = b'\xe0\0' @@ -120,12 +120,12 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._keep_alive = 0 self._pid = 0 # paho-style method callbacks - self._on_connect = None - self._on_disconnect = None - self._on_publish = None - self._on_subscribe = None - self._on_unsubscribe = None - self._on_log = None + self.on_connect = None + self.on_disconnect = None + self.on_publish = None + self.on_subscribe = None + self.on_unsubscribe = None + self.on_log = None self._user_data = None self._is_loop = False self.last_will() @@ -184,7 +184,7 @@ def is_connected(self): """Returns MQTT client session status.""" return self._is_connected - ### Core MQTT Methods ### + # Core MQTT Methods def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. @@ -248,8 +248,8 @@ def connect(self, clean_session=True): raise MMQTTException(CONNACK_ERRORS[rc[3]]) self._is_connected = True result = rc[2] & 1 - if self._on_connect is not None: - self._on_connect(self, self._user_data, result, rc[3]) + if self.on_connect is not None: + self.on_connect(self, self._user_data, result, rc[3]) return result def disconnect(self): @@ -260,8 +260,8 @@ def disconnect(self): self._sock.write(MQTT_DISCONNECT) self._sock.close() self._is_connected = False - if self._on_disconnect is not None: - self._on_disconnect(self, self._user_data, 0) + if self.on_disconnect is not None: + self.on_disconnect(self, self._user_data, 0) def ping(self): """Pings the MQTT Broker to confirm if the server is alive or @@ -318,15 +318,15 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) self._send_str(topic) if qos == 0: - if self._on_publish is not None: - self._on_publish(self, self._user_data, self._pid) + if self.on_publish is not None: + self.on_publish(self, self._user_data, self._pid) if qos > 0: self.pid += 1 pid = self.pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) - if self._on_publish is not None: - self._on_publish(self, self._user_data, pid) + if self.on_publish is not None: + self.on_publish(self, self._user_data, pid) self._sock.write(msg) if qos == 1: while 1: @@ -336,12 +336,12 @@ def publish(self, topic, msg, retain=False, qos=0): assert sz == b"\x02" rcv_pid = self._sock.read(2) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] - if self._on_publish is not None: - self._on_publish(self, self._user_data, rcv_pid) + if self.on_publish is not None: + self.on_publish(self, self._user_data, rcv_pid) if pid == rcv_pid: return elif qos == 2: - if self._on_publish is not None: + if self.on_publish is not None: raise NotImplementedError('on_publish callback not implemented for QoS > 1.') assert 0 @@ -392,7 +392,7 @@ def subscribe(self, topic, method_handler=None, qos=0): assert rc[1] == pkt[2] and rc[2] == pkt[3] if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') - if self._on_subscribe is not None: + if self.on_subscribe is not None: self.on_subscribe(self, self._user_data, rc[3]) return @@ -419,12 +419,13 @@ def unsubscribe(self, topic): op = self.wait_for_msg(0.1) # remove topic and method from method_handlers self._method_handlers.pop(topic) - if self._on_unsubscribe is not None: - self._on_unsubscribe(self, self._user_data) + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, self._user_data) return except RuntimeError: raise MMQTTException('Could not unsubscribe from feed.') + @property def mqtt_msg(self): """Returns maximum MQTT payload and topic size.""" @@ -572,154 +573,13 @@ def loop(self, timeout=1.0): your client will be regularly disconnected by the broker. :param float timeout: Blocks between calls to wait_for_msg() """ - # TODO: Untested! - self.wait_for_msg(timeout) + return None def loop_forever(self): """Blocking network loop, will not return until disconnect() is called from the client. Automatically handles the re-connection. """ - # TODO! return None ## Logging ## - # TODO: Set up Logging with the CircuitPython logger module. - - ## Acknowledgement Callbacks ## - - @property - def user_data(self): - """Returns the user_data variable passed to callbacks. - """ - return self._user_data - - @user_data.setter - def user_data(self, data): - """Sets the private user_data variable passed to callbacks. - :param data: Any data type. - """ - self._user_data = data - - @property - def on_connect(self): - """Called when the MQTT broker responds to a connection request. - """ - return self._on_connect - - @on_connect.setter - def on_connect(self, method): - """Defines the method which runs when the client is connected. - :param unbound_method method: user-defined method for connection. - - The on_connect method signature takes the following format: - on_connect_method(client, userdata, flags, rc) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - :param flags: CONNACK flags. - :param int rc: Response code. - """ - self._on_connect = method - - @property - def on_disconnect(self): - """Called when the MQTT broker responds to a disconnection request. - """ - return self._on_disconnect - - @on_disconnect.setter - def on_disconnect(self, method): - """Defines the method which runs when the client is disconnected. - :param unbound_method method: user-defined method for disconnection. - - The on_disconnect method signature takes the following format: - on_disconnect_method(client, userdata, rc) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - :param int rc: Response code. - """ - self._on_disconnect = method - - @property - def on_publish(self): - """Called when the MQTT broker responds to a publish request. - """ - return self._on_publish - - @on_publish.setter - def on_publish(self, method): - """Defines the method which runs when the client publishes data to a feed. - :param unbound_method method: user-defined method for disconnection. - - The on_publish method signature takes the following format: - on_publish(client, userdata, rc) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - :param int rc: Response code. - """ - self._on_publish = method - - @property - def on_subscribe(self): - """Called when the MQTT broker successfully subscribes to a feed. - """ - return self._on_subscribe - - @on_subscribe.setter - def on_subscribe(self, method): - """Defines the method which runs when a client subscribes to a feed. - - :param unbound_method method: user-defined method for subscribing to a feed. - - The on_subscribe method signature takes the following format: - on_subscribe(client, userdata, rc) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - :param int granted_qos: QoS level the broker has granted the subscription request.. - """ - self._on_subscribe = method - - @property - def on_unsubscribe(self): - """Called when the MQTT broker responds to a call to unsubscribe(). - """ - return self._on_unsubscribe - - @on_unsubscribe.setter - def on_unsubscribe(self, method): - """Defines the method which runs when a client unsubscribes from a feed. - :param unbound_method method: user-defined method for unsubscribing from a feed. - - The on_unsubscribe signature takes the following format: - on_unsubscribe(client, userdata) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - """ - self._on_unsubscribe = method - - @property - def on_publish(self): - """Called when the MQTT broker successfully publishes to a feed. - """ - return self._on_publish - - @on_publish.setter - def on_publish(self, method): - """Defines the method which runs when a client publishes to a feed. - - :param unbound_method method: user-defined method for disconnection. - - The on_publish method signature takes the following format: - on_publish(client, userdata, rc) - and expects the following parameters: - :param client: MiniMQTT Client Instance. - :param userdata: User data, previously set in the user_data method. - :param int granted_qos: QoS level the broker has granted the subscription request.. - """ - self._on_publish = method - - # TODO: Implement on_log \ No newline at end of file + # TODO: Set up Logging with the CircuitPython logger module. \ No newline at end of file From ef74dfbc6867776484b8701078e4f140e7884ec1 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 17:02:23 -0400 Subject: [PATCH 059/148] add prelimary support for logging --- adafruit_minimqtt.py | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 861864a..86e8123 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -39,11 +39,12 @@ https://github.com/adafruit/circuitpython/releases """ -import time import struct -from micropython import const +import time from random import randint import microcontroller +import adafruit_logging as logging +from micropython import const __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" @@ -86,11 +87,12 @@ class MQTT: :param str password: Password for broker authentication. :param str client_id: Optional client identifier, defaults to a randomly generated id. :param bool is_ssl: Enables TCP mode if false (port 1883). Defaults to True (port 8883). + :param bool log: Creates a logger module for debugging. Defaults to ERROR level. """ TCP_MODE = const(0) TLS_MODE = const(2) def __init__(self, esp, socket, server_address, port=8883, username=None, - password = None, client_id=None, is_ssl=True): + password = None, client_id=None, is_ssl=True, log=False): if esp and socket is not None: self._esp = esp self._socket = socket @@ -111,6 +113,9 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, # generated client_id's enforce length rules if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') + if log: + self._logger = logging.getLogger('log') + self._logger.setLevel(logging.ERROR) # subscription method handler dictionary self._method_handlers = {} self._is_connected = False @@ -119,6 +124,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self.packet_id = 0 self._keep_alive = 0 self._pid = 0 + self._user_data = None # paho-style method callbacks self.on_connect = None self.on_disconnect = None @@ -126,8 +132,6 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self.on_subscribe = None self.on_unsubscribe = None self.on_log = None - self._user_data = None - self._is_loop = False self.last_will() def __enter__(self): @@ -269,7 +273,7 @@ def ping(self): Raises an error if server is not alive. Returns PINGRESP if server is alive. """ - # note: sock.write handles the PINGRESP + self._logger.debug('Sending PINGREQ') self._sock.write(MQTT_PING_REQ) res = self._sock.read(1) if res != MQTT_PINGRESP: @@ -315,6 +319,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {1}\n Retain: {2}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: @@ -581,5 +586,26 @@ def loop_forever(self): """ return None - ## Logging ## - # TODO: Set up Logging with the CircuitPython logger module. \ No newline at end of file + # Logging + @property + def logging(self): + """Returns the logger object. + """ + return self._logger + + @logging.setter + def logging(self, log_level): + """Sets the level of the logger, if defined during init. + :param string log_level: Level of logging to output to the REPL. Accepted + levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. + """ + if log_level == 'DEBUG': + self._logger.setLevel(logger.DEBUG) + elif log_level == 'INFO': + self._logger.setLevel(logger.INFO) + elif log_level == 'WARNING': + self._logger.setLevel(logger.WARNING) + elif log_level == 'ERROR': + self._logger.setLevel(logger.CRITICIAL) + else: + raise MMQTTException('Incorrect logging level provided!') \ No newline at end of file From 7aa160b4e30113fd63c8d3514ff6eb8bad9cc4d9 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 3 Jul 2019 17:04:42 -0400 Subject: [PATCH 060/148] fix logging, success from logger publish --- adafruit_minimqtt.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 86e8123..fa69a6e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -587,25 +587,19 @@ def loop_forever(self): return None # Logging - @property - def logging(self): - """Returns the logger object. - """ - return self._logger - - @logging.setter + def logging(self, log_level): """Sets the level of the logger, if defined during init. :param string log_level: Level of logging to output to the REPL. Accepted levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. """ if log_level == 'DEBUG': - self._logger.setLevel(logger.DEBUG) + self._logger.setLevel(logging.DEBUG) elif log_level == 'INFO': - self._logger.setLevel(logger.INFO) + self._logger.setLevel(logging.INFO) elif log_level == 'WARNING': - self._logger.setLevel(logger.WARNING) + self._logger.setLevel(logging.WARNING) elif log_level == 'ERROR': - self._logger.setLevel(logger.CRITICIAL) + self._logger.setLevel(logging.CRITICIAL) else: raise MMQTTException('Incorrect logging level provided!') \ No newline at end of file From 345f8f1cd5eee49d25126a3e4fa3012f70684737 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 10:38:42 -0400 Subject: [PATCH 061/148] fix ping's handling of PINGRESP --- adafruit_minimqtt.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index fa69a6e..73a09cc 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -268,18 +268,20 @@ def disconnect(self): self.on_disconnect(self, self._user_data, 0) def ping(self): - """Pings the MQTT Broker to confirm if the server is alive or - if the network connection is active. - Raises an error if server is not alive. - Returns PINGRESP if server is alive. + """Pings the MQTT Broker to confirm if the server is alive or if + there is an active network connection. + Raises a MMQTTException if the server does not respond with a PINGRESP packet. """ self._logger.debug('Sending PINGREQ') self._sock.write(MQTT_PING_REQ) res = self._sock.read(1) - if res != MQTT_PINGRESP: - raise MMQTTException('PINGRESP was not received') - return res - + self._logger.debug('Checking PINGRESP') + if res == MQTT_PINGRESP: + sz = self._sock.read(1)[0] + assert sz == 0 + return None + else: + raise MMQTTException('Server did not return with PINGRESP') def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to the MQTT broker. @@ -380,7 +382,7 @@ def subscribe(self, topic, method_handler=None, qos=0): if method_handler is None: self._method_handlers.update( {topic : self.default_sub_handler} ) else: - self._method_handlers.update( {topic : custom_method_handler} ) + self._method_handlers.update( {topic : method_handler} ) if self._sock is None: raise MMQTTException("MiniMQTT not connected.") pkt = MQTT_SUB @@ -430,7 +432,6 @@ def unsubscribe(self, topic): except RuntimeError: raise MMQTTException('Could not unsubscribe from feed.') - @property def mqtt_msg(self): """Returns maximum MQTT payload and topic size.""" @@ -587,7 +588,6 @@ def loop_forever(self): return None # Logging - def logging(self, log_level): """Sets the level of the logger, if defined during init. :param string log_level: Level of logging to output to the REPL. Accepted From 00756c9ded85800b11fa4d5ce6975924eb4cc680 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 11:03:39 -0400 Subject: [PATCH 062/148] add logger debug statements, reduce reused variables in wait_for_msg --- adafruit_minimqtt.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 73a09cc..8b69a60 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -115,7 +115,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, raise ValueError('MQTT Client ID must be between 1 and 23 bytes') if log: self._logger = logging.getLogger('log') - self._logger.setLevel(logging.ERROR) + self._logger.setLevel(logging.DEBUG) # subscription method handler dictionary self._method_handlers = {} self._is_connected = False @@ -156,6 +156,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): raise MMQTTException('Last Will should be defined before connect() is called.') if qos < 0 or qos > 2: raise MMQTTException("Invalid QoS level, must be between 0 and 2.") + self._logger.debug('Setting last will properties') self._lw_qos = qos self._lw_topic = topic self._lw_msg = message @@ -169,6 +170,7 @@ def reconnect(self, retries=30, resub_topics=False): """ retries = 0 while not self._is_connected: + self._logger.debug('Attempting to reconnect to broker') try: self.connect(False) except OSError as e: @@ -179,7 +181,9 @@ def reconnect(self, retries=30, resub_topics=False): self._esp.reset() continue self._is_connected = True + self._logger.debug('Reconnected to broker') if resub_topics: + self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') while len(self._method_handlers) > 0: feed = self._method_handlers.popitem() self.subscribe(feed) @@ -189,13 +193,13 @@ def is_connected(self): return self._is_connected # Core MQTT Methods - def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session with the broker. Defaults to a non-persistent session. """ if self._esp: + self._logger.debug('Creating new socket') self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: @@ -203,12 +207,14 @@ def connect(self, clean_session=True): self._sock.settimeout(10) if self.port == 8883: try: + self._logger.debug('Attempting to establish secure MQTT connection with %s'%self.server) self._sock.connect((self.server, self.port), TLS_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] try: + self._logger.debug('Attempting to establish an insecure MQTT connection with %s'%self.server) self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") @@ -233,6 +239,7 @@ def connect(self, clean_session=True): sz >>= 7 i += 1 premsg[i] = sz + self._logger.debug('Sending CONNECT packet to server') self._sock.write(premsg) self._sock.write(msg) # [MQTT-3.1.3-4] @@ -246,6 +253,7 @@ def connect(self, clean_session=True): else: self._send_str(self._user) self._send_str(self._pass) + self._logger.debug('Receiving CONNACK packet from server') rc = self._sock.read(4) assert rc[0] == const(0x20) and rc[1] == const(0x02) if rc[3] !=0: @@ -261,7 +269,9 @@ def disconnect(self): """ if self._sock is None: raise MMQTTException("MiniMQTT is not connected.") + self._logger.debug('Sending DISCONNECT packet to server') self._sock.write(MQTT_DISCONNECT) + self._logger.debug('Closing socket') self._sock.close() self._is_connected = False if self.on_disconnect is not None: @@ -334,6 +344,7 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: @@ -388,6 +399,7 @@ def subscribe(self, topic, method_handler=None, qos=0): pkt = MQTT_SUB self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) + self._logger.debug('Subscribing to {0} with a QOS of {1}'.format(topic, qos)) self._sock.write(pkt) # [MQTT-3.8.3-1] self._send_str(topic) @@ -418,11 +430,13 @@ def unsubscribe(self, topic): remaining_length = 2 remaining_length += 2 + len(topic) struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) + self._logger.debug('Unsubscribing from %s'%topic) self._sock.write(pkt) self._send_str(topic) while 1: try: # attempt to UNSUBACK + self._logger.debug('Sending UNSUBACK') op = self.wait_for_msg(0.1) # remove topic and method from method_handlers self._method_handlers.pop(topic) @@ -506,7 +520,8 @@ def subscribe_multiple(self, topic_info, timeout=1.0): def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. - :param float timeout: The time in seconds to wait for network traffic before returning. + :param float timeout: The time in seconds to wait for network before returning. + Setting this to 0.0 will cause the socket to block until it reads. """ self._sock.settimeout(timeout) res = self._sock.read(1) @@ -516,31 +531,30 @@ def wait_for_msg(self, timeout=0.1): sz = self._sock.read(1)[0] assert sz == 0 return None - op = res[0] - if op & 0xf0 != 0x30: - return op + if res[0] & const(0xf0) != const(0x30): + return res[0] sz = self._recv_len() topic_len = self._sock.read(2) topic_len = (topic_len[0] << 8) | topic_len[1] topic = self._sock.read(topic_len) topic = str(topic, 'utf-8') sz -= topic_len + 2 - if op & 6: + if res[0] & const(0x06): pid = self._sock.read(2) - pid = pid[0] << 8 | pid[1] - sz -= 2 + pid = pid[0] << const(0x08) | pid[1] + sz -= const(0x02) msg = self._sock.read(sz) # call the topic's handler method if topic in self._method_handlers: method_handler = self._method_handlers[topic] method_handler(topic, str(msg, 'utf-8')) - if op & 6 == 2: + if res[0] & const(0x06) == const(0x02): pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) self._sock.write(pkt) - elif op & 6 == 4: + elif res[0] & 6 == 4: assert 0 - return op + return res[0] def _recv_len(self): """Receives the size of the topic length.""" @@ -558,6 +572,7 @@ def default_sub_handler(self, topic, msg): :param str topic: Subscription topic. :param str msg: Message content. """ + self._logger.debug('default_sub_handler called') print('New message on {0}: {1}'.format(topic, msg)) def _send_str(self, string): From 28ebc6bd876cb85bd4a63e099b4494cb92c0e2a0 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 11:31:13 -0400 Subject: [PATCH 063/148] starting mqtt test client --- examples/test_mqtt_client.py | 126 +++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100755 examples/test_mqtt_client.py diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py new file mode 100755 index 0000000..c56a7f3 --- /dev/null +++ b/examples/test_mqtt_client.py @@ -0,0 +1,126 @@ +""" +CircuitPython_MiniMQTT Module Tester + +by Brent Rubell for Adafruit Industries, 2019 +""" + +import time +import board +import busio +from digitalio import DigitalInOut +import neopixel +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +# MiniMQTT +from adafruit_minimqtt import MQTT + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards + + +""" +Generic Unittest Assertions +""" +#pylint: disable=keyword-arg-before-vararg +def assertAlmostEqual(x, y, places=None, msg=''): + """Raises an AssertionError if two float values are not equal. + (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py) + """ + if x == y: + return + if places is None: + places = 2 + if round(abs(y-x), places) == 0: + return + if not msg: + msg = '%r != %r within %r places' % (x, y, places) + assert False, msg + +def assertRaises(exc,func=None, *args, **kwargs): + """Raises based on context. + (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py) + """ + try: + func(*args, **kwargs) + assert False, "%r not raised" % exc + except Exception as e: + if isinstance(e, exc): + return + raise + +def assertIsNone(x): + """Raises an AssertionError if x is None. + """ + if x is None: + raise AssertionError('%r is None'%x) + +def assertEqual(val_1, val_2): + """Raises an AssertionError if the two specified values are not equal. + """ + if val_1 != val_2: + raise AssertionError('Values are not equal:', val_1, val_2) + +# Default MiniMQTT Callback Message Handlers +def connect(client, userdata, flags, rc): + """.connect() called""" + print('Connected to MQTT Broker!') + print('Flags: {0}\n RC: {1}'.format(flags, rc)) + +def disconnect(client, userdata, rc): + """.disconnect() called""" + print('Disconnected from MQTT Broker!') + print('RC: {0}\n'.format(rc)) + +def subscribe(client, userdata, qos): + """.subscribe() called""" + print('Client successfully subscribed to feed with a QOS of %d'%qos) + +def publish(client, userdata, pid): + """.publish() called""" + print('Client successfully published to feed with a PID of %d'%pid) + +def unsubscribe(P1, P2): + # unsubscribe() called from the mqtt_client. + print('Client successfully unsubscribed from feed!') + +# MQTT Client Tests +def create_insecure_mqtt_client_esp32spi(esp, socket): + """Creates a new INSECURE (port 1883) MiniMQTT client for boards with an ESP32 module. + :param esp: ESP32SPI object. + :param socket: ESP32SPI_Socket object. + """ + mqtt_client = MQTT(esp, socket, secrets['aio_url'], + username=secrets['aio_user'], password=secrets['aio_password'], + is_ssl=False) + +def create_secure_mqtt_client_esp32spi(esp, socket): + """Creates a new SECURE (port 8883) MiniMQTT client for boards with an ESP32 module. + :param esp: ESP32SPI object. + :param socket: ESP32SPI_Socket object. + """ + mqtt_client = MQTT(esp, socket, secrets['aio_url'], + username=secrets['aio_user'], password=secrets['aio_password'], + is_ssl=True) + + +# Test Scripting +print("Connecting to AP...") +while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue +print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) From eaaebb9c1ef41dccfa3ff728886ceb0b13c8f4a3 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 12:06:49 -0400 Subject: [PATCH 064/148] add test harness and additional tests --- examples/test_mqtt_client.py | 53 ++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index c56a7f3..2e3aeae 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -3,7 +3,6 @@ by Brent Rubell for Adafruit Industries, 2019 """ - import time import board import busio @@ -96,26 +95,46 @@ def unsubscribe(P1, P2): print('Client successfully unsubscribed from feed!') # MQTT Client Tests -def create_insecure_mqtt_client_esp32spi(esp, socket): - """Creates a new INSECURE (port 1883) MiniMQTT client for boards with an ESP32 module. - :param esp: ESP32SPI object. - :param socket: ESP32SPI_Socket object. - """ +def test_create_insecure_mqtt_client_esp32spi(): + """Creates an insecure MQTT client using an ESP32SPI socket connection.""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=False) + assertEqual(mqtt_client.port, 1883) -def create_secure_mqtt_client_esp32spi(esp, socket): - """Creates a new SECURE (port 8883) MiniMQTT client for boards with an ESP32 module. - :param esp: ESP32SPI object. - :param socket: ESP32SPI_Socket object. - """ +def test_create_secure_mqtt_client_esp32spi(): + """Creates a secure MQTT client using an ESP32SPI socket connection.""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=True) + assertEqual(mqtt_client.port, 8883) + +def test_connect_mqtts_esp32spi(): + """Securely connects to a MQTT broker using an ESP32SPI_Socket connection.""" + mqtt_client = MQTT(esp, socket, secrets['aio_url'], + username=secrets['aio_user'], password=secrets['aio_password'], + is_ssl=True) + mqtt_client.connect() + assertEqual(mqtt_client._is_connected, True) + +def test_connect_mqtt_esp32spi(): + """Inesecurely connects to a MQTT broker using an ESP32SPI_Socket connection.""" + mqtt_client = MQTT(esp, socket, secrets['aio_url'], + username=secrets['aio_user'], password=secrets['aio_password'], + is_ssl=False) + mqtt_client.connect() + assertEqual(mqtt_client._is_connected, True) + + +# Timeout between tests, in seconds. This value depends on the timeout of your MQTT broker. +TEST_TIMEOUT = 1 +# Tests methods +tests = [test_create_insecure_mqtt_client_esp32spi, test_create_secure_mqtt_client_esp32spi, + test_connect_mqtts_esp32spi, test_connect_mqtt_esp32spi] -# Test Scripting + +# Establish ESP32SPI connection print("Connecting to AP...") while not esp.is_connected: try: @@ -124,3 +143,13 @@ def create_secure_mqtt_client_esp32spi(esp, socket): print("could not connect to AP, retrying: ",e) continue print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) + +# Test harness + +start_time = time.monotonic() +for i in enumerate(tests): + print('Running test: ', i) + i[1]() + print('OK!') + time.sleep(TEST_TIMEOUT) +print('Ran {0} tests in {1}s.'.format(len(tests), time.monotonic() - start_time)) From ffd87eb88c548e2661bac1cb30ce4dfa0b6890f1 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 12:43:15 -0400 Subject: [PATCH 065/148] consolidate some tests, fixup bugs --- examples/test_mqtt_client.py | 86 +++++++++++++++++------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index 2e3aeae..f9d0517 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -30,13 +30,12 @@ """ -Generic Unittest Assertions +Generic Unittest-like Assertions """ #pylint: disable=keyword-arg-before-vararg def assertAlmostEqual(x, y, places=None, msg=''): """Raises an AssertionError if two float values are not equal. - (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py) - """ + (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py).""" if x == y: return if places is None: @@ -48,9 +47,8 @@ def assertAlmostEqual(x, y, places=None, msg=''): assert False, msg def assertRaises(exc,func=None, *args, **kwargs): - """Raises based on context. - (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py) - """ + """Raises based on exception context. + (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py)""" try: func(*args, **kwargs) assert False, "%r not raised" % exc @@ -60,79 +58,78 @@ def assertRaises(exc,func=None, *args, **kwargs): raise def assertIsNone(x): - """Raises an AssertionError if x is None. - """ + """Raises an AssertionError if x is None.""" if x is None: raise AssertionError('%r is None'%x) def assertEqual(val_1, val_2): - """Raises an AssertionError if the two specified values are not equal. - """ + """Raises an AssertionError if the two specified values are not equal.""" if val_1 != val_2: raise AssertionError('Values are not equal:', val_1, val_2) -# Default MiniMQTT Callback Message Handlers -def connect(client, userdata, flags, rc): - """.connect() called""" - print('Connected to MQTT Broker!') - print('Flags: {0}\n RC: {1}'.format(flags, rc)) - -def disconnect(client, userdata, rc): - """.disconnect() called""" - print('Disconnected from MQTT Broker!') - print('RC: {0}\n'.format(rc)) - -def subscribe(client, userdata, qos): - """.subscribe() called""" - print('Client successfully subscribed to feed with a QOS of %d'%qos) - -def publish(client, userdata, pid): - """.publish() called""" - print('Client successfully published to feed with a PID of %d'%pid) - -def unsubscribe(P1, P2): - # unsubscribe() called from the mqtt_client. - print('Client successfully unsubscribed from feed!') - # MQTT Client Tests -def test_create_insecure_mqtt_client_esp32spi(): +def test_mqtt_create_client_esp32spi(): """Creates an insecure MQTT client using an ESP32SPI socket connection.""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=False) + mqtt_client.logging('DEBUG') assertEqual(mqtt_client.port, 1883) -def test_create_secure_mqtt_client_esp32spi(): +def test_mqtts_create_client_esp32spi(): """Creates a secure MQTT client using an ESP32SPI socket connection.""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=True) + mqtt_client.logging('DEBUG') assertEqual(mqtt_client.port, 8883) -def test_connect_mqtts_esp32spi(): - """Securely connects to a MQTT broker using an ESP32SPI_Socket connection.""" +def test_mqtts_connect_disconnect_esp32spi(): + """Creates a MQTTS client, connects, and attempts a disconnection.""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=True) + mqtt_client.logging('DEBUG') mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) + mqtt_client.disconnect() + assertEqual(mqtt_client._is_connected, False) -def test_connect_mqtt_esp32spi(): - """Inesecurely connects to a MQTT broker using an ESP32SPI_Socket connection.""" +def test_sub_pub(): + """Creates a MQTTS client, connects, subscribes, publishes, and checks data + received from broker matches data sent by client""" mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl=False) + is_ssl=True) + mqtt_client.logging('DEBUG') + # Callback responses + callback_msgs = [] + def on_message(client, topic, msg): + callback_msgs.append([topic, msg]) + print(callback_msgs) mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) + mqtt_client.subscribe('brubell/feeds/testfeed', on_message) + mqtt_client.publish('brubell/feeds/testfeed', 42) + start_timer = time.monotonic() + while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): + mqtt_client.wait_for_msg() + # check message with payload sent has been RXd properly + print(callback_msgs) # Timeout between tests, in seconds. This value depends on the timeout of your MQTT broker. TEST_TIMEOUT = 1 -# Tests methods -tests = [test_create_insecure_mqtt_client_esp32spi, test_create_secure_mqtt_client_esp32spi, - test_connect_mqtts_esp32spi, test_connect_mqtt_esp32spi] +# Connection/Client Tests +conn_tests = [test_mqtt_create_client_esp32spi, test_mqtts_create_client_esp32spi, + test_mqtts_connect_disconnect_esp32spi] +# PUB/SUB API Tests +pub_sub_tests = [test_sub_pub] + +# The test routine runs the following test(s): +tests = pub_sub_tests # Establish ESP32SPI connection print("Connecting to AP...") @@ -144,8 +141,7 @@ def test_connect_mqtt_esp32spi(): continue print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) -# Test harness - +## Test Routine ## start_time = time.monotonic() for i in enumerate(tests): print('Running test: ', i) From acae093efbc19405b63d10300e04198f1be5e235 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 12:44:12 -0400 Subject: [PATCH 066/148] create a logger by default, pass client information to the user-defined subscribe method handler --- adafruit_minimqtt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8b69a60..691d089 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -87,12 +87,11 @@ class MQTT: :param str password: Password for broker authentication. :param str client_id: Optional client identifier, defaults to a randomly generated id. :param bool is_ssl: Enables TCP mode if false (port 1883). Defaults to True (port 8883). - :param bool log: Creates a logger module for debugging. Defaults to ERROR level. """ TCP_MODE = const(0) TLS_MODE = const(2) def __init__(self, esp, socket, server_address, port=8883, username=None, - password = None, client_id=None, is_ssl=True, log=False): + password = None, client_id=None, is_ssl=True): if esp and socket is not None: self._esp = esp self._socket = socket @@ -113,9 +112,8 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, # generated client_id's enforce length rules if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') - if log: - self._logger = logging.getLogger('log') - self._logger.setLevel(logging.DEBUG) + self._logger = logging.getLogger('log') + self._logger.setLevel(logging.INFO) # subscription method handler dictionary self._method_handlers = {} self._is_connected = False @@ -547,7 +545,7 @@ def wait_for_msg(self, timeout=0.1): # call the topic's handler method if topic in self._method_handlers: method_handler = self._method_handlers[topic] - method_handler(topic, str(msg, 'utf-8')) + method_handler(self, topic, str(msg, 'utf-8')) if res[0] & const(0x06) == const(0x02): pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) From ebff8e4f03dd455a417a46d64709d1c6281bf543 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 12:46:07 -0400 Subject: [PATCH 067/148] docstring the strict method handler signature --- adafruit_minimqtt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 691d089..913b1bb 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -367,6 +367,8 @@ def subscribe(self, topic, method_handler=None, qos=0): :param str topic: Unique MQTT topic identifier. :param method method_handler: Predefined method for handling messages recieved from a topic. Defaults to default_sub_handler if None. + A user-defined metod_handler MUST use the following signature: + method_handler(client, topic, message) :param int qos: Quality of Service level for the topic. Example of subscribing to one topic: From f17f74f5583572de3fc68e9877a7711577a65440 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 13:38:42 -0400 Subject: [PATCH 068/148] remove topic-based callbacks in favor of default user-defined callbacks since wildcards could become a problem here. --- adafruit_minimqtt.py | 68 ++++++-------------------------------------- 1 file changed, 9 insertions(+), 59 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 913b1bb..2308089 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -115,7 +115,6 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._logger = logging.getLogger('log') self._logger.setLevel(logging.INFO) # subscription method handler dictionary - self._method_handlers = {} self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM self.server = server_address @@ -123,7 +122,8 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._keep_alive = 0 self._pid = 0 self._user_data = None - # paho-style method callbacks + # Server callbacks + self.on_message = None self.on_connect = None self.on_disconnect = None self.on_publish = None @@ -182,9 +182,10 @@ def reconnect(self, retries=30, resub_topics=False): self._logger.debug('Reconnected to broker') if resub_topics: self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') - while len(self._method_handlers) > 0: - feed = self._method_handlers.popitem() - self.subscribe(feed) + # TODO - add topic-based resubscription + #while len(self._method_handlers) > 0: + # feed = self._method_handlers.popitem() + # self.subscribe(feed) def is_connected(self): """Returns MQTT client session status.""" @@ -361,14 +362,10 @@ def publish(self, topic, msg, retain=False, qos=0): raise NotImplementedError('on_publish callback not implemented for QoS > 1.') assert 0 - def subscribe(self, topic, method_handler=None, qos=0): + def subscribe(self, topic, qos=0): """Subscribes to a topic on the MQTT Broker. This method can subscribe to one topics or multiple topics. :param str topic: Unique MQTT topic identifier. - :param method method_handler: Predefined method for handling messages - recieved from a topic. Defaults to default_sub_handler if None. - A user-defined metod_handler MUST use the following signature: - method_handler(client, topic, message) :param int qos: Quality of Service level for the topic. Example of subscribing to one topic: @@ -378,22 +375,11 @@ def subscribe(self, topic, method_handler=None, qos=0): Example of subscribing to one topic and setting the Quality of Service level to 1: .. code-block:: python mqtt_client.subscribe('topics/ledState', 1) - - Example of subscribing to one topic and attaching a method handler: - .. code-block:: python - mqtt_client.subscribe('topics/ledState', led_setter) """ if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') if topic is None or len(topic) == 0: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") - if topic in self._method_handlers: - raise MMQTTException('Already subscribed to topic.') - # associate topic subscription with method_handler. - if method_handler is None: - self._method_handlers.update( {topic : self.default_sub_handler} ) - else: - self._method_handlers.update( {topic : method_handler} ) if self._sock is None: raise MMQTTException("MiniMQTT not connected.") pkt = MQTT_SUB @@ -422,8 +408,7 @@ def unsubscribe(self, topic): """ if topic is None or len(topic) == 0: raise MMQTTException("Invalid MQTT topic - must have a length > 0.") - if topic not in self._method_handlers: - raise MMQTTException('Must be subscribed to %s before unsubscribing'%topic) + # TODO: Check if topic is already subscribed before attempting to unsubscribe pkt = MQTT_UNSUB self._pid+=1 # variable header length @@ -438,8 +423,7 @@ def unsubscribe(self, topic): # attempt to UNSUBACK self._logger.debug('Sending UNSUBACK') op = self.wait_for_msg(0.1) - # remove topic and method from method_handlers - self._method_handlers.pop(topic) + # TODO: Remove topic from topic list! if self.on_unsubscribe is not None: self.on_unsubscribe(self, self._user_data) return @@ -488,36 +472,6 @@ def publish_multiple(self, data, timeout=0.0): self.publish(topic, msg, retain, qos) time.sleep(timeout) - def subscribe_multiple(self, topic_info, timeout=1.0): - """Subscribes to multiple MQTT broker topics. - :param tuple topic_info: A list of tuple format: - :param str topic: Unique topic identifier. - :param method method_handler: Predefined method for handling messages - recieved from a topic. Defaults to default_sub_handler if None. - :param int qos: Quality of Service level for the topic. Defaults to 0. - :param float timeout: Timeout between calls to subscribe(). - """ - #TODO: This could be simplified - # 1 mqtt subscription call, multiple topics - print('topics:', topic_info) - for i in range(len(topic_info)): - topic = topic_info[i][0] - try: - if topic_info[i][1]: - method_handler = topic_info[i][1] - except IndexError: - method_handler = None - pass - try: - if topic_info[i][2]: - qos = topic_info[i][2] - except IndexError: - qos = 0 - pass - print('Subscribing to:', topic, method_handler, qos) - self.subscribe(topic, method_handler, qos) - time.sleep(timeout) - def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. :param float timeout: The time in seconds to wait for network before returning. @@ -544,10 +498,6 @@ def wait_for_msg(self, timeout=0.1): pid = pid[0] << const(0x08) | pid[1] sz -= const(0x02) msg = self._sock.read(sz) - # call the topic's handler method - if topic in self._method_handlers: - method_handler = self._method_handlers[topic] - method_handler(self, topic, str(msg, 'utf-8')) if res[0] & const(0x06) == const(0x02): pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) From 8b864e3ad41dbfcf19e47b66ee0ab56b68a7771b Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 13:43:31 -0400 Subject: [PATCH 069/148] fix PUBLISH log statement --- adafruit_minimqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2308089..59e0f7c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -330,7 +330,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {1}\n Retain: {2}'.format(topic, msg, qos, retain)) + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\n Retain: {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: From d36b7160c5b0db5838f8b95cc70727b9430ae820 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 13:47:16 -0400 Subject: [PATCH 070/148] add callback to on_message within wait_for_msg, handle encoding the msg properly --- adafruit_minimqtt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 59e0f7c..38d9601 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -498,6 +498,8 @@ def wait_for_msg(self, timeout=0.1): pid = pid[0] << const(0x08) | pid[1] sz -= const(0x02) msg = self._sock.read(sz) + if self.on_message is not None: + self.on_message(self, topic, str(msg, 'utf-8')) if res[0] & const(0x06) == const(0x02): pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) From 2f7a4d5ef0b2a3a1651f2c44a258f43abe56f0f0 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 13:47:55 -0400 Subject: [PATCH 071/148] update pubsub test for on_message --- examples/test_mqtt_client.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index f9d0517..1335915 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -98,6 +98,8 @@ def test_mqtts_connect_disconnect_esp32spi(): def test_sub_pub(): """Creates a MQTTS client, connects, subscribes, publishes, and checks data received from broker matches data sent by client""" + MSG_TOPIC = 'brubell/feeds/testfeed' + MSG_DATA = 42 mqtt_client = MQTT(esp, socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], is_ssl=True) @@ -106,16 +108,19 @@ def test_sub_pub(): callback_msgs = [] def on_message(client, topic, msg): callback_msgs.append([topic, msg]) - print(callback_msgs) + mqtt_client.on_message = on_message mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe('brubell/feeds/testfeed', on_message) - mqtt_client.publish('brubell/feeds/testfeed', 42) + mqtt_client.subscribe(MSG_TOPIC) + mqtt_client.publish(MSG_TOPIC, MSG_DATA) start_timer = time.monotonic() + print('listening...') while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): mqtt_client.wait_for_msg() - # check message with payload sent has been RXd properly - print(callback_msgs) + # check message+topic has been RX'd by the client's callback + assertEqual(callback_msgs[0][0], MSG_TOPIC) + assertEqual(callback_msgs[0][1], str(MSG_DATA)) + mqtt_client.disconnect() # Timeout between tests, in seconds. This value depends on the timeout of your MQTT broker. @@ -129,7 +134,7 @@ def on_message(client, topic, msg): pub_sub_tests = [test_sub_pub] # The test routine runs the following test(s): -tests = pub_sub_tests +tests = [test_sub_pub] # Establish ESP32SPI connection print("Connecting to AP...") From af8e094c01b763501ecafad71973e8c7857da083 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 14:03:25 -0400 Subject: [PATCH 072/148] topic-based resub, add a subscribed_topics list to track topics --- adafruit_minimqtt.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 38d9601..3853341 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -122,6 +122,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, self._keep_alive = 0 self._pid = 0 self._user_data = None + self._subscribed_topics = [] # Server callbacks self.on_message = None self.on_connect = None @@ -182,10 +183,9 @@ def reconnect(self, retries=30, resub_topics=False): self._logger.debug('Reconnected to broker') if resub_topics: self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') - # TODO - add topic-based resubscription - #while len(self._method_handlers) > 0: - # feed = self._method_handlers.popitem() - # self.subscribe(feed) + while len(self._subscribed_topics) > 0: + feed = self._subscribed_topics.pop() + self.subscribe(feed) def is_connected(self): """Returns MQTT client session status.""" @@ -382,6 +382,8 @@ def subscribe(self, topic, qos=0): raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if self._sock is None: raise MMQTTException("MiniMQTT not connected.") + if isinstance(topic, list): + topic_qos_list = [] pkt = MQTT_SUB self._pid += 11 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) @@ -408,7 +410,8 @@ def unsubscribe(self, topic): """ if topic is None or len(topic) == 0: raise MMQTTException("Invalid MQTT topic - must have a length > 0.") - # TODO: Check if topic is already subscribed before attempting to unsubscribe + if topic in self._subscribed_topics: + raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') pkt = MQTT_UNSUB self._pid+=1 # variable header length @@ -423,7 +426,8 @@ def unsubscribe(self, topic): # attempt to UNSUBACK self._logger.debug('Sending UNSUBACK') op = self.wait_for_msg(0.1) - # TODO: Remove topic from topic list! + # remove topic from subscription list + self._subscribed_topics.remove(topic) if self.on_unsubscribe is not None: self.on_unsubscribe(self, self._user_data) return From 5ced62c2cc20461238d5e46efe9ba90be3dac720 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 16:19:22 -0400 Subject: [PATCH 073/148] multiple send start work --- adafruit_minimqtt.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 3853341..f91a607 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -382,18 +382,34 @@ def subscribe(self, topic, qos=0): raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if self._sock is None: raise MMQTTException("MiniMQTT not connected.") + topic_qos_list = None if isinstance(topic, list): topic_qos_list = [] - pkt = MQTT_SUB + for t, q in topic: + if q < 0 or q > 2: + raise MMQTTException('QoS level must be between 1 and 2.') + if t is None or len(t) == 0: + raise MMQTTException("Invalid MQTT Topic, must have length > 0.") + topic_qos_list.append((t, q)) + print('TOPIC QOS LIST:', topic_qos_list) # TODO: remove this! + payload = bytearray(b'\x82\0\0\0') + # generate message ID self._pid += 11 - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self._pid) - self._logger.debug('Subscribing to {0} with a QOS of {1}'.format(topic, qos)) - self._sock.write(pkt) + # calculate the length + remaining_length = 2 + for t in topic_qos_list: + remaining_length += 2 + len(t) +1 + struct.pack_into("!BH", payload, 1, remaining_length, self._pid) + print('payload: ', payload) + self._sock.write(payload) # [MQTT-3.8.3-1] - self._send_str(topic) - self._sock.write(qos.to_bytes(1, "little")) + for t, q in topic_qos_list: + self._send_str(t) + self._sock.write(q.to_bytes(1, "little")) while 1: + print('waiting for OP.') op = self.wait_for_msg() + print('OP:', op) if op == 0x90: rc = self._sock.read(4) assert rc[1] == pkt[2] and rc[2] == pkt[3] @@ -404,6 +420,7 @@ def subscribe(self, topic, qos=0): return + def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. From 0668f106a2066f69088aafcbef78eaac551f4efa Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 17:07:33 -0400 Subject: [PATCH 074/148] adding new subscription code, broken down --- adafruit_minimqtt.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f91a607..859f3d6 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -378,38 +378,39 @@ def subscribe(self, topic, qos=0): """ if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') - if topic is None or len(topic) == 0: - raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if self._sock is None: raise MMQTTException("MiniMQTT not connected.") topic_qos_list = None + if isinstance(topic, str): + if topic is None or len(topic) == 0 or len(topic.encode('utf-8')) > 65536: + raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if isinstance(topic, list): topic_qos_list = [] for t, q in topic: if q < 0 or q > 2: raise MMQTTException('QoS level must be between 1 and 2.') - if t is None or len(t) == 0: + if t is None or len(t) == 0 or len(topic.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topic_qos_list.append((t, q)) print('TOPIC QOS LIST:', topic_qos_list) # TODO: remove this! - payload = bytearray(b'\x82\0\0\0') - # generate message ID - self._pid += 11 - # calculate the length - remaining_length = 2 - for t in topic_qos_list: - remaining_length += 2 + len(t) +1 - struct.pack_into("!BH", payload, 1, remaining_length, self._pid) - print('payload: ', payload) - self._sock.write(payload) - # [MQTT-3.8.3-1] - for t, q in topic_qos_list: - self._send_str(t) - self._sock.write(q.to_bytes(1, "little")) + if topic_qos_list is not None: + packet_length = 2 + (2 * len(topic)) + (1 * len(topic)) + sum(len(t) for t, qos in topic_qos_list) + else: + packet_length = 2 + 2 + len(topic) + 1 + packet_length_byte = packet_length.to_bytes(2, 'big') + + self._pid += 1 + packet_id_bytes = self._pid.to_bytes(2, 'big') + + topic_size = len(topic).to_bytes(2, 'big') + topic_bytes = topic + qos_byte = qos.to_bytes(1, 'little') + # Assembled packet + packet = b'\x82' + packet_length_byte + packet_id_bytes + topic_size + topic_bytes +qos_byte + print(packet) + self._sock.write(packet) while 1: - print('waiting for OP.') op = self.wait_for_msg() - print('OP:', op) if op == 0x90: rc = self._sock.read(4) assert rc[1] == pkt[2] and rc[2] == pkt[3] From ef151e7f7e770f3c3131b0c380907caedb1cf37f Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 17:16:27 -0400 Subject: [PATCH 075/148] working single subscription --- adafruit_minimqtt.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 859f3d6..799567b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -393,18 +393,17 @@ def subscribe(self, topic, qos=0): raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topic_qos_list.append((t, q)) print('TOPIC QOS LIST:', topic_qos_list) # TODO: remove this! - if topic_qos_list is not None: - packet_length = 2 + (2 * len(topic)) + (1 * len(topic)) + sum(len(t) for t, qos in topic_qos_list) - else: - packet_length = 2 + 2 + len(topic) + 1 - packet_length_byte = packet_length.to_bytes(2, 'big') + + # PID toplen topic qos + packet_length = 2 + 2 + len(topic) + 1 + packet_length_byte = packet_length.to_bytes(1, 'big') self._pid += 1 packet_id_bytes = self._pid.to_bytes(2, 'big') topic_size = len(topic).to_bytes(2, 'big') topic_bytes = topic - qos_byte = qos.to_bytes(1, 'little') + qos_byte = qos.to_bytes(1, 'big') # Assembled packet packet = b'\x82' + packet_length_byte + packet_id_bytes + topic_size + topic_bytes +qos_byte print(packet) @@ -413,7 +412,7 @@ def subscribe(self, topic, qos=0): op = self.wait_for_msg() if op == 0x90: rc = self._sock.read(4) - assert rc[1] == pkt[2] and rc[2] == pkt[3] + assert rc[1] == packet[2] and rc[2] == packet[3] if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') if self.on_subscribe is not None: From 30a497ae69e3bdeba57c511e78f421178eb0fbe2 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 17:43:41 -0400 Subject: [PATCH 076/148] subscriptions to multiple topics work! --- adafruit_minimqtt.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 799567b..6f26ef0 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -59,7 +59,7 @@ # MQTT Commands MQTT_PING_REQ = b'\xc0' MQTT_PINGRESP = b'\xd0' -MQTT_SUB = bytearray(b'\x82\0\0\0') +MQTT_SUB = b'\x82' MQTT_UNSUB = bytearray(b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') @@ -376,40 +376,43 @@ def subscribe(self, topic, qos=0): .. code-block:: python mqtt_client.subscribe('topics/ledState', 1) """ - if qos < 0 or qos > 2: - raise MMQTTException('QoS level must be between 1 and 2.') if self._sock is None: raise MMQTTException("MiniMQTT not connected.") - topic_qos_list = None + topics = None if isinstance(topic, str): if topic is None or len(topic) == 0 or len(topic.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") + if qos < 0 or qos > 2: + raise MMQTTException('QoS level must be between 1 and 2.') + topics = [(topic, qos)] if isinstance(topic, list): - topic_qos_list = [] + topics = [] for t, q in topic: if q < 0 or q > 2: raise MMQTTException('QoS level must be between 1 and 2.') - if t is None or len(t) == 0 or len(topic.encode('utf-8')) > 65536: + if t is None or len(t) == 0 or len(t.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") - topic_qos_list.append((t, q)) - print('TOPIC QOS LIST:', topic_qos_list) # TODO: remove this! - + topics.append((t, q)) # PID toplen topic qos - packet_length = 2 + 2 + len(topic) + 1 + #packet_length = 2 + 2 + len(topic) + 1 + packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic, qos in topics) packet_length_byte = packet_length.to_bytes(1, 'big') - + self._pid += 1 packet_id_bytes = self._pid.to_bytes(2, 'big') - topic_size = len(topic).to_bytes(2, 'big') - topic_bytes = topic - qos_byte = qos.to_bytes(1, 'big') - # Assembled packet - packet = b'\x82' + packet_length_byte + packet_id_bytes + topic_size + topic_bytes +qos_byte - print(packet) + # Packet with variable and fixed headers + packet = MQTT_SUB + packet_length_byte + packet_id_bytes + for topic, qos in topics: + topic_size = len(topic).to_bytes(2, 'big') + qos_byte = qos.to_bytes(1, 'big') + packet += topic_size + topic + qos_byte + + self._logger.debug('SUBSCRIBING to topic(s)') self._sock.write(packet) while 1: op = self.wait_for_msg() + print(op) if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] @@ -419,8 +422,6 @@ def subscribe(self, topic, qos=0): self.on_subscribe(self, self._user_data, rc[3]) return - - def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. From 599a70f6ae54f0fb6da3bcc6725fec15bdf825a6 Mon Sep 17 00:00:00 2001 From: brentru Date: Fri, 5 Jul 2019 17:45:26 -0400 Subject: [PATCH 077/148] add comments --- adafruit_minimqtt.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 6f26ef0..bbcd0a8 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -393,21 +393,20 @@ def subscribe(self, topic, qos=0): if t is None or len(t) == 0 or len(t.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topics.append((t, q)) - # PID toplen topic qos - #packet_length = 2 + 2 + len(topic) + 1 + # Assemble packet packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic, qos in topics) packet_length_byte = packet_length.to_bytes(1, 'big') - - self._pid += 1 + self._pid += 11 packet_id_bytes = self._pid.to_bytes(2, 'big') # Packet with variable and fixed headers packet = MQTT_SUB + packet_length_byte + packet_id_bytes + + # attaching topic and QOS level to the packet for topic, qos in topics: topic_size = len(topic).to_bytes(2, 'big') qos_byte = qos.to_bytes(1, 'big') packet += topic_size + topic + qos_byte - self._logger.debug('SUBSCRIBING to topic(s)') self._sock.write(packet) while 1: From 951443cca814d9460ab48db16fff0d4e429e1091 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 11:02:06 -0400 Subject: [PATCH 078/148] accept tuple into subscribe method signature --- adafruit_minimqtt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bbcd0a8..6db0039 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -379,6 +379,8 @@ def subscribe(self, topic, qos=0): if self._sock is None: raise MMQTTException("MiniMQTT not connected.") topics = None + if isinstance(topic, tuple): + topic, qos = topic if isinstance(topic, str): if topic is None or len(topic) == 0 or len(topic.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") @@ -411,7 +413,6 @@ def subscribe(self, topic, qos=0): self._sock.write(packet) while 1: op = self.wait_for_msg() - print(op) if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] From e9c07d7af98a2844346b0e2c4b1faba0ab37d72b Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 11:13:28 -0400 Subject: [PATCH 079/148] log subscriptions to multiple topics --- adafruit_minimqtt.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 6db0039..cc81cbd 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -366,15 +366,24 @@ def subscribe(self, topic, qos=0): """Subscribes to a topic on the MQTT Broker. This method can subscribe to one topics or multiple topics. :param str topic: Unique MQTT topic identifier. - :param int qos: Quality of Service level for the topic. + :param int qos: Quality of Service level for the topic, defaults to zero. - Example of subscribing to one topic: + Example of subscribing a topic string. .. code-block:: python mqtt_client.subscribe('topics/ledState') - Example of subscribing to one topic and setting the Quality of Service level to 1: + Example of subscribing to a topic and setting the qos level to 1. .. code-block:: python mqtt_client.subscribe('topics/ledState', 1) + + Example of subscribing to topic string and setting qos level to 1, as a tuple. + .. code-block:: python + mqtt_client.subscribe(('topics/ledState', 1)) + + Example of subscribing to multiple topics with different qos levels. + .. code-block:: python + mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)]) + """ if self._sock is None: raise MMQTTException("MiniMQTT not connected.") @@ -409,7 +418,11 @@ def subscribe(self, topic, qos=0): topic_size = len(topic).to_bytes(2, 'big') qos_byte = qos.to_bytes(1, 'big') packet += topic_size + topic + qos_byte - self._logger.debug('SUBSCRIBING to topic(s)') + if len(topics) == 1: + self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topics[0][0], topics[0][1])) + else: + for topic, qos in topics: + self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) self._sock.write(packet) while 1: op = self.wait_for_msg() From 8db7be27f533f69248968fc19bcd2a3559967fe2 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 11:24:45 -0400 Subject: [PATCH 080/148] extend docstring for subscribe --- adafruit_minimqtt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index cc81cbd..bd94c9c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -367,6 +367,8 @@ def subscribe(self, topic, qos=0): This method can subscribe to one topics or multiple topics. :param str topic: Unique MQTT topic identifier. :param int qos: Quality of Service level for the topic, defaults to zero. + :param tuple topic: Tuple containing topic identifier strings and qos level integers. + :param list topic: List of tuples containing topic identifier strings and qos level integers. Example of subscribing a topic string. .. code-block:: python @@ -383,7 +385,6 @@ def subscribe(self, topic, qos=0): Example of subscribing to multiple topics with different qos levels. .. code-block:: python mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)]) - """ if self._sock is None: raise MMQTTException("MiniMQTT not connected.") @@ -436,8 +437,9 @@ def subscribe(self, topic, qos=0): return def unsubscribe(self, topic): - """Unsubscribes from a MQTT topic. - :param str topic: Unique MQTT topic identifier. + """Unsubscribes the client from subscribed mqtt topic(s). + :param str topic: Topic identifier. + :param list topic: List of topic identifier strings. """ if topic is None or len(topic) == 0: raise MMQTTException("Invalid MQTT topic - must have a length > 0.") From 972f995b69406f1715260c797f93e6111dcd7333 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 12:04:25 -0400 Subject: [PATCH 081/148] allow multiple UNSUBSCRIBE topics, add back subscribed_topics list to subscribe and unsubscribe --- adafruit_minimqtt.py | 72 ++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bd94c9c..0e15cd2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -60,7 +60,8 @@ MQTT_PING_REQ = b'\xc0' MQTT_PINGRESP = b'\xd0' MQTT_SUB = b'\x82' -MQTT_UNSUB = bytearray(b'\xA2\0\0\0') +MQTT_UNSUB = b'\xA2' +#MQTT_UNSUB = bytearray(b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable CONNECT header [MQTT 3.1.2] @@ -410,30 +411,28 @@ def subscribe(self, topic, qos=0): packet_length_byte = packet_length.to_bytes(1, 'big') self._pid += 11 packet_id_bytes = self._pid.to_bytes(2, 'big') - # Packet with variable and fixed headers packet = MQTT_SUB + packet_length_byte + packet_id_bytes - # attaching topic and QOS level to the packet for topic, qos in topics: topic_size = len(topic).to_bytes(2, 'big') qos_byte = qos.to_bytes(1, 'big') packet += topic_size + topic + qos_byte - if len(topics) == 1: - self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topics[0][0], topics[0][1])) - else: - for topic, qos in topics: - self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) + for topic, qos in topics: + self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) self._sock.write(packet) while 1: op = self.wait_for_msg() if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] + print('a') if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') - if self.on_subscribe is not None: - self.on_subscribe(self, self._user_data, rc[3]) + for t in topics: + if self.on_subscribe is not None: + self.on_subscribe(self, self._user_data, rc[3]) + self._subscribed_topics.append(t[0]) return def unsubscribe(self, topic): @@ -441,28 +440,43 @@ def unsubscribe(self, topic): :param str topic: Topic identifier. :param list topic: List of topic identifier strings. """ - if topic is None or len(topic) == 0: - raise MMQTTException("Invalid MQTT topic - must have a length > 0.") - if topic in self._subscribed_topics: - raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') - pkt = MQTT_UNSUB - self._pid+=1 - # variable header length - remaining_length = 2 - remaining_length += 2 + len(topic) - struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) - self._logger.debug('Unsubscribing from %s'%topic) - self._sock.write(pkt) - self._send_str(topic) + topics = None + if isinstance(topic, str): + if topic is None or len(topic) == 0 or len(t.encode('utf-8')) > 65536: + raise MMQTTException("Invalid MQTT topic.") + if topic in self._subscribed_topics: + raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + topics = [(topic)] + if isinstance(topic, list): + topics = [] + for t in topic: + if t is None or len(t) == 0 or len(t.encode('utf-8')) > 65536: + raise MMQTTException('Invalid MQTT Topic: %s'%t) + topics.append((t)) + # Assemble packet length first + packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic in topics) + packet_length_byte = packet_length.to_bytes(1, 'big') + # packet identifier + self._pid+=11 + packet_id_bytes=self._pid.to_bytes(2, 'big') + # packet with variable and fixed headers + packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes + # attach topics to the packet + for topic in topics: + topic_size = len(topic).to_bytes(2, 'big') + packet += topic_size + topic + # write the packet + self._sock.write(packet) + for t in topics: + self._logger.debug('SUBSCRIBING to topic {0}'.format(t)) while 1: try: - # attempt to UNSUBACK - self._logger.debug('Sending UNSUBACK') - op = self.wait_for_msg(0.1) + op = self.wait_for_msg() # remove topic from subscription list - self._subscribed_topics.remove(topic) - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, self._user_data) + for t in topics: + self._subscribed_topics.remove(t) + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, self._user_data) return except RuntimeError: raise MMQTTException('Could not unsubscribe from feed.') From 41bc399564c44afb170daf6ed9526d74e422aaf6 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 12:15:34 -0400 Subject: [PATCH 082/148] refactor is_connected method, use it as a universal check before network actions --- adafruit_minimqtt.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 0e15cd2..9298936 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -61,7 +61,6 @@ MQTT_PINGRESP = b'\xd0' MQTT_SUB = b'\x82' MQTT_UNSUB = b'\xA2' -#MQTT_UNSUB = bytearray(b'\xA2\0\0\0') MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable CONNECT header [MQTT 3.1.2] @@ -190,6 +189,8 @@ def reconnect(self, retries=30, resub_topics=False): def is_connected(self): """Returns MQTT client session status.""" + if self._sock is None or self._is_connected is False: + raise MMQTTException("MiniMQTT is not connected.") return self._is_connected # Core MQTT Methods @@ -267,8 +268,7 @@ def connect(self, clean_session=True): def disconnect(self): """Disconnects from the broker. """ - if self._sock is None: - raise MMQTTException("MiniMQTT is not connected.") + self.is_connected() self._logger.debug('Sending DISCONNECT packet to server') self._sock.write(MQTT_DISCONNECT) self._logger.debug('Closing socket') @@ -282,6 +282,7 @@ def ping(self): there is an active network connection. Raises a MMQTTException if the server does not respond with a PINGRESP packet. """ + self.is_connected() self._logger.debug('Sending PINGREQ') self._sock.write(MQTT_PING_REQ) res = self._sock.read(1) @@ -300,6 +301,7 @@ def publish(self, topic, msg, retain=False, qos=0): :param bool retain: Whether the message is saved by the broker. :param int qos: Quality of Service level for the message. """ + self.is_connected() if topic is None or len(topic) == 0: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if '+' in topic or '#' in topic: @@ -317,8 +319,6 @@ def publish(self, topic, msg, retain=False, qos=0): raise MMQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) if qos < 0 or qos > 2: raise MMQTTException("Invalid QoS level, must be between 0 and 2.") - if self._sock is None: - raise MMQTTException("MiniMQTT not connected.") pkt = MQTT_PUB pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -331,7 +331,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\n Retain: {3}'.format(topic, msg, qos, retain)) + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: @@ -387,8 +387,7 @@ def subscribe(self, topic, qos=0): .. code-block:: python mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)]) """ - if self._sock is None: - raise MMQTTException("MiniMQTT not connected.") + self.is_connected() topics = None if isinstance(topic, tuple): topic, qos = topic From 66c460c8196b63275853b47bcd2de019b434f96e Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 12:19:16 -0400 Subject: [PATCH 083/148] add on_publish cb for qos of 2, assert --- adafruit_minimqtt.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 9298936..4fe8626 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -359,9 +359,9 @@ def publish(self, topic, msg, retain=False, qos=0): if pid == rcv_pid: return elif qos == 2: - if self.on_publish is not None: - raise NotImplementedError('on_publish callback not implemented for QoS > 1.') assert 0 + if self.on_publish is not None: + self.on_publish(self, self._user_data, rcv_pid) def subscribe(self, topic, qos=0): """Subscribes to a topic on the MQTT Broker. @@ -386,6 +386,7 @@ def subscribe(self, topic, qos=0): Example of subscribing to multiple topics with different qos levels. .. code-block:: python mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)]) + """ self.is_connected() topics = None @@ -438,6 +439,15 @@ def unsubscribe(self, topic): """Unsubscribes the client from subscribed mqtt topic(s). :param str topic: Topic identifier. :param list topic: List of topic identifier strings. + + Example of unsubscribing from a topic. + .. code-block:: python + mqtt_client.unsubscribe('topics/ledState') + + Example of unsubscribing from multiple topics. + .. code-block:: python + mqtt_client.unsubscribe('topics/ledState', 'topics/servoAngle') + """ topics = None if isinstance(topic, str): From 5b7da5e889aaff31ebf21895f5cc0e60bb1cbe18 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 12:36:35 -0400 Subject: [PATCH 084/148] stricter esp32spi check for attributes in the init --- adafruit_minimqtt.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4fe8626..702ef68 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -79,8 +79,8 @@ class MMQTTException(Exception): class MQTT: """ MQTT client interface for CircuitPython devices. - :param esp: ESP32SPI object. - :param socket: ESP32SPI Socket object. + :param ESP_SPIcontrol esp: An ESP network interface object. + :param socket: Socket object for provided network interface :param str server_address: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. @@ -92,9 +92,12 @@ class MQTT: TLS_MODE = const(2) def __init__(self, esp, socket, server_address, port=8883, username=None, password = None, client_id=None, is_ssl=True): - if esp and socket is not None: - self._esp = esp + if socket is not None: self._socket = socket + else: + raise MMQTTException('MiniMQTT requires a valid socket interface.') + if hasattr(esp, '_gpio0'): + self._esp = esp else: raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI connection.') self.port = port @@ -188,12 +191,12 @@ def reconnect(self, retries=30, resub_topics=False): self.subscribe(feed) def is_connected(self): - """Returns MQTT client session status.""" + """Returns MQTT client session status as True if connected, raises + a MMQTTException if False.""" if self._sock is None or self._is_connected is False: raise MMQTTException("MiniMQTT is not connected.") return self._is_connected - # Core MQTT Methods def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session @@ -295,11 +298,26 @@ def ping(self): raise MMQTTException('Server did not return with PINGRESP') def publish(self, topic, msg, retain=False, qos=0): - """Publishes a message to the MQTT broker. + """Publishes a message to a topic provided. :param str topic: Unique topic identifier. :param str msg: Data to send to the broker. + :param int msg: Data to send to the broker. + :param float msg: Data to send to the broker. :param bool retain: Whether the message is saved by the broker. :param int qos: Quality of Service level for the message. + + Example of sending an integer, 3, to the broker on topic 'piVal'. + .. code-block:: python + mqtt_client.publish('topics/piVal', 3) + + Example of sending a float, 3.14, to the broker on topic 'piVal'. + .. code-block:: python + mqtt_client.publish('topics/piVal', 3.14) + + Example of sending a string, 'threepointonefour', to the broker on topic piVal. + .. code-block:: python + mqtt_client.publish('topics/piVal', 'threepointonefour') + """ self.is_connected() if topic is None or len(topic) == 0: From 93ca0832f84edbae99f6984a23f30adc605a59e1 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 12:54:36 -0400 Subject: [PATCH 085/148] redo init to be flexible enough for future network interfaces, fix how port is set to allow non-ssl/tls ports like 3033 --- adafruit_minimqtt.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 702ef68..2a8bddd 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -49,12 +49,12 @@ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" - # Client-specific variables MQTT_MSG_MAX_SZ = const(268435455) MQTT_MSG_SZ_LIM = const(10000000) MQTT_TOPIC_SZ_LIMIT = const(65536) MQTT_TCP_PORT = const(1883) +MQTT_TLS_PORT = const(8883) # MQTT Commands MQTT_PING_REQ = b'\xc0' @@ -79,30 +79,36 @@ class MMQTTException(Exception): class MQTT: """ MQTT client interface for CircuitPython devices. - :param ESP_SPIcontrol esp: An ESP network interface object. :param socket: Socket object for provided network interface :param str server_address: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. :param str password: Password for broker authentication. - :param str client_id: Optional client identifier, defaults to a randomly generated id. - :param bool is_ssl: Enables TCP mode if false (port 1883). Defaults to True (port 8883). + :param ESP_SPIcontrol esp: An ESP network interface object. + :param str client_id: Optional client identifier, defaults to a unique, generated string. + :param bool is_ssl: Sets a secure or insecure connection with the broker. Defaults to True (port 8883). """ TCP_MODE = const(0) TLS_MODE = const(2) - def __init__(self, esp, socket, server_address, port=8883, username=None, - password = None, client_id=None, is_ssl=True): - if socket is not None: - self._socket = socket + def __init__(self, socket, server_address, port=None, username=None, + password = None, esp=None, client_id=None, is_ssl=True): + # network interface + self._socket = socket + if esp is not None: + if hasattr(esp, '_gpio0'): + self._esp = esp + else: + raise MMQTTException('Invalid ESP32SPI object provided.') else: - raise MMQTTException('MiniMQTT requires a valid socket interface.') - if hasattr(esp, '_gpio0'): - self._esp = esp - else: - raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI connection.') - self.port = port - if not is_ssl: + raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object, please provide one.') + # port/ssl + if is_ssl: self.port = MQTT_TCP_PORT + else: + self.port = MQTT_TLS_PORT + if port is not None: + self.port = port + # session identifiers self._user = username self._pass = password if client_id is not None: @@ -112,7 +118,7 @@ def __init__(self, esp, socket, server_address, port=8883, username=None, else: # assign a unique client_id self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) - # generated client_id's enforce length rules + # generated client_id's enforce spec's stright length rules if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') self._logger = logging.getLogger('log') From 5a168699e15c120388763da8637dd4870345de4f Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 13:08:40 -0400 Subject: [PATCH 086/148] re-enable log boolean, check if the logger handler has been attached before outputting log info, remove old default handler --- adafruit_minimqtt.py | 82 +++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2a8bddd..e5ad5ef 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -87,11 +87,12 @@ class MQTT: :param ESP_SPIcontrol esp: An ESP network interface object. :param str client_id: Optional client identifier, defaults to a unique, generated string. :param bool is_ssl: Sets a secure or insecure connection with the broker. Defaults to True (port 8883). + :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ TCP_MODE = const(0) TLS_MODE = const(2) def __init__(self, socket, server_address, port=None, username=None, - password = None, esp=None, client_id=None, is_ssl=True): + password = None, esp=None, client_id=None, is_ssl=True, log = False): # network interface self._socket = socket if esp is not None: @@ -118,11 +119,13 @@ def __init__(self, socket, server_address, port=None, username=None, else: # assign a unique client_id self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) - # generated client_id's enforce spec's stright length rules + # generated client_id's enforce spec.'s length rules if len(self._client_id) > 23 or len(self._client_id) < 1: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') - self._logger = logging.getLogger('log') - self._logger.setLevel(logging.INFO) + self._logger = None + if log is True: + self._logger = logging.getLogger('log') + self._logger.setLevel(logging.INFO) # subscription method handler dictionary self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM @@ -139,7 +142,6 @@ def __init__(self, socket, server_address, port=None, username=None, self.on_publish = None self.on_subscribe = None self.on_unsubscribe = None - self.on_log = None self.last_will() def __enter__(self): @@ -164,7 +166,8 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): raise MMQTTException('Last Will should be defined before connect() is called.') if qos < 0 or qos > 2: raise MMQTTException("Invalid QoS level, must be between 0 and 2.") - self._logger.debug('Setting last will properties') + if self._logger is not None: + self._logger.debug('Setting last will properties') self._lw_qos = qos self._lw_topic = topic self._lw_msg = message @@ -178,7 +181,8 @@ def reconnect(self, retries=30, resub_topics=False): """ retries = 0 while not self._is_connected: - self._logger.debug('Attempting to reconnect to broker') + if self._logger is not None: + self._logger.debug('Attempting to reconnect to broker') try: self.connect(False) except OSError as e: @@ -189,9 +193,11 @@ def reconnect(self, retries=30, resub_topics=False): self._esp.reset() continue self._is_connected = True - self._logger.debug('Reconnected to broker') + if self._logger is not None: + self._logger.debug('Reconnected to broker') if resub_topics: - self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') + if self._logger is not None: + self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') while len(self._subscribed_topics) > 0: feed = self._subscribed_topics.pop() self.subscribe(feed) @@ -209,7 +215,8 @@ def connect(self, clean_session=True): with the broker. Defaults to a non-persistent session. """ if self._esp: - self._logger.debug('Creating new socket') + if self._logger is not None: + self._logger.debug('Creating new socket') self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: @@ -217,14 +224,16 @@ def connect(self, clean_session=True): self._sock.settimeout(10) if self.port == 8883: try: - self._logger.debug('Attempting to establish secure MQTT connection with %s'%self.server) + if self._logger is not None: + self._logger.debug('Attempting to establish secure MQTT connection with %s'%self.server) self._sock.connect((self.server, self.port), TLS_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] try: - self._logger.debug('Attempting to establish an insecure MQTT connection with %s'%self.server) + if self._logger is not None: + self._logger.debug('Attempting to establish insecure MQTT connection with %s'%self.server) self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") @@ -249,7 +258,8 @@ def connect(self, clean_session=True): sz >>= 7 i += 1 premsg[i] = sz - self._logger.debug('Sending CONNECT packet to server') + if self._logger is not None: + self._logger.debug('Sending CONNECT packet to server') self._sock.write(premsg) self._sock.write(msg) # [MQTT-3.1.3-4] @@ -263,7 +273,8 @@ def connect(self, clean_session=True): else: self._send_str(self._user) self._send_str(self._pass) - self._logger.debug('Receiving CONNACK packet from server') + if self._logger is not None: + self._logger.debug('Receiving CONNACK packet from server') rc = self._sock.read(4) assert rc[0] == const(0x20) and rc[1] == const(0x02) if rc[3] !=0: @@ -278,9 +289,11 @@ def disconnect(self): """Disconnects from the broker. """ self.is_connected() - self._logger.debug('Sending DISCONNECT packet to server') + if self._logger is not None: + self._logger.debug('Sending DISCONNECT packet to server') self._sock.write(MQTT_DISCONNECT) - self._logger.debug('Closing socket') + if self._logger is not None: + self._logger.debug('Closing socket') self._sock.close() self._is_connected = False if self.on_disconnect is not None: @@ -292,10 +305,12 @@ def ping(self): Raises a MMQTTException if the server does not respond with a PINGRESP packet. """ self.is_connected() - self._logger.debug('Sending PINGREQ') + if self._logger is not None: + self._logger.debug('Sending PINGREQ') self._sock.write(MQTT_PING_REQ) res = self._sock.read(1) - self._logger.debug('Checking PINGRESP') + if self._logger is not None: + self._logger.debug('Checking PINGRESP') if res == MQTT_PINGRESP: sz = self._sock.read(1)[0] assert sz == 0 @@ -355,7 +370,8 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) + if self._logger is not None: + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: @@ -368,7 +384,8 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) - self._logger.debug('Sending PUBACK') + if self._logger is not None: + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: @@ -442,8 +459,9 @@ def subscribe(self, topic, qos=0): topic_size = len(topic).to_bytes(2, 'big') qos_byte = qos.to_bytes(1, 'big') packet += topic_size + topic + qos_byte - for topic, qos in topics: - self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) + if self._logger is not None: + for topic, qos in topics: + self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) self._sock.write(packet) while 1: op = self.wait_for_msg() @@ -500,8 +518,9 @@ def unsubscribe(self, topic): packet += topic_size + topic # write the packet self._sock.write(packet) - for t in topics: - self._logger.debug('SUBSCRIBING to topic {0}'.format(t)) + if self._logger is not None: + for t in topics: + self._logger.debug('SUBSCRIBING to topic {0}'.format(t)) while 1: try: op = self.wait_for_msg() @@ -593,7 +612,6 @@ def wait_for_msg(self, timeout=0.1): return res[0] def _recv_len(self): - """Receives the size of the topic length.""" n = 0 sh = 0 while 1: @@ -603,16 +621,8 @@ def _recv_len(self): return n sh += 7 - def default_sub_handler(self, topic, msg): - """Default feed subscription handler method. - :param str topic: Subscription topic. - :param str msg: Message content. - """ - self._logger.debug('default_sub_handler called') - print('New message on {0}: {1}'.format(topic, msg)) - def _send_str(self, string): - """Packs a string into a struct, and writes it to a socket as an utf-8 encoded string. + """Writes a provided string to a socket. :param str string: String to write to the socket. """ self._sock.write(struct.pack("!H", len(string))) @@ -621,8 +631,6 @@ def _send_str(self, string): else: self._sock.write(string) - # Network Loop Methods - def loop(self, timeout=1.0): """Call regularly to process network events. This function blocks for up to timeout seconds. @@ -644,6 +652,8 @@ def logging(self, log_level): :param string log_level: Level of logging to output to the REPL. Accepted levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. """ + if self._logger is None: + raise MMQTTException('No logger attached - did you create it during initialization?') if log_level == 'DEBUG': self._logger.setLevel(logging.DEBUG) elif log_level == 'INFO': From db8f2547198958920b2197d3398e7b95e531993f Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 13:33:09 -0400 Subject: [PATCH 087/148] drop multiple publishing, not defined within spec. drop esp-related actions, let the user handle those instead --- adafruit_minimqtt.py | 54 ++++---------------------------------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index e5ad5ef..b57c65f 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -175,7 +175,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): def reconnect(self, retries=30, resub_topics=False): """Attempts to reconnect to the MQTT broker. - :param int retries: Amount of retries before resetting the ESP32 hardware. + :param int retries: Amount of retries before resetting the provided network interface hardware. :param bool resub_topics: Client resubscribes to previously subscribed topics upon a successful reconnection. """ @@ -190,7 +190,7 @@ def reconnect(self, retries=30, resub_topics=False): retries+=1 if retries >= 30: retries = 0 - self._esp.reset() + time.sleep(0.5) continue self._is_connected = True if self._logger is not None: @@ -370,8 +370,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - if self._logger is not None: - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: @@ -384,8 +383,7 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) - if self._logger is not None: - self._logger.debug('Sending PUBACK') + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: @@ -546,35 +544,6 @@ def mqtt_msg(self, msg_size): if msg_size < MQTT_MSG_MAX_SZ: self.__msg_size_lim = msg_size - def publish_multiple(self, data, timeout=0.0): - """Publishes to multiple MQTT broker topics. - :param tuple data: A list of tuple format: - :param str topic: Unique topic identifier. - :param str msg: Data to send to the broker. - :param bool retain: Whether the message is saved by the broker. - :param int qos: Quality of Service level for the message. - :param float timeout: Timeout between calls to publish(). This value - is usually set by your MQTT broker. Defaults to 1.0 - """ - # TODO: Untested! - for i in range(len(data)): - topic = data[i][0] - msg = data[i][1] - try: - if data[i][2]: - retain = data[i][2] - except IndexError: - retain = False - pass - try: - if data[i][3]: - qos = data[i][3] - except IndexError: - qos = 0 - pass - self.publish(topic, msg, retain, qos) - time.sleep(timeout) - def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. :param float timeout: The time in seconds to wait for network before returning. @@ -631,21 +600,6 @@ def _send_str(self, string): else: self._sock.write(string) - def loop(self, timeout=1.0): - """Call regularly to process network events. - This function blocks for up to timeout seconds. - Timeout must not exceed the keepalive value for the client or - your client will be regularly disconnected by the broker. - :param float timeout: Blocks between calls to wait_for_msg() - """ - return None - - def loop_forever(self): - """Blocking network loop, will not return until disconnect() is called from - the client. Automatically handles the re-connection. - """ - return None - # Logging def logging(self, log_level): """Sets the level of the logger, if defined during init. From 3a3f9d5e3c3702f97e99cb6ddf8695d6627f643f Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 13:42:11 -0400 Subject: [PATCH 088/148] add create_logger method for creating a new logger, post-initialization --- adafruit_minimqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b57c65f..ba49953 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -220,7 +220,7 @@ def connect(self, clean_session=True): self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: - raise TypeError('ESP32SPI interface required!') + raise TypeError('No network interface hardware found.') self._sock.settimeout(10) if self.port == 8883: try: @@ -286,7 +286,7 @@ def connect(self, clean_session=True): return result def disconnect(self): - """Disconnects from the broker. + """Disconnects from the broker and closes the socket. """ self.is_connected() if self._logger is not None: @@ -383,7 +383,8 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) - self._logger.debug('Sending PUBACK') + if self._logger is not None: + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: @@ -466,7 +467,6 @@ def subscribe(self, topic, qos=0): if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] - print('a') if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') for t in topics: @@ -601,13 +601,18 @@ def _send_str(self, string): self._sock.write(string) # Logging - def logging(self, log_level): - """Sets the level of the logger, if defined during init. + def create_logger(self): + """Initalizes a new logger instance. + """ + self._logger = logging.getLogger('log') + + def set_logger_level(self, log_level): + """Sets the level of the logger. :param string log_level: Level of logging to output to the REPL. Accepted levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. """ if self._logger is None: - raise MMQTTException('No logger attached - did you create it during initialization?') + raise MMQTTException('No logger attached to MQTT Client.') if log_level == 'DEBUG': self._logger.setLevel(logging.DEBUG) elif log_level == 'INFO': From eb30162c6ab8a592e94ef875ae17bb61acfabec6 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 14:52:43 -0400 Subject: [PATCH 089/148] add a AMQTT-like example for publishing/subscribing with AIO --- examples/minimqtt_adafruitio.py | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100755 examples/minimqtt_adafruitio.py diff --git a/examples/minimqtt_adafruitio.py b/examples/minimqtt_adafruitio.py new file mode 100755 index 0000000..df00269 --- /dev/null +++ b/examples/minimqtt_adafruitio.py @@ -0,0 +1,77 @@ +import time +import board +import busio +from digitalio import DigitalInOut +import neopixel +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_socket as socket + +from adafruit_minimqtt import MQTT + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +try: + esp32_cs = DigitalInOut(board.ESP_CS) + esp32_ready = DigitalInOut(board.ESP_BUSY) + esp32_reset = DigitalInOut(board.ESP_RESET) +except: + esp32_cs = DigitalInOut(board.D9) + esp32_ready = DigitalInOut(board.D10) + esp32_reset = DigitalInOut(board.D5) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED""" +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) + +# Setup a feed called `photocell` for publishing. +AIO_Publish_Feed = secrets['aio_user']+'feeds/photocell' + +# Setup a feed called `on_off_button` for subscribing to changes. +AIO_Subscribe_Feed = secrets['aio_user']+'feeds/on_off_button' + +# Set up a MiniMQTT Client +mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp, log = True) + +print("Connecting to %s..."%secrets['ssid']) +while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue +print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) +print("IP: ", esp.pretty_ip(esp.ip_address)) + +mqtt_client.connect() + +mqtt_client.subscribe(on_off_button) + +photocell_val = 0 +while True: + # ping the server to keep the mqtt connection alive + mqtt_client.ping() + + subscription_data = mqtt_client.wait_for_msg() + + if subscription_data: + print('Got: {0}'.format(subscription_data)) + + print('Sending photocell value: %d'%photocell_val) + mqtt.publish(AIO_Publish_Feed, photocell_value) From d85dc6ac1a10b4e4d4fed7f854e9000297fd5fd4 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:12:50 -0400 Subject: [PATCH 090/148] pylinting and fixing bugs --- adafruit_minimqtt.py | 128 ++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index ba49953..909be34 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -4,7 +4,7 @@ # # Original Work Copyright (c) 2016 Paul Sokolovsky, uMQTT # Modified Work Copyright (c) 2019 Bradley Beach, esp32spi_mqtt -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights @@ -43,8 +43,8 @@ import time from random import randint import microcontroller -import adafruit_logging as logging from micropython import const +import adafruit_logging as logging __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git" @@ -55,6 +55,8 @@ MQTT_TOPIC_SZ_LIMIT = const(65536) MQTT_TCP_PORT = const(1883) MQTT_TLS_PORT = const(8883) +TCP_MODE = const(0) +TLS_MODE = const(2) # MQTT Commands MQTT_PING_REQ = b'\xc0' @@ -68,13 +70,15 @@ MQTT_DISCONNECT = b'\xe0\0' CONNACK_ERRORS = {const(0x01) : 'Connection Refused - Incorrect Protocol Version', - const(0x02) : 'Connection Refused - ID Rejected', - const(0x03) : 'Connection Refused - Server unavailable', - const(0x04) : 'Connection Refused - Incorrect username/password', - const(0x05) : 'Connection Refused - Unauthorized'} + const(0x02) : 'Connection Refused - ID Rejected', + const(0x03) : 'Connection Refused - Server unavailable', + const(0x04) : 'Connection Refused - Incorrect username/password', + const(0x05) : 'Connection Refused - Unauthorized'} class MMQTTException(Exception): - pass + """MiniMQTT Exception class.""" + # pylint : disable=unnecessary-pass + #pass class MQTT: """ @@ -86,13 +90,13 @@ class MQTT: :param str password: Password for broker authentication. :param ESP_SPIcontrol esp: An ESP network interface object. :param str client_id: Optional client identifier, defaults to a unique, generated string. - :param bool is_ssl: Sets a secure or insecure connection with the broker. Defaults to True (port 8883). + :param bool is_ssl: Sets a secure or insecure connection with the broker. + Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - TCP_MODE = const(0) - TLS_MODE = const(2) + # pylint: disable=too-many-arguments,too-many-instance-attributes def __init__(self, socket, server_address, port=None, username=None, - password = None, esp=None, client_id=None, is_ssl=True, log = False): + password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface self._socket = socket if esp is not None: @@ -101,7 +105,7 @@ def __init__(self, socket, server_address, port=None, username=None, else: raise MMQTTException('Invalid ESP32SPI object provided.') else: - raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object, please provide one.') + raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object.') # port/ssl if is_ssl: self.port = MQTT_TCP_PORT @@ -118,9 +122,10 @@ def __init__(self, socket, server_address, port=None, username=None, self._client_id = client_id else: # assign a unique client_id - self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) + self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], + randint(0, 9)) # generated client_id's enforce spec.'s length rules - if len(self._client_id) > 23 or len(self._client_id) < 1: + if len(self._client_id) > 23 or not self._client_id: raise ValueError('MQTT Client ID must be between 1 and 23 bytes') self._logger = None if log is True: @@ -160,7 +165,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): :param str topic: MQTT Broker topic. :param str message: Last will disconnection message. :param int qos: Quality of Service level. - :param bool retain: Specifies if the message is to be retained when it is published. + :param bool retain: Specifies if the message is to be retained when it is published. """ if self._is_connected: raise MMQTTException('Last Will should be defined before connect() is called.') @@ -175,7 +180,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): def reconnect(self, retries=30, resub_topics=False): """Attempts to reconnect to the MQTT broker. - :param int retries: Amount of retries before resetting the provided network interface hardware. + :param int retries: Amount of retries before resetting the network interface. :param bool resub_topics: Client resubscribes to previously subscribed topics upon a successful reconnection. """ @@ -187,7 +192,7 @@ def reconnect(self, retries=30, resub_topics=False): self.connect(False) except OSError as e: print('Failed to connect to the broker, retrying\n', e) - retries+=1 + retries += 1 if retries >= 30: retries = 0 time.sleep(0.5) @@ -198,7 +203,7 @@ def reconnect(self, retries=30, resub_topics=False): if resub_topics: if self._logger is not None: self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') - while len(self._subscribed_topics) > 0: + while self._subscribed_topics: feed = self._subscribed_topics.pop() self.subscribe(feed) @@ -220,12 +225,12 @@ def connect(self, clean_session=True): self._socket.set_interface(self._esp) self._sock = self._socket.socket() else: - raise TypeError('No network interface hardware found.') + raise TypeError('ESP32SPI interface required!') self._sock.settimeout(10) if self.port == 8883: try: if self._logger is not None: - self._logger.debug('Attempting to establish secure MQTT connection with %s'%self.server) + self._logger.debug('Attempting to establish secure MQTT connection...') self._sock.connect((self.server, self.port), TLS_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") @@ -233,7 +238,7 @@ def connect(self, clean_session=True): addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] try: if self._logger is not None: - self._logger.debug('Attempting to establish insecure MQTT connection with %s'%self.server) + self._logger.debug('Attempting to establish insecure MQTT connection...') self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") @@ -277,16 +282,16 @@ def connect(self, clean_session=True): self._logger.debug('Receiving CONNACK packet from server') rc = self._sock.read(4) assert rc[0] == const(0x20) and rc[1] == const(0x02) - if rc[3] !=0: + if rc[3] != 0: raise MMQTTException(CONNACK_ERRORS[rc[3]]) self._is_connected = True result = rc[2] & 1 if self.on_connect is not None: - self.on_connect(self, self._user_data, result, rc[3]) + self.on_connect(self, self._user_data, result, rc[3]) return result def disconnect(self): - """Disconnects from the broker and closes the socket. + """Disconnects from the broker. """ self.is_connected() if self._logger is not None: @@ -311,12 +316,10 @@ def ping(self): res = self._sock.read(1) if self._logger is not None: self._logger.debug('Checking PINGRESP') - if res == MQTT_PINGRESP: - sz = self._sock.read(1)[0] - assert sz == 0 - return None - else: - raise MMQTTException('Server did not return with PINGRESP') + #sz = self._sock.read(1)[0] + print('resp: ', res) + #print('SZ: ', sz) + #assert sz == 0 def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to a topic provided. @@ -341,7 +344,7 @@ def publish(self, topic, msg, retain=False, qos=0): """ self.is_connected() - if topic is None or len(topic) == 0: + if topic is None or not topic: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if '+' in topic or '#' in topic: raise MMQTTException('Topic can not contain wildcards.') @@ -370,21 +373,21 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\ + \nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: if self.on_publish is not None: self.on_publish(self, self._user_data, self._pid) if qos > 0: - self.pid += 1 - pid = self.pid + self._pid += 1 + pid = self._pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) - if self._logger is not None: - self._logger.debug('Sending PUBACK') + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: @@ -409,7 +412,8 @@ def subscribe(self, topic, qos=0): :param str topic: Unique MQTT topic identifier. :param int qos: Quality of Service level for the topic, defaults to zero. :param tuple topic: Tuple containing topic identifier strings and qos level integers. - :param list topic: List of tuples containing topic identifier strings and qos level integers. + :param list topic: List of tuples containing topic identifier strings and + qos level integers. Example of subscribing a topic string. .. code-block:: python @@ -418,7 +422,7 @@ def subscribe(self, topic, qos=0): Example of subscribing to a topic and setting the qos level to 1. .. code-block:: python mqtt_client.subscribe('topics/ledState', 1) - + Example of subscribing to topic string and setting qos level to 1, as a tuple. .. code-block:: python mqtt_client.subscribe(('topics/ledState', 1)) @@ -433,7 +437,7 @@ def subscribe(self, topic, qos=0): if isinstance(topic, tuple): topic, qos = topic if isinstance(topic, str): - if topic is None or len(topic) == 0 or len(topic.encode('utf-8')) > 65536: + if topic is None or not topic or len(topic.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') @@ -443,21 +447,22 @@ def subscribe(self, topic, qos=0): for t, q in topic: if q < 0 or q > 2: raise MMQTTException('QoS level must be between 1 and 2.') - if t is None or len(t) == 0 or len(t.encode('utf-8')) > 65536: + if t is None or not t or len(t.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topics.append((t, q)) # Assemble packet - packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic, qos in topics) + packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + packet_length += sum(len(topic) for topic, qos in topics) packet_length_byte = packet_length.to_bytes(1, 'big') self._pid += 11 packet_id_bytes = self._pid.to_bytes(2, 'big') # Packet with variable and fixed headers packet = MQTT_SUB + packet_length_byte + packet_id_bytes - # attaching topic and QOS level to the packet - for topic, qos in topics: - topic_size = len(topic).to_bytes(2, 'big') - qos_byte = qos.to_bytes(1, 'big') - packet += topic_size + topic + qos_byte + # attaching topic and QOS level to the packet + for t, q in topics: + topic_size = len(t).to_bytes(2, 'big') + qos_byte = q.to_bytes(1, 'big') + packet += topic_size + t + qos_byte if self._logger is not None: for topic, qos in topics: self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) @@ -467,6 +472,7 @@ def subscribe(self, topic, qos=0): if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] + print('a') if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') for t in topics: @@ -491,23 +497,24 @@ def unsubscribe(self, topic): """ topics = None if isinstance(topic, str): - if topic is None or len(topic) == 0 or len(t.encode('utf-8')) > 65536: + if topic is None or not topic or len(topic.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT topic.") - if topic in self._subscribed_topics: - raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + print('TOPICS:', self._subscribed_topics) + if topic not in self._subscribed_topics: + raise MMQTTException('Topic must be subscribed to before unsubscribing.') topics = [(topic)] if isinstance(topic, list): topics = [] for t in topic: - if t is None or len(t) == 0 or len(t.encode('utf-8')) > 65536: + if t is None or not t or len(t.encode('utf-8')) > 65536: raise MMQTTException('Invalid MQTT Topic: %s'%t) topics.append((t)) # Assemble packet length first packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic in topics) packet_length_byte = packet_length.to_bytes(1, 'big') # packet identifier - self._pid+=11 - packet_id_bytes=self._pid.to_bytes(2, 'big') + self._pid += 11 + packet_id_bytes = self._pid.to_bytes(2, 'big') # packet with variable and fixed headers packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes # attach topics to the packet @@ -521,7 +528,7 @@ def unsubscribe(self, topic): self._logger.debug('SUBSCRIBING to topic {0}'.format(t)) while 1: try: - op = self.wait_for_msg() + self.wait_for_msg() # remove topic from subscription list for t in topics: self._subscribed_topics.remove(t) @@ -595,24 +602,19 @@ def _send_str(self, string): :param str string: String to write to the socket. """ self._sock.write(struct.pack("!H", len(string))) - if type(string) == str: + if isinstance(string, str): self._sock.write(str.encode(string, 'utf-8')) else: self._sock.write(string) # Logging - def create_logger(self): - """Initalizes a new logger instance. - """ - self._logger = logging.getLogger('log') - - def set_logger_level(self, log_level): - """Sets the level of the logger. + def logging(self, log_level): + """Sets the level of the logger, if defined during init. :param string log_level: Level of logging to output to the REPL. Accepted levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. """ if self._logger is None: - raise MMQTTException('No logger attached to MQTT Client.') + raise MMQTTException('No logger attached - did you create it during initialization?') if log_level == 'DEBUG': self._logger.setLevel(logging.DEBUG) elif log_level == 'INFO': @@ -622,4 +624,4 @@ def set_logger_level(self, log_level): elif log_level == 'ERROR': self._logger.setLevel(logging.CRITICIAL) else: - raise MMQTTException('Incorrect logging level provided!') \ No newline at end of file + raise MMQTTException('Incorrect logging level provided!') From 1e70f9b941ae809aa6650e91a559e7270fab65e1 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:22:10 -0400 Subject: [PATCH 091/148] pass pylint --- adafruit_minimqtt.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 909be34..2324f7c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -77,7 +77,7 @@ class MMQTTException(Exception): """MiniMQTT Exception class.""" - # pylint : disable=unnecessary-pass + # pylint: disable=unnecessary-pass #pass class MQTT: @@ -94,7 +94,7 @@ class MQTT: Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - # pylint: disable=too-many-arguments,too-many-instance-attributes + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, no-member, invalid-name def __init__(self, socket, server_address, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface @@ -106,6 +106,7 @@ def __init__(self, socket, server_address, port=None, username=None, raise MMQTTException('Invalid ESP32SPI object provided.') else: raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object.') + self._sock = self._socket.socket() # port/ssl if is_ssl: self.port = MQTT_TCP_PORT @@ -214,6 +215,7 @@ def is_connected(self): raise MMQTTException("MiniMQTT is not connected.") return self._is_connected + # pylint: disable=too-many-branches, too-many-statements def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session @@ -223,7 +225,6 @@ def connect(self, clean_session=True): if self._logger is not None: self._logger.debug('Creating new socket') self._socket.set_interface(self._esp) - self._sock = self._socket.socket() else: raise TypeError('ESP32SPI interface required!') self._sock.settimeout(10) @@ -321,6 +322,7 @@ def ping(self): #print('SZ: ', sz) #assert sz == 0 + # pylint: disable=too-many-branches, too-many-statements def publish(self, topic, msg, retain=False, qos=0): """Publishes a message to a topic provided. :param str topic: Unique topic identifier. @@ -451,7 +453,7 @@ def subscribe(self, topic, qos=0): raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topics.append((t, q)) # Assemble packet - packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) packet_length += sum(len(topic) for topic, qos in topics) packet_length_byte = packet_length.to_bytes(1, 'big') self._pid += 11 @@ -464,8 +466,8 @@ def subscribe(self, topic, qos=0): qos_byte = q.to_bytes(1, 'big') packet += topic_size + t + qos_byte if self._logger is not None: - for topic, qos in topics: - self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(topic, qos)) + for t, q in topics: + self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(t, q)) self._sock.write(packet) while 1: op = self.wait_for_msg() @@ -510,7 +512,8 @@ def unsubscribe(self, topic): raise MMQTTException('Invalid MQTT Topic: %s'%t) topics.append((t)) # Assemble packet length first - packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + sum(len(topic) for topic in topics) + packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) + packet_length += sum(len(topic) for topic in topics) packet_length_byte = packet_length.to_bytes(1, 'big') # packet identifier self._pid += 11 @@ -518,9 +521,9 @@ def unsubscribe(self, topic): # packet with variable and fixed headers packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes # attach topics to the packet - for topic in topics: - topic_size = len(topic).to_bytes(2, 'big') - packet += topic_size + topic + for t in topics: + topic_size = len(t).to_bytes(2, 'big') + packet += topic_size + t # write the packet self._sock.write(packet) if self._logger is not None: @@ -549,7 +552,7 @@ def mqtt_msg(self, msg_size): :param int msg_size: Maximum MQTT payload size. """ if msg_size < MQTT_MSG_MAX_SZ: - self.__msg_size_lim = msg_size + self._msg_size_lim = msg_size def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. From 6413e9f7538f1f604cbe7ee649f3fd7687300010 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:32:59 -0400 Subject: [PATCH 092/148] fix ping() method, pingreq is TWO bytes not one --- adafruit_minimqtt.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2324f7c..68dea60 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -59,8 +59,8 @@ TLS_MODE = const(2) # MQTT Commands -MQTT_PING_REQ = b'\xc0' -MQTT_PINGRESP = b'\xd0' +MQTT_PINGREQ = b'\xc0\0' +MQTT_PINGRESP = const(0xd0) MQTT_SUB = b'\x82' MQTT_UNSUB = b'\xA2' MQTT_PUB = bytearray(b'\x30\0') @@ -94,7 +94,7 @@ class MQTT: Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, no-member, invalid-name + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable def __init__(self, socket, server_address, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface @@ -106,7 +106,6 @@ def __init__(self, socket, server_address, port=None, username=None, raise MMQTTException('Invalid ESP32SPI object provided.') else: raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object.') - self._sock = self._socket.socket() # port/ssl if is_ssl: self.port = MQTT_TCP_PORT @@ -123,6 +122,7 @@ def __init__(self, socket, server_address, port=None, username=None, self._client_id = client_id else: # assign a unique client_id + # pylint: disable=no-member self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) # generated client_id's enforce spec.'s length rules @@ -225,6 +225,7 @@ def connect(self, clean_session=True): if self._logger is not None: self._logger.debug('Creating new socket') self._socket.set_interface(self._esp) + self._sock = self._socket.socket() else: raise TypeError('ESP32SPI interface required!') self._sock.settimeout(10) @@ -313,14 +314,14 @@ def ping(self): self.is_connected() if self._logger is not None: self._logger.debug('Sending PINGREQ') - self._sock.write(MQTT_PING_REQ) - res = self._sock.read(1) + self._sock.write(MQTT_PINGREQ) if self._logger is not None: self._logger.debug('Checking PINGRESP') - #sz = self._sock.read(1)[0] - print('resp: ', res) - #print('SZ: ', sz) - #assert sz == 0 + ping_resp = self._sock.read(2) + if ping_resp[0] == MQTT_PINGRESP: + return + else: + raise MMQTTException('PINGRESP not returned from server.') # pylint: disable=too-many-branches, too-many-statements def publish(self, topic, msg, retain=False, qos=0): @@ -552,7 +553,7 @@ def mqtt_msg(self, msg_size): :param int msg_size: Maximum MQTT payload size. """ if msg_size < MQTT_MSG_MAX_SZ: - self._msg_size_lim = msg_size + self.__msg_size_lim = msg_size def wait_for_msg(self, timeout=0.1): """Waits for and processes network events. Returns if successful. From e914e28c5c116b4ca889c89109736402c2d8e870 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:46:01 -0400 Subject: [PATCH 093/148] add working AMQTT library example --- examples/minimqtt_adafruitio.py | 71 ++++++++++++++------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/examples/minimqtt_adafruitio.py b/examples/minimqtt_adafruitio.py index df00269..b176eaa 100755 --- a/examples/minimqtt_adafruitio.py +++ b/examples/minimqtt_adafruitio.py @@ -15,40 +15,13 @@ print("WiFi secrets are kept in secrets.py, please add them there!") raise -try: - esp32_cs = DigitalInOut(board.ESP_CS) - esp32_ready = DigitalInOut(board.ESP_BUSY) - esp32_reset = DigitalInOut(board.ESP_RESET) -except: - esp32_cs = DigitalInOut(board.D9) - esp32_ready = DigitalInOut(board.D10) - esp32_reset = DigitalInOut(board.D5) - +# Initialize an ESP32SPI network interface +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -"""Use below for Most Boards""" -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards -"""Uncomment below for ItsyBitsy M4""" -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) -"""Uncomment below for an externally defined RGB LED""" -# import adafruit_rgbled -# from adafruit_esp32spi import PWMOut -# RED_LED = PWMOut.PWMOut(esp, 26) -# GREEN_LED = PWMOut.PWMOut(esp, 27) -# BLUE_LED = PWMOut.PWMOut(esp, 25) -# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) - -# Setup a feed called `photocell` for publishing. -AIO_Publish_Feed = secrets['aio_user']+'feeds/photocell' - -# Setup a feed called `on_off_button` for subscribing to changes. -AIO_Subscribe_Feed = secrets['aio_user']+'feeds/on_off_button' - -# Set up a MiniMQTT Client -mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp, log = True) - print("Connecting to %s..."%secrets['ssid']) while not esp.is_connected: try: @@ -59,19 +32,37 @@ print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("IP: ", esp.pretty_ip(esp.ip_address)) +def on_message(client, topic, message): + """This method is called whenever a new message is received + from the server. + """ + print('Value on topic {0}: {1}'.format(topic, message)) + +# Setup a feed named `photocell` for publishing. +AIO_Publish_Feed = secrets['aio_user']+'/'+'feeds/photocell' + +# Setup a feed named `onoffbutton` for subscribing to changes. +AIO_Subscribe_Feed = secrets['aio_user']+'/'+'feeds/onoffbutton' + +# Set up a MiniMQTT Client +mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp, log = True) + +# Attach on_message method to the MQTT Client +mqtt_client.on_message = on_message + +# Initialize the MQTT Client mqtt_client.connect() -mqtt_client.subscribe(on_off_button) +# Subscribe the client to topic AIO_SUBSCRIBE_FEED +mqtt_client.subscribe(AIO_Subscribe_Feed) photocell_val = 0 while True: - # ping the server to keep the mqtt connection alive - mqtt_client.ping() - - subscription_data = mqtt_client.wait_for_msg() - - if subscription_data: - print('Got: {0}'.format(subscription_data)) + # Poll the message queue and ping the server + mqtt_client.wait_for_msg() print('Sending photocell value: %d'%photocell_val) - mqtt.publish(AIO_Publish_Feed, photocell_value) + mqtt_client.publish(AIO_Publish_Feed, photocell_val) + photocell_val += 1 + time.sleep(0.5) From 88e000d6ebe3babbc4ec8721f22cc554a7d9804c Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:53:12 -0400 Subject: [PATCH 094/148] fix SSL port instanciation --- adafruit_minimqtt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 68dea60..c96233c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -108,9 +108,9 @@ def __init__(self, socket, server_address, port=None, username=None, raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object.') # port/ssl if is_ssl: - self.port = MQTT_TCP_PORT - else: self.port = MQTT_TLS_PORT + else: + self.port = MQTT_TCP_PORT if port is not None: self.port = port # session identifiers @@ -475,7 +475,6 @@ def subscribe(self, topic, qos=0): if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] - print('a') if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') for t in topics: From 508f03527c68d59872dcbebac44a2e6d2f2fd137 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 16:55:54 -0400 Subject: [PATCH 095/148] check if a logger exists before calling it within publish --- adafruit_minimqtt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c96233c..1c34bb8 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -376,8 +376,9 @@ def publish(self, topic, msg, retain=False, qos=0): sz >>= 7 i += 1 pkt[i] = sz - self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\ - \nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) + if self._logger is not None: + self._logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\ + \nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain)) self._sock.write(pkt) self._send_str(topic) if qos == 0: @@ -390,7 +391,8 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(pkt) if self.on_publish is not None: self.on_publish(self, self._user_data, pid) - self._logger.debug('Sending PUBACK') + if self._logger is not None: + self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: while 1: From e332074818cae51aa6d397743296fddc5f9aea5f Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 17:05:56 -0400 Subject: [PATCH 096/148] restructure test framework --- examples/test_mqtt_client.py | 120 ++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index 1335915..5752ab8 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -10,24 +10,8 @@ import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket -# MiniMQTT -from adafruit_minimqtt import MQTT - -# Get wifi details and more from a secrets.py file -try: - from secrets import secrets -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") - raise - -esp32_cs = DigitalInOut(board.ESP_CS) -esp32_ready = DigitalInOut(board.ESP_BUSY) -esp32_reset = DigitalInOut(board.ESP_RESET) - -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +from adafruit_minimqtt import MQTT """ Generic Unittest-like Assertions @@ -69,27 +53,17 @@ def assertEqual(val_1, val_2): # MQTT Client Tests def test_mqtt_create_client_esp32spi(): - """Creates an insecure MQTT client using an ESP32SPI socket connection.""" - mqtt_client = MQTT(esp, socket, secrets['aio_url'], - username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl=False) - mqtt_client.logging('DEBUG') + """Creates an INSECURE MQTT client using an ESP32SPI socket connection.""" + # TODO: reflect test_mqtts_connect_disconnect_esp32spi + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp, is_ssl=False) assertEqual(mqtt_client.port, 1883) -def test_mqtts_create_client_esp32spi(): - """Creates a secure MQTT client using an ESP32SPI socket connection.""" - mqtt_client = MQTT(esp, socket, secrets['aio_url'], - username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl=True) - mqtt_client.logging('DEBUG') - assertEqual(mqtt_client.port, 8883) - def test_mqtts_connect_disconnect_esp32spi(): """Creates a MQTTS client, connects, and attempts a disconnection.""" - mqtt_client = MQTT(esp, socket, secrets['aio_url'], - username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl=True) - mqtt_client.logging('DEBUG') + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp) + assertEqual(mqtt_client.port, 8883) mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) mqtt_client.disconnect() @@ -100,10 +74,8 @@ def test_sub_pub(): received from broker matches data sent by client""" MSG_TOPIC = 'brubell/feeds/testfeed' MSG_DATA = 42 - mqtt_client = MQTT(esp, socket, secrets['aio_url'], - username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl=True) - mqtt_client.logging('DEBUG') + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp) # Callback responses callback_msgs = [] def on_message(client, topic, msg): @@ -117,24 +89,84 @@ def on_message(client, topic, msg): print('listening...') while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): mqtt_client.wait_for_msg() - # check message+topic has been RX'd by the client's callback + # check message and topic has been RX'd by the client's callback assertEqual(callback_msgs[0][0], MSG_TOPIC) assertEqual(callback_msgs[0][1], str(MSG_DATA)) mqtt_client.disconnect() +def test_sub_pub_multiple(): + """Subscribe to multiple topics, publish to one, unsubscribe from both. + """ + MSG_TOPIC_1 = 'brubell/feeds/testfeed1' + MSG_TOPIC_2 = 'brubell/feeds/testfeed2' + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp) + # Callback responses + callback_msgs = [] + def on_message(client, topic, msg): + callback_msgs.append([topic, msg]) + mqtt_client.on_message = on_message + mqtt_client.connect() + assertEqual(mqtt_client._is_connected, True) + # subscribe to two topics with different QoS levels + mqtt_client.subscribe([(MSG_TOPIC_1, 1), (MSG_TOPIC_2, 0)]) + mqtt_client.publish(MSG_TOPIC_2, 42) + start_timer = time.monotonic() + print('listening...') + while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): + mqtt_client.wait_for_msg() + # check message and topic has been RX'd by the client's callback + assertEqual(callback_msgs[0][0], MSG_TOPIC_2) + assertEqual(callback_msgs[0][1], str(42)) + mqtt_client.unsubscribe([MSG_TOPIC_1, MSG_TOPIC_2]) + mqtt_client.disconnect() + +def test_publish_errors(): + """Testing invalid publish() calls, expecting specific MMQTExceptions. + """ + MSG_TOPIC = 'brubell/feeds/testfeed' + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp) + # Callback responses + callback_msgs = [] + def on_message(client, topic, msg): + callback_msgs.append([topic, msg]) + mqtt_client.on_message = on_message + mqtt_client.connect() + assertEqual(mqtt_client._is_connected, True) + mqtt_client.subscribe(MSG_TOPIC) + try: + mqtt_client.publish(MSG_TOPIC, None) + except MMQTException as exception: + print(MMQTException) -# Timeout between tests, in seconds. This value depends on the timeout of your MQTT broker. +# Timeout between tests, in seconds. This value depends on the MQTT broker. TEST_TIMEOUT = 1 # Connection/Client Tests -conn_tests = [test_mqtt_create_client_esp32spi, test_mqtts_create_client_esp32spi, - test_mqtts_connect_disconnect_esp32spi] +conn_tests = [test_mqtt_create_client_esp32spi, test_mqtts_connect_disconnect_esp32spi] # PUB/SUB API Tests -pub_sub_tests = [test_sub_pub] +pub_sub_tests = [test_sub_pub, test_sub_pub_multiple, test_publish_errors] # The test routine runs the following test(s): -tests = [test_sub_pub] +tests = pub_sub_tests + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# Define an ESP32SPI network interface +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards + # Establish ESP32SPI connection print("Connecting to AP...") From 21b74deb3b0d676a42f6feeaeec54caea2944e65 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 17:30:32 -0400 Subject: [PATCH 097/148] add wildcard publish test --- examples/test_mqtt_client.py | 84 ++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index 5752ab8..1fcd6a3 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -1,5 +1,5 @@ """ -CircuitPython_MiniMQTT Module Tester +CircuitPython_MiniMQTT Methods Test Suite by Brent Rubell for Adafruit Industries, 2019 """ @@ -11,11 +11,27 @@ from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_minimqtt import MQTT +from adafruit_minimqtt import MQTT, MMQTTException """ -Generic Unittest-like Assertions +Generic cpython3 unittest-like assertions """ + +class AssertRaisesContext: + def __init__(exc): + expected = exc + + def __enter__(self): + return self + + def __exit__(exc_type, exc_value, tb): + if exc_type is None: + assert False, "%r not raised" % expected + if issubclass(exc_type, expected): + return True + return False + + #pylint: disable=keyword-arg-before-vararg def assertAlmostEqual(x, y, places=None, msg=''): """Raises an AssertionError if two float values are not equal. @@ -30,17 +46,6 @@ def assertAlmostEqual(x, y, places=None, msg=''): msg = '%r != %r within %r places' % (x, y, places) assert False, msg -def assertRaises(exc,func=None, *args, **kwargs): - """Raises based on exception context. - (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py)""" - try: - func(*args, **kwargs) - assert False, "%r not raised" % exc - except Exception as e: - if isinstance(e, exc): - return - raise - def assertIsNone(x): """Raises an AssertionError if x is None.""" if x is None: @@ -51,13 +56,27 @@ def assertEqual(val_1, val_2): if val_1 != val_2: raise AssertionError('Values are not equal:', val_1, val_2) +def assertRaises(exc, func=None, *args, **kwargs): + if func is None: + return AssertRaisesContext(exc) + + try: + func(*args, **kwargs) + assert False, "%r not raised" % exc + except Exception as e: + if isinstance(e, exc): + return + raise + # MQTT Client Tests -def test_mqtt_create_client_esp32spi(): - """Creates an INSECURE MQTT client using an ESP32SPI socket connection.""" - # TODO: reflect test_mqtts_connect_disconnect_esp32spi +def test_mqtt_connect_disconnect_esp32spi(): + """Creates an INSECURE MQTT client, connects, and attempts a disconnection.""" mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], esp = esp, is_ssl=False) assertEqual(mqtt_client.port, 1883) + assertEqual(mqtt_client._is_connected, True) + mqtt_client.disconnect() + assertEqual(mqtt_client._is_connected, False) def test_mqtts_connect_disconnect_esp32spi(): """Creates a MQTTS client, connects, and attempts a disconnection.""" @@ -135,22 +154,41 @@ def on_message(client, topic, msg): mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) mqtt_client.subscribe(MSG_TOPIC) + # Publishing message of NoneType + # TODO: check over assertRaises callback in Micropython try: mqtt_client.publish(MSG_TOPIC, None) - except MMQTException as exception: - print(MMQTException) + except MMQTTException as e: + print('e: ', e) + pass + # Publishing to a wildcard topic + try: + mqtt_client.publish('brubell/feeds/+', 42) + except MMQTTException as e: + print('e: ', e) + pass + try: + mqtt_client.publish('brubell/feeds/*', 42) + except MMQTTException as e: + print('e: ', e) + pass + # Publishing message with length greater than MQTT_MSG_MAX_SZ + + #try: + #mqtt_client.publish(MSG_TOPIC, ) + # Timeout between tests, in seconds. This value depends on the MQTT broker. TEST_TIMEOUT = 1 # Connection/Client Tests -conn_tests = [test_mqtt_create_client_esp32spi, test_mqtts_connect_disconnect_esp32spi] +conn_tests = [test_mqtt_connect_disconnect_esp32spi, test_mqtts_connect_disconnect_esp32spi] -# PUB/SUB API Tests +# Publish/Subscribe tests pub_sub_tests = [test_sub_pub, test_sub_pub_multiple, test_publish_errors] -# The test routine runs the following test(s): -tests = pub_sub_tests +# The test routine will run the following test(s): +tests = [test_publish_errors] # Get wifi details and more from a secrets.py file try: From f7c6d5565dfbf45dd007ae61a790e5d42281ff92 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 17:43:07 -0400 Subject: [PATCH 098/148] enforce topic name and filter limit on unsubscribe --- adafruit_minimqtt.py | 18 +++++++++--------- examples/test_mqtt_client.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 1c34bb8..64de17f 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -52,7 +52,7 @@ # Client-specific variables MQTT_MSG_MAX_SZ = const(268435455) MQTT_MSG_SZ_LIM = const(10000000) -MQTT_TOPIC_SZ_LIMIT = const(65536) +MQTT_TOPIC_LENGTH_LIMIT = const(65535) MQTT_TCP_PORT = const(1883) MQTT_TLS_PORT = const(8883) TCP_MODE = const(0) @@ -252,7 +252,7 @@ def connect(self, clean_session=True): sz += 2 + len(self._user) + 2 + len(self._pass) msg[6] |= 0xC0 if self._keep_alive: - assert self._keep_alive < MQTT_TOPIC_SZ_LIMIT + assert self._keep_alive < MQTT_TOPIC_LENGTH_LIMIT msg[7] |= self._keep_alive >> 8 msg[8] |= self._keep_alive & 0x00FF if self._lw_topic: @@ -347,8 +347,8 @@ def publish(self, topic, msg, retain=False, qos=0): """ self.is_connected() - if topic is None or not topic: - raise MMQTTException("Invalid MQTT Topic, must have length > 0.") + if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): + raise MMQTTException("Invalid MQTT Topic length.") if '+' in topic or '#' in topic: raise MMQTTException('Topic can not contain wildcards.') # check msg/qos kwargs @@ -442,7 +442,7 @@ def subscribe(self, topic, qos=0): if isinstance(topic, tuple): topic, qos = topic if isinstance(topic, str): - if topic is None or not topic or len(topic.encode('utf-8')) > 65536: + if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): raise MMQTTException("Invalid MQTT Topic, must have length > 0.") if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') @@ -496,12 +496,12 @@ def unsubscribe(self, topic): Example of unsubscribing from multiple topics. .. code-block:: python - mqtt_client.unsubscribe('topics/ledState', 'topics/servoAngle') + mqtt_client.unsubscribe(['topics/ledState', 'topics/servoAngle']) """ topics = None if isinstance(topic, str): - if topic is None or not topic or len(topic.encode('utf-8')) > 65536: + if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): raise MMQTTException("Invalid MQTT topic.") print('TOPICS:', self._subscribed_topics) if topic not in self._subscribed_topics: @@ -510,7 +510,7 @@ def unsubscribe(self, topic): if isinstance(topic, list): topics = [] for t in topic: - if t is None or not t or len(t.encode('utf-8')) > 65536: + if t is None or not t or len(t.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): raise MMQTTException('Invalid MQTT Topic: %s'%t) topics.append((t)) # Assemble packet length first @@ -546,7 +546,7 @@ def unsubscribe(self, topic): @property def mqtt_msg(self): """Returns maximum MQTT payload and topic size.""" - return self._msg_size_lim, MQTT_TOPIC_SZ_LIMIT + return self._msg_size_lim, MQTT_TOPIC_LENGTH_LIMIT @mqtt_msg.setter def mqtt_msg(self, msg_size): diff --git a/examples/test_mqtt_client.py b/examples/test_mqtt_client.py index 1fcd6a3..159fb71 100755 --- a/examples/test_mqtt_client.py +++ b/examples/test_mqtt_client.py @@ -156,6 +156,7 @@ def on_message(client, topic, msg): mqtt_client.subscribe(MSG_TOPIC) # Publishing message of NoneType # TODO: check over assertRaises callback in Micropython + # TODO: list of incorrect topics and/or data, iterate thru 'em try: mqtt_client.publish(MSG_TOPIC, None) except MMQTTException as e: @@ -172,10 +173,29 @@ def on_message(client, topic, msg): except MMQTTException as e: print('e: ', e) pass - # Publishing message with length greater than MQTT_MSG_MAX_SZ - - #try: - #mqtt_client.publish(MSG_TOPIC, ) + # TODO: Publishing message with length greater than MQTT_MSG_MAX_SZ + # Publishing with an invalid QoS level + try: + mqtt_client.publish(MSG_TOPIC, 42, qos=3) + except MMQTTException as e: + print ('e: ', e) + pass + + def test_topic_wildcards(): + """Tests if topic wildcards work as specified in MQTT[4.7.1]. + """ + MSG_TOPIC = 'brubell/feeds/testfeed' + mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], + esp = esp) + # Callback responses + callback_msgs = [] + def on_message(client, topic, msg): + callback_msgs.append([topic, msg]) + mqtt_client.on_message = on_message + mqtt_client.connect() + assertEqual(mqtt_client._is_connected, True) + mqtt_client.subscribe('brubell/feeds/tests/#') + mqtt_client.publish('brubell/feeds/tests.testone') # Timeout between tests, in seconds. This value depends on the MQTT broker. @@ -187,8 +207,11 @@ def on_message(client, topic, msg): # Publish/Subscribe tests pub_sub_tests = [test_sub_pub, test_sub_pub_multiple, test_publish_errors] +# Wildcard and subscription filter errors +test_topic_wildcards = [test_topic_wildcards] + # The test routine will run the following test(s): -tests = [test_publish_errors] +tests = test_topic_wildcards # Get wifi details and more from a secrets.py file try: From c817477d0dea09e6673b543b7ee1ece255456f9e Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 17:53:57 -0400 Subject: [PATCH 099/148] define a method to allow post-initialization attachment of a loger object, change logger to a more verbose method name --- adafruit_minimqtt.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 64de17f..807146e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -613,7 +613,15 @@ def _send_str(self, string): self._sock.write(string) # Logging - def logging(self, log_level): + + def attach_logger(self, logger_name='log'): + """Initializes and attaches a logger to the MQTTClient. + :param str logger_name: Name of the logger instance + """ + self._logger = logging.getLogger(logger_name) + self._logger.setLevel(logging.INFO) + + def set_logger_level(self, log_level): """Sets the level of the logger, if defined during init. :param string log_level: Level of logging to output to the REPL. Accepted levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. From 7181ed47e8d20e452ccf9cac93f46cc6dc9cdc93 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 18:16:16 -0400 Subject: [PATCH 100/148] add a valid check for UNSUBACK, wasnt explicitly checking the fixed header contents prior. Need to refactor back into multiple unsubscribe calls again --- adafruit_minimqtt.py | 85 ++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 58 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 807146e..c38eaa0 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -62,7 +62,9 @@ MQTT_PINGREQ = b'\xc0\0' MQTT_PINGRESP = const(0xd0) MQTT_SUB = b'\x82' -MQTT_UNSUB = b'\xA2' +#MQTT_UNSUB = b'\xA2' +MQTT_UNSUB = bytearray(b'\xA2\0\0\0') +MQTT_UNSUBACK = const(0xb0) MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable CONNECT header [MQTT 3.1.2] @@ -318,9 +320,7 @@ def ping(self): if self._logger is not None: self._logger.debug('Checking PINGRESP') ping_resp = self._sock.read(2) - if ping_resp[0] == MQTT_PINGRESP: - return - else: + if ping_resp[0] != MQTT_PINGRESP or ping_resp[1] != const(0x00): raise MMQTTException('PINGRESP not returned from server.') # pylint: disable=too-many-branches, too-many-statements @@ -486,62 +486,31 @@ def subscribe(self, topic, qos=0): return def unsubscribe(self, topic): - """Unsubscribes the client from subscribed mqtt topic(s). - :param str topic: Topic identifier. - :param list topic: List of topic identifier strings. - - Example of unsubscribing from a topic. - .. code-block:: python - mqtt_client.unsubscribe('topics/ledState') - - Example of unsubscribing from multiple topics. - .. code-block:: python - mqtt_client.unsubscribe(['topics/ledState', 'topics/servoAngle']) - + """Unsubscribes from a MQTT topic. + :param str topic: Unique MQTT topic identifier. + TODO: REFACTOR THIS BACK INTO MULTIPLE UNSUBSCRIBE COMMANDS! """ - topics = None - if isinstance(topic, str): - if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): - raise MMQTTException("Invalid MQTT topic.") - print('TOPICS:', self._subscribed_topics) - if topic not in self._subscribed_topics: - raise MMQTTException('Topic must be subscribed to before unsubscribing.') - topics = [(topic)] - if isinstance(topic, list): - topics = [] - for t in topic: - if t is None or not t or len(t.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): - raise MMQTTException('Invalid MQTT Topic: %s'%t) - topics.append((t)) - # Assemble packet length first - packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) - packet_length += sum(len(topic) for topic in topics) - packet_length_byte = packet_length.to_bytes(1, 'big') - # packet identifier - self._pid += 11 - packet_id_bytes = self._pid.to_bytes(2, 'big') - # packet with variable and fixed headers - packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes - # attach topics to the packet - for t in topics: - topic_size = len(t).to_bytes(2, 'big') - packet += topic_size + t - # write the packet - self._sock.write(packet) + if topic is None or len(topic) == 0: + raise MMQTTException("Invalid MQTT topic - must have a length > 0.") + if topic not in self._subscribed_topics: + raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + pkt = MQTT_UNSUB + self._pid+=1 + # variable header length + remaining_length = 2 + remaining_length += 2 + len(topic) + struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) + self._logger.debug('Unsubscribing from %s'%topic) + self._sock.write(pkt) + self._send_str(topic) if self._logger is not None: - for t in topics: - self._logger.debug('SUBSCRIBING to topic {0}'.format(t)) - while 1: - try: - self.wait_for_msg() - # remove topic from subscription list - for t in topics: - self._subscribed_topics.remove(t) - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, self._user_data) - return - except RuntimeError: - raise MMQTTException('Could not unsubscribe from feed.') + self._logger.debug('Checking UNSUBACK') + unsuback = self._sock.read(2) + if unsuback[0] != const(MQTT_UNSUBACK) or unsuback[1] != const(0x02): + raise MMQTTException('Did not receive SUBACK') + self._subscribed_topics.remove(topic) + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, self._user_data) @property def mqtt_msg(self): From a4027f25a88b05e71fae3f3eec005fdd3e20e53b Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 18:29:29 -0400 Subject: [PATCH 101/148] add a generic topic check private method --- adafruit_minimqtt.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c38eaa0..b244181 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -347,8 +347,7 @@ def publish(self, topic, msg, retain=False, qos=0): """ self.is_connected() - if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): - raise MMQTTException("Invalid MQTT Topic length.") + self._check_topic(topic) if '+' in topic or '#' in topic: raise MMQTTException('Topic can not contain wildcards.') # check msg/qos kwargs @@ -441,9 +440,9 @@ def subscribe(self, topic, qos=0): topics = None if isinstance(topic, tuple): topic, qos = topic + self._check_topic(topic) if isinstance(topic, str): - if topic is None or not topic or len(topic.encode('utf-8')) > const(MQTT_TOPIC_LENGTH_LIMIT): - raise MMQTTException("Invalid MQTT Topic, must have length > 0.") + self._check_topic(topic) if qos < 0 or qos > 2: raise MMQTTException('QoS level must be between 1 and 2.') topics = [(topic, qos)] @@ -485,15 +484,30 @@ def subscribe(self, topic, qos=0): self._subscribed_topics.append(t[0]) return + def _check_topic(self, topic): + """Checks if topic provided is a valid mqtt topic. + :param str topic: Topic identifier + """ + print('TOPIC: ', topic) + if topic is None or not len(topic) or len(topic.encode('utf-8') > MQTT_TOPIC_LENGTH_LIMIT): + raise MMQTTException('Invalid MQTT topic') + def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. TODO: REFACTOR THIS BACK INTO MULTIPLE UNSUBSCRIBE COMMANDS! """ - if topic is None or len(topic) == 0: - raise MMQTTException("Invalid MQTT topic - must have a length > 0.") + topics = None + self._check_topic(topic) if topic not in self._subscribed_topics: raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + if isinstance(topic, str): + topics = [(topic)] + if isinstance(topic, list): + topics = [] + for t in topics: + self._check_topic(t) + pkt = MQTT_UNSUB self._pid+=1 # variable header length From 70c9e8e6fa998469c884f7d4c5da255729924061 Mon Sep 17 00:00:00 2001 From: brentru Date: Mon, 8 Jul 2019 18:37:00 -0400 Subject: [PATCH 102/148] strict topic check added --- adafruit_minimqtt.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b244181..4effb1a 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -451,6 +451,7 @@ def subscribe(self, topic, qos=0): for t, q in topic: if q < 0 or q > 2: raise MMQTTException('QoS level must be between 1 and 2.') + # TODO: Check topic length in here... if t is None or not t or len(t.encode('utf-8')) > 65536: raise MMQTTException("Invalid MQTT Topic, must have length > 0.") topics.append((t, q)) @@ -488,9 +489,12 @@ def _check_topic(self, topic): """Checks if topic provided is a valid mqtt topic. :param str topic: Topic identifier """ - print('TOPIC: ', topic) - if topic is None or not len(topic) or len(topic.encode('utf-8') > MQTT_TOPIC_LENGTH_LIMIT): - raise MMQTTException('Invalid MQTT topic') + if topic is None: + raise MMQTTException('Topic may not be Nonetype') + elif not len(topic): + raise MMQTTException('Topic may not be empty.') + elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: + raise MMQTTException('Topic length is too large.') def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. From 7f17ff57b6855c2873eaa88b9d890074c9fd8a7f Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 10:27:59 -0400 Subject: [PATCH 103/148] strict length check on password --- adafruit_minimqtt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4effb1a..1acb5c6 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -117,6 +117,9 @@ def __init__(self, socket, server_address, port=None, username=None, self.port = port # session identifiers self._user = username + # [MQTT-3.1.3.5] + if len(password.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: + raise MMQTTException('Password length is too large.') self._pass = password if client_id is not None: # user-defined client_id MAY allow client_id's > 23 bytes or @@ -491,8 +494,10 @@ def _check_topic(self, topic): """ if topic is None: raise MMQTTException('Topic may not be Nonetype') + # [MQTT-4.7.3-1] elif not len(topic): raise MMQTTException('Topic may not be empty.') + # [MQTT-4.7.3-3] elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: raise MMQTTException('Topic length is too large.') From c02703975e613d350511ab3eb8ae4734e9c00162 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 11:17:27 -0400 Subject: [PATCH 104/148] rewrite of unsubscribe with strict return code handling to conform with MQTT spec --- adafruit_minimqtt.py | 87 ++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 1acb5c6..23ace6d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -62,9 +62,7 @@ MQTT_PINGREQ = b'\xc0\0' MQTT_PINGRESP = const(0xd0) MQTT_SUB = b'\x82' -#MQTT_UNSUB = b'\xA2' -MQTT_UNSUB = bytearray(b'\xA2\0\0\0') -MQTT_UNSUBACK = const(0xb0) +MQTT_UNSUB = b'\xA2' MQTT_PUB = bytearray(b'\x30\0') MQTT_CON = bytearray(b'\x10\0\0') # Variable CONNECT header [MQTT 3.1.2] @@ -454,9 +452,7 @@ def subscribe(self, topic, qos=0): for t, q in topic: if q < 0 or q > 2: raise MMQTTException('QoS level must be between 1 and 2.') - # TODO: Check topic length in here... - if t is None or not t or len(t.encode('utf-8')) > 65536: - raise MMQTTException("Invalid MQTT Topic, must have length > 0.") + self._check_topic(t) topics.append((t, q)) # Assemble packet packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) @@ -488,52 +484,63 @@ def subscribe(self, topic, qos=0): self._subscribed_topics.append(t[0]) return - def _check_topic(self, topic): - """Checks if topic provided is a valid mqtt topic. - :param str topic: Topic identifier - """ - if topic is None: - raise MMQTTException('Topic may not be Nonetype') - # [MQTT-4.7.3-1] - elif not len(topic): - raise MMQTTException('Topic may not be empty.') - # [MQTT-4.7.3-3] - elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: - raise MMQTTException('Topic length is too large.') - def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. TODO: REFACTOR THIS BACK INTO MULTIPLE UNSUBSCRIBE COMMANDS! """ topics = None - self._check_topic(topic) - if topic not in self._subscribed_topics: - raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') if isinstance(topic, str): + self._check_topic(topic) topics = [(topic)] if isinstance(topic, list): topics = [] - for t in topics: + for t in topic: self._check_topic(t) - - pkt = MQTT_UNSUB - self._pid+=1 - # variable header length - remaining_length = 2 - remaining_length += 2 + len(topic) - struct.pack_into("!BH", pkt, 1, remaining_length, self._pid) - self._logger.debug('Unsubscribing from %s'%topic) - self._sock.write(pkt) - self._send_str(topic) + topics.append((t)) + for t in topics: + if t not in self._subscribed_topics: + raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + # Assemble packet + packet_length = 2 + (2 * len(topics)) + packet_length += sum(len(topic) for topic in topics) + packet_length_byte = packet_length.to_bytes(1, 'big') + self._pid+=11 + packet_id_bytes = self._pid.to_bytes(2, 'big') + packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes + for t in topics: + topic_size = len(t).to_bytes(2, 'big') + packet += topic_size + t if self._logger is not None: - self._logger.debug('Checking UNSUBACK') - unsuback = self._sock.read(2) - if unsuback[0] != const(MQTT_UNSUBACK) or unsuback[1] != const(0x02): - raise MMQTTException('Did not receive SUBACK') - self._subscribed_topics.remove(topic) - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, self._user_data) + for t in topics: + self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) + self._sock.write(packet) + while 1: + op = self.wait_for_msg() + if op == const(0x01): + return_code = self._sock.read(4) + # [MQTT-3.11.1] + assert return_code[1] == const(0x02) + # [MQTT-3.32] + assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] + for t in topics: + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, t, self._pid) + self._subscribed_topics.remove(t) + return + + def _check_topic(self, topic): + """Checks if topic provided is a valid mqtt topic. + :param str topic: Topic identifier + """ + if topic is None: + raise MMQTTException('Topic may not be Nonetype') + # [MQTT-4.7.3-1] + elif not len(topic): + raise MMQTTException('Topic may not be empty.') + # [MQTT-4.7.3-3] + elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: + raise MMQTTException('Topic length is too large.') @property def mqtt_msg(self): From 47f2ce91f59b9927b7ce15ea21f0e22f43a2ea6a Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 11:22:51 -0400 Subject: [PATCH 105/148] add to docstring... --- adafruit_minimqtt.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 23ace6d..28a6c4e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -487,7 +487,16 @@ def subscribe(self, topic, qos=0): def unsubscribe(self, topic): """Unsubscribes from a MQTT topic. :param str topic: Unique MQTT topic identifier. - TODO: REFACTOR THIS BACK INTO MULTIPLE UNSUBSCRIBE COMMANDS! + :param list topic: List of tuples containing topic identifier strings. + + Example of unsubscribing from a topic string. + .. code-block:: python + mqtt_client.unsubscribe('topics/ledState') + + Example of unsubscribing from multiple topics. + .. code-block:: python + mqtt_client.unsubscribe([('topics/ledState'), ('topics/servoAngle')]) + """ topics = None if isinstance(topic, str): @@ -515,6 +524,7 @@ def unsubscribe(self, topic): for t in topics: self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) self._sock.write(packet) + # Check UNSUBACK while 1: op = self.wait_for_msg() if op == const(0x01): From 3461736cc62cded0e19481ca28dc6a8e09b291e3 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 13:01:16 -0400 Subject: [PATCH 106/148] add _check_qos --- adafruit_minimqtt.py | 88 ++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 28a6c4e..89638eb 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -350,7 +350,7 @@ def publish(self, topic, msg, retain=False, qos=0): self.is_connected() self._check_topic(topic) if '+' in topic or '#' in topic: - raise MMQTTException('Topic can not contain wildcards.') + raise MMQTTException('Publish topic can not contain wildcards.') # check msg/qos kwargs if msg is None: raise MMQTTException('Message can not be None.') @@ -362,8 +362,7 @@ def publish(self, topic, msg, retain=False, qos=0): raise MMQTTException('Invalid message data type.') if len(msg) > MQTT_MSG_MAX_SZ: raise MMQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ) - if qos < 0 or qos > 2: - raise MMQTTException("Invalid QoS level, must be between 0 and 2.") + self._check_qos(qos) pkt = MQTT_PUB pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) @@ -401,10 +400,10 @@ def publish(self, topic, msg, retain=False, qos=0): sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) - rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] - if self.on_publish is not None: - self.on_publish(self, self._user_data, rcv_pid) + rcv_pid = rcv_pid[0] << const(0x08) | rcv_pid[1] if pid == rcv_pid: + if self.on_publish is not None: + self.on_publish(self, self._user_data, rcv_pid) return elif qos == 2: assert 0 @@ -442,23 +441,22 @@ def subscribe(self, topic, qos=0): if isinstance(topic, tuple): topic, qos = topic self._check_topic(topic) + self._check_qos(qos) if isinstance(topic, str): self._check_topic(topic) - if qos < 0 or qos > 2: - raise MMQTTException('QoS level must be between 1 and 2.') + self._check_qos(qos) topics = [(topic, qos)] if isinstance(topic, list): topics = [] for t, q in topic: - if q < 0 or q > 2: - raise MMQTTException('QoS level must be between 1 and 2.') + self._check_qos(q) self._check_topic(t) topics.append((t, q)) # Assemble packet packet_length = 2 + (2 * len(topics)) + (1 * len(topics)) packet_length += sum(len(topic) for topic, qos in topics) packet_length_byte = packet_length.to_bytes(1, 'big') - self._pid += 11 + self._pid += 1 packet_id_bytes = self._pid.to_bytes(2, 'big') # Packet with variable and fixed headers packet = MQTT_SUB + packet_length_byte + packet_id_bytes @@ -514,7 +512,7 @@ def unsubscribe(self, topic): packet_length = 2 + (2 * len(topics)) packet_length += sum(len(topic) for topic in topics) packet_length_byte = packet_length.to_bytes(1, 'big') - self._pid+=11 + self._pid+=1 packet_id_bytes = self._pid.to_bytes(2, 'big') packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes for t in topics: @@ -539,34 +537,8 @@ def unsubscribe(self, topic): self._subscribed_topics.remove(t) return - def _check_topic(self, topic): - """Checks if topic provided is a valid mqtt topic. - :param str topic: Topic identifier - """ - if topic is None: - raise MMQTTException('Topic may not be Nonetype') - # [MQTT-4.7.3-1] - elif not len(topic): - raise MMQTTException('Topic may not be empty.') - # [MQTT-4.7.3-3] - elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: - raise MMQTTException('Topic length is too large.') - - @property - def mqtt_msg(self): - """Returns maximum MQTT payload and topic size.""" - return self._msg_size_lim, MQTT_TOPIC_LENGTH_LIMIT - - @mqtt_msg.setter - def mqtt_msg(self, msg_size): - """Sets the maximum MQTT message payload size. - :param int msg_size: Maximum MQTT payload size. - """ - if msg_size < MQTT_MSG_MAX_SZ: - self.__msg_size_lim = msg_size - def wait_for_msg(self, timeout=0.1): - """Waits for and processes network events. Returns if successful. + """Reads and processes network events. Returns network response if successful. :param float timeout: The time in seconds to wait for network before returning. Setting this to 0.0 will cause the socket to block until it reads. """ @@ -621,8 +593,44 @@ def _send_str(self, string): else: self._sock.write(string) - # Logging + def _check_topic(self, topic): + """Checks if topic provided is a valid mqtt topic. + :param str topic: Topic identifier + """ + if topic is None: + raise MMQTTException('Topic may not be Nonetype') + # [MQTT-4.7.3-1] + elif not len(topic): + raise MMQTTException('Topic may not be empty.') + # [MQTT-4.7.3-3] + elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: + raise MMQTTException('Topic length is too large.') + + def _check_qos(self, qos_level): + """Validates the quality of service level. + :param int qos_level: Desired QoS level. + """ + if isinstance(qos_level, int): + if qos_level < 0 or qos_level > 2: + raise MMQTTException('QoS must be between 1 and 2.') + else: + raise MMQTTException('QoS must be an integer.') + return + @property + def mqtt_msg(self): + """Returns maximum MQTT payload and topic size.""" + return self._msg_size_lim, MQTT_TOPIC_LENGTH_LIMIT + + @mqtt_msg.setter + def mqtt_msg(self, msg_size): + """Sets the maximum MQTT message payload size. + :param int msg_size: Maximum MQTT payload size. + """ + if msg_size < MQTT_MSG_MAX_SZ: + self.__msg_size_lim = msg_size + + # Logging def attach_logger(self, logger_name='log'): """Initializes and attaches a logger to the MQTTClient. :param str logger_name: Name of the logger instance From a6b387e8732b7b33cdc8e263bd62968fb1195f6a Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 13:47:05 -0400 Subject: [PATCH 107/148] handle unsuback correctly --- adafruit_minimqtt.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 89638eb..72a4f93 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -522,20 +522,16 @@ def unsubscribe(self, topic): for t in topics: self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) self._sock.write(packet) - # Check UNSUBACK - while 1: - op = self.wait_for_msg() - if op == const(0x01): - return_code = self._sock.read(4) - # [MQTT-3.11.1] - assert return_code[1] == const(0x02) - # [MQTT-3.32] - assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] - for t in topics: - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, t, self._pid) - self._subscribed_topics.remove(t) - return + return_code = self._sock.read(4) + print(return_code) + # [MQTT-3.11.1] + assert return_code[1] == const(0x02) + # [MQTT-3.32] + assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] + for t in topics: + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, t, self._pid) + self._subscribed_topics.remove(t) def wait_for_msg(self, timeout=0.1): """Reads and processes network events. Returns network response if successful. From 612ab3f549735a4c0ba91bee1abeacfc69a17f89 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 14:10:22 -0400 Subject: [PATCH 108/148] update hardware test to allow a settings python dict file for configuration --- adafruit_minimqtt.py | 2 +- examples/minimqtt_hardware_test/settings.py | 12 ++ .../test_mqtt_client.py | 160 ++++++------------ 3 files changed, 68 insertions(+), 106 deletions(-) create mode 100755 examples/minimqtt_hardware_test/settings.py rename examples/{ => minimqtt_hardware_test}/test_mqtt_client.py (54%) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 72a4f93..c80c59b 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -522,8 +522,8 @@ def unsubscribe(self, topic): for t in topics: self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) self._sock.write(packet) + # handle unsuback return_code = self._sock.read(4) - print(return_code) # [MQTT-3.11.1] assert return_code[1] == const(0x02) # [MQTT-3.32] diff --git a/examples/minimqtt_hardware_test/settings.py b/examples/minimqtt_hardware_test/settings.py new file mode 100755 index 0000000..579f96b --- /dev/null +++ b/examples/minimqtt_hardware_test/settings.py @@ -0,0 +1,12 @@ +# Test Settings for `minimqtt_hardware_test.py` +settings = { + 'url': 'io.adafruit.com', + 'username' : 'brubell', + 'password' : '65076d01a37745fa9a3cda8ba66a00cc', + 'test_timeout' : 1, + 'default_topic' : 'brubell/feeds/testfeed1', + 'alt_topic' : 'brubell/feeds/testfeed2', + 'default_data_int' : 42, + 'default_data_float' : 3.14, + 'default_data_str' : 'pi' +} \ No newline at end of file diff --git a/examples/test_mqtt_client.py b/examples/minimqtt_hardware_test/test_mqtt_client.py similarity index 54% rename from examples/test_mqtt_client.py rename to examples/minimqtt_hardware_test/test_mqtt_client.py index 159fb71..8355bb6 100755 --- a/examples/test_mqtt_client.py +++ b/examples/minimqtt_hardware_test/test_mqtt_client.py @@ -17,22 +17,19 @@ Generic cpython3 unittest-like assertions """ -class AssertRaisesContext: - def __init__(exc): - expected = exc - - def __enter__(self): - return self - - def __exit__(exc_type, exc_value, tb): - if exc_type is None: - assert False, "%r not raised" % expected - if issubclass(exc_type, expected): - return True - return False +#pylint: disable=keyword-arg-before-vararg +def assertRaises(self, exc, func=None, *args, **kwargs): + if func is None: + return AssertRaisesContext(exc) + try: + func(*args, **kwargs) + assert False, "%r not raised" % exc + except Exception as e: + if isinstance(e, exc): + return + raise -#pylint: disable=keyword-arg-before-vararg def assertAlmostEqual(x, y, places=None, msg=''): """Raises an AssertionError if two float values are not equal. (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py).""" @@ -71,19 +68,27 @@ def assertRaises(exc, func=None, *args, **kwargs): # MQTT Client Tests def test_mqtt_connect_disconnect_esp32spi(): """Creates an INSECURE MQTT client, connects, and attempts a disconnection.""" - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp, is_ssl=False) + mqtt_client = MQTT(socket, + settings['url'], + username=settings['username'], + password=settings['password'], + esp = esp, + is_ssl = False) + mqtt_client.connect() assertEqual(mqtt_client.port, 1883) assertEqual(mqtt_client._is_connected, True) mqtt_client.disconnect() assertEqual(mqtt_client._is_connected, False) def test_mqtts_connect_disconnect_esp32spi(): - """Creates a MQTTS client, connects, and attempts a disconnection.""" - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp) - assertEqual(mqtt_client.port, 8883) + """Creates a SECURE MQTT client, connects, and attempts a disconnection.""" + mqtt_client = MQTT(socket, + settings['url'], + username=settings['username'], + password=settings['password'], + esp = esp) mqtt_client.connect() + assertEqual(mqtt_client.port, 8883) assertEqual(mqtt_client._is_connected, True) mqtt_client.disconnect() assertEqual(mqtt_client._is_connected, False) @@ -91,10 +96,13 @@ def test_mqtts_connect_disconnect_esp32spi(): def test_sub_pub(): """Creates a MQTTS client, connects, subscribes, publishes, and checks data received from broker matches data sent by client""" - MSG_TOPIC = 'brubell/feeds/testfeed' - MSG_DATA = 42 - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp) + mqtt_client = MQTT(socket, + settings['url'], + username=settings['username'], + password=settings['password'], + esp = esp) + # listen in on the logger... + mqtt_client.set_logger_level = 'DEBUG' # Callback responses callback_msgs = [] def on_message(client, topic, msg): @@ -102,24 +110,28 @@ def on_message(client, topic, msg): mqtt_client.on_message = on_message mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe(MSG_TOPIC) - mqtt_client.publish(MSG_TOPIC, MSG_DATA) + mqtt_client.subscribe(['default_topic']) + mqtt_client.publish(settings['default_topic'], settings['default_data_int'], 1) start_timer = time.monotonic() - print('listening...') while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): mqtt_client.wait_for_msg() # check message and topic has been RX'd by the client's callback - assertEqual(callback_msgs[0][0], MSG_TOPIC) - assertEqual(callback_msgs[0][1], str(MSG_DATA)) + assertEqual(callback_msgs[0][0], settings['default_topic']) + assertEqual(callback_msgs[0][1], str(settings['default_data_int'])) + mqtt_client.unsubscribe(settings['default_topic']) mqtt_client.disconnect() + return def test_sub_pub_multiple(): """Subscribe to multiple topics, publish to one, unsubscribe from both. """ - MSG_TOPIC_1 = 'brubell/feeds/testfeed1' - MSG_TOPIC_2 = 'brubell/feeds/testfeed2' - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp) + TOPIC_1 = settings['default_topic'] + TOPIC_2 = settings['alt_topic'] + mqtt_client = MQTT(socket, + settings['url'], + username=settings['username'], + password=settings['password'], + esp = esp) # Callback responses callback_msgs = [] def on_message(client, topic, msg): @@ -128,94 +140,32 @@ def on_message(client, topic, msg): mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) # subscribe to two topics with different QoS levels - mqtt_client.subscribe([(MSG_TOPIC_1, 1), (MSG_TOPIC_2, 0)]) - mqtt_client.publish(MSG_TOPIC_2, 42) + mqtt_client.subscribe([(TOPIC_1, 1), (TOPIC_2, 0)]) + mqtt_client.publish(TOPIC_2, 42) start_timer = time.monotonic() - print('listening...') while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): mqtt_client.wait_for_msg() # check message and topic has been RX'd by the client's callback - assertEqual(callback_msgs[0][0], MSG_TOPIC_2) + assertEqual(callback_msgs[0][0], TOPIC_2) assertEqual(callback_msgs[0][1], str(42)) - mqtt_client.unsubscribe([MSG_TOPIC_1, MSG_TOPIC_2]) + mqtt_client.unsubscribe([(TOPIC_1), (TOPIC_2)]) mqtt_client.disconnect() -def test_publish_errors(): - """Testing invalid publish() calls, expecting specific MMQTExceptions. - """ - MSG_TOPIC = 'brubell/feeds/testfeed' - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp) - # Callback responses - callback_msgs = [] - def on_message(client, topic, msg): - callback_msgs.append([topic, msg]) - mqtt_client.on_message = on_message - mqtt_client.connect() - assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe(MSG_TOPIC) - # Publishing message of NoneType - # TODO: check over assertRaises callback in Micropython - # TODO: list of incorrect topics and/or data, iterate thru 'em - try: - mqtt_client.publish(MSG_TOPIC, None) - except MMQTTException as e: - print('e: ', e) - pass - # Publishing to a wildcard topic - try: - mqtt_client.publish('brubell/feeds/+', 42) - except MMQTTException as e: - print('e: ', e) - pass - try: - mqtt_client.publish('brubell/feeds/*', 42) - except MMQTTException as e: - print('e: ', e) - pass - # TODO: Publishing message with length greater than MQTT_MSG_MAX_SZ - # Publishing with an invalid QoS level - try: - mqtt_client.publish(MSG_TOPIC, 42, qos=3) - except MMQTTException as e: - print ('e: ', e) - pass - - def test_topic_wildcards(): - """Tests if topic wildcards work as specified in MQTT[4.7.1]. - """ - MSG_TOPIC = 'brubell/feeds/testfeed' - mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp) - # Callback responses - callback_msgs = [] - def on_message(client, topic, msg): - callback_msgs.append([topic, msg]) - mqtt_client.on_message = on_message - mqtt_client.connect() - assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe('brubell/feeds/tests/#') - mqtt_client.publish('brubell/feeds/tests.testone') - - -# Timeout between tests, in seconds. This value depends on the MQTT broker. -TEST_TIMEOUT = 1 + # Connection/Client Tests conn_tests = [test_mqtt_connect_disconnect_esp32spi, test_mqtts_connect_disconnect_esp32spi] # Publish/Subscribe tests -pub_sub_tests = [test_sub_pub, test_sub_pub_multiple, test_publish_errors] - -# Wildcard and subscription filter errors -test_topic_wildcards = [test_topic_wildcards] +pub_sub_tests = [test_sub_pub, test_sub_pub_multiple] # The test routine will run the following test(s): -tests = test_topic_wildcards +tests = pub_sub_tests -# Get wifi details and more from a secrets.py file try: from secrets import secrets + # Test Setup + from settings import settings except ImportError: print("WiFi secrets are kept in secrets.py, please add them there!") raise @@ -245,5 +195,5 @@ def on_message(client, topic, msg): print('Running test: ', i) i[1]() print('OK!') - time.sleep(TEST_TIMEOUT) + time.sleep(settings['test_timeout']) print('Ran {0} tests in {1}s.'.format(len(tests), time.monotonic() - start_time)) From e4ccdb5f3672357675f99b132b967c431f09533a Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 14:13:05 -0400 Subject: [PATCH 109/148] fix import order --- .../test_mqtt_client.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/minimqtt_hardware_test/test_mqtt_client.py b/examples/minimqtt_hardware_test/test_mqtt_client.py index 8355bb6..9cef679 100755 --- a/examples/minimqtt_hardware_test/test_mqtt_client.py +++ b/examples/minimqtt_hardware_test/test_mqtt_client.py @@ -110,7 +110,7 @@ def on_message(client, topic, msg): mqtt_client.on_message = on_message mqtt_client.connect() assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe(['default_topic']) + mqtt_client.subscribe(settings['default_topic']) mqtt_client.publish(settings['default_topic'], settings['default_data_int'], 1) start_timer = time.monotonic() while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): @@ -162,14 +162,6 @@ def on_message(client, topic, msg): # The test routine will run the following test(s): tests = pub_sub_tests -try: - from secrets import secrets - # Test Setup - from settings import settings -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") - raise - # Define an ESP32SPI network interface esp32_cs = DigitalInOut(board.ESP_CS) esp32_ready = DigitalInOut(board.ESP_BUSY) @@ -178,8 +170,15 @@ def on_message(client, topic, msg): esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +# Import Test Settings +try: + from secrets import secrets + from settings import settings +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise -# Establish ESP32SPI connection +# Establish WiFi Connection print("Connecting to AP...") while not esp.is_connected: try: @@ -189,7 +188,7 @@ def on_message(client, topic, msg): continue print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) -## Test Routine ## +# Test Harness start_time = time.monotonic() for i in enumerate(tests): print('Running test: ', i) From 623148e7612897401a93de7b2a6e44275296acd8 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 16:29:36 -0400 Subject: [PATCH 110/148] fix password encoding issue --- adafruit_minimqtt.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index c80c59b..616a380 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -115,10 +115,10 @@ def __init__(self, socket, server_address, port=None, username=None, self.port = port # session identifiers self._user = username - # [MQTT-3.1.3.5] - if len(password.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: - raise MMQTTException('Password length is too large.') + # [MQTT-3.1.3.5] self._pass = password + if self._pass is not None and len(password.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: + raise MMQTTException('Password length is too large.') if client_id is not None: # user-defined client_id MAY allow client_id's > 23 bytes or # non-alpha-numeric characters @@ -241,9 +241,12 @@ def connect(self, clean_session=True): raise MMQTTException("Invalid server address defined.") else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] + print('Addresss', addr) + print('Addresss', self.server) try: if self._logger is not None: self._logger.debug('Attempting to establish insecure MQTT connection...') + print('Addresss', addr) self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") From 2d40156393b6f02900dcc15871f7b16a0063216d Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 16:39:51 -0400 Subject: [PATCH 111/148] switching server_Address to server --- adafruit_minimqtt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 616a380..bfcb255 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -84,7 +84,7 @@ class MQTT: """ MQTT client interface for CircuitPython devices. :param socket: Socket object for provided network interface - :param str server_address: Server URL or IP Address. + :param str server: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. :param str password: Password for broker authentication. @@ -95,7 +95,7 @@ class MQTT: :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable - def __init__(self, socket, server_address, port=None, username=None, + def __init__(self, socket, server, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface self._socket = socket @@ -138,7 +138,7 @@ def __init__(self, socket, server_address, port=None, username=None, # subscription method handler dictionary self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM - self.server = server_address + self.server = server self.packet_id = 0 self._keep_alive = 0 self._pid = 0 @@ -241,12 +241,9 @@ def connect(self, clean_session=True): raise MMQTTException("Invalid server address defined.") else: addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] - print('Addresss', addr) - print('Addresss', self.server) try: if self._logger is not None: self._logger.debug('Attempting to establish insecure MQTT connection...') - print('Addresss', addr) self._sock.connect(addr, TCP_MODE) except RuntimeError: raise MMQTTException("Invalid server address defined.") @@ -254,6 +251,7 @@ def connect(self, clean_session=True): msg = MQTT_CON_HEADER msg[6] = clean_session << 1 sz = 12 + len(self._client_id) + print('USER: ', self._user) if self._user is not None: sz += 2 + len(self._user) + 2 + len(self._pass) msg[6] |= 0xC0 From 6ae098aa729559aafb6ec8724247ca82ba5e61a1 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 17:41:15 -0400 Subject: [PATCH 112/148] pass topic along with granted QOs to on_subscribe --- adafruit_minimqtt.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bfcb255..03bc521 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -251,7 +251,6 @@ def connect(self, clean_session=True): msg = MQTT_CON_HEADER msg[6] = clean_session << 1 sz = 12 + len(self._client_id) - print('USER: ', self._user) if self._user is not None: sz += 2 + len(self._user) + 2 + len(self._pass) msg[6] |= 0xC0 @@ -477,9 +476,9 @@ def subscribe(self, topic, qos=0): assert rc[1] == packet[2] and rc[2] == packet[3] if rc[3] == 0x80: raise MMQTTException('SUBACK Failure!') - for t in topics: + for t, q in topics: if self.on_subscribe is not None: - self.on_subscribe(self, self._user_data, rc[3]) + self.on_subscribe(self, self._user_data, t, q) self._subscribed_topics.append(t[0]) return @@ -531,7 +530,7 @@ def unsubscribe(self, topic): assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] for t in topics: if self.on_unsubscribe is not None: - self.on_unsubscribe(self, t, self._pid) + self.on_unsubscribe(self, self._user_data, t, self._pid) self._subscribed_topics.remove(t) def wait_for_msg(self, timeout=0.1): From 16e0917b77029987d038d4747b7b46e89a45379b Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 17:51:16 -0400 Subject: [PATCH 113/148] server/broker --- adafruit_minimqtt.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 03bc521..2a86d0f 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -84,7 +84,7 @@ class MQTT: """ MQTT client interface for CircuitPython devices. :param socket: Socket object for provided network interface - :param str server: Server URL or IP Address. + :param str broker: Server URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. :param str password: Password for broker authentication. @@ -95,7 +95,7 @@ class MQTT: :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable - def __init__(self, socket, server, port=None, username=None, + def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface self._socket = socket @@ -138,7 +138,7 @@ def __init__(self, socket, server, port=None, username=None, # subscription method handler dictionary self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM - self.server = server + self.broker = broker self.packet_id = 0 self._keep_alive = 0 self._pid = 0 @@ -236,17 +236,17 @@ def connect(self, clean_session=True): try: if self._logger is not None: self._logger.debug('Attempting to establish secure MQTT connection...') - self._sock.connect((self.server, self.port), TLS_MODE) + self._sock.connect((self.broker, self.port), TLS_MODE) except RuntimeError: - raise MMQTTException("Invalid server address defined.") + raise MMQTTException("Invalid broker address defined.") else: - addr = self._socket.getaddrinfo(self.server, self.port)[0][-1] + addr = self._socket.getaddrinfo(self.broker, self.port)[0][-1] try: if self._logger is not None: self._logger.debug('Attempting to establish insecure MQTT connection...') self._sock.connect(addr, TCP_MODE) except RuntimeError: - raise MMQTTException("Invalid server address defined.") + raise MMQTTException("Invalid broker address defined.") premsg = MQTT_CON msg = MQTT_CON_HEADER msg[6] = clean_session << 1 @@ -269,7 +269,7 @@ def connect(self, clean_session=True): i += 1 premsg[i] = sz if self._logger is not None: - self._logger.debug('Sending CONNECT packet to server') + self._logger.debug('Sending CONNECT packet to broker') self._sock.write(premsg) self._sock.write(msg) # [MQTT-3.1.3-4] @@ -284,7 +284,7 @@ def connect(self, clean_session=True): self._send_str(self._user) self._send_str(self._pass) if self._logger is not None: - self._logger.debug('Receiving CONNACK packet from server') + self._logger.debug('Receiving CONNACK packet from broker') rc = self._sock.read(4) assert rc[0] == const(0x20) and rc[1] == const(0x02) if rc[3] != 0: @@ -300,7 +300,7 @@ def disconnect(self): """ self.is_connected() if self._logger is not None: - self._logger.debug('Sending DISCONNECT packet to server') + self._logger.debug('Sending DISCONNECT packet to broker') self._sock.write(MQTT_DISCONNECT) if self._logger is not None: self._logger.debug('Closing socket') @@ -310,9 +310,9 @@ def disconnect(self): self.on_disconnect(self, self._user_data, 0) def ping(self): - """Pings the MQTT Broker to confirm if the server is alive or if + """Pings the MQTT Broker to confirm if the broker is alive or if there is an active network connection. - Raises a MMQTTException if the server does not respond with a PINGRESP packet. + Raises a MMQTTException if the broker does not respond with a PINGRESP packet. """ self.is_connected() if self._logger is not None: @@ -322,7 +322,7 @@ def ping(self): self._logger.debug('Checking PINGRESP') ping_resp = self._sock.read(2) if ping_resp[0] != MQTT_PINGRESP or ping_resp[1] != const(0x00): - raise MMQTTException('PINGRESP not returned from server.') + raise MMQTTException('PINGRESP not returned from broker.') # pylint: disable=too-many-branches, too-many-statements def publish(self, topic, msg, retain=False, qos=0): @@ -368,7 +368,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz = 2 + len(topic) + len(msg) if qos > 0: sz += 2 - assert sz < 2097152 + assert sz < const(2097152) i = 1 while sz > 0x7f: pkt[i] = (sz & 0x7f) | const(0x80) @@ -382,14 +382,14 @@ def publish(self, topic, msg, retain=False, qos=0): self._send_str(topic) if qos == 0: if self.on_publish is not None: - self.on_publish(self, self._user_data, self._pid) + self.on_publish(self, self._user_data, topic, self._pid) if qos > 0: self._pid += 1 pid = self._pid struct.pack_into("!H", pkt, 0, pid) self._sock.write(pkt) if self.on_publish is not None: - self.on_publish(self, self._user_data, pid) + self.on_publish(self, self._user_data, topic, pid) if self._logger is not None: self._logger.debug('Sending PUBACK') self._sock.write(msg) @@ -403,12 +403,12 @@ def publish(self, topic, msg, retain=False, qos=0): rcv_pid = rcv_pid[0] << const(0x08) | rcv_pid[1] if pid == rcv_pid: if self.on_publish is not None: - self.on_publish(self, self._user_data, rcv_pid) + self.on_publish(self, self._user_data, topic, rcv_pid) return elif qos == 2: assert 0 if self.on_publish is not None: - self.on_publish(self, self._user_data, rcv_pid) + self.on_publish(self, self._user_data, topic, rcv_pid) def subscribe(self, topic, qos=0): """Subscribes to a topic on the MQTT Broker. @@ -479,7 +479,7 @@ def subscribe(self, topic, qos=0): for t, q in topics: if self.on_subscribe is not None: self.on_subscribe(self, self._user_data, t, q) - self._subscribed_topics.append(t[0]) + self._subscribed_topics.append(t) return def unsubscribe(self, topic): @@ -506,6 +506,7 @@ def unsubscribe(self, topic): self._check_topic(t) topics.append((t)) for t in topics: + print(self._subscribed_topics) if t not in self._subscribed_topics: raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') # Assemble packet From 0a20d6bb9d4aa61b33ddd958a3f4b7832c197b07 Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 9 Jul 2019 18:36:49 -0400 Subject: [PATCH 114/148] add minimqtt with replicatable unsuback issue --- adafruit_minimqtt.py | 24 ++++++---- examples/minimqtt_simpletest.py | 84 ++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 43 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2a86d0f..8c73bf6 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -506,7 +506,6 @@ def unsubscribe(self, topic): self._check_topic(t) topics.append((t)) for t in topics: - print(self._subscribed_topics) if t not in self._subscribed_topics: raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') # Assemble packet @@ -523,16 +522,21 @@ def unsubscribe(self, topic): for t in topics: self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) self._sock.write(packet) + self._logger.debug('Waiting for UNSUBACK...') # handle unsuback - return_code = self._sock.read(4) - # [MQTT-3.11.1] - assert return_code[1] == const(0x02) - # [MQTT-3.32] - assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] - for t in topics: - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, self._user_data, t, self._pid) - self._subscribed_topics.remove(t) + while 1: + op = self.wait_for_msg() + print(op) + return_code = self._sock.read(4) + print(return_code) + assert return_code[1] == const(0x02) + # [MQTT-3.32] + assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] + for t in topics: + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, self._user_data, t, self._pid) + self._subscribed_topics.remove(t) + return def wait_for_msg(self, timeout=0.1): """Reads and processes network events. Returns network response if successful. diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index b0d3972..7bbea4a 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -41,11 +41,6 @@ # BLUE_LED = PWMOut.PWMOut(esp, 25) # status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) -# Instanciate a MQTT Client -mqtt_client = MQTT(esp, socket, secrets['aio_url'], - username=secrets['aio_user'], password=secrets['aio_password'], - is_ssl = True) - print("Connecting to AP...") while not esp.is_connected: try: @@ -56,49 +51,72 @@ print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("My IP address is", esp.pretty_ip(esp.ip_address)) -print('Connecting to {0}:{1}...'.format(mqtt_client.server, mqtt_client.port)) +# Default MiniMQTT Topic +default_feed = 'brubell/feeds/temperature' -# ACK Callbacks for testing +# MiniMQTT Callback Handlers def connect(client, userdata, flags, rc): - """.connect() called""" + # This method is called when client.connect() is called. print('Connected to MQTT Broker!') print('Flags: {0}\n RC: {1}'.format(flags, rc)) def disconnect(client, userdata, rc): - """.disconnect() called""" + # This method is called when client.disconnect() is called. print('Disconnected from MQTT Broker!') - print('RC: {0}\n'.format(rc)) -def subscribe(client, userdata, qos): - """.subscribe() called""" - print('Client successfully subscribed to feed with a QOS of %d'%qos) +def subscribe(client, userdata, topic, granted_qos): + # This method is called when client.subscribe() is called. + print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos)) + +def unsubscribe(client, userdata, topic, pid): + # This method is called when client.unsubscribe() is called. + print('Unsubscribed from {0} with PID {1}'.format(topic, pid)) -def publish(client, userdata, pid): - """.publish() called""" - print('Client successfully published to feed with a PID of %d'%pid) +def publish(client, userdata, topic, pid): + # This method is called when client.publish() is called. + print('Published to {0} with PID {1}'.format(topic, pid)) + +# Instanciate a new MiniMQTT Client +client = MQTT(socket, secrets['aio_url'], + username=secrets['aio_user'], + password=secrets['aio_password'], + esp= esp, + log=True) # Set the user_data to a generated client_id -mqtt_client.user_data = mqtt_client._client_id +client.user_data = client._client_id + +# Connect callback handlers +client.on_connect = connect +client.on_disconnect = disconnect +client.on_subscribe = subscribe +client.on_unsubscribe = unsubscribe +client.on_publish = publish + +# Optionally set a logging level +#client.set_logger_level('DEBUG') + +print('Attempting to connect to %s'%client.broker) +client.connect() + +print('Subscribing to %s'%default_feed) +client.subscribe(default_feed) -# define callbacks -mqtt_client.on_connect = connect -mqtt_client.on_subscribe = subscribe -mqtt_client.on_publish = publish -mqtt_client.on_disconnect = disconnect +print('Publishing to %s'%default_feed) +client.publish(default_feed, 'Hello Broker!') -# Connect MQTT Client -print('connecting...') -mqtt_client.connect() +print('Unsubscribing from %s'%default_feed) +client.unsubscribe(default_feed) -# Subscribe to feed -print('subscribing...') -mqtt_client.subscribe('brubell/feeds/temperature') +print('Subscribing to %s'%default_feed) +client.subscribe(default_feed) -print('publishing...') -mqtt_client.publish('brubell/feeds/temperature', 50) +print('Publishing to %s'%default_feed) +client.publish(default_feed, 'Hello Broker!') -# Disconnect from MQTT Client -print('disconnecting...') -mqtt_client.disconnect() +print('Unsubscribing from %s'%default_feed) +client.unsubscribe(default_feed) +print('Disconnecting from %s'%client.broker) +client.disconnect() From 8f6994079cd6c85389313b391039710146ac57db Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 11:35:25 -0400 Subject: [PATCH 115/148] add a method for setting network interface hardware, useful for later when we have more interfaces --- adafruit_minimqtt.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 8c73bf6..a63db5e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -105,7 +105,10 @@ def __init__(self, socket, broker, port=None, username=None, else: raise MMQTTException('Invalid ESP32SPI object provided.') else: - raise NotImplementedError('MiniMQTT currently only supports an ESP32SPI object.') + raise NotImplementedError('MiniMQTT currently supports an ESP32SPI network interface.') + # broker + # TODO: add check for IP string instead of URL + self.broker = broker # port/ssl if is_ssl: self.port = MQTT_TLS_PORT @@ -138,7 +141,6 @@ def __init__(self, socket, broker, port=None, username=None, # subscription method handler dictionary self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM - self.broker = broker self.packet_id = 0 self._keep_alive = 0 self._pid = 0 @@ -218,19 +220,26 @@ def is_connected(self): raise MMQTTException("MiniMQTT is not connected.") return self._is_connected + def _set_interface(self): + """Sets a desired network hardware interface. + Note: The network hardware must be set in init + prior to calling this method. + """ + if self._esp: + self._socket.set_interface(self._esp) + else: + raise TypeError('network interface required.') + # pylint: disable=too-many-branches, too-many-statements def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. :param bool clean_session: Establishes a persistent session with the broker. Defaults to a non-persistent session. """ - if self._esp: - if self._logger is not None: - self._logger.debug('Creating new socket') - self._socket.set_interface(self._esp) - self._sock = self._socket.socket() - else: - raise TypeError('ESP32SPI interface required!') + self._set_interface() + if self._logger is not None: + self._logger.debug('Creating new socket') + self._sock = self._socket.socket() self._sock.settimeout(10) if self.port == 8883: try: From f5f65b6607f4858cad5f57f494cfcb35addcd72f Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 11:43:54 -0400 Subject: [PATCH 116/148] rewrite of reconnect --- adafruit_minimqtt.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index a63db5e..89391b1 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -184,7 +184,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): self._lw_msg = message self._lw_retain = retain - def reconnect(self, retries=30, resub_topics=False): + def reconnect(self, retries=30, resub_topics=True): """Attempts to reconnect to the MQTT broker. :param int retries: Amount of retries before resetting the network interface. :param bool resub_topics: Client resubscribes to previously subscribed topics upon @@ -195,23 +195,22 @@ def reconnect(self, retries=30, resub_topics=False): if self._logger is not None: self._logger.debug('Attempting to reconnect to broker') try: - self.connect(False) + self.connect() + if self._logger is not None: + self._logger.debug('Reconnected to broker') + if resub_topics: + if self._logger is not None: + self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') + while self._subscribed_topics: + feed = self._subscribed_topics.pop() + self.subscribe(feed) except OSError as e: print('Failed to connect to the broker, retrying\n', e) retries += 1 if retries >= 30: retries = 0 - time.sleep(0.5) + time.sleep(1) continue - self._is_connected = True - if self._logger is not None: - self._logger.debug('Reconnected to broker') - if resub_topics: - if self._logger is not None: - self._logger.debug('Attempting to resubscribe to prv. subscribed topics.') - while self._subscribed_topics: - feed = self._subscribed_topics.pop() - self.subscribe(feed) def is_connected(self): """Returns MQTT client session status as True if connected, raises From 7df2b9fb234a152a60e913dc3381defc69657f8b Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 12:39:25 -0400 Subject: [PATCH 117/148] allow IP addresses along with URLs --- adafruit_minimqtt.py | 53 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 89391b1..0e624ba 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -84,7 +84,7 @@ class MQTT: """ MQTT client interface for CircuitPython devices. :param socket: Socket object for provided network interface - :param str broker: Server URL or IP Address. + :param str broker: MQTT Broker URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. :param str password: Password for broker authentication. @@ -107,8 +107,10 @@ def __init__(self, socket, broker, port=None, username=None, else: raise NotImplementedError('MiniMQTT currently supports an ESP32SPI network interface.') # broker - # TODO: add check for IP string instead of URL - self.broker = broker + try: # set broker IP + self.broker = self._esp.unpretty_ip(broker) + except ValueError: # set broker URL + self.broker = broker # port/ssl if is_ssl: self.port = MQTT_TLS_PORT @@ -212,23 +214,6 @@ def reconnect(self, retries=30, resub_topics=True): time.sleep(1) continue - def is_connected(self): - """Returns MQTT client session status as True if connected, raises - a MMQTTException if False.""" - if self._sock is None or self._is_connected is False: - raise MMQTTException("MiniMQTT is not connected.") - return self._is_connected - - def _set_interface(self): - """Sets a desired network hardware interface. - Note: The network hardware must be set in init - prior to calling this method. - """ - if self._esp: - self._socket.set_interface(self._esp) - else: - raise TypeError('network interface required.') - # pylint: disable=too-many-branches, too-many-statements def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. @@ -248,13 +233,17 @@ def connect(self, clean_session=True): except RuntimeError: raise MMQTTException("Invalid broker address defined.") else: - addr = self._socket.getaddrinfo(self.broker, self.port)[0][-1] + if isinstance(self.broker, str): + addr = self._socket.getaddrinfo(self.broker, self.port)[0][-1] + else: + addr = (self.broker, self.port) try: if self._logger is not None: self._logger.debug('Attempting to establish insecure MQTT connection...') + #self._sock.connect((self.broker, self.port), TCP_MODE) self._sock.connect(addr, TCP_MODE) - except RuntimeError: - raise MMQTTException("Invalid broker address defined.") + except RuntimeError as e: + raise MMQTTException("Invalid broker address defined.", e) premsg = MQTT_CON msg = MQTT_CON_HEADER msg[6] = clean_session << 1 @@ -626,6 +615,24 @@ def _check_qos(self, qos_level): raise MMQTTException('QoS must be an integer.') return + def _set_interface(self): + """Sets a desired network hardware interface. + Note: The network hardware must be set in init + prior to calling this method. + """ + if self._esp: + self._socket.set_interface(self._esp) + else: + raise TypeError('network interface required.') + + def is_connected(self): + """Returns MQTT client session status as True if connected, raises + a MMQTTException if False. + """ + if self._sock is None or self._is_connected is False: + raise MMQTTException("MiniMQTT is not connected.") + return self._is_connected + @property def mqtt_msg(self): """Returns maximum MQTT payload and topic size.""" From 3ac677209ecca5fda85006d0f2f2250eb9749f48 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 12:44:04 -0400 Subject: [PATCH 118/148] lint --- adafruit_minimqtt.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 0e624ba..f834174 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -94,7 +94,7 @@ class MQTT: Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface @@ -112,10 +112,9 @@ def __init__(self, socket, broker, port=None, username=None, except ValueError: # set broker URL self.broker = broker # port/ssl + self.port = MQTT_TCP_PORT if is_ssl: self.port = MQTT_TLS_PORT - else: - self.port = MQTT_TCP_PORT if port is not None: self.port = port # session identifiers @@ -140,7 +139,7 @@ def __init__(self, socket, broker, port=None, username=None, if log is True: self._logger = logging.getLogger('log') self._logger.setLevel(logging.INFO) - # subscription method handler dictionary + self._sock = None self._is_connected = False self._msg_size_lim = MQTT_MSG_SZ_LIM self.packet_id = 0 @@ -504,12 +503,12 @@ def unsubscribe(self, topic): topics.append((t)) for t in topics: if t not in self._subscribed_topics: - raise MMQTTException('Topic must be subscribed to before attempting to unsubscribe.') + raise MMQTTException('Topic must be subscribed to before attempting unsubscribe.') # Assemble packet packet_length = 2 + (2 * len(topics)) packet_length += sum(len(topic) for topic in topics) packet_length_byte = packet_length.to_bytes(1, 'big') - self._pid+=1 + self._pid += 1 packet_id_bytes = self._pid.to_bytes(2, 'big') packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes for t in topics: @@ -591,20 +590,22 @@ def _send_str(self, string): else: self._sock.write(string) - def _check_topic(self, topic): + @staticmethod + def _check_topic(topic): """Checks if topic provided is a valid mqtt topic. :param str topic: Topic identifier """ if topic is None: raise MMQTTException('Topic may not be Nonetype') # [MQTT-4.7.3-1] - elif not len(topic): + elif not topic: raise MMQTTException('Topic may not be empty.') # [MQTT-4.7.3-3] elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT: raise MMQTTException('Topic length is too large.') - def _check_qos(self, qos_level): + @staticmethod + def _check_qos(qos_level): """Validates the quality of service level. :param int qos_level: Desired QoS level. """ @@ -613,7 +614,6 @@ def _check_qos(self, qos_level): raise MMQTTException('QoS must be between 1 and 2.') else: raise MMQTTException('QoS must be an integer.') - return def _set_interface(self): """Sets a desired network hardware interface. @@ -644,7 +644,7 @@ def mqtt_msg(self, msg_size): :param int msg_size: Maximum MQTT payload size. """ if msg_size < MQTT_MSG_MAX_SZ: - self.__msg_size_lim = msg_size + self._msg_size_lim = msg_size # Logging def attach_logger(self, logger_name='log'): From 69d36ba4a9691b532e02a2d5f45e2085a3b0378d Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 12:48:28 -0400 Subject: [PATCH 119/148] only log when a logger is provided --- adafruit_minimqtt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f834174..b0874a2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -518,13 +518,11 @@ def unsubscribe(self, topic): for t in topics: self._logger.debug('UNSUBSCRIBING from topic {0}.'.format(t)) self._sock.write(packet) - self._logger.debug('Waiting for UNSUBACK...') - # handle unsuback + if self._logger is not None: + self._logger.debug('Waiting for UNSUBACK...') while 1: op = self.wait_for_msg() - print(op) return_code = self._sock.read(4) - print(return_code) assert return_code[1] == const(0x02) # [MQTT-3.32] assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] From 7981e9eca0d01911019a80517a13cf165adb1101 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 12:49:40 -0400 Subject: [PATCH 120/148] add generalized example code with support for AIO or MQTT brokers --- examples/minimqtt_simpletest.py | 42 +++++++++++---------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index 7bbea4a..fd094ab 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -51,9 +51,11 @@ print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("My IP address is", esp.pretty_ip(esp.ip_address)) +# MQTT Topic +mqtt_topic = 'test/topic' -# Default MiniMQTT Topic -default_feed = 'brubell/feeds/temperature' +# Adafruit IO-style Topic +# mqtt_topic = 'aio_user/feeds/temperature' # MiniMQTT Callback Handlers def connect(client, userdata, flags, rc): @@ -78,14 +80,10 @@ def publish(client, userdata, topic, pid): print('Published to {0} with PID {1}'.format(topic, pid)) # Instanciate a new MiniMQTT Client -client = MQTT(socket, secrets['aio_url'], - username=secrets['aio_user'], - password=secrets['aio_password'], - esp= esp, - log=True) - -# Set the user_data to a generated client_id -client.user_data = client._client_id +client = MQTT(socket, secrets['broker'], + username=secrets['user'], + password=secrets['pass'], + esp= esp) # Connect callback handlers client.on_connect = connect @@ -94,29 +92,17 @@ def publish(client, userdata, topic, pid): client.on_unsubscribe = unsubscribe client.on_publish = publish -# Optionally set a logging level -#client.set_logger_level('DEBUG') - print('Attempting to connect to %s'%client.broker) client.connect() -print('Subscribing to %s'%default_feed) -client.subscribe(default_feed) - -print('Publishing to %s'%default_feed) -client.publish(default_feed, 'Hello Broker!') - -print('Unsubscribing from %s'%default_feed) -client.unsubscribe(default_feed) - -print('Subscribing to %s'%default_feed) -client.subscribe(default_feed) +print('Subscribing to %s'%mqtt_topic) +client.subscribe(mqtt_topic) -print('Publishing to %s'%default_feed) -client.publish(default_feed, 'Hello Broker!') +print('Publishing to %s'%mqtt_topic) +client.publish(mqtt_topic, 'Hello Broker!') -print('Unsubscribing from %s'%default_feed) -client.unsubscribe(default_feed) +print('Unsubscribing from %s'%mqtt_topic) +client.unsubscribe(mqtt_topic) print('Disconnecting from %s'%client.broker) client.disconnect() From c45c209704cc861ce1596d593ac847bafd879b28 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 13:11:48 -0400 Subject: [PATCH 121/148] nicer code example for AIO --- examples/minimqtt_adafruitio.py | 69 +++++++++++++++++++++------------ examples/minimqtt_simpletest.py | 12 ------ 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/examples/minimqtt_adafruitio.py b/examples/minimqtt_adafruitio.py index b176eaa..34af273 100755 --- a/examples/minimqtt_adafruitio.py +++ b/examples/minimqtt_adafruitio.py @@ -1,3 +1,5 @@ +# CircuitPython MiniMQTT Library +# Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) import time import board import busio @@ -5,9 +7,10 @@ import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket - from adafruit_minimqtt import MQTT +### WiFi ### + # Get wifi details and more from a secrets.py file try: from secrets import secrets @@ -15,38 +18,54 @@ print("WiFi secrets are kept in secrets.py, please add them there!") raise -# Initialize an ESP32SPI network interface +# If you are using a board with pre-defined ESP32 Pins: esp32_cs = DigitalInOut(board.ESP_CS) esp32_ready = DigitalInOut(board.ESP_BUSY) esp32_reset = DigitalInOut(board.ESP_RESET) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -print("Connecting to %s..."%secrets['ssid']) -while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue -print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) -print("IP: ", esp.pretty_ip(esp.ip_address)) - -def on_message(client, topic, message): - """This method is called whenever a new message is received - from the server. - """ - print('Value on topic {0}: {1}'.format(topic, message)) +### Adafruit IO Setup ### # Setup a feed named `photocell` for publishing. -AIO_Publish_Feed = secrets['aio_user']+'/'+'feeds/photocell' +aio_publish_feed = secrets['user']+'/feeds/photocell' # Setup a feed named `onoffbutton` for subscribing to changes. -AIO_Subscribe_Feed = secrets['aio_user']+'/'+'feeds/onoffbutton' +aio_subscribe_feed = secrets['user']+'/feeds/onoffbutton' + +### Code ### + +def connect_wifi(): + print("Connecting to %s..."%secrets['ssid']) + while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue + print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) + print("IP: ", esp.pretty_ip(esp.ip_address)) + +def on_message(client, topic, message): + # This method is called whenever a new message is received + # from the server. + print('New message on topic {0}: {1}'.format(topic, message)) + +# Connect to WiFi +connect_wifi() # Set up a MiniMQTT Client -mqtt_client = MQTT(socket, secrets['aio_url'], username=secrets['aio_user'], password=secrets['aio_password'], - esp = esp, log = True) +mqtt_client = MQTT(socket, + secrets['broker'], + username=secrets['user'], + password=secrets['pass'], + esp = esp) # Attach on_message method to the MQTT Client mqtt_client.on_message = on_message @@ -54,15 +73,15 @@ def on_message(client, topic, message): # Initialize the MQTT Client mqtt_client.connect() -# Subscribe the client to topic AIO_SUBSCRIBE_FEED -mqtt_client.subscribe(AIO_Subscribe_Feed) +# Subscribe the client to topic aio_subscribe_feed +mqtt_client.subscribe(aio_subscribe_feed) photocell_val = 0 while True: - # Poll the message queue and ping the server + # Poll the message queue mqtt_client.wait_for_msg() print('Sending photocell value: %d'%photocell_val) - mqtt_client.publish(AIO_Publish_Feed, photocell_val) + mqtt_client.publish(aio_publish_feed, photocell_val) photocell_val += 1 time.sleep(0.5) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index fd094ab..371fd77 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -29,18 +29,6 @@ spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -"""Use below for Most Boards""" -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards -"""Uncomment below for ItsyBitsy M4""" -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) -"""Uncomment below for an externally defined RGB LED""" -# import adafruit_rgbled -# from adafruit_esp32spi import PWMOut -# RED_LED = PWMOut.PWMOut(esp, 26) -# GREEN_LED = PWMOut.PWMOut(esp, 27) -# BLUE_LED = PWMOut.PWMOut(esp, 25) -# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) - print("Connecting to AP...") while not esp.is_connected: try: From 5c45ec3112e2b0674c95e34ff7b2aef24c511720 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 13:19:59 -0400 Subject: [PATCH 122/148] nicer code for simpletest --- examples/minimqtt_simpletest.py | 57 ++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index 371fd77..d16f51f 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -8,7 +8,7 @@ from adafruit_minimqtt import MQTT -print("CircuitPython MiniMQTT WiFi Test") +### WiFi ### # Get wifi details and more from a secrets.py file try: @@ -17,34 +17,43 @@ print("WiFi secrets are kept in secrets.py, please add them there!") raise -try: - esp32_cs = DigitalInOut(board.ESP_CS) - esp32_ready = DigitalInOut(board.ESP_BUSY) - esp32_reset = DigitalInOut(board.ESP_RESET) -except: - esp32_cs = DigitalInOut(board.D9) - esp32_ready = DigitalInOut(board.D10) - esp32_reset = DigitalInOut(board.D5) +# If you are using a board with pre-defined ESP32 Pins: +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -print("Connecting to AP...") -while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue -print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) -print("My IP address is", esp.pretty_ip(esp.ip_address)) +### Topic Setup ### # MQTT Topic +# Use this topic if you'd like to connect to a standard MQTT broker mqtt_topic = 'test/topic' # Adafruit IO-style Topic +# Use this topic if you'd like to connect to io.adafruit.com # mqtt_topic = 'aio_user/feeds/temperature' +### Code ### + +def connect_wifi(): + # Connects the ESP32 to WiFi + print("Connecting to %s..."%secrets['ssid']) + while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue + print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) + print("IP: ", esp.pretty_ip(esp.ip_address)) + # MiniMQTT Callback Handlers def connect(client, userdata, flags, rc): # This method is called when client.connect() is called. @@ -67,13 +76,17 @@ def publish(client, userdata, topic, pid): # This method is called when client.publish() is called. print('Published to {0} with PID {1}'.format(topic, pid)) -# Instanciate a new MiniMQTT Client -client = MQTT(socket, secrets['broker'], +# Connect to WiFi +connect_wifi() + +# Set up a MiniMQTT Client +mqtt_client = MQTT(socket, + secrets['broker'], username=secrets['user'], password=secrets['pass'], - esp= esp) + esp = esp) -# Connect callback handlers +# Connect callback handlers to client client.on_connect = connect client.on_disconnect = disconnect client.on_subscribe = subscribe From 1fa78355c21c98d603d6cb6fc363bf0cc9f91a2d Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 13:28:26 -0400 Subject: [PATCH 123/148] fixup example --- examples/minimqtt_simpletest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index d16f51f..da58ceb 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -1,8 +1,6 @@ -import time import board import busio from digitalio import DigitalInOut -import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket @@ -80,7 +78,7 @@ def publish(client, userdata, topic, pid): connect_wifi() # Set up a MiniMQTT Client -mqtt_client = MQTT(socket, +client = MQTT(socket, secrets['broker'], username=secrets['user'], password=secrets['pass'], From ddf3e10157e66f35baeaf0918e3a1b0eb3609119 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 13:41:38 -0400 Subject: [PATCH 124/148] fix some pylint defs --- adafruit_minimqtt.py | 7 +- examples/minimqtt_hardware_test/settings.py | 12 -- .../test_mqtt_client.py | 198 ------------------ 3 files changed, 3 insertions(+), 214 deletions(-) delete mode 100755 examples/minimqtt_hardware_test/settings.py delete mode 100755 examples/minimqtt_hardware_test/test_mqtt_client.py diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index b0874a2..f8d8728 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -75,9 +75,9 @@ const(0x04) : 'Connection Refused - Incorrect username/password', const(0x05) : 'Connection Refused - Unauthorized'} +# pylint: disable=unnecessary-pass class MMQTTException(Exception): """MiniMQTT Exception class.""" - # pylint: disable=unnecessary-pass #pass class MQTT: @@ -94,7 +94,7 @@ class MQTT: Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface @@ -129,7 +129,6 @@ def __init__(self, socket, broker, port=None, username=None, self._client_id = client_id else: # assign a unique client_id - # pylint: disable=no-member self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) # generated client_id's enforce spec.'s length rules @@ -521,7 +520,7 @@ def unsubscribe(self, topic): if self._logger is not None: self._logger.debug('Waiting for UNSUBACK...') while 1: - op = self.wait_for_msg() + self.wait_for_msg() return_code = self._sock.read(4) assert return_code[1] == const(0x02) # [MQTT-3.32] diff --git a/examples/minimqtt_hardware_test/settings.py b/examples/minimqtt_hardware_test/settings.py deleted file mode 100755 index 579f96b..0000000 --- a/examples/minimqtt_hardware_test/settings.py +++ /dev/null @@ -1,12 +0,0 @@ -# Test Settings for `minimqtt_hardware_test.py` -settings = { - 'url': 'io.adafruit.com', - 'username' : 'brubell', - 'password' : '65076d01a37745fa9a3cda8ba66a00cc', - 'test_timeout' : 1, - 'default_topic' : 'brubell/feeds/testfeed1', - 'alt_topic' : 'brubell/feeds/testfeed2', - 'default_data_int' : 42, - 'default_data_float' : 3.14, - 'default_data_str' : 'pi' -} \ No newline at end of file diff --git a/examples/minimqtt_hardware_test/test_mqtt_client.py b/examples/minimqtt_hardware_test/test_mqtt_client.py deleted file mode 100755 index 9cef679..0000000 --- a/examples/minimqtt_hardware_test/test_mqtt_client.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -CircuitPython_MiniMQTT Methods Test Suite - -by Brent Rubell for Adafruit Industries, 2019 -""" -import time -import board -import busio -from digitalio import DigitalInOut -import neopixel -from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_socket as socket - -from adafruit_minimqtt import MQTT, MMQTTException - -""" -Generic cpython3 unittest-like assertions -""" - -#pylint: disable=keyword-arg-before-vararg -def assertRaises(self, exc, func=None, *args, **kwargs): - if func is None: - return AssertRaisesContext(exc) - - try: - func(*args, **kwargs) - assert False, "%r not raised" % exc - except Exception as e: - if isinstance(e, exc): - return - raise - -def assertAlmostEqual(x, y, places=None, msg=''): - """Raises an AssertionError if two float values are not equal. - (from https://github.com/micropython/micropython-lib/blob/master/unittest/unittest.py).""" - if x == y: - return - if places is None: - places = 2 - if round(abs(y-x), places) == 0: - return - if not msg: - msg = '%r != %r within %r places' % (x, y, places) - assert False, msg - -def assertIsNone(x): - """Raises an AssertionError if x is None.""" - if x is None: - raise AssertionError('%r is None'%x) - -def assertEqual(val_1, val_2): - """Raises an AssertionError if the two specified values are not equal.""" - if val_1 != val_2: - raise AssertionError('Values are not equal:', val_1, val_2) - -def assertRaises(exc, func=None, *args, **kwargs): - if func is None: - return AssertRaisesContext(exc) - - try: - func(*args, **kwargs) - assert False, "%r not raised" % exc - except Exception as e: - if isinstance(e, exc): - return - raise - -# MQTT Client Tests -def test_mqtt_connect_disconnect_esp32spi(): - """Creates an INSECURE MQTT client, connects, and attempts a disconnection.""" - mqtt_client = MQTT(socket, - settings['url'], - username=settings['username'], - password=settings['password'], - esp = esp, - is_ssl = False) - mqtt_client.connect() - assertEqual(mqtt_client.port, 1883) - assertEqual(mqtt_client._is_connected, True) - mqtt_client.disconnect() - assertEqual(mqtt_client._is_connected, False) - -def test_mqtts_connect_disconnect_esp32spi(): - """Creates a SECURE MQTT client, connects, and attempts a disconnection.""" - mqtt_client = MQTT(socket, - settings['url'], - username=settings['username'], - password=settings['password'], - esp = esp) - mqtt_client.connect() - assertEqual(mqtt_client.port, 8883) - assertEqual(mqtt_client._is_connected, True) - mqtt_client.disconnect() - assertEqual(mqtt_client._is_connected, False) - -def test_sub_pub(): - """Creates a MQTTS client, connects, subscribes, publishes, and checks data - received from broker matches data sent by client""" - mqtt_client = MQTT(socket, - settings['url'], - username=settings['username'], - password=settings['password'], - esp = esp) - # listen in on the logger... - mqtt_client.set_logger_level = 'DEBUG' - # Callback responses - callback_msgs = [] - def on_message(client, topic, msg): - callback_msgs.append([topic, msg]) - mqtt_client.on_message = on_message - mqtt_client.connect() - assertEqual(mqtt_client._is_connected, True) - mqtt_client.subscribe(settings['default_topic']) - mqtt_client.publish(settings['default_topic'], settings['default_data_int'], 1) - start_timer = time.monotonic() - while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): - mqtt_client.wait_for_msg() - # check message and topic has been RX'd by the client's callback - assertEqual(callback_msgs[0][0], settings['default_topic']) - assertEqual(callback_msgs[0][1], str(settings['default_data_int'])) - mqtt_client.unsubscribe(settings['default_topic']) - mqtt_client.disconnect() - return - -def test_sub_pub_multiple(): - """Subscribe to multiple topics, publish to one, unsubscribe from both. - """ - TOPIC_1 = settings['default_topic'] - TOPIC_2 = settings['alt_topic'] - mqtt_client = MQTT(socket, - settings['url'], - username=settings['username'], - password=settings['password'], - esp = esp) - # Callback responses - callback_msgs = [] - def on_message(client, topic, msg): - callback_msgs.append([topic, msg]) - mqtt_client.on_message = on_message - mqtt_client.connect() - assertEqual(mqtt_client._is_connected, True) - # subscribe to two topics with different QoS levels - mqtt_client.subscribe([(TOPIC_1, 1), (TOPIC_2, 0)]) - mqtt_client.publish(TOPIC_2, 42) - start_timer = time.monotonic() - while len(callback_msgs) == 0 and (time.monotonic() - start_timer < 30): - mqtt_client.wait_for_msg() - # check message and topic has been RX'd by the client's callback - assertEqual(callback_msgs[0][0], TOPIC_2) - assertEqual(callback_msgs[0][1], str(42)) - mqtt_client.unsubscribe([(TOPIC_1), (TOPIC_2)]) - mqtt_client.disconnect() - - - -# Connection/Client Tests -conn_tests = [test_mqtt_connect_disconnect_esp32spi, test_mqtts_connect_disconnect_esp32spi] - -# Publish/Subscribe tests -pub_sub_tests = [test_sub_pub, test_sub_pub_multiple] - -# The test routine will run the following test(s): -tests = pub_sub_tests - -# Define an ESP32SPI network interface -esp32_cs = DigitalInOut(board.ESP_CS) -esp32_ready = DigitalInOut(board.ESP_BUSY) -esp32_reset = DigitalInOut(board.ESP_RESET) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards - -# Import Test Settings -try: - from secrets import secrets - from settings import settings -except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") - raise - -# Establish WiFi Connection -print("Connecting to AP...") -while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue -print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) - -# Test Harness -start_time = time.monotonic() -for i in enumerate(tests): - print('Running test: ', i) - i[1]() - print('OK!') - time.sleep(settings['test_timeout']) -print('Ran {0} tests in {1}s.'.format(len(tests), time.monotonic() - start_time)) From 8a35e0a8033eaa8ac5df081ee2b2e87ae05c363a Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 13:46:38 -0400 Subject: [PATCH 125/148] update README.md --- README.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 6fd6066..f07d654 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,7 @@ Introduction :target: https://travis-ci.com/adafruit/Adafruit_CircuitPython_MiniMQTT :alt: Build Status -MQTT client library for CircuitPython - +MQTT Client library for CircuitPython. Dependencies ============= @@ -31,9 +30,6 @@ Installing from PyPI .. note:: This library is not available on PyPI yet. Install documentation is included as a standard element. Stay tuned for PyPI availability! -.. todo:: Remove the above note if PyPI version is/will be available at time of release. - If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section. - On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from PyPI `_. To install for current user: @@ -59,7 +55,8 @@ To install in a virtual environment in your current project: Usage Example ============= -.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst. +Please check the `examples folder `_ +for usage examples for this library. Contributing ============ From 64052864cdc1161d1baefc311ea51c8aea967947 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 14:07:42 -0400 Subject: [PATCH 126/148] add logger to requirements --- adafruit_minimqtt.py | 5 ++++- docs/index.rst | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f8d8728..545288c 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -38,6 +38,9 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases +* Adafruit CircuitPython Logger + https://github.com/adafruit/Adafruit_CircuitPython_Logger + """ import struct import time @@ -83,7 +86,7 @@ class MMQTTException(Exception): class MQTT: """ MQTT client interface for CircuitPython devices. - :param socket: Socket object for provided network interface + :param socket: Socket object for network interface. :param str broker: MQTT Broker URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. diff --git a/docs/index.rst b/docs/index.rst index cb0a324..9208b98 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,15 +23,9 @@ Table of Contents .. toctree:: :caption: Tutorials -.. todo:: Add any Learn guide links here. If there are none, then simply delete this todo and leave - the toctree above for use later. - .. toctree:: :caption: Related Products -.. todo:: Add any product links here. If there are none, then simply delete this todo and leave - the toctree above for use later. - .. toctree:: :caption: Other Links From f93feae12099c5d1fd304653439a067990ae9783 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 14:47:42 -0400 Subject: [PATCH 127/148] remove variable timeout, blocking read on wait_for_msg by default --- adafruit_minimqtt.py | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 545288c..f3d2101 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -38,9 +38,6 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases -* Adafruit CircuitPython Logger - https://github.com/adafruit/Adafruit_CircuitPython_Logger - """ import struct import time @@ -78,15 +75,15 @@ const(0x04) : 'Connection Refused - Incorrect username/password', const(0x05) : 'Connection Refused - Unauthorized'} -# pylint: disable=unnecessary-pass class MMQTTException(Exception): """MiniMQTT Exception class.""" + # pylint: disable=unnecessary-pass #pass class MQTT: """ MQTT client interface for CircuitPython devices. - :param socket: Socket object for network interface. + :param socket: Socket object for provided network interface :param str broker: MQTT Broker URL or IP Address. :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. @@ -97,7 +94,7 @@ class MQTT: Defaults to True (port 8883). :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ - # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): # network interface @@ -132,6 +129,7 @@ def __init__(self, socket, broker, port=None, username=None, self._client_id = client_id else: # assign a unique client_id + # pylint: disable=no-member self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) # generated client_id's enforce spec.'s length rules @@ -368,7 +366,7 @@ def publish(self, topic, msg, retain=False, qos=0): sz += 2 assert sz < const(2097152) i = 1 - while sz > 0x7f: + while sz > const(0x7f): pkt[i] = (sz & 0x7f) | const(0x80) sz >>= 7 i += 1 @@ -523,23 +521,23 @@ def unsubscribe(self, topic): if self._logger is not None: self._logger.debug('Waiting for UNSUBACK...') while 1: - self.wait_for_msg() - return_code = self._sock.read(4) - assert return_code[1] == const(0x02) - # [MQTT-3.32] - assert return_code[2] == packet_id_bytes[0] and return_code[3] == packet_id_bytes[1] - for t in topics: - if self.on_unsubscribe is not None: - self.on_unsubscribe(self, self._user_data, t, self._pid) - self._subscribed_topics.remove(t) - return - - def wait_for_msg(self, timeout=0.1): - """Reads and processes network events. Returns network response if successful. - :param float timeout: The time in seconds to wait for network before returning. - Setting this to 0.0 will cause the socket to block until it reads. + op = self.wait_for_msg() + if op == const(176): + return_code = self._sock.read(3) + assert return_code[0] == const(0x02) + # [MQTT-3.32] + assert return_code[1] == packet_id_bytes[0] and return_code[2] == packet_id_bytes[1] + for t in topics: + if self.on_unsubscribe is not None: + self.on_unsubscribe(self, self._user_data, t, self._pid) + self._subscribed_topics.remove(t) + return + + def wait_for_msg(self): + """Reads and processes network events. + Returns response code if successful. """ - self._sock.settimeout(timeout) + self._sock.settimeout(0.0) res = self._sock.read(1) if res in [None, b""]: return None From 4c7fc288da5089f3933ee5196675d4ba34c1c7bd Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 14:49:41 -0400 Subject: [PATCH 128/148] add a long variable timeout to catch brokers like io forcefully disconnecting a client --- adafruit_minimqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index f3d2101..dbfc6c2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -537,7 +537,7 @@ def wait_for_msg(self): """Reads and processes network events. Returns response code if successful. """ - self._sock.settimeout(0.0) + self._sock.settimeout(30.0) res = self._sock.read(1) if res in [None, b""]: return None From 1df31d4346a857c43762b77f59f08fd0bf1f61b4 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 16:00:45 -0400 Subject: [PATCH 129/148] enable and add mock_imports to satisfy sphinx --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4b4fabd..6a64015 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -# autodoc_mock_imports = ["digitalio", "busio"] +autodoc_mock_imports = ["micropython", "microcontroller", "random"] intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)} From 89680ccd938b177914751ec60fa9340278d18c00 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 16:02:59 -0400 Subject: [PATCH 130/148] add logging since it's not in pypi.. --- docs/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6a64015..0fd26d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,11 +16,10 @@ 'sphinx.ext.todo', ] -# TODO: Please Read! # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -autodoc_mock_imports = ["micropython", "microcontroller", "random"] +autodoc_mock_imports = ["micropython", "microcontroller", "random", "adafruit_logging"] intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)} From d2662aef83e537cc2d12d38a4541ec975d7b09c4 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 16:12:26 -0400 Subject: [PATCH 131/148] linting --- ...imqtt_adafruitio.py => minimqtt_adafruitio_wifi.py} | 10 +++++----- examples/minimqtt_simpletest.py | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) rename examples/{minimqtt_adafruitio.py => minimqtt_adafruitio_wifi.py} (92%) mode change 100755 => 100644 diff --git a/examples/minimqtt_adafruitio.py b/examples/minimqtt_adafruitio_wifi.py old mode 100755 new mode 100644 similarity index 92% rename from examples/minimqtt_adafruitio.py rename to examples/minimqtt_adafruitio_wifi.py index 34af273..a51708f --- a/examples/minimqtt_adafruitio.py +++ b/examples/minimqtt_adafruitio_wifi.py @@ -4,7 +4,6 @@ import board import busio from digitalio import DigitalInOut -import neopixel from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_minimqtt import MQTT @@ -52,6 +51,7 @@ def connect_wifi(): print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("IP: ", esp.pretty_ip(esp.ip_address)) +# pylint: disable=unused-argument def on_message(client, topic, message): # This method is called whenever a new message is received # from the server. @@ -62,10 +62,10 @@ def on_message(client, topic, message): # Set up a MiniMQTT Client mqtt_client = MQTT(socket, - secrets['broker'], - username=secrets['user'], - password=secrets['pass'], - esp = esp) + broker = secrets['broker'], + username = secrets['user'], + password = secrets['pass'], + esp = esp) # Attach on_message method to the MQTT Client mqtt_client.on_message = on_message diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index da58ceb..fb9b543 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -53,6 +53,7 @@ def connect_wifi(): print("IP: ", esp.pretty_ip(esp.ip_address)) # MiniMQTT Callback Handlers +# pylint: disable=unused-argument, redefined-outer-name def connect(client, userdata, flags, rc): # This method is called when client.connect() is called. print('Connected to MQTT Broker!') @@ -79,10 +80,10 @@ def publish(client, userdata, topic, pid): # Set up a MiniMQTT Client client = MQTT(socket, - secrets['broker'], - username=secrets['user'], - password=secrets['pass'], - esp = esp) + broker = secrets['broker'], + username = secrets['user'], + password = secrets['pass'], + esp = esp) # Connect callback handlers to client client.on_connect = connect From c9d6b8d4b5c14391a83cd8ed2cd4988968b4113f Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 16:39:32 -0400 Subject: [PATCH 132/148] add non-blocking check for message --- adafruit_minimqtt.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index dbfc6c2..a558e5e 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -533,12 +533,19 @@ def unsubscribe(self, topic): self._subscribed_topics.remove(t) return + def check_for_msg(self): + """Checks if a pending message from the server is avaliable. + If not, returns None. + """ + self._sock.settimeout(0.1) + self.wait_for_msg() + def wait_for_msg(self): """Reads and processes network events. Returns response code if successful. """ - self._sock.settimeout(30.0) res = self._sock.read(1) + self._sock.settimeout(0.0) if res in [None, b""]: return None if res == MQTT_PINGRESP: From a799f542be49f00c350fdcdc138b66f29c9cb5f5 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:01:40 -0400 Subject: [PATCH 133/148] add non-blocking and blocking control loops, make wait_for_msg a private method --- adafruit_minimqtt.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index a558e5e..4fa9897 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -391,7 +391,7 @@ def publish(self, topic, msg, retain=False, qos=0): self._sock.write(msg) if qos == 1: while 1: - op = self.wait_for_msg() + op = self._wait_for_msg() if op == const(0x40): sz = self._sock.read(1) assert sz == b"\x02" @@ -466,7 +466,7 @@ def subscribe(self, topic, qos=0): self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(t, q)) self._sock.write(packet) while 1: - op = self.wait_for_msg() + op = self._wait_for_msg() if op == 0x90: rc = self._sock.read(4) assert rc[1] == packet[2] and rc[2] == packet[3] @@ -521,7 +521,7 @@ def unsubscribe(self, topic): if self._logger is not None: self._logger.debug('Waiting for UNSUBACK...') while 1: - op = self.wait_for_msg() + op = self._wait_for_msg() if op == const(176): return_code = self._sock.read(3) assert return_code[0] == const(0x02) @@ -533,19 +533,39 @@ def unsubscribe(self, topic): self._subscribed_topics.remove(t) return - def check_for_msg(self): - """Checks if a pending message from the server is avaliable. - If not, returns None. + def loop_forever(self): + """Starts a blocking message loop. Use this + method if you want to run a program + forever. Network reconnection is handled within + this call. Your code will not execute anything + below this call. + """ + run = True + while run: + if self._is_connected: + self._wait_for_msg(0.0) + else: + if self._logger is not None: + self._logger.debug('Lost connection, reconnecting and resubscribing...') + self.reconnect(resub_topics=True) + if self._logger is not None: + self._logger.debug('Connection restored, continuing to loop forever...') + + def loop(self): + """Non-blocking message loop. Use this method to + check incoming subscription messages. Does not handle + network reconnection like loop_forever - reconnection must + be handled within your code. """ self._sock.settimeout(0.1) - self.wait_for_msg() + return self._wait_for_msg() - def wait_for_msg(self): + def _wait_for_msg(self, timeout=30): """Reads and processes network events. Returns response code if successful. """ res = self._sock.read(1) - self._sock.settimeout(0.0) + self._sock.settimeout(timeout) if res in [None, b""]: return None if res == MQTT_PINGRESP: From 1be45575fa13497145e2da108d7ee85588a010b8 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:26:35 -0400 Subject: [PATCH 134/148] add two new examples showing blocking vs nonblocking message loops, refactor adafruitiowifi example --- examples/minimqtt_adafruitio_wifi.py | 2 +- examples/minimqtt_pub_sub_blocking.py | 94 ++++++++++++++++++++++ examples/minimqtt_pub_sub_nonblocking.py | 99 ++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 examples/minimqtt_pub_sub_blocking.py create mode 100644 examples/minimqtt_pub_sub_nonblocking.py diff --git a/examples/minimqtt_adafruitio_wifi.py b/examples/minimqtt_adafruitio_wifi.py index a51708f..d523960 100644 --- a/examples/minimqtt_adafruitio_wifi.py +++ b/examples/minimqtt_adafruitio_wifi.py @@ -79,7 +79,7 @@ def on_message(client, topic, message): photocell_val = 0 while True: # Poll the message queue - mqtt_client.wait_for_msg() + mqtt_client.loop() print('Sending photocell value: %d'%photocell_val) mqtt_client.publish(aio_publish_feed, photocell_val) diff --git a/examples/minimqtt_pub_sub_blocking.py b/examples/minimqtt_pub_sub_blocking.py new file mode 100644 index 0000000..9f30cd2 --- /dev/null +++ b/examples/minimqtt_pub_sub_blocking.py @@ -0,0 +1,94 @@ +# CircuitPython MiniMQTT Library +# Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) +import time +import board +import busio +from digitalio import DigitalInOut +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_minimqtt import MQTT + +### WiFi ### + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# If you are using a board with pre-defined ESP32 Pins: +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + +### Adafruit IO Setup ### + +# Setup a feed named `testfeed` for publishing. +default_topic = secrets['user']+'/feeds/testfeed' + +### Code ### + +def connect_wifi(): + print("Connecting to %s..."%secrets['ssid']) + while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue + print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) + print("IP: ", esp.pretty_ip(esp.ip_address)) + +# Define callback methods which are called when events occur +# pylint: disable=unused-argument +def connected(client, userdata, flags, rc): + # This function will be called when the client is connected + # successfully to the broker. + print('Connected to MQTT broker! Listening for topic changes on %s'%default_topic) + # Subscribe to all changes on the default_topic feed. + client.subscribe(default_topic) + +def disconnected(client, userdata, rc): + # This method is called when the client is disconnected + print('Disconnected from MQTT Broker!') + +def message(client, topic, message): + """Method callled when a client's subscribed feed has a new + value. + :param str topic: The topic of the feed with a new value. + :param str message: The new value + """ + print('New message on topic {0}: {1}'.format(topic, message)) + +# Connect to WiFi +connect_wifi() + +# Initialize a MiniMQTT Client +mqtt_client = MQTT(socket, + broker = secrets['broker'], + username = secrets['user'], + password = secrets['pass'], + esp = esp) + +# Setup the callback methods above +mqtt_client.on_connect = connected +mqtt_client.on_disconnect = disconnected +mqtt_client.on_message = message + +# Connect the client to the MQTT broker. +mqtt_client.connect() + +# Start a blocking message loop +# If you only want to listen to incoming messages, +# you'll want to loop_forever as it handles network reconnections +# No code below this line will execute. +mqtt_client.loop_forever() diff --git a/examples/minimqtt_pub_sub_nonblocking.py b/examples/minimqtt_pub_sub_nonblocking.py new file mode 100644 index 0000000..b54bf60 --- /dev/null +++ b/examples/minimqtt_pub_sub_nonblocking.py @@ -0,0 +1,99 @@ +# CircuitPython MiniMQTT Library +# Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) +import time +import board +import busio +from digitalio import DigitalInOut +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_minimqtt import MQTT + +### WiFi ### + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# If you are using a board with pre-defined ESP32 Pins: +esp32_cs = DigitalInOut(board.ESP_CS) +esp32_ready = DigitalInOut(board.ESP_BUSY) +esp32_reset = DigitalInOut(board.ESP_RESET) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) + +spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + +### Adafruit IO Setup ### + +# Setup a feed named `testfeed` for publishing. +default_topic = secrets['user']+'/feeds/testfeed' + +### Code ### + +def connect_wifi(): + print("Connecting to %s..."%secrets['ssid']) + while not esp.is_connected: + try: + esp.connect_AP(secrets['ssid'], secrets['password']) + except RuntimeError as e: + print("could not connect to AP, retrying: ",e) + continue + print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) + print("IP: ", esp.pretty_ip(esp.ip_address)) + +# Define callback methods which are called when events occur +# pylint: disable=unused-argument +def connected(client, userdata, flags, rc): + # This function will be called when the client is connected + # successfully to the broker. + print('Connected to MQTT broker! Listening for topic changes on %s'%default_topic) + # Subscribe to all changes on the default_topic feed. + client.subscribe(default_topic) + +def disconnected(client, userdata, rc): + # This method is called when the client is disconnected + print('Disconnected from MQTT Broker!') + +def message(client, topic, message): + """Method callled when a client's subscribed feed has a new + value. + :param str topic: The topic of the feed with a new value. + :param str message: The new value + """ + print('New message on topic {0}: {1}'.format(topic, message)) + +# Connect to WiFi +connect_wifi() + +# Initialize a MiniMQTT Client +mqtt_client = MQTT(socket, + broker = secrets['broker'], + username = secrets['user'], + password = secrets['pass'], + esp = esp) + +# Setup the callback methods above +mqtt_client.on_connect = connected +mqtt_client.on_disconnect = disconnected +mqtt_client.on_message = message + +# Connect the client to the MQTT broker. +mqtt_client.connect() + +photocell_val = 0 +while True: + # Poll the message queue + mqtt_client.loop() + + # Send a new message + print('Sending photocell value: %d'%photocell_val) + mqtt_client.publish(default_topic, photocell_val) + photocell_val += 1 + time.sleep(0.5) From b0a4e5371d9c3454da5056af4253bb364fd0d5b5 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:29:43 -0400 Subject: [PATCH 135/148] update examples --- examples/minimqtt_adafruitio_wifi.py | 2 -- examples/minimqtt_pub_sub_nonblocking.py | 2 -- examples/minimqtt_simpletest.py | 14 ++++++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/minimqtt_adafruitio_wifi.py b/examples/minimqtt_adafruitio_wifi.py index d523960..9ce1272 100644 --- a/examples/minimqtt_adafruitio_wifi.py +++ b/examples/minimqtt_adafruitio_wifi.py @@ -1,5 +1,3 @@ -# CircuitPython MiniMQTT Library -# Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) import time import board import busio diff --git a/examples/minimqtt_pub_sub_nonblocking.py b/examples/minimqtt_pub_sub_nonblocking.py index b54bf60..8c78bcd 100644 --- a/examples/minimqtt_pub_sub_nonblocking.py +++ b/examples/minimqtt_pub_sub_nonblocking.py @@ -1,5 +1,3 @@ -# CircuitPython MiniMQTT Library -# Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) import time import board import busio diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index fb9b543..fe50c15 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -52,27 +52,29 @@ def connect_wifi(): print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) print("IP: ", esp.pretty_ip(esp.ip_address)) -# MiniMQTT Callback Handlers +# Define callback methods which are called when events occur # pylint: disable=unused-argument, redefined-outer-name def connect(client, userdata, flags, rc): - # This method is called when client.connect() is called. + # This function will be called when the client is connected + # successfully to the broker. print('Connected to MQTT Broker!') print('Flags: {0}\n RC: {1}'.format(flags, rc)) def disconnect(client, userdata, rc): - # This method is called when client.disconnect() is called. + # This method is called when the client disconnects + # from the broker. print('Disconnected from MQTT Broker!') def subscribe(client, userdata, topic, granted_qos): - # This method is called when client.subscribe() is called. + # This method is called when the client subscribes to a new feed. print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos)) def unsubscribe(client, userdata, topic, pid): - # This method is called when client.unsubscribe() is called. + # This method is called when the client unsubscribes from a feed. print('Unsubscribed from {0} with PID {1}'.format(topic, pid)) def publish(client, userdata, topic, pid): - # This method is called when client.publish() is called. + # This method is called when the client publishes data to a feed. print('Published to {0} with PID {1}'.format(topic, pid)) # Connect to WiFi From 0587b664d62dec0de667ab135c943a303acbb74e Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:31:24 -0400 Subject: [PATCH 136/148] lint! --- examples/minimqtt_pub_sub_blocking.py | 3 +-- examples/minimqtt_pub_sub_nonblocking.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/minimqtt_pub_sub_blocking.py b/examples/minimqtt_pub_sub_blocking.py index 9f30cd2..85c8c63 100644 --- a/examples/minimqtt_pub_sub_blocking.py +++ b/examples/minimqtt_pub_sub_blocking.py @@ -1,6 +1,5 @@ # CircuitPython MiniMQTT Library # Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) -import time import board import busio from digitalio import DigitalInOut @@ -49,7 +48,7 @@ def connect_wifi(): print("IP: ", esp.pretty_ip(esp.ip_address)) # Define callback methods which are called when events occur -# pylint: disable=unused-argument +# pylint: disable=unused-argument, redefined-outer-name def connected(client, userdata, flags, rc): # This function will be called when the client is connected # successfully to the broker. diff --git a/examples/minimqtt_pub_sub_nonblocking.py b/examples/minimqtt_pub_sub_nonblocking.py index 8c78bcd..478078f 100644 --- a/examples/minimqtt_pub_sub_nonblocking.py +++ b/examples/minimqtt_pub_sub_nonblocking.py @@ -47,7 +47,7 @@ def connect_wifi(): print("IP: ", esp.pretty_ip(esp.ip_address)) # Define callback methods which are called when events occur -# pylint: disable=unused-argument +# pylint: disable=unused-argument, redefined-outer-name def connected(client, userdata, flags, rc): # This function will be called when the client is connected # successfully to the broker. From 268210e8b4017a5f5734f640e07ec9d700578c4e Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:42:42 -0400 Subject: [PATCH 137/148] fix docstring --- adafruit_minimqtt.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4fa9897..a9e19f2 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -81,22 +81,20 @@ class MMQTTException(Exception): #pass class MQTT: - """ - MQTT client interface for CircuitPython devices. - :param socket: Socket object for provided network interface - :param str broker: MQTT Broker URL or IP Address. - :param int port: Optional port definition, defaults to 8883. - :param str username: Username for broker authentication. - :param str password: Password for broker authentication. - :param ESP_SPIcontrol esp: An ESP network interface object. - :param str client_id: Optional client identifier, defaults to a unique, generated string. - :param bool is_ssl: Sets a secure or insecure connection with the broker. - Defaults to True (port 8883). - :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. - """ - # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name + # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): + """Initializes a MQTT client object. + :param socket: Socket object for provided network interface + :param str broker: MQTT Broker URL or IP Address. + :param int port: Optional port definition, defaults to 8883. + :param str username: Username for broker authentication. + :param str password: Password for broker authentication. + :param ESP_SPIcontrol esp: An ESP network interface object. + :param str client_id: Optional client identifier, defaults to a unique, generated string. + :param bool is_ssl: Sets a secure or insecure connection with the broker. + :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. + """ # network interface self._socket = socket if esp is not None: @@ -129,7 +127,6 @@ def __init__(self, socket, broker, port=None, username=None, self._client_id = client_id else: # assign a unique client_id - # pylint: disable=no-member self._client_id = 'cpy{0}{1}'.format(microcontroller.cpu.uid[randint(0, 15)], randint(0, 9)) # generated client_id's enforce spec.'s length rules From 600e336f312dda822e1b0d2d7c20ece11b37ca90 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 17:53:30 -0400 Subject: [PATCH 138/148] add docstring for class-level? --- adafruit_minimqtt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index a9e19f2..02b6807 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -81,6 +81,7 @@ class MMQTTException(Exception): #pass class MQTT: + """MiniMQTT - a MQTT Client for CircuitPython""" # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): From ec209906eb58d4411bbf05eff19b90a6e75709b3 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 18:12:14 -0400 Subject: [PATCH 139/148] add more docstrings --- adafruit_minimqtt.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 02b6807..cf2a168 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -86,6 +86,7 @@ class MQTT: def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): """Initializes a MQTT client object. + :param socket: Socket object for provided network interface :param str broker: MQTT Broker URL or IP Address. :param int port: Optional port definition, defaults to 8883. @@ -161,7 +162,8 @@ def __exit__(self, exception_type, exception_value, traceback): self.deinit() def deinit(self): - """Disconnects the MQTT client from the broker. + """De-initializes the MQTT client and disconnects from + the mqtt broker. """ self.disconnect() @@ -290,7 +292,8 @@ def connect(self, clean_session=True): return result def disconnect(self): - """Disconnects from the broker. + """Disconnects the MiniMQTT client from + the MQTT broker. """ self.is_connected() if self._logger is not None: @@ -300,13 +303,14 @@ def disconnect(self): self._logger.debug('Closing socket') self._sock.close() self._is_connected = False + self._subscribed_topics = None if self.on_disconnect is not None: self.on_disconnect(self, self._user_data, 0) def ping(self): """Pings the MQTT Broker to confirm if the broker is alive or if there is an active network connection. - Raises a MMQTTException if the broker does not respond with a PINGRESP packet. + """ self.is_connected() if self._logger is not None: @@ -533,10 +537,9 @@ def unsubscribe(self, topic): def loop_forever(self): """Starts a blocking message loop. Use this - method if you want to run a program - forever. Network reconnection is handled within - this call. Your code will not execute anything - below this call. + method if you want to run a program forever. + Network reconnection is handled within this call. + Your code will not execute anything below this call. """ run = True while run: @@ -604,7 +607,7 @@ def _recv_len(self): sh += 7 def _send_str(self, string): - """Writes a provided string to a socket. + """Packs and encodes a string to a socket. :param str string: String to write to the socket. """ self._sock.write(struct.pack("!H", len(string))) @@ -619,7 +622,7 @@ def _check_topic(topic): :param str topic: Topic identifier """ if topic is None: - raise MMQTTException('Topic may not be Nonetype') + raise MMQTTException('Topic may not be NoneType') # [MQTT-4.7.3-1] elif not topic: raise MMQTTException('Topic may not be empty.') From 4e3456cce806b1bf03bdeaf6290c208c9c584583 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 18:22:21 -0400 Subject: [PATCH 140/148] add blank lines --- adafruit_minimqtt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index cf2a168..2cca5f1 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -96,6 +96,7 @@ def __init__(self, socket, broker, port=None, username=None, :param str client_id: Optional client identifier, defaults to a unique, generated string. :param bool is_ssl: Sets a secure or insecure connection with the broker. :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. + """ # network interface self._socket = socket @@ -334,14 +335,17 @@ def publish(self, topic, msg, retain=False, qos=0): Example of sending an integer, 3, to the broker on topic 'piVal'. .. code-block:: python + mqtt_client.publish('topics/piVal', 3) Example of sending a float, 3.14, to the broker on topic 'piVal'. .. code-block:: python + mqtt_client.publish('topics/piVal', 3.14) Example of sending a string, 'threepointonefour', to the broker on topic piVal. .. code-block:: python + mqtt_client.publish('topics/piVal', 'threepointonefour') """ @@ -419,18 +423,22 @@ def subscribe(self, topic, qos=0): Example of subscribing a topic string. .. code-block:: python + mqtt_client.subscribe('topics/ledState') Example of subscribing to a topic and setting the qos level to 1. .. code-block:: python + mqtt_client.subscribe('topics/ledState', 1) Example of subscribing to topic string and setting qos level to 1, as a tuple. .. code-block:: python + mqtt_client.subscribe(('topics/ledState', 1)) Example of subscribing to multiple topics with different qos levels. .. code-block:: python + mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)]) """ @@ -487,10 +495,12 @@ def unsubscribe(self, topic): Example of unsubscribing from a topic string. .. code-block:: python + mqtt_client.unsubscribe('topics/ledState') Example of unsubscribing from multiple topics. .. code-block:: python + mqtt_client.unsubscribe([('topics/ledState'), ('topics/servoAngle')]) """ From 11776316c8656cf8f399e0c1b3c0ba9ccd5a5e82 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 10 Jul 2019 18:48:03 -0400 Subject: [PATCH 141/148] pylint!!! --- adafruit_minimqtt.py | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2cca5f1..4691f24 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -81,23 +81,20 @@ class MMQTTException(Exception): #pass class MQTT: - """MiniMQTT - a MQTT Client for CircuitPython""" + """MQTT Client for CircuitPython + :param socket: Socket object for provided network interface + :param str broker: MQTT Broker URL or IP Address. + :param int port: Optional port definition, defaults to 8883. + :param str username: Username for broker authentication. + :param str password: Password for broker authentication. + :param ESP_SPIcontrol esp: An ESP network interface object. + :param str client_id: Optional client identifier, defaults to a unique, generated string. + :param bool is_ssl: Sets a secure or insecure connection with the broker. + :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. + """ # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member def __init__(self, socket, broker, port=None, username=None, password=None, esp=None, client_id=None, is_ssl=True, log=False): - """Initializes a MQTT client object. - - :param socket: Socket object for provided network interface - :param str broker: MQTT Broker URL or IP Address. - :param int port: Optional port definition, defaults to 8883. - :param str username: Username for broker authentication. - :param str password: Password for broker authentication. - :param ESP_SPIcontrol esp: An ESP network interface object. - :param str client_id: Optional client identifier, defaults to a unique, generated string. - :param bool is_ssl: Sets a secure or insecure connection with the broker. - :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. - - """ # network interface self._socket = socket if esp is not None: @@ -189,8 +186,7 @@ def last_will(self, topic=None, message=None, qos=0, retain=False): def reconnect(self, retries=30, resub_topics=True): """Attempts to reconnect to the MQTT broker. :param int retries: Amount of retries before resetting the network interface. - :param bool resub_topics: Client resubscribes to previously subscribed topics upon - a successful reconnection. + :param bool resub_topics: Resubscribe to previously subscribed topics. """ retries = 0 while not self._is_connected: @@ -217,8 +213,7 @@ def reconnect(self, retries=30, resub_topics=True): # pylint: disable=too-many-branches, too-many-statements def connect(self, clean_session=True): """Initiates connection with the MQTT Broker. - :param bool clean_session: Establishes a persistent session - with the broker. Defaults to a non-persistent session. + :param bool clean_session: Establishes a persistent session. """ self._set_interface() if self._logger is not None: @@ -293,8 +288,7 @@ def connect(self, clean_session=True): return result def disconnect(self): - """Disconnects the MiniMQTT client from - the MQTT broker. + """Disconnects the MiniMQTT client from the MQTT broker. """ self.is_connected() if self._logger is not None: @@ -311,7 +305,6 @@ def disconnect(self): def ping(self): """Pings the MQTT Broker to confirm if the broker is alive or if there is an active network connection. - """ self.is_connected() if self._logger is not None: @@ -418,8 +411,7 @@ def subscribe(self, topic, qos=0): :param str topic: Unique MQTT topic identifier. :param int qos: Quality of Service level for the topic, defaults to zero. :param tuple topic: Tuple containing topic identifier strings and qos level integers. - :param list topic: List of tuples containing topic identifier strings and - qos level integers. + :param list topic: List of tuples containing topic identifier strings and qos. Example of subscribing a topic string. .. code-block:: python @@ -653,7 +645,7 @@ def _check_qos(qos_level): def _set_interface(self): """Sets a desired network hardware interface. - Note: The network hardware must be set in init + The network hardware must be set in init prior to calling this method. """ if self._esp: @@ -692,8 +684,7 @@ def attach_logger(self, logger_name='log'): def set_logger_level(self, log_level): """Sets the level of the logger, if defined during init. - :param string log_level: Level of logging to output to the REPL. Accepted - levels are DEBUG, INFO, WARNING, EROR, and CRITICIAL. + :param string log_level: Level of logging to output to the REPL. """ if self._logger is None: raise MMQTTException('No logger attached - did you create it during initialization?') From e7ed0fa21c4fd2ab3336dfc8e58574230016bc46 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 12:29:11 -0400 Subject: [PATCH 142/148] esp32spi -> networkmanager object instead --- adafruit_minimqtt.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 4691f24..41b0228 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -87,26 +87,24 @@ class MQTT: :param int port: Optional port definition, defaults to 8883. :param str username: Username for broker authentication. :param str password: Password for broker authentication. - :param ESP_SPIcontrol esp: An ESP network interface object. + :param network_manager: NetworkManager object, such as WiFiManager from ESPSPI_WiFiManager. :param str client_id: Optional client identifier, defaults to a unique, generated string. :param bool is_ssl: Sets a secure or insecure connection with the broker. :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO. """ # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member def __init__(self, socket, broker, port=None, username=None, - password=None, esp=None, client_id=None, is_ssl=True, log=False): - # network interface + password=None, network_manager=None, client_id=None, is_ssl=True, log=False): + # network management self._socket = socket - if esp is not None: - if hasattr(esp, '_gpio0'): - self._esp = esp - else: - raise MMQTTException('Invalid ESP32SPI object provided.') + network_manager_type = str(type(network_manager)) + if ('ESPSPI_WiFiManager' in network_manager_type): + self._wifi = network_manager else: - raise NotImplementedError('MiniMQTT currently supports an ESP32SPI network interface.') + raise TypeError("This library requires a NetworkManager object.") # broker try: # set broker IP - self.broker = self._esp.unpretty_ip(broker) + self.broker = self._wifi.esp.unpretty_ip(broker) except ValueError: # set broker URL self.broker = broker # port/ssl @@ -648,10 +646,10 @@ def _set_interface(self): The network hardware must be set in init prior to calling this method. """ - if self._esp: - self._socket.set_interface(self._esp) + if self._wifi: + self._socket.set_interface(self._wifi.esp) else: - raise TypeError('network interface required.') + raise TypeError('Network Manager Required.') def is_connected(self): """Returns MQTT client session status as True if connected, raises From 6d928e4535ca026c5f1c03d1eb3b3e6fa7c85d12 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 12:36:07 -0400 Subject: [PATCH 143/148] drop esp32spi object in favor of wifimanager, update all examples --- adafruit_minimqtt.py | 2 +- examples/minimqtt_adafruitio_wifi.py | 29 ++++++++++-------- examples/minimqtt_pub_sub_blocking.py | 31 ++++++++++--------- examples/minimqtt_pub_sub_nonblocking.py | 32 ++++++++++---------- examples/minimqtt_simpletest.py | 38 +++++++++++++----------- 5 files changed, 71 insertions(+), 61 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 41b0228..bc09774 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -98,7 +98,7 @@ def __init__(self, socket, broker, port=None, username=None, # network management self._socket = socket network_manager_type = str(type(network_manager)) - if ('ESPSPI_WiFiManager' in network_manager_type): + if 'ESPSPI_WiFiManager' in network_manager_type: self._wifi = network_manager else: raise TypeError("This library requires a NetworkManager object.") diff --git a/examples/minimqtt_adafruitio_wifi.py b/examples/minimqtt_adafruitio_wifi.py index 9ce1272..f44828d 100644 --- a/examples/minimqtt_adafruitio_wifi.py +++ b/examples/minimqtt_adafruitio_wifi.py @@ -1,8 +1,10 @@ import time import board import busio +import neopixel from digitalio import DigitalInOut from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_minimqtt import MQTT @@ -27,6 +29,18 @@ spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +# Uncomment below for an externally defined RGB LED +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) +wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) ### Adafruit IO Setup ### @@ -38,17 +52,6 @@ ### Code ### -def connect_wifi(): - print("Connecting to %s..."%secrets['ssid']) - while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue - print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) - print("IP: ", esp.pretty_ip(esp.ip_address)) - # pylint: disable=unused-argument def on_message(client, topic, message): # This method is called whenever a new message is received @@ -56,14 +59,14 @@ def on_message(client, topic, message): print('New message on topic {0}: {1}'.format(topic, message)) # Connect to WiFi -connect_wifi() +wifi.connect() # Set up a MiniMQTT Client mqtt_client = MQTT(socket, broker = secrets['broker'], username = secrets['user'], password = secrets['pass'], - esp = esp) + network_manager = wifi) # Attach on_message method to the MQTT Client mqtt_client.on_message = on_message diff --git a/examples/minimqtt_pub_sub_blocking.py b/examples/minimqtt_pub_sub_blocking.py index 85c8c63..452af72 100644 --- a/examples/minimqtt_pub_sub_blocking.py +++ b/examples/minimqtt_pub_sub_blocking.py @@ -2,8 +2,10 @@ # Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) import board import busio +import neopixel from digitalio import DigitalInOut from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_minimqtt import MQTT @@ -28,6 +30,18 @@ spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +# Uncomment below for an externally defined RGB LED +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) +wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) ### Adafruit IO Setup ### @@ -36,17 +50,6 @@ ### Code ### -def connect_wifi(): - print("Connecting to %s..."%secrets['ssid']) - while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue - print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) - print("IP: ", esp.pretty_ip(esp.ip_address)) - # Define callback methods which are called when events occur # pylint: disable=unused-argument, redefined-outer-name def connected(client, userdata, flags, rc): @@ -69,14 +72,14 @@ def message(client, topic, message): print('New message on topic {0}: {1}'.format(topic, message)) # Connect to WiFi -connect_wifi() +wifi.connect() -# Initialize a MiniMQTT Client +# Set up a MiniMQTT Client mqtt_client = MQTT(socket, broker = secrets['broker'], username = secrets['user'], password = secrets['pass'], - esp = esp) + network_manager = wifi) # Setup the callback methods above mqtt_client.on_connect = connected diff --git a/examples/minimqtt_pub_sub_nonblocking.py b/examples/minimqtt_pub_sub_nonblocking.py index 478078f..1ab4c38 100644 --- a/examples/minimqtt_pub_sub_nonblocking.py +++ b/examples/minimqtt_pub_sub_nonblocking.py @@ -1,8 +1,10 @@ import time import board import busio +import neopixel from digitalio import DigitalInOut from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_minimqtt import MQTT @@ -27,6 +29,18 @@ spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +# Uncomment below for an externally defined RGB LED +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) +wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) ### Adafruit IO Setup ### @@ -34,18 +48,6 @@ default_topic = secrets['user']+'/feeds/testfeed' ### Code ### - -def connect_wifi(): - print("Connecting to %s..."%secrets['ssid']) - while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue - print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) - print("IP: ", esp.pretty_ip(esp.ip_address)) - # Define callback methods which are called when events occur # pylint: disable=unused-argument, redefined-outer-name def connected(client, userdata, flags, rc): @@ -68,14 +70,14 @@ def message(client, topic, message): print('New message on topic {0}: {1}'.format(topic, message)) # Connect to WiFi -connect_wifi() +wifi.connect() -# Initialize a MiniMQTT Client +# Set up a MiniMQTT Client mqtt_client = MQTT(socket, broker = secrets['broker'], username = secrets['user'], password = secrets['pass'], - esp = esp) + network_manager = wifi) # Setup the callback methods above mqtt_client.on_connect = connected diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index fe50c15..428b95b 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -1,7 +1,9 @@ import board import busio +import neopixel from digitalio import DigitalInOut from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_minimqtt import MQTT @@ -27,6 +29,18 @@ spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +"""Use below for Most Boards""" +status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards +"""Uncomment below for ItsyBitsy M4""" +# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +# Uncomment below for an externally defined RGB LED +# import adafruit_rgbled +# from adafruit_esp32spi import PWMOut +# RED_LED = PWMOut.PWMOut(esp, 26) +# GREEN_LED = PWMOut.PWMOut(esp, 27) +# BLUE_LED = PWMOut.PWMOut(esp, 25) +# status_light = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) +wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) ### Topic Setup ### @@ -40,18 +54,6 @@ ### Code ### -def connect_wifi(): - # Connects the ESP32 to WiFi - print("Connecting to %s..."%secrets['ssid']) - while not esp.is_connected: - try: - esp.connect_AP(secrets['ssid'], secrets['password']) - except RuntimeError as e: - print("could not connect to AP, retrying: ",e) - continue - print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi) - print("IP: ", esp.pretty_ip(esp.ip_address)) - # Define callback methods which are called when events occur # pylint: disable=unused-argument, redefined-outer-name def connect(client, userdata, flags, rc): @@ -78,14 +80,14 @@ def publish(client, userdata, topic, pid): print('Published to {0} with PID {1}'.format(topic, pid)) # Connect to WiFi -connect_wifi() +wifi.connect() # Set up a MiniMQTT Client -client = MQTT(socket, - broker = secrets['broker'], - username = secrets['user'], - password = secrets['pass'], - esp = esp) +client = MQTT(socket, + broker = secrets['broker'], + username = secrets['user'], + password = secrets['pass'], + network_manager = wifi) # Connect callback handlers to client client.on_connect = connect From 4cade9492136fa5fb71c87687a91c814e6f3e928 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 13:43:10 -0400 Subject: [PATCH 144/148] add a wait_for_msg call to catch any async publish before pingresp --- adafruit_minimqtt.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index bc09774..2be2c0d 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -310,9 +310,13 @@ def ping(self): self._sock.write(MQTT_PINGREQ) if self._logger is not None: self._logger.debug('Checking PINGRESP') - ping_resp = self._sock.read(2) - if ping_resp[0] != MQTT_PINGRESP or ping_resp[1] != const(0x00): - raise MMQTTException('PINGRESP not returned from broker.') + while 1: + op = self._wait_for_msg(0.5) + if op == const(208): + ping_resp = self._sock.read(2) + if ping_resp[0] != const(0x00): + raise MMQTTException('PINGRESP not returned from broker.') + return # pylint: disable=too-many-branches, too-many-statements def publish(self, topic, msg, retain=False, qos=0): From 5c15d23868cc23cc20fee37b729b7065705dab47 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 13:52:31 -0400 Subject: [PATCH 145/148] check for a PUBLISH before a CONNACK --- adafruit_minimqtt.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 2be2c0d..18527ba 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -275,15 +275,18 @@ def connect(self, clean_session=True): self._send_str(self._pass) if self._logger is not None: self._logger.debug('Receiving CONNACK packet from broker') - rc = self._sock.read(4) - assert rc[0] == const(0x20) and rc[1] == const(0x02) - if rc[3] != 0: - raise MMQTTException(CONNACK_ERRORS[rc[3]]) - self._is_connected = True - result = rc[2] & 1 - if self.on_connect is not None: - self.on_connect(self, self._user_data, result, rc[3]) - return result + while True: + op = self._wait_for_msg() + if op == 32: + rc = self._sock.read(3) + assert rc[0] == const(0x02) + if rc[2] != const(0x00): + raise MMQTTException(CONNACK_ERRORS[rc[3]]) + self._is_connected = True + result = rc[0] & 1 + if self.on_connect is not None: + self.on_connect(self, self._user_data, result, rc[3]) + return result def disconnect(self): """Disconnects the MiniMQTT client from the MQTT broker. From 07102d874dade78f0770c9e8a2b8fef089c5e5fb Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 15:15:46 -0400 Subject: [PATCH 146/148] fix out of bytestring error --- adafruit_minimqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index 18527ba..a4f91f7 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -285,7 +285,7 @@ def connect(self, clean_session=True): self._is_connected = True result = rc[0] & 1 if self.on_connect is not None: - self.on_connect(self, self._user_data, result, rc[3]) + self.on_connect(self, self._user_data, result, rc[2]) return result def disconnect(self): From ceb26b67f2e74214841562aace774f9b228f0d43 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 11 Jul 2019 18:04:51 -0400 Subject: [PATCH 147/148] fix pylint errors --- examples/minimqtt_adafruitio_wifi.py | 2 +- examples/minimqtt_pub_sub_blocking.py | 2 +- examples/minimqtt_pub_sub_nonblocking.py | 2 +- examples/minimqtt_simpletest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/minimqtt_adafruitio_wifi.py b/examples/minimqtt_adafruitio_wifi.py index f44828d..f93cd15 100644 --- a/examples/minimqtt_adafruitio_wifi.py +++ b/examples/minimqtt_adafruitio_wifi.py @@ -1,8 +1,8 @@ import time import board import busio -import neopixel from digitalio import DigitalInOut +import neopixel from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket diff --git a/examples/minimqtt_pub_sub_blocking.py b/examples/minimqtt_pub_sub_blocking.py index 452af72..5831466 100644 --- a/examples/minimqtt_pub_sub_blocking.py +++ b/examples/minimqtt_pub_sub_blocking.py @@ -2,8 +2,8 @@ # Adafruit IO SSL/TLS Example for WiFi (ESP32SPI) import board import busio -import neopixel from digitalio import DigitalInOut +import neopixel from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket diff --git a/examples/minimqtt_pub_sub_nonblocking.py b/examples/minimqtt_pub_sub_nonblocking.py index 1ab4c38..fdde9a7 100644 --- a/examples/minimqtt_pub_sub_nonblocking.py +++ b/examples/minimqtt_pub_sub_nonblocking.py @@ -1,8 +1,8 @@ import time import board import busio -import neopixel from digitalio import DigitalInOut +import neopixel from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket diff --git a/examples/minimqtt_simpletest.py b/examples/minimqtt_simpletest.py index 428b95b..a600c69 100644 --- a/examples/minimqtt_simpletest.py +++ b/examples/minimqtt_simpletest.py @@ -1,7 +1,7 @@ import board import busio -import neopixel from digitalio import DigitalInOut +import neopixel from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager import adafruit_esp32spi.adafruit_esp32spi_socket as socket From 1ae11a8f8998ebe2491f44f662be4d7d569a3cea Mon Sep 17 00:00:00 2001 From: brentru Date: Tue, 16 Jul 2019 12:28:18 -0400 Subject: [PATCH 148/148] changes per @ladyada's review --- adafruit_minimqtt.py | 45 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/adafruit_minimqtt.py b/adafruit_minimqtt.py index a4f91f7..5959dab 100644 --- a/adafruit_minimqtt.py +++ b/adafruit_minimqtt.py @@ -201,7 +201,8 @@ def reconnect(self, retries=30, resub_topics=True): feed = self._subscribed_topics.pop() self.subscribe(feed) except OSError as e: - print('Failed to connect to the broker, retrying\n', e) + if self._logger is not None: + self._logger.debug('Lost connection, reconnecting and resubscribing...', e) retries += 1 if retries >= 30: retries = 0 @@ -279,8 +280,8 @@ def connect(self, clean_session=True): op = self._wait_for_msg() if op == 32: rc = self._sock.read(3) - assert rc[0] == const(0x02) - if rc[2] != const(0x00): + assert rc[0] == 0x02 + if rc[2] != 0x00: raise MMQTTException(CONNACK_ERRORS[rc[3]]) self._is_connected = True result = rc[0] & 1 @@ -313,11 +314,11 @@ def ping(self): self._sock.write(MQTT_PINGREQ) if self._logger is not None: self._logger.debug('Checking PINGRESP') - while 1: + while True: op = self._wait_for_msg(0.5) - if op == const(208): + if op == 208: ping_resp = self._sock.read(2) - if ping_resp[0] != const(0x00): + if ping_resp[0] != 0x00: raise MMQTTException('PINGRESP not returned from broker.') return @@ -368,10 +369,10 @@ def publish(self, topic, msg, retain=False, qos=0): sz = 2 + len(topic) + len(msg) if qos > 0: sz += 2 - assert sz < const(2097152) + assert sz < 2097152 i = 1 - while sz > const(0x7f): - pkt[i] = (sz & 0x7f) | const(0x80) + while sz > 0x7f: + pkt[i] = (sz & 0x7f) | 0x80 sz >>= 7 i += 1 pkt[i] = sz @@ -394,13 +395,13 @@ def publish(self, topic, msg, retain=False, qos=0): self._logger.debug('Sending PUBACK') self._sock.write(msg) if qos == 1: - while 1: + while True: op = self._wait_for_msg() - if op == const(0x40): + if op == 0x40: sz = self._sock.read(1) assert sz == b"\x02" rcv_pid = self._sock.read(2) - rcv_pid = rcv_pid[0] << const(0x08) | rcv_pid[1] + rcv_pid = rcv_pid[0] << 0x08 | rcv_pid[1] if pid == rcv_pid: if self.on_publish is not None: self.on_publish(self, self._user_data, topic, rcv_pid) @@ -472,7 +473,7 @@ def subscribe(self, topic, qos=0): for t, q in topics: self._logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(t, q)) self._sock.write(packet) - while 1: + while True: op = self._wait_for_msg() if op == 0x90: rc = self._sock.read(4) @@ -529,11 +530,11 @@ def unsubscribe(self, topic): self._sock.write(packet) if self._logger is not None: self._logger.debug('Waiting for UNSUBACK...') - while 1: + while True: op = self._wait_for_msg() - if op == const(176): + if op == 176: return_code = self._sock.read(3) - assert return_code[0] == const(0x02) + assert return_code[0] == 0x02 # [MQTT-3.32] assert return_code[1] == packet_id_bytes[0] and return_code[2] == packet_id_bytes[1] for t in topics: @@ -580,7 +581,7 @@ def _wait_for_msg(self, timeout=30): sz = self._sock.read(1)[0] assert sz == 0 return None - if res[0] & const(0xf0) != const(0x30): + if res[0] & 0xf0 != 0x30: return res[0] sz = self._recv_len() topic_len = self._sock.read(2) @@ -588,14 +589,14 @@ def _wait_for_msg(self, timeout=30): topic = self._sock.read(topic_len) topic = str(topic, 'utf-8') sz -= topic_len + 2 - if res[0] & const(0x06): + if res[0] & 0x06: pid = self._sock.read(2) - pid = pid[0] << const(0x08) | pid[1] - sz -= const(0x02) + pid = pid[0] << 0x08 | pid[1] + sz -= 0x02 msg = self._sock.read(sz) if self.on_message is not None: self.on_message(self, topic, str(msg, 'utf-8')) - if res[0] & const(0x06) == const(0x02): + if res[0] & 0x06 == 0x02: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) self._sock.write(pkt) @@ -606,7 +607,7 @@ def _wait_for_msg(self, timeout=30): def _recv_len(self): n = 0 sh = 0 - while 1: + while True: b = self._sock.read(1)[0] n |= (b & 0x7f) << sh if not b & 0x80: