diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..21c125c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +.py text eol=lf +.rst text eol=lf +.txt text eol=lf +.yaml text eol=lf +.toml text eol=lf +.license text eol=lf +.md text eol=lf diff --git a/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md new file mode 100644 index 0000000..8de294e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2021 Adafruit Industries +# +# SPDX-License-Identifier: MIT + +Thank you for contributing! Before you submit a pull request, please read the following. + +Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html + +If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs + +Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code + +Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f539d8..041a337 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT name: Build CI @@ -10,48 +10,5 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install dependencies - # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) - run: | - source actions-ci/install.sh - - name: Pip install pylint, Sphinx, pre-commit - run: | - pip install --force-reinstall pylint Sphinx sphinx-rtd-theme pre-commit - - name: Library version - run: git describe --dirty --always --tags - - name: Pre-commit hooks - run: | - pre-commit run --all-files - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Build docs - working-directory: docs - run: sphinx-build -E -W -b html . _build/html + - name: Run Build CI workflow + uses: adafruit/workflows-circuitpython-libs/build@main diff --git a/.github/workflows/failure-help-text.yml b/.github/workflows/failure-help-text.yml new file mode 100644 index 0000000..0b1194f --- /dev/null +++ b/.github/workflows/failure-help-text.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: Failure help text + +on: + workflow_run: + workflows: ["Build CI"] + types: + - completed + +jobs: + post-help: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: Post comment to help + uses: adafruit/circuitpython-action-library-ci-failed@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c872723..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -name: Release Actions - -on: - release: - types: [published] - -jobs: - upload-release-assets: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install deps - run: | - source actions-ci/install.sh - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Upload Release Assets - # the 'official' actions version does not yet support dynamically - # supplying asset names to upload. @csexton's version chosen based on - # discussion in the issue below, as its the simplest to implement and - # allows for selecting files with a pattern. - # https://github.com/actions/upload-release-asset/issues/4 - #uses: actions/upload-release-asset@v1.0.1 - uses: csexton/release-asset-action@master - with: - pattern: "bundles/*" - github-token: ${{ secrets.GITHUB_TOKEN }} - - upload-pypi: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Check For setup.py - id: need-pypi - run: | - echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) - - name: Set up Python - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - env: - TWINE_USERNAME: ${{ secrets.pypi_username }} - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - python setup.py sdist - twine upload dist/* diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 0000000..9acec60 --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: GitHub Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run GitHub Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-gh@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + upload-url: ${{ github.event.release.upload_url }} diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..65775b7 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: PyPI Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run PyPI Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-pypi@main + with: + pypi-username: ${{ secrets.pypi_username }} + pypi-password: ${{ secrets.pypi_password }} diff --git a/.gitignore b/.gitignore index 7455881..db3d538 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,48 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT +# Do not include files and directories created by your personal work environment, such as the IDE +# you use, except for those already listed here. Pull requests including changes to this file will +# not be accepted. + +# This .gitignore file contains rules for files generated by working with CircuitPython libraries, +# including building Sphinx, testing with pip, and creating a virual environment, as well as the +# MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. + +# If you find that there are files being generated on your machine that should not be included in +# your git commit, you should create a .gitignore_global file on your computer to include the +# files created by your personal setup. To do so, follow the two steps below. + +# First, create a file called .gitignore_global somewhere convenient for you, and add rules for +# the files you want to exclude from git commits. + +# Second, configure Git to use the exclude file for all Git repositories by running the +# following via commandline, replacing "path/to/your/" with the actual path to your newly created +# .gitignore_global file: +# git config --global core.excludesfile path/to/your/.gitignore_global + +# CircuitPython-specific files *.mpy -.idea + +# Python-specific files __pycache__ -_build *.pyc + +# Sphinx build-specific files +_build + +# This file results from running `pip -e install .` in a local repository +*.egg-info + +# Virtual environment-specific files .env -bundles +.venv + +# MacOS-specific files *.DS_Store -.eggs -dist -**/*.egg-info + +# IDE-specific files +.idea +.vscode +*~ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 354c761..ff19dde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,34 +1,21 @@ -# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries # # SPDX-License-Identifier: Unlicense repos: -- repo: https://github.com/python/black - rev: 20.8b1 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 hooks: - - id: black -- repo: https://github.com/fsfe/reuse-tool - rev: v0.12.1 + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 hooks: - - id: reuse -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + - id: ruff-format + - id: ruff + args: ["--fix"] + - repo: https://github.com/fsfe/reuse-tool + rev: v3.0.1 hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/pycqa/pylint - rev: pylint-2.7.1 - hooks: - - id: pylint - name: pylint (library code) - types: [python] - exclude: "^(docs/|examples/|setup.py$)" -- repo: local - hooks: - - id: pylint_examples - name: pylint (examples code) - description: Run pylint rules on "examples/*.py" files - entry: /usr/bin/env bash -c - args: ['([[ ! -d "examples" ]] || for example in $(find . -path "./examples/*.py"); do pylint --disable=missing-docstring,invalid-name $example; done)'] - language: system + - id: reuse diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index ab4d457..0000000 --- a/.pylintrc +++ /dev/null @@ -1,436 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -[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 - -# 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,bad-continuation - -# 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=yes - -# Minimum lines number of a similarity. -min-similarity-lines=12 - - -[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.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..255dafd --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + os: ubuntu-lts-latest + tools: + python: "3" + +python: + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 011c3a9..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -python: - version: 3 -requirements_file: requirements.txt diff --git a/README.rst b/README.rst index bc48ff4..cb21217 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,10 @@ Introduction ============ .. image:: https://readthedocs.org/projects/adafruit-circuitpython-esp32spi/badge/?version=latest - :target: https://circuitpython.readthedocs.io/projects/esp32spi/en/latest/ + :target: https://docs.circuitpython.org/projects/esp32spi/en/latest/ :alt: Documentation Status -.. image:: https://img.shields.io/discord/327254708534116352.svg +.. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg :target: https://adafru.it/discord :alt: Discord @@ -13,6 +13,10 @@ Introduction :target: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/actions/ :alt: Build Status +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Code Style: Ruff + CircuitPython driver library for using ESP32 as WiFi co-processor using SPI. The companion firmware `is available on GitHub `_. Please be sure to check the example code for @@ -24,7 +28,9 @@ Dependencies This driver depends on: * `Adafruit CircuitPython `_ -* `Bus Device `_ +* `Adafruit Bus Device `_ +* `Adafruit CircuitPython ConnectionManager `_ +* `Adafruit CircuitPython Requests `_ Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading @@ -50,8 +56,8 @@ 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 + python3 -m venv .venv + source .venv/bin/activate pip3 install adafruit-circuitpython-esp32spi Usage Example @@ -59,14 +65,16 @@ Usage Example Check the examples folder for various demos for connecting and fetching data! +Documentation +============= + +API documentation for this library can be found on `Read the Docs `_. + +For information on building library documentation, please check out `this guide `_. + Contributing ============ Contributions are welcome! Please read our `Code of Conduct -`_ +`_ before contributing to help this project stay welcoming. - -Documentation -============= - -For information on building library documentation, please check out `this guide `_. diff --git a/adafruit_esp32spi/PWMOut.py b/adafruit_esp32spi/PWMOut.py index f116103..94abfce 100755 --- a/adafruit_esp32spi/PWMOut.py +++ b/adafruit_esp32spi/PWMOut.py @@ -26,9 +26,7 @@ class PWMOut: [0, 1, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33] ) - def __init__( - self, esp, pwm_pin, *, frequency=500, duty_cycle=0, variable_frequency=False - ): + def __init__(self, esp, pwm_pin, *, frequency=500, duty_cycle=0, variable_frequency=False): if pwm_pin in self.ESP32_PWM_PINS: self._pwm_pin = pwm_pin else: diff --git a/adafruit_esp32spi/__init__.py b/adafruit_esp32spi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index 76193a8..f0ff6b6 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -21,21 +21,27 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases -* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +* Adafruit's Bus Device library: + https://github.com/adafruit/Adafruit_CircuitPython_BusDevice """ import struct import time -from micropython import const -from digitalio import Direction +import warnings + from adafruit_bus_device.spi_device import SPIDevice +from digitalio import Direction +from micropython import const -__version__ = "0.0.0-auto.0" +__version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI.git" _SET_NET_CMD = const(0x10) _SET_PASSPHRASE_CMD = const(0x11) +_SET_IP_CONFIG = const(0x14) +_SET_DNS_CONFIG = const(0x15) +_SET_HOSTNAME = const(0x16) _SET_AP_NET_CMD = const(0x18) _SET_AP_PASSPHRASE_CMD = const(0x19) _SET_DEBUG_CMD = const(0x1A) @@ -66,6 +72,7 @@ _START_SCAN_NETWORKS = const(0x36) _GET_FW_VERSION_CMD = const(0x37) _SEND_UDP_DATA_CMD = const(0x39) +_GET_REMOTE_DATA_CMD = const(0x3A) _GET_TIME = const(0x3B) _GET_IDX_BSSID_CMD = const(0x3C) _GET_IDX_CHAN_CMD = const(0x3D) @@ -124,7 +131,94 @@ ADC_ATTEN_DB_11 = const(3) -class ESP_SPIcontrol: # pylint: disable=too-many-public-methods, too-many-instance-attributes +class Network: + """A wifi network provided by a nearby access point.""" + + def __init__( + self, + esp_spi_control=None, + raw_ssid=None, + raw_bssid=None, + raw_rssi=None, + raw_channel=None, + raw_country=None, + raw_authmode=None, + ): + self._esp_spi_control = esp_spi_control + self._raw_ssid = raw_ssid + self._raw_bssid = raw_bssid + self._raw_rssi = raw_rssi + self._raw_channel = raw_channel + self._raw_country = raw_country + self._raw_authmode = raw_authmode + + def _get_response(self, cmd): + respose = self._esp_spi_control._send_command_get_response(cmd, [b"\xff"]) + return respose[0] + + @property + def ssid(self): + """String id of the network""" + if self._raw_ssid: + response = self._raw_ssid + else: + response = self._get_response(_GET_CURR_SSID_CMD) + return response.decode("utf-8") + + @property + def bssid(self): + """BSSID of the network (usually the AP’s MAC address)""" + if self._raw_bssid: + response = self._raw_bssid + else: + response = self._get_response(_GET_CURR_BSSID_CMD) + return bytes(response) + + @property + def rssi(self): + """Signal strength of the network""" + if self._raw_bssid: + response = self._raw_rssi + else: + response = self._get_response(_GET_CURR_RSSI_CMD) + return struct.unpack("= 3: print() - # pylint: disable=too-many-branches def _send_command(self, cmd, params=None, *, param_len_16=False): """Send over a command with a list of parameters""" if not params: @@ -235,15 +336,11 @@ def _send_command(self, cmd, params=None, *, param_len_16=False): if self._ready.value: # ok ready to send! break else: - raise RuntimeError("ESP32 timed out on SPI select") - spi.write( - self._sendbuf, start=0, end=packet_len - ) # pylint: disable=no-member + raise TimeoutError("ESP32 timed out on SPI select") + spi.write(self._sendbuf, start=0, end=packet_len) if self._debug >= 3: print("Wrote: ", [hex(b) for b in self._sendbuf[0:packet_len]]) - # pylint: disable=too-many-branches - def _read_byte(self, spi): """Read one byte from SPI""" spi.readinto(self._pbuf) @@ -260,21 +357,21 @@ def _read_bytes(self, spi, buffer, start=0, end=None): print("\t\tRead:", [hex(i) for i in buffer]) def _wait_spi_char(self, spi, desired): - """Read a byte with a time-out, and if we get it, check that its what we expect""" - times = time.monotonic() - while (time.monotonic() - times) < 0.1: + """Read a byte with a retry loop, and if we get it, check that its what we expect""" + for _ in range(10): r = self._read_byte(spi) if r == _ERR_CMD: - raise RuntimeError("Error response to command") + raise BrokenPipeError("Error response to command") if r == desired: return True - raise RuntimeError("Timed out waiting for SPI char") + time.sleep(0.01) + raise TimeoutError("Timed out waiting for SPI char") def _check_data(self, spi, desired): """Read a byte and verify its the value we want""" r = self._read_byte(spi) if r != desired: - raise RuntimeError("Expected %02X but got %02X" % (desired, r)) + raise BrokenPipeError(f"Expected {desired:02X} but got {r:02X}") def _wait_response_cmd(self, cmd, num_responses=None, *, param_len_16=False): """Wait for ready, then parse the response""" @@ -287,7 +384,7 @@ def _wait_response_cmd(self, cmd, num_responses=None, *, param_len_16=False): if self._ready.value: # ok ready to send! break else: - raise RuntimeError("ESP32 timed out on SPI select") + raise TimeoutError("ESP32 timed out on SPI select") self._wait_spi_char(spi, _START_CMD) self._check_data(spi, cmd | _REPLY_FLAG) @@ -318,13 +415,11 @@ def _send_command_get_response( *, reply_params=1, sent_param_len_16=False, - recv_param_len_16=False + recv_param_len_16=False, ): """Send a high level SPI command, wait and return the response""" self._send_command(cmd, params, param_len_16=sent_param_len_16) - return self._wait_response_cmd( - cmd, reply_params, param_len_16=recv_param_len_16 - ) + return self._wait_response_cmd(cmd, reply_params, param_len_16=recv_param_len_16) @property def status(self): @@ -343,25 +438,25 @@ def firmware_version(self): if self._debug: print("Firmware version") resp = self._send_command_get_response(_GET_FW_VERSION_CMD) - return resp[0] + return resp[0].decode("utf-8").replace("\x00", "") @property - def MAC_address(self): # pylint: disable=invalid-name + def MAC_address(self): """A bytearray containing the MAC address of the ESP32""" if self._debug: print("MAC address") - resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xFF"]) + resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xff"]) return resp[0] @property - def MAC_address_actual(self): # pylint: disable=invalid-name + def MAC_address_actual(self): """A bytearray containing the actual MAC address of the ESP32""" - if self._debug: - print("MAC address") - resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xFF"]) - new_resp = bytearray(resp[0]) - new_resp = reversed(new_resp) - return new_resp + return bytearray(reversed(self.MAC_address)) + + @property + def mac_address(self): + """A bytes containing the actual MAC address of the ESP32""" + return bytes(self.MAC_address_actual) def start_scan_networks(self): """Begin a scan of visible access points. Follow up with a call @@ -370,26 +465,30 @@ def start_scan_networks(self): print("Start scan") resp = self._send_command_get_response(_START_SCAN_NETWORKS) if resp[0][0] != 1: - raise RuntimeError("Failed to start AP scan") + raise OSError("Failed to start AP scan") def get_scan_networks(self): """The results of the latest SSID scan. Returns a list of dictionaries with - 'ssid', 'rssi', 'encryption', bssid, and channel entries, one for each AP found""" + 'ssid', 'rssi', 'encryption', bssid, and channel entries, one for each AP found + """ self._send_command(_SCAN_NETWORKS) names = self._wait_response_cmd(_SCAN_NETWORKS) # print("SSID names:", names) - APs = [] # pylint: disable=invalid-name + APs = [] for i, name in enumerate(names): - a_p = {"ssid": name} - rssi = self._send_command_get_response(_GET_IDX_RSSI_CMD, ((i,),))[0] - a_p["rssi"] = struct.unpack(" 32: - raise RuntimeError("ssid must be no more than 32 characters") + raise ValueError("ssid must be no more than 32 characters") if password and (len(password) < 8 or len(password) > 64): - raise RuntimeError("password must be 8 - 63 characters") + raise ValueError("password must be 8 - 63 characters") if channel < 1 or channel > 14: - raise RuntimeError("channel must be between 1 and 14") + raise ValueError("channel must be between 1 and 14") if isinstance(channel, int): channel = bytes(channel) @@ -586,14 +725,19 @@ def create_AP( return stat time.sleep(0.05) if stat == WL_AP_FAILED: - raise RuntimeError("Failed to create AP", ssid) - raise RuntimeError("Unknown error 0x%02x" % stat) + raise ConnectionError("Failed to create AP", ssid) + raise OSError(f"Unknown error 0x{stat:02x}") + + @property + def ipv4_address(self): + """IP address of the station when connected to an access point.""" + return self.pretty_ip(self.ip_address) - def pretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name + def pretty_ip(self, ip): # noqa: PLR6301 """Converts a bytearray IP address to a dotted-quad string for printing""" - return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3]) + return f"{ip[0]}.{ip[1]}.{ip[2]}.{ip[3]}" - def unpretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name + def unpretty_ip(self, ip): # noqa: PLR6301 """Converts a dotted-quad string to a bytearray IP address""" octets = [int(x) for x in ip.split(".")] return bytes(octets) @@ -607,7 +751,7 @@ def get_host_by_name(self, hostname): hostname = bytes(hostname, "utf-8") resp = self._send_command_get_response(_REQ_HOST_BY_NAME_CMD, (hostname,)) if resp[0][0] != 1: - raise RuntimeError("Failed to request hostname") + raise ConnectionError("Failed to request hostname") resp = self._send_command_get_response(_GET_HOST_BY_NAME_CMD) return resp[0] @@ -643,7 +787,7 @@ def socket_open(self, socket_num, dest, port, conn_mode=TCP_MODE): if self._debug: print("*** Open socket to", dest, port, conn_mode) if conn_mode == ESP_SPIcontrol.TLS_MODE and self._tls_socket is not None: - raise OSError(23) # ENFILE - File table overflow + raise OSError(23, "Only one open SSL connection allowed") port_param = struct.pack(">H", port) if isinstance(dest, str): # use the 5 arg version dest = bytes(dest, "utf-8") @@ -663,7 +807,7 @@ def socket_open(self, socket_num, dest, port, conn_mode=TCP_MODE): (dest, port_param, self._socknum_ll[0], (conn_mode,)), ) if resp[0][0] != 1: - raise RuntimeError("Could not connect to remote server") + raise ConnectionError("Could not connect to remote server") if conn_mode == ESP_SPIcontrol.TLS_MODE: self._tls_socket = socket_num @@ -673,9 +817,7 @@ def socket_status(self, socket_num): SOCKET_FIN_WAIT_2, SOCKET_CLOSE_WAIT, SOCKET_CLOSING, SOCKET_LAST_ACK, or SOCKET_TIME_WAIT""" self._socknum_ll[0][0] = socket_num - resp = self._send_command_get_response( - _GET_CLIENT_STATE_TCP_CMD, self._socknum_ll - ) + resp = self._send_command_get_response(_GET_CLIENT_STATE_TCP_CMD, self._socknum_ll) return resp[0][0] def socket_connected(self, socket_num): @@ -706,24 +848,20 @@ def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE): if conn_mode == self.UDP_MODE: # UDP verifies chunks on write, not bytes if sent != total_chunks: - raise RuntimeError( - "Failed to write %d chunks (sent %d)" % (total_chunks, sent) - ) + raise ConnectionError("Failed to write %d chunks (sent %d)" % (total_chunks, sent)) # UDP needs to finalize with this command, does the actual sending resp = self._send_command_get_response(_SEND_UDP_DATA_CMD, self._socknum_ll) if resp[0][0] != 1: - raise RuntimeError("Failed to send UDP data") + raise ConnectionError("Failed to send UDP data") return if sent != len(buffer): self.socket_close(socket_num) - raise RuntimeError( - "Failed to send %d bytes (sent %d)" % (len(buffer), sent) - ) + raise ConnectionError("Failed to send %d bytes (sent %d)" % (len(buffer), sent)) resp = self._send_command_get_response(_DATA_SENT_TCP_CMD, self._socknum_ll) if resp[0][0] != 1: - raise RuntimeError("Failed to verify data sent") + raise ConnectionError("Failed to verify data sent") def socket_available(self, socket_num): """Determine how many bytes are waiting to be read on the socket""" @@ -735,7 +873,7 @@ def socket_available(self, socket_num): return reply def socket_read(self, socket_num, size): - """Read up to 'size' bytes from the socket number. Returns a bytearray""" + """Read up to 'size' bytes from the socket number. Returns a bytes""" if self._debug: print( "Reading %d bytes from ESP socket with status %d" @@ -770,7 +908,7 @@ def socket_connect(self, socket_num, dest, port, conn_mode=TCP_MODE): if self.socket_connected(socket_num): return True time.sleep(0.01) - raise RuntimeError("Failed to establish connection") + raise TimeoutError("Failed to establish connection") def socket_close(self, socket_num): """Close a socket using the ESP32's internal reference number""" @@ -779,14 +917,12 @@ def socket_close(self, socket_num): self._socknum_ll[0][0] = socket_num try: self._send_command_get_response(_STOP_CLIENT_TCP_CMD, self._socknum_ll) - except RuntimeError: + except OSError: pass if socket_num == self._tls_socket: self._tls_socket = None - def start_server( - self, port, socket_num, conn_mode=TCP_MODE, ip=None - ): # pylint: disable=invalid-name + def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): # pylint: disable=invalid-name """Opens a server on the specified port, using the ESP32's internal reference number""" if self._debug: print("*** starting server") @@ -797,7 +933,7 @@ def start_server( resp = self._send_command_get_response(_START_SERVER_TCP_CMD, params) if resp[0][0] != 1: - raise RuntimeError("Could not start server") + raise OSError("Could not start server") def server_state(self, socket_num): """Get the state of the ESP32's internal reference server socket number""" @@ -805,16 +941,23 @@ def server_state(self, socket_num): resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll) return resp[0][0] + def get_remote_data(self, socket_num): + """Get the IP address and port of the remote host""" + self._socknum_ll[0][0] = socket_num + resp = self._send_command_get_response( + _GET_REMOTE_DATA_CMD, self._socknum_ll, reply_params=2 + ) + return {"ip_addr": resp[0], "port": struct.unpack(" 1.5.0 - fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2] + fw_semver_maj = self.firmware_version[2] assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above." resp = self._send_command_get_response(_SET_DIGITAL_READ_CMD, ((pin,),))[0] @@ -871,27 +1007,22 @@ def set_digital_read(self, pin): return False if resp[0] == 1: return True - raise ValueError( - "_SET_DIGITAL_READ response error: response is not boolean", resp[0] - ) + raise OSError("_SET_DIGITAL_READ response error: response is not boolean", resp[0]) def set_analog_read(self, pin, atten=ADC_ATTEN_DB_11): - """ - Get the analog input value of pin. Returns an int between 0 and 65536. + """Get the analog input value of pin. Returns an int between 0 and 65536. :param int pin: ESP32 GPIO pin to read from. :param int atten: attenuation constant """ # Verify nina-fw => 1.5.0 - fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2] + fw_semver_maj = self.firmware_version[2] assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above." resp = self._send_command_get_response(_SET_ANALOG_READ_CMD, ((pin,), (atten,))) resp_analog = struct.unpack(" 0 and time.monotonic() - stamp > self._timeout: - self.close() # Make sure to close socket so that we don't exhaust sockets. - raise RuntimeError("Didn't receive full response, failing out") - firstline, self._buffer = self._buffer.split(b"\r\n", 1) - gc.collect() - return firstline - - def recv(self, bufsize=0): - """Reads some bytes from the connected remote address. Will only return - an empty string after the configured timeout. - - :param int bufsize: maximum number of bytes to receive - """ - # print("Socket read", bufsize) - if bufsize == 0: # read as much as we can at the moment - while True: - avail = self.available() - if avail: - self._buffer += _the_interface.socket_read(self._socknum, avail) - else: - break - gc.collect() - ret = self._buffer - self._buffer = b"" - gc.collect() - return ret - stamp = time.monotonic() - - to_read = bufsize - len(self._buffer) - received = [] - while to_read > 0: - # print("Bytes to read:", to_read) - avail = self.available() - if avail: - stamp = time.monotonic() - recv = _the_interface.socket_read(self._socknum, min(to_read, avail)) - received.append(recv) - to_read -= len(recv) - gc.collect() - elif received: - # We've received some bytes but no more are available. So return - # what we have. - break - if self._timeout > 0 and time.monotonic() - stamp > self._timeout: - break - # print(received) - self._buffer += b"".join(received) - - ret = None - if len(self._buffer) == bufsize: - ret = self._buffer - self._buffer = b"" - else: - ret = self._buffer[:bufsize] - self._buffer = self._buffer[bufsize:] - gc.collect() - return ret - - def read(self, size=0): - """Read up to 'size' bytes from the socket, this may be buffered internally! - If 'size' isnt specified, return everything in the buffer. - NOTE: This method is deprecated and will be removed. - """ - return self.recv(size) - - def settimeout(self, value): - """Set the read timeout for sockets, if value is 0 it will block""" - self._timeout = value - - def available(self): - """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)""" - if self.socknum != NO_SOCKET_AVAIL: - return min(_the_interface.socket_available(self._socknum), MAX_PACKET) - return 0 - - def connected(self): - """Whether or not we are connected to the socket""" - if self.socknum == NO_SOCKET_AVAIL: - return False - if self.available(): - return True - status = _the_interface.socket_status(self.socknum) - result = status not in ( - adafruit_esp32spi.SOCKET_LISTEN, - adafruit_esp32spi.SOCKET_CLOSED, - adafruit_esp32spi.SOCKET_FIN_WAIT_1, - adafruit_esp32spi.SOCKET_FIN_WAIT_2, - adafruit_esp32spi.SOCKET_TIME_WAIT, - adafruit_esp32spi.SOCKET_SYN_SENT, - adafruit_esp32spi.SOCKET_SYN_RCVD, - adafruit_esp32spi.SOCKET_CLOSE_WAIT, - ) - if not result: - self.close() - self._socknum = NO_SOCKET_AVAIL - return result - - @property - def socknum(self): - """The socket number""" - return self._socknum - - def close(self): - """Close the socket, after reading whatever remains""" - _the_interface.socket_close(self._socknum) - - -# pylint: enable=unused-argument, redefined-builtin, invalid-name diff --git a/adafruit_esp32spi/adafruit_esp32spi_socketpool.py b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py new file mode 100644 index 0000000..91615dc --- /dev/null +++ b/adafruit_esp32spi/adafruit_esp32spi_socketpool.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2019 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_esp32spi_socketpool` +================================================================================ + +A socket compatible interface thru the ESP SPI command set + +* Author(s): ladyada +""" + +from __future__ import annotations + +try: + from typing import TYPE_CHECKING, Optional + + if TYPE_CHECKING: + from esp32spi.adafruit_esp32spi import ESP_SPIcontrol # noqa: UP007 +except ImportError: + pass + + +import errno +import gc +import time + +from micropython import const + +from adafruit_esp32spi import adafruit_esp32spi as esp32spi + +_global_socketpool = {} + + +class SocketPool: + """ESP32SPI SocketPool library""" + + SOCK_STREAM = const(0) + SOCK_DGRAM = const(1) + AF_INET = const(2) + NO_SOCKET_AVAIL = const(255) + + MAX_PACKET = const(4000) + + def __new__(cls, iface: ESP_SPIcontrol): + # We want to make sure to return the same pool for the same interface + if iface not in _global_socketpool: + _global_socketpool[iface] = super().__new__(cls) + return _global_socketpool[iface] + + def __init__(self, iface: ESP_SPIcontrol): + self._interface = iface + + def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + """Given a hostname and a port name, return a 'socket.getaddrinfo' + compatible list of tuples. Honestly, we ignore anything but host & port""" + if not isinstance(port, int): + raise ValueError("Port must be an integer") + ipaddr = self._interface.get_host_by_name(host) + return [(SocketPool.AF_INET, socktype, proto, "", (ipaddr, port))] + + def socket( + self, + family=AF_INET, + type=SOCK_STREAM, + proto=0, + fileno=None, + ): + """Create a new socket and return it""" + return Socket(self, family, type, proto, fileno) + + +class Socket: + """A simplified implementation of the Python 'socket' class, for connecting + through an interface to a remote device""" + + def __init__( + self, + socket_pool: SocketPool, + family: int = SocketPool.AF_INET, + type: int = SocketPool.SOCK_STREAM, + proto: int = 0, + fileno: Optional[int] = None, # noqa: UP007 + ): + if family != SocketPool.AF_INET: + raise ValueError("Only AF_INET family supported") + self._socket_pool = socket_pool + self._interface = self._socket_pool._interface + self._type = type + self._buffer = b"" + self._socknum = self._interface.get_socket() + self.settimeout(0) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + while self._interface.socket_status(self._socknum) != esp32spi.SOCKET_CLOSED: + pass + + def connect(self, address, conntype=None): + """Connect the socket to the 'address' (which can be 32bit packed IP or + a hostname string). 'conntype' is an extra that may indicate SSL or not, + depending on the underlying interface""" + host, port = address + if conntype is None: + conntype = ( + self._interface.UDP_MODE + if self._type == SocketPool.SOCK_DGRAM + else self._interface.TCP_MODE + ) + if not self._interface.socket_connect(self._socknum, host, port, conn_mode=conntype): + raise ConnectionError("Failed to connect to host", host) + self._buffer = b"" + + def send(self, data): + """Send some data to the socket.""" + if self._type == SocketPool.SOCK_DGRAM: + conntype = self._interface.UDP_MODE + else: + conntype = self._interface.TCP_MODE + self._interface.socket_write(self._socknum, data, conn_mode=conntype) + gc.collect() + + def sendto(self, data, address): + """Connect and send some data to the socket.""" + self.connect(address) + self.send(data) + + def recv(self, bufsize: int) -> bytes: + """Reads some bytes from the connected remote address. Will only return + an empty string after the configured timeout. + + :param int bufsize: maximum number of bytes to receive + """ + buf = bytearray(bufsize) + self.recv_into(buf, bufsize) + return bytes(buf) + + def recv_into(self, buffer, nbytes: int = 0): + """Read bytes from the connected remote address into a given buffer. + + :param bytearray buffer: the buffer to read into + :param int nbytes: maximum number of bytes to receive; if 0, + receive as many bytes as possible before filling the + buffer or timing out + """ + if not 0 <= nbytes <= len(buffer): + raise ValueError("nbytes must be 0 to len(buffer)") + + last_read_time = time.monotonic() + num_to_read = len(buffer) if nbytes == 0 else nbytes + num_read = 0 + while num_to_read > 0: + # we might have read socket data into the self._buffer with: + # esp32spi_wsgiserver: socket_readline + if len(self._buffer) > 0: + bytes_to_read = min(num_to_read, len(self._buffer)) + buffer[num_read : num_read + bytes_to_read] = self._buffer[:bytes_to_read] + num_read += bytes_to_read + num_to_read -= bytes_to_read + self._buffer = self._buffer[bytes_to_read:] + # explicitly recheck num_to_read to avoid extra checks + continue + + num_avail = self._available() + if num_avail > 0: + last_read_time = time.monotonic() + bytes_read = self._interface.socket_read(self._socknum, min(num_to_read, num_avail)) + buffer[num_read : num_read + len(bytes_read)] = bytes_read + num_read += len(bytes_read) + num_to_read -= len(bytes_read) + elif num_read > 0: + # We got a message, but there are no more bytes to read, so we can stop. + break + # No bytes yet, or more bytes requested. + if self._timeout > 0 and time.monotonic() - last_read_time > self._timeout: + raise OSError(errno.ETIMEDOUT) + return num_read + + def settimeout(self, value): + """Set the read timeout for sockets. + If value is 0 socket reads will block until a message is available. + """ + self._timeout = value + + def _available(self): + """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)""" + if self._socknum != SocketPool.NO_SOCKET_AVAIL: + return min(self._interface.socket_available(self._socknum), SocketPool.MAX_PACKET) + return 0 + + def _connected(self): + """Whether or not we are connected to the socket""" + if self._socknum == SocketPool.NO_SOCKET_AVAIL: + return False + if self._available(): + return True + status = self._interface.socket_status(self._socknum) + result = status not in { + esp32spi.SOCKET_LISTEN, + esp32spi.SOCKET_CLOSED, + esp32spi.SOCKET_FIN_WAIT_1, + esp32spi.SOCKET_FIN_WAIT_2, + esp32spi.SOCKET_TIME_WAIT, + esp32spi.SOCKET_SYN_SENT, + esp32spi.SOCKET_SYN_RCVD, + esp32spi.SOCKET_CLOSE_WAIT, + } + if not result: + self.close() + self._socknum = SocketPool.NO_SOCKET_AVAIL + return result + + def close(self): + """Close the socket, after reading whatever remains""" + self._interface.socket_close(self._socknum) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py index 930b33c..23c65b9 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py @@ -11,17 +11,17 @@ * Author(s): Melissa LeBlanc-Williams, ladyada """ -# pylint: disable=no-name-in-module - +import warnings from time import sleep + +import adafruit_connection_manager +import adafruit_requests from micropython import const -import adafruit_requests as requests + from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -# pylint: disable=too-many-instance-attributes -class ESPSPI_WiFiManager: +class WiFiManager: """ A class to help manage the Wifi connection """ @@ -29,11 +29,14 @@ class ESPSPI_WiFiManager: NORMAL = const(1) ENTERPRISE = const(2) - # pylint: disable=too-many-arguments def __init__( self, esp, - secrets, + ssid, + password=None, + *, + enterprise_ident=None, + enterprise_user=None, status_pixel=None, attempts=2, connection_type=NORMAL, @@ -41,9 +44,17 @@ def __init__( ): """ :param ESP_SPIcontrol esp: The ESP object we are using - :param dict secrets: The WiFi and Adafruit IO secrets dict (See examples) + :param str ssid: the SSID of the access point. Must be less than 32 chars. + :param str password: the password for the access point. Must be 8-63 chars. + :param str enterprise_ident: the ident to use when connecting to an enterprise access point. + :param str enterprise_user: the username to use when connecting to an enterprise access + point. :param status_pixel: (Optional) The pixel device - A NeoPixel, DotStar, - or RGB LED (default=None) + or RGB LED (default=None). The status LED, if given, turns red when + attempting to connect to a Wi-Fi network or create an access point, + turning green upon success. Additionally, if given, it will turn blue + when attempting an HTTP method or returning IP address, turning off + upon success. :type status_pixel: NeoPixel, DotStar, or RGB LED :param int attempts: (Optional) Failed attempts before resetting the ESP32 (default=2) :param const connection_type: (Optional) Type of WiFi connection: NORMAL or ENTERPRISE @@ -51,28 +62,24 @@ def __init__( # Read the settings self.esp = esp self.debug = debug - self.ssid = secrets["ssid"] - self.password = secrets.get("password", None) + self.ssid = ssid + self.password = password self.attempts = attempts self._connection_type = connection_type - requests.set_socket(socket, esp) self.statuspix = status_pixel self.pixel_status(0) self._ap_index = 0 - # Check for WPA2 Enterprise keys in the secrets dictionary and load them if they exist - if secrets.get("ent_ssid"): - self.ent_ssid = secrets["ent_ssid"] - else: - self.ent_ssid = secrets["ssid"] - if secrets.get("ent_ident"): - self.ent_ident = secrets["ent_ident"] - else: - self.ent_ident = "" - if secrets.get("ent_user"): - self.ent_user = secrets["ent_user"] - if secrets.get("ent_password"): - self.ent_password = secrets["ent_password"] + # create requests session + pool = adafruit_connection_manager.get_radio_socketpool(self.esp) + ssl_context = adafruit_connection_manager.get_radio_ssl_context(self.esp) + self._requests = adafruit_requests.Session(pool, ssl_context) + + # Check for WPA2 Enterprise values + self.ent_ssid = ssid + self.ent_ident = enterprise_ident + self.ent_user = enterprise_user + self.ent_password = password # pylint: enable=too-many-arguments @@ -94,21 +101,16 @@ def connect(self): print("Firmware vers.", self.esp.firmware_version) print("MAC addr:", [hex(i) for i in self.esp.MAC_address]) for access_pt in self.esp.scan_networks(): - print( - "\t%s\t\tRSSI: %d" - % (str(access_pt["ssid"], "utf-8"), access_pt["rssi"]) - ) - if self._connection_type == ESPSPI_WiFiManager.NORMAL: + print("\t%s\t\tRSSI: %d" % (access_pt.ssid, access_pt.rssi)) + if self._connection_type == WiFiManager.NORMAL: self.connect_normal() - elif self._connection_type == ESPSPI_WiFiManager.ENTERPRISE: + elif self._connection_type == WiFiManager.ENTERPRISE: self.connect_enterprise() else: raise TypeError("Invalid WiFi connection type specified") def _get_next_ap(self): - if isinstance(self.ssid, (tuple, list)) and isinstance( - self.password, (tuple, list) - ): + if isinstance(self.ssid, (tuple, list)) and isinstance(self.password, (tuple, list)): if not self.ssid or not self.password: raise ValueError("SSID and Password should contain at least 1 value") if len(self.ssid) != len(self.password): @@ -118,9 +120,7 @@ def _get_next_ap(self): if self._ap_index >= len(self.ssid): self._ap_index = 0 return access_point - if isinstance(self.ssid, (tuple, list)) or isinstance( - self.password, (tuple, list) - ): + if isinstance(self.ssid, (tuple, list)) or isinstance(self.password, (tuple, list)): raise NotImplementedError( "If using multiple passwords, both SSID and Password should be lists or tuples" ) @@ -128,7 +128,7 @@ def _get_next_ap(self): def connect_normal(self): """ - Attempt a regular style WiFi connection + Attempt a regular style WiFi connection. """ failure_count = 0 (ssid, password) = self._get_next_ap() @@ -140,7 +140,7 @@ def connect_normal(self): self.esp.connect_AP(bytes(ssid, "utf-8"), bytes(password, "utf-8")) failure_count = 0 self.pixel_status((0, 100, 0)) - except (ValueError, RuntimeError) as error: + except OSError as error: print("Failed to connect, retrying\n", error) failure_count += 1 if failure_count >= self.attempts: @@ -162,21 +162,19 @@ def create_ap(self): print("Waiting for AP to be initialized...") self.pixel_status((100, 0, 0)) if self.password: - self.esp.create_AP( - bytes(self.ssid, "utf-8"), bytes(self.password, "utf-8") - ) + self.esp.create_AP(bytes(self.ssid, "utf-8"), bytes(self.password, "utf-8")) else: self.esp.create_AP(bytes(self.ssid, "utf-8"), None) failure_count = 0 self.pixel_status((0, 100, 0)) - except (ValueError, RuntimeError) as error: + except OSError as error: print("Failed to create access point\n", error) failure_count += 1 if failure_count >= self.attempts: failure_count = 0 self.reset() continue - print("Access Point created! Connect to ssid:\n {}".format(self.ssid)) + print(f"Access Point created! Connect to ssid:\n {self.ssid}") def connect_enterprise(self): """ @@ -191,15 +189,13 @@ def connect_enterprise(self): while not self.esp.is_connected: try: if self.debug: - print( - "Waiting for the ESP32 to connect to the WPA2 Enterprise AP..." - ) + print("Waiting for the ESP32 to connect to the WPA2 Enterprise AP...") self.pixel_status((100, 0, 0)) sleep(1) failure_count = 0 self.pixel_status((0, 100, 0)) sleep(1) - except (ValueError, RuntimeError) as error: + except OSError as error: print("Failed to connect, retrying\n", error) failure_count += 1 if failure_count >= self.attempts: @@ -222,7 +218,7 @@ def get(self, url, **kw): if not self.esp.is_connected: self.connect() self.pixel_status((0, 0, 100)) - return_val = requests.get(url, **kw) + return_val = self._requests.get(url, **kw) self.pixel_status(0) return return_val @@ -241,7 +237,8 @@ def post(self, url, **kw): if not self.esp.is_connected: self.connect() self.pixel_status((0, 0, 100)) - return_val = requests.post(url, **kw) + return_val = self._requests.post(url, **kw) + self.pixel_status(0) return return_val def put(self, url, **kw): @@ -259,7 +256,7 @@ def put(self, url, **kw): if not self.esp.is_connected: self.connect() self.pixel_status((0, 0, 100)) - return_val = requests.put(url, **kw) + return_val = self._requests.put(url, **kw) self.pixel_status(0) return return_val @@ -278,7 +275,7 @@ def patch(self, url, **kw): if not self.esp.is_connected: self.connect() self.pixel_status((0, 0, 100)) - return_val = requests.patch(url, **kw) + return_val = self._requests.patch(url, **kw) self.pixel_status(0) return return_val @@ -297,7 +294,7 @@ def delete(self, url, **kw): if not self.esp.is_connected: self.connect() self.pixel_status((0, 0, 100)) - return_val = requests.delete(url, **kw) + return_val = self._requests.delete(url, **kw) self.pixel_status(0) return return_val @@ -325,7 +322,7 @@ def ip_address(self): self.connect() self.pixel_status((0, 0, 100)) self.pixel_status(0) - return self.esp.pretty_ip(self.esp.ip_address) + return self.esp.ipv4_address def pixel_status(self, value): """ @@ -346,4 +343,52 @@ def signal_strength(self): """ if not self.esp.is_connected: self.connect() - return self.esp.rssi + return self.esp.ap_info.rssi + + +class ESPSPI_WiFiManager(WiFiManager): + """ + A legacy class to help manage the Wifi connection. Please update to using WiFiManager + """ + + def __init__( + self, + esp, + secrets, + status_pixel=None, + attempts=2, + connection_type=WiFiManager.NORMAL, + debug=False, + ): + """ + :param ESP_SPIcontrol esp: The ESP object we are using + :param dict secrets: The WiFi secrets dict + The use of secrets.py to populate the secrets dict is deprecated + in favor of using settings.toml. + :param status_pixel: (Optional) The pixel device - A NeoPixel, DotStar, + or RGB LED (default=None). The status LED, if given, turns red when + attempting to connect to a Wi-Fi network or create an access point, + turning green upon success. Additionally, if given, it will turn blue + when attempting an HTTP method or returning IP address, turning off + upon success. + :type status_pixel: NeoPixel, DotStar, or RGB LED + :param int attempts: (Optional) Failed attempts before resetting the ESP32 (default=2) + :param const connection_type: (Optional) Type of WiFi connection: NORMAL or ENTERPRISE + """ + + warnings.warn( + "ESP32WiFiManager, which uses `secrets`, is deprecated. Use WifiManager instead and " + "fetch values from settings.toml with `os.getenv()`." + ) + + super().__init__( + esp=esp, + ssid=secrets.get("ssid"), + password=secrets.get("password"), + enterprise_ident=secrets.get("ent_ident", ""), + enterprise_user=secrets.get("ent_user"), + status_pixel=status_pixel, + attempts=attempts, + connection_type=connection_type, + debug=debug, + ) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py b/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py deleted file mode 100644 index ff89a73..0000000 --- a/adafruit_esp32spi/adafruit_esp32spi_wsgiserver.py +++ /dev/null @@ -1,208 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019 Matt Costi for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -""" -`adafruit_esp32spi_wsgiserver` -================================================================================ - -A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI. -Opens a specified port on the ESP32 to listen for incoming HTTP Requests and -Accepts an Application object that must be callable, which gets called -whenever a new HTTP Request has been received. - -The Application MUST accept 2 ordered parameters: - 1. environ object (incoming request data) - 2. start_response function. Must be called before the Application - callable returns, in order to set the response status and headers. - -The Application MUST return a single string in a list, -which is the response data - -Requires update_poll being called in the applications main event loop. - -For more details about Python WSGI see: -https://www.python.org/dev/peps/pep-0333/ - -* Author(s): Matt Costi -""" -# pylint: disable=no-name-in-module - -import io -import gc -from micropython import const -from adafruit_requests import parse_headers -import adafruit_esp32spi.adafruit_esp32spi_socket as socket - -_the_interface = None # pylint: disable=invalid-name - - -def set_interface(iface): - """Helper to set the global internet interface""" - global _the_interface # pylint: disable=global-statement, invalid-name - _the_interface = iface - socket.set_interface(iface) - - -NO_SOCK_AVAIL = const(255) - -# pylint: disable=invalid-name -class WSGIServer: - """ - A simple server that implements the WSGI interface - """ - - def __init__(self, port=80, debug=False, application=None): - self.application = application - self.port = port - self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL) - self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL) - self._debug = debug - - self._response_status = None - self._response_headers = [] - - def start(self): - """ - starts the server and begins listening for incoming connections. - Call update_poll in the main loop for the application callable to be - invoked on receiving an incoming request. - """ - self._server_sock = socket.socket() - _the_interface.start_server(self.port, self._server_sock.socknum) - if self._debug: - ip = _the_interface.pretty_ip(_the_interface.ip_address) - print("Server available at {0}:{1}".format(ip, self.port)) - print( - "Server status: ", - _the_interface.server_state(self._server_sock.socknum), - ) - - def update_poll(self): - """ - Call this method inside your main event loop to get the server - check for new incoming client requests. When a request comes in, - the application callable will be invoked. - """ - self.client_available() - if self._client_sock and self._client_sock.available(): - environ = self._get_environ(self._client_sock) - result = self.application(environ, self._start_response) - self.finish_response(result) - - def finish_response(self, result): - """ - Called after the application callbile returns result data to respond with. - Creates the HTTP Response payload from the response_headers and results data, - and sends it back to client. - - :param string result: the data string to send back in the response to the client. - """ - try: - response = "HTTP/1.1 {0}\r\n".format(self._response_status) - for header in self._response_headers: - response += "{0}: {1}\r\n".format(*header) - response += "\r\n" - self._client_sock.send(response.encode("utf-8")) - for data in result: - if isinstance(data, bytes): - self._client_sock.send(data) - else: - self._client_sock.send(data.encode("utf-8")) - gc.collect() - finally: - print("closing") - self._client_sock.close() - - def client_available(self): - """ - returns a client socket connection if available. - Otherwise, returns None - :return: the client - :rtype: Socket - """ - sock = None - if self._server_sock.socknum != NO_SOCK_AVAIL: - if self._client_sock.socknum != NO_SOCK_AVAIL: - # check previous received client socket - if self._debug > 2: - print("checking if last client sock still valid") - if self._client_sock.connected() and self._client_sock.available(): - sock = self._client_sock - if not sock: - # check for new client sock - if self._debug > 2: - print("checking for new client sock") - client_sock_num = _the_interface.socket_available( - self._server_sock.socknum - ) - sock = socket.socket(socknum=client_sock_num) - else: - print("Server has not been started, cannot check for clients!") - - if sock and sock.socknum != NO_SOCK_AVAIL: - if self._debug > 2: - print("client sock num is: ", sock.socknum) - self._client_sock = sock - return self._client_sock - - return None - - def _start_response(self, status, response_headers): - """ - The application callable will be given this method as the second param - This is to be called before the application callable returns, to signify - the response can be started with the given status and headers. - - :param string status: a status string including the code and reason. ex: "200 OK" - :param list response_headers: a list of tuples to represent the headers. - ex ("header-name", "header value") - """ - self._response_status = status - self._response_headers = [("Server", "esp32WSGIServer")] + response_headers - - def _get_environ(self, client): - """ - The application callable will be given the resulting environ dictionary. - It contains metadata about the incoming request and the request body ("wsgi.input") - - :param Socket client: socket to read the request from - """ - env = {} - line = str(client.readline(), "utf-8") - (method, path, ver) = line.rstrip("\r\n").split(None, 2) - - env["wsgi.version"] = (1, 0) - env["wsgi.url_scheme"] = "http" - env["wsgi.multithread"] = False - env["wsgi.multiprocess"] = False - env["wsgi.run_once"] = False - - env["REQUEST_METHOD"] = method - env["SCRIPT_NAME"] = "" - env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address) - env["SERVER_PROTOCOL"] = ver - env["SERVER_PORT"] = self.port - if path.find("?") >= 0: - env["PATH_INFO"] = path.split("?")[0] - env["QUERY_STRING"] = path.split("?")[1] - else: - env["PATH_INFO"] = path - - headers = parse_headers(client) - if "content-type" in headers: - env["CONTENT_TYPE"] = headers.get("content-type") - if "content-length" in headers: - env["CONTENT_LENGTH"] = headers.get("content-length") - body = client.read(int(env["CONTENT_LENGTH"])) - env["wsgi.input"] = io.StringIO(body) - else: - body = client.read() - env["wsgi.input"] = io.StringIO(body) - for name, value in headers.items(): - key = "HTTP_" + name.replace("-", "_").upper() - if key in env: - value = "{0},{1}".format(env[key], value) - env[key] = value - - return env diff --git a/adafruit_esp32spi/digitalio.py b/adafruit_esp32spi/digitalio.py index f46fc9b..a9ee458 100755 --- a/adafruit_esp32spi/digitalio.py +++ b/adafruit_esp32spi/digitalio.py @@ -8,10 +8,12 @@ DigitalIO for ESP32 over SPI. * Author(s): Brent Rubell, based on Adafruit_Blinka digitalio implementation -and bcm283x Pin implementation. + and bcm283x Pin implementation. + https://github.com/adafruit/Adafruit_Blinka/blob/master/src/adafruit_blinka/microcontroller/bcm283x/pin.py https://github.com/adafruit/Adafruit_Blinka/blob/master/src/digitalio.py """ + from micropython import const @@ -27,7 +29,6 @@ class Pin: or the use of internal pull-up resistors. """ - # pylint: disable=invalid-name IN = const(0x00) OUT = const(0x01) LOW = const(0x00) @@ -49,6 +50,7 @@ def __init__(self, esp_pin, esp): def init(self, mode=IN): """Initalizes a pre-defined pin. + :param mode: Pin mode (IN, OUT, LOW, HIGH). Defaults to IN. """ if mode is not None: @@ -59,10 +61,11 @@ def init(self, mode=IN): self._mode = self.OUT self._esp.set_pin_mode(self.pin_id, 1) else: - raise RuntimeError("Invalid mode defined") + raise ValueError("Invalid mode defined") def value(self, val=None): """Sets ESP32 Pin GPIO output mode. + :param val: Pin output level (LOW, HIGH) """ if val is not None: @@ -73,17 +76,14 @@ def value(self, val=None): self._value = val self._esp.set_digital_write(self.pin_id, 1) else: - raise RuntimeError("Invalid value for pin") + raise ValueError("Invalid value for pin") else: - raise NotImplementedError( - "digitalRead not currently implemented in esp32spi" - ) + raise NotImplementedError("digitalRead not currently implemented in esp32spi") def __repr__(self): return str(self.pin_id) -# pylint: disable = too-few-public-methods class DriveMode: """DriveMode Enum.""" @@ -114,7 +114,7 @@ class DigitalInOut: """ _pin = None - # pylint: disable = attribute-defined-outside-init + def __init__(self, esp, pin): self._esp = esp self._pin = Pin(pin, self._esp) @@ -132,6 +132,7 @@ def deinit(self): def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL): """Set the drive mode and value and then switch to writing out digital values. + :param bool value: Default mode to set upon switching. :param DriveMode drive_mode: Drive mode for the output. """ @@ -141,11 +142,10 @@ def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL): def switch_to_input(self, pull=None): """Sets the pull and then switch to read in digital values. + :param Pull pull: Pull configuration for the input. """ - raise NotImplementedError( - "Digital reads are not currently supported in ESP32SPI." - ) + raise NotImplementedError("Digital reads are not currently supported in ESP32SPI.") @property def direction(self): @@ -155,6 +155,7 @@ def direction(self): @direction.setter def direction(self, pin_dir): """Sets the direction of the pin. + :param Direction dir: Pin direction (Direction.OUTPUT or Direction.INPUT) """ self.__direction = pin_dir @@ -175,6 +176,7 @@ def value(self): @value.setter def value(self, val): """Sets the digital logic level of the pin. + :param type value: Pin logic level. :param int value: Pin logic level. 1 is logic high, 0 is logic low. :param bool value: Pin logic level. True is logic high, False is logic low. @@ -194,13 +196,11 @@ def drive_mode(self): @drive_mode.setter def drive_mode(self, mode): """Sets the pin drive mode. + :param DriveMode mode: Defines the drive mode when outputting digital values. - Either PUSH_PULL or OPEN_DRAIN + Either PUSH_PULL or OPEN_DRAIN """ - self.__drive_mode = mode if mode is DriveMode.OPEN_DRAIN: - raise NotImplementedError( - "Drive mode %s not implemented in ESP32SPI." % mode - ) + raise NotImplementedError("Drive mode %s not implemented in ESP32SPI." % mode) if mode is DriveMode.PUSH_PULL: self._pin.init(mode=Pin.OUT) diff --git a/docs/api.rst b/docs/api.rst index bee5de4..4ac85a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,8 +4,20 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" +API Reference +############# + .. automodule:: adafruit_esp32spi.adafruit_esp32spi :members: -.. automodule:: adafruit_esp32spi.adafruit_esp32spi_socket +.. automodule:: adafruit_esp32spi.adafruit_esp32spi_socketpool + :members: + +.. automodule:: adafruit_esp32spi.adafruit_esp32spi_wifimanager + :members: + +.. automodule:: adafruit_esp32spi.digitalio + :members: + +.. automodule:: adafruit_esp32spi.PWMOut :members: diff --git a/docs/conf.py b/docs/conf.py index 879c0aa..4040956 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- - # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries # # SPDX-License-Identifier: MIT +import datetime import os import sys @@ -16,6 +15,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinxcontrib.jquery", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.todo", @@ -25,16 +25,16 @@ # 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 = ["adafruit_requests"] intersphinx_mapping = { - "python": ("https://docs.python.org/3.4", None), + "python": ("https://docs.python.org/3", None), "BusDevice": ( - "https://circuitpython.readthedocs.io/projects/busdevice/en/latest/", + "https://docs.circuitpython.org/projects/busdevice/en/latest/", None, ), - "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), + "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), } # Add any paths that contain templates here, relative to this directory. @@ -47,7 +47,12 @@ # General information about the project. project = "Adafruit ESP32SPI Library" -copyright = "2019 ladyada" +creation_year = "2019" +current_year = str(datetime.datetime.now().year) +year_duration = ( + current_year if current_year == creation_year else creation_year + " - " + current_year +) +copyright = year_duration + " ladyada" author = "ladyada" # The version info for the project you're documenting, acts as replacement for @@ -64,7 +69,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -96,19 +101,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] - except: - html_theme = "default" - html_theme_path = ["."] -else: - html_theme_path = ["."] +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/index.rst b/docs/index.rst index 77f1cdc..7fcb348 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,8 +29,9 @@ Table of Contents .. toctree:: :caption: Other Links - Download - CircuitPython Reference Documentation + Download from GitHub + Download Library Bundle + CircuitPython Reference Documentation CircuitPython Support Forum Discord Chat Adafruit Learning System diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..979f568 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +sphinx +sphinxcontrib-jquery +sphinx-rtd-theme diff --git a/examples/esp32spi_aio_post.py b/examples/esp32spi_aio_post.py index b74e4e3..8be7e35 100644 --- a/examples/esp32spi_aio_post.py +++ b/examples/esp32spi_aio_post.py @@ -2,21 +2,26 @@ # SPDX-License-Identifier: MIT import time +from os import getenv + import board import busio -from digitalio import DigitalInOut import neopixel +from digitalio import DigitalInOut + from adafruit_esp32spi import adafruit_esp32spi -from adafruit_esp32spi import adafruit_esp32spi_wifimanager +from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager print("ESP32 SPI webclient 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +# ADAFRUIT_AIO_USERNAME, ADAFRUIT_AIO_KEY +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") + +aio_username = getenv("ADAFRUIT_AIO_USERNAME") +aio_key = getenv("ADAFRUIT_AIO_KEY") # If you are using a board with pre-defined ESP32 Pins: esp32_cs = DigitalInOut(board.ESP_CS) @@ -28,22 +33,25 @@ # esp32_ready = DigitalInOut(board.D10) # esp32_reset = DigitalInOut(board.D5) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + 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 +status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) """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 +# status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" # 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) +# status_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) + +wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) counter = 0 @@ -54,19 +62,15 @@ feed = "test" payload = {"value": data} response = wifi.post( - "https://io.adafruit.com/api/v2/" - + secrets["aio_username"] - + "/feeds/" - + feed - + "/data", + "https://io.adafruit.com/api/v2/" + aio_username + "/feeds/" + feed + "/data", json=payload, - headers={"X-AIO-KEY": secrets["aio_key"]}, + headers={"X-AIO-KEY": aio_key}, ) print(response.json()) response.close() counter = counter + 1 print("OK") - except (ValueError, RuntimeError) as e: + except OSError as e: print("Failed to get data, retrying\n", e) wifi.reset() continue diff --git a/examples/esp32spi_cheerlights.py b/examples/esp32spi_cheerlights.py index 46d75af..ca74e90 100644 --- a/examples/esp32spi_cheerlights.py +++ b/examples/esp32spi_cheerlights.py @@ -2,40 +2,55 @@ # SPDX-License-Identifier: MIT import time +from os import getenv + +import adafruit_fancyled.adafruit_fancyled as fancy import board import busio -from digitalio import DigitalInOut - import neopixel -import adafruit_fancyled.adafruit_fancyled as fancy +from digitalio import DigitalInOut from adafruit_esp32spi import adafruit_esp32spi -from adafruit_esp32spi import adafruit_esp32spi_wifimanager +from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager -# 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") print("ESP32 SPI webclient test") DATA_SOURCE = "https://api.thingspeak.com/channels/1417/feeds.json?results=1" DATA_LOCATION = ["feeds", 0, "field2"] -esp32_cs = DigitalInOut(board.D9) -esp32_ready = DigitalInOut(board.D10) -esp32_reset = DigitalInOut(board.D5) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +# 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) + +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + 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 +status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) """Uncomment below for ItsyBitsy M4""" -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) -wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) +# status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" +# 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_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) +wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) # neopixels pixels = neopixel.NeoPixel(board.A1, 16, brightness=0.3) @@ -54,7 +69,7 @@ value = value[key] print(value) response.close() - except (ValueError, RuntimeError) as e: + except OSError as e: print("Failed to get data, retrying\n", e) wifi.reset() continue diff --git a/examples/esp32spi_ipconfig.py b/examples/esp32spi_ipconfig.py new file mode 100644 index 0000000..36b8b8b --- /dev/null +++ b/examples/esp32spi_ipconfig.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +import time +from os import getenv + +import board +import busio +from digitalio import DigitalInOut + +import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool +from adafruit_esp32spi import adafruit_esp32spi + +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") + +HOSTNAME = "esp32-spi-hostname-test" + +IP_ADDRESS = "192.168.1.111" +GATEWAY_ADDRESS = "192.168.1.1" +SUBNET_MASK = "255.255.255.0" + +UDP_IN_ADDR = "192.168.1.1" +UDP_IN_PORT = 5500 + +UDP_TIMEOUT = 20 + +# 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) + +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) + +esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) +pool = socketpool.SocketPool(esp) + +s_in = pool.socket(type=pool.SOCK_DGRAM) +s_in.settimeout(UDP_TIMEOUT) +print("set hostname:", HOSTNAME) +esp.set_hostname(HOSTNAME) + +if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: + print("ESP32 found and in idle mode") +print("Firmware vers.", esp.firmware_version) +print("MAC addr:", [hex(i) for i in esp.MAC_address]) + +print("Connecting to AP...") +while not esp.is_connected: + try: + esp.connect_AP(ssid, password) + except OSError as e: + print("could not connect to AP, retrying: ", e) + continue +print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) +ip1 = esp.ip_address + +print("set ip dns") +esp.set_dns_config("192.168.1.1", "8.8.8.8") + +print("set ip config") +esp.set_ip_config(IP_ADDRESS, GATEWAY_ADDRESS, SUBNET_MASK) + +time.sleep(1) +ip2 = esp.ip_address + +time.sleep(1) +info = esp.network_data +print( + "get network_data: ", + esp.pretty_ip(info["ip_addr"]), + esp.pretty_ip(info["gateway"]), + esp.pretty_ip(info["netmask"]), +) + +print("My IP address is", esp.ipv4_address) +print("udp in addr: ", UDP_IN_ADDR, UDP_IN_PORT) + +socketaddr_udp_in = pool.getaddrinfo(UDP_IN_ADDR, UDP_IN_PORT)[0][4] +s_in.connect(socketaddr_udp_in, conntype=esp.UDP_MODE) +print("connected local UDP") + +while True: + data = s_in.recv(1205) + if len(data) >= 1: + data = data.decode("utf-8") + print(len(data), data) diff --git a/examples/esp32spi_localtime.py b/examples/esp32spi_localtime.py index 1c27304..0157864 100644 --- a/examples/esp32spi_localtime.py +++ b/examples/esp32spi_localtime.py @@ -2,37 +2,56 @@ # SPDX-License-Identifier: MIT import time +from os import getenv + import board import busio -from digitalio import DigitalInOut import neopixel import rtc +from digitalio import DigitalInOut + from adafruit_esp32spi import adafruit_esp32spi -from adafruit_esp32spi import adafruit_esp32spi_wifimanager +from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager -# 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") print("ESP32 local time") TIME_API = "http://worldtimeapi.org/api/ip" +# 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) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) + +# If you have an externally connected ESP32: +# esp32_cs = DigitalInOut(board.D9) +# esp32_ready = DigitalInOut(board.D10) +# esp32_reset = DigitalInOut(board.D5) + +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + 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 +status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) """Uncomment below for ItsyBitsy M4""" -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) -wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) +# status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" +# 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_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) + +wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) the_rtc = rtc.RTC() @@ -42,25 +61,23 @@ print("Fetching json from", TIME_API) response = wifi.get(TIME_API) break - except (ValueError, RuntimeError) as e: + except OSError as e: print("Failed to get data, retrying\n", e) continue json = response.json() current_time = json["datetime"] the_date, the_time = current_time.split("T") -year, month, mday = [int(x) for x in the_date.split("-")] +year, month, mday = (int(x) for x in the_date.split("-")) the_time = the_time.split(".")[0] -hours, minutes, seconds = [int(x) for x in the_time.split(":")] +hours, minutes, seconds = (int(x) for x in the_time.split(":")) # We can also fill in these extra nice things year_day = json["day_of_year"] week_day = json["day_of_week"] is_dst = json["dst"] -now = time.struct_time( - (year, month, mday, hours, minutes, seconds, week_day, year_day, is_dst) -) +now = time.struct_time((year, month, mday, hours, minutes, seconds, week_day, year_day, is_dst)) print(now) the_rtc.datetime = now diff --git a/examples/esp32spi_secrets.py b/examples/esp32spi_secrets.py deleted file mode 100644 index 02b75eb..0000000 --- a/examples/esp32spi_secrets.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# SPDX-License-Identifier: MIT - -# This file is where you keep secret settings, passwords, and tokens! -# If you put them in the code you risk committing that info or sharing it - -secrets = { - "ssid": "yourssid", - "password": "yourpassword", - "timezone": "America/New_York", # Check http://worldtimeapi.org/timezones - "aio_username": "youraiousername", - "aio_key": "youraiokey", -} diff --git a/examples/esp32spi_settings.toml b/examples/esp32spi_settings.toml new file mode 100644 index 0000000..4f5866e --- /dev/null +++ b/examples/esp32spi_settings.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# This file is where you keep secret settings, passwords, and tokens! +# If you put them in the code you risk committing that info or sharing it + +# The file should be renamed to `settings.toml` and saved in the root of +# the CIRCUITPY drive. + +CIRCUITPY_WIFI_SSID="yourssid" +CIRCUITPY_WIFI_PASSWORD="yourpassword" +CIRCUITPY_TIMEZONE="America/New_York" +ADAFRUIT_AIO_USERNAME="youraiousername" +ADAFRUIT_AIO_KEY="youraiokey" diff --git a/examples/esp32spi_simpletest.py b/examples/esp32spi_simpletest.py index af8c2f1..2124991 100644 --- a/examples/esp32spi_simpletest.py +++ b/examples/esp32spi_simpletest.py @@ -1,24 +1,25 @@ # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT +from os import getenv + +import adafruit_connection_manager +import adafruit_requests import board import busio from digitalio import DigitalInOut -import adafruit_requests as requests -import adafruit_esp32spi.adafruit_esp32spi_socket as socket + from adafruit_esp32spi import adafruit_esp32spi -# 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") print("ESP32 SPI webclient test") TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" -JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json" +JSON_URL = "http://wifitest.adafruit.com/testwifi/sample.json" # If you are using a board with pre-defined ESP32 Pins: @@ -42,31 +43,35 @@ # esp32_ready = DigitalInOut(board.D10) # esp32_reset = DigitalInOut(board.D5) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -requests.set_socket(socket, esp) +pool = adafruit_connection_manager.get_radio_socketpool(esp) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) +requests = adafruit_requests.Session(pool, ssl_context) if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: print("ESP32 found and in idle mode") print("Firmware vers.", esp.firmware_version) -print("MAC addr:", [hex(i) for i in esp.MAC_address]) +print("MAC addr:", ":".join("%02X" % byte for byte in esp.MAC_address)) for ap in esp.scan_networks(): - print("\t%s\t\tRSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"])) + print("\t%-23s RSSI: %d" % (ap.ssid, ap.rssi)) print("Connecting to AP...") while not esp.is_connected: try: - esp.connect_AP(secrets["ssid"], secrets["password"]) - except RuntimeError as e: + esp.connect_AP(ssid, password) + except OSError 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)) -print( - "IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com")) -) +print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) +print("My IP address is", esp.ipv4_address) +print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) print("Ping google.com: %d ms" % esp.ping("google.com")) # esp._debug = True diff --git a/examples/esp32spi_simpletest_rp2040.py b/examples/esp32spi_simpletest_rp2040.py index eb52592..aab6776 100644 --- a/examples/esp32spi_simpletest_rp2040.py +++ b/examples/esp32spi_simpletest_rp2040.py @@ -1,24 +1,25 @@ # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT +from os import getenv + +import adafruit_connection_manager +import adafruit_requests import board import busio from digitalio import DigitalInOut -import adafruit_requests as requests -import adafruit_esp32spi.adafruit_esp32spi_socket as socket + from adafruit_esp32spi import adafruit_esp32spi -# 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") print("Raspberry Pi RP2040 - ESP32 SPI webclient test") TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" -JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json" +JSON_URL = "http://wifitest.adafruit.com/testwifi/sample.json" # Raspberry Pi RP2040 Pinout esp32_cs = DigitalInOut(board.GP13) @@ -28,7 +29,9 @@ spi = busio.SPI(board.GP10, board.GP11, board.GP12) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -requests.set_socket(socket, esp) +pool = adafruit_connection_manager.get_radio_socketpool(esp) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) +requests = adafruit_requests.Session(pool, ssl_context) if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: print("ESP32 found and in idle mode") @@ -36,20 +39,18 @@ print("MAC addr:", [hex(i) for i in esp.MAC_address]) for ap in esp.scan_networks(): - print("\t%s\t\tRSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"])) + print("\t%s\t\tRSSI: %d" % (ap.ssid, ap.rssi)) print("Connecting to AP...") while not esp.is_connected: try: - esp.connect_AP(secrets["ssid"], secrets["password"]) - except RuntimeError as e: + esp.connect_AP(ssid, password) + except OSError 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)) -print( - "IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com")) -) +print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) +print("My IP address is", esp.ipv4_address) +print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) print("Ping google.com: %d ms" % esp.ping("google.com")) # esp._debug = True diff --git a/examples/esp32spi_tcp_client.py b/examples/esp32spi_tcp_client.py index d178c20..f781cb9 100644 --- a/examples/esp32spi_tcp_client.py +++ b/examples/esp32spi_tcp_client.py @@ -1,36 +1,48 @@ # SPDX-FileCopyrightText: 2021 Adafruit Industries # SPDX-License-Identifier: MIT +from os import getenv + import board +import busio from digitalio import DigitalInOut + +import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -import adafruit_requests as requests -from secrets import secrets +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") TIMEOUT = 5 # edit host and port to match server HOST = "wifitest.adafruit.com" PORT = 80 +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +elif "SPI" in dir(board): + spi = board.SPI() +else: + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) # PyPortal or similar; edit pins as needed -spi = board.SPI() esp32_cs = DigitalInOut(board.ESP_CS) esp32_ready = DigitalInOut(board.ESP_BUSY) esp32_reset = DigitalInOut(board.ESP_RESET) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) # connect to wifi AP -esp.connect(secrets) +esp.connect(ssid, password) # test for connectivity to server print("Server ping:", esp.ping(HOST), "ms") # create the socket -socket.set_interface(esp) -socketaddr = socket.getaddrinfo(HOST, PORT)[0][4] -s = socket.socket() +pool = socketpool.SocketPool(esp) +socketaddr = pool.getaddrinfo(HOST, PORT)[0][4] +s = pool.socket() s.settimeout(TIMEOUT) print("Connecting") diff --git a/examples/esp32spi_udp_client.py b/examples/esp32spi_udp_client.py index 176e7a8..b1226f9 100644 --- a/examples/esp32spi_udp_client.py +++ b/examples/esp32spi_udp_client.py @@ -3,17 +3,19 @@ import struct import time +from os import getenv + import board +import busio from digitalio import DigitalInOut + +import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -# 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") TIMEOUT = 5 # edit host and port to match server @@ -21,23 +23,29 @@ PORT = 123 NTP_TO_UNIX_EPOCH = 2208988800 # 1970-01-01 00:00:00 +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +elif "SPI" in dir(board): + spi = board.SPI() +else: + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) # PyPortal or similar; edit pins as needed -spi = board.SPI() esp32_cs = DigitalInOut(board.ESP_CS) esp32_ready = DigitalInOut(board.ESP_BUSY) esp32_reset = DigitalInOut(board.ESP_RESET) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) # connect to wifi AP -esp.connect(secrets) +esp.connect(ssid, password) # test for connectivity to server print("Server ping:", esp.ping(HOST), "ms") # create the socket -socket.set_interface(esp) -socketaddr = socket.getaddrinfo(HOST, PORT)[0][4] -s = socket.socket(type=socket.SOCK_DGRAM) +pool = socketpool.SocketPool(esp) +socketaddr = pool.getaddrinfo(HOST, PORT)[0][4] +s = pool.socket(type=pool.SOCK_DGRAM) s.settimeout(TIMEOUT) diff --git a/examples/esp32spi_wpa2ent_aio_post.py b/examples/esp32spi_wpa2ent_aio_post.py index b6bdd99..78e56a2 100644 --- a/examples/esp32spi_wpa2ent_aio_post.py +++ b/examples/esp32spi_wpa2ent_aio_post.py @@ -2,21 +2,29 @@ # SPDX-License-Identifier: MIT import time +from os import getenv + import board import busio -from digitalio import DigitalInOut import neopixel +from digitalio import DigitalInOut + from adafruit_esp32spi import adafruit_esp32spi -from adafruit_esp32spi.adafruit_esp32spi_wifimanager import ESPSPI_WiFiManager +from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager print("ESP32 SPI WPA2 Enterprise webclient 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 +# Get wifi details and more from a settings.toml file +# tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD, +# CIRCUITPY_WIFI_ENT_USER, CIRCUITPY_WIFI_ENT_IDENT, +# ADAFRUIT_AIO_USERNAME, ADAFRUIT_AIO_KEY +ssid = getenv("CIRCUITPY_WIFI_SSID") +password = getenv("CIRCUITPY_WIFI_PASSWORD") +enterprise_ident = getenv("CIRCUITPY_WIFI_ENT_IDENT") +enterprise_user = getenv("CIRCUITPY_WIFI_ENT_USER") + +aio_username = getenv("ADAFRUIT_AIO_USERNAME") +aio_key = getenv("ADAFRUIT_AIO_KEY") # ESP32 setup # If your board does define the three pins listed below, @@ -30,16 +38,33 @@ esp32_ready = DigitalInOut(board.D10) esp32_reset = DigitalInOut(board.D5) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + 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 +status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) """Uncomment below for ItsyBitsy M4""" -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) -wifi = ESPSPI_WiFiManager( - esp, secrets, status_light, connection_type=ESPSPI_WiFiManager.ENTERPRISE +# status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) +"""Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" +# 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_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) + +wifi = WiFiManager( + esp, + ssid, + password, + enterprise_ident=enterprise_ident, + enterprise_user=enterprise_user, + status_pixel=status_pixel, + connection_type=WiFiManager.ENTERPRISE, ) counter = 0 @@ -51,19 +76,15 @@ feed = "test" payload = {"value": data} response = wifi.post( - "https://io.adafruit.com/api/v2/" - + secrets["aio_username"] - + "/feeds/" - + feed - + "/data", + "https://io.adafruit.com/api/v2/" + aio_username + "/feeds/" + feed + "/data", json=payload, - headers={"X-AIO-KEY": secrets["aio_key"]}, + headers={"X-AIO-KEY": aio_key}, ) print(response.json()) response.close() counter = counter + 1 print("OK") - except (ValueError, RuntimeError) as e: + except OSError as e: print("Failed to get data, retrying\n", e) wifi.reset() continue diff --git a/examples/esp32spi_wpa2ent_simpletest.py b/examples/esp32spi_wpa2ent_simpletest.py index 692177f..f4b648e 100644 --- a/examples/esp32spi_wpa2ent_simpletest.py +++ b/examples/esp32spi_wpa2ent_simpletest.py @@ -11,14 +11,16 @@ import re import time + +import adafruit_connection_manager +import adafruit_requests import board import busio from digitalio import DigitalInOut -import adafruit_requests as requests -import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_esp32spi import adafruit_esp32spi + # Version number comparison code. Credit to gnud on stackoverflow # (https://stackoverflow.com/a/1714190), swapping out cmp() to # support Python 3.x and thus, CircuitPython @@ -26,9 +28,7 @@ def version_compare(version1, version2): def normalize(v): return [int(x) for x in re.sub(r"(\.0+)*$", "", v).split(".")] - return (normalize(version1) > normalize(version2)) - ( - normalize(version1) < normalize(version2) - ) + return (normalize(version1) > normalize(version2)) - (normalize(version1) < normalize(version2)) print("ESP32 SPI WPA2 Enterprise test") @@ -45,25 +45,29 @@ def normalize(v): esp32_ready = DigitalInOut(board.D10) esp32_reset = DigitalInOut(board.D5) -spi = busio.SPI(board.SCK, board.MOSI, board.MISO) +# Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 +if "SCK1" in dir(board): + spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) +else: + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) -requests.set_socket(socket, esp) +pool = adafruit_connection_manager.get_radio_socketpool(esp) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) +requests = adafruit_requests.Session(pool, ssl_context) if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: print("ESP32 found and in idle mode") -# Get the ESP32 fw version number, remove trailing byte off the returned bytearray -# and then convert it to a string for prettier printing and later comparison -firmware_version = "".join([chr(b) for b in esp.firmware_version[:-1]]) -print("Firmware vers.", firmware_version) +# Get the ESP32 fw version number +print("Firmware vers.", esp.firmware_version) print("MAC addr:", [hex(i) for i in esp.MAC_address]) # WPA2 Enterprise support was added in fw ver 1.3.0. Check that the ESP32 # is running at least that version, otherwise, bail out assert ( - version_compare(firmware_version, "1.3.0") >= 0 + version_compare(esp.firmware_version, "1.3.0") >= 0 ), "Incorrect ESP32 firmware version; >= 1.3.0 required." # Set up the SSID you would like to connect to @@ -92,11 +96,9 @@ def normalize(v): time.sleep(2) print("") -print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi) -print("My IP address is", esp.pretty_ip(esp.ip_address)) -print( - "IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com")) -) +print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) +print("My IP address is", esp.ipv4_address) +print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) print("Ping google.com: %d ms" % esp.ping("google.com")) print("Done!") diff --git a/examples/gpio/esp32spi_gpio.py b/examples/gpio/esp32spi_gpio.py index a9c857d..91a0fd0 100644 --- a/examples/gpio/esp32spi_gpio.py +++ b/examples/gpio/esp32spi_gpio.py @@ -2,13 +2,14 @@ # # SPDX-License-Identifier: MIT -import time import random +import time + import board -from digitalio import DigitalInOut, Direction import pulseio -from adafruit_esp32spi import adafruit_esp32spi +from digitalio import DigitalInOut, Direction +from adafruit_esp32spi import adafruit_esp32spi # ESP32SPI Digital and Analog Pin Reads & Writes @@ -67,17 +68,9 @@ def esp_init_pin_modes(din, dout): esp_reset_all() -espfirmware = "" -for _ in esp.firmware_version: - if _ != 0: - espfirmware += "{:c}".format(_) -print("ESP32 Firmware:", espfirmware) +print("ESP32 Firmware:", esp.firmware_version) -print( - "ESP32 MAC: {5:02X}:{4:02X}:{3:02X}:{2:02X}:{1:02X}:{0:02X}".format( - *esp.MAC_address - ) -) +print("ESP32 MAC: {5:02X}:{4:02X}:{3:02X}:{2:02X}:{1:02X}:{0:02X}".format(*esp.MAC_address)) # initial digital write values m4_d_w_val = False @@ -94,7 +87,7 @@ def esp_init_pin_modes(din, dout): esp_init_pin_modes(ESP_D_R_PIN, ESP_D_W_PIN) esp_d_r_val = esp.set_digital_read(ESP_D_R_PIN) print("--> ESP read:", esp_d_r_val) - except (RuntimeError, AssertionError) as e: + except OSError as e: print("ESP32 Error", e) esp_reset_all() @@ -104,7 +97,7 @@ def esp_init_pin_modes(din, dout): esp_init_pin_modes(ESP_D_R_PIN, ESP_D_W_PIN) esp.set_digital_write(ESP_D_W_PIN, esp_d_w_val) print("ESP wrote:", esp_d_w_val, "--> Red LED") - except (RuntimeError) as e: + except OSError as e: print("ESP32 Error", e) esp_reset_all() @@ -117,11 +110,11 @@ def esp_init_pin_modes(din, dout): "Potentiometer --> ESP read: ", esp_a_r_val, " (", - "{:1.1f}".format(esp_a_r_val * 3.3 / 65536), + f"{esp_a_r_val * 3.3 / 65536:1.1f}", "v)", sep="", ) - except (RuntimeError, AssertionError) as e: + except OSError as e: print("ESP32 Error", e) esp_reset_all() @@ -132,12 +125,12 @@ def esp_init_pin_modes(din, dout): esp.set_analog_write(ESP_A_W_PIN, esp_a_w_val) print( "ESP wrote: ", - "{:1.2f}".format(esp_a_w_val), + f"{esp_a_w_val:1.2f}", " (", - "{:d}".format(int(esp_a_w_val * 65536)), + f"{int(esp_a_w_val * 65536):d}", ")", " (", - "{:1.1f}".format(esp_a_w_val * 3.3), + f"{esp_a_w_val * 3.3:1.1f}", "v)", sep="", end=" ", @@ -153,12 +146,12 @@ def esp_init_pin_modes(din, dout): duty = M4_A_R_PIN[0] / (M4_A_R_PIN[0] + M4_A_R_PIN[1]) print( "--> M4 read: ", - "{:1.2f}".format(duty), + f"{duty:1.2f}", " (", - "{:d}".format(int(duty * 65536)), + f"{int(duty * 65536):d}", ")", " (", - "{:1.1f}".format(duty * 3.3), + f"{duty * 3.3:1.1f}", "v)", " [len=", len(M4_A_R_PIN), @@ -166,7 +159,7 @@ def esp_init_pin_modes(din, dout): sep="", ) - except (RuntimeError) as e: + except OSError as e: print("ESP32 Error", e) esp_reset_all() diff --git a/examples/gpio/gpio.md b/examples/gpio/gpio.md index 57b8336..42d40a8 100644 --- a/examples/gpio/gpio.md +++ b/examples/gpio/gpio.md @@ -16,7 +16,7 @@ As of NINA firmware version 1.5.0, the ESP32SPI library can be used to read digi ``` # ESP32_GPIO_PINS: - # https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/master/adafruit_esp32spi/digitalio.py + # https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/main/adafruit_esp32spi/digitalio.py # 0, 1, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33, 34, 35, 36, 39 # # Pins Used for ESP32SPI diff --git a/examples/server/esp32spi_wsgiserver.py b/examples/server/esp32spi_wsgiserver.py deleted file mode 100755 index d9f0979..0000000 --- a/examples/server/esp32spi_wsgiserver.py +++ /dev/null @@ -1,246 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -import os -import board -import busio -from digitalio import DigitalInOut -import neopixel - -from adafruit_esp32spi import adafruit_esp32spi -import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager -import adafruit_esp32spi.adafruit_esp32spi_wsgiserver as server - -# This example depends on the 'static' folder in the examples folder -# being copied to the root of the circuitpython filesystem. -# This is where our static assets like html, js, and css live. - -# 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: - import json as json_module -except ImportError: - import ujson as json_module - -print("ESP32 SPI simple web server test!") - -# 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 -) # pylint: disable=line-too-long - -print("MAC addr:", [hex(i) for i in esp.MAC_address]) -print("MAC addr actual:", [hex(i) for i in esp.MAC_address_actual]) - -# Use below for Most Boards -status_light = neopixel.NeoPixel( - board.NEOPIXEL, 1, brightness=0.2 -) # Uncomment for Most Boards -# Uncomment below for ItsyBitsy M4 -# import adafruit_dotstar as dotstar -# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) - -## If you want to connect to wifi with secrets: -wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) -wifi.connect() - -## If you want to create a WIFI hotspot to connect to with secrets: -# secrets = {"ssid": "My ESP32 AP!", "password": "supersecret"} -# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) -# wifi.create_ap() - -## To you want to create an un-protected WIFI hotspot to connect to with secrets:" -# secrets = {"ssid": "My ESP32 AP!"} -# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) -# wifi.create_ap() - - -class SimpleWSGIApplication: - """ - An example of a simple WSGI Application that supports - basic route handling and static asset file serving for common file types - """ - - INDEX = "/index.html" - CHUNK_SIZE = 8912 # max number of bytes to read at once when reading files - - def __init__(self, static_dir=None, debug=False): - self._debug = debug - self._listeners = {} - self._start_response = None - self._static = static_dir - if self._static: - self._static_files = ["/" + file for file in os.listdir(self._static)] - - def __call__(self, environ, start_response): - """ - Called whenever the server gets a request. - The environ dict has details about the request per wsgi specification. - Call start_response with the response status string and headers as a list of tuples. - Return a single item list with the item being your response data string. - """ - if self._debug: - self._log_environ(environ) - - self._start_response = start_response - status = "" - headers = [] - resp_data = [] - - key = self._get_listener_key( - environ["REQUEST_METHOD"].lower(), environ["PATH_INFO"] - ) - if key in self._listeners: - status, headers, resp_data = self._listeners[key](environ) - if environ["REQUEST_METHOD"].lower() == "get" and self._static: - path = environ["PATH_INFO"] - if path in self._static_files: - status, headers, resp_data = self.serve_file( - path, directory=self._static - ) - elif path == "/" and self.INDEX in self._static_files: - status, headers, resp_data = self.serve_file( - self.INDEX, directory=self._static - ) - - self._start_response(status, headers) - return resp_data - - def on(self, method, path, request_handler): - """ - Register a Request Handler for a particular HTTP method and path. - request_handler will be called whenever a matching HTTP request is received. - - request_handler should accept the following args: - (Dict environ) - request_handler should return a tuple in the shape of: - (status, header_list, data_iterable) - - :param str method: the method of the HTTP request - :param str path: the path of the HTTP request - :param func request_handler: the function to call - """ - self._listeners[self._get_listener_key(method, path)] = request_handler - - def serve_file(self, file_path, directory=None): - status = "200 OK" - headers = [("Content-Type", self._get_content_type(file_path))] - - full_path = file_path if not directory else directory + file_path - - def resp_iter(): - with open(full_path, "rb") as file: - while True: - chunk = file.read(self.CHUNK_SIZE) - if chunk: - yield chunk - else: - break - - return (status, headers, resp_iter()) - - def _log_environ(self, environ): # pylint: disable=no-self-use - print("environ map:") - for name, value in environ.items(): - print(name, value) - - def _get_listener_key(self, method, path): # pylint: disable=no-self-use - return "{0}|{1}".format(method.lower(), path) - - def _get_content_type(self, file): # pylint: disable=no-self-use - ext = file.split(".")[-1] - if ext in ("html", "htm"): - return "text/html" - if ext == "js": - return "application/javascript" - if ext == "css": - return "text/css" - if ext in ("jpg", "jpeg"): - return "image/jpeg" - if ext == "png": - return "image/png" - return "text/plain" - - -# Our HTTP Request handlers -def led_on(environ): # pylint: disable=unused-argument - print("led on!") - status_light.fill((0, 0, 100)) - return web_app.serve_file("static/index.html") - - -def led_off(environ): # pylint: disable=unused-argument - print("led off!") - status_light.fill(0) - return web_app.serve_file("static/index.html") - - -def led_color(environ): # pylint: disable=unused-argument - json = json_module.loads(environ["wsgi.input"].getvalue()) - print(json) - rgb_tuple = (json.get("r"), json.get("g"), json.get("b")) - status_light.fill(rgb_tuple) - return ("200 OK", [], []) - - -# Here we create our application, setting the static directory location -# and registering the above request_handlers for specific HTTP requests -# we want to listen and respond to. -static = "/static" -try: - static_files = os.listdir(static) - if "index.html" not in static_files: - raise RuntimeError( - """ - This example depends on an index.html, but it isn't present. - Please add it to the {0} directory""".format( - static - ) - ) -except (OSError) as e: - raise RuntimeError( - """ - This example depends on a static asset directory. - Please create one named {0} in the root of the device filesystem.""".format( - static - ) - ) from e - -web_app = SimpleWSGIApplication(static_dir=static) -web_app.on("GET", "/led_on", led_on) -web_app.on("GET", "/led_off", led_off) -web_app.on("POST", "/ajax/ledcolor", led_color) - -# Here we setup our server, passing in our web_app as the application -server.set_interface(esp) -wsgiServer = server.WSGIServer(80, application=web_app) - -print("open this IP in your browser: ", esp.pretty_ip(esp.ip_address)) - -# Start the server -wsgiServer.start() -while True: - # Our main loop where we have the server poll for incoming requests - try: - wsgiServer.update_poll() - # Could do any other background tasks here, like reading sensors - except (ValueError, RuntimeError) as e: - print("Failed to update server, restarting ESP32\n", e) - wifi.reset() - continue diff --git a/examples/server/static/index.html b/examples/server/static/index.html deleted file mode 100755 index df08ec7..0000000 --- a/examples/server/static/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - -

LED color picker demo!

- - - diff --git a/examples/server/static/led_color_picker_example.js b/examples/server/static/led_color_picker_example.js deleted file mode 100755 index 810ca44..0000000 --- a/examples/server/static/led_color_picker_example.js +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -let canvas = document.getElementById('colorPicker'); -let ctx = canvas.getContext("2d"); -ctx.width = 300; -ctx.height = 300; - -function drawColorPicker() { - /** - * Color picker inspired by: - * https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43 - */ - let radius = 150; - let image = ctx.createImageData(2*radius, 2*radius); - let data = image.data; - - for (let x = -radius; x < radius; x++) { - for (let y = -radius; y < radius; y++) { - - let [r, phi] = xy2polar(x, y); - - if (r > radius) { - // skip all (x,y) coordinates that are outside of the circle - continue; - } - - let deg = rad2deg(phi); - - // Figure out the starting index of this pixel in the image data array. - let rowLength = 2*radius; - let adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array) - let adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array) - let pixelWidth = 4; // each pixel requires 4 slots in the data array - let index = (adjustedX + (adjustedY * rowLength)) * pixelWidth; - - let hue = deg; - let saturation = r / radius; - let value = 1.0; - - let [red, green, blue] = hsv2rgb(hue, saturation, value); - let alpha = 255; - - data[index] = red; - data[index+1] = green; - data[index+2] = blue; - data[index+3] = alpha; - } - } - - ctx.putImageData(image, 0, 0); -} - -function xy2polar(x, y) { - let r = Math.sqrt(x*x + y*y); - let phi = Math.atan2(y, x); - return [r, phi]; -} - -// rad in [-π, π] range -// return degree in [0, 360] range -function rad2deg(rad) { - return ((rad + Math.PI) / (2 * Math.PI)) * 360; -} - - // hue in range [0, 360] - // saturation, value in range [0,1] - // return [r,g,b] each in range [0,255] - // See: https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV -function hsv2rgb(hue, saturation, value) { - let chroma = value * saturation; - let hue1 = hue / 60; - let x = chroma * (1- Math.abs((hue1 % 2) - 1)); - let r1, g1, b1; - if (hue1 >= 0 && hue1 <= 1) { - ([r1, g1, b1] = [chroma, x, 0]); - } else if (hue1 >= 1 && hue1 <= 2) { - ([r1, g1, b1] = [x, chroma, 0]); - } else if (hue1 >= 2 && hue1 <= 3) { - ([r1, g1, b1] = [0, chroma, x]); - } else if (hue1 >= 3 && hue1 <= 4) { - ([r1, g1, b1] = [0, x, chroma]); - } else if (hue1 >= 4 && hue1 <= 5) { - ([r1, g1, b1] = [x, 0, chroma]); - } else if (hue1 >= 5 && hue1 <= 6) { - ([r1, g1, b1] = [chroma, 0, x]); - } - - let m = value - chroma; - let [r,g,b] = [r1+m, g1+m, b1+m]; - - // Change r,g,b values from [0,1] to [0,255] - return [255*r,255*g,255*b]; -} - -function onColorPick(event) { - coords = getCursorPosition(canvas, event) - imageData = ctx.getImageData(coords[0],coords[1],1,1) - rgbObject = { - r: imageData.data[0], - g: imageData.data[1], - b: imageData.data[2] - } - console.log(`r: ${rgbObject.r} g: ${rgbObject.g} b: ${rgbObject.b}`); - data = JSON.stringify(rgbObject); - window.fetch("/ajax/ledcolor", { - method: "POST", - body: data, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }).then(response => { - console.log("sucess!: " + response) - }, error => { - console.log("error!: " + error) - }) -} - -function getCursorPosition(canvas, event) { - const rect = canvas.getBoundingClientRect() - const x = event.clientX - rect.left - const y = event.clientY - rect.top - console.log("x: " + x + " y: " + y) - return [x,y] -} - -drawColorPicker(); -canvas.addEventListener('mousedown', onColorPick); diff --git a/optional_requirements.txt b/optional_requirements.txt new file mode 100644 index 0000000..f2e3a6c --- /dev/null +++ b/optional_requirements.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +adafruit-circuitpython-neopixel +adafruit-circuitpython-fancyled +adafruit-circuitpython-requests diff --git a/pyproject.toml b/pyproject.toml index f3c35ae..3c42c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,46 @@ -# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT -[tool.black] -target-version = ['py35'] +[build-system] +requires = [ + "setuptools", + "wheel", + "setuptools-scm", +] + +[project] +name = "adafruit-circuitpython-esp32spi" +description = "CircuitPython driver library for using ESP32 as WiFi co-processor using SPI" +version = "0.0.0+auto.0" +readme = "README.rst" +authors = [ + {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} +] +urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI"} +keywords = [ + "adafruit", + "blinka", + "circuitpython", + "micropython", + "esp32spi", + "wifi", + "esp32", +] +license = {text = "MIT"} +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +dynamic = ["dependencies", "optional-dependencies"] + +[tool.setuptools] +packages = ["adafruit_esp32spi"] + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} diff --git a/requirements.txt b/requirements.txt index e50bb10..d689206 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense Adafruit-Blinka adafruit-circuitpython-busdevice +adafruit-circuitpython-connectionmanager +adafruit-circuitpython-requests diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..36332ff --- /dev/null +++ b/ruff.toml @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +target-version = "py38" +line-length = 100 + +[lint] +preview = true +select = ["I", "PL", "UP"] + +extend-select = [ + "D419", # empty-docstring + "E501", # line-too-long + "W291", # trailing-whitespace + "PLC0414", # useless-import-alias + "PLC2401", # non-ascii-name + "PLC2801", # unnecessary-dunder-call + "PLC3002", # unnecessary-direct-lambda-call + "E999", # syntax-error + "PLE0101", # return-in-init + "F706", # return-outside-function + "F704", # yield-outside-function + "PLE0116", # continue-in-finally + "PLE0117", # nonlocal-without-binding + "PLE0241", # duplicate-bases + "PLE0302", # unexpected-special-method-signature + "PLE0604", # invalid-all-object + "PLE0605", # invalid-all-format + "PLE0643", # potential-index-error + "PLE0704", # misplaced-bare-raise + "PLE1141", # dict-iter-missing-items + "PLE1142", # await-outside-async + "PLE1205", # logging-too-many-args + "PLE1206", # logging-too-few-args + "PLE1307", # bad-string-format-type + "PLE1310", # bad-str-strip-call + "PLE1507", # invalid-envvar-value + "PLE2502", # bidirectional-unicode + "PLE2510", # invalid-character-backspace + "PLE2512", # invalid-character-sub + "PLE2513", # invalid-character-esc + "PLE2514", # invalid-character-nul + "PLE2515", # invalid-character-zero-width-space + "PLR0124", # comparison-with-itself + "PLR0202", # no-classmethod-decorator + "PLR0203", # no-staticmethod-decorator + "UP004", # useless-object-inheritance + "PLR0206", # property-with-parameters + "PLR0904", # too-many-public-methods + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0914", # too-many-locals + "PLR0915", # too-many-statements + "PLR0916", # too-many-boolean-expressions + "PLR1702", # too-many-nested-blocks + "PLR1704", # redefined-argument-from-local + "PLR1711", # useless-return + "C416", # unnecessary-comprehension + "PLR1733", # unnecessary-dict-index-lookup + "PLR1736", # unnecessary-list-index-lookup + + # ruff reports this rule is unstable + #"PLR6301", # no-self-use + + "PLW0108", # unnecessary-lambda + "PLW0120", # useless-else-on-loop + "PLW0127", # self-assigning-variable + "PLW0129", # assert-on-string-literal + "B033", # duplicate-value + "PLW0131", # named-expr-without-context + "PLW0245", # super-without-brackets + "PLW0406", # import-self + "PLW0602", # global-variable-not-assigned + "PLW0603", # global-statement + "PLW0604", # global-at-module-level + + # fails on the try: import typing used by libraries + #"F401", # unused-import + + "F841", # unused-variable + "E722", # bare-except + "PLW0711", # binary-op-exception + "PLW1501", # bad-open-mode + "PLW1508", # invalid-envvar-default + "PLW1509", # subprocess-popen-preexec-fn + "PLW2101", # useless-with-lock + "PLW3301", # nested-min-max +] + +ignore = [ + "PLR2004", # magic-value-comparison + "UP030", # format literals + "PLW1514", # unspecified-encoding + "PLR0913", # too-many-arguments + "PLR0915", # too-many-statements + "PLR0917", # too-many-positional-arguments + "PLR0904", # too-many-public-methods + "PLR0912", # too-many-branches + "PLR0916", # too-many-boolean-expressions +] + +[format] +line-ending = "lf" diff --git a/setup.py b/setup.py deleted file mode 100644 index e9829b4..0000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -"""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-esp32spi", - use_scm_version=True, - setup_requires=["setuptools_scm"], - description="CircuitPython driver library for using ESP32 as WiFi co-processor using SPI", - long_description=long_description, - long_description_content_type="text/x-rst", - # The project's main homepage. - url="https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI", - # Author details - author="Adafruit Industries", - author_email="circuitpython@adafruit.com", - install_requires=["Adafruit-Blinka", "adafruit-circuitpython-busdevice"], - # 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 esp32spi wifi esp32", - # 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=['...']` - packages=["adafruit_esp32spi"], -)