├── README.rst.license ├── docs ├── api.rst.license ├── _static │ ├── favicon.ico │ ├── favicon.ico.license │ └── custom.css ├── examples.rst.license ├── index.rst.license ├── requirements.txt ├── examples.rst ├── index.rst ├── api.rst └── conf.py ├── optional_requirements.txt ├── requirements.txt ├── .gitattributes ├── .github ├── workflows │ ├── build.yml │ ├── release_pypi.yml │ ├── release_gh.yml │ └── failure-help-text.yml └── PULL_REQUEST_TEMPLATE │ └── adafruit_circuitpython_pr.md ├── adafruit_esp32spi ├── __init__.py ├── socketpool.py ├── wifimanager.py ├── PWMOut.py ├── digitalio.py ├── adafruit_esp32spi_socketpool.py ├── adafruit_esp32spi_wifimanager.py └── adafruit_esp32spi.py ├── .readthedocs.yaml ├── examples ├── esp32spi_settings.toml ├── esp32spi_tcp_client.py ├── esp32spi_udp_client.py ├── esp32spi_simpletest_rp2040.py ├── esp32spi_aio_post.py ├── esp32spi_localtime.py ├── esp32spi_simpletest.py ├── esp32spi_ipconfig.py ├── esp32spi_cheerlights.py ├── esp32spi_wpa2ent_aio_post.py ├── esp32spi_wpa2ent_simpletest.py └── gpio │ ├── esp32spi_gpio.py │ └── gpio.md ├── .pre-commit-config.yaml ├── LICENSE ├── LICENSES ├── MIT.txt ├── Unlicense.txt └── CC-BY-4.0.txt ├── pyproject.toml ├── .gitignore ├── README.rst ├── ruff.toml └── CODE_OF_CONDUCT.md /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_ESP32SPI/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | sphinx 6 | sphinxcontrib-jquery 7 | sphinx-rtd-theme 8 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | adafruit-circuitpython-neopixel 6 | adafruit-circuitpython-fancyled 7 | adafruit-circuitpython-requests 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | Adafruit-Blinka 6 | adafruit-circuitpython-busdevice 7 | adafruit-circuitpython-connectionmanager 8 | adafruit-circuitpython-requests 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | .py text eol=lf 6 | .rst text eol=lf 7 | .txt text eol=lf 8 | .yaml text eol=lf 9 | .toml text eol=lf 10 | .license text eol=lf 11 | .md text eol=lf 12 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Sam Blenny 2 | * SPDX-License-Identifier: MIT 3 | */ 4 | 5 | /* Monkey patch the rtd theme to prevent horizontal stacking of short items 6 | * see https://github.com/readthedocs/sphinx_rtd_theme/issues/1301 7 | */ 8 | .py.property{display: block !important;} 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run Build CI workflow 14 | uses: adafruit/workflows-circuitpython-libs/build@main 15 | -------------------------------------------------------------------------------- /adafruit_esp32spi/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Dan Halbert for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Allow 6 | # import adafruit_esp32spi 7 | # from adafruit_esp32spi.ESP_SPIcontrol 8 | # etc. 9 | # instead of the more verbose 10 | # import adafruit_esp32pi.adafruit_esp32spi 11 | # etc. 12 | 13 | from .adafruit_esp32spi import * 14 | -------------------------------------------------------------------------------- /adafruit_esp32spi/socketpool.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Dan Halbert for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Allow 6 | # from adafruit_esp32pi.socketpool import SocketPool 7 | # instead of the more verbose 8 | # from adafruit_esp32pi.adafruit_esp32spi_socketpool import SocketPool 9 | 10 | from .adafruit_esp32spi_socketpool import * 11 | -------------------------------------------------------------------------------- /adafruit_esp32spi/wifimanager.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Dan Halbert for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Allow imports like 6 | # from adafruit_esp32pi.wifimanager import WiFiManager 7 | # instead of the more verbose 8 | # from adafruit_esp32pi.adafruit_esp32spi_wifimanager import WiFiManager 9 | 10 | from .adafruit_esp32spi_wifimanager import * 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # Read the Docs configuration file 6 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 7 | 8 | # Required 9 | version: 2 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: ubuntu-lts-latest 16 | tools: 17 | python: "3" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Simple test 2 | ------------ 3 | 4 | Ensure your device works with this simple test. 5 | 6 | .. literalinclude:: ../examples/esp32spi_simpletest.py 7 | :caption: examples/esp32spi_simpletest.py 8 | :linenos: 9 | 10 | 11 | Other Examples 12 | --------------- 13 | 14 | .. literalinclude:: ../examples/esp32spi_cheerlights.py 15 | :caption: examples/esp32spi_cheerlights.py 16 | :linenos: 17 | 18 | .. literalinclude:: ../examples/esp32spi_aio_post.py 19 | :caption: examples/esp32spi_aio_post.py 20 | :linenos: 21 | -------------------------------------------------------------------------------- /.github/workflows/release_pypi.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: PyPI Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run PyPI Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-pypi@main 17 | with: 18 | pypi-username: ${{ secrets.pypi_username }} 19 | pypi-password: ${{ secrets.pypi_password }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release_gh.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: GitHub Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run GitHub Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-gh@main 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | upload-url: ${{ github.event.release.upload_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/failure-help-text.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Failure help text 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Build CI"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | post-help: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - name: Post comment to help 19 | uses: adafruit/circuitpython-action-library-ci-failed@v1 20 | -------------------------------------------------------------------------------- /examples/esp32spi_settings.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # This file is where you keep secret settings, passwords, and tokens! 5 | # If you put them in the code you risk committing that info or sharing it 6 | 7 | # The file should be renamed to `settings.toml` and saved in the root of 8 | # the CIRCUITPY drive. 9 | 10 | CIRCUITPY_WIFI_SSID="yourssid" 11 | CIRCUITPY_WIFI_PASSWORD="yourpassword" 12 | CIRCUITPY_TIMEZONE="America/New_York" 13 | ADAFRUIT_AIO_USERNAME="youraiousername" 14 | ADAFRUIT_AIO_KEY="youraiokey" 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.3.4 14 | hooks: 15 | - id: ruff-format 16 | - id: ruff 17 | args: ["--fix"] 18 | - repo: https://github.com/fsfe/reuse-tool 19 | rev: v3.0.1 20 | hooks: 21 | - id: reuse 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Thank you for contributing! Before you submit a pull request, please read the following. 6 | 7 | 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 8 | 9 | If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs 10 | 11 | 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 12 | 13 | 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. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 ladyada for Adafruit Industries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :hidden: 9 | 10 | self 11 | 12 | .. toctree:: 13 | :caption: Examples 14 | 15 | examples 16 | 17 | .. toctree:: 18 | :caption: API Reference 19 | :maxdepth: 3 20 | 21 | api 22 | 23 | .. toctree:: 24 | :caption: Tutorials 25 | 26 | .. toctree:: 27 | :caption: Related Products 28 | 29 | .. toctree:: 30 | :caption: Other Links 31 | 32 | Download from GitHub 33 | Download Library Bundle 34 | CircuitPython Reference Documentation 35 | CircuitPython Support Forum 36 | Discord Chat 37 | Adafruit Learning System 38 | Adafruit Blog 39 | Adafruit Store 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools", 8 | "wheel", 9 | "setuptools-scm", 10 | ] 11 | 12 | [project] 13 | name = "adafruit-circuitpython-esp32spi" 14 | description = "CircuitPython driver library for using ESP32 as WiFi co-processor using SPI" 15 | version = "0.0.0+auto.0" 16 | readme = "README.rst" 17 | authors = [ 18 | {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} 19 | ] 20 | urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI"} 21 | keywords = [ 22 | "adafruit", 23 | "blinka", 24 | "circuitpython", 25 | "micropython", 26 | "esp32spi", 27 | "wifi", 28 | "esp32", 29 | ] 30 | license = {text = "MIT"} 31 | classifiers = [ 32 | "Intended Audience :: Developers", 33 | "Topic :: Software Development :: Libraries", 34 | "Topic :: Software Development :: Embedded Systems", 35 | "Topic :: System :: Hardware", 36 | "License :: OSI Approved :: MIT License", 37 | "Programming Language :: Python :: 3", 38 | ] 39 | dynamic = ["dependencies", "optional-dependencies"] 40 | 41 | [tool.setuptools] 42 | packages = ["adafruit_esp32spi"] 43 | 44 | [tool.setuptools.dynamic] 45 | dependencies = {file = ["requirements.txt"]} 46 | optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Do not include files and directories created by your personal work environment, such as the IDE 6 | # you use, except for those already listed here. Pull requests including changes to this file will 7 | # not be accepted. 8 | 9 | # This .gitignore file contains rules for files generated by working with CircuitPython libraries, 10 | # including building Sphinx, testing with pip, and creating a virual environment, as well as the 11 | # MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. 12 | 13 | # If you find that there are files being generated on your machine that should not be included in 14 | # your git commit, you should create a .gitignore_global file on your computer to include the 15 | # files created by your personal setup. To do so, follow the two steps below. 16 | 17 | # First, create a file called .gitignore_global somewhere convenient for you, and add rules for 18 | # the files you want to exclude from git commits. 19 | 20 | # Second, configure Git to use the exclude file for all Git repositories by running the 21 | # following via commandline, replacing "path/to/your/" with the actual path to your newly created 22 | # .gitignore_global file: 23 | # git config --global core.excludesfile path/to/your/.gitignore_global 24 | 25 | # CircuitPython-specific files 26 | *.mpy 27 | 28 | # Python-specific files 29 | __pycache__ 30 | *.pyc 31 | 32 | # Sphinx build-specific files 33 | _build 34 | 35 | # This file results from running `pip -e install .` in a local repository 36 | *.egg-info 37 | 38 | # Virtual environment-specific files 39 | .env 40 | .venv 41 | 42 | # MacOS-specific files 43 | *.DS_Store 44 | 45 | # IDE-specific files 46 | .idea 47 | .vscode 48 | *~ 49 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ############# 3 | 4 | .. automodule:: adafruit_esp32spi.adafruit_esp32spi 5 | 6 | .. note:: 7 | As of version 11.0.0, it simpler to import this library and its submodules 8 | The examples in this documentation use the new import names. 9 | The old import names are still available, but are deprecated and may be removed in a future release. 10 | 11 | Before version 11.0.0, the library was structured like this (not all components are shown): 12 | 13 | * ``adafruit_esp32spi`` 14 | 15 | * ``adafruit_esp32spi`` 16 | 17 | * ``ESP32_SPIcontrol`` 18 | 19 | * ``adafruit_esp32spi_socketpool`` 20 | 21 | * ``SocketPool`` 22 | 23 | * ``adafruit_esp32spi_wifimanager`` 24 | 25 | * ``WiFiManager`` 26 | 27 | .. code:: python 28 | 29 | # Old import scheme 30 | from adafruit_esp32spi import adafruit_esp32spi 31 | from adafruit_esp32spi.adafruit_esp32spi_socketpool import SocketPool 32 | from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager 33 | 34 | Now, the duplicated top-most name is not needed, and there are shorter names for the submodules. 35 | 36 | * ``adafruit_esp32spi`` 37 | 38 | * ``ESP32_SPIcontrol`` 39 | 40 | * ``socketpool`` 41 | 42 | * ``SocketPool`` 43 | 44 | * ``wifimanager`` 45 | 46 | * ``WiFiManager`` 47 | 48 | .. code:: python 49 | 50 | # New import scheme 51 | import adafruit_esp32spi 52 | from adafruit_esp32spi.socketpool import SocketPool 53 | from adafruit_esp32spi.wifimanager import WiFiManager 54 | 55 | 56 | .. automodule:: adafruit_esp32spi 57 | :imported-members: 58 | :members: 59 | 60 | .. automodule:: adafruit_esp32spi.socketpool 61 | :imported-members: 62 | :members: 63 | 64 | .. automodule:: adafruit_esp32spi.wifimanager 65 | :imported-members: 66 | :members: 67 | 68 | .. automodule:: adafruit_esp32spi.digitalio 69 | :members: 70 | 71 | .. automodule:: adafruit_esp32spi.PWMOut 72 | :members: 73 | -------------------------------------------------------------------------------- /examples/esp32spi_tcp_client.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | from os import getenv 5 | 6 | import board 7 | import busio 8 | from digitalio import DigitalInOut 9 | 10 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 11 | # Note that frozen libraries may not be up to date. 12 | # import adafruit_esp32spi 13 | # from adafruit_esp32spi import socketpool 14 | import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool 15 | from adafruit_esp32spi import adafruit_esp32spi 16 | 17 | # Get wifi details and more from a settings.toml file 18 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 19 | ssid = getenv("CIRCUITPY_WIFI_SSID") 20 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | TIMEOUT = 5 23 | # edit host and port to match server 24 | HOST = "wifitest.adafruit.com" 25 | PORT = 80 26 | 27 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 28 | if "SCK1" in dir(board): 29 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 30 | elif "SPI" in dir(board): 31 | spi = board.SPI() 32 | else: 33 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 34 | # PyPortal or similar; edit pins as needed 35 | esp32_cs = DigitalInOut(board.ESP_CS) 36 | esp32_ready = DigitalInOut(board.ESP_BUSY) 37 | esp32_reset = DigitalInOut(board.ESP_RESET) 38 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 39 | 40 | # connect to wifi AP 41 | esp.connect(ssid, password) 42 | 43 | # test for connectivity to server 44 | print("Server ping:", esp.ping(HOST), "ms") 45 | 46 | # create the socket 47 | pool = socketpool.SocketPool(esp) 48 | socketaddr = pool.getaddrinfo(HOST, PORT)[0][4] 49 | s = pool.socket() 50 | s.settimeout(TIMEOUT) 51 | 52 | print("Connecting") 53 | s.connect(socketaddr) 54 | 55 | print("Sending") 56 | s.send(b"GET /testwifi/index.html HTTP/1.0\r\n\r\n") 57 | 58 | print("Receiving") 59 | print(s.recv(1024)) 60 | -------------------------------------------------------------------------------- /examples/esp32spi_udp_client.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import struct 5 | import time 6 | from os import getenv 7 | 8 | import board 9 | import busio 10 | from digitalio import DigitalInOut 11 | 12 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 13 | # Note that frozen libraries may not be up to date. 14 | # import adafruit_esp32spi 15 | # from adafruit_esp32spi import socketpool 16 | import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool 17 | from adafruit_esp32spi import adafruit_esp32spi 18 | 19 | # Get wifi details and more from a settings.toml file 20 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 21 | ssid = getenv("CIRCUITPY_WIFI_SSID") 22 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 23 | 24 | TIMEOUT = 5 25 | # edit host and port to match server 26 | HOST = "pool.ntp.org" 27 | PORT = 123 28 | NTP_TO_UNIX_EPOCH = 2208988800 # 1970-01-01 00:00:00 29 | 30 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 31 | if "SCK1" in dir(board): 32 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 33 | elif "SPI" in dir(board): 34 | spi = board.SPI() 35 | else: 36 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 37 | # PyPortal or similar; edit pins as needed 38 | esp32_cs = DigitalInOut(board.ESP_CS) 39 | esp32_ready = DigitalInOut(board.ESP_BUSY) 40 | esp32_reset = DigitalInOut(board.ESP_RESET) 41 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 42 | 43 | # connect to wifi AP 44 | esp.connect(ssid, password) 45 | 46 | # test for connectivity to server 47 | print("Server ping:", esp.ping(HOST), "ms") 48 | 49 | # create the socket 50 | pool = socketpool.SocketPool(esp) 51 | socketaddr = pool.getaddrinfo(HOST, PORT)[0][4] 52 | s = pool.socket(type=pool.SOCK_DGRAM) 53 | 54 | s.settimeout(TIMEOUT) 55 | 56 | print("Sending") 57 | s.connect(socketaddr, conntype=esp.UDP_MODE) 58 | packet = bytearray(48) 59 | packet[0] = 0b00100011 # Not leap second, NTP version 4, Client mode 60 | s.send(packet) 61 | 62 | print("Receiving") 63 | packet = s.recv(48) 64 | seconds = struct.unpack_from("!I", packet, offset=len(packet) - 8)[0] 65 | print("Time:", time.localtime(seconds - NTP_TO_UNIX_EPOCH)) 66 | -------------------------------------------------------------------------------- /examples/esp32spi_simpletest_rp2040.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | from os import getenv 5 | 6 | import adafruit_connection_manager 7 | import adafruit_requests 8 | import board 9 | import busio 10 | from digitalio import DigitalInOut 11 | 12 | # Use this import for adafruit_esp32spi version 11.0.0 and up. 13 | # Note that frozen libraries may not be up to date. 14 | # import adafruit_esp32spi 15 | from adafruit_esp32spi import adafruit_esp32spi 16 | 17 | # Get wifi details and more from a settings.toml file 18 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 19 | ssid = getenv("CIRCUITPY_WIFI_SSID") 20 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | print("Raspberry Pi RP2040 - ESP32 SPI webclient test") 23 | 24 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 25 | JSON_URL = "http://wifitest.adafruit.com/testwifi/sample.json" 26 | 27 | # Raspberry Pi RP2040 Pinout 28 | esp32_cs = DigitalInOut(board.GP13) 29 | esp32_ready = DigitalInOut(board.GP14) 30 | esp32_reset = DigitalInOut(board.GP15) 31 | 32 | spi = busio.SPI(board.GP10, board.GP11, board.GP12) 33 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 34 | 35 | pool = adafruit_connection_manager.get_radio_socketpool(esp) 36 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) 37 | requests = adafruit_requests.Session(pool, ssl_context) 38 | 39 | if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: 40 | print("ESP32 found and in idle mode") 41 | print("Firmware vers.", esp.firmware_version) 42 | print("MAC addr:", [hex(i) for i in esp.MAC_address]) 43 | 44 | for ap in esp.scan_networks(): 45 | print("\t%s\t\tRSSI: %d" % (ap.ssid, ap.rssi)) 46 | 47 | print("Connecting to AP...") 48 | while not esp.is_connected: 49 | try: 50 | esp.connect_AP(ssid, password) 51 | except OSError as e: 52 | print("could not connect to AP, retrying: ", e) 53 | continue 54 | print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) 55 | print("My IP address is", esp.ipv4_address) 56 | print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) 57 | print("Ping google.com: %d ms" % esp.ping("google.com")) 58 | 59 | # esp._debug = True 60 | print("Fetching text from", TEXT_URL) 61 | r = requests.get(TEXT_URL) 62 | print("-" * 40) 63 | print(r.text) 64 | print("-" * 40) 65 | r.close() 66 | 67 | print() 68 | print("Fetching json from", JSON_URL) 69 | r = requests.get(JSON_URL) 70 | print("-" * 40) 71 | print(r.json()) 72 | print("-" * 40) 73 | r.close() 74 | 75 | print("Done!") 76 | -------------------------------------------------------------------------------- /examples/esp32spi_aio_post.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import board 8 | import busio 9 | import neopixel 10 | from digitalio import DigitalInOut 11 | 12 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 13 | # Note that frozen libraries may not be up to date. 14 | # import adafruit_esp32spi 15 | # from adafruit_esp32spi.wifimanager import WiFiManager 16 | from adafruit_esp32spi import adafruit_esp32spi 17 | from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager 18 | 19 | print("ESP32 SPI webclient test") 20 | 21 | # Get wifi details and more from a settings.toml file 22 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 23 | # ADAFRUIT_AIO_USERNAME, ADAFRUIT_AIO_KEY 24 | ssid = getenv("CIRCUITPY_WIFI_SSID") 25 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 26 | 27 | aio_username = getenv("ADAFRUIT_AIO_USERNAME") 28 | aio_key = getenv("ADAFRUIT_AIO_KEY") 29 | 30 | # If you are using a board with pre-defined ESP32 Pins: 31 | esp32_cs = DigitalInOut(board.ESP_CS) 32 | esp32_ready = DigitalInOut(board.ESP_BUSY) 33 | esp32_reset = DigitalInOut(board.ESP_RESET) 34 | 35 | # If you have an externally connected ESP32: 36 | # esp32_cs = DigitalInOut(board.D9) 37 | # esp32_ready = DigitalInOut(board.D10) 38 | # esp32_reset = DigitalInOut(board.D5) 39 | 40 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 41 | if "SCK1" in dir(board): 42 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 43 | else: 44 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 45 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 46 | """Use below for Most Boards""" 47 | status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 48 | """Uncomment below for ItsyBitsy M4""" 49 | # status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) 50 | """Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" 51 | # import adafruit_rgbled 52 | # from adafruit_esp32spi import PWMOut 53 | # RED_LED = PWMOut.PWMOut(esp, 26) 54 | # GREEN_LED = PWMOut.PWMOut(esp, 27) 55 | # BLUE_LED = PWMOut.PWMOut(esp, 25) 56 | # status_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) 57 | 58 | wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) 59 | 60 | counter = 0 61 | 62 | while True: 63 | try: 64 | print("Posting data...", end="") 65 | data = counter 66 | feed = "test" 67 | payload = {"value": data} 68 | response = wifi.post( 69 | "https://io.adafruit.com/api/v2/" + aio_username + "/feeds/" + feed + "/data", 70 | json=payload, 71 | headers={"X-AIO-KEY": aio_key}, 72 | ) 73 | print(response.json()) 74 | response.close() 75 | counter = counter + 1 76 | print("OK") 77 | except OSError as e: 78 | print("Failed to get data, retrying\n", e) 79 | wifi.reset() 80 | continue 81 | response = None 82 | time.sleep(15) 83 | -------------------------------------------------------------------------------- /examples/esp32spi_localtime.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import board 8 | import busio 9 | import neopixel 10 | import rtc 11 | from digitalio import DigitalInOut 12 | 13 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 14 | # Note that frozen libraries may not be up to date. 15 | # import adafruit_esp32spi 16 | # from adafruit_esp32spi.wifimanager import WiFiManager 17 | from adafruit_esp32spi import adafruit_esp32spi 18 | from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager 19 | 20 | # Get wifi details and more from a settings.toml file 21 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 22 | ssid = getenv("CIRCUITPY_WIFI_SSID") 23 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 24 | 25 | print("ESP32 local time") 26 | 27 | TIME_API = "http://worldtimeapi.org/api/ip" 28 | 29 | # If you are using a board with pre-defined ESP32 Pins: 30 | esp32_cs = DigitalInOut(board.ESP_CS) 31 | esp32_ready = DigitalInOut(board.ESP_BUSY) 32 | esp32_reset = DigitalInOut(board.ESP_RESET) 33 | 34 | # If you have an externally connected ESP32: 35 | # esp32_cs = DigitalInOut(board.D9) 36 | # esp32_ready = DigitalInOut(board.D10) 37 | # esp32_reset = DigitalInOut(board.D5) 38 | 39 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 40 | if "SCK1" in dir(board): 41 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 42 | else: 43 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 44 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 45 | 46 | """Use below for Most Boards""" 47 | status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 48 | """Uncomment below for ItsyBitsy M4""" 49 | # status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) 50 | """Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" 51 | # import adafruit_rgbled 52 | # from adafruit_esp32spi import PWMOut 53 | # RED_LED = PWMOut.PWMOut(esp, 26) 54 | # GREEN_LED = PWMOut.PWMOut(esp, 27) 55 | # BLUE_LED = PWMOut.PWMOut(esp, 25) 56 | # status_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) 57 | 58 | wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) 59 | 60 | the_rtc = rtc.RTC() 61 | 62 | response = None 63 | while True: 64 | try: 65 | print("Fetching json from", TIME_API) 66 | response = wifi.get(TIME_API) 67 | break 68 | except OSError as e: 69 | print("Failed to get data, retrying\n", e) 70 | continue 71 | 72 | json = response.json() 73 | current_time = json["datetime"] 74 | the_date, the_time = current_time.split("T") 75 | year, month, mday = (int(x) for x in the_date.split("-")) 76 | the_time = the_time.split(".")[0] 77 | hours, minutes, seconds = (int(x) for x in the_time.split(":")) 78 | 79 | # We can also fill in these extra nice things 80 | year_day = json["day_of_year"] 81 | week_day = json["day_of_week"] 82 | is_dst = json["dst"] 83 | 84 | now = time.struct_time((year, month, mday, hours, minutes, seconds, week_day, year_day, is_dst)) 85 | print(now) 86 | the_rtc.datetime = now 87 | 88 | while True: 89 | print(time.localtime()) 90 | time.sleep(1) 91 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. image:: https://readthedocs.org/projects/adafruit-circuitpython-esp32spi/badge/?version=latest 5 | :target: https://docs.circuitpython.org/projects/esp32spi/en/latest/ 6 | :alt: Documentation Status 7 | 8 | .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg 9 | :target: https://adafru.it/discord 10 | :alt: Discord 11 | 12 | .. image:: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/workflows/Build%20CI/badge.svg 13 | :target: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/actions/ 14 | :alt: Build Status 15 | 16 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 17 | :target: https://github.com/astral-sh/ruff 18 | :alt: Code Style: Ruff 19 | 20 | CircuitPython driver library for using ESP32 as WiFi co-processor using SPI. 21 | The companion firmware `is available on GitHub 22 | `_. Please be sure to check the example code for 23 | any specific firmware version dependencies that may exist. 24 | 25 | 26 | Dependencies 27 | ============= 28 | This driver depends on: 29 | 30 | * `Adafruit CircuitPython `_ 31 | * `Adafruit Bus Device `_ 32 | * `Adafruit CircuitPython ConnectionManager `_ 33 | * `Adafruit CircuitPython Requests `_ 34 | 35 | Please ensure all dependencies are available on the CircuitPython filesystem. 36 | This is easily achieved by downloading 37 | `the Adafruit library and driver bundle `_. 38 | 39 | Installing from PyPI 40 | ==================== 41 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 42 | PyPI `_. To install for current user: 43 | 44 | .. code-block:: shell 45 | 46 | pip3 install adafruit-circuitpython-esp32spi 47 | 48 | To install system-wide (this may be required in some cases): 49 | 50 | .. code-block:: shell 51 | 52 | sudo pip3 install adafruit-circuitpython-esp32spi 53 | 54 | To install in a virtual environment in your current project: 55 | 56 | .. code-block:: shell 57 | 58 | mkdir project-name && cd project-name 59 | python3 -m venv .venv 60 | source .venv/bin/activate 61 | pip3 install adafruit-circuitpython-esp32spi 62 | 63 | Usage Example 64 | ============= 65 | 66 | Check the examples folder for various demos for connecting and fetching data! 67 | 68 | Documentation 69 | ============= 70 | 71 | API documentation for this library can be found on `Read the Docs `_. 72 | 73 | For information on building library documentation, please check out `this guide `_. 74 | 75 | Contributing 76 | ============ 77 | 78 | Contributions are welcome! Please read our `Code of Conduct 79 | `_ 80 | before contributing to help this project stay welcoming. 81 | -------------------------------------------------------------------------------- /examples/esp32spi_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | from os import getenv 5 | 6 | import adafruit_connection_manager 7 | import adafruit_requests 8 | import board 9 | import busio 10 | from digitalio import DigitalInOut 11 | 12 | # Use this import for adafruit_esp32spi version 11.0.0 and up. 13 | # Note that frozen libraries may not be up to date. 14 | # import adafruit_esp32spi 15 | from adafruit_esp32spi import adafruit_esp32spi 16 | 17 | # Get wifi details and more from a settings.toml file 18 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 19 | ssid = getenv("CIRCUITPY_WIFI_SSID") 20 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | print("ESP32 SPI webclient test") 23 | 24 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 25 | JSON_URL = "http://wifitest.adafruit.com/testwifi/sample.json" 26 | 27 | 28 | # If you are using a board with pre-defined ESP32 Pins: 29 | esp32_cs = DigitalInOut(board.ESP_CS) 30 | esp32_ready = DigitalInOut(board.ESP_BUSY) 31 | esp32_reset = DigitalInOut(board.ESP_RESET) 32 | 33 | # If you have an AirLift Shield: 34 | # esp32_cs = DigitalInOut(board.D10) 35 | # esp32_ready = DigitalInOut(board.D7) 36 | # esp32_reset = DigitalInOut(board.D5) 37 | 38 | # If you have an AirLift Featherwing or ItsyBitsy Airlift: 39 | # esp32_cs = DigitalInOut(board.D13) 40 | # esp32_ready = DigitalInOut(board.D11) 41 | # esp32_reset = DigitalInOut(board.D12) 42 | 43 | # If you have an externally connected ESP32: 44 | # NOTE: You may need to change the pins to reflect your wiring 45 | # esp32_cs = DigitalInOut(board.D9) 46 | # esp32_ready = DigitalInOut(board.D10) 47 | # esp32_reset = DigitalInOut(board.D5) 48 | 49 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 50 | if "SCK1" in dir(board): 51 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 52 | else: 53 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 54 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 55 | 56 | pool = adafruit_connection_manager.get_radio_socketpool(esp) 57 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) 58 | requests = adafruit_requests.Session(pool, ssl_context) 59 | 60 | if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: 61 | print("ESP32 found and in idle mode") 62 | print("Firmware vers.", esp.firmware_version) 63 | print("MAC addr:", ":".join("%02X" % byte for byte in esp.MAC_address)) 64 | 65 | for ap in esp.scan_networks(): 66 | print("\t%-23s RSSI: %d" % (ap.ssid, ap.rssi)) 67 | 68 | print("Connecting to AP...") 69 | while not esp.is_connected: 70 | try: 71 | esp.connect_AP(ssid, password) 72 | except OSError as e: 73 | print("could not connect to AP, retrying: ", e) 74 | continue 75 | print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) 76 | print("My IP address is", esp.ipv4_address) 77 | print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) 78 | print("Ping google.com: %d ms" % esp.ping("google.com")) 79 | 80 | # esp._debug = True 81 | print("Fetching text from", TEXT_URL) 82 | r = requests.get(TEXT_URL) 83 | print("-" * 40) 84 | print(r.text) 85 | print("-" * 40) 86 | r.close() 87 | 88 | print() 89 | print("Fetching json from", JSON_URL) 90 | r = requests.get(JSON_URL) 91 | print("-" * 40) 92 | print(r.json()) 93 | print("-" * 40) 94 | r.close() 95 | 96 | print("Done!") 97 | -------------------------------------------------------------------------------- /examples/esp32spi_ipconfig.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import board 8 | import busio 9 | from digitalio import DigitalInOut 10 | 11 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 12 | # Note that frozen libraries may not be up to date. 13 | # import adafruit_esp32spi 14 | # from adafruit_esp32spi import socketpool 15 | import adafruit_esp32spi.adafruit_esp32spi_socketpool as socketpool 16 | from adafruit_esp32spi import adafruit_esp32spi 17 | 18 | # Get wifi details and more from a settings.toml file 19 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 20 | ssid = getenv("CIRCUITPY_WIFI_SSID") 21 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 22 | 23 | HOSTNAME = "esp32-spi-hostname-test" 24 | 25 | IP_ADDRESS = "192.168.1.111" 26 | GATEWAY_ADDRESS = "192.168.1.1" 27 | SUBNET_MASK = "255.255.255.0" 28 | 29 | UDP_IN_ADDR = "192.168.1.1" 30 | UDP_IN_PORT = 5500 31 | 32 | UDP_TIMEOUT = 20 33 | 34 | # If you are using a board with pre-defined ESP32 Pins: 35 | esp32_cs = DigitalInOut(board.ESP_CS) 36 | esp32_ready = DigitalInOut(board.ESP_BUSY) 37 | esp32_reset = DigitalInOut(board.ESP_RESET) 38 | 39 | # If you have an externally connected ESP32: 40 | # esp32_cs = DigitalInOut(board.D9) 41 | # esp32_ready = DigitalInOut(board.D10) 42 | # esp32_reset = DigitalInOut(board.D5) 43 | 44 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 45 | if "SCK1" in dir(board): 46 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 47 | else: 48 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 49 | 50 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 51 | pool = socketpool.SocketPool(esp) 52 | 53 | s_in = pool.socket(type=pool.SOCK_DGRAM) 54 | s_in.settimeout(UDP_TIMEOUT) 55 | print("set hostname:", HOSTNAME) 56 | esp.set_hostname(HOSTNAME) 57 | 58 | if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: 59 | print("ESP32 found and in idle mode") 60 | print("Firmware vers.", esp.firmware_version) 61 | print("MAC addr:", [hex(i) for i in esp.MAC_address]) 62 | 63 | print("Connecting to AP...") 64 | while not esp.is_connected: 65 | try: 66 | esp.connect_AP(ssid, password) 67 | except OSError as e: 68 | print("could not connect to AP, retrying: ", e) 69 | continue 70 | print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) 71 | ip1 = esp.ip_address 72 | 73 | print("set ip dns") 74 | esp.set_dns_config("192.168.1.1", "8.8.8.8") 75 | 76 | print("set ip config") 77 | esp.set_ip_config(IP_ADDRESS, GATEWAY_ADDRESS, SUBNET_MASK) 78 | 79 | time.sleep(1) 80 | ip2 = esp.ip_address 81 | 82 | time.sleep(1) 83 | info = esp.network_data 84 | print( 85 | "get network_data: ", 86 | esp.pretty_ip(info["ip_addr"]), 87 | esp.pretty_ip(info["gateway"]), 88 | esp.pretty_ip(info["netmask"]), 89 | ) 90 | 91 | print("My IP address is", esp.ipv4_address) 92 | print("udp in addr: ", UDP_IN_ADDR, UDP_IN_PORT) 93 | 94 | socketaddr_udp_in = pool.getaddrinfo(UDP_IN_ADDR, UDP_IN_PORT)[0][4] 95 | s_in.connect(socketaddr_udp_in, conntype=esp.UDP_MODE) 96 | print("connected local UDP") 97 | 98 | while True: 99 | data = s_in.recv(1205) 100 | if len(data) >= 1: 101 | data = data.decode("utf-8") 102 | print(len(data), data) 103 | -------------------------------------------------------------------------------- /examples/esp32spi_cheerlights.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import adafruit_fancyled.adafruit_fancyled as fancy 8 | import board 9 | import busio 10 | import neopixel 11 | from digitalio import DigitalInOut 12 | 13 | # Use these imports for adafruit_esp32spi version 11.0.0 and up. 14 | # Note that frozen libraries may not be up to date. 15 | # import adafruit_esp32spi 16 | # from adafruit_esp32spi.wifimanager import WiFiManager 17 | from adafruit_esp32spi import adafruit_esp32spi 18 | from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager 19 | 20 | # Get wifi details and more from a settings.toml file 21 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD 22 | ssid = getenv("CIRCUITPY_WIFI_SSID") 23 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 24 | 25 | print("ESP32 SPI webclient test") 26 | 27 | DATA_SOURCE = "https://api.thingspeak.com/channels/1417/feeds.json?results=1" 28 | DATA_LOCATION = ["feeds", 0, "field2"] 29 | 30 | # If you are using a board with pre-defined ESP32 Pins: 31 | esp32_cs = DigitalInOut(board.ESP_CS) 32 | esp32_ready = DigitalInOut(board.ESP_BUSY) 33 | esp32_reset = DigitalInOut(board.ESP_RESET) 34 | 35 | # If you have an externally connected ESP32: 36 | # esp32_cs = DigitalInOut(board.D9) 37 | # esp32_ready = DigitalInOut(board.D10) 38 | # esp32_reset = DigitalInOut(board.D5) 39 | 40 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 41 | if "SCK1" in dir(board): 42 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 43 | else: 44 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 45 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 46 | """Use below for Most Boards""" 47 | status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 48 | """Uncomment below for ItsyBitsy M4""" 49 | # status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) 50 | """Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" 51 | # import adafruit_rgbled 52 | # from adafruit_esp32spi import PWMOut 53 | # RED_LED = PWMOut.PWMOut(esp, 26) 54 | # GREEN_LED = PWMOut.PWMOut(esp, 27) 55 | # BLUE_LED = PWMOut.PWMOut(esp, 25) 56 | # status_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) 57 | wifi = WiFiManager(esp, ssid, password, status_pixel=status_pixel) 58 | 59 | # neopixels 60 | pixels = neopixel.NeoPixel(board.A1, 16, brightness=0.3) 61 | pixels.fill(0) 62 | 63 | # we'll save the value in question 64 | last_value = value = None 65 | 66 | while True: 67 | try: 68 | print("Fetching json from", DATA_SOURCE) 69 | response = wifi.get(DATA_SOURCE) 70 | print(response.json()) 71 | value = response.json() 72 | for key in DATA_LOCATION: 73 | value = value[key] 74 | print(value) 75 | response.close() 76 | except OSError as e: 77 | print("Failed to get data, retrying\n", e) 78 | wifi.reset() 79 | continue 80 | 81 | if not value: 82 | continue 83 | if last_value != value: 84 | color = int(value[1:], 16) 85 | red = color >> 16 & 0xFF 86 | green = color >> 8 & 0xFF 87 | blue = color & 0xFF 88 | gamma_corrected = fancy.gamma_adjust(fancy.CRGB(red, green, blue)).pack() 89 | 90 | pixels.fill(gamma_corrected) 91 | last_value = value 92 | response = None 93 | time.sleep(60) 94 | -------------------------------------------------------------------------------- /adafruit_esp32spi/PWMOut.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Brent Rubell for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `PWMOut` 7 | ============================== 8 | PWMOut CircuitPython API for ESP32SPI. 9 | 10 | * Author(s): Brent Rubell 11 | """ 12 | 13 | 14 | class PWMOut: 15 | """ 16 | Implementation of CircuitPython PWMOut for ESP32SPI. 17 | 18 | :param int esp_pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS. 19 | :param ESP_SPIcontrol esp: The ESP object we are using. 20 | :param int duty_cycle: The fraction of each pulse which is high, 16-bit. 21 | :param int frequency: The target frequency in Hertz (32-bit). 22 | :param bool variable_frequency: True if the frequency will change over time. 23 | """ 24 | 25 | ESP32_PWM_PINS = set( 26 | [0, 1, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33] 27 | ) 28 | 29 | def __init__(self, esp, pwm_pin, *, frequency=500, duty_cycle=0, variable_frequency=False): 30 | if pwm_pin in self.ESP32_PWM_PINS: 31 | self._pwm_pin = pwm_pin 32 | else: 33 | raise AttributeError("Pin %d is not a valid ESP32 GPIO Pin." % pwm_pin) 34 | self._esp = esp 35 | self._duty_cycle = duty_cycle 36 | self._freq = frequency 37 | self._var_freq = variable_frequency 38 | 39 | def __enter__(self): 40 | return self 41 | 42 | def __exit__(self, exc_type, exc_value, exc_traceback): 43 | self.deinit() 44 | 45 | def deinit(self): 46 | """De-initalize the PWMOut object.""" 47 | self._duty_cycle = 0 48 | self._freq = 0 49 | self._pwm_pin = None 50 | 51 | def _is_deinited(self): 52 | """Checks if PWMOut object has been previously de-initalized""" 53 | if self._pwm_pin is None: 54 | raise ValueError( 55 | "PWMOut Object has been deinitialized and can no longer " 56 | "be used. Create a new PWMOut object." 57 | ) 58 | 59 | @property 60 | def duty_cycle(self): 61 | """Returns the PWMOut object's duty cycle as a 62 | ratio from 0.0 to 1.0.""" 63 | self._is_deinited() 64 | return self._duty_cycle 65 | 66 | @duty_cycle.setter 67 | def duty_cycle(self, duty_cycle): 68 | """Sets the PWMOut duty cycle. 69 | :param float duty_cycle: Between 0.0 (low) and 1.0 (high). 70 | :param int duty_cycle: Between 0 (low) and 1 (high). 71 | """ 72 | self._is_deinited() 73 | if not isinstance(duty_cycle, (int, float)): 74 | raise TypeError("Invalid duty_cycle, should be int or float.") 75 | duty_cycle /= 65535.0 76 | if not 0.0 <= duty_cycle <= 1.0: 77 | raise ValueError("Invalid duty_cycle, should be between 0.0 and 1.0") 78 | self._esp.set_analog_write(self._pwm_pin, duty_cycle) 79 | 80 | @property 81 | def frequency(self): 82 | """Returns the PWMOut object's frequency value.""" 83 | self._is_deinited() 84 | return self._freq 85 | 86 | @frequency.setter 87 | def frequency(self, freq): 88 | """Sets the PWMOut object's frequency value. 89 | :param int freq: 32-bit value that dictates the PWM frequency in Hertz. 90 | NOTE: Only writeable when constructed with variable_Frequency=True. 91 | """ 92 | self._is_deinited() 93 | self._freq = freq 94 | raise NotImplementedError("PWMOut Frequency not implemented in ESP32SPI") 95 | -------------------------------------------------------------------------------- /examples/esp32spi_wpa2ent_aio_post.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import board 8 | import busio 9 | import neopixel 10 | from digitalio import DigitalInOut 11 | 12 | # Use this import for adafruit_esp32spi version 11.0.0 and up. 13 | # Note that frozen libraries may not be up to date. 14 | # import adafruit_esp32spi 15 | # from adafruit_esp32spi.wifimanager import WiFiManager 16 | from adafruit_esp32spi import adafruit_esp32spi 17 | from adafruit_esp32spi.adafruit_esp32spi_wifimanager import WiFiManager 18 | 19 | print("ESP32 SPI WPA2 Enterprise webclient test") 20 | 21 | # Get wifi details and more from a settings.toml file 22 | # tokens used by this Demo: CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD, 23 | # CIRCUITPY_WIFI_ENT_USER, CIRCUITPY_WIFI_ENT_IDENT, 24 | # ADAFRUIT_AIO_USERNAME, ADAFRUIT_AIO_KEY 25 | ssid = getenv("CIRCUITPY_WIFI_SSID") 26 | password = getenv("CIRCUITPY_WIFI_PASSWORD") 27 | enterprise_ident = getenv("CIRCUITPY_WIFI_ENT_IDENT") 28 | enterprise_user = getenv("CIRCUITPY_WIFI_ENT_USER") 29 | 30 | aio_username = getenv("ADAFRUIT_AIO_USERNAME") 31 | aio_key = getenv("ADAFRUIT_AIO_KEY") 32 | 33 | # ESP32 setup 34 | # If your board does define the three pins listed below, 35 | # you can set the correct pins in the second block 36 | try: 37 | esp32_cs = DigitalInOut(board.ESP_CS) 38 | esp32_ready = DigitalInOut(board.ESP_BUSY) 39 | esp32_reset = DigitalInOut(board.ESP_RESET) 40 | except AttributeError: 41 | esp32_cs = DigitalInOut(board.D9) 42 | esp32_ready = DigitalInOut(board.D10) 43 | esp32_reset = DigitalInOut(board.D5) 44 | 45 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 46 | if "SCK1" in dir(board): 47 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 48 | else: 49 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 50 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 51 | 52 | """Use below for Most Boards""" 53 | status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 54 | """Uncomment below for ItsyBitsy M4""" 55 | # status_pixel = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2) 56 | """Uncomment below for an externally defined RGB LED (including Arduino Nano Connect)""" 57 | # import adafruit_rgbled 58 | # from adafruit_esp32spi import PWMOut 59 | # RED_LED = PWMOut.PWMOut(esp, 26) 60 | # GREEN_LED = PWMOut.PWMOut(esp, 27) 61 | # BLUE_LED = PWMOut.PWMOut(esp, 25) 62 | # status_pixel = adafruit_rgbled.RGBLED(RED_LED, BLUE_LED, GREEN_LED) 63 | 64 | wifi = WiFiManager( 65 | esp, 66 | ssid, 67 | password, 68 | enterprise_ident=enterprise_ident, 69 | enterprise_user=enterprise_user, 70 | status_pixel=status_pixel, 71 | connection_type=WiFiManager.ENTERPRISE, 72 | ) 73 | 74 | counter = 0 75 | 76 | while True: 77 | try: 78 | print("Posting data...", end="") 79 | data = counter 80 | feed = "test" 81 | payload = {"value": data} 82 | response = wifi.post( 83 | "https://io.adafruit.com/api/v2/" + aio_username + "/feeds/" + feed + "/data", 84 | json=payload, 85 | headers={"X-AIO-KEY": aio_key}, 86 | ) 87 | print(response.json()) 88 | response.close() 89 | counter = counter + 1 90 | print("OK") 91 | except OSError as e: 92 | print("Failed to get data, retrying\n", e) 93 | wifi.reset() 94 | continue 95 | response = None 96 | time.sleep(15) 97 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | target-version = "py38" 6 | line-length = 100 7 | 8 | [lint] 9 | preview = true 10 | select = ["I", "PL", "UP"] 11 | 12 | extend-select = [ 13 | "D419", # empty-docstring 14 | "E501", # line-too-long 15 | "W291", # trailing-whitespace 16 | "PLC0414", # useless-import-alias 17 | "PLC2401", # non-ascii-name 18 | "PLC2801", # unnecessary-dunder-call 19 | "PLC3002", # unnecessary-direct-lambda-call 20 | "PLE0101", # return-in-init 21 | "F706", # return-outside-function 22 | "F704", # yield-outside-function 23 | "PLE0116", # continue-in-finally 24 | "PLE0117", # nonlocal-without-binding 25 | "PLE0241", # duplicate-bases 26 | "PLE0302", # unexpected-special-method-signature 27 | "PLE0604", # invalid-all-object 28 | "PLE0605", # invalid-all-format 29 | "PLE0643", # potential-index-error 30 | "PLE0704", # misplaced-bare-raise 31 | "PLE1141", # dict-iter-missing-items 32 | "PLE1142", # await-outside-async 33 | "PLE1205", # logging-too-many-args 34 | "PLE1206", # logging-too-few-args 35 | "PLE1307", # bad-string-format-type 36 | "PLE1310", # bad-str-strip-call 37 | "PLE1507", # invalid-envvar-value 38 | "PLE2502", # bidirectional-unicode 39 | "PLE2510", # invalid-character-backspace 40 | "PLE2512", # invalid-character-sub 41 | "PLE2513", # invalid-character-esc 42 | "PLE2514", # invalid-character-nul 43 | "PLE2515", # invalid-character-zero-width-space 44 | "PLR0124", # comparison-with-itself 45 | "PLR0202", # no-classmethod-decorator 46 | "PLR0203", # no-staticmethod-decorator 47 | "UP004", # useless-object-inheritance 48 | "PLR0206", # property-with-parameters 49 | "PLR0904", # too-many-public-methods 50 | "PLR0911", # too-many-return-statements 51 | "PLR0912", # too-many-branches 52 | "PLR0913", # too-many-arguments 53 | "PLR0914", # too-many-locals 54 | "PLR0915", # too-many-statements 55 | "PLR0916", # too-many-boolean-expressions 56 | "PLR1702", # too-many-nested-blocks 57 | "PLR1704", # redefined-argument-from-local 58 | "PLR1711", # useless-return 59 | "C416", # unnecessary-comprehension 60 | "PLR1733", # unnecessary-dict-index-lookup 61 | "PLR1736", # unnecessary-list-index-lookup 62 | 63 | # ruff reports this rule is unstable 64 | #"PLR6301", # no-self-use 65 | 66 | "PLW0108", # unnecessary-lambda 67 | "PLW0120", # useless-else-on-loop 68 | "PLW0127", # self-assigning-variable 69 | "PLW0129", # assert-on-string-literal 70 | "B033", # duplicate-value 71 | "PLW0131", # named-expr-without-context 72 | "PLW0245", # super-without-brackets 73 | "PLW0406", # import-self 74 | "PLW0602", # global-variable-not-assigned 75 | "PLW0603", # global-statement 76 | "PLW0604", # global-at-module-level 77 | 78 | # fails on the try: import typing used by libraries 79 | #"F401", # unused-import 80 | 81 | "F841", # unused-variable 82 | "E722", # bare-except 83 | "PLW0711", # binary-op-exception 84 | "PLW1501", # bad-open-mode 85 | "PLW1508", # invalid-envvar-default 86 | "PLW1509", # subprocess-popen-preexec-fn 87 | "PLW2101", # useless-with-lock 88 | "PLW3301", # nested-min-max 89 | ] 90 | 91 | ignore = [ 92 | "PLR2004", # magic-value-comparison 93 | "UP030", # format literals 94 | "PLW1514", # unspecified-encoding 95 | "PLR0913", # too-many-arguments 96 | "PLR0915", # too-many-statements 97 | "PLR0917", # too-many-positional-arguments 98 | "PLR0904", # too-many-public-methods 99 | "PLR0912", # too-many-branches 100 | "PLR0916", # too-many-boolean-expressions 101 | ] 102 | 103 | [format] 104 | line-ending = "lf" 105 | -------------------------------------------------------------------------------- /examples/esp32spi_wpa2ent_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Example code implementing WPA2 Enterprise mode 5 | # 6 | # This code requires firmware version 1.3.0, or newer, running 7 | # on the ESP32 WiFi co-processor. The latest firmware, and wiring 8 | # info if you are using something other than a PyPortal, can be found 9 | # in the Adafruit Learning System: 10 | # https://learn.adafruit.com/adding-a-wifi-co-processor-to-circuitpython-esp8266-esp32/firmware-files#esp32-only-spi-firmware-3-8 11 | 12 | import re 13 | import time 14 | 15 | import adafruit_connection_manager 16 | import adafruit_requests 17 | import board 18 | import busio 19 | from digitalio import DigitalInOut 20 | 21 | # Use this import for adafruit_esp32spi version 11.0.0 and up. 22 | # Note that frozen libraries may not be up to date. 23 | # import adafruit_esp32spi 24 | from adafruit_esp32spi import adafruit_esp32spi 25 | 26 | 27 | # Version number comparison code. Credit to gnud on stackoverflow 28 | # (https://stackoverflow.com/a/1714190), swapping out cmp() to 29 | # support Python 3.x and thus, CircuitPython 30 | def version_compare(version1, version2): 31 | def normalize(v): 32 | return [int(x) for x in re.sub(r"(\.0+)*$", "", v).split(".")] 33 | 34 | return (normalize(version1) > normalize(version2)) - (normalize(version1) < normalize(version2)) 35 | 36 | 37 | print("ESP32 SPI WPA2 Enterprise test") 38 | 39 | # ESP32 setup 40 | # If your board does define the three pins listed below, 41 | # you can set the correct pins in the second block 42 | try: 43 | esp32_cs = DigitalInOut(board.ESP_CS) 44 | esp32_ready = DigitalInOut(board.ESP_BUSY) 45 | esp32_reset = DigitalInOut(board.ESP_RESET) 46 | except AttributeError: 47 | esp32_cs = DigitalInOut(board.D9) 48 | esp32_ready = DigitalInOut(board.D10) 49 | esp32_reset = DigitalInOut(board.D5) 50 | 51 | # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040 52 | if "SCK1" in dir(board): 53 | spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1) 54 | else: 55 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 56 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 57 | 58 | pool = adafruit_connection_manager.get_radio_socketpool(esp) 59 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) 60 | requests = adafruit_requests.Session(pool, ssl_context) 61 | 62 | if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: 63 | print("ESP32 found and in idle mode") 64 | 65 | # Get the ESP32 fw version number 66 | print("Firmware vers.", esp.firmware_version) 67 | 68 | print("MAC addr:", [hex(i) for i in esp.MAC_address]) 69 | 70 | # WPA2 Enterprise support was added in fw ver 1.3.0. Check that the ESP32 71 | # is running at least that version, otherwise, bail out 72 | assert ( 73 | version_compare(esp.firmware_version, "1.3.0") >= 0 74 | ), "Incorrect ESP32 firmware version; >= 1.3.0 required." 75 | 76 | # Set up the SSID you would like to connect to 77 | # Note that we need to call wifi_set_network prior 78 | # to calling wifi_set_enable. 79 | esp.wifi_set_network(b"YOUR_SSID_HERE") 80 | 81 | # If your WPA2 Enterprise network requires an anonymous 82 | # identity to be set, you may set that here 83 | esp.wifi_set_entidentity(b"") 84 | 85 | # Set the WPA2 Enterprise username you'd like to use 86 | esp.wifi_set_entusername(b"MY_USERNAME") 87 | 88 | # Set the WPA2 Enterprise password you'd like to use 89 | esp.wifi_set_entpassword(b"MY_PASSWORD") 90 | 91 | # Once the network settings have been configured, 92 | # we need to enable WPA2 Enterprise mode on the ESP32 93 | esp.wifi_set_entenable() 94 | 95 | # Wait for the network to come up 96 | print("Connecting to AP...") 97 | while not esp.is_connected: 98 | print(".", end="") 99 | time.sleep(2) 100 | 101 | print("") 102 | print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi) 103 | print("My IP address is", esp.ipv4_address) 104 | print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com"))) 105 | print("Ping google.com: %d ms" % esp.ping("google.com")) 106 | 107 | print("Done!") 108 | -------------------------------------------------------------------------------- /examples/gpio/esp32spi_gpio.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import random 6 | import time 7 | 8 | import board 9 | import pulseio 10 | from digitalio import DigitalInOut, Direction 11 | 12 | import adafruit_esp32spi 13 | 14 | # ESP32SPI Digital and Analog Pin Reads & Writes 15 | 16 | # This example targets a Feather M4 or ItsyBitsy M4 as the CircuitPython MCU, 17 | # along with either an ESP32 Feather or ESP32 Breakout as Wi-Fi co-processor. 18 | # You may need to choose different pins for other targets. 19 | 20 | 21 | def esp_reset_all(): 22 | # esp.reset() will reset the ESP32 using its RST pin 23 | # side effect is re-initializing ESP32 pin modes and debug output 24 | esp.reset() 25 | time.sleep(1) 26 | # (re-)set NINA serial debug on ESP32 TX 27 | esp.set_esp_debug(True) # False, True 28 | # (re-)set digital pin modes 29 | esp_init_pin_modes(ESP_D_R_PIN, ESP_D_W_PIN) 30 | 31 | 32 | def esp_init_pin_modes(din, dout): 33 | # ESP32 Digital Input 34 | esp.set_pin_mode(din, 0x0) 35 | 36 | # ESP32 Digital Output (no output on pins 34-39) 37 | esp.set_pin_mode(dout, 0x1) 38 | 39 | 40 | # M4 R/W Pin Assignments 41 | M4_D_W_PIN = DigitalInOut(board.A1) # digital write to ESP_D_R_PIN 42 | M4_D_W_PIN.direction = Direction.OUTPUT 43 | M4_A_R_PIN = pulseio.PulseIn(board.A0, maxlen=64) # PWM read from ESP_A_W_PIN 44 | M4_A_R_PIN.pause() 45 | 46 | # ESP32 R/W Pin assignments & connections 47 | ESP_D_R_PIN = 12 # digital read from M4_D_W_PIN 48 | ESP_D_W_PIN = 13 # digital write to Red LED on Feather ESP32 and ESP32 Breakout 49 | # ESP32 Analog Input using ADC1 50 | # esp.set_pin_mode(36, 0x0) # Hall Effect Sensor 51 | # esp.set_pin_mode(37, 0x0) # Not Exposed 52 | # esp.set_pin_mode(38, 0x0) # Not Exposed 53 | # esp.set_pin_mode(39, 0x0) # Hall Effect Sensor 54 | # esp.set_pin_mode(32, 0x0) # INPUT OK 55 | # esp.set_pin_mode(33, 0x0) # DO NOT USE: ESP32SPI Busy/!Rdy 56 | # esp.set_pin_mode(34, 0x0) # INPUT OK 57 | # esp.set_pin_mode(35, 0x0) # INPUT OK (1/2 of Battery on ESP32 Feather) 58 | ESP_A_R_PIN = 32 # analog read from 10k potentiometer 59 | # ESP32 Analog (PWM/LEDC) Output (no output on pins 34-39) 60 | ESP_A_W_PIN = 27 # analog (PWM) write to M4_A_R_PIN 61 | 62 | spi = board.SPI() 63 | # Airlift FeatherWing & Bitsy Add-On compatible 64 | esp32_cs = DigitalInOut(board.D13) # M4 Red LED 65 | esp32_ready = DigitalInOut(board.D11) 66 | esp32_reset = DigitalInOut(board.D12) 67 | esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 68 | 69 | esp_reset_all() 70 | 71 | print("ESP32 Firmware:", esp.firmware_version) 72 | 73 | print("ESP32 MAC: {5:02X}:{4:02X}:{3:02X}:{2:02X}:{1:02X}:{0:02X}".format(*esp.MAC_address)) 74 | 75 | # initial digital write values 76 | m4_d_w_val = False 77 | esp_d_w_val = False 78 | 79 | while True: 80 | print("\nESP32 DIGITAL:") 81 | 82 | # ESP32 digital read 83 | try: 84 | M4_D_W_PIN.value = m4_d_w_val 85 | print("M4 wrote:", m4_d_w_val, end=" ") 86 | # b/c ESP32 might have reset out from under us 87 | esp_init_pin_modes(ESP_D_R_PIN, ESP_D_W_PIN) 88 | esp_d_r_val = esp.set_digital_read(ESP_D_R_PIN) 89 | print("--> ESP read:", esp_d_r_val) 90 | except OSError as e: 91 | print("ESP32 Error", e) 92 | esp_reset_all() 93 | 94 | # ESP32 digital write 95 | try: 96 | # b/c ESP32 might have reset out from under us 97 | esp_init_pin_modes(ESP_D_R_PIN, ESP_D_W_PIN) 98 | esp.set_digital_write(ESP_D_W_PIN, esp_d_w_val) 99 | print("ESP wrote:", esp_d_w_val, "--> Red LED") 100 | except OSError as e: 101 | print("ESP32 Error", e) 102 | esp_reset_all() 103 | 104 | print("ESP32 ANALOG:") 105 | 106 | # ESP32 analog read 107 | try: 108 | esp_a_r_val = esp.set_analog_read(ESP_A_R_PIN) 109 | print( 110 | "Potentiometer --> ESP read: ", 111 | esp_a_r_val, 112 | " (", 113 | f"{esp_a_r_val * 3.3 / 65536:1.1f}", 114 | "v)", 115 | sep="", 116 | ) 117 | except OSError as e: 118 | print("ESP32 Error", e) 119 | esp_reset_all() 120 | 121 | # ESP32 analog write 122 | try: 123 | # don't set the low end to 0 or the M4's pulseio read will stall 124 | esp_a_w_val = random.uniform(0.1, 0.9) 125 | esp.set_analog_write(ESP_A_W_PIN, esp_a_w_val) 126 | print( 127 | "ESP wrote: ", 128 | f"{esp_a_w_val:1.2f}", 129 | " (", 130 | f"{int(esp_a_w_val * 65536):d}", 131 | ")", 132 | " (", 133 | f"{esp_a_w_val * 3.3:1.1f}", 134 | "v)", 135 | sep="", 136 | end=" ", 137 | ) 138 | 139 | # ESP32 "analog" write is a 1000Hz PWM 140 | # use pulseio to extract the duty cycle 141 | M4_A_R_PIN.clear() 142 | M4_A_R_PIN.resume() 143 | while len(M4_A_R_PIN) < 2: 144 | pass 145 | M4_A_R_PIN.pause() 146 | duty = M4_A_R_PIN[0] / (M4_A_R_PIN[0] + M4_A_R_PIN[1]) 147 | print( 148 | "--> M4 read: ", 149 | f"{duty:1.2f}", 150 | " (", 151 | f"{int(duty * 65536):d}", 152 | ")", 153 | " (", 154 | f"{duty * 3.3:1.1f}", 155 | "v)", 156 | " [len=", 157 | len(M4_A_R_PIN), 158 | "]", 159 | sep="", 160 | ) 161 | 162 | except OSError as e: 163 | print("ESP32 Error", e) 164 | esp_reset_all() 165 | 166 | # toggle digital write values 167 | m4_d_w_val = not m4_d_w_val 168 | esp_d_w_val = not esp_d_w_val 169 | 170 | time.sleep(5) 171 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import datetime 6 | import os 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath("..")) 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinxcontrib.jquery", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.todo", 22 | ] 23 | 24 | # TODO: Please Read! 25 | # Uncomment the below if you use native CircuitPython modules such as 26 | # digitalio, micropython and busio. List the modules you use. Without it, the 27 | # autodoc module docs will fail to generate with a warning. 28 | autodoc_mock_imports = ["adafruit_requests"] 29 | 30 | 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3", None), 33 | "BusDevice": ( 34 | "https://docs.circuitpython.org/projects/busdevice/en/latest/", 35 | None, 36 | ), 37 | "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), 38 | } 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | source_suffix = ".rst" 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "Adafruit ESP32SPI Library" 50 | creation_year = "2019" 51 | current_year = str(datetime.datetime.now().year) 52 | year_duration = ( 53 | current_year if current_year == creation_year else creation_year + " - " + current_year 54 | ) 55 | copyright = year_duration + " ladyada" 56 | author = "ladyada" 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = "1.0" 64 | # The full version, including alpha/beta/rc tags. 65 | release = "1.0" 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = "en" 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | # 82 | default_role = "any" 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | # 86 | add_function_parentheses = True 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = "sphinx" 90 | 91 | # If true, `todo` and `todoList` produce output, else they produce nothing. 92 | todo_include_todos = False 93 | 94 | # If this is True, todo emits a warning for each TODO entries. The default is False. 95 | todo_emit_warnings = True 96 | 97 | napoleon_numpy_docstring = False 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | # 104 | import sphinx_rtd_theme 105 | 106 | html_theme = "sphinx_rtd_theme" 107 | 108 | # Add any paths that contain custom static files (such as style sheets) here, 109 | # relative to this directory. They are copied after the builtin static files, 110 | # so a file named "default.css" will overwrite the builtin "default.css". 111 | html_static_path = ["_static"] 112 | 113 | # Include extra css to work around rtd theme glitches 114 | html_css_files = ["custom.css"] 115 | 116 | # The name of an image file (relative to this directory) to use as a favicon of 117 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | # 120 | html_favicon = "_static/favicon.ico" 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = "AdafruitEsp32spiLibrarydoc" 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | # The font size ('10pt', '11pt' or '12pt'). 132 | # 133 | # 'pointsize': '10pt', 134 | # Additional stuff for the LaTeX preamble. 135 | # 136 | # 'preamble': '', 137 | # Latex figure (float) alignment 138 | # 139 | # 'figure_align': 'htbp', 140 | } 141 | 142 | # Grouping the document tree into LaTeX files. List of tuples 143 | # (source start file, target name, title, 144 | # author, documentclass [howto, manual, or own class]). 145 | latex_documents = [ 146 | ( 147 | master_doc, 148 | "AdafruitESP32SPILibrary.tex", 149 | "AdafruitESP32SPI Library Documentation", 150 | author, 151 | "manual", 152 | ), 153 | ] 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | ( 161 | master_doc, 162 | "AdafruitESP32SPIlibrary", 163 | "Adafruit ESP32SPI Library Documentation", 164 | [author], 165 | 1, 166 | ) 167 | ] 168 | 169 | # -- Options for Texinfo output ------------------------------------------- 170 | 171 | # Grouping the document tree into Texinfo files. List of tuples 172 | # (source start file, target name, title, author, 173 | # dir menu entry, description, category) 174 | texinfo_documents = [ 175 | ( 176 | master_doc, 177 | "AdafruitESP32SPILibrary", 178 | "Adafruit ESP32SPI Library Documentation", 179 | author, 180 | "AdafruitESP32SPILibrary", 181 | "One line description of project.", 182 | "Miscellaneous", 183 | ), 184 | ] 185 | -------------------------------------------------------------------------------- /examples/gpio/gpio.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Using ESP32 co-processor GPIO pins with CircuitPython ESP32SPI 8 | 9 | As of NINA firmware version 1.3.1, the ESP32SPI library can be used to write digital values to many of the ESP32 GPIO pins using CircuitPython. It can also write "analog" signals using a float between 0 and 1 as the duty cycle (which is converted to an 8-bit integer for use by the NINA firmware). Keep in mind that these are 1000Hz PWM signals using the ESP32 LED Control peripheral, not true analog signals using an on-chip DAC. More information can be found here: 10 | 11 | 12 | As of NINA firmware version 1.5.0, the ESP32SPI library can be used to read digital signals from many of the ESP32 GPIO pins using CircuitPython. It can also read analog signals using ESP32 on-chip ADC1. The ESP32 can theoretically be set to use between 8 and 12 bits of resolution for analog reads. For our purposes, it is a 12-bit read within the NINA firmware, but the CircuitPython library converts it to a 16-bit integer consistent with CircuitPython `analogio` `AnalogIn`. There's an optional keyword argument in the `set_analog_read(self, pin, atten=ADC_ATTEN_DB_11)` function that changes the attenuation of the analog read, and therefore also changes the effective voltage range of the read. With the default 11dB attenuation, Espressif recommends keeping input voltages between 150mV to 2450mV for best results. More information can be found here: 13 | 14 | 15 | ## GPIO Pins available to ESP32SPI 16 | 17 | ``` 18 | # ESP32_GPIO_PINS: 19 | # https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/main/adafruit_esp32spi/digitalio.py 20 | # 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 21 | # 22 | # Pins Used for ESP32SPI 23 | # 5, 14, 18, 23, 33 24 | 25 | # Avialable ESP32SPI Outputs (digital or 'analog' PWM) with NINA FW >= 1.3.1 26 | # 27 | # Adafruit ESP32 Breakout 28 | # *, 2, 4, 12, R, 15, 16, 17, 19, 21, 22, 25, 26, 27, 32 29 | # Adafruit ESP32 Feather 30 | # 4, 12, R, 15, 16, 17, 19, 21, 22, 25, 26, 27, 32 31 | # TinyPICO 32 | # 4, 15, 19, 21, 22, 25, 26, 27, 32 33 | # Adafruit ESP32 Airlift Breakout† 34 | # G, R, B 35 | # Adafruit ESP32 Airlift Feather† 36 | # G, R, B 37 | # Adafruit ESP32 Airlift Bitsy Add-On† 38 | # G, R, B 39 | 40 | # Avialable ESP32SPI Digital Inputs with NINA FW >= 1.5.0 41 | # 42 | # Adafruit ESP32 Breakout 43 | # *, 2, 4, 12, R, 15, 16, 17, 19, 21, 22, 25, 26, 27, 32, 34, 35, 36, 39 44 | # Adafruit ESP32 Feather 45 | # 4, 12, R, 15, 16, 17, 19, 21, 22, 25, 26, 27, 32, 34, 36, 39 46 | # TinyPICO 47 | # 4, 15, 19, 21, 22, 25, 26, 27, 32 CH 48 | 49 | # Avialable ESP32SPI Analog Inputs (ADC1) with NINA FW >= 1.5.0 50 | # 51 | # Adafruit ESP32 Breakout 52 | # *, 32, 34, 35, HE, HE 53 | # Adafruit ESP32 Feather 54 | # *, 32, 34, BA, HE, HE 55 | # TinyPICO 56 | # 32, BA 57 | 58 | Notes: 59 | * Used for bootloading 60 | G Green LED 61 | R Red LED 62 | B Blue LED 63 | BA On-board connection to battery via 50:50 voltage divider 64 | CH Battery charging state (digital pin) 65 | HE Hall Effect sensor 66 | ``` 67 | 68 | Note that on the Airlift FeatherWing and the Airlift Bitsy Add-On, the ESP32 SPI Chip Select (CS) pin aligns with M4's D13 Red LED pin: 69 | ``` 70 | esp32_cs = DigitalInOut(board.D13) # M4 Red LED 71 | esp32_ready = DigitalInOut(board.D11) 72 | esp32_reset = DigitalInOut(board.D12) 73 | ``` 74 | So the Red LED on the main Feather processor will almost always appear to be ON or slightly flickering when ESP32SPI is active. 75 | 76 | ## ESP32 Reset 77 | 78 | Because the ESP32 may reset without indication to the CircuitPython code, putting ESP32 GPIO pins into input mode, `esp.set_digital_write(pin, val)` should be preceded by `esp.set_pin_mode(pin, 0x1)`, with appropriate error handling. Other non-default `esp` states (e.g., `esp.set_esp_debug()`) will also get re-initialized to default settings upon ESP32 reset, so CircuitPython code should anticipate this. 79 | 80 | ## GPIO on Airlift add-on boards 81 | 82 | It should also be possible to do ESP32SPI reads and writes on the Airlift add-on boards, but other than the SPI pins and the green, blue, and red LEDs, the only pins available are RX (GPIO3), TX (GPIO1), and GPIO0, so function is extremely limited. Analog input is ruled out since none of those pins are on ADC1. 83 | 84 | The Airlift Breakout has level-shifting on RX and GPIO0, so those could be digital inputs only. TX could be used as a digital input or as a digital or analog (PWM) output. 85 | 86 | The Airlift FeatherWing and Bitsy Add-On have no level-shifting since they're designed to be stacked onto their associated M4 microcontrollers, so theoretically RX, TX, and GPIO0 could be used as digital inputs, or as digital or analog (PWM) outputs. It's hard to find a use case for doing this when stacked since RX, TX, and GPIO0 will be connected to M4 GPIO pins. 87 | 88 | The Airlift [Metro / Arduino] Shield has level-shifting on RX and GPIO0, with stacking issues similar to the FeatherWings. 89 | 90 | The RX, TX, and GPIO0 pins are used for updating the NINA firmware, and have specific behaviors immediately following reboot that need to be considered if reusing them as GPIO. On the Airlift FeatherWing and Bitsy Add-On, there are pads that need to be soldered to connect the pins. NINA does output messages to TX when connected, depending on the esp debug level set. 91 | 92 | Ultimately it makes the most sense by far to use a non-stacked full-pinout ESP32 as co-processor for ESP32SPI pin read and write features. 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @danh#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | -------------------------------------------------------------------------------- /adafruit_esp32spi/digitalio.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Brent Rubell for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `digitalio` 7 | ============================== 8 | DigitalIO for ESP32 over SPI. 9 | 10 | * Author(s): Brent Rubell, based on Adafruit_Blinka digitalio implementation 11 | and bcm283x Pin implementation. 12 | 13 | https://github.com/adafruit/Adafruit_Blinka/blob/master/src/adafruit_blinka/microcontroller/bcm283x/pin.py 14 | https://github.com/adafruit/Adafruit_Blinka/blob/master/src/digitalio.py 15 | """ 16 | 17 | from micropython import const 18 | 19 | 20 | class Pin: 21 | """ 22 | Implementation of CircuitPython API Pin Handling 23 | for ESP32SPI. 24 | 25 | :param int esp_pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS. 26 | :param ESP_SPIcontrol esp: The ESP object we are using. 27 | 28 | NOTE: This class does not currently implement reading digital pins 29 | or the use of internal pull-up resistors. 30 | """ 31 | 32 | IN = const(0x00) 33 | OUT = const(0x01) 34 | LOW = const(0x00) 35 | HIGH = const(0x01) 36 | _value = LOW 37 | _mode = IN 38 | pin_id = None 39 | 40 | ESP32_GPIO_PINS = set( 41 | [0, 1, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33] 42 | ) 43 | 44 | def __init__(self, esp_pin, esp): 45 | if esp_pin in self.ESP32_GPIO_PINS: 46 | self.pin_id = esp_pin 47 | else: 48 | raise AttributeError("Pin %d is not a valid ESP32 GPIO Pin." % esp_pin) 49 | self._esp = esp 50 | 51 | def init(self, mode=IN): 52 | """Initalizes a pre-defined pin. 53 | 54 | :param mode: Pin mode (IN, OUT, LOW, HIGH). Defaults to IN. 55 | """ 56 | if mode is not None: 57 | if mode == self.IN: 58 | self._mode = self.IN 59 | self._esp.set_pin_mode(self.pin_id, 0) 60 | elif mode == self.OUT: 61 | self._mode = self.OUT 62 | self._esp.set_pin_mode(self.pin_id, 1) 63 | else: 64 | raise ValueError("Invalid mode defined") 65 | 66 | def value(self, val=None): 67 | """Sets ESP32 Pin GPIO output mode. 68 | 69 | :param val: Pin output level (LOW, HIGH) 70 | """ 71 | if val is not None: 72 | if val == self.LOW: 73 | self._value = val 74 | self._esp.set_digital_write(self.pin_id, 0) 75 | elif val == self.HIGH: 76 | self._value = val 77 | self._esp.set_digital_write(self.pin_id, 1) 78 | else: 79 | raise ValueError("Invalid value for pin") 80 | else: 81 | raise NotImplementedError("digitalRead not currently implemented in esp32spi") 82 | 83 | def __repr__(self): 84 | return str(self.pin_id) 85 | 86 | 87 | class DriveMode: 88 | """DriveMode Enum.""" 89 | 90 | PUSH_PULL = None 91 | OPEN_DRAIN = None 92 | 93 | 94 | DriveMode.PUSH_PULL = DriveMode() 95 | DriveMode.OPEN_DRAIN = DriveMode() 96 | 97 | 98 | class Direction: 99 | """DriveMode Enum.""" 100 | 101 | INPUT = None 102 | OUTPUT = None 103 | 104 | 105 | Direction.INPUT = Direction() 106 | Direction.OUTPUT = Direction() 107 | 108 | 109 | class DigitalInOut: 110 | """Implementation of DigitalIO module for ESP32SPI. 111 | 112 | :param ESP_SPIcontrol esp: The ESP object we are using. 113 | :param int pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS. 114 | """ 115 | 116 | _pin = None 117 | 118 | def __init__(self, esp, pin): 119 | self._esp = esp 120 | self._pin = Pin(pin, self._esp) 121 | self.direction = Direction.INPUT 122 | 123 | def __enter__(self): 124 | return self 125 | 126 | def __exit__(self, exception_type, exception_value, traceback): 127 | self.deinit() 128 | 129 | def deinit(self): 130 | """De-initializes the pin object.""" 131 | self._pin = None 132 | 133 | def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL): 134 | """Set the drive mode and value and then switch to writing out digital values. 135 | 136 | :param bool value: Default mode to set upon switching. 137 | :param DriveMode drive_mode: Drive mode for the output. 138 | """ 139 | self._direction = Direction.OUTPUT 140 | self._drive_mode = drive_mode 141 | self.value = value 142 | 143 | def switch_to_input(self, pull=None): 144 | """Sets the pull and then switch to read in digital values. 145 | 146 | :param Pull pull: Pull configuration for the input. 147 | """ 148 | raise NotImplementedError("Digital reads are not currently supported in ESP32SPI.") 149 | 150 | @property 151 | def direction(self): 152 | """Returns the pin's direction.""" 153 | return self.__direction 154 | 155 | @direction.setter 156 | def direction(self, pin_dir): 157 | """Sets the direction of the pin. 158 | 159 | :param Direction dir: Pin direction (Direction.OUTPUT or Direction.INPUT) 160 | """ 161 | self.__direction = pin_dir 162 | if pin_dir is Direction.OUTPUT: 163 | self._pin.init(mode=Pin.OUT) 164 | self.value = False 165 | self.drive_mode = DriveMode.PUSH_PULL 166 | elif pin_dir is Direction.INPUT: 167 | self._pin.init(mode=Pin.IN) 168 | else: 169 | raise AttributeError("Not a Direction") 170 | 171 | @property 172 | def value(self): 173 | """Returns the digital logic level value of the pin.""" 174 | return self._pin.value() == 1 175 | 176 | @value.setter 177 | def value(self, val): 178 | """Sets the digital logic level of the pin. 179 | 180 | :param type value: Pin logic level. 181 | :param int value: Pin logic level. 1 is logic high, 0 is logic low. 182 | :param bool value: Pin logic level. True is logic high, False is logic low. 183 | """ 184 | if self.direction is Direction.OUTPUT: 185 | self._pin.value(1 if val else 0) 186 | else: 187 | raise AttributeError("Not an output") 188 | 189 | @property 190 | def drive_mode(self): 191 | """Returns pin drive mode.""" 192 | if self.direction is Direction.OUTPUT: 193 | return self._drive_mode 194 | raise AttributeError("Not an output") 195 | 196 | @drive_mode.setter 197 | def drive_mode(self, mode): 198 | """Sets the pin drive mode. 199 | 200 | :param DriveMode mode: Defines the drive mode when outputting digital values. 201 | Either PUSH_PULL or OPEN_DRAIN 202 | """ 203 | if mode is DriveMode.OPEN_DRAIN: 204 | raise NotImplementedError("Drive mode %s not implemented in ESP32SPI." % mode) 205 | if mode is DriveMode.PUSH_PULL: 206 | self._pin.init(mode=Pin.OUT) 207 | -------------------------------------------------------------------------------- /adafruit_esp32spi/adafruit_esp32spi_socketpool.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_esp32spi_socketpool` 7 | ================================================================================ 8 | 9 | A socket compatible interface thru the ESP SPI command set 10 | 11 | * Author(s): ladyada 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | try: 17 | from typing import TYPE_CHECKING, Optional, Tuple 18 | 19 | if TYPE_CHECKING: 20 | from esp32spi.adafruit_esp32spi import ESP_SPIcontrol # noqa: UP007 21 | except ImportError: 22 | pass 23 | 24 | 25 | import errno 26 | import gc 27 | import time 28 | 29 | from micropython import const 30 | 31 | from adafruit_esp32spi import adafruit_esp32spi as esp32spi 32 | 33 | _global_socketpool = {} 34 | 35 | 36 | class SocketPool: 37 | """ESP32SPI SocketPool library""" 38 | 39 | # socketpool constants 40 | SOCK_STREAM = const(1) 41 | SOCK_DGRAM = const(2) 42 | AF_INET = const(2) 43 | SOL_SOCKET = const(0xFFF) 44 | SO_REUSEADDR = const(0x0004) 45 | 46 | # implementation specific constants 47 | NO_SOCKET_AVAIL = const(255) 48 | MAX_PACKET = const(4000) 49 | 50 | def __new__(cls, iface: ESP_SPIcontrol): 51 | # We want to make sure to return the same pool for the same interface 52 | if iface not in _global_socketpool: 53 | _global_socketpool[iface] = super().__new__(cls) 54 | return _global_socketpool[iface] 55 | 56 | def __init__(self, iface: ESP_SPIcontrol): 57 | self._interface = iface 58 | 59 | def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): 60 | """Given a hostname and a port name, return a 'socket.getaddrinfo' 61 | compatible list of tuples. Honestly, we ignore anything but host & port""" 62 | if not isinstance(port, int): 63 | raise ValueError("Port must be an integer") 64 | ipaddr = self._interface.get_host_by_name(host) 65 | return [(SocketPool.AF_INET, socktype, proto, "", (ipaddr, port))] 66 | 67 | def socket( 68 | self, 69 | family=AF_INET, 70 | type=SOCK_STREAM, 71 | proto=0, 72 | fileno=None, 73 | ): 74 | """Create a new socket and return it""" 75 | return Socket(self, family, type, proto, fileno) 76 | 77 | 78 | class Socket: 79 | """A simplified implementation of the Python 'socket' class, for connecting 80 | through an interface to a remote device. Has properties specific to the 81 | implementation. 82 | 83 | :param SocketPool socket_pool: The underlying socket pool. 84 | :param Optional[int] socknum: Allows wrapping a Socket instance around a socket 85 | number returned by the nina firmware. Used internally. 86 | """ 87 | 88 | def __init__( 89 | self, 90 | socket_pool: SocketPool, 91 | family: int = SocketPool.AF_INET, 92 | type: int = SocketPool.SOCK_STREAM, 93 | proto: int = 0, 94 | fileno: Optional[int] = None, # noqa: UP007 95 | socknum: Optional[int] = None, # noqa: UP007 96 | ): 97 | if family != SocketPool.AF_INET: 98 | raise ValueError("Only AF_INET family supported") 99 | self._socket_pool = socket_pool 100 | self._interface = self._socket_pool._interface 101 | self._type = type 102 | self._buffer = b"" 103 | self._socknum = socknum if socknum is not None else self._interface.get_socket() 104 | self._bound = () 105 | self.settimeout(None) 106 | 107 | def __enter__(self): 108 | return self 109 | 110 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: 111 | self.close() 112 | while self._interface.socket_status(self._socknum) != esp32spi.SOCKET_CLOSED: 113 | pass 114 | 115 | def connect(self, address, conntype=None): 116 | """Connect the socket to the 'address' (which can be 32bit packed IP or 117 | a hostname string). 'conntype' is an extra that may indicate SSL or not, 118 | depending on the underlying interface""" 119 | host, port = address 120 | if conntype is None: 121 | conntype = ( 122 | self._interface.UDP_MODE 123 | if self._type == SocketPool.SOCK_DGRAM 124 | else self._interface.TCP_MODE 125 | ) 126 | if not self._interface.socket_connect(self._socknum, host, port, conn_mode=conntype): 127 | raise ConnectionError("Failed to connect to host", host) 128 | self._buffer = b"" 129 | 130 | def send(self, data): 131 | """Send some data to the socket.""" 132 | if self._type == SocketPool.SOCK_DGRAM: 133 | conntype = self._interface.UDP_MODE 134 | else: 135 | conntype = self._interface.TCP_MODE 136 | sent = self._interface.socket_write(self._socknum, data, conn_mode=conntype) 137 | gc.collect() 138 | return sent 139 | 140 | def sendto(self, data, address): 141 | """Connect and send some data to the socket.""" 142 | self.connect(address) 143 | return self.send(data) 144 | 145 | def recv(self, bufsize: int) -> bytes: 146 | """Reads some bytes from the connected remote address. Will only return 147 | an empty string after the configured timeout. 148 | 149 | :param int bufsize: maximum number of bytes to receive 150 | """ 151 | buf = bytearray(bufsize) 152 | self.recv_into(buf, bufsize) 153 | return bytes(buf) 154 | 155 | def recv_into(self, buffer, nbytes: int = 0): 156 | """Read bytes from the connected remote address into a given buffer. 157 | 158 | :param bytearray buffer: the buffer to read into 159 | :param int nbytes: maximum number of bytes to receive; if 0, 160 | receive as many bytes as possible before filling the 161 | buffer or timing out 162 | """ 163 | if not 0 <= nbytes <= len(buffer): 164 | raise ValueError("nbytes must be 0 to len(buffer)") 165 | 166 | last_read_time = time.monotonic_ns() 167 | num_to_read = len(buffer) if nbytes == 0 else nbytes 168 | num_read = 0 169 | while num_to_read > 0: 170 | # we might have read socket data into the self._buffer with: 171 | # adafruit_wsgi.esp32spi_wsgiserver: socket_readline 172 | if len(self._buffer) > 0: 173 | bytes_to_read = min(num_to_read, len(self._buffer)) 174 | buffer[num_read : num_read + bytes_to_read] = self._buffer[:bytes_to_read] 175 | num_read += bytes_to_read 176 | num_to_read -= bytes_to_read 177 | self._buffer = self._buffer[bytes_to_read:] 178 | # explicitly recheck num_to_read to avoid extra checks 179 | continue 180 | 181 | num_avail = self._available() 182 | if num_avail > 0: 183 | last_read_time = time.monotonic_ns() 184 | bytes_read = self._interface.socket_read(self._socknum, min(num_to_read, num_avail)) 185 | buffer[num_read : num_read + len(bytes_read)] = bytes_read 186 | num_read += len(bytes_read) 187 | num_to_read -= len(bytes_read) 188 | elif num_read > 0: 189 | # We got a message, but there are no more bytes to read, so we can stop. 190 | break 191 | # No bytes yet, or more bytes requested. 192 | 193 | if self._timeout == 0: # if in non-blocking mode, stop now. 194 | break 195 | 196 | # Time out if there's a positive timeout set. 197 | delta = (time.monotonic_ns() - last_read_time) // 1_000_000 198 | if self._timeout > 0 and delta > self._timeout: 199 | raise OSError(errno.ETIMEDOUT) 200 | return num_read 201 | 202 | def settimeout(self, value): 203 | """Set the read timeout for sockets in seconds. 204 | ``0`` means non-blocking. ``None`` means block indefinitely. 205 | """ 206 | if value is None: 207 | self._timeout = -1 208 | else: 209 | if value < 0: 210 | raise ValueError("Timeout cannot be a negative number") 211 | # internally in milliseconds as an int 212 | self._timeout = int(value * 1000) 213 | 214 | def _available(self): 215 | """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)""" 216 | if self._socknum != SocketPool.NO_SOCKET_AVAIL: 217 | return min(self._interface.socket_available(self._socknum), SocketPool.MAX_PACKET) 218 | return 0 219 | 220 | def _connected(self): 221 | """Whether or not we are connected to the socket""" 222 | if self._socknum == SocketPool.NO_SOCKET_AVAIL: 223 | return False 224 | if self._available(): 225 | return True 226 | status = self._interface.socket_status(self._socknum) 227 | result = status not in { 228 | esp32spi.SOCKET_LISTEN, 229 | esp32spi.SOCKET_CLOSED, 230 | esp32spi.SOCKET_FIN_WAIT_1, 231 | esp32spi.SOCKET_FIN_WAIT_2, 232 | esp32spi.SOCKET_TIME_WAIT, 233 | esp32spi.SOCKET_SYN_SENT, 234 | esp32spi.SOCKET_SYN_RCVD, 235 | esp32spi.SOCKET_CLOSE_WAIT, 236 | } 237 | if not result: 238 | self.close() 239 | self._socknum = SocketPool.NO_SOCKET_AVAIL 240 | return result 241 | 242 | def close(self): 243 | """Close the socket, after reading whatever remains""" 244 | self._interface.socket_close(self._socknum) 245 | 246 | def accept(self): 247 | """Accept a connection on a listening socket of type SOCK_STREAM, 248 | creating a new socket of type SOCK_STREAM. Returns a tuple of 249 | (new_socket, remote_address) 250 | """ 251 | client_sock_num = self._interface.socket_available(self._socknum) 252 | if client_sock_num != SocketPool.NO_SOCKET_AVAIL: 253 | sock = Socket(self._socket_pool, socknum=client_sock_num) 254 | # get remote information (addr and port) 255 | remote = self._interface.get_remote_data(client_sock_num) 256 | ip_address = "{}.{}.{}.{}".format(*remote["ip_addr"]) 257 | port = remote["port"] 258 | client_address = (ip_address, port) 259 | return sock, client_address 260 | raise OSError(errno.ECONNRESET) 261 | 262 | def bind(self, address: tuple[str, int]): 263 | """Bind a socket to an address""" 264 | self._bound = address 265 | 266 | def listen(self, backlog: int): # pylint: disable=unused-argument 267 | """Set socket to listen for incoming connections. 268 | :param int backlog: length of backlog queue for waiting connections (ignored) 269 | """ 270 | if not self._bound: 271 | self._bound = (self._interface.ip_address, 80) 272 | port = self._bound[1] 273 | self._interface.start_server(port, self._socknum) 274 | 275 | def setblocking(self, flag: bool): 276 | """Set the blocking behaviour of this socket. 277 | :param bool flag: False means non-blocking, True means block indefinitely. 278 | """ 279 | if flag: 280 | self.settimeout(None) 281 | else: 282 | self.settimeout(0) 283 | 284 | def setsockopt(self, *opts, **kwopts): 285 | """Dummy call for compatibility.""" 286 | -------------------------------------------------------------------------------- /adafruit_esp32spi/adafruit_esp32spi_wifimanager.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Melissa LeBlanc-Williams for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_esp32spi_wifimanager` 7 | ================================================================================ 8 | 9 | WiFi Manager for making ESP32 SPI as WiFi much easier 10 | 11 | * Author(s): Melissa LeBlanc-Williams, ladyada 12 | """ 13 | 14 | import warnings 15 | from time import sleep 16 | 17 | import adafruit_connection_manager 18 | import adafruit_requests 19 | from micropython import const 20 | 21 | from adafruit_esp32spi import adafruit_esp32spi 22 | 23 | 24 | class WiFiManager: 25 | """ 26 | A class to help manage the Wifi connection 27 | """ 28 | 29 | NORMAL = const(1) 30 | ENTERPRISE = const(2) 31 | 32 | def __init__( 33 | self, 34 | esp, 35 | ssid, 36 | password=None, 37 | *, 38 | enterprise_ident=None, 39 | enterprise_user=None, 40 | status_pixel=None, 41 | attempts=2, 42 | connection_type=NORMAL, 43 | debug=False, 44 | ): 45 | """ 46 | :param ESP_SPIcontrol esp: The ESP object we are using 47 | :param str ssid: the SSID of the access point. Must be less than 32 chars. 48 | :param str password: the password for the access point. Must be 8-63 chars. 49 | :param str enterprise_ident: the ident to use when connecting to an enterprise access point. 50 | :param str enterprise_user: the username to use when connecting to an enterprise access 51 | point. 52 | :param status_pixel: (Optional) The pixel device - A NeoPixel, DotStar, 53 | or RGB LED (default=None). The status LED, if given, turns red when 54 | attempting to connect to a Wi-Fi network or create an access point, 55 | turning green upon success. Additionally, if given, it will turn blue 56 | when attempting an HTTP method or returning IP address, turning off 57 | upon success. 58 | :type status_pixel: NeoPixel, DotStar, or RGB LED 59 | :param int attempts: (Optional) Failed attempts before resetting the ESP32 (default=2) 60 | :param const connection_type: (Optional) Type of WiFi connection: NORMAL or ENTERPRISE 61 | """ 62 | # Read the settings 63 | self.esp = esp 64 | self.debug = debug 65 | self.ssid = ssid 66 | self.password = password 67 | self.attempts = attempts 68 | self._connection_type = connection_type 69 | self.statuspix = status_pixel 70 | self.pixel_status(0) 71 | self._ap_index = 0 72 | 73 | # create requests session 74 | pool = adafruit_connection_manager.get_radio_socketpool(self.esp) 75 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(self.esp) 76 | self._requests = adafruit_requests.Session(pool, ssl_context) 77 | 78 | # Check for WPA2 Enterprise values 79 | self.ent_ssid = ssid 80 | self.ent_ident = enterprise_ident 81 | self.ent_user = enterprise_user 82 | self.ent_password = password 83 | 84 | # pylint: enable=too-many-arguments 85 | 86 | def reset(self): 87 | """ 88 | Perform a hard reset on the ESP32 89 | """ 90 | if self.debug: 91 | print("Resetting ESP32") 92 | self.esp.reset() 93 | 94 | def connect(self): 95 | """ 96 | Attempt to connect to WiFi using the current settings 97 | """ 98 | if self.debug: 99 | if self.esp.status == adafruit_esp32spi.WL_IDLE_STATUS: 100 | print("ESP32 found and in idle mode") 101 | print("Firmware vers.", self.esp.firmware_version) 102 | print("MAC addr:", [hex(i) for i in self.esp.MAC_address]) 103 | for access_pt in self.esp.scan_networks(): 104 | print("\t%s\t\tRSSI: %d" % (access_pt.ssid, access_pt.rssi)) 105 | if self._connection_type == WiFiManager.NORMAL: 106 | self.connect_normal() 107 | elif self._connection_type == WiFiManager.ENTERPRISE: 108 | self.connect_enterprise() 109 | else: 110 | raise TypeError("Invalid WiFi connection type specified") 111 | 112 | def _get_next_ap(self): 113 | if isinstance(self.ssid, (tuple, list)) and isinstance(self.password, (tuple, list)): 114 | if not self.ssid or not self.password: 115 | raise ValueError("SSID and Password should contain at least 1 value") 116 | if len(self.ssid) != len(self.password): 117 | raise ValueError("The length of SSIDs and Passwords should match") 118 | access_point = (self.ssid[self._ap_index], self.password[self._ap_index]) 119 | self._ap_index += 1 120 | if self._ap_index >= len(self.ssid): 121 | self._ap_index = 0 122 | return access_point 123 | if isinstance(self.ssid, (tuple, list)) or isinstance(self.password, (tuple, list)): 124 | raise NotImplementedError( 125 | "If using multiple passwords, both SSID and Password should be lists or tuples" 126 | ) 127 | return (self.ssid, self.password) 128 | 129 | def connect_normal(self): 130 | """ 131 | Attempt a regular style WiFi connection. 132 | """ 133 | failure_count = 0 134 | (ssid, password) = self._get_next_ap() 135 | while not self.esp.is_connected: 136 | try: 137 | if self.debug: 138 | print("Connecting to AP...") 139 | self.pixel_status((100, 0, 0)) 140 | self.esp.connect_AP(bytes(ssid, "utf-8"), bytes(password, "utf-8")) 141 | failure_count = 0 142 | self.pixel_status((0, 100, 0)) 143 | except OSError as error: 144 | print("Failed to connect, retrying\n", error) 145 | failure_count += 1 146 | if failure_count >= self.attempts: 147 | failure_count = 0 148 | (ssid, password) = self._get_next_ap() 149 | self.reset() 150 | continue 151 | 152 | def create_ap(self): 153 | """ 154 | Attempt to initialize in Access Point (AP) mode. 155 | Uses SSID and optional passphrase from the current settings 156 | Other WiFi devices will be able to connect to the created Access Point 157 | """ 158 | failure_count = 0 159 | while not self.esp.ap_listening: 160 | try: 161 | if self.debug: 162 | print("Waiting for AP to be initialized...") 163 | self.pixel_status((100, 0, 0)) 164 | if self.password: 165 | self.esp.create_AP(bytes(self.ssid, "utf-8"), bytes(self.password, "utf-8")) 166 | else: 167 | self.esp.create_AP(bytes(self.ssid, "utf-8"), None) 168 | failure_count = 0 169 | self.pixel_status((0, 100, 0)) 170 | except OSError as error: 171 | print("Failed to create access point\n", error) 172 | failure_count += 1 173 | if failure_count >= self.attempts: 174 | failure_count = 0 175 | self.reset() 176 | continue 177 | print(f"Access Point created! Connect to ssid:\n {self.ssid}") 178 | 179 | def connect_enterprise(self): 180 | """ 181 | Attempt an enterprise style WiFi connection 182 | """ 183 | failure_count = 0 184 | self.esp.wifi_set_network(bytes(self.ent_ssid, "utf-8")) 185 | self.esp.wifi_set_entidentity(bytes(self.ent_ident, "utf-8")) 186 | self.esp.wifi_set_entusername(bytes(self.ent_user, "utf-8")) 187 | self.esp.wifi_set_entpassword(bytes(self.ent_password, "utf-8")) 188 | self.esp.wifi_set_entenable() 189 | while not self.esp.is_connected: 190 | try: 191 | if self.debug: 192 | print("Waiting for the ESP32 to connect to the WPA2 Enterprise AP...") 193 | self.pixel_status((100, 0, 0)) 194 | sleep(1) 195 | failure_count = 0 196 | self.pixel_status((0, 100, 0)) 197 | sleep(1) 198 | except OSError as error: 199 | print("Failed to connect, retrying\n", error) 200 | failure_count += 1 201 | if failure_count >= self.attempts: 202 | failure_count = 0 203 | self.reset() 204 | continue 205 | 206 | def get(self, url, **kw): 207 | """ 208 | Pass the Get request to requests and update status LED 209 | 210 | :param str url: The URL to retrieve data from 211 | :param dict data: (Optional) Form data to submit 212 | :param dict json: (Optional) JSON data to submit. (Data must be None) 213 | :param dict header: (Optional) Header data to include 214 | :param bool stream: (Optional) Whether to stream the Response 215 | :return: The response from the request 216 | :rtype: Response 217 | """ 218 | if not self.esp.is_connected: 219 | self.connect() 220 | self.pixel_status((0, 0, 100)) 221 | return_val = self._requests.get(url, **kw) 222 | self.pixel_status(0) 223 | return return_val 224 | 225 | def post(self, url, **kw): 226 | """ 227 | Pass the Post request to requests and update status LED 228 | 229 | :param str url: The URL to post data to 230 | :param dict data: (Optional) Form data to submit 231 | :param dict json: (Optional) JSON data to submit. (Data must be None) 232 | :param dict header: (Optional) Header data to include 233 | :param bool stream: (Optional) Whether to stream the Response 234 | :return: The response from the request 235 | :rtype: Response 236 | """ 237 | if not self.esp.is_connected: 238 | self.connect() 239 | self.pixel_status((0, 0, 100)) 240 | return_val = self._requests.post(url, **kw) 241 | self.pixel_status(0) 242 | return return_val 243 | 244 | def put(self, url, **kw): 245 | """ 246 | Pass the put request to requests and update status LED 247 | 248 | :param str url: The URL to PUT data to 249 | :param dict data: (Optional) Form data to submit 250 | :param dict json: (Optional) JSON data to submit. (Data must be None) 251 | :param dict header: (Optional) Header data to include 252 | :param bool stream: (Optional) Whether to stream the Response 253 | :return: The response from the request 254 | :rtype: Response 255 | """ 256 | if not self.esp.is_connected: 257 | self.connect() 258 | self.pixel_status((0, 0, 100)) 259 | return_val = self._requests.put(url, **kw) 260 | self.pixel_status(0) 261 | return return_val 262 | 263 | def patch(self, url, **kw): 264 | """ 265 | Pass the patch request to requests and update status LED 266 | 267 | :param str url: The URL to PUT data to 268 | :param dict data: (Optional) Form data to submit 269 | :param dict json: (Optional) JSON data to submit. (Data must be None) 270 | :param dict header: (Optional) Header data to include 271 | :param bool stream: (Optional) Whether to stream the Response 272 | :return: The response from the request 273 | :rtype: Response 274 | """ 275 | if not self.esp.is_connected: 276 | self.connect() 277 | self.pixel_status((0, 0, 100)) 278 | return_val = self._requests.patch(url, **kw) 279 | self.pixel_status(0) 280 | return return_val 281 | 282 | def delete(self, url, **kw): 283 | """ 284 | Pass the delete request to requests and update status LED 285 | 286 | :param str url: The URL to PUT data to 287 | :param dict data: (Optional) Form data to submit 288 | :param dict json: (Optional) JSON data to submit. (Data must be None) 289 | :param dict header: (Optional) Header data to include 290 | :param bool stream: (Optional) Whether to stream the Response 291 | :return: The response from the request 292 | :rtype: Response 293 | """ 294 | if not self.esp.is_connected: 295 | self.connect() 296 | self.pixel_status((0, 0, 100)) 297 | return_val = self._requests.delete(url, **kw) 298 | self.pixel_status(0) 299 | return return_val 300 | 301 | def ping(self, host, ttl=250): 302 | """ 303 | Pass the Ping request to the ESP32, update status LED, return response time 304 | 305 | :param str host: The hostname or IP address to ping 306 | :param int ttl: (Optional) The Time To Live in milliseconds for the packet (default=250) 307 | :return: The response time in milliseconds 308 | :rtype: int 309 | """ 310 | if not self.esp.is_connected: 311 | self.connect() 312 | self.pixel_status((0, 0, 100)) 313 | response_time = self.esp.ping(host, ttl=ttl) 314 | self.pixel_status(0) 315 | return response_time 316 | 317 | def ip_address(self): 318 | """ 319 | Returns a formatted local IP address, update status pixel. 320 | """ 321 | if not self.esp.is_connected: 322 | self.connect() 323 | self.pixel_status((0, 0, 100)) 324 | self.pixel_status(0) 325 | return self.esp.ipv4_address 326 | 327 | def pixel_status(self, value): 328 | """ 329 | Change Status Pixel if it was defined 330 | 331 | :param value: The value to set the Board's status LED to 332 | :type value: int or 3-value tuple 333 | """ 334 | if self.statuspix: 335 | if hasattr(self.statuspix, "color"): 336 | self.statuspix.color = value 337 | else: 338 | self.statuspix.fill(value) 339 | 340 | def signal_strength(self): 341 | """ 342 | Returns receiving signal strength indicator in dBm 343 | """ 344 | if not self.esp.is_connected: 345 | self.connect() 346 | return self.esp.ap_info.rssi 347 | 348 | 349 | class ESPSPI_WiFiManager(WiFiManager): 350 | """ 351 | A legacy class to help manage the Wifi connection. Please update to using WiFiManager 352 | """ 353 | 354 | def __init__( 355 | self, 356 | esp, 357 | secrets, 358 | status_pixel=None, 359 | attempts=2, 360 | connection_type=WiFiManager.NORMAL, 361 | debug=False, 362 | ): 363 | """ 364 | :param ESP_SPIcontrol esp: The ESP object we are using 365 | :param dict secrets: The WiFi secrets dict 366 | The use of secrets.py to populate the secrets dict is deprecated 367 | in favor of using settings.toml. 368 | :param status_pixel: (Optional) The pixel device - A NeoPixel, DotStar, 369 | or RGB LED (default=None). The status LED, if given, turns red when 370 | attempting to connect to a Wi-Fi network or create an access point, 371 | turning green upon success. Additionally, if given, it will turn blue 372 | when attempting an HTTP method or returning IP address, turning off 373 | upon success. 374 | :type status_pixel: NeoPixel, DotStar, or RGB LED 375 | :param int attempts: (Optional) Failed attempts before resetting the ESP32 (default=2) 376 | :param const connection_type: (Optional) Type of WiFi connection: NORMAL or ENTERPRISE 377 | """ 378 | 379 | warnings.warn( 380 | "ESP32WiFiManager, which uses `secrets`, is deprecated. Use WifiManager instead and " 381 | "fetch values from settings.toml with `os.getenv()`." 382 | ) 383 | 384 | super().__init__( 385 | esp=esp, 386 | ssid=secrets.get("ssid"), 387 | password=secrets.get("password"), 388 | enterprise_ident=secrets.get("ent_ident", ""), 389 | enterprise_user=secrets.get("ent_user"), 390 | status_pixel=status_pixel, 391 | attempts=attempts, 392 | connection_type=connection_type, 393 | debug=debug, 394 | ) 395 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International Creative Commons Corporation 2 | ("Creative Commons") is not a law firm and does not provide legal services 3 | or legal advice. Distribution of Creative Commons public licenses does not 4 | create a lawyer-client or other relationship. Creative Commons makes its licenses 5 | and related information available on an "as-is" basis. Creative Commons gives 6 | no warranties regarding its licenses, any material licensed under their terms 7 | and conditions, or any related information. Creative Commons disclaims all 8 | liability for damages resulting from their use to the fullest extent possible. 9 | 10 | Using Creative Commons Public Licenses 11 | 12 | Creative Commons public licenses provide a standard set of terms and conditions 13 | that creators and other rights holders may use to share original works of 14 | authorship and other material subject to copyright and certain other rights 15 | specified in the public license below. The following considerations are for 16 | informational purposes only, are not exhaustive, and do not form part of our 17 | licenses. 18 | 19 | Considerations for licensors: Our public licenses are intended for use by 20 | those authorized to give the public permission to use material in ways otherwise 21 | restricted by copyright and certain other rights. Our licenses are irrevocable. 22 | Licensors should read and understand the terms and conditions of the license 23 | they choose before applying it. Licensors should also secure all rights necessary 24 | before applying our licenses so that the public can reuse the material as 25 | expected. Licensors should clearly mark any material not subject to the license. 26 | This includes other CC-licensed material, or material used under an exception 27 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 28 | 29 | Considerations for the public: By using one of our public licenses, a licensor 30 | grants the public permission to use the licensed material under specified 31 | terms and conditions. If the licensor's permission is not necessary for any 32 | reason–for example, because of any applicable exception or limitation to copyright–then 33 | that use is not regulated by the license. Our licenses grant only permissions 34 | under copyright and certain other rights that a licensor has authority to 35 | grant. Use of the licensed material may still be restricted for other reasons, 36 | including because others have copyright or other rights in the material. A 37 | licensor may make special requests, such as asking that all changes be marked 38 | or described. Although not required by our licenses, you are encouraged to 39 | respect those requests where reasonable. More considerations for the public 40 | : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution 41 | 4.0 International Public License 42 | 43 | By exercising the Licensed Rights (defined below), You accept and agree to 44 | be bound by the terms and conditions of this Creative Commons Attribution 45 | 4.0 International Public License ("Public License"). To the extent this Public 46 | License may be interpreted as a contract, You are granted the Licensed Rights 47 | in consideration of Your acceptance of these terms and conditions, and the 48 | Licensor grants You such rights in consideration of benefits the Licensor 49 | receives from making the Licensed Material available under these terms and 50 | conditions. 51 | 52 | Section 1 – Definitions. 53 | 54 | a. Adapted Material means material subject to Copyright and Similar Rights 55 | that is derived from or based upon the Licensed Material and in which the 56 | Licensed Material is translated, altered, arranged, transformed, or otherwise 57 | modified in a manner requiring permission under the Copyright and Similar 58 | Rights held by the Licensor. For purposes of this Public License, where the 59 | Licensed Material is a musical work, performance, or sound recording, Adapted 60 | Material is always produced where the Licensed Material is synched in timed 61 | relation with a moving image. 62 | 63 | b. Adapter's License means the license You apply to Your Copyright and Similar 64 | Rights in Your contributions to Adapted Material in accordance with the terms 65 | and conditions of this Public License. 66 | 67 | c. Copyright and Similar Rights means copyright and/or similar rights closely 68 | related to copyright including, without limitation, performance, broadcast, 69 | sound recording, and Sui Generis Database Rights, without regard to how the 70 | rights are labeled or categorized. For purposes of this Public License, the 71 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 72 | 73 | d. Effective Technological Measures means those measures that, in the absence 74 | of proper authority, may not be circumvented under laws fulfilling obligations 75 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 76 | and/or similar international agreements. 77 | 78 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other 79 | exception or limitation to Copyright and Similar Rights that applies to Your 80 | use of the Licensed Material. 81 | 82 | f. Licensed Material means the artistic or literary work, database, or other 83 | material to which the Licensor applied this Public License. 84 | 85 | g. Licensed Rights means the rights granted to You subject to the terms and 86 | conditions of this Public License, which are limited to all Copyright and 87 | Similar Rights that apply to Your use of the Licensed Material and that the 88 | Licensor has authority to license. 89 | 90 | h. Licensor means the individual(s) or entity(ies) granting rights under this 91 | Public License. 92 | 93 | i. Share means to provide material to the public by any means or process that 94 | requires permission under the Licensed Rights, such as reproduction, public 95 | display, public performance, distribution, dissemination, communication, or 96 | importation, and to make material available to the public including in ways 97 | that members of the public may access the material from a place and at a time 98 | individually chosen by them. 99 | 100 | j. Sui Generis Database Rights means rights other than copyright resulting 101 | from Directive 96/9/EC of the European Parliament and of the Council of 11 102 | March 1996 on the legal protection of databases, as amended and/or succeeded, 103 | as well as other essentially equivalent rights anywhere in the world. 104 | 105 | k. You means the individual or entity exercising the Licensed Rights under 106 | this Public License. Your has a corresponding meaning. 107 | 108 | Section 2 – Scope. 109 | 110 | a. License grant. 111 | 112 | 1. Subject to the terms and conditions of this Public License, the Licensor 113 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 114 | irrevocable license to exercise the Licensed Rights in the Licensed Material 115 | to: 116 | 117 | A. reproduce and Share the Licensed Material, in whole or in part; and 118 | 119 | B. produce, reproduce, and Share Adapted Material. 120 | 121 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 122 | and Limitations apply to Your use, this Public License does not apply, and 123 | You do not need to comply with its terms and conditions. 124 | 125 | 3. Term. The term of this Public License is specified in Section 6(a). 126 | 127 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 128 | You to exercise the Licensed Rights in all media and formats whether now known 129 | or hereafter created, and to make technical modifications necessary to do 130 | so. The Licensor waives and/or agrees not to assert any right or authority 131 | to forbid You from making technical modifications necessary to exercise the 132 | Licensed Rights, including technical modifications necessary to circumvent 133 | Effective Technological Measures. For purposes of this Public License, simply 134 | making modifications authorized by this Section 2(a)(4) never produces Adapted 135 | Material. 136 | 137 | 5. Downstream recipients. 138 | 139 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 140 | Material automatically receives an offer from the Licensor to exercise the 141 | Licensed Rights under the terms and conditions of this Public License. 142 | 143 | B. No downstream restrictions. You may not offer or impose any additional 144 | or different terms or conditions on, or apply any Effective Technological 145 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 146 | Rights by any recipient of the Licensed Material. 147 | 148 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 149 | as permission to assert or imply that You are, or that Your use of the Licensed 150 | Material is, connected with, or sponsored, endorsed, or granted official status 151 | by, the Licensor or others designated to receive attribution as provided in 152 | Section 3(a)(1)(A)(i). 153 | 154 | b. Other rights. 155 | 156 | 1. Moral rights, such as the right of integrity, are not licensed under this 157 | Public License, nor are publicity, privacy, and/or other similar personality 158 | rights; however, to the extent possible, the Licensor waives and/or agrees 159 | not to assert any such rights held by the Licensor to the limited extent necessary 160 | to allow You to exercise the Licensed Rights, but not otherwise. 161 | 162 | 2. Patent and trademark rights are not licensed under this Public License. 163 | 164 | 3. To the extent possible, the Licensor waives any right to collect royalties 165 | from You for the exercise of the Licensed Rights, whether directly or through 166 | a collecting society under any voluntary or waivable statutory or compulsory 167 | licensing scheme. In all other cases the Licensor expressly reserves any right 168 | to collect such royalties. 169 | 170 | Section 3 – License Conditions. 171 | 172 | Your exercise of the Licensed Rights is expressly made subject to the following 173 | conditions. 174 | 175 | a. Attribution. 176 | 177 | 1. If You Share the Licensed Material (including in modified form), You must: 178 | 179 | A. retain the following if it is supplied by the Licensor with the Licensed 180 | Material: 181 | 182 | i. identification of the creator(s) of the Licensed Material and any others 183 | designated to receive attribution, in any reasonable manner requested by the 184 | Licensor (including by pseudonym if designated); 185 | 186 | ii. a copyright notice; 187 | 188 | iii. a notice that refers to this Public License; 189 | 190 | iv. a notice that refers to the disclaimer of warranties; 191 | 192 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 193 | 194 | B. indicate if You modified the Licensed Material and retain an indication 195 | of any previous modifications; and 196 | 197 | C. indicate the Licensed Material is licensed under this Public License, and 198 | include the text of, or the URI or hyperlink to, this Public License. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 201 | based on the medium, means, and context in which You Share the Licensed Material. 202 | For example, it may be reasonable to satisfy the conditions by providing a 203 | URI or hyperlink to a resource that includes the required information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the information required 206 | by Section 3(a)(1)(A) to the extent reasonably practicable. 207 | 208 | 4. If You Share Adapted Material You produce, the Adapter's License You apply 209 | must not prevent recipients of the Adapted Material from complying with this 210 | Public License. 211 | 212 | Section 4 – Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that apply to 215 | Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 218 | reuse, reproduce, and Share all or a substantial portion of the contents of 219 | the database; 220 | 221 | b. if You include all or a substantial portion of the database contents in 222 | a database in which You have Sui Generis Database Rights, then the database 223 | in which You have Sui Generis Database Rights (but not its individual contents) 224 | is Adapted Material; and 225 | 226 | c. You must comply with the conditions in Section 3(a) if You Share all or 227 | a substantial portion of the contents of the database. 228 | 229 | For the avoidance of doubt, this Section 4 supplements and does not replace 230 | Your obligations under this Public License where the Licensed Rights include 231 | other Copyright and Similar Rights. 232 | 233 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 234 | 235 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 236 | the Licensor offers the Licensed Material as-is and as-available, and makes 237 | no representations or warranties of any kind concerning the Licensed Material, 238 | whether express, implied, statutory, or other. This includes, without limitation, 239 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 240 | absence of latent or other defects, accuracy, or the presence or absence of 241 | errors, whether or not known or discoverable. Where disclaimers of warranties 242 | are not allowed in full or in part, this disclaimer may not apply to You. 243 | 244 | b. To the extent possible, in no event will the Licensor be liable to You 245 | on any legal theory (including, without limitation, negligence) or otherwise 246 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 247 | or other losses, costs, expenses, or damages arising out of this Public License 248 | or use of the Licensed Material, even if the Licensor has been advised of 249 | the possibility of such losses, costs, expenses, or damages. Where a limitation 250 | of liability is not allowed in full or in part, this limitation may not apply 251 | to You. 252 | 253 | c. The disclaimer of warranties and limitation of liability provided above 254 | shall be interpreted in a manner that, to the extent possible, most closely 255 | approximates an absolute disclaimer and waiver of all liability. 256 | 257 | Section 6 – Term and Termination. 258 | 259 | a. This Public License applies for the term of the Copyright and Similar Rights 260 | licensed here. However, if You fail to comply with this Public License, then 261 | Your rights under this Public License terminate automatically. 262 | 263 | b. Where Your right to use the Licensed Material has terminated under Section 264 | 6(a), it reinstates: 265 | 266 | 1. automatically as of the date the violation is cured, provided it is cured 267 | within 30 days of Your discovery of the violation; or 268 | 269 | 2. upon express reinstatement by the Licensor. 270 | 271 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 272 | the Licensor may have to seek remedies for Your violations of this Public 273 | License. 274 | 275 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 276 | under separate terms or conditions or stop distributing the Licensed Material 277 | at any time; however, doing so will not terminate this Public License. 278 | 279 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 280 | 281 | Section 7 – Other Terms and Conditions. 282 | 283 | a. The Licensor shall not be bound by any additional or different terms or 284 | conditions communicated by You unless expressly agreed. 285 | 286 | b. Any arrangements, understandings, or agreements regarding the Licensed 287 | Material not stated herein are separate from and independent of the terms 288 | and conditions of this Public License. 289 | 290 | Section 8 – Interpretation. 291 | 292 | a. For the avoidance of doubt, this Public License does not, and shall not 293 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 294 | of the Licensed Material that could lawfully be made without permission under 295 | this Public License. 296 | 297 | b. To the extent possible, if any provision of this Public License is deemed 298 | unenforceable, it shall be automatically reformed to the minimum extent necessary 299 | to make it enforceable. If the provision cannot be reformed, it shall be severed 300 | from this Public License without affecting the enforceability of the remaining 301 | terms and conditions. 302 | 303 | c. No term or condition of this Public License will be waived and no failure 304 | to comply consented to unless expressly agreed to by the Licensor. 305 | 306 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 307 | upon, or waiver of, any privileges and immunities that apply to the Licensor 308 | or You, including from the legal processes of any jurisdiction or authority. 309 | 310 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 311 | Commons may elect to apply one of its public licenses to material it publishes 312 | and in those instances will be considered the "Licensor." The text of the 313 | Creative Commons public licenses is dedicated to the public domain under the 314 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 315 | that material is shared under a Creative Commons public license or as otherwise 316 | permitted by the Creative Commons policies published at creativecommons.org/policies, 317 | Creative Commons does not authorize the use of the trademark "Creative Commons" 318 | or any other trademark or logo of Creative Commons without its prior written 319 | consent including, without limitation, in connection with any unauthorized 320 | modifications to any of its public licenses or any other arrangements, understandings, 321 | or agreements concerning use of licensed material. For the avoidance of doubt, 322 | this paragraph does not form part of the public licenses. 323 | 324 | Creative Commons may be contacted at creativecommons.org. 325 | -------------------------------------------------------------------------------- /adafruit_esp32spi/adafruit_esp32spi.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_esp32spi` 7 | ================================================================================ 8 | 9 | CircuitPython driver library for using ESP32 as WiFi co-processor using SPI 10 | 11 | 12 | * Author(s): ladyada 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | **Hardware:** 18 | 19 | **Software and Dependencies:** 20 | 21 | * Adafruit CircuitPython firmware for the supported boards: 22 | https://github.com/adafruit/circuitpython/releases 23 | 24 | * Adafruit's Bus Device library: 25 | https://github.com/adafruit/Adafruit_CircuitPython_BusDevice 26 | 27 | """ 28 | 29 | import struct 30 | import time 31 | import warnings 32 | 33 | from adafruit_bus_device.spi_device import SPIDevice 34 | from digitalio import Direction 35 | from micropython import const 36 | 37 | __version__ = "0.0.0+auto.0" 38 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI.git" 39 | 40 | _SET_NET_CMD = const(0x10) 41 | _SET_PASSPHRASE_CMD = const(0x11) 42 | _SET_IP_CONFIG = const(0x14) 43 | _SET_DNS_CONFIG = const(0x15) 44 | _SET_HOSTNAME = const(0x16) 45 | _SET_AP_NET_CMD = const(0x18) 46 | _SET_AP_PASSPHRASE_CMD = const(0x19) 47 | _SET_DEBUG_CMD = const(0x1A) 48 | 49 | _GET_CONN_STATUS_CMD = const(0x20) 50 | _GET_IPADDR_CMD = const(0x21) 51 | _GET_MACADDR_CMD = const(0x22) 52 | _GET_CURR_SSID_CMD = const(0x23) 53 | _GET_CURR_BSSID_CMD = const(0x24) 54 | _GET_CURR_RSSI_CMD = const(0x25) 55 | _GET_CURR_ENCT_CMD = const(0x26) 56 | 57 | _SCAN_NETWORKS = const(0x27) 58 | _START_SERVER_TCP_CMD = const(0x28) 59 | _GET_SOCKET_CMD = const(0x3F) 60 | _GET_STATE_TCP_CMD = const(0x29) 61 | _DATA_SENT_TCP_CMD = const(0x2A) 62 | _AVAIL_DATA_TCP_CMD = const(0x2B) 63 | _GET_DATA_TCP_CMD = const(0x2C) 64 | _START_CLIENT_TCP_CMD = const(0x2D) 65 | _STOP_CLIENT_TCP_CMD = const(0x2E) 66 | _GET_CLIENT_STATE_TCP_CMD = const(0x2F) 67 | _DISCONNECT_CMD = const(0x30) 68 | _GET_IDX_RSSI_CMD = const(0x32) 69 | _GET_IDX_ENCT_CMD = const(0x33) 70 | _REQ_HOST_BY_NAME_CMD = const(0x34) 71 | _GET_HOST_BY_NAME_CMD = const(0x35) 72 | _START_SCAN_NETWORKS = const(0x36) 73 | _GET_FW_VERSION_CMD = const(0x37) 74 | _SEND_UDP_DATA_CMD = const(0x39) 75 | _GET_REMOTE_DATA_CMD = const(0x3A) 76 | _GET_TIME = const(0x3B) 77 | _GET_IDX_BSSID_CMD = const(0x3C) 78 | _GET_IDX_CHAN_CMD = const(0x3D) 79 | _PING_CMD = const(0x3E) 80 | 81 | _SEND_DATA_TCP_CMD = const(0x44) 82 | _GET_DATABUF_TCP_CMD = const(0x45) 83 | _INSERT_DATABUF_TCP_CMD = const(0x46) 84 | _SET_ENT_IDENT_CMD = const(0x4A) 85 | _SET_ENT_UNAME_CMD = const(0x4B) 86 | _SET_ENT_PASSWD_CMD = const(0x4C) 87 | _SET_ENT_ENABLE_CMD = const(0x4F) 88 | _SET_CLI_CERT = const(0x40) 89 | _SET_PK = const(0x41) 90 | 91 | _SET_PIN_MODE_CMD = const(0x50) 92 | _SET_DIGITAL_WRITE_CMD = const(0x51) 93 | _SET_ANALOG_WRITE_CMD = const(0x52) 94 | _SET_DIGITAL_READ_CMD = const(0x53) 95 | _SET_ANALOG_READ_CMD = const(0x54) 96 | 97 | _START_CMD = const(0xE0) 98 | _END_CMD = const(0xEE) 99 | _ERR_CMD = const(0xEF) 100 | _REPLY_FLAG = const(1 << 7) 101 | _CMD_FLAG = const(0) 102 | 103 | SOCKET_CLOSED = const(0) 104 | SOCKET_LISTEN = const(1) 105 | SOCKET_SYN_SENT = const(2) 106 | SOCKET_SYN_RCVD = const(3) 107 | SOCKET_ESTABLISHED = const(4) 108 | SOCKET_FIN_WAIT_1 = const(5) 109 | SOCKET_FIN_WAIT_2 = const(6) 110 | SOCKET_CLOSE_WAIT = const(7) 111 | SOCKET_CLOSING = const(8) 112 | SOCKET_LAST_ACK = const(9) 113 | SOCKET_TIME_WAIT = const(10) 114 | 115 | WL_NO_SHIELD = const(0xFF) 116 | WL_NO_MODULE = const(0xFF) 117 | WL_STOPPED = const(0xFE) 118 | WL_IDLE_STATUS = const(0) 119 | WL_NO_SSID_AVAIL = const(1) 120 | WL_SCAN_COMPLETED = const(2) 121 | WL_CONNECTED = const(3) 122 | WL_CONNECT_FAILED = const(4) 123 | WL_CONNECTION_LOST = const(5) 124 | WL_DISCONNECTED = const(6) 125 | WL_AP_LISTENING = const(7) 126 | WL_AP_CONNECTED = const(8) 127 | WL_AP_FAILED = const(9) 128 | 129 | ADC_ATTEN_DB_0 = const(0) 130 | ADC_ATTEN_DB_2_5 = const(1) 131 | ADC_ATTEN_DB_6 = const(2) 132 | ADC_ATTEN_DB_11 = const(3) 133 | 134 | 135 | class Network: 136 | """A wifi network provided by a nearby access point.""" 137 | 138 | def __init__( 139 | self, 140 | esp_spi_control=None, 141 | raw_ssid=None, 142 | raw_bssid=None, 143 | raw_rssi=None, 144 | raw_channel=None, 145 | raw_country=None, 146 | raw_authmode=None, 147 | ): 148 | self._esp_spi_control = esp_spi_control 149 | self._raw_ssid = raw_ssid 150 | self._raw_bssid = raw_bssid 151 | self._raw_rssi = raw_rssi 152 | self._raw_channel = raw_channel 153 | self._raw_country = raw_country 154 | self._raw_authmode = raw_authmode 155 | 156 | def _get_response(self, cmd): 157 | respose = self._esp_spi_control._send_command_get_response(cmd, [b"\xff"]) 158 | return respose[0] 159 | 160 | @property 161 | def ssid(self): 162 | """String id of the network""" 163 | if self._raw_ssid: 164 | response = self._raw_ssid 165 | else: 166 | response = self._get_response(_GET_CURR_SSID_CMD) 167 | return response.decode("utf-8") 168 | 169 | @property 170 | def bssid(self): 171 | """BSSID of the network (usually the AP’s MAC address)""" 172 | if self._raw_bssid: 173 | response = self._raw_bssid 174 | else: 175 | response = self._get_response(_GET_CURR_BSSID_CMD) 176 | return bytes(response) 177 | 178 | @property 179 | def rssi(self): 180 | """Signal strength of the network""" 181 | if self._raw_bssid: 182 | response = self._raw_rssi 183 | else: 184 | response = self._get_response(_GET_CURR_RSSI_CMD) 185 | return struct.unpack("= 3: 284 | print("Wait for ESP32 ready", end="") 285 | times = time.monotonic() 286 | while (time.monotonic() - times) < 10: # wait up to 10 seconds 287 | if not self._ready.value: # we're ready! 288 | break 289 | if self._debug >= 3: 290 | print(".", end="") 291 | time.sleep(0.05) 292 | else: 293 | raise TimeoutError("ESP32 not responding") 294 | if self._debug >= 3: 295 | print() 296 | 297 | def _send_command(self, cmd, params=None, *, param_len_16=False): 298 | """Send over a command with a list of parameters""" 299 | if not params: 300 | params = () 301 | 302 | packet_len = 4 # header + end byte 303 | for i, param in enumerate(params): 304 | packet_len += len(param) # parameter 305 | packet_len += 1 # size byte 306 | if param_len_16: 307 | packet_len += 1 # 2 of em here! 308 | while packet_len % 4 != 0: 309 | packet_len += 1 310 | # we may need more space 311 | if packet_len > len(self._sendbuf): 312 | self._sendbuf = bytearray(packet_len) 313 | 314 | self._sendbuf[0] = _START_CMD 315 | self._sendbuf[1] = cmd & ~_REPLY_FLAG 316 | self._sendbuf[2] = len(params) 317 | 318 | # handle parameters here 319 | ptr = 3 320 | for i, param in enumerate(params): 321 | if self._debug >= 2: 322 | print("\tSending param #%d is %d bytes long" % (i, len(param))) 323 | if param_len_16: 324 | self._sendbuf[ptr] = (len(param) >> 8) & 0xFF 325 | ptr += 1 326 | self._sendbuf[ptr] = len(param) & 0xFF 327 | ptr += 1 328 | for j, par in enumerate(param): 329 | self._sendbuf[ptr + j] = par 330 | ptr += len(param) 331 | self._sendbuf[ptr] = _END_CMD 332 | 333 | self._wait_for_ready() 334 | with self._spi_device as spi: 335 | times = time.monotonic() 336 | while (time.monotonic() - times) < 1: # wait up to 1000ms 337 | if self._ready.value: # ok ready to send! 338 | break 339 | else: 340 | raise TimeoutError("ESP32 timed out on SPI select") 341 | spi.write(self._sendbuf, start=0, end=packet_len) 342 | if self._debug >= 3: 343 | print("Wrote: ", [hex(b) for b in self._sendbuf[0:packet_len]]) 344 | 345 | def _read_byte(self, spi): 346 | """Read one byte from SPI""" 347 | spi.readinto(self._pbuf) 348 | if self._debug >= 3: 349 | print("\t\tRead:", hex(self._pbuf[0])) 350 | return self._pbuf[0] 351 | 352 | def _read_bytes(self, spi, buffer, start=0, end=None): 353 | """Read many bytes from SPI""" 354 | if not end: 355 | end = len(buffer) 356 | spi.readinto(buffer, start=start, end=end) 357 | if self._debug >= 3: 358 | print("\t\tRead:", [hex(i) for i in buffer]) 359 | 360 | def _wait_spi_char(self, spi, desired): 361 | """Read a byte with a retry loop, and if we get it, check that its what we expect""" 362 | for _ in range(10): 363 | r = self._read_byte(spi) 364 | if r == _ERR_CMD: 365 | raise BrokenPipeError("Error response to command") 366 | if r == desired: 367 | return True 368 | time.sleep(0.01) 369 | raise TimeoutError("Timed out waiting for SPI char") 370 | 371 | def _check_data(self, spi, desired): 372 | """Read a byte and verify its the value we want""" 373 | r = self._read_byte(spi) 374 | if r != desired: 375 | raise BrokenPipeError(f"Expected {desired:02X} but got {r:02X}") 376 | 377 | def _wait_response_cmd(self, cmd, num_responses=None, *, param_len_16=False): 378 | """Wait for ready, then parse the response""" 379 | self._wait_for_ready() 380 | 381 | responses = [] 382 | with self._spi_device as spi: 383 | times = time.monotonic() 384 | while (time.monotonic() - times) < 1: # wait up to 1000ms 385 | if self._ready.value: # ok ready to send! 386 | break 387 | else: 388 | raise TimeoutError("ESP32 timed out on SPI select") 389 | 390 | self._wait_spi_char(spi, _START_CMD) 391 | self._check_data(spi, cmd | _REPLY_FLAG) 392 | if num_responses is not None: 393 | self._check_data(spi, num_responses) 394 | else: 395 | num_responses = self._read_byte(spi) 396 | for num in range(num_responses): 397 | param_len = self._read_byte(spi) 398 | if param_len_16: 399 | param_len <<= 8 400 | param_len |= self._read_byte(spi) 401 | if self._debug >= 2: 402 | print("\tParameter #%d length is %d" % (num, param_len)) 403 | response = bytearray(param_len) 404 | self._read_bytes(spi, response) 405 | responses.append(response) 406 | self._check_data(spi, _END_CMD) 407 | 408 | if self._debug >= 2: 409 | print("Read %d: " % len(responses[0]), responses) 410 | return responses 411 | 412 | def _send_command_get_response( 413 | self, 414 | cmd, 415 | params=None, 416 | *, 417 | reply_params=1, 418 | sent_param_len_16=False, 419 | recv_param_len_16=False, 420 | ): 421 | """Send a high level SPI command, wait and return the response""" 422 | self._send_command(cmd, params, param_len_16=sent_param_len_16) 423 | return self._wait_response_cmd(cmd, reply_params, param_len_16=recv_param_len_16) 424 | 425 | @property 426 | def status(self): 427 | """The status of the ESP32 WiFi core. Can be WL_NO_SHIELD or WL_NO_MODULE 428 | (not found), WL_STOPPED, WL_IDLE_STATUS, WL_NO_SSID_AVAIL, WL_SCAN_COMPLETED, 429 | WL_CONNECTED, WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED, 430 | WL_AP_LISTENING, WL_AP_CONNECTED, WL_AP_FAILED""" 431 | resp = self._send_command_get_response(_GET_CONN_STATUS_CMD) 432 | if self._debug: 433 | print("Connection status:", resp[0][0]) 434 | return resp[0][0] # one byte response 435 | 436 | @property 437 | def firmware_version(self): 438 | """A string of the firmware version on the ESP32""" 439 | if self._debug: 440 | print("Firmware version") 441 | resp = self._send_command_get_response(_GET_FW_VERSION_CMD) 442 | return resp[0].decode("utf-8").replace("\x00", "") 443 | 444 | @property 445 | def MAC_address(self): 446 | """A bytearray containing the MAC address of the ESP32""" 447 | if self._debug: 448 | print("MAC address") 449 | resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xff"]) 450 | return resp[0] 451 | 452 | @property 453 | def MAC_address_actual(self): 454 | """A bytearray containing the actual MAC address of the ESP32""" 455 | return bytearray(reversed(self.MAC_address)) 456 | 457 | @property 458 | def mac_address(self): 459 | """A bytes containing the actual MAC address of the ESP32""" 460 | return bytes(self.MAC_address_actual) 461 | 462 | def start_scan_networks(self): 463 | """Begin a scan of visible access points. Follow up with a call 464 | to 'get_scan_networks' for response""" 465 | if self._debug: 466 | print("Start scan") 467 | resp = self._send_command_get_response(_START_SCAN_NETWORKS) 468 | if resp[0][0] != 1: 469 | raise OSError("Failed to start AP scan") 470 | 471 | def get_scan_networks(self): 472 | """The results of the latest SSID scan. Returns a list of dictionaries with 473 | 'ssid', 'rssi', 'encryption', bssid, and channel entries, one for each AP found 474 | """ 475 | self._send_command(_SCAN_NETWORKS) 476 | names = self._wait_response_cmd(_SCAN_NETWORKS) 477 | # print("SSID names:", names) 478 | APs = [] 479 | for i, name in enumerate(names): 480 | bssid = self._send_command_get_response(_GET_IDX_BSSID_CMD, ((i,),))[0] 481 | rssi = self._send_command_get_response(_GET_IDX_RSSI_CMD, ((i,),))[0] 482 | channel = self._send_command_get_response(_GET_IDX_CHAN_CMD, ((i,),))[0] 483 | authmode = self._send_command_get_response(_GET_IDX_ENCT_CMD, ((i,),))[0] 484 | APs.append( 485 | Network( 486 | raw_ssid=name, 487 | raw_bssid=bssid, 488 | raw_rssi=rssi, 489 | raw_channel=channel, 490 | raw_authmode=authmode, 491 | ) 492 | ) 493 | return APs 494 | 495 | def scan_networks(self): 496 | """Scan for visible access points, returns a list of access point details. 497 | Returns a list of dictionaries with 'ssid', 'rssi' and 'encryption' entries, 498 | one for each AP found""" 499 | self.start_scan_networks() 500 | for _ in range(10): # attempts 501 | time.sleep(2) 502 | APs = self.get_scan_networks() 503 | if APs: 504 | return APs 505 | return None 506 | 507 | def set_ip_config(self, ip_address, gateway, mask="255.255.255.0"): 508 | """Tells the ESP32 to set ip, gateway and network mask b"\xff" 509 | 510 | :param str ip_address: IP address (as a string). 511 | :param str gateway: Gateway (as a string). 512 | :param str mask: Mask, defaults to 255.255.255.0 (as a string). 513 | """ 514 | resp = self._send_command_get_response( 515 | _SET_IP_CONFIG, 516 | params=[ 517 | b"\x00", 518 | self.unpretty_ip(ip_address), 519 | self.unpretty_ip(gateway), 520 | self.unpretty_ip(mask), 521 | ], 522 | sent_param_len_16=False, 523 | ) 524 | return resp 525 | 526 | def set_dns_config(self, dns1, dns2): 527 | """Tells the ESP32 to set DNS 528 | 529 | :param str dns1: DNS server 1 IP as a string. 530 | :param str dns2: DNS server 2 IP as a string. 531 | """ 532 | resp = self._send_command_get_response( 533 | _SET_DNS_CONFIG, [b"\x00", self.unpretty_ip(dns1), self.unpretty_ip(dns2)] 534 | ) 535 | if resp[0][0] != 1: 536 | raise OSError("Failed to set dns with esp32") 537 | 538 | def set_hostname(self, hostname): 539 | """Tells the ESP32 to set hostname for DHCP. 540 | 541 | :param str hostname: The new host name. 542 | """ 543 | resp = self._send_command_get_response(_SET_HOSTNAME, [hostname.encode()]) 544 | if resp[0][0] != 1: 545 | raise OSError("Failed to set hostname with esp32") 546 | 547 | def wifi_set_network(self, ssid): 548 | """Tells the ESP32 to set the access point to the given ssid""" 549 | resp = self._send_command_get_response(_SET_NET_CMD, [ssid]) 550 | if resp[0][0] != 1: 551 | raise OSError("Failed to set network") 552 | 553 | def wifi_set_passphrase(self, ssid, passphrase): 554 | """Sets the desired access point ssid and passphrase""" 555 | resp = self._send_command_get_response(_SET_PASSPHRASE_CMD, [ssid, passphrase]) 556 | if resp[0][0] != 1: 557 | raise OSError("Failed to set passphrase") 558 | 559 | def wifi_set_entidentity(self, ident): 560 | """Sets the WPA2 Enterprise anonymous identity""" 561 | resp = self._send_command_get_response(_SET_ENT_IDENT_CMD, [ident]) 562 | if resp[0][0] != 1: 563 | raise OSError("Failed to set enterprise anonymous identity") 564 | 565 | def wifi_set_entusername(self, username): 566 | """Sets the desired WPA2 Enterprise username""" 567 | resp = self._send_command_get_response(_SET_ENT_UNAME_CMD, [username]) 568 | if resp[0][0] != 1: 569 | raise OSError("Failed to set enterprise username") 570 | 571 | def wifi_set_entpassword(self, password): 572 | """Sets the desired WPA2 Enterprise password""" 573 | resp = self._send_command_get_response(_SET_ENT_PASSWD_CMD, [password]) 574 | if resp[0][0] != 1: 575 | raise OSError("Failed to set enterprise password") 576 | 577 | def wifi_set_entenable(self): 578 | """Enables WPA2 Enterprise mode""" 579 | resp = self._send_command_get_response(_SET_ENT_ENABLE_CMD) 580 | if resp[0][0] != 1: 581 | raise OSError("Failed to enable enterprise mode") 582 | 583 | def _wifi_set_ap_network(self, ssid, channel): 584 | """Creates an Access point with SSID and Channel""" 585 | resp = self._send_command_get_response(_SET_AP_NET_CMD, [ssid, channel]) 586 | if resp[0][0] != 1: 587 | raise OSError("Failed to setup AP network") 588 | 589 | def _wifi_set_ap_passphrase(self, ssid, passphrase, channel): 590 | """Creates an Access point with SSID, passphrase, and Channel""" 591 | resp = self._send_command_get_response(_SET_AP_PASSPHRASE_CMD, [ssid, passphrase, channel]) 592 | if resp[0][0] != 1: 593 | raise OSError("Failed to setup AP password") 594 | 595 | @property 596 | def ap_info(self): 597 | """Network object containing BSSID, SSID, authmode, channel, country and RSSI when 598 | connected to an access point. None otherwise.""" 599 | if self.is_connected: 600 | return Network(esp_spi_control=self) 601 | return None 602 | 603 | @property 604 | def network_data(self): 605 | """A dictionary containing current connection details such as the 'ip_addr', 606 | 'netmask' and 'gateway'""" 607 | resp = self._send_command_get_response(_GET_IPADDR_CMD, [b"\xff"], reply_params=3) 608 | return {"ip_addr": resp[0], "netmask": resp[1], "gateway": resp[2]} 609 | 610 | @property 611 | def ip_address(self): 612 | """Our local IP address""" 613 | return self.network_data["ip_addr"] 614 | 615 | @property 616 | def connected(self): 617 | """Whether the ESP32 is connected to an access point""" 618 | try: 619 | return self.status == WL_CONNECTED 620 | except OSError: 621 | self.reset() 622 | return False 623 | 624 | @property 625 | def is_connected(self): 626 | """Whether the ESP32 is connected to an access point""" 627 | return self.connected 628 | 629 | @property 630 | def ap_listening(self): 631 | """Returns if the ESP32 is in access point mode and is listening for connections""" 632 | try: 633 | return self.status == WL_AP_LISTENING 634 | except OSError: 635 | self.reset() 636 | return False 637 | 638 | def disconnect(self): 639 | """Disconnect from the access point""" 640 | resp = self._send_command_get_response(_DISCONNECT_CMD) 641 | if resp[0][0] != 1: 642 | raise OSError("Failed to disconnect") 643 | 644 | def connect(self, ssid, password=None, timeout=10): 645 | """Connect to an access point with given name and password. 646 | 647 | **Deprecated functionality:** If the first argument (``ssid``) is a ``dict``, 648 | assume it is a dictionary with entries for keys ``"ssid"`` and, optionally, ``"password"``. 649 | This mimics the previous signature for ``connect()``. 650 | This upward compatibility will be removed in a future release. 651 | """ 652 | if isinstance(ssid, dict): # secrets 653 | warnings.warn( 654 | "The passing in of `secrets`, is deprecated. Use connect() with `ssid` and " 655 | "`password` instead and fetch values from settings.toml with `os.getenv()`." 656 | ) 657 | ssid, password = ssid["ssid"], ssid.get("password") 658 | self.connect_AP(ssid, password, timeout_s=timeout) 659 | 660 | def connect_AP(self, ssid, password, timeout_s=10): 661 | """Connect to an access point with given name and password. 662 | Will wait until specified timeout seconds and return on success 663 | or raise an exception on failure. 664 | 665 | :param ssid: the SSID to connect to 666 | :param passphrase: the password of the access point 667 | :param timeout_s: number of seconds until we time out and fail to create AP 668 | """ 669 | if self._debug: 670 | print( 671 | f"Connect to AP: {ssid=}, password=\ 672 | {repr(password if self._debug_show_secrets else '*' * len(password))}" 673 | ) 674 | if isinstance(ssid, str): 675 | ssid = bytes(ssid, "utf-8") 676 | if password: 677 | if isinstance(password, str): 678 | password = bytes(password, "utf-8") 679 | self.wifi_set_passphrase(ssid, password) 680 | else: 681 | self.wifi_set_network(ssid) 682 | times = time.monotonic() 683 | while (time.monotonic() - times) < timeout_s: # wait up until timeout 684 | stat = self.status 685 | if stat == WL_CONNECTED: 686 | return stat 687 | time.sleep(0.05) 688 | if stat in {WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED}: 689 | raise ConnectionError("Failed to connect to ssid", ssid) 690 | if stat == WL_NO_SSID_AVAIL: 691 | raise ConnectionError("No such ssid", ssid) 692 | raise OSError(f"Unknown error 0x{stat:02X}") 693 | 694 | def create_AP(self, ssid, password, channel=1, timeout=10): 695 | """Create an access point with the given name, password, and channel. 696 | Will wait until specified timeout seconds and return on success 697 | or raise an exception on failure. 698 | 699 | :param str ssid: the SSID of the created Access Point. Must be less than 32 chars. 700 | :param str password: the password of the created Access Point. Must be 8-63 chars. 701 | :param int channel: channel of created Access Point (1 - 14). 702 | :param int timeout: number of seconds until we time out and fail to create AP 703 | """ 704 | if len(ssid) > 32: 705 | raise ValueError("ssid must be no more than 32 characters") 706 | if password and (len(password) < 8 or len(password) > 64): 707 | raise ValueError("password must be 8 - 63 characters") 708 | if channel < 1 or channel > 14: 709 | raise ValueError("channel must be between 1 and 14") 710 | 711 | if isinstance(channel, int): 712 | channel = bytes(channel) 713 | if isinstance(ssid, str): 714 | ssid = bytes(ssid, "utf-8") 715 | if password: 716 | if isinstance(password, str): 717 | password = bytes(password, "utf-8") 718 | self._wifi_set_ap_passphrase(ssid, password, channel) 719 | else: 720 | self._wifi_set_ap_network(ssid, channel) 721 | 722 | times = time.monotonic() 723 | while (time.monotonic() - times) < timeout: # wait up to timeout 724 | stat = self.status 725 | if stat == WL_AP_LISTENING: 726 | return stat 727 | time.sleep(0.05) 728 | if stat == WL_AP_FAILED: 729 | raise ConnectionError("Failed to create AP", ssid) 730 | raise OSError(f"Unknown error 0x{stat:02x}") 731 | 732 | @property 733 | def ipv4_address(self): 734 | """IP address of the station when connected to an access point.""" 735 | return self.pretty_ip(self.ip_address) 736 | 737 | def pretty_ip(self, ip): # noqa: PLR6301 738 | """Converts a bytearray IP address to a dotted-quad string for printing""" 739 | return f"{ip[0]}.{ip[1]}.{ip[2]}.{ip[3]}" 740 | 741 | def unpretty_ip(self, ip): # noqa: PLR6301 742 | """Converts a dotted-quad string to a bytearray IP address""" 743 | octets = [int(x) for x in ip.split(".")] 744 | return bytes(octets) 745 | 746 | def get_host_by_name(self, hostname): 747 | """Convert a hostname to a packed 4-byte IP address. Returns 748 | a 4 bytearray""" 749 | if self._debug: 750 | print("*** Get host by name") 751 | if isinstance(hostname, str): 752 | hostname = bytes(hostname, "utf-8") 753 | resp = self._send_command_get_response(_REQ_HOST_BY_NAME_CMD, (hostname,)) 754 | if resp[0][0] != 1: 755 | raise ConnectionError("Failed to request hostname") 756 | resp = self._send_command_get_response(_GET_HOST_BY_NAME_CMD) 757 | return resp[0] 758 | 759 | def ping(self, dest, ttl=250): 760 | """Ping a destination IP address or hostname, with a max time-to-live 761 | (ttl). Returns a millisecond timing value""" 762 | if isinstance(dest, str): # convert to IP address 763 | dest = self.get_host_by_name(dest) 764 | # ttl must be between 0 and 255 765 | ttl = max(0, min(ttl, 255)) 766 | resp = self._send_command_get_response(_PING_CMD, (dest, (ttl,))) 767 | return struct.unpack("H", port) 793 | if isinstance(dest, str): # use the 5 arg version 794 | dest = bytes(dest, "utf-8") 795 | resp = self._send_command_get_response( 796 | _START_CLIENT_TCP_CMD, 797 | ( 798 | dest, 799 | b"\x00\x00\x00\x00", 800 | port_param, 801 | self._socknum_ll[0], 802 | (conn_mode,), 803 | ), 804 | ) 805 | else: # ip address, use 4 arg vesion 806 | resp = self._send_command_get_response( 807 | _START_CLIENT_TCP_CMD, 808 | (dest, port_param, self._socknum_ll[0], (conn_mode,)), 809 | ) 810 | if resp[0][0] != 1: 811 | raise ConnectionError("Could not connect to remote server") 812 | if conn_mode == ESP_SPIcontrol.TLS_MODE: 813 | self._tls_socket = socket_num 814 | 815 | def socket_status(self, socket_num): 816 | """Get the socket connection status, can be SOCKET_CLOSED, SOCKET_LISTEN, 817 | SOCKET_SYN_SENT, SOCKET_SYN_RCVD, SOCKET_ESTABLISHED, SOCKET_FIN_WAIT_1, 818 | SOCKET_FIN_WAIT_2, SOCKET_CLOSE_WAIT, SOCKET_CLOSING, SOCKET_LAST_ACK, or 819 | SOCKET_TIME_WAIT""" 820 | self._socknum_ll[0][0] = socket_num 821 | resp = self._send_command_get_response(_GET_CLIENT_STATE_TCP_CMD, self._socknum_ll) 822 | return resp[0][0] 823 | 824 | def socket_connected(self, socket_num): 825 | """Test if a socket is connected to the destination, returns boolean true/false""" 826 | return self.socket_status(socket_num) == SOCKET_ESTABLISHED 827 | 828 | def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE): 829 | """Write the bytearray buffer to a socket. 830 | Returns the number of bytes written""" 831 | if self._debug: 832 | print("Writing:", buffer) 833 | self._socknum_ll[0][0] = socket_num 834 | sent = 0 835 | total_chunks = (len(buffer) // 64) + 1 836 | send_command = _SEND_DATA_TCP_CMD 837 | if conn_mode == self.UDP_MODE: # UDP requires a different command to write 838 | send_command = _INSERT_DATABUF_TCP_CMD 839 | for chunk in range(total_chunks): 840 | resp = self._send_command_get_response( 841 | send_command, 842 | ( 843 | self._socknum_ll[0], 844 | memoryview(buffer)[(chunk * 64) : ((chunk + 1) * 64)], 845 | ), 846 | sent_param_len_16=True, 847 | ) 848 | sent += resp[0][0] 849 | 850 | if conn_mode == self.UDP_MODE: 851 | # UDP verifies chunks on write, not bytes 852 | if sent != total_chunks: 853 | raise ConnectionError("Failed to write %d chunks (sent %d)" % (total_chunks, sent)) 854 | # UDP needs to finalize with this command, does the actual sending 855 | resp = self._send_command_get_response(_SEND_UDP_DATA_CMD, self._socknum_ll) 856 | if resp[0][0] != 1: 857 | raise ConnectionError("Failed to send UDP data") 858 | return sent 859 | 860 | if sent != len(buffer): 861 | self.socket_close(socket_num) 862 | raise ConnectionError("Failed to send %d bytes (sent %d)" % (len(buffer), sent)) 863 | 864 | resp = self._send_command_get_response(_DATA_SENT_TCP_CMD, self._socknum_ll) 865 | if resp[0][0] != 1: 866 | raise ConnectionError("Failed to verify data sent") 867 | 868 | return sent 869 | 870 | def socket_available(self, socket_num): 871 | """Determine how many bytes are waiting to be read on the socket""" 872 | self._socknum_ll[0][0] = socket_num 873 | resp = self._send_command_get_response(_AVAIL_DATA_TCP_CMD, self._socknum_ll) 874 | reply = struct.unpack("> 8) & 0xFF)), 890 | sent_param_len_16=True, 891 | recv_param_len_16=True, 892 | ) 893 | return bytes(resp[0]) 894 | 895 | def socket_connect(self, socket_num, dest, port, conn_mode=TCP_MODE): 896 | """Open and verify we connected a socket to a destination IP address or hostname 897 | using the ESP32's internal reference number. By default we use 898 | 'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE (dest must 899 | be hostname for TLS_MODE!)""" 900 | if self._debug: 901 | print("*** Socket connect mode", conn_mode) 902 | 903 | self.socket_open(socket_num, dest, port, conn_mode=conn_mode) 904 | if conn_mode == self.UDP_MODE: 905 | # UDP doesn't actually establish a connection 906 | # but the socket for writing is created via start_server 907 | self.start_server(port, socket_num, conn_mode) 908 | return True 909 | 910 | times = time.monotonic() 911 | while (time.monotonic() - times) < 3: # wait 3 seconds 912 | if self.socket_connected(socket_num): 913 | return True 914 | time.sleep(0.01) 915 | raise TimeoutError("Failed to establish connection") 916 | 917 | def socket_close(self, socket_num): 918 | """Close a socket using the ESP32's internal reference number""" 919 | if self._debug: 920 | print("*** Closing socket #%d" % socket_num) 921 | self._socknum_ll[0][0] = socket_num 922 | try: 923 | self._send_command_get_response(_STOP_CLIENT_TCP_CMD, self._socknum_ll) 924 | except OSError: 925 | pass 926 | if socket_num == self._tls_socket: 927 | self._tls_socket = None 928 | 929 | def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): # pylint: disable=invalid-name 930 | """Opens a server on the specified port, using the ESP32's internal reference number""" 931 | if self._debug: 932 | print("*** starting server") 933 | self._socknum_ll[0][0] = socket_num 934 | params = [struct.pack(">H", port), self._socknum_ll[0], (conn_mode,)] 935 | if ip: 936 | params.insert(0, ip) 937 | resp = self._send_command_get_response(_START_SERVER_TCP_CMD, params) 938 | 939 | if resp[0][0] != 1: 940 | raise OSError("Could not start server") 941 | 942 | def server_state(self, socket_num): 943 | """Get the state of the ESP32's internal reference server socket number""" 944 | self._socknum_ll[0][0] = socket_num 945 | resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll) 946 | return resp[0][0] 947 | 948 | def get_remote_data(self, socket_num): 949 | """Get the IP address and port of the remote host""" 950 | self._socknum_ll[0][0] = socket_num 951 | resp = self._send_command_get_response( 952 | _GET_REMOTE_DATA_CMD, self._socknum_ll, reply_params=2 953 | ) 954 | return {"ip_addr": resp[0], "port": struct.unpack(" 1.5.0 1006 | fw_semver_maj = self.firmware_version[2] 1007 | assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above." 1008 | 1009 | resp = self._send_command_get_response(_SET_DIGITAL_READ_CMD, ((pin,),))[0] 1010 | if resp[0] == 0: 1011 | return False 1012 | if resp[0] == 1: 1013 | return True 1014 | raise OSError("_SET_DIGITAL_READ response error: response is not boolean", resp[0]) 1015 | 1016 | def set_analog_read(self, pin, atten=ADC_ATTEN_DB_11): 1017 | """Get the analog input value of pin. Returns an int between 0 and 65536. 1018 | 1019 | :param int pin: ESP32 GPIO pin to read from. 1020 | :param int atten: attenuation constant 1021 | """ 1022 | # Verify nina-fw => 1.5.0 1023 | fw_semver_maj = self.firmware_version[2] 1024 | assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above." 1025 | 1026 | resp = self._send_command_get_response(_SET_ANALOG_READ_CMD, ((pin,), (atten,))) 1027 | resp_analog = struct.unpack("