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"], -) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy