diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb2f60e..041a337 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,68 +10,5 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.x - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install dependencies - # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) - run: | - source actions-ci/install.sh - - name: Pip install Sphinx, pre-commit - run: | - pip install --force-reinstall Sphinx sphinx-rtd-theme pre-commit - - name: Library version - run: git describe --dirty --always --tags - - name: Setup problem matchers - uses: adafruit/circuitpython-action-library-ci-problem-matchers@v1 - - name: Pre-commit hooks - run: | - pre-commit run --all-files - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Archive bundles - uses: actions/upload-artifact@v2 - with: - name: bundles - path: ${{ github.workspace }}/bundles/ - - name: Build docs - working-directory: docs - run: sphinx-build -E -W -b html . _build/html - - name: Check For pyproject.toml - id: need-pypi - run: | - echo ::set-output name=pyproject-toml::$( find . -wholename './pyproject.toml' ) - - name: Build Python package - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - run: | - pip install --upgrade build twine - for file in $(find -not -path "./.*" -not -path "./docs*" \( -name "*.py" -o -name "*.toml" \) ); do - sed -i -e "s/0.0.0+auto.0/1.2.3/" $file; - done; - python -m build - twine check dist/* + - name: Run Build CI workflow + uses: adafruit/workflows-circuitpython-libs/build@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f3a0325..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,88 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -name: Release Actions - -on: - release: - types: [published] - -jobs: - upload-release-assets: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.x - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install deps - run: | - source actions-ci/install.sh - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Upload Release Assets - # the 'official' actions version does not yet support dynamically - # supplying asset names to upload. @csexton's version chosen based on - # discussion in the issue below, as its the simplest to implement and - # allows for selecting files with a pattern. - # https://github.com/actions/upload-release-asset/issues/4 - #uses: actions/upload-release-asset@v1.0.1 - uses: csexton/release-asset-action@master - with: - pattern: "bundles/*" - github-token: ${{ secrets.GITHUB_TOKEN }} - - upload-pypi: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Check For pyproject.toml - id: need-pypi - run: | - echo ::set-output name=pyproject-toml::$( find . -wholename './pyproject.toml' ) - - name: Set up Python - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - run: | - python -m pip install --upgrade pip - pip install --upgrade build twine - - name: Build and publish - if: contains(steps.need-pypi.outputs.pyproject-toml, 'pyproject.toml') - env: - TWINE_USERNAME: ${{ secrets.pypi_username }} - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - for file in $(find -not -path "./.*" -not -path "./docs*" \( -name "*.py" -o -name "*.toml" \) ); do - sed -i -e "s/0.0.0+auto.0/${{github.event.release.tag_name}}/" $file; - done; - python -m build - twine upload dist/* diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 0000000..9acec60 --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: GitHub Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run GitHub Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-gh@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + upload-url: ${{ github.event.release.upload_url }} diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..65775b7 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: PyPI Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run PyPI Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-pypi@main + with: + pypi-username: ${{ secrets.pypi_username }} + pypi-password: ${{ secrets.pypi_password }} diff --git a/.gitignore b/.gitignore index 544ec4a..db3d538 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ _build # Virtual environment-specific files .env +.venv # MacOS-specific files *.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3343606..70ade69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,21 +4,21 @@ repos: - repo: https://github.com/python/black - rev: 22.3.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/fsfe/reuse-tool - rev: v0.14.0 + rev: v1.1.2 hooks: - id: reuse - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.4.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pycqa/pylint - rev: v2.11.1 + rev: v2.17.4 hooks: - id: pylint name: pylint (library code) diff --git a/.pylintrc b/.pylintrc index f772971..f945e92 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # # SPDX-License-Identifier: Unlicense @@ -26,7 +26,7 @@ jobs=1 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes @@ -54,8 +54,8 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation,unspecified-encoding +# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call +disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -225,12 +225,6 @@ max-line-length=100 # Maximum number of lines in a module max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no @@ -257,38 +251,22 @@ min-similarity-lines=12 [BASIC] -# Naming hint for argument names -argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct argument names argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ -# Naming hint for attribute names -attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct attribute names attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Naming hint for class names -# class-name-hint=[A-Z_][a-zA-Z0-9]+$ -class-name-hint=[A-Z_][a-zA-Z0-9_]+$ - # Regular expression matching correct class names # class-rgx=[A-Z_][a-zA-Z0-9]+$ class-rgx=[A-Z_][a-zA-Z0-9_]+$ -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ @@ -296,9 +274,6 @@ const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # ones are exempt. docstring-min-length=-1 -# Naming hint for function names -function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct function names function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ @@ -309,21 +284,12 @@ good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ # Include a hint for the correct naming format with invalid-name include-naming-hint=no -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Naming hint for method names -method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct method names method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ @@ -339,9 +305,6 @@ no-docstring-rgx=^_ # to this list to register other decorators that produce valid properties. property-classes=abc.abstractproperty -# Naming hint for variable names -variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct variable names variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ @@ -433,4 +396,4 @@ min-public-methods=1 # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 33c2a61..88bca9f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,9 @@ # Required version: 2 +sphinx: + configuration: docs/conf.py + build: os: ubuntu-20.04 tools: diff --git a/README.rst b/README.rst index 489a996..2cf7cad 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,8 @@ Dependencies This driver depends on: * `Adafruit CircuitPython `_ +* `Adafruit CircuitPython ConnectionManager `_ +* `Adafruit CircuitPython Requests `_ Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading diff --git a/adafruit_espatcontrol/adafruit_espatcontrol.py b/adafruit_espatcontrol/adafruit_espatcontrol.py index e0edc78..721d307 100644 --- a/adafruit_espatcontrol/adafruit_espatcontrol.py +++ b/adafruit_espatcontrol/adafruit_espatcontrol.py @@ -65,10 +65,16 @@ class ESP_ATcontrol: TYPE_UDP = "UDP" TYPE_SSL = "SSL" TLS_MODE = "SSL" - STATUS_APCONNECTED = 2 - STATUS_SOCKETOPEN = 3 - STATUS_SOCKETCLOSED = 4 - STATUS_NOTCONNECTED = 5 + STATUS_APCONNECTED = 2 # CIPSTATUS method + STATUS_WIFI_APCONNECTED = 2 # CWSTATE method + STATUS_SOCKETOPEN = 3 # CIPSTATUS method + STATUS_SOCKET_OPEN = 3 # CIPSTATE method + STATUS_SOCKETCLOSED = 4 # CIPSTATUS method + STATUS_SOCKET_CLOSED = 4 # CIPSTATE method + STATUS_NOTCONNECTED = 5 # CIPSTATUS method + STATUS_WIFI_NOTCONNECTED = 1 # CWSTATE method + STATUS_WIFI_DISCONNECTED = 4 # CWSTATE method + USER_AGENT = "esp-idf/1.0 esp32" def __init__( @@ -79,7 +85,8 @@ def __init__( run_baudrate: Optional[int] = None, rts_pin: Optional[DigitalInOut] = None, reset_pin: Optional[DigitalInOut] = None, - debug: bool = False + debug: bool = False, + use_cipstatus: bool = False, ): """This function doesn't try to do any sync'ing, just sets up # the hardware, that way nothing can unexpectedly fail!""" @@ -106,6 +113,7 @@ def __init__( self._ifconfig = [] self._initialized = False self._conntype = None + self._use_cipstatus = use_cipstatus def begin(self) -> None: """Initialize the module by syncing, resetting if necessary, setting up @@ -130,6 +138,15 @@ def begin(self) -> None: except OKError: # ESP32 doesnt use CIPSSLSIZE, its ok! self.at_response("AT+CIPSSLCCONF?") + + try: + self.at_response("AT+CWSTATE?", retries=1, timeout=3) + except OKError: + # ESP8285's use CIPSTATUS and have no CWSTATE or CWIPSTATUS functions + self._use_cipstatus = True + if self._debug: + print("No CWSTATE support, using CIPSTATUS, it's ok!") + self._initialized = True return except OKError: @@ -169,6 +186,59 @@ def connect( print("Failed to connect\n", exp) raise + def connect_enterprise( + self, secrets: Dict[str, Union[str, int]], timeout: int = 15, retries: int = 3 + ) -> None: + """Repeatedly try to connect to an enterprise access point with the details in + the passed in 'secrets' dictionary. Be sure 'ssid','password','username','identity' + and 'method' are defined in the secrets dict! If 'timezone' is set, we'll also + configure SNTP""" + # Connect to WiFi if not already + retries = 3 + if self._debug: + print("In connect_enterprise()") + while True: + try: + if not self._initialized or retries == 0: + self.begin() + retries = 3 + AP = self.remote_AP # pylint: disable=invalid-name + if AP[0] is not None: + print("Connected to", AP[0]) + if AP[0] != secrets["ssid"]: + if self._debug: + print("Doing Enterprise connection sequence") + self.join_AP_Enterprise( + secrets["ssid"], + secrets["username"], + secrets["identity"], + secrets["password"], + secrets["method"], + timeout=timeout, + retries=retries, + ) + if "timezone" in secrets: + tzone = secrets["timezone"] + ntp = None + if "ntp_server" in secrets: + ntp = secrets["ntp_server"] + self.sntp_config(True, tzone, ntp) + print("Connected to", self.remote_AP[0]) + print("My IP Address:", self.local_ip) + return # yay! + except (RuntimeError, OKError) as exp: + print("Failed to connect, retrying\n", exp) + retries -= 1 + continue + + def set_autoconnect(self, autoconnect: bool) -> None: + """Set the auto connection status if the wifi connects automatically on powerup""" + if autoconnect is True: + auto_flag = "1" + else: + auto_flag = "0" + self.at_response("AT+CWAUTOCONN=" + auto_flag) + # *************************** SOCKET SETUP **************************** @property @@ -187,7 +257,7 @@ def socket_connect( # pylint: disable=too-many-branches remote_port: int, *, keepalive: int = 10, - retries: int = 1 + retries: int = 1, ) -> bool: """Open a socket. conntype can be TYPE_TCP, TYPE_UDP, or TYPE_SSL. Remote can be an IP address or DNS (we'll do the lookup for you. Remote port @@ -240,6 +310,8 @@ def socket_connect( # pylint: disable=too-many-branches + "," + str(keepalive) ) + if self._debug is True: + print("socket_connect(): Going to send command") replies = self.at_response(cmd, timeout=10, retries=retries).split(b"\r\n") for reply in replies: if reply == b"CONNECT" and ( @@ -249,6 +321,7 @@ def socket_connect( # pylint: disable=too-many-branches ): self._conntype = conntype return True + return False def socket_send(self, buffer: bytes, timeout: int = 1) -> bool: @@ -323,7 +396,6 @@ def socket_receive(self, timeout: int = 5) -> bytearray: i = 0 # reset the input buffer now that we know the size elif i > 20: i = 0 # Hmm we somehow didnt get a proper +IPD packet? start over - else: self.hw_flow(False) # stop the flow # read as much as we can! @@ -340,15 +412,14 @@ def socket_receive(self, timeout: int = 5) -> bytearray: break # We've received all the data. Don't wait until timeout. else: # no data waiting self.hw_flow(True) # start the floooow - totalsize = sum([len(x) for x in bundle]) + totalsize = sum(len(x) for x in bundle) ret = bytearray(totalsize) i = 0 for x in bundle: for char in x: ret[i] = char i += 1 - for x in bundle: - del x + del bundle gc.collect() return ret @@ -405,20 +476,112 @@ def is_connected(self) -> bool: self.STATUS_SOCKETOPEN, self.STATUS_SOCKETCLOSED, ): + if self._debug: + print("is_connected(): status says connected") return True except (OKError, RuntimeError): pass + if self._debug: + print("is_connected(): status says not connected") return False + # pylint: disable=too-many-branches + # pylint: disable=too-many-return-statements @property def status(self) -> Union[int, None]: """The IP connection status number (see AT+CIPSTATUS datasheet for meaning)""" - replies = self.at_response("AT+CIPSTATUS", timeout=5).split(b"\r\n") + if self._use_cipstatus: + replies = self.at_response("AT+CIPSTATUS", timeout=5).split(b"\r\n") + for reply in replies: + if reply.startswith(b"STATUS:"): + if self._debug: + print(f"CIPSTATUS state is {int(reply[7:8])}") + return int(reply[7:8]) + else: + status_w = self.status_wifi + status_s = self.status_socket + + # debug only, Check CIPSTATUS messages against CWSTATE/CIPSTATE + if self._debug: + replies = self.at_response("AT+CIPSTATUS", timeout=5).split(b"\r\n") + for reply in replies: + if reply.startswith(b"STATUS:"): + cipstatus = int(reply[7:8]) + print( + f"STATUS: CWSTATE: {status_w}, CIPSTATUS: {cipstatus}, CIPSTATE: {status_s}" + ) + + # Produce a cipstatus-compatible status code + # Codes are not the same between CWSTATE/CIPSTATUS so in some combinations + # we just pick what we hope is best. + if status_w in ( + self.STATUS_WIFI_NOTCONNECTED, + self.STATUS_WIFI_DISCONNECTED, + ): + if self._debug: + print(f"STATUS returning {self.STATUS_NOTCONNECTED}") + return self.STATUS_NOTCONNECTED + + if status_s == self.STATUS_SOCKET_OPEN: + if self._debug: + print(f"STATUS returning {self.STATUS_SOCKETOPEN}") + return self.STATUS_SOCKETOPEN + + if status_w == self.STATUS_WIFI_APCONNECTED: + if self._debug: + print(f"STATUS returning {self.STATUS_APCONNECTED}") + return self.STATUS_APCONNECTED + + # handle extra codes from CWSTATE + if status_w == 0: # station has not started any Wi-Fi connection. + if self._debug: + print("STATUS returning 1") + return 1 # this cipstatus had no previous handler variable + + # pylint: disable=line-too-long + if ( + status_w == 1 + ): # station has connected to an AP, but does not get an IPv4 address yet. + if self._debug: + print("STATUS returning 1") + return 1 # this cipstatus had no previous handler variable + + if status_w == 3: # station is in Wi-Fi connecting or reconnecting state. + if self._debug: + print(f"STATUS returning {self.STATUS_NOTCONNECTED}") + return self.STATUS_NOTCONNECTED + + if status_s == self.STATUS_SOCKET_CLOSED: + if self._debug: + print(f"STATUS returning {self.STATUS_SOCKET_CLOSED}") + return self.STATUS_SOCKET_CLOSED + + return None + + @property + def status_wifi(self) -> Union[int, None]: + """The WIFI connection status number (see AT+CWSTATE datasheet for meaning)""" + replies = self.at_response("AT+CWSTATE?", timeout=5).split(b"\r\n") for reply in replies: - if reply.startswith(b"STATUS:"): - return int(reply[7:8]) + if reply.startswith(b"+CWSTATE:"): + state_info = reply.split(b",") + if self._debug: + print( + f"State reply is {reply}, state_info[1] is {int(state_info[0][9:10])}" + ) + return int(state_info[0][9:10]) return None + @property + def status_socket(self) -> Union[int, None]: + """The Socket connection status number (see AT+CIPSTATE for meaning)""" + replies = self.at_response("AT+CIPSTATE?", timeout=5).split(b"\r\n") + for reply in replies: + # If there are any +CIPSTATE lines that means it's an open socket + if reply.startswith(b"+CIPSTATE:"): + return self.STATUS_SOCKET_OPEN + return self.STATUS_SOCKET_CLOSED + @property def mode(self) -> Union[int, None]: """What mode we're in, can be MODE_STATION, MODE_SOFTAP or MODE_SOFTAPSTATION""" @@ -507,6 +670,8 @@ def join_AP( # pylint: disable=invalid-name """Try to join an access point by name and password, will return immediately if we're already connected and won't try to reconnect""" # First make sure we're in 'station' mode so we can connect to AP's + if self._debug: + print("In join_AP()") if self.mode != self.MODE_STATION: self.mode = self.MODE_STATION @@ -526,6 +691,98 @@ def join_AP( # pylint: disable=invalid-name raise RuntimeError("Didn't get IP address") return + # pylint: disable=invalid-name + # pylint: disable=too-many-arguments + def join_AP_Enterprise( + self, + ssid: str, + username: str, + identity: str, + password: str, + method: int, + timeout: int = 30, + retries: int = 3, + ) -> None: + """Try to join an Enterprise access point by name and password, will return + immediately if we're already connected and won't try to reconnect""" + # Not sure how to verify certificates so we set that to not verify. + certificate_security = 0 # Bit0: Client certificate.Bit1: Server certificate. + + if self._debug: + print("In join_AP_Enterprise()") + if self.mode != self.MODE_STATION: + self.mode = self.MODE_STATION + + router = self.remote_AP + if router and router[0] == ssid: + return # we're already connected! + reply = self.at_response( + 'AT+CWJEAP="' + + ssid + + '",' + + str(method) + + ',"' + + identity + + '","' + + username + + '","' + + password + + '",' + + str(certificate_security), + timeout=timeout, + retries=retries, + ) + if b"WIFI CONNECTED" not in reply: + print("no CONNECTED") + raise RuntimeError("Couldn't connect to Enterprise WiFi") + if b"WIFI GOT IP" not in reply: + print("no IP") + raise RuntimeError("Didn't get IP address") + return + + def disconnect(self, timeout: int = 5, retries: int = 3): + """Disconnect from the AP. Tries whether connected or not.""" + # If we're not connected we likely don't get a "WIFI DISCONNECT" and just get the OK + # Note it still tries to disconnect even if it says we're not connected. + if not self._initialized: + self.begin() + stat = self.status + if stat in ( + self.STATUS_APCONNECTED, + self.STATUS_SOCKETOPEN, + self.STATUS_SOCKETCLOSED, + ): + wait_for_disconnect = True + else: + wait_for_disconnect = False + if self._debug is True: + print("disconnect(): Not connected, not waiting for disconnect message") + reply = self.at_response("AT+CWQAP", timeout=timeout, retries=retries) + # Don't bother waiting for disconnect message if we weren't connected already + # sometimes the "WIFI DISCONNECT" shows up in the reply and sometimes it doesn't. + if wait_for_disconnect is True: + if b"WIFI DISCONNECT" in reply: + if self._debug is True: + print(f"disconnect(): Got WIFI DISCONNECT: {reply}") + else: + stamp = time.monotonic() + response = b"" + while (time.monotonic() - stamp) < timeout: + if self._uart.in_waiting: + response += self._uart.read(1) + self.hw_flow(False) + if response[-15:] == b"WIFI DISCONNECT": + break + else: + self.hw_flow(True) + if self._debug: + if response[-15:] == b"WIFI DISCONNECT": + print(f"disconnect(): Got WIFI DISCONNECT: {response}") + else: + print( + f"disconnect(): Timed out wating for WIFI DISCONNECT: {response}" + ) + def scan_APs( # pylint: disable=invalid-name self, retries: int = 3 ) -> Union[List[List[bytes]], None]: @@ -601,7 +858,7 @@ def at_response(self, at_cmd: str, timeout: int = 5, retries: int = 3) -> bytes: break if response[-7:] == b"ERROR\r\n": break - if "AT+CWJAP=" in at_cmd: + if "AT+CWJAP=" in at_cmd or "AT+CWJEAP=" in at_cmd: if b"WIFI GOT IP\r\n" in response: break else: @@ -617,6 +874,11 @@ def at_response(self, at_cmd: str, timeout: int = 5, retries: int = 3) -> bytes: # special case, AT+CWJAP= does not return an ok :P if "AT+CWJAP=" in at_cmd and b"WIFI GOT IP\r\n" in response: return response + # special case, AT+CWJEAP= does not return an ok :P + if "AT+CWJEAP=" in at_cmd and b"WIFI GOT IP\r\n" in response: + return response + if "AT+CWQAP=" in at_cmd and b"WIFI DISCONNECT" in response: + return response # special case, ping also does not return an OK if "AT+PING" in at_cmd and b"ERROR\r\n" in response: return response @@ -672,16 +934,32 @@ def echo(self, echo: bool) -> None: else: self.at_response("ATE0", timeout=1) - def soft_reset(self) -> bool: + def soft_reset(self, timeout: int = 5) -> bool: """Perform a software reset by AT command. Returns True if we successfully performed, false if failed to reset""" try: self._uart.reset_input_buffer() reply = self.at_response("AT+RST", timeout=1) - if reply.strip(b"\r\n") == b"AT+RST": - time.sleep(2) - self._uart.reset_input_buffer() - return True + if self._debug: + print(f"Resetting with AT+RST, reply was {reply}") + stamp = time.monotonic() + response = b"" + while (time.monotonic() - stamp) < timeout: + if self._uart.in_waiting: + response += self._uart.read(1) + self.hw_flow(False) + if response[-5:] == b"ready": + break + else: + self.hw_flow(True) + if self._debug: + if response[-5:] == b"ready": + print(f"soft_reset(): Got ready: {response}") + else: + print(f"soft_reset(): imed out waiting for ready: {response}") + self._uart.reset_input_buffer() + self.sync() + return True except OKError: pass # fail, see below return False diff --git a/adafruit_espatcontrol/adafruit_espatcontrol_socket.py b/adafruit_espatcontrol/adafruit_espatcontrol_socket.py index d063552..e8a1c8b 100644 --- a/adafruit_espatcontrol/adafruit_espatcontrol_socket.py +++ b/adafruit_espatcontrol/adafruit_espatcontrol_socket.py @@ -23,6 +23,7 @@ def set_interface(iface: ESP_ATcontrol) -> None: SOCK_STREAM = const(1) AF_INET = const(2) + # pylint: disable=too-many-arguments, unused-argument def getaddrinfo( host: str, @@ -64,7 +65,8 @@ def __init__( def connect(self, address: Tuple[str, int], conntype: Optional[str] = None) -> None: """Connect the socket to the 'address' (which should be dotted quad IP). 'conntype' - is an extra that may indicate SSL or not, depending on the underlying interface""" + is an extra that may indicate SSL or not, depending on the underlying interface + """ host, port = address if not _the_interface.socket_connect( diff --git a/adafruit_espatcontrol/adafruit_espatcontrol_wifimanager.py b/adafruit_espatcontrol/adafruit_espatcontrol_wifimanager.py index 43e17cb..1bbda76 100755 --- a/adafruit_espatcontrol/adafruit_espatcontrol_wifimanager.py +++ b/adafruit_espatcontrol/adafruit_espatcontrol_wifimanager.py @@ -13,8 +13,9 @@ # pylint: disable=no-name-in-module -import adafruit_requests as requests -import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket +import adafruit_connection_manager +import adafruit_requests +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool from adafruit_espatcontrol.adafruit_espatcontrol import ESP_ATcontrol try: @@ -29,12 +30,15 @@ class ESPAT_WiFiManager: A class to help manage the Wifi connection """ + # pylint: disable=too-many-arguments def __init__( self, esp: ESP_ATcontrol, secrets: Dict[str, Union[str, int]], status_pixel: Optional[FillBasedLED] = None, attempts: int = 2, + enterprise: bool = False, + debug: bool = False, ): """ :param ESP_SPIcontrol esp: The ESP object we are using @@ -42,23 +46,36 @@ def __init__( :param status_pixel: (Optional) The pixel device - A NeoPixel or DotStar (default=None) :type status_pixel: NeoPixel or DotStar :param int attempts: (Optional) Unused, only for compatibility for old code + :param bool enterprise: (Optional) If True, try to connect to Enterprise AP + :param bool debug: (Optional) Print debug messages during operation """ # Read the settings self._esp = esp - self.debug = False + self.debug = debug self.secrets = secrets self.attempts = attempts - requests.set_socket(socket, esp) self.statuspix = status_pixel self.pixel_status(0) + self.enterprise = enterprise - def reset(self) -> None: + # create requests session + ssl_context = adafruit_connection_manager.create_fake_ssl_context( + pool, self._esp + ) + self._requests = adafruit_requests.Session(pool, ssl_context) + + def reset(self, hard_reset: bool = True, soft_reset: bool = False) -> None: """ Perform a hard reset on the ESP """ + self.pixel_status((100, 100, 100)) if self.debug: print("Resetting ESP") - self._esp.hard_reset() + if hard_reset is True: + self._esp.hard_reset() + if soft_reset is True: + self._esp.soft_reset() + self.pixel_status(0) def connect(self, timeout: int = 15, retries: int = 3) -> None: """ @@ -68,7 +85,12 @@ def connect(self, timeout: int = 15, retries: int = 3) -> None: if self.debug: print("Connecting to AP...") self.pixel_status((100, 0, 0)) - self._esp.connect(self.secrets, timeout=timeout, retries=retries) + if self.enterprise is False: + self._esp.connect(self.secrets, timeout=timeout, retries=retries) + else: + self._esp.connect_enterprise( + self.secrets, timeout=timeout, retries=retries + ) self.pixel_status((0, 100, 0)) except (ValueError, RuntimeError) as error: print("Failed to connect\n", error) @@ -82,7 +104,13 @@ def set_conntype(self, url: str) -> None: else ESP_ATcontrol.TYPE_TCP ) - def get(self, url: str, **kw: Any) -> requests.Response: + def disconnect(self) -> None: + """ + Disconnect the Wifi from the AP if any + """ + self._esp.disconnect() + + def get(self, url: str, **kw: Any) -> adafruit_requests.Response: """ Pass the Get request to requests and update Status NeoPixel @@ -98,11 +126,11 @@ def get(self, url: str, **kw: Any) -> requests.Response: self.connect() self.pixel_status((0, 0, 100)) self.set_conntype(url) - return_val = requests.get(url, **kw) + return_val = self._requests.get(url, **kw) self.pixel_status(0) return return_val - def post(self, url: str, **kw: Any) -> requests.Response: + def post(self, url: str, **kw: Any) -> adafruit_requests.Response: """ Pass the Post request to requests and update Status NeoPixel @@ -114,14 +142,20 @@ def post(self, url: str, **kw: Any) -> requests.Response: :return: The response from the request :rtype: Response """ + if self.debug: + print("in post()") if not self._esp.is_connected: + if self.debug: + print("post(): not connected, trying to connect") self.connect() self.pixel_status((0, 0, 100)) self.set_conntype(url) - return_val = requests.post(url, **kw) + return_val = self._requests.post(url, **kw) + self.pixel_status(0) + return return_val - def put(self, url: str, **kw: Any) -> requests.Response: + def put(self, url: str, **kw: Any) -> adafruit_requests.Response: """ Pass the put request to requests and update Status NeoPixel @@ -137,11 +171,11 @@ def put(self, url: str, **kw: Any) -> requests.Response: self.connect() self.pixel_status((0, 0, 100)) self.set_conntype(url) - return_val = requests.put(url, **kw) + return_val = self._requests.put(url, **kw) self.pixel_status(0) return return_val - def patch(self, url: str, **kw: Any) -> requests.Response: + def patch(self, url: str, **kw: Any) -> adafruit_requests.Response: """ Pass the patch request to requests and update Status NeoPixel @@ -157,11 +191,11 @@ def patch(self, url: str, **kw: Any) -> requests.Response: self.connect() self.pixel_status((0, 0, 100)) self.set_conntype(url) - return_val = requests.patch(url, **kw) + return_val = self._requests.patch(url, **kw) self.pixel_status(0) return return_val - def delete(self, url: str, **kw: Any) -> requests.Response: + def delete(self, url: str, **kw: Any) -> adafruit_requests.Response: """ Pass the delete request to requests and update Status NeoPixel @@ -177,7 +211,7 @@ def delete(self, url: str, **kw: Any) -> requests.Response: self.connect() self.pixel_status((0, 0, 100)) self.set_conntype(url) - return_val = requests.delete(url, **kw) + return_val = self._requests.delete(url, **kw) self.pixel_status(0) return return_val diff --git a/docs/conf.py b/docs/conf.py index f7b483f..9663e07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinxcontrib.jquery", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.todo", @@ -100,19 +101,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] - except: - html_theme = "default" - html_theme_path = ["."] -else: - html_theme_path = ["."] +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/requirements.txt b/docs/requirements.txt index 88e6733..979f568 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,6 @@ # # SPDX-License-Identifier: Unlicense -sphinx>=4.0.0 +sphinx +sphinxcontrib-jquery +sphinx-rtd-theme diff --git a/examples/esp_atcontrol_AIO_no_wifimanager-enterprise.py b/examples/esp_atcontrol_AIO_no_wifimanager-enterprise.py new file mode 100644 index 0000000..ca096e8 --- /dev/null +++ b/examples/esp_atcontrol_AIO_no_wifimanager-enterprise.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Note, you must create a feed called "test" in your AdafruitIO account. +# Your secrets file must contain your aio_username and aio_key + +import time +import board +import busio +import adafruit_connection_manager +import adafruit_requests +from digitalio import DigitalInOut +from digitalio import Direction +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool + + +# ESP32 AT +from adafruit_espatcontrol import adafruit_espatcontrol + + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# Debug Level +# Change the Debug Flag if you have issues with AT commands +debugflag = False + +if board.board_id == "challenger_rp2040_wifi": + RX = board.ESP_RX + TX = board.ESP_TX + resetpin = DigitalInOut(board.WIFI_RESET) + rtspin = False + uart = busio.UART(TX, RX, baudrate=11520, receiver_buffer_size=2048) + esp_boot = DigitalInOut(board.WIFI_MODE) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None +else: + RX = board.TX + TX = board.RX + resetpin = DigitalInOut(board.D4) + rtspin = DigitalInOut(board.D5) + uart = busio.UART( + board.TX, board.RX, baudrate=11520, timeout=0.1, receiver_buffer_size=512 + ) + esp_boot = DigitalInOut(board.D9) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None + +print("ESP AT commands") +esp = adafruit_espatcontrol.ESP_ATcontrol( + uart, 115200, reset_pin=resetpin, rts_pin=rtspin, debug=debugflag +) + +ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, esp) +requests = adafruit_requests.Session(pool, ssl_context) + +counter = 0 + +while True: + try: + while not esp.is_connected: + print("Connecting...") + esp.connect_enterprise(secrets) + print("Posting data...", end="") + data = counter + feed = "example" + payload = {"value": data} + response = requests.post( + "https://io.adafruit.com/api/v2/" + + secrets["aio_username"] + + "/feeds/" + + feed + + "/data", + json=payload, + headers={"X-AIO-KEY": secrets["aio_key"]}, + ) + print(response.json()) + response.close() + counter = counter + 1 + print("OK") + except (ValueError, RuntimeError, adafruit_espatcontrol.OKError) as e: + print("Failed to get data, retrying\n", e) + esp.soft_reset() + continue + response = None + time.sleep(15) diff --git a/examples/esp_atcontrol_AIO_wifimanager_enterprise.py b/examples/esp_atcontrol_AIO_wifimanager_enterprise.py new file mode 100644 index 0000000..ba19417 --- /dev/null +++ b/examples/esp_atcontrol_AIO_wifimanager_enterprise.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Note, you must create a feed called "test" in your AdafruitIO account. +# Your secrets file must contain your aio_username and aio_key + +import time +import board +import busio +from digitalio import DigitalInOut +from digitalio import Direction +import neopixel + +# ESP32 AT +from adafruit_espatcontrol import ( + adafruit_espatcontrol, + adafruit_espatcontrol_wifimanager, +) + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +pixel_status = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3, auto_write=True) + +# Debug Level +# Change the Debug Flag if you have issues with AT commands +debugflag = False + +if board.board_id == "challenger_rp2040_wifi": + RX = board.ESP_RX + TX = board.ESP_TX + resetpin = DigitalInOut(board.WIFI_RESET) + rtspin = False + uart = busio.UART(TX, RX, baudrate=11520, receiver_buffer_size=2048) + esp_boot = DigitalInOut(board.WIFI_MODE) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None +else: + RX = board.TX + TX = board.RX + resetpin = DigitalInOut(board.D4) + rtspin = DigitalInOut(board.D5) + uart = busio.UART(board.TX, board.RX, timeout=0.1) + esp_boot = DigitalInOut(board.D9) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = pixel_status + +print("ESP AT commands") +esp = adafruit_espatcontrol.ESP_ATcontrol( + uart, 115200, reset_pin=resetpin, rts_pin=rtspin, debug=debugflag +) +wifi = adafruit_espatcontrol_wifimanager.ESPAT_WiFiManager( + esp, secrets, status_light, enterprise=True, debug=debugflag +) +wifi.disconnect() +wifi.reset(soft_reset=True) + +counter = 0 + +while True: + try: + print("Posting data...", end="") + data = counter + feed = "example" + payload = {"value": data} + response = wifi.post( + "https://io.adafruit.com/api/v2/" + + secrets["aio_username"] + + "/feeds/" + + feed + + "/data", + json=payload, + headers={"X-AIO-KEY": secrets["aio_key"]}, + ) + print(response.json()) + response.close() + + counter = counter + 1 + print("OK") + wifi.disconnect() + + except (ValueError, RuntimeError, adafruit_espatcontrol.OKError) as e: + print("Failed to get data, retrying\n", e) + wifi.reset(soft_reset=True) + continue + response = None + time.sleep(15) diff --git a/examples/esp_atcontrol_countviewer.py b/examples/esp_atcontrol_countviewer.py index 671fb4c..aab3b64 100644 --- a/examples/esp_atcontrol_countviewer.py +++ b/examples/esp_atcontrol_countviewer.py @@ -14,8 +14,9 @@ from digitalio import Direction import neopixel from adafruit_ht16k33 import segments -import adafruit_requests as requests -import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket +import adafruit_connection_manager +import adafruit_requests +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool from adafruit_espatcontrol import adafruit_espatcontrol # Get wifi details and more from a secrets.py file @@ -101,7 +102,9 @@ ) esp.hard_reset() -requests.set_socket(socket, esp) +ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, esp) +requests = adafruit_requests.Session(pool, ssl_context) + # display if DISPLAY_ATTACHED: # Create the I2C interface. diff --git a/examples/esp_atcontrol_countviewer_enterprise.py b/examples/esp_atcontrol_countviewer_enterprise.py new file mode 100644 index 0000000..749a68a --- /dev/null +++ b/examples/esp_atcontrol_countviewer_enterprise.py @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +""" +This example will access an API, grab a number like hackaday skulls, github +stars, price of bitcoin, twitter followers... if you can find something that +spits out JSON data, we can display it! +""" +import gc +import time +import board +import busio +from digitalio import DigitalInOut +from digitalio import Direction +import neopixel +import adafruit_connection_manager +import adafruit_requests +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool +from adafruit_espatcontrol import adafruit_espatcontrol + +try: + from adafruit_ht16k33 import segments +except ImportError: + pass + + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# CONFIGURATION +PLAY_SOUND_ON_CHANGE = False +NEOPIXELS_ON_CHANGE = True +DISPLAY_ATTACHED = False +TIME_BETWEEN_QUERY = 60 # in seconds + +# Some data sources and JSON locations to try out + +# Bitcoin value in USD +# DATA_SOURCE = "http://api.coindesk.com/v1/bpi/currentprice.json" +# DATA_LOCATION = ["bpi", "USD", "rate_float"] + +# Github stars! You can query 1ce a minute without an API key token +# DATA_SOURCE = "https://api.github.com/repos/adafruit/circuitpython" +# if 'github_token' in secrets: +# DATA_SOURCE += "?access_token="+secrets['github_token'] +# DATA_LOCATION = ["stargazers_count"] + +# Youtube stats +# CHANNEL_ID = "UCpOlOeQjj7EsVnDh3zuCgsA" # this isn't a secret but you have to look it up +# DATA_SOURCE = "https://www.googleapis.com/youtube/v3/channels/?part=statistics&id=" \ +# + CHANNEL_ID +"&key="+secrets['youtube_token'] +# #try also 'viewCount' or 'videoCount +# DATA_LOCATION = ["items", 0, "statistics", "subscriberCount"] + + +# # Subreddit subscribers +# DATA_SOURCE = "https://www.reddit.com/r/circuitpython/about.json" +# DATA_LOCATION = ["data", "subscribers"] + +# Hackaday Skulls (likes), requires an API key +# DATA_SOURCE = "https://api.hackaday.io/v1/projects/1340?api_key="+secrets['hackaday_token'] +# DATA_LOCATION = ["skulls"] + +# Twitter followers +DATA_SOURCE = ( + "http://cdn.syndication.twimg.com/widgets/followbutton/info.json?" + + "screen_names=adafruit" +) +DATA_LOCATION = [0, "followers_count"] + +# Debug Level +# Change the Debug Flag if you have issues with AT commands +debugflag = True + +if board.board_id == "challenger_rp2040_wifi": + RX = board.ESP_RX + TX = board.ESP_TX + resetpin = DigitalInOut(board.WIFI_RESET) + rtspin = False + uart = busio.UART(TX, RX, baudrate=11520, receiver_buffer_size=2048) + esp_boot = DigitalInOut(board.WIFI_MODE) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None + pixel_pin = board.NEOPIXEL + num_pixels = 1 + pixel_type = "RGBW/GRBW" +else: + RX = board.TX + TX = board.RX + resetpin = DigitalInOut(board.D4) + rtspin = DigitalInOut(board.D5) + uart = busio.UART( + board.TX, board.RX, baudrate=11520, timeout=0.1, receiver_buffer_size=512 + ) + esp_boot = DigitalInOut(board.D9) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None + pixel_pin = board.A1 + num_pixels = 16 + pixel_type = "RGB/GRB" + +# Create the connection to the co-processor and reset +esp = adafruit_espatcontrol.ESP_ATcontrol( + uart, 115200, reset_pin=resetpin, rts_pin=rtspin, debug=debugflag +) +esp.soft_reset() +esp.disconnect() + +ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, esp) +requests = adafruit_requests.Session(pool, ssl_context) + +# display +if DISPLAY_ATTACHED: + # Create the I2C interface. + i2c = busio.I2C(board.SCL, board.SDA) + # Attach a 7 segment display and display -'s so we know its not live yet + display = segments.Seg7x4(i2c) + display.print("----") + +# neopixels +if NEOPIXELS_ON_CHANGE: + pixels = neopixel.NeoPixel( + pixel_pin, num_pixels, brightness=0.4, pixel_order=(1, 0, 2, 3) + ) + pixels.fill(20) + +# music! +if PLAY_SOUND_ON_CHANGE: + import audioio + + wave_file = open("coin.wav", "rb") # pylint: disable=consider-using-with + wave = audioio.WaveFile(wave_file) + +# we'll save the value in question +last_value = value = None +the_time = None +times = 0 + + +def chime_light(): + """Light up LEDs and play a tune""" + if NEOPIXELS_ON_CHANGE: + for i in range(0, 100, 10): + if pixel_type == "RGB/GRB": + pixels.fill((i, i, i)) + elif pixel_type == "RGBW/GRBW": + pixels.fill((i, i, i, i)) + pixels.show() + time.sleep(1) + if PLAY_SOUND_ON_CHANGE: + with audioio.AudioOut(board.A0) as audio: + audio.play(wave) + while audio.playing: + pass + if NEOPIXELS_ON_CHANGE: + for i in range(100, 0, -10): + if pixel_type == "RGB/GRB": + pixels.fill((i, i, i)) + elif pixel_type == "RGBW/GRBW": + pixels.fill((i, i, i, i)) + pixels.show() + time.sleep(1) + pixels.fill(0) + + +while True: + try: + while not esp.is_connected: + # secrets dictionary must contain 'ssid' and 'password' at a minimum + esp.connect_enterprise(secrets) + + the_time = esp.sntp_time + + # great, lets get the data + print("Retrieving data source...", end="") + r = requests.get(DATA_SOURCE) + print("Reply is OK!") + except (ValueError, RuntimeError, adafruit_espatcontrol.OKError) as e: + print("Failed to get data, retrying\n", e) + continue + # print('-'*40,) + # print("Headers: ", r.headers) + # print("Text:", r.text) + # print('-'*40) + + value = r.json() + for x in DATA_LOCATION: + value = value[x] + if not value: + continue + print("Times:{0}. The Time:{1}. Value: {2}".format(times, the_time, value)) + if DISPLAY_ATTACHED: + display.print(int(value)) + else: + print("INT Value:{0}".format(int(value))) + + if last_value != value: + chime_light() # animate the neopixels + last_value = value + times += 1 + + # normally we wouldn't have to do this, but we get bad fragments + r = value = None + gc.collect() + print("GC MEM:{0}".format(gc.mem_free())) # pylint: disable=no-member + print("Sleeping for: {0} Seconds".format(TIME_BETWEEN_QUERY)) + time.sleep(TIME_BETWEEN_QUERY) diff --git a/examples/esp_atcontrol_simple_enterprise.py b/examples/esp_atcontrol_simple_enterprise.py new file mode 100644 index 0000000..bfaabaa --- /dev/null +++ b/examples/esp_atcontrol_simple_enterprise.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +import time +import board +import busio +from digitalio import DigitalInOut +from digitalio import Direction +import adafruit_connection_manager +import adafruit_requests +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool +from adafruit_espatcontrol import adafruit_espatcontrol + + +# Get wifi details and more from a secrets.py file +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + +# Debug Level +# Change the Debug Flag if you have issues with AT commands +debugflag = False + +# How long between queries +TIME_BETWEEN_QUERY = 60 # in seconds + +if board.board_id == "challenger_rp2040_wifi": + RX = board.ESP_RX + TX = board.ESP_TX + resetpin = DigitalInOut(board.WIFI_RESET) + rtspin = False + uart = busio.UART(TX, RX, baudrate=11520, receiver_buffer_size=2048) + esp_boot = DigitalInOut(board.WIFI_MODE) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None +else: + RX = board.TX + TX = board.RX + resetpin = DigitalInOut(board.D4) + rtspin = DigitalInOut(board.D5) + uart = busio.UART( + board.TX, board.RX, baudrate=11520, timeout=0.1, receiver_buffer_size=512 + ) + esp_boot = DigitalInOut(board.D9) + esp_boot.direction = Direction.OUTPUT + esp_boot.value = True + status_light = None + +print("ESP AT commands") +esp = adafruit_espatcontrol.ESP_ATcontrol( + uart, 115200, reset_pin=resetpin, rts_pin=rtspin, debug=debugflag +) + +URL = "http://wifitest.adafruit.com/testwifi/index.html" +print("ESP AT GET URL", URL) + +print("Resetting ESP module") +esp.hard_reset() +esp.soft_reset() +esp.disconnect() +# time.sleep(20) +esp.set_autoconnect(False) + +ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, esp) +requests = adafruit_requests.Session(pool, ssl_context) + +while True: + try: + print(f"Checking connection to {secrets['ssid']}...") + while not esp.is_connected: + print("Connecting...") + esp.connect_enterprise(secrets) + # great, lets get the data + print("Retrieving URL...", end="") + r = requests.get(URL) + print("Status:", r.status_code) + print("Content type:", r.headers["content-type"]) + print("Content size:", r.headers["content-length"]) + print("Encoding:", r.encoding) + print("Text:", r.text) + print("Disconnecting from WiFi") + esp.disconnect() + esp.disconnect() + print("Sleeping for: {0} Seconds".format(TIME_BETWEEN_QUERY)) + time.sleep(TIME_BETWEEN_QUERY) + except (ValueError, RuntimeError, adafruit_espatcontrol.OKError) as e: + print("Failed to get data, retrying\n", e) + continue diff --git a/examples/esp_atcontrol_webclient.py b/examples/esp_atcontrol_webclient.py index da50e7f..00b5811 100644 --- a/examples/esp_atcontrol_webclient.py +++ b/examples/esp_atcontrol_webclient.py @@ -6,8 +6,9 @@ import busio from digitalio import DigitalInOut from digitalio import Direction -import adafruit_requests as requests -import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket +import adafruit_connection_manager +import adafruit_requests +import adafruit_espatcontrol.adafruit_espatcontrol_socket as pool from adafruit_espatcontrol import adafruit_espatcontrol @@ -57,7 +58,8 @@ print("Resetting ESP module") esp.hard_reset() -requests.set_socket(socket, esp) +ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, esp) +requests = adafruit_requests.Session(pool, ssl_context) while True: try: diff --git a/examples/secrets_enterprise.py b/examples/secrets_enterprise.py new file mode 100644 index 0000000..ceae755 --- /dev/null +++ b/examples/secrets_enterprise.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# This file is where you keep secret settings, passwords, and tokens! +# If you put them in the code you risk committing that info or sharing it + +secrets = { + "ssid": "your-ssid", + "password": "your-password", + "identity": "your-identity", + "username": "your-username", + "method": 1, # 0 = EAP-TLS, 1=EAP-PEAP, 2=EAP-TTLSs + "timezone": -2, # this is offset from UTC + "github_token": "abcdefghij0123456789", + "aio_username": "your-aio-username", + "aio_key": "your-aio-key", +} diff --git a/requirements.txt b/requirements.txt index 45266c4..13401af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ Adafruit-Blinka adafruit-circuitpython-typing>=1.4.0 +adafruit-circuitpython-connectionmanager +adafruit-circuitpython-requests pyserial