├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue.yml └── workflows │ ├── release.yml │ ├── test-release.yaml │ ├── test.yml │ └── unittest.yaml ├── .gitignore ├── .gitmodules ├── .yamllint ├── LICENSE ├── README.md ├── boot.py ├── changelog.md ├── main.py ├── microdot ├── LICENSE ├── __init__.py ├── microdot.py ├── microdot_asyncio.py └── microdot_utemplate.py ├── package.json ├── pkg_resources └── pkg_resources.py ├── requirements-deploy.txt ├── requirements-test.txt ├── requirements.txt ├── sdist_upip.py ├── setup.py ├── simulation ├── .coveragerc ├── LICENSE.txt ├── README.md ├── create_report_dirs.py ├── prepare_test.sh ├── requirements.txt ├── run.sh ├── setup.cfg ├── setup.py ├── src │ ├── __init__.py │ ├── generic_helper │ │ ├── __init__.py │ │ ├── generic_helper.py │ │ └── message.py │ ├── led_helper │ │ ├── __init__.py │ │ ├── led_helper.py │ │ └── neopixel.py │ ├── machine │ │ ├── __init__.py │ │ ├── machine.py │ │ ├── pin.py │ │ ├── rtc.py │ │ └── timer.py │ ├── path_helper │ │ ├── __init__.py │ │ └── path_helper.py │ ├── run_simulation.py │ ├── time_helper │ │ ├── __init__.py │ │ └── time_helper.py │ ├── wifi_helper │ │ ├── __init__.py │ │ ├── network.py │ │ └── wifi_helper.py │ └── wifi_manager │ │ ├── __init__.py │ │ └── wifi_manager.py ├── templates │ ├── data.tpl.html │ ├── index.tpl.html │ ├── modbus_data.json │ ├── remove.tpl.html │ ├── result.html │ ├── select.tpl.html │ └── wifi_select_loader.tpl.html └── tests │ ├── data │ ├── encrypted │ │ ├── multi-network.json │ │ └── single-network.json │ └── unencrypted │ │ ├── multi-network.json │ │ ├── no-json.txt │ │ └── single-network.json │ ├── test_absolute_truth.py │ ├── test_generic_helper.py │ ├── test_led_helper.py │ ├── test_machine.py │ ├── test_message.py │ ├── test_neopixel.py │ ├── test_network.py │ ├── test_path_helper.py │ ├── test_pin.py │ ├── test_rtc.py │ ├── test_template.py │ ├── test_time_helper.py │ ├── test_timer.py │ ├── test_wifi_helper.py │ ├── test_wifi_manager.py │ ├── unittest.cfg │ └── unittest_helper.py ├── static ├── README.md ├── css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.gz │ ├── bootstrap.min.css.old │ ├── list-groups.css │ ├── main.css │ └── style.css ├── favicon.ico ├── js │ ├── bootstrap.min.js │ ├── bootstrap.min.js.gz │ ├── toast.js │ └── toast.js.gz └── style.css ├── templates ├── index.tpl ├── remove.tpl ├── select.tpl ├── wifi_configs.tpl └── wifi_select_loader.tpl ├── utemplate ├── compiled.py ├── recompile.py └── source.py └── wifi_manager ├── __init__.py ├── version.py └── wifi_manager.py /.flake8: -------------------------------------------------------------------------------- 1 | # Configuration for flake8 analysis 2 | [flake8] 3 | # Set the maximum length that any line (with some exceptions) may be. 4 | # max-line-length = 120 5 | 6 | # Set the maximum length that a comment or docstring line may be. 7 | # max-doc-length = 120 8 | 9 | # Set the maximum allowed McCabe complexity value for a block of code. 10 | # max-complexity = 15 11 | 12 | # Specify a list of codes to ignore. 13 | # D107: Missing docstring in __init__ 14 | # D400: First line should end with a period 15 | # W504: line break after binary operator -> Cannot break line with a long pathlib Path 16 | # D204: 1 blank line required after class docstring 17 | ignore = D107, D400, W504, D204 18 | 19 | # Specify a list of mappings of files and the codes that should be ignored for the entirety of the file. 20 | per-file-ignores = 21 | tests/*:D101,D102,D104 22 | 23 | # Provide a comma-separated list of glob patterns to exclude from checks. 24 | exclude = 25 | # No need to traverse our git directory 26 | .git, 27 | # Python virtual environments 28 | .venv, 29 | # tox virtual environments 30 | .tox, 31 | # There's no value in checking cache directories 32 | __pycache__, 33 | # The conf file is mostly autogenerated, ignore it 34 | docs/source/conf.py, 35 | # This contains our built documentation 36 | build, 37 | # This contains builds that we don't want to check 38 | dist, 39 | # We don't use __init__.py for scripts 40 | __init__.py 41 | # example testing folder before going live 42 | thinking 43 | .idea 44 | # custom scripts, not being part of the distribution 45 | libs_external 46 | sdist_upip.py 47 | setup.py 48 | R.py 49 | # external packages added to this package to be independent 50 | utemplate 51 | 52 | # Provide a comma-separated list of glob patterns to add to the list of excluded ones. 53 | # extend-exclude = 54 | # legacy/, 55 | # vendor/ 56 | 57 | # Provide a comma-separate list of glob patterns to include for checks. 58 | # filename = 59 | # example.py, 60 | # another-example*.py 61 | 62 | # Enable PyFlakes syntax checking of doctests in docstrings. 63 | doctests = False 64 | 65 | # Specify which files are checked by PyFlakes for doctest syntax. 66 | # include-in-doctest = 67 | # dir/subdir/file.py, 68 | # dir/other/file.py 69 | 70 | # Specify which files are not to be checked by PyFlakes for doctest syntax. 71 | # exclude-from-doctest = 72 | # tests/* 73 | 74 | # Enable off-by-default extensions. 75 | # enable-extensions = 76 | # H111, 77 | # G123 78 | 79 | # If True, report all errors, even if it is on the same line as a # NOQA comment. 80 | disable-noqa = False 81 | 82 | # Specify the number of subprocesses that Flake8 will use to run checks in parallel. 83 | jobs = auto 84 | 85 | # Also print output to stdout if output-file has been configured. 86 | tee = True 87 | 88 | # Count the number of occurrences of each error/warning code and print a report. 89 | statistics = True 90 | 91 | # Print the total number of errors. 92 | count = True 93 | 94 | # Print the source code generating the error/warning in question. 95 | show-source = True 96 | 97 | # Decrease the verbosity of Flake8’s output. Each time you specify it, it will print less and less information. 98 | quiet = 0 99 | 100 | # Select the formatter used to display errors to the user. 101 | format = pylint 102 | 103 | [pydocstyle] 104 | # choose the basic list of checked errors by specifying an existing convention. Possible conventions: pep257, numpy, google. 105 | convention = pep257 106 | 107 | # check only files that exactly match regular expression 108 | # match = (?!test_).*\.py 109 | 110 | # search only dirs that exactly match regular expression 111 | # match_dir = [^\.].* 112 | 113 | # ignore any functions or methods that are decorated by a function with a name fitting the regular expression. 114 | # ignore_decorators = 115 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | blank_issues_enabled: false 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: "Default issue" 4 | description: Report any kind of issue 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Please enter an explicit description of your issue 11 | placeholder: Short and explicit description of your incident... 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: reproduction 16 | attributes: 17 | label: Reproduction steps 18 | description: | 19 | Please enter an explicit description to reproduce this issue 20 | value: | 21 | 1. 22 | 2. 23 | 3. 24 | ... 25 | validations: 26 | required: true 27 | - type: input 28 | id: version 29 | attributes: 30 | label: MicroPython version 31 | description: Which MicroPython version are you using? 32 | placeholder: v1.20.0 33 | validations: 34 | required: true 35 | - type: dropdown 36 | id: board 37 | attributes: 38 | label: MicroPython board 39 | description: Which MicroPython board are you using? 40 | options: 41 | - pyboard 42 | - Raspberry Pico 43 | - ESP32 44 | - ESP8266 45 | - WiPy 46 | - i.MXRT 47 | - SAMD21/SAMD51 48 | - Renesas 49 | - Zephyr 50 | - UNIX 51 | - other 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: package-version 56 | attributes: 57 | label: MicroPython ESP WiFi Manager version 58 | description: Which version of this lib are you using? 59 | value: | 60 | # e.g. v1.9.0 61 | # use the following command to get the used version 62 | import os 63 | from wifi_manager import version 64 | print('MicroPython infos:', os.uname()) 65 | print('Used micropthon-esp-wifi-manager version:', version.__version__)) 66 | render: python 67 | validations: 68 | required: true 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Relevant log output 73 | description: > 74 | Please copy and paste any relevant log output. 75 | This will be automatically formatted into code, so no need for 76 | backticks. 77 | render: bash 78 | - type: textarea 79 | id: usercode 80 | attributes: 81 | label: User code 82 | description: > 83 | Please copy and paste any relevant user code. 84 | This will be automatically formatted into Python code, so no need for 85 | backticks. 86 | render: python 87 | - type: textarea 88 | id: additional 89 | attributes: 90 | label: Additional informations 91 | description: Please provide additional informations if available 92 | placeholder: Some more informations 93 | validations: 94 | required: false 95 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help make automated releases for this project 5 | 6 | name: Upload Python Package 7 | 8 | on: 9 | push: 10 | branches: 11 | - develop 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Set up Python 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: '3.9' 26 | - name: Install build dependencies 27 | run: | 28 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 29 | - name: Build package 30 | run: | 31 | changelog2version \ 32 | --changelog_file changelog.md \ 33 | --version_file wifi_manager/version.py \ 34 | --version_file_type py \ 35 | --debug 36 | python setup.py sdist 37 | rm dist/*.orig 38 | # sdist call create non conform twine files *.orig, remove them 39 | - name: Publish package 40 | uses: pypa/gh-action-pypi-publish@release/v1.5 41 | with: 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | skip_existing: true 44 | verbose: true 45 | print_hash: true 46 | - name: 'Create changelog based release' 47 | uses: brainelectronics/changelog-based-release@v1 48 | with: 49 | # note you'll typically need to create a personal access token 50 | # with permissions to create releases in the other repo 51 | # or you set the "contents" permissions to "write" as in this example 52 | changelog-path: changelog.md 53 | tag-name-prefix: '' 54 | tag-name-extension: '' 55 | release-name-prefix: '' 56 | release-name-extension: '' 57 | draft-release: true 58 | prerelease: false 59 | -------------------------------------------------------------------------------- /.github/workflows/test-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help make automated test releases for this project 5 | 6 | name: Upload Python Package to test.pypi.org 7 | 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | test-deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Set up Python 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: '3.9' 23 | - name: Install build dependencies 24 | run: | 25 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 26 | - name: Build package 27 | run: | 28 | changelog2version \ 29 | --changelog_file changelog.md \ 30 | --version_file wifi_manager/version.py \ 31 | --version_file_type py \ 32 | --additional_version_info="-rc${{ github.run_number }}.dev${{ github.event.number }}" \ 33 | --debug 34 | python setup.py sdist 35 | - name: Test built package 36 | # sdist call creates non twine conform "*.orig" files, remove them 37 | run: | 38 | rm dist/*.orig 39 | twine check dist/*.tar.gz 40 | - name: Archive build package artifact 41 | uses: actions/upload-artifact@v3 42 | with: 43 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 44 | # ${{ github.repository }} and ${{ github.ref_name }} can't be used 45 | # for artifact name due to unallowed '/' 46 | name: dist_repo.${{ github.event.repository.name }}_sha.${{ github.sha }}_build.${{ github.run_number }} 47 | path: dist/*.tar.gz 48 | retention-days: 14 49 | - name: Publish package 50 | uses: pypa/gh-action-pypi-publish@release/v1.5 51 | with: 52 | repository_url: https://test.pypi.org/legacy/ 53 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 54 | skip_existing: true 55 | verbose: true 56 | print_hash: true 57 | - name: 'Create changelog based prerelease' 58 | uses: brainelectronics/changelog-based-release@v1 59 | with: 60 | # note you'll typically need to create a personal access token 61 | # with permissions to create releases in the other repo 62 | # or you set the "contents" permissions to "write" as in this example 63 | changelog-path: changelog.md 64 | tag-name-prefix: '' 65 | tag-name-extension: '-rc${{ github.run_number }}.dev${{ github.event.number }}' 66 | release-name-prefix: '' 67 | release-name-extension: '-rc${{ github.run_number }}.dev${{ github.event.number }}' 68 | draft-release: true 69 | prerelease: true 70 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # This workflow will install Python dependencies, run tests and lint with a 4 | # specific Python version 5 | # For more information see: 6 | # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 7 | 8 | name: Test Python package 9 | 10 | on: 11 | push: 12 | # branches: [ $default-branch ] 13 | branches-ignore: 14 | - 'main' 15 | - 'develop' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Set up Python 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: '3.9' 30 | - name: Install test dependencies 31 | run: | 32 | pip install -r requirements-test.txt 33 | - name: Lint with flake8 34 | run: | 35 | flake8 . 36 | - name: Install deploy dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 40 | - name: Build package 41 | run: | 42 | changelog2version \ 43 | --changelog_file changelog.md \ 44 | --version_file wifi_manager/version.py \ 45 | --version_file_type py \ 46 | --debug 47 | python setup.py sdist 48 | - name: Test built package 49 | # sdist call creates non twine conform "*.orig" files, remove them 50 | run: | 51 | rm dist/*.orig 52 | twine check dist/* 53 | - name: Validate mip package file 54 | run: | 55 | upy-package \ 56 | --setup_file setup.py \ 57 | --package_changelog_file changelog.md \ 58 | --package_file package.json \ 59 | --validate \ 60 | --ignore-version \ 61 | --ignore-deps \ 62 | --ignore-boot-main 63 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help run automated tests for this project 5 | 6 | name: Unittest Python Package 7 | 8 | on: [push, pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test-and-coverage: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-python@v3 19 | with: 20 | python-version: '3.9' 21 | - name: Execute tests 22 | run: | 23 | pip install -r requirements-test.txt 24 | cd simulation 25 | python create_report_dirs.py 26 | nose2 --config tests/unittest.cfg 27 | - name: Create coverage report 28 | run: | 29 | cd simulation 30 | coverage xml 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v3 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | files: simulation/reports/coverage/coverage.xml 36 | flags: unittests 37 | fail_ci_if_error: true 38 | # path_to_write_report: ./reports/coverage/codecov_report.txt 39 | verbose: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | */R.py 3 | 4 | .vagrant/ 5 | 6 | # project specific files 7 | simulation/wifi-secure.json 8 | .idea 9 | simulation/reports/* 10 | .rshell-setup 11 | configs/* 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | # micropython libs are stored in lib/ 30 | # lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | cover/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | *.sqlite3 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | .pybuilder/ 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | # For a library or package, you might want to ignore these files since the code is 101 | # intended to run in multiple environments; otherwise, check them in: 102 | # .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | 148 | # pytype static type analyzer 149 | .pytype/ 150 | 151 | # Cython debug symbols 152 | cython_debug/ 153 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/.gitmodules -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | ignore: 6 | - .tox 7 | - .venv 8 | 9 | rules: 10 | line-length: 11 | level: warning 12 | ignore: 13 | - .github/workflows/* 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 brainelectronics and contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP WiFi Manager 2 | 3 | [![Downloads](https://pepy.tech/badge/micropython-esp-wifi-manager)](https://pepy.tech/project/micropython-esp-wifi-manager) 4 | ![Release](https://img.shields.io/github/v/release/brainelectronics/micropython-esp-wifi-manager?include_prereleases&color=success) 5 | ![MicroPython](https://img.shields.io/badge/micropython-Ok-green.svg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![CI](https://github.com/brainelectronics/micropython-esp-wifi-manager/actions/workflows/release.yml/badge.svg)](https://github.com/brainelectronics/micropython-esp-wifi-manager/actions/workflows/release.yml) 8 | 9 | MicroPython WiFi Manager to configure and connect to networks 10 | 11 | ----------------------- 12 | 13 | 14 | 15 | - [Installation](#installation) 16 | - [Install required tools](#install-required-tools) 17 | - [Flash firmware](#flash-firmware) 18 | - [Upload files to board](#upload-files-to-board) 19 | - [Install package](#install-package) 20 | - [General](#general) 21 | - [Hook the WiFi Manager boot logic](#hook-the-wifi-manager-boot-logic) 22 | - [Specific version](#specific-version) 23 | - [Test version](#test-version) 24 | - [Manually](#manually) 25 | - [Upload files to board](#upload-files-to-board-1) 26 | - [Install additional MicroPython packages](#install-additional-micropython-packages) 27 | - [Usage](#usage) 28 | 29 | 30 | 31 | ## Installation 32 | 33 | ### Install required tools 34 | 35 | Python3 must be installed on your system. Check the current Python version 36 | with the following command 37 | 38 | ```bash 39 | python --version 40 | python3 --version 41 | ``` 42 | 43 | Depending on which command `Python 3.x.y` (with x.y as some numbers) is 44 | returned, use that command to proceed. 45 | 46 | ```bash 47 | python3 -m venv .venv 48 | source .venv/bin/activate 49 | 50 | pip install -r requirements.txt 51 | ``` 52 | 53 | Test both tools by showing their man/help info description. 54 | 55 | ```bash 56 | esptool.py --help 57 | rshell --help 58 | ``` 59 | 60 | ### Flash firmware 61 | 62 | To flash the [micropython firmware][ref-upy-firmware-download] as described on 63 | the micropython firmware download page, use the `esptool.py` to erase the 64 | flash before flashing the firmware. 65 | 66 | ```bash 67 | esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART erase_flash 68 | esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 460800 write_flash -z 0x1000 esp32-20210623-v1.16.bin 69 | ``` 70 | 71 | If the Micropython board is equipped with an external PSRAM chip, the 72 | `esp32spiram-20210623-v1.16.bin` can also be used for ESP32 devices. If there 73 | is no external PRSAM only the non SPIRAM version is working. 74 | 75 | ### Upload files to board 76 | 77 | #### Install package 78 | 79 | Connect your MicroPython board to a network 80 | 81 | ```python 82 | import network 83 | station = network.WLAN(network.STA_IF) 84 | station.active(True) 85 | station.connect('SSID', 'PASSWORD') 86 | station.isconnected() 87 | ``` 88 | 89 | #### General 90 | 91 | Install the latest package version of this lib on the MicroPython device 92 | 93 | ```python 94 | import mip 95 | mip.install("github:brainelectronics/micropython-esp-wifi-manager") 96 | 97 | # maybe install the dependencies manually afterwards 98 | # mip.install("github:brainelectronics/micropython-modules") 99 | ``` 100 | 101 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 102 | 103 | ```python 104 | import upip 105 | upip.install('micropython-esp-wifi-manager') 106 | # dependencies will be installed automatically 107 | ``` 108 | 109 | #### Hook the WiFi Manager boot logic 110 | 111 | The `boot.py` and `main.py` files of this package are installed into `/lib` of 112 | the MicroPython device by `mip`. They are fully functional and without any 113 | other dependencies or MicroPython port specific commands. Simply add the 114 | following line to the `boot.py` file of your device. *The following commands 115 | are not working if this package got installed by `upip`* 116 | 117 | ```python 118 | import wifi_manager.boot 119 | ``` 120 | 121 | And also add this line to your `main.py`, before your application code 122 | 123 | ```python 124 | import wifi_manager.main 125 | ``` 126 | 127 | #### Specific version 128 | 129 | Install a specific, fixed package version of this lib on the MicroPython device 130 | 131 | ```python 132 | import mip 133 | # install a verions of a specific branch 134 | mip.install("github:brainelectronics/micropython-esp-wifi-manager", version="feature/support-mip") 135 | # install a tag version 136 | mip.install("github:brainelectronics/micropython-esp-wifi-manager", version="1.7.0") 137 | ``` 138 | 139 | #### Test version 140 | 141 | Install a specific release candidate version uploaded to 142 | [Test Python Package Index](https://test.pypi.org/) on every PR on the 143 | MicroPython device. If no specific version is set, the latest stable version 144 | will be used. 145 | 146 | ```python 147 | import mip 148 | mip.install("github:brainelectronics/micropython-esp-wifi-manager", version="1.7.0-rc5.dev22") 149 | ``` 150 | 151 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 152 | 153 | ```python 154 | import upip 155 | # overwrite index_urls to only take artifacts from test.pypi.org 156 | upip.index_urls = ['https://test.pypi.org/pypi'] 157 | upip.install('micropython-esp-wifi-manager') 158 | ``` 159 | 160 | See also [brainelectronics Test PyPi Server in Docker][ref-brainelectronics-test-pypiserver] 161 | for a test PyPi server running on Docker. 162 | 163 | #### Manually 164 | 165 | ##### Upload files to board 166 | 167 | Copy the module(s) to the MicroPython board and import them as shown below 168 | using [Remote MicroPython shell][ref-remote-upy-shell] 169 | 170 | Open the remote shell with the following command. Additionally use `-b 115200` 171 | in case no CP210x is used but a CH34x. 172 | 173 | ```bash 174 | rshell -p /dev/tty.SLAB_USBtoUART --editor nano 175 | ``` 176 | 177 | Create compressed CSS and JS files as described in the 178 | [simulation static files README](simulation/static) to save disk space on the 179 | device and increase the performance (webpages are loading faster) 180 | 181 | ```bash 182 | mkdir /pyboard/lib/ 183 | mkdir /pyboard/lib/microdot/ 184 | mkdir /pyboard/lib/utemplate/ 185 | mkdir /pyboard/lib/wifi_manager/ 186 | mkdir /pyboard/lib/static/ 187 | mkdir /pyboard/lib/static/css 188 | mkdir /pyboard/lib/static/js 189 | 190 | cp static/css/*.gz /pyboard/lib/static/css 191 | cp static/js/*.gz /pyboard/lib/static/js 192 | # around 24kB compared to uncompressed 120kB 193 | 194 | # optional, not used so far 195 | # mkdir /pyboard/lib/static/js 196 | # cp static/js/*.gz /pyboard/lib/static/js 197 | # around 12kB compared to uncompressed 40kB 198 | 199 | mkdir /pyboard/lib/templates/ 200 | cp templates/* /pyboard/lib/templates/ 201 | # around 20kB 202 | 203 | cp wifi_manager/* /pyboard/lib/wifi_manager/ 204 | cp microdot/* /pyboard/lib/microdot/ 205 | cp utemplate/* /pyboard/lib/utemplate/ 206 | cp main.py /pyboard 207 | cp boot.py /pyboard 208 | # around 40kB 209 | ``` 210 | 211 | ##### Install additional MicroPython packages 212 | 213 | As this package has not been installed with `upip` additional modules are 214 | required, which are not part of this repo. 215 | 216 | Connect the board to a network and install the package like this for 217 | MicroPython 1.20.0 or never 218 | 219 | ```python 220 | import mip 221 | mip.install("github:brainelectronics/micropython-modules") 222 | ``` 223 | 224 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 225 | 226 | ```python 227 | import upip 228 | upip.install('micropython-brainelectronics-helper') 229 | ``` 230 | 231 | ## Usage 232 | 233 | After all files have been transfered or installed open a REPL to the device. 234 | 235 | The device will try to load and connect to the configured networks based on an 236 | encrypted JSON file. 237 | 238 | In case no network has been configured or no connection could be established 239 | to any of the configured networks within the timeout of each 5 seconds an 240 | AccessPoint at `192.168.4.1` is created. 241 | 242 | A simple Picoweb webserver is hosting the webpages to connect to new networks, 243 | to remove already configured networks from the list of connections to 244 | establish and to get the latest available networks as JSON. 245 | 246 | This is a list of available webpages 247 | 248 | | URL | Description | 249 | |-----|-------------| 250 | | `/` | Root index page, to choose from the available pages | 251 | | `/select` | Select and configure a network | 252 | | `/configure` | Manage already configured networks | 253 | | `/scan_result` | JSON of available networks | 254 | | `/shutdown` | Shutdown webserver and return from `run` function | 255 | 256 | To leave from the Webinterface, just press CTRL+C and wait until all threads 257 | finish running. This takes around 1 second. The device will return to its REPL 258 | 259 | 260 | [ref-esptool]: https://github.com/espressif/esptool 261 | [ref-remote-upy-shell]: https://github.com/dhylands/rshell 262 | [ref-brainelectronics-test-pypiserver]: https://github.com/brainelectronics/test-pypiserver 263 | [ref-upy-firmware-download]: https://micropython.org/download/ 264 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | boot script, do initial stuff here, similar to the setup() function on Arduino 6 | """ 7 | 8 | import gc 9 | import network 10 | from time import sleep 11 | 12 | 13 | station = network.WLAN(network.STA_IF) 14 | if station.active() and station.isconnected(): 15 | station.disconnect() 16 | sleep(1) 17 | station.active(False) 18 | sleep(1) 19 | station.active(True) 20 | 21 | # run garbage collector at the end to clean up 22 | gc.collect() 23 | 24 | print('Finished booting steps of MicroPython WiFiManager') 25 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | main script, do your stuff here, similar to the loop() function on Arduino 6 | """ 7 | 8 | from wifi_manager import WiFiManager 9 | 10 | wm = WiFiManager() 11 | 12 | # if there is enough RAM on the board, may increase the buffer size on send 13 | # stream operations to read bigger chunks from disk, default is 128 14 | # wm.app.SEND_BUFSZ = 1024 15 | 16 | result = wm.load_and_connect() 17 | print('Connection result: {}'.format(result)) 18 | 19 | if result is False: 20 | print('Starting config server') 21 | wm.start_config() 22 | else: 23 | print('Successfully connected to a network :)') 24 | 25 | print('Finished booting steps of MicroPython WiFiManager') 26 | -------------------------------------------------------------------------------- /microdot/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Miguel Grinberg 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 | -------------------------------------------------------------------------------- /microdot/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .microdot import * 5 | from .microdot_utemplate import * 6 | -------------------------------------------------------------------------------- /microdot/microdot_utemplate.py: -------------------------------------------------------------------------------- 1 | from utemplate import recompile 2 | 3 | _loader = None 4 | 5 | 6 | def init_templates(template_dir='templates', loader_class=recompile.Loader): 7 | """Initialize the templating subsystem. 8 | 9 | :param template_dir: the directory where templates are stored. This 10 | argument is optional. The default is to load templates 11 | from a *templates* subdirectory. 12 | :param loader_class: the ``utemplate.Loader`` class to use when loading 13 | templates. This argument is optional. The default is 14 | the ``recompile.Loader`` class, which automatically 15 | recompiles templates when they change. 16 | """ 17 | global _loader 18 | _loader = loader_class(None, template_dir) 19 | 20 | 21 | def render_template(template, *args, **kwargs): 22 | """Render a template. 23 | 24 | :param template: The filename of the template to render, relative to the 25 | configured template directory. 26 | :param args: Positional arguments to be passed to the render engine. 27 | :param kwargs: Keyword arguments to be passed to the render engine. 28 | 29 | The return value is an iterator that returns sections of rendered template. 30 | """ 31 | if _loader is None: # pragma: no cover 32 | init_templates() 33 | render = _loader.load(template) 34 | return render(*args, **kwargs) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | [ 4 | "wifi_manager/__init__.py", 5 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/wifi_manager/__init__.py" 6 | ], 7 | [ 8 | "wifi_manager/boot.py", 9 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/boot.py" 10 | ], 11 | [ 12 | "wifi_manager/main.py", 13 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/main.py" 14 | ], 15 | [ 16 | "wifi_manager/version.py", 17 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/wifi_manager/version.py" 18 | ], 19 | [ 20 | "wifi_manager/wifi_manager.py", 21 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/wifi_manager/wifi_manager.py" 22 | ], 23 | [ 24 | "microdot/__init__.py", 25 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/microdot/__init__.py" 26 | ], 27 | [ 28 | "microdot/microdot.py", 29 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/microdot/microdot.py" 30 | ], 31 | [ 32 | "microdot/microdot_asyncio.py", 33 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/microdot/microdot_asyncio.py" 34 | ], 35 | [ 36 | "microdot/microdot_utemplate.py", 37 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/microdot/microdot_utemplate.py" 38 | ], 39 | [ 40 | "utemplate/compiled.py", 41 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/utemplate/compiled.py" 42 | ], 43 | [ 44 | "utemplate/recompile.py", 45 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/utemplate/recompile.py" 46 | ], 47 | [ 48 | "utemplate/source.py", 49 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/utemplate/source.py" 50 | ], 51 | [ 52 | "pkg_resources/pkg_resources.py", 53 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/pkg_resources/pkg_resources.py" 54 | ], 55 | [ 56 | "static/css/bootstrap.min.css", 57 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/static/css/bootstrap.min.css" 58 | ], 59 | [ 60 | "static/css/bootstrap.min.css.gz", 61 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/static/css/bootstrap.min.css.gz" 62 | ], 63 | [ 64 | "static/favicon.ico", 65 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/static/favicon.ico" 66 | ], 67 | [ 68 | "static/js/toast.js", 69 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/static/js/toast.js" 70 | ], 71 | [ 72 | "static/js/toast.js.gz", 73 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/static/js/toast.js.gz" 74 | ], 75 | [ 76 | "templates/index.tpl", 77 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/templates/index.tpl" 78 | ], 79 | [ 80 | "templates/remove.tpl", 81 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/templates/remove.tpl" 82 | ], 83 | [ 84 | "templates/select.tpl", 85 | "github:brainelectronics/Micropython-ESP-WiFi-Manager/templates/select.tpl" 86 | ] 87 | ], 88 | "deps": [ 89 | ["github:brainelectronics/micropython-modules", "develop"], 90 | ["pkg_resources", "0.2.1"] 91 | ], 92 | "version": "1.11.0" 93 | } -------------------------------------------------------------------------------- /pkg_resources/pkg_resources.py: -------------------------------------------------------------------------------- 1 | import uio 2 | 3 | c = {} 4 | 5 | 6 | def resource_stream(package, resource): 7 | if package not in c: 8 | try: 9 | if package: 10 | p = __import__(package + ".R", None, None, True) 11 | else: 12 | p = __import__("R") 13 | c[package] = p.R 14 | except ImportError: 15 | if package: 16 | p = __import__(package) 17 | d = p.__path__ 18 | else: 19 | d = "." 20 | # if d[0] != "/": 21 | # import uos 22 | # d = uos.getcwd() + "/" + d 23 | c[package] = d + "/" 24 | 25 | p = c[package] 26 | if isinstance(p, dict): 27 | return uio.BytesIO(p[resource]) 28 | return open(p + resource, "rb") 29 | -------------------------------------------------------------------------------- /requirements-deploy.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | # # to upload package to PyPi or other package hosts 4 | twine>=4.0.1,<5 5 | changelog2version>=0.5.0,<1 -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | flake8>=5.0.0,<6 4 | coverage>=6.4.2,<7 5 | nose2>=0.12.0,<1 6 | setup2upypackage>=0.4.0,<1 7 | -r simulation/requirements.txt 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | esptool 2 | rshell>=0.0.30,<1.0.0 -------------------------------------------------------------------------------- /sdist_upip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # This module is part of Pycopy https://github.com/pfalcon/pycopy 4 | # and pycopy-lib https://github.com/pfalcon/pycopy-lib, projects to 5 | # create a (very) lightweight full-stack Python distribution. 6 | # 7 | # Copyright (c) 2016-2019 Paul Sokolovsky 8 | # Licence: MIT 9 | # 10 | # This module overrides distutils (also compatible with setuptools) "sdist" 11 | # command to perform pre- and post-processing as required for MicroPython's 12 | # upip package manager. 13 | # 14 | # Preprocessing steps: 15 | # * Creation of Python resource module (R.py) from each top-level package's 16 | # resources. 17 | # Postprocessing steps: 18 | # * Removing metadata files not used by upip (this includes setup.py) 19 | # * Recompressing gzip archive with 4K dictionary size so it can be 20 | # installed even on low-heap targets. 21 | # 22 | import sys 23 | import os 24 | import zlib 25 | import tarfile 26 | import re 27 | import io 28 | 29 | from distutils.filelist import FileList 30 | from setuptools.command.sdist import sdist as _sdist 31 | 32 | 33 | FILTERS = [ 34 | # include, exclude, repeat 35 | (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), 36 | (r".+\.py$", r"[^/]+$"), 37 | (None, r".+\.egg-info/.+"), 38 | ] 39 | outbuf = io.BytesIO() 40 | 41 | 42 | def gzip_4k(inf, fname): 43 | comp = zlib.compressobj(level=9, wbits=16 + 12) 44 | with open(fname + ".out", "wb") as outf: 45 | while 1: 46 | data = inf.read(1024) 47 | if not data: 48 | break 49 | outf.write(comp.compress(data)) 50 | outf.write(comp.flush()) 51 | os.rename(fname, fname + ".orig") 52 | os.rename(fname + ".out", fname) 53 | 54 | 55 | def filter_tar(name): 56 | fin = tarfile.open(name, "r:gz") 57 | fout = tarfile.open(fileobj=outbuf, mode="w") 58 | for info in fin: 59 | # print(info) 60 | if not "/" in info.name: 61 | continue 62 | fname = info.name.split("/", 1)[1] 63 | include = None 64 | 65 | for inc_re, exc_re in FILTERS: 66 | if include is None and inc_re: 67 | if re.match(inc_re, fname): 68 | include = True 69 | 70 | if include is None and exc_re: 71 | if re.match(exc_re, fname): 72 | include = False 73 | 74 | if include is None: 75 | include = True 76 | 77 | if include: 78 | print("including:", fname) 79 | else: 80 | print("excluding:", fname) 81 | continue 82 | 83 | farch = fin.extractfile(info) 84 | fout.addfile(info, farch) 85 | fout.close() 86 | fin.close() 87 | 88 | 89 | def make_resource_module(manifest_files): 90 | resources = [] 91 | # Any non-python file included in manifest is resource 92 | for fname in manifest_files: 93 | ext = fname.rsplit(".", 1)[1] 94 | if ext != "py": 95 | resources.append(fname) 96 | 97 | if resources: 98 | print("creating resource module R.py") 99 | resources.sort() 100 | last_pkg = None 101 | r_file = None 102 | for fname in resources: 103 | try: 104 | pkg, res_name = fname.split("/", 1) 105 | except ValueError: 106 | print("not treating %s as a resource" % fname) 107 | continue 108 | if last_pkg != pkg: 109 | last_pkg = pkg 110 | if r_file: 111 | r_file.write("}\n") 112 | r_file.close() 113 | r_file = open(pkg + "/R.py", "w") 114 | r_file.write("R = {\n") 115 | 116 | with open(fname, "rb") as f: 117 | r_file.write("%r: %r,\n" % (res_name, f.read())) 118 | 119 | if r_file: 120 | r_file.write("}\n") 121 | r_file.close() 122 | 123 | 124 | class sdist(_sdist): 125 | 126 | def run(self): 127 | self.filelist = FileList() 128 | self.get_file_list() 129 | make_resource_module(self.filelist.files) 130 | 131 | r = super().run() 132 | 133 | assert len(self.archive_files) == 1 134 | print("filtering files and recompressing with 4K dictionary") 135 | filter_tar(self.archive_files[0]) 136 | outbuf.seek(0) 137 | gzip_4k(outbuf, self.archive_files[0]) 138 | 139 | return r 140 | 141 | 142 | # For testing only 143 | if __name__ == "__main__": 144 | filter_tar(sys.argv[1]) 145 | outbuf.seek(0) 146 | gzip_4k(outbuf, sys.argv[1]) 147 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from setuptools import setup 5 | import pathlib 6 | import sdist_upip 7 | 8 | here = pathlib.Path(__file__).parent.resolve() 9 | 10 | # Get the long description from the README file 11 | long_description = (here / 'README.md').read_text(encoding='utf-8') 12 | 13 | # load elements of version.py 14 | exec(open(here / 'wifi_manager' / 'version.py').read()) 15 | 16 | setup( 17 | name='micropython-esp-wifi-manager', 18 | version=__version__, 19 | description="MicroPython WiFi Manager to configure and connect to networks", 20 | long_description=long_description, 21 | long_description_content_type='text/markdown', 22 | url='https://github.com/brainelectronics/Micropython-ESP-WiFi-Manager', 23 | author='brainelectronics', 24 | author_email='info@brainelectronics.de', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python :: Implementation :: MicroPython', 30 | ], 31 | keywords='micropython, brainelectronics, wifi, wifimanager, library', 32 | project_urls={ 33 | 'Bug Reports': 'https://github.com/brainelectronics/Micropython-ESP-WiFi-Manager/issues', 34 | 'Source': 'https://github.com/brainelectronics/Micropython-ESP-WiFi-Manager', 35 | }, 36 | license='MIT', 37 | cmdclass={'sdist': sdist_upip.sdist}, 38 | packages=[ 39 | 'wifi_manager', 40 | 'microdot', 41 | 'utemplate', 42 | 'pkg_resources', 43 | ], 44 | # Although 'package_data' is the preferred approach, in some case you may 45 | # need to place data files outside of your packages. See: 46 | # http://docs.python.org/distutils/setupscript.html#installing-additional-files 47 | # 48 | # In this case, 'data_file' will be installed into '/my_data' 49 | # data_files=[('my_data', ['data/data_file'])], 50 | data_files=[ 51 | ( 52 | 'static', 53 | [ 54 | 'static/css/bootstrap.min.css', 55 | 'static/css/bootstrap.min.css.gz', 56 | 'static/favicon.ico', 57 | 'static/js/toast.js', 58 | 'static/js/toast.js.gz', 59 | ] 60 | ), 61 | ( 62 | 'templates', 63 | [ 64 | 'templates/index.tpl', 65 | 'templates/remove.tpl', 66 | 'templates/select.tpl', 67 | ] 68 | ) 69 | ], 70 | install_requires=[ 71 | 'micropython-brainelectronics-helpers', 72 | ] 73 | ) 74 | -------------------------------------------------------------------------------- /simulation/.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for python coverage package 2 | [run] 3 | branch = True 4 | omit = 5 | */tests/*, 6 | .venv/*, 7 | .idea/*, 8 | setup*, 9 | .eggs/* 10 | .tox/*, 11 | build/*, 12 | dist/*, 13 | version.py 14 | 15 | [report] 16 | include = src/* 17 | # Regexes for lines to exclude from consideration 18 | 19 | ignore_errors = True 20 | 21 | [html] 22 | directory = reports/coverage/html 23 | skip_empty = True 24 | 25 | [xml] 26 | output = reports/coverage/coverage.xml 27 | 28 | [json] 29 | output = reports/coverage/coverage.json 30 | pretty_print = True 31 | show_contexts = True -------------------------------------------------------------------------------- /simulation/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Empty -------------------------------------------------------------------------------- /simulation/README.md: -------------------------------------------------------------------------------- 1 | # ESP WiFi Manager Simulation 2 | 3 | Simple Flask Server to create ESP WiFi Manager webpages 4 | 5 | 6 | ----------------------- 7 | 8 | 9 | ## Setup 10 | 11 | Install all required packages with the following command in a virtual 12 | environment to avoid any conflicts with other packages installed on your local 13 | system. 14 | 15 | ```bash 16 | python3 -m venv .venv 17 | source .venv/bin/activate 18 | 19 | pip install -r requirements.txt 20 | ``` 21 | 22 | ### Windows 23 | 24 | On Windows the package `pycryptodome>=3.14.0,<4` shall be used instead of 25 | `pycrypto>=2.6.1,<3` 26 | 27 | ## Bootstrap 28 | 29 | Simulation webpages use [bootstrap 3.4][ref-bootstrap-34]. 30 | 31 | ## Usage 32 | 33 | Run the simulation of the ESP WiFi Manager **after** activating the virtual 34 | environment of the [Setup section](#setup) 35 | 36 | ```bash 37 | sh run.sh 38 | ``` 39 | 40 | Open [`http://127.0.0.1:5000/`](http://127.0.0.1:5000/){:target="_blank"} in a browser 41 | 42 | ## Unittests 43 | 44 | ### General 45 | 46 | The unittests are covering a wide range of the simulation interface and use 47 | the `nose2` package to do so. 48 | 49 | ### Usage 50 | 51 | Run unittests for all or a single class or even for a single function as shown 52 | onwards 53 | 54 | ### Prepare 55 | 56 | To create the necessary coverage report directories call the 57 | [`prepare_test.sh`](prepare_test.sh) script 58 | 59 | ```bash 60 | cd ./ 61 | 62 | # prepare coverage directories 63 | sh prepare_test.sh 64 | 65 | # run all unittests 66 | nose2 --config tests/unittest.cfg -v tests 67 | 68 | # run all unittests of the class WiFiManager 69 | nose2 --config tests/unittest.cfg -v tests.test_wifi_manager 70 | 71 | # run the unittest of the "load_and_connect" function of WiFiManager class 72 | nose2 --config tests/unittest.cfg -v tests.test_wifi_manager.TestWiFiManager.test_load_and_connect 73 | ``` 74 | 75 | ### Available tests 76 | 77 | #### Absolute truth 78 | 79 | Test absolute truth of the unittesting framework itself. 80 | 81 | ```bash 82 | nose2 --config tests/unittest.cfg -v tests.test_absolute_truth.TestAbsoluteTruth 83 | ``` 84 | 85 | #### Generic Helper 86 | 87 | Test [`generic helper`][ref-generic-helper-test] implementation 88 | 89 | ```bash 90 | nose2 --config tests/unittest.cfg -v tests.test_generic_helper.TestGenericHelper 91 | ``` 92 | 93 | #### LED Helper 94 | 95 | Test [`led helper`][ref-led-helper-test] implementation. Currently only 96 | the used Neopixel part of it. 97 | 98 | ```bash 99 | nose2 --config tests/unittest.cfg -v tests.test_led_helper.TestNeopixel 100 | ``` 101 | 102 | #### Fakes 103 | 104 | Unittests of all Micropython fake modules. 105 | 106 | ##### Machine 107 | 108 | Test [`fake machine`][ref-machine-test] implementations. 109 | 110 | ###### Machine 111 | 112 | ```bash 113 | nose2 --config tests/unittest.cfg -v tests.test_machine.TestMachine 114 | ``` 115 | 116 | ###### Pin 117 | 118 | ```bash 119 | nose2 --config tests/unittest.cfg -v tests.test_pin.TestPin 120 | ``` 121 | 122 | ###### RTC 123 | 124 | ```bash 125 | nose2 --config tests/unittest.cfg -v tests.test_rtc.TestRTC 126 | ``` 127 | 128 | ##### Neopixel 129 | 130 | Test [`fake neopixel`][ref-neopixel-test] implementations. 131 | 132 | ```bash 133 | nose2 --config tests/unittest.cfg -v tests.test_neopixel.TestNeoPixel 134 | ``` 135 | 136 | ##### Network 137 | 138 | Test [`fake network`][ref-network-test] implementations. 139 | 140 | ```bash 141 | nose2 --config tests/unittest.cfg -v tests.test_network.TestNetworkHelper 142 | ``` 143 | 144 | ##### Primitives 145 | 146 | Test [`message`][ref-message-test] implementations. 147 | 148 | ```bash 149 | nose2 --config tests/unittest.cfg -v tests.test_message.TestMessage 150 | ``` 151 | 152 | #### Path Helper 153 | 154 | Test [`path helper`][ref-path-helper-test] implementation. 155 | 156 | ```bash 157 | nose2 --config tests/unittest.cfg -v tests.test_path_helper.TestPathHelper 158 | ``` 159 | 160 | #### Time Helper 161 | 162 | Test [`time helper`][ref-time-helper-test] implementation. 163 | 164 | ```bash 165 | nose2 --config tests/unittest.cfg -v tests.test_time_helper.TestTimeHelper 166 | ``` 167 | 168 | #### WiFi Helper 169 | 170 | Test [`wifi helper`][ref-wifi-helper-test] implementation. 171 | 172 | ```bash 173 | nose2 --config tests/unittest.cfg -v tests.test_wifi_helper.TestWifiHelper 174 | ``` 175 | 176 | #### WiFi Manager 177 | 178 | Test [`wifi manager`][ref-wifi-manager-test] implementation. 179 | 180 | ```bash 181 | nose2 --config tests/unittest.cfg -v tests.test_wifi_manager.TestWiFiManager 182 | ``` 183 | 184 | 185 | 186 | [ref-bootstrap-34]: https://getbootstrap.com/docs/3.4/getting-started/#download 187 | 188 | 189 | [ref-generic-helper-test]: src/generic_helper/generic_helper.py 190 | [ref-led-helper-test]: src/led_helper/led_helper.py 191 | 192 | 193 | [ref-machine-test]: src/machine 194 | [ref-neopixel-test]: src/led_helper/neopixel.py 195 | [ref-network-test]: src/wifi_helper/network.py 196 | 197 | 198 | [ref-message-test]: src/generic_helper/message.py 199 | 200 | 201 | [ref-path-helper-test]: src/path_helper/path_helper.py 202 | [ref-time-helper-test]: src/time_helper/time_helper.py 203 | [ref-wifi-helper-test]: src/wifi_helper/wifi_helper.py 204 | [ref-wifi-manager-test]: src/wifi_manager/wifi_manager.py 205 | -------------------------------------------------------------------------------- /simulation/create_report_dirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Create test report directories.""" 3 | from pathlib import Path 4 | import os 5 | import shutil 6 | 7 | if os.path.exists('reports'): 8 | shutil.rmtree('reports', ignore_errors=True) 9 | 10 | Path('reports/sca').mkdir(parents=True, exist_ok=True) 11 | Path('reports/test_results').mkdir(parents=True, exist_ok=True) 12 | Path('reports/coverage').mkdir(parents=True, exist_ok=True) 13 | -------------------------------------------------------------------------------- /simulation/prepare_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # title :prepare_test.sh 3 | # description :Prepare directories for unittest report files 4 | # author :brainelectronics 5 | # date :20220206 6 | # version :0.1.0 7 | # usage :sh prepare_test.sh 8 | # notes :None 9 | # bash_version :3.2.53(1)-release 10 | #============================================================================= 11 | 12 | mkdir -p reports/sca 13 | mkdir -p reports/test_results 14 | mkdir -p reports/coverage -------------------------------------------------------------------------------- /simulation/requirements.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | flask>=2.0.2,<3 4 | jinja2>=3.0.2,<4 5 | PyYAML>=5.4.1,<6 6 | netifaces>=0.11.0,<1 7 | pycrypto>=2.6.1,<3 8 | microdot>=1.2.4,<2 9 | -------------------------------------------------------------------------------- /simulation/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # title :run.sh 3 | # description :Start Flask server 4 | # author :brainelectronics 5 | # date :20220202 6 | # version :0.1.0 7 | # usage :sh run.sh 8 | # notes :None 9 | # bash_version :3.2.53(1)-release 10 | #============================================================================= 11 | 12 | cd src 13 | python run_simulation.py 14 | -------------------------------------------------------------------------------- /simulation/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html 3 | 4 | # This includes the license file(s) in the wheel. 5 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 6 | license_files = LICENSE.txt 7 | 8 | 9 | # This is the name of your project. The first time you publish this 10 | # package, this name will be registered for you. It will determine how 11 | # users can install this project, e.g.: 12 | # 13 | # $ pip install PACKAGE-NAME 14 | # 15 | # There are some restrictions on what makes a valid project name 16 | # specification here: 17 | # https://packaging.python.org/specifications/core-metadata/#name 18 | name = wifi-mananger-simulation 19 | 20 | # For a discussion on single-sourcing the version across setup.py and 21 | # the project code, see 22 | # https://packaging.python.org/en/latest/single_source_version.html 23 | # Version will be automatically calculated by setuptools_scm 24 | # version = 25 | 26 | # This is a one-line description or tagline of what your project does. 27 | # This corresponds to the "Summary" metadata field: 28 | # https://packaging.python.org/specifications/core-metadata/#summary 29 | description = WiFi Manager Simulation 30 | 31 | # This is an optional longer description of your project that represents 32 | # the body of text which users will see when they visit PyPI. 33 | # 34 | # Often, this is the same as your README, so you can just read it in 35 | # from that file directly (as we have already done above) 36 | # 37 | # This field corresponds to the "Description" metadata field: 38 | # https://packaging.python.org/specifications/core-metadata/#description-optional 39 | long_description = file: README.md 40 | 41 | # Denotes that our long_description is in Markdown; valid values are 42 | # text/plain, text/x-rst, and text/markdown 43 | # 44 | # Optional if long_description is written in reStructuredText (rst) but 45 | # required for plain-text or Markdown; if unspecified, "applications 46 | # should attempt to render [the long_description] as 47 | # text/x-rst; charset=UTF-8 and fall back to text/plain if it is not 48 | # valid rst" (see link below) 49 | # 50 | # This field corresponds to the "Description-Content-Type" metadata field: 51 | # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional 52 | long_description_content_type = text/markdown 53 | 54 | # This should be a valid link to your project's main homepage. 55 | # 56 | # This field corresponds to the "Home-Page" metadata field: 57 | # https://packaging.python.org/specifications/core-metadata/#home-page-optional 58 | url = https://github.com/brainelectronics 59 | 60 | # List additional URLs that are relevant to your project as a dict. 61 | # 62 | # This field corresponds to the "Project-URL" metadata fields: 63 | # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use 64 | # 65 | # Examples listed include a pattern for specifying where the package 66 | # tracks issues, where the source is hosted, where to say thanks to the 67 | # package maintainers, and where to support the project financially. 68 | # The key is what's used to render the link text on PyPI. 69 | project_urls = 70 | API Documentation = https://github.com/brainelectronics 71 | 72 | # This should be your name or the name of the organization which owns 73 | # the project. 74 | author = Jonas Scharpf (brainelectronics) 75 | maintainer = Jonas Scharpf (brainelectronics) 76 | 77 | # This should be a valid email address corresponding to the author 78 | # listed above. 79 | author_email = 'info@brainelectronics.de' 80 | maintainer_email = 'info@brainelectronics.de' 81 | 82 | # Classifiers help users find your project by categorizing it. 83 | # 84 | # For a list of valid classifiers, see https://pypi.org/classifiers/ 85 | classifiers = 86 | # How mature is this project? Common values are 87 | # 3 - Alpha 88 | # 4 - Beta 89 | # 5 - Production/Stable 90 | Development Status :: 3 - Alpha 91 | 92 | # Indicate who your project is intended for 93 | Intended Audience :: Developers 94 | Topic :: Software Development :: Build Tools 95 | 96 | # Pick your license as you wish 97 | # License :: OSI Approved :: MIT License, 98 | 99 | Operating System :: OS Independent 100 | 101 | # Specify the Python versions you support here. In particular, ensure 102 | # that you indicate you support Python 3. These classifiers are *not* 103 | # checked by 'pip install'. See instead 'python_requires' below. 104 | 'Programming Language :: Python :: 3', 105 | 'Programming Language :: Python :: 3.8', 106 | 'Programming Language :: Python :: 3.9', 107 | 'Programming Language :: Python :: 3 :: Only', 108 | 109 | # This field adds keywords for your project which will appear on the 110 | # project page. What does your project relate to? 111 | # 112 | # Note that this is a list of additional keywords, separated 113 | # by commas, to be used to assist searching for the distribution in a 114 | # larger catalog. 115 | keywords = 'wifi, manager, simulation, esp32' 116 | 117 | [options] 118 | # When your source code is in a subdirectory under the project root, e.g. 119 | # `src/`, it is necessary to specify the `package_dir` argument. 120 | package_dir = 121 | = src 122 | 123 | # You can just specify package directories manually here if your project is 124 | # simple. Or you can use find_packages(). 125 | # 126 | # Alternatively, if you just want to distribute a single Python file, use 127 | # the `py_modules` argument instead as follows, which will expect a file 128 | # called `my_module.py` to exist: 129 | # 130 | # py_modules=["my_module"], 131 | # 132 | packages = find: 133 | 134 | # Specify which Python versions you support. In contrast to the 135 | # 'Programming Language' classifiers above, 'pip install' will check this 136 | # and refuse to install the project if the version does not match. See 137 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires 138 | python_requires = >=3.8, <4 139 | 140 | setup_requires = setuptools_scm 141 | 142 | 143 | # This field lists other packages that your project depends on to run. 144 | # Any package you put here will be installed by pip when your project is 145 | # installed, so they must be valid existing projects. 146 | # 147 | # For an analysis of "install_requires" vs pip's requirements files see: 148 | # https://packaging.python.org/en/latest/requirements.html 149 | # install_requires = 150 | # GitPython >= 3.1.14,<4 151 | 152 | # List additional groups of dependencies here (e.g. development 153 | # dependencies). Users will be able to install these using the "extras" 154 | # syntax, for example: 155 | # 156 | # $ pip install sampleproject[dev] 157 | # 158 | # Similar to `install_requires` above, these must be valid existing 159 | # projects. 160 | [options.extras_require] 161 | dev = 162 | flake8>=3.9.0,<4 163 | flake8-docstrings>=1.6.0,<2 164 | nose2>=0.9.2,<1 165 | tox>=3.23.0,<4 166 | doc = 167 | sphinx >= 3.5.2,<4 168 | sphinx_rtd_theme 169 | m2rr >= 0.2.3,<1 170 | setuptools_scm 171 | 172 | [options.packages.find] 173 | where = src 174 | 175 | # [options.package_data] 176 | # If there are data files included in your packages that need to be 177 | # installed, specify them here. 178 | # * = *.txt, *.rst 179 | # hello = *.msg 180 | 181 | [options.data_files] 182 | # Although 'package_data' is the preferred approach, in some case you may 183 | # need to place data files outside of your packages. See: 184 | # http://docs.python.org/distutils/setupscript.html#installing-additional-files 185 | # 186 | # In this case, 'data_file' will be installed into '/my_data' 187 | # /etc/my_package = 188 | # site.d/00_default.conf 189 | # host.d/00_default.conf 190 | # data = data/img/logo.png, data/svg/icon.svg 191 | 192 | [options.entry_points] 193 | # To provide executable scripts, use entry points in preference to the 194 | # "scripts" keyword. Entry points provide cross-platform support and allow 195 | # `pip` to create the appropriate form of executable for the target 196 | # platform. 197 | # 198 | # For example, the following would provide a command called `sample` which 199 | # executes the function `main` from this package when invoked: 200 | # console_scripts = 201 | # command = folder-inside-options-packages-find.file_name:function 202 | # test by installing and calling a valid command, e.g. --help 203 | # $ python setup.py develop 204 | # $ command --help 205 | -------------------------------------------------------------------------------- /simulation/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | A setuptools based setup module. 5 | 6 | See: 7 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 8 | https://github.com/pypa/sampleproject 9 | """ 10 | 11 | # Always prefer setuptools over distutils 12 | from setuptools import setup 13 | import pathlib 14 | import shutil 15 | 16 | # Arguments marked as "Required" below must be included for upload to PyPI. 17 | # Fields marked as "Optional" may be commented out. 18 | 19 | # Clean up previous builds 20 | if pathlib.Path('build').is_dir(): 21 | shutil.rmtree('build', ignore_errors=True) 22 | if pathlib.Path('dist').is_dir(): 23 | shutil.rmtree('dist', ignore_errors=True) 24 | 25 | setup( 26 | # Versions should comply with PEP 440: 27 | # https://www.python.org/dev/peps/pep-0440/ 28 | # 29 | # For a discussion on single-sourcing the version across setup.py and the 30 | # project code, see 31 | # https://packaging.python.org/en/latest/single_source_version.html 32 | # version='2.0.0', # Required 33 | use_scm_version=True, 34 | ) 35 | -------------------------------------------------------------------------------- /simulation/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/simulation/src/__init__.py -------------------------------------------------------------------------------- /simulation/src/generic_helper/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .generic_helper import GenericHelper 5 | from .message import Message 6 | -------------------------------------------------------------------------------- /simulation/src/generic_helper/generic_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Generic helper 6 | 7 | Collection of helper functions used in other modules 8 | """ 9 | 10 | import gc 11 | import json 12 | import logging as logging 13 | from machine import machine 14 | import os 15 | import random 16 | import sys 17 | import time 18 | import binascii as ubinascii 19 | 20 | from typing import Optional, Union 21 | 22 | 23 | class GenericHelper(object): 24 | """docstring for GenericHelper""" 25 | def __init__(self): 26 | pass # pragma: no cover 27 | 28 | @staticmethod 29 | def create_logger(logger_name: Optional[str] = None) -> logging.Logger: 30 | """ 31 | Create a logger. 32 | 33 | :param logger_name: The logger name 34 | :type logger_name: str, optional 35 | 36 | :returns: Configured logger 37 | :rtype: logging.Logger 38 | """ 39 | custom_format = '[%(asctime)s] [%(levelname)-8s] [%(filename)-15s @'\ 40 | ' %(funcName)-15s:%(lineno)4s] %(message)s' 41 | 42 | # configure logging 43 | logging.basicConfig(level=logging.INFO, 44 | format=custom_format, 45 | stream=sys.stdout) 46 | 47 | if logger_name and (isinstance(logger_name, str)): 48 | logger = logging.getLogger(logger_name) 49 | else: 50 | logger = logging.getLogger(__name__) 51 | 52 | # set the logger level to DEBUG if specified differently 53 | logger.setLevel(logging.DEBUG) 54 | 55 | return logger 56 | 57 | @staticmethod 58 | def set_level(logger: logging.Logger, level: str) -> None: 59 | """ 60 | Set the level of a logger. 61 | 62 | :param logger: The logger to set the level 63 | :type logger: logging.Logger 64 | :param level: The new level 65 | :type level: str 66 | """ 67 | if level.lower() == 'debug': 68 | logger.setLevel(level=logging.DEBUG) 69 | elif level.lower() == 'info': 70 | logger.setLevel(level=logging.INFO) 71 | elif level.lower() == 'warning': 72 | logger.setLevel(level=logging.WARNING) 73 | elif level.lower() == 'error': 74 | logger.setLevel(level=logging.ERROR) 75 | elif level.lower() == 'critical': 76 | logger.setLevel(level=logging.CRITICAL) 77 | else: 78 | pass 79 | 80 | @staticmethod 81 | def set_logger_verbose_level(logger: logging.Logger, 82 | verbose_level: int, 83 | debug_output: bool): 84 | """ 85 | Set the logger verbose level and debug output 86 | 87 | :param logger: The logger to apply the settings to 88 | :type logger: logging.Logger 89 | :param verbose_level: The verbose level 90 | :type verbose_level: int 91 | :param debug_output: The debug mode 92 | :type debug_output: bool 93 | """ 94 | LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 95 | LOG_LEVELS = LOG_LEVELS[::-1] 96 | 97 | if verbose_level is None: 98 | if not debug_output: 99 | # disable the logger 100 | logger.disabled = True 101 | else: 102 | log_level = min(len(LOG_LEVELS) - 1, max(verbose_level, 0)) 103 | log_level_name = LOG_LEVELS[log_level] 104 | 105 | # set the level of the logger 106 | logger.setLevel(log_level_name) 107 | 108 | @staticmethod 109 | def get_random_value(lower: int = 0, upper: int = 255) -> int: 110 | """ 111 | Get a random value within the given range 112 | 113 | :param lower: The lower boundary 114 | :type lower: int, optional 115 | :param upper: The upper boundary, inclusive, default 255 116 | :type upper: int, optional 117 | 118 | :returns: The random value. 119 | :rtype: int 120 | """ 121 | return random.randint(lower, upper) 122 | 123 | @staticmethod 124 | def get_uuid(length: Optional[int] = None) -> bytes: 125 | """ 126 | Get the UUID of the device. 127 | 128 | :param length: The length of the UUID 129 | :type length: int, optional 130 | 131 | :returns: The uuid. 132 | :rtype: bytes 133 | """ 134 | uuid = ubinascii.hexlify(machine.unique_id()) 135 | 136 | if length is not None: 137 | uuid_len = len(uuid) 138 | amount = abs(length) // uuid_len + (abs(length) % uuid_len > 0) 139 | 140 | if length < 0: 141 | return (uuid * amount)[length:] 142 | else: 143 | return (uuid * amount)[:length] 144 | else: 145 | return uuid 146 | 147 | @staticmethod 148 | def df(path: str = '//', unit: Optional[str] = None) -> Union[int, str]: 149 | """ 150 | Get free disk space 151 | 152 | :param path: Path to obtain informations 153 | :type path: str, optional 154 | :param unit: Unit of returned size [None, 'byte', 'kB', 'MB'] 155 | :type unit: str, optional 156 | 157 | :returns: Available disk space in byte or as string in kB or MB 158 | :rtype: int or str 159 | """ 160 | info = os.statvfs(path) 161 | 162 | result = -1 163 | 164 | if unit is None: 165 | result = info[0] * info[3] 166 | elif unit.lower() == 'byte': 167 | result = ('{} byte'.format((info[0] * info[3]))) 168 | elif unit.lower() == 'kb': 169 | result = ('{0:.3f} kB'.format((info[0] * info[3]) / 1024)) 170 | elif unit.lower() == 'mb': 171 | result = ('{0:.3f} MB'.format((info[0] * info[3]) / 1048576)) 172 | 173 | return result 174 | 175 | @staticmethod 176 | def get_free_memory() -> dict: 177 | """ 178 | Get free memory (RAM) 179 | 180 | :param update: Flag to collect latest informations 181 | :type update: bool, optional 182 | 183 | :returns: Informations about system RAM 184 | :rtype: dict 185 | """ 186 | gc.collect() 187 | free = gc.mem_free() 188 | allocated = gc.mem_alloc() 189 | total = free + allocated 190 | 191 | if total: 192 | percentage = '{0:.2f}%'.format((free / total) * 100) 193 | else: 194 | percentage = '100.00%' 195 | 196 | memory_stats = {'free': free, 197 | 'total': total, 198 | 'percentage': percentage} 199 | 200 | return memory_stats 201 | 202 | @staticmethod 203 | def free(full: bool = False) -> Union[int, str]: 204 | """ 205 | Get detailed informations about free RAM 206 | 207 | :param full: Flag to return str with total, free, percentage 208 | :type full: bool, optional 209 | 210 | :returns: Informations, percentage by default 211 | :rtype: int or str 212 | """ 213 | memory_stats = GenericHelper.get_free_memory() 214 | 215 | if full is False: 216 | return memory_stats['percentage'] 217 | else: 218 | return ('Total: {0:.1f} kB, Free: {1:.2f} kB ({2})'. 219 | format(memory_stats['total'] / 1024, 220 | memory_stats['free'] / 1024, 221 | memory_stats['percentage'])) 222 | 223 | @staticmethod 224 | def get_system_infos_raw() -> dict: 225 | """ 226 | Get the raw system infos. 227 | 228 | :returns: The raw system infos. 229 | :rtype: dict 230 | """ 231 | sys_info = dict() 232 | memory_info = GenericHelper.get_free_memory() 233 | 234 | sys_info['df'] = GenericHelper.df(path='/', unit='kB') 235 | sys_info['free_ram'] = memory_info['free'] 236 | sys_info['total_ram'] = memory_info['total'] 237 | sys_info['percentage_ram'] = memory_info['percentage'] 238 | sys_info['frequency'] = machine.freq() 239 | sys_info['uptime'] = time.ticks_ms() 240 | 241 | return sys_info 242 | 243 | @staticmethod 244 | def get_system_infos_human() -> dict: 245 | """ 246 | Get the human formatted system infos 247 | 248 | :returns: The human formatted system infos. 249 | :rtype: dict 250 | """ 251 | sys_info = dict() 252 | memory_info = GenericHelper.get_free_memory() 253 | 254 | # (year, month, mday, hour, minute, second, weekday, yearday) 255 | # (0, 1, 2, 3, 4, 5, 6, 7) 256 | seconds = int(time.ticks_ms() / 1000) 257 | uptime = time.gmtime(seconds) 258 | days = "{days:01d}".format(days=int(seconds / 86400)) 259 | 260 | sys_info['df'] = GenericHelper.df(path='/', unit='kB') 261 | sys_info['free_ram'] = "{} kB".format(memory_info['free'] / 1000.0) 262 | sys_info['total_ram'] = "{} kB".format(memory_info['total'] / 1000.0) 263 | sys_info['percentage_ram'] = memory_info['percentage'] 264 | sys_info['frequency'] = "{} MHz".format(int(machine.freq() / 1000000)) 265 | sys_info['uptime'] = "{d} days, {hour:02d}:{min:02d}:{sec:02d}".format( 266 | d=days, 267 | hour=uptime[3], 268 | min=uptime[4], 269 | sec=uptime[5]) 270 | 271 | return sys_info 272 | 273 | @staticmethod 274 | def str_to_dict(data: str) -> dict: 275 | """ 276 | Convert string to dictionary 277 | 278 | :param data: The data 279 | :type data: str 280 | 281 | :returns: Dictionary of string 282 | :rtype: dict 283 | """ 284 | return json.loads(data.replace("'", "\"")) 285 | 286 | @staticmethod 287 | def save_json(data: dict, path: str, mode: str = 'w') -> None: 288 | """ 289 | Save data as JSON file. 290 | 291 | :param data: The data 292 | :type data: dict 293 | :param path: The path to the JSON file 294 | :type path: str 295 | :param mode: The mode of file operation 296 | :type mode: str, optional 297 | """ 298 | with open(path, mode) as file: 299 | json.dump(data, file) 300 | 301 | @staticmethod 302 | def load_json(path: str, mode: str = 'r') -> dict: 303 | """ 304 | Load data from JSON file. 305 | 306 | :param path: The path to the JSON file 307 | :type path: str 308 | :param mode: The mode of file operation 309 | :type mode: str, optional 310 | 311 | :returns: Loaded data 312 | :rtype: dict 313 | """ 314 | read_data = dict() 315 | with open(path, mode) as file: 316 | read_data = json.load(file) 317 | 318 | return read_data 319 | 320 | @staticmethod 321 | def save_file(data: str, path: str, mode: str = 'wb') -> None: 322 | """ 323 | Save data to a file. 324 | 325 | :param data: The data 326 | :type data: str 327 | :param path: The path to the file 328 | :type path: str 329 | :param mode: The mode of file operation 330 | :type mode: str, optional 331 | """ 332 | # save to file as binary by default 333 | with open(path, mode) as file: 334 | file.write(data) 335 | 336 | @staticmethod 337 | def load_file(path: str, mode: str = 'rb') -> str: 338 | """ 339 | Wrapper for read_file. 340 | 341 | :param path: The path to the file to read 342 | :type path: str 343 | 344 | :returns: The raw file content. 345 | :rtype: str 346 | :param mode: The mode of file operation 347 | :type mode: str, optional 348 | 349 | :returns: Content of file 350 | :rtype: str 351 | """ 352 | return GenericHelper.read_file(path=path, mode=mode) 353 | 354 | @staticmethod 355 | def read_file(path: str, mode: str = 'rb') -> str: 356 | """ 357 | Read file content. 358 | 359 | :param path: The path to the file to read 360 | :type path: str 361 | :param mode: The mode of file operation 362 | :type mode: str, optional 363 | 364 | :returns: The raw file content. 365 | :rtype: str 366 | """ 367 | read_data = "" 368 | 369 | with open(path, mode) as file: 370 | read_data = file.read() 371 | 372 | return read_data 373 | -------------------------------------------------------------------------------- /simulation/src/generic_helper/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | # message.py 5 | # https://github.com/peterhinch/micropython-async/blob/a87bda1b716090da27fd288cc8b19b20525ea20c/v3/primitives/message.py 6 | # Now uses ThreadSafeFlag for efficiency 7 | 8 | # Copyright (c) 2018-2021 Peter Hinch 9 | # Released under the MIT License (MIT) - see LICENSE file 10 | 11 | # Usage: 12 | # from primitives.message import Message 13 | 14 | try: 15 | import uasyncio as asyncio 16 | except ImportError: 17 | import asyncio 18 | 19 | # A coro waiting on a message issues await message 20 | # A coro or hard/soft ISR raising the message issues.set(payload) 21 | # .clear() should be issued by at least one waiting task and before 22 | # next event. 23 | 24 | 25 | # class Message(asyncio.ThreadSafeFlag): 26 | class Message(object): 27 | def __init__(self, _=0): 28 | # Arg: poll interval. Compatibility with old code. 29 | self._evt = asyncio.Event() 30 | self._data = None # Message 31 | self._state = False # Ensure only one task waits on ThreadSafeFlag 32 | self._is_set = False # For .is_set() 33 | # super().__init__() 34 | 35 | def clear(self): 36 | # At least one task must call clear when scheduled 37 | self._state = False 38 | self._is_set = False 39 | 40 | def __iter__(self): 41 | yield from self.wait() 42 | return self._data 43 | 44 | async def wait(self): 45 | if self._state: 46 | # A task waits on ThreadSafeFlag 47 | await self._evt.wait() # Wait on event 48 | else: 49 | # First task to wait 50 | self._state = True 51 | # Ensure other tasks see updated ._state before they wait 52 | await asyncio.sleep_ms(0) 53 | await super().wait() # Wait on ThreadSafeFlag 54 | self._evt.set() 55 | self._evt.clear() 56 | 57 | return self._data 58 | 59 | def set(self, data=None): 60 | # Can be called from a hard ISR 61 | self._data = data 62 | self._is_set = True 63 | # super().set() 64 | 65 | def is_set(self): 66 | return self._is_set 67 | 68 | def value(self): 69 | return self._data 70 | -------------------------------------------------------------------------------- /simulation/src/led_helper/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .led_helper import Neopixel 5 | from .led_helper import Led 6 | from .neopixel import NeoPixel 7 | -------------------------------------------------------------------------------- /simulation/src/led_helper/neopixel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake Micropython NeoPixel 6 | 7 | See https://docs.micropython.org/en/latest/library/neopixel.html 8 | """ 9 | 10 | from machine import Pin 11 | 12 | 13 | class NeoPixel(object): 14 | """docstring for NeoPixel""" 15 | def __init__(self, pin: Pin, n: int, bpp: int = 3, timing: int = 1): 16 | """ 17 | Initialise NeoPixel 18 | 19 | :param pin: Pin of Neopixel LED 20 | :type pin: Pin 21 | :param n: Number of Neopixel LEDs 22 | :type n: int 23 | :param bpp: 3 for RGB LEDs, 4 for RGBW LEDs 24 | :type bpp: int, optional 25 | :param timing: 0 for 400KHz, and 1 for 800kHz LEDs 26 | :type timing: int, optional 27 | """ 28 | self._pin = pin 29 | self._amount = n 30 | 31 | def fill(self, pixel) -> None: 32 | for _ in range(0, self.__len__()): 33 | pass 34 | 35 | def __len__(self) -> int: 36 | return self._amount 37 | 38 | def __setitem__(self, index, value) -> None: 39 | setattr(self, str(index), value) 40 | 41 | def __getitem__(self, index): 42 | return getattr(self, index) 43 | 44 | def write(self) -> None: 45 | pass 46 | -------------------------------------------------------------------------------- /simulation/src/machine/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .machine import machine 5 | from .pin import Pin 6 | from .rtc import RTC 7 | from .timer import Timer, TimerError 8 | -------------------------------------------------------------------------------- /simulation/src/machine/machine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake Micropython Machine class 6 | 7 | See https://docs.micropython.org/en/latest/library/machine.html 8 | """ 9 | import binascii 10 | 11 | 12 | class machine(object): 13 | """docstring for machine""" 14 | UNKNOWN_RESET = 0 15 | PWRON_RESET = 1 16 | HARD_RESET = 2 17 | WDT_RESET = 3 18 | DEEPSLEEP_RESET = 4 19 | SOFT_RESET = 5 20 | 21 | def __init__(self): 22 | pass 23 | 24 | @staticmethod 25 | def reset_cause() -> int: 26 | """ 27 | Get last reset cause 28 | 29 | :returns: Reset cause 30 | :rtype: int 31 | """ 32 | return machine.SOFT_RESET 33 | 34 | @staticmethod 35 | def unique_id() -> bytes: 36 | """ 37 | Get unique device ID 38 | 39 | :returns: Device ID 40 | :rtype: bytes 41 | """ 42 | return binascii.unhexlify(b'DEADBEEF') 43 | 44 | @staticmethod 45 | def freq() -> int: 46 | """ 47 | Get current CPU frequency 48 | 49 | :returns: CPU frequency in Hz 50 | :rtype: int 51 | """ 52 | return 160 * 1000 * 1000 53 | -------------------------------------------------------------------------------- /simulation/src/machine/pin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake Micropython Pin class 6 | 7 | See https://docs.micropython.org/en/latest/library/machine.Pin.html 8 | """ 9 | from typing import Optional, Union 10 | 11 | 12 | class Pin(object): 13 | """docstring for Pin""" 14 | IN = 1 15 | OUT = 2 16 | 17 | def __init__(self, pin: int, mode: int): 18 | self._pin = pin 19 | self._mode = mode 20 | self._value = False 21 | 22 | def value(self, val: Optional[Union[int, bool]] = None) -> Optional[bool]: 23 | """ 24 | Set or get the value of the pin 25 | 26 | :param val: The value 27 | :type val: Optional[Union[int, bool]] 28 | 29 | :returns: State of the pin if no value specifed, None otherwise 30 | :rtype: Optional[bool] 31 | """ 32 | if val is not None and self._mode == Pin.OUT: 33 | # set pin state 34 | self._value = bool(val) 35 | else: 36 | # get pin state 37 | return self._value 38 | 39 | def on(self) -> None: 40 | """Set pin to "1" output level""" 41 | if self._mode == Pin.OUT: 42 | self.value(val=True) 43 | 44 | def off(self) -> None: 45 | """Set pin to "0" output level""" 46 | if self._mode == Pin.OUT: 47 | self.value(val=False) 48 | -------------------------------------------------------------------------------- /simulation/src/machine/rtc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake Micropython RTC class 6 | 7 | See https://docs.micropython.org/en/latest/library/machine.RTC.html 8 | """ 9 | 10 | import time 11 | from datetime import date 12 | 13 | from typing import Tuple 14 | 15 | 16 | class RTC(object): 17 | """docstring for RTC""" 18 | def __init__(self, id: int = 0): 19 | self._time_tuple = ( 20 | 2000, # year 0 21 | 1, # month 1 22 | 1, # day 2 23 | 0, # weekday 3 24 | 0, # hour 4 25 | 0, # minute 5 26 | 0, # second 6 27 | 0 # subsecond 7 28 | ) 29 | 30 | def init(self, time_tuple: Tuple[int]) -> None: 31 | """ 32 | Initialize the RTC with the given time tuple. 33 | 34 | :param time_tuple: The time tuple 35 | :type time_tuple: Tuple[int] 36 | """ 37 | print('RTC init as: {}'.format(time_tuple)) 38 | print('year, month, day, hour, minute, second, subsec, tzinfo') 39 | 40 | weekday = date(time_tuple[0], time_tuple[1], time_tuple[2]).weekday() 41 | 42 | # set current time to the given time tuple 43 | self._time_tuple = ( 44 | time_tuple[0], time_tuple[1], time_tuple[2], # year, month, day 45 | weekday, # weekday 46 | time_tuple[3], time_tuple[4], time_tuple[5], # hour, min, sec 47 | time_tuple[6] # subsecond 48 | ) 49 | 50 | def datetime(self) -> time.struct_time: 51 | """ 52 | Get current datetime 53 | 54 | :returns: Time tuple 55 | :rtype: time.struct_time 56 | """ 57 | # return time.localtime() 58 | return self._time_tuple 59 | -------------------------------------------------------------------------------- /simulation/src/machine/timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake Micropython Timer class 6 | 7 | See https://docs.micropython.org/en/latest/library/machine.Timer.html 8 | """ 9 | 10 | from threading import Timer as ThreadTimer 11 | 12 | from typing import Any, Callable 13 | 14 | 15 | class RepeatTimer(ThreadTimer): 16 | def run(self): 17 | while not self.finished.wait(self.interval): 18 | self.function(*self.args, **self.kwargs) 19 | 20 | 21 | class TimerError(Exception): 22 | """Base class for exceptions in this module.""" 23 | pass 24 | 25 | 26 | class Timer(object): 27 | """docstring for Timer""" 28 | ONE_SHOT = 0 29 | PERIODIC = 1 30 | 31 | def __init__(self, id: int = 0): 32 | self._timer = None 33 | 34 | def init(self, 35 | mode: int = PERIODIC, 36 | period: int = -1, 37 | callback: Callable[[Any], None] = None) -> None: 38 | """ 39 | Initialise the timer 40 | 41 | :param mode: The mode 42 | :type mode: int 43 | :param period: The timer period in milliseconds 44 | :type period: int 45 | :param callback: The callable to call upon expiration of the 46 | timer period 47 | :type callback: Callable[[Any], None] 48 | """ 49 | if mode == self.ONE_SHOT: 50 | self._timer = ThreadTimer(interval=(period / 1000), 51 | function=callback, 52 | args=None) 53 | self._timer.start() 54 | elif mode == self.PERIODIC: 55 | self._timer = RepeatTimer(interval=(period / 1000), 56 | function=callback, 57 | args=None) 58 | self._timer.start() 59 | else: 60 | raise TimerError('Unsupported Timer mode: {}'.format(mode)) 61 | 62 | def deinit(self) -> None: 63 | """Deinitialises/stops the timer""" 64 | if self._timer: 65 | self._timer.cancel() 66 | -------------------------------------------------------------------------------- /simulation/src/path_helper/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .path_helper import PathHelper 5 | -------------------------------------------------------------------------------- /simulation/src/path_helper/path_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Path Helper 6 | 7 | Provide unavailable path functions for Micropython boards 8 | """ 9 | 10 | # import os 11 | from pathlib import Path 12 | 13 | 14 | class PathHelper(object): 15 | """docstring for PathHelper""" 16 | def __init__(self): 17 | pass 18 | 19 | # There's currently no os.path.exists() support in MicroPython 20 | @staticmethod 21 | def exists(path: str) -> bool: 22 | """ 23 | Check existance of file at given path. 24 | 25 | :param path: The path to the file 26 | :type path: str 27 | 28 | :returns: Existance of file 29 | :rtype: bool 30 | """ 31 | result = Path(path).exists() 32 | return result 33 | 34 | """ 35 | result = False 36 | 37 | path_to_file_list = path.split('/') 38 | # ['path', 'to', 'some', 'file.txt'] 39 | 40 | root_path = '' 41 | # if sys.platform == 'esp32': 42 | # if sys.platform not in ['aix', 'linux', 'win32', 'cygwin', 'darwin']: 43 | # root_path = '' 44 | # else: 45 | # root_path = os.path.dirname(os.path.abspath(__file__)) 46 | # print('The root path: {}'.format(root_path)) 47 | 48 | this_path = root_path 49 | for ele in path_to_file_list[:-1]: 50 | files_in_dir = os.listdir(this_path) 51 | # print('Files in {}: {}'.format(this_path, files_in_dir)) 52 | 53 | if ele in files_in_dir: 54 | # print('"{}" found in "{}"'.format(ele, files_in_dir)) 55 | 56 | if this_path == '': 57 | this_path += '{}'.format(ele) 58 | else: 59 | this_path += '/{}'.format(ele) 60 | 61 | # print('Next folder to be checked is: {}'.format(this_path)) 62 | else: 63 | return result 64 | 65 | files_in_dir = os.listdir(this_path) 66 | if path_to_file_list[-1] in files_in_dir: 67 | # print('File "{}" found in "{}"'. 68 | # format(path_to_file_list[-1], this_path)) 69 | return True 70 | else: 71 | return False 72 | """ 73 | -------------------------------------------------------------------------------- /simulation/src/run_simulation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Run WiFi Manager simulation""" 5 | 6 | from wifi_manager import WiFiManager 7 | 8 | 9 | def main(): 10 | wm = WiFiManager(logger=None, quiet=False) 11 | result = wm.load_and_connect() 12 | print('Result of load_and_connect: {}'.format(result)) 13 | 14 | print('Starting configuration anyway in simulation') 15 | result = False 16 | 17 | if result is False: 18 | wm.start_config() 19 | else: 20 | print('Successfully connected to a network :)') 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /simulation/src/time_helper/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .time_helper import TimeHelper 5 | -------------------------------------------------------------------------------- /simulation/src/time_helper/time_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Time Helper 6 | 7 | Sync and set internal clock (RTC) with NTP server time 8 | """ 9 | 10 | from machine import RTC 11 | # import ntptime 12 | import time 13 | 14 | 15 | class TimeHelper(object): 16 | """docstring for TimeHelper""" 17 | def __init__(self, tz: int = 1): 18 | """ 19 | Initialize TimeHelper 20 | 21 | :param tz: Timezone offset 22 | :type tz: int, optional 23 | """ 24 | self.rtc = RTC() 25 | self._timezone = tz 26 | 27 | def sync_time(self, timezone: int = None) -> None: 28 | """ 29 | Sync the RTC with data from NTP server. 30 | 31 | No network check is performed. 32 | Existing RTC value will not be changed if NTP server is not reached. 33 | No daylight saving is implemented. 34 | 35 | :param timezone: The timezone shift 36 | :type timezone: int, optional 37 | """ 38 | tm = time.localtime() 39 | print("Local time before synchronization: {}".format(tm)) 40 | # Local time before synchronization: (2000, 1, 1, 0, 9, 44, 5, 1) 41 | # (y, m, d, h, min, sec, wd, yd) 42 | 43 | # sync time with NTP server 44 | """ 45 | try: 46 | ntptime.settime() 47 | print('Synced with NTP server') 48 | except Exception as e: 49 | print('Failed to sync with NTP server due to {}'.format(e)) 50 | return 51 | """ 52 | 53 | if timezone is None: 54 | timezone = self._timezone 55 | 56 | tm = time.localtime() 57 | tm = (tm[0], tm[1], tm[2], tm[3] + timezone, tm[4], tm[5], tm[6], tm[7]) # noqa 58 | # (year, month, day, hour, min, sec, wday, yday) 59 | 60 | print("Local time after synchronization: {}".format(tm)) 61 | # Local time after synchronization: (2021, 7, 15, 19, 12, 25, 1, 196) 62 | # (2021, 7, 15, 19, 12, 25, 1, 196) 63 | # (year, m, day, h, min,sec, weekday, yearday) 64 | # (0, 1, 2, 3, 4, 5, 6, 7) 65 | 66 | # year, month, day, hours, minutes, seconds, subsec, tzinfo 67 | self.rtc.init( 68 | (tm[0], tm[1], tm[2], tm[3], tm[4], tm[5], 0, self.timezone) 69 | ) 70 | 71 | @property 72 | def timezone(self) -> int: 73 | """ 74 | Current timezone 75 | 76 | :returns: This timezone 77 | :rtype: int 78 | """ 79 | return self._timezone 80 | 81 | @timezone.setter 82 | def timezone(self, value: int) -> None: 83 | """ 84 | Set timezone for RTC 85 | 86 | :param value: The timezone offset 87 | :type value: int 88 | """ 89 | self._timezone = value 90 | 91 | @property 92 | def year(self) -> int: 93 | """ 94 | Current year from RTC 95 | 96 | :returns: This year 97 | :rtype: int 98 | """ 99 | # (y, m, d, wd, h, m, s, subseconds) 100 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 101 | return self.rtc.datetime()[0] 102 | 103 | @property 104 | def month(self) -> int: 105 | """ 106 | Current month from RTc 107 | 108 | :returns: This month 109 | :rtype: int 110 | """ 111 | # (y, m, d, wd, h, m, s, subseconds) 112 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 113 | return self.rtc.datetime()[1] 114 | 115 | @property 116 | def day(self) -> int: 117 | """ 118 | Current day from RTC 119 | 120 | :returns: This day 121 | :rtype: int 122 | """ 123 | # (y, m, d, wd, h, m, s, subseconds) 124 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 125 | return self.rtc.datetime()[2] 126 | 127 | @property 128 | def weekday(self) -> int: 129 | """ 130 | Current weekday from RTC 131 | 132 | :returns: This weekday 133 | :rtype: int 134 | """ 135 | # (y, m, d, wd, h, m, s, subseconds) 136 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 137 | return self.rtc.datetime()[3] 138 | 139 | @property 140 | def hour(self) -> int: 141 | """ 142 | Current hour from RTC 143 | 144 | :returns: This hour 145 | :rtype: int 146 | """ 147 | # (y, m, d, wd, h, m, s, subseconds) 148 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 149 | return self.rtc.datetime()[4] 150 | 151 | @property 152 | def minute(self) -> int: 153 | """ 154 | Current minute from RTC 155 | 156 | :returns: This minute 157 | :rtype: int 158 | """ 159 | # (y, m, d, wd, h, m, s, subseconds) 160 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 161 | return self.rtc.datetime()[5] 162 | 163 | @property 164 | def second(self) -> int: 165 | """ 166 | Current second from RTC 167 | 168 | :returns: This second 169 | :rtype: int 170 | """ 171 | # (y, m, d, wd, h, m, s, subseconds) 172 | # (2021, 7, 15, 5, 19, 12, 25, 1, 196) 173 | return self.rtc.datetime()[6] 174 | 175 | @property 176 | def current_timestamp(self) -> tuple: 177 | """ 178 | Get current timestamp 179 | 180 | :returns: Current system timestamp 181 | :rtype: tuple 182 | """ 183 | # (2021, 7, 15, 19, 12, 25, 1, 196) 184 | # (year, m, day, h, min, sec, weekday, yearday) 185 | # (0, 1, 2, 3, 4, 5, 6, 7) 186 | return time.localtime() 187 | 188 | @property 189 | def current_timestamp_iso8601(self) -> str: 190 | """ 191 | Get current timestamp in ISO8601 format 192 | 193 | :returns: Timestamp as HH:MM:SS YYYY-MM-DD 194 | :rtype: str 195 | """ 196 | now = self.current_timestamp 197 | return ( 198 | '{hour:02d}:{minute:02d}:{sec:02d} {year}-{month:02d}-{day:02d}'. 199 | format(hour=now[3], 200 | minute=now[4], 201 | sec=now[5], 202 | year=now[0], 203 | month=now[1], 204 | day=now[2])) 205 | 206 | @property 207 | def current_timestamp_human(self) -> str: 208 | """ 209 | Wrapper around @see current_timestamp_iso8601 210 | 211 | :returns: Timestamp as HH:MM:SS YYYY-MM-DD 212 | :rtype: str 213 | """ 214 | return self.current_timestamp_iso8601 215 | -------------------------------------------------------------------------------- /simulation/src/wifi_helper/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .wifi_helper import WifiHelper 5 | -------------------------------------------------------------------------------- /simulation/src/wifi_manager/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .wifi_manager import WiFiManager 5 | -------------------------------------------------------------------------------- /simulation/templates/data.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Modbus Data 9 | 10 | 15 | 22 | 33 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 105 | 106 | 185 | 186 | -------------------------------------------------------------------------------- /simulation/templates/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Setup 9 | 10 | 11 | 18 | 19 | 20 |
21 |
22 |
23 | 26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /simulation/templates/modbus_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ISTS": { 3 | "ENABLE_BUTTON_STATE_ISTS": { 4 | "register": 11, 5 | "val": [ 6 | false, 7 | false, 8 | false, 9 | false, 10 | false, 11 | false, 12 | false, 13 | false 14 | ] 15 | }, 16 | "SSR_STATE_ISTS": { 17 | "register": 10, 18 | "val": [ 19 | false, 20 | false, 21 | false, 22 | false, 23 | false, 24 | false, 25 | false, 26 | false 27 | ] 28 | }, 29 | "CHARGING_ACTIVE_ISTS": { 30 | "register": 12, 31 | "val": [ 32 | false, 33 | false, 34 | false, 35 | false, 36 | false, 37 | false, 38 | false, 39 | false 40 | ] 41 | } 42 | }, 43 | "IREGS": { 44 | "CP_HIGH_VOLTAGE_MEDIAN_IREG": { 45 | "register": 105, 46 | "val": 2848 47 | }, 48 | "LOOP_TIME_US_IREG": { 49 | "register": 10, 50 | "val": [ 51 | 0, 52 | 24611 53 | ] 54 | }, 55 | "HW_VERSION_PATCH_IREG": { 56 | "register": 35, 57 | "val": 1 58 | }, 59 | "CHARGING_BEGIN_TIME_IREG": { 60 | "register": 51, 61 | "val": [ 62 | 0, 63 | 0 64 | ] 65 | }, 66 | "COMPLETE_CHARGING_CYCLES_IREG": { 67 | "register": 57, 68 | "val": [ 69 | 0, 70 | 0 71 | ] 72 | }, 73 | "CREATION_DATE_IREG": { 74 | "register": 36, 75 | "val": 19041 76 | }, 77 | "CHARGING_DURATION_IREG": { 78 | "register": 55, 79 | "val": [ 80 | 0, 81 | 0 82 | ] 83 | }, 84 | "CP_LOW_VOLTAGE_MEDIAN_IREG": { 85 | "register": 104, 86 | "val": 0 87 | }, 88 | "DEVICE_UUID_IREG": { 89 | "register": 14, 90 | "val": [ 91 | 66, 92 | 54, 93 | 17234, 94 | 20761, 95 | 8248, 96 | 14133 97 | ] 98 | }, 99 | "PROGRAM_RAM_IREG": { 100 | "register": 24, 101 | "val": [ 102 | 0, 103 | 2200 104 | ] 105 | }, 106 | "SW_VERSION_MAJOR_IREG": { 107 | "register": 30, 108 | "val": 3 109 | }, 110 | "CHARGING_END_TIME_IREG": { 111 | "register": 53, 112 | "val": [ 113 | 0, 114 | 0 115 | ] 116 | }, 117 | "RAW_CP_HIGH_VALUE_MEDIAN_IREG": { 118 | "register": 103, 119 | "val": 3540 120 | }, 121 | "PP_VOLTAGE_MEDIAN_IREG": { 122 | "register": 101, 123 | "val": 2777 124 | }, 125 | "FREE_RAM_IREG": { 126 | "register": 22, 127 | "val": [ 128 | 0, 129 | 3688 130 | ] 131 | }, 132 | "HW_VERSION_MAJOR_IREG": { 133 | "register": 33, 134 | "val": 4 135 | }, 136 | "MEASUREMENT_TIME_CP_IREG": { 137 | "register": 106, 138 | "val": 8801 139 | }, 140 | "SW_VERSION_PATCH_IREG": { 141 | "register": 32, 142 | "val": 0 143 | }, 144 | "SW_VERSION_MINOR_IREG": { 145 | "register": 31, 146 | "val": 2 147 | }, 148 | "RAW_PP_VALUE_MEDIAN_IREG": { 149 | "register": 100, 150 | "val": 3448 151 | }, 152 | "HW_VERSION_MINOR_IREG": { 153 | "register": 34, 154 | "val": 0 155 | }, 156 | "CHARGING_STATE_IREG": { 157 | "register": 62, 158 | "val": 0 159 | }, 160 | "MEASUREMENT_TIME_PP_IREG": { 161 | "register": 107, 162 | "val": 9346 163 | }, 164 | "UPTIME_MS_IREG": { 165 | "register": 12, 166 | "val": [ 167 | 5406, 168 | 53656 169 | ] 170 | }, 171 | "CHARGING_DUTYCYCLE_IREG": { 172 | "register": 60, 173 | "val": 255 174 | }, 175 | "SW_VERSION_IREG": { 176 | "register": 20, 177 | "val": [ 178 | 0, 179 | 320 180 | ] 181 | }, 182 | "CABLE_AMPACITY_IREG": { 183 | "register": 61, 184 | "val": 0 185 | }, 186 | "RAW_CP_LOW_VALUE_MEDIAN_IREG": { 187 | "register": 102, 188 | "val": 0 189 | }, 190 | "CHARGING_CURRENT_IREG": { 191 | "register": 59, 192 | "val": 13 193 | } 194 | }, 195 | "HREGS": { 196 | "DEVICE_ID_HREG": { 197 | "register": 10, 198 | "val": 10 199 | }, 200 | "CHARGING_CURRENT_HREG": { 201 | "register": 21, 202 | "val": 6 203 | } 204 | }, 205 | "COILS": { 206 | "SYSTEM_RESET_COIL": { 207 | "register": 10, 208 | "val": [ 209 | false, 210 | false, 211 | false, 212 | false, 213 | false, 214 | false, 215 | false, 216 | false 217 | ] 218 | }, 219 | "USE_MB_CURRENT_COIL": { 220 | "register": 20, 221 | "val": [ 222 | false, 223 | false, 224 | false, 225 | false, 226 | false, 227 | false, 228 | false, 229 | false 230 | ] 231 | }, 232 | "CONFIG_RESET_COIL": { 233 | "register": 11, 234 | "val": [ 235 | false, 236 | false, 237 | false, 238 | false, 239 | false, 240 | false, 241 | false, 242 | false 243 | ] 244 | } 245 | } 246 | } -------------------------------------------------------------------------------- /simulation/templates/remove.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Remove WiFi Network 9 | 10 | 11 | 15 | 22 | 33 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 74 |
75 | 76 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /simulation/templates/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Result 9 | 10 | 11 | 12 | 13 | 14 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for key, value in result.items() %} 37 | 38 | 39 | 41 | 42 | {% endfor %} 43 | 44 |
#KeyValue
{{ loop.index }} {{ key }} 40 | {{ value }}
45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /simulation/templates/select.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Select WiFi 9 | 10 | 11 | 18 | 29 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | 59 | 60 | 99 | 100 | -------------------------------------------------------------------------------- /simulation/templates/wifi_select_loader.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Select WiFi 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 | 32 |
33 | 34 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /simulation/tests/data/encrypted/multi-network.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/simulation/tests/data/encrypted/multi-network.json -------------------------------------------------------------------------------- /simulation/tests/data/encrypted/single-network.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/simulation/tests/data/encrypted/single-network.json -------------------------------------------------------------------------------- /simulation/tests/data/unencrypted/multi-network.json: -------------------------------------------------------------------------------- 1 | [{"ssid": "SSID Name", "password": "1234qwertz@"}, {"ssid": "Other-Network@1", "password": "password"}] -------------------------------------------------------------------------------- /simulation/tests/data/unencrypted/no-json.txt: -------------------------------------------------------------------------------- 1 | {'ssid': 'MyNet', 'password': 'empty'} -------------------------------------------------------------------------------- /simulation/tests/data/unencrypted/single-network.json: -------------------------------------------------------------------------------- 1 | {"ssid": "MyNet", "password": "empty"} -------------------------------------------------------------------------------- /simulation/tests/test_absolute_truth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | """Unittest for testing the absolute truth""" 4 | 5 | import logging 6 | import sys 7 | import unittest 8 | 9 | 10 | class TestAbsoluteTruth(unittest.TestCase): 11 | def setUp(self) -> None: 12 | """Run before every test method""" 13 | # define a format 14 | custom_format = "[%(asctime)s][%(levelname)-8s][%(filename)-20s @" \ 15 | " %(funcName)-15s:%(lineno)4s] %(message)s" 16 | 17 | # set basic config and level for all loggers 18 | logging.basicConfig(level=logging.INFO, 19 | format=custom_format, 20 | stream=sys.stdout) 21 | 22 | # create a logger for this TestSuite 23 | self.test_logger = logging.getLogger(__name__) 24 | 25 | # set the test logger level 26 | self.test_logger.setLevel(logging.DEBUG) 27 | 28 | # enable/disable the log output of the device logger for the tests 29 | # if enabled log data inside this test will be printed 30 | self.test_logger.disabled = False 31 | 32 | def test_absolute_truth(self) -> None: 33 | """Test the unittest itself""" 34 | x = 0 35 | y = 1 36 | z = 2 37 | none_thing = None 38 | some_dict = dict() 39 | some_list = [x, y, 40, "asdf", z] 40 | 41 | self.assertTrue(True) 42 | self.assertFalse(False) 43 | 44 | self.assertEqual(y, 1) 45 | assert y == 1 46 | self.assertNotEqual(x, y) 47 | assert x != y 48 | 49 | self.assertIsNone(none_thing) 50 | self.assertIsNotNone(some_dict) 51 | 52 | self.assertIn(y, some_list) 53 | self.assertNotIn(12, some_list) 54 | 55 | # self.assertRaises(exc, fun, args, *kwds) 56 | 57 | self.assertIsInstance(some_dict, dict) 58 | 59 | self.assertGreater(a=y, b=x) 60 | self.assertGreaterEqual(a=y, b=x) 61 | self.assertLess(a=x, b=y) 62 | 63 | self.test_logger.debug("Sample debug message") 64 | 65 | def tearDown(self) -> None: 66 | """Run after every test method""" 67 | pass 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /simulation/tests/test_machine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Machine""" 5 | 6 | import unittest 7 | 8 | # custom imports 9 | from machine import machine 10 | 11 | 12 | class TestMachine(unittest.TestCase): 13 | def setUp(self) -> None: 14 | pass 15 | 16 | def tearDown(self) -> None: 17 | pass 18 | 19 | def test_reset_cause(self) -> None: 20 | """Test getting the reset cause""" 21 | result = machine.reset_cause() 22 | self.assertEqual(result, 5) 23 | self.assertEqual(result, machine.SOFT_RESET) 24 | 25 | def test_unique_id(self) -> None: 26 | """Test getting the unique ID""" 27 | result = machine.unique_id() 28 | self.assertIsInstance(result, bytes) 29 | self.assertEqual(result, b'\xde\xad\xbe\xef') 30 | 31 | def test_freq(self) -> None: 32 | """Test getting the device CPU frequency""" 33 | result = machine.freq() 34 | self.assertIsInstance(result, int) 35 | self.assertEqual(result, 160 * 1000 * 1000) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /simulation/tests/test_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Message""" 5 | 6 | from nose2.tools import params 7 | from typing import Any 8 | import unittest 9 | 10 | # custom imports 11 | from generic_helper import Message 12 | 13 | 14 | class TestMessage(unittest.TestCase): 15 | def setUp(self) -> None: 16 | pass 17 | 18 | def tearDown(self) -> None: 19 | pass 20 | 21 | @unittest.skip("Checked by test_set") 22 | def test_clear(self) -> None: 23 | pass 24 | 25 | @params( 26 | (None), 27 | (False), 28 | (True), 29 | (0), 30 | (1), 31 | (3.14), 32 | ("string"), 33 | (["string", True]), 34 | ({'key': ["string", True], 'key2': 42}), 35 | ) 36 | def test_set(self, data: Any) -> None: 37 | msg = Message() 38 | msg.set(data) 39 | 40 | result = msg.value() 41 | self.assertEqual(result, data) 42 | 43 | for x in range(3, 10): 44 | msg.set(x) 45 | result = msg.value() 46 | self.assertNotEqual(result, data) 47 | self.assertEqual(result, x) 48 | 49 | msg2 = Message() 50 | msg.set(msg2) 51 | 52 | result = msg.value() 53 | self.assertEqual(result, msg2) 54 | 55 | @unittest.skip("Checked by test_set") 56 | def test_is_set(self) -> None: 57 | pass 58 | 59 | @unittest.skip("Checked by test_set") 60 | def test_value(self) -> None: 61 | """Test getting latest value of Message""" 62 | pass 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /simulation/tests/test_neopixel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of NeoPixel""" 5 | 6 | from nose2.tools import params 7 | import unittest 8 | 9 | # custom imports 10 | from led_helper import NeoPixel 11 | from machine import Pin 12 | 13 | 14 | class TestNeoPixel(unittest.TestCase): 15 | def setUp(self) -> None: 16 | pass 17 | 18 | def tearDown(self) -> None: 19 | pass 20 | 21 | def test__init__(self) -> None: 22 | """Test __init__ function""" 23 | pixel_pin = Pin(12, Pin.OUT) 24 | 25 | pixel = NeoPixel(pin=pixel_pin, n=34) 26 | 27 | self.assertIsInstance(pixel._pin, Pin) 28 | self.assertEqual(pixel._amount, 34) 29 | 30 | @unittest.skip("Not yet implemented") 31 | @params( 32 | (None), 33 | ('Sauerkraut') 34 | ) 35 | def test_fill(self, pixel) -> None: 36 | """Test setting the value of all pixels to the pixel value""" 37 | pass 38 | 39 | def test__len__(self) -> None: 40 | """Test getting the number of LEDs""" 41 | pixel_pin = Pin(32, Pin.OUT) 42 | 43 | pixels = 42 44 | pixel = NeoPixel(pin=pixel_pin, n=pixels) 45 | 46 | amount_of_pixels = len(pixel) 47 | self.assertEqual(amount_of_pixels, pixels) 48 | 49 | @unittest.skip("Not yet implemented") 50 | def test_write(self) -> None: 51 | pass 52 | 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /simulation/tests/test_network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Network""" 5 | 6 | from nose2.tools import params 7 | import random 8 | import unittest 9 | from unittest.mock import patch 10 | import sys 11 | 12 | # custom imports 13 | from wifi_helper.network import NetworkHelper 14 | # from wifi_helper.network import Station 15 | from wifi_helper.network import Client 16 | # from wifi_helper.network import WLAN 17 | 18 | 19 | class TestNetworkHelper(unittest.TestCase): 20 | def setUp(self) -> None: 21 | pass 22 | 23 | def tearDown(self) -> None: 24 | pass 25 | 26 | @unittest.skip("Not yet implemented") 27 | def test__internal_scan(self) -> None: 28 | pass 29 | # with patch('sys.platform', 'unsupported') as mock_platform: 30 | # nets = NetworkHelper._internal_scan() 31 | 32 | @unittest.skipIf(sys.platform == "linux" or sys.platform == "linux2", 33 | 'Linux not tested on this system') 34 | def test__scan_linux(self) -> None: 35 | pass 36 | 37 | @unittest.skipIf(sys.platform == 'darwin', 38 | 'Linux not tested on this system') 39 | def test__scan_mac(self) -> None: 40 | pass 41 | 42 | @unittest.skipIf(sys.platform == 'win32', 43 | 'Windows not tested on this system') 44 | @unittest.skip("Not yet implemented") 45 | def test__scan_windows(self) -> None: 46 | pass 47 | 48 | @params( 49 | (50, -75), # percentage, dBm 50 | ) 51 | def test__quality_to_dbm(self, quality: int, expectation: int) -> None: 52 | result = NetworkHelper._quality_to_dbm(quality=quality) 53 | self.assertEqual(result, expectation) 54 | 55 | def test__dummy_data(self) -> None: 56 | # get two random values for RSSI in range of 0 to 100 57 | mocked_rssi = random.sample(range(0, 100), 2) 58 | 59 | # patch call of random.randint inside of _dummy_data with mocked_rssi 60 | with patch('random.randint') as mock_randint: 61 | mock_randint.side_effect = mocked_rssi 62 | nets = NetworkHelper._dummy_data() 63 | 64 | self.assertIsInstance(nets, list) 65 | self.assertEqual(len(nets), 2) 66 | 67 | for net in nets: 68 | self.assertIsInstance(net, dict) 69 | required_keys = [ 70 | 'ssid', 'RSSI', 'bssid', 'authmode', 'channel', 'hidden' 71 | ] 72 | self.assertEqual(len(net.keys()), len(required_keys)) 73 | self.assertTrue(all(name in required_keys for name in net.keys())) 74 | 75 | # check first network 76 | self.assertEqual(nets[0]['ssid'], 'TP-LINK_FBFC3C') 77 | self.assertEqual(nets[0]['RSSI'], -mocked_rssi[0]) 78 | self.assertEqual(nets[0]['bssid'], 'a0f3c1fbfc3c') 79 | self.assertEqual(nets[0]['authmode'], 'WPA/WPA2-PSK') 80 | self.assertEqual(nets[0]['channel'], 1) 81 | self.assertEqual(nets[0]['hidden'], False) 82 | 83 | # check second network 84 | self.assertEqual(nets[1]['ssid'], 'FRITZ!Box 7490') 85 | self.assertEqual(nets[1]['RSSI'], -mocked_rssi[1]) 86 | self.assertEqual(nets[1]['bssid'], '3810d517eb39') 87 | self.assertEqual(nets[1]['authmode'], 'WPA2-PSK') 88 | self.assertEqual(nets[1]['channel'], 11) 89 | self.assertEqual(nets[1]['hidden'], False) 90 | 91 | def test__dummy_data_upy(self) -> None: 92 | """Test getting dummy data in same style as Micropython""" 93 | # get two random values for RSSI in range of 0 to 100 94 | mocked_rssi = random.sample(range(0, 100), 2) 95 | 96 | # patch call of random.randint inside of _dummy_data with mocked_rssi 97 | with patch('random.randint') as mock_randint: 98 | mock_randint.side_effect = mocked_rssi 99 | dummy_data_upy = NetworkHelper._dummy_data_upy() 100 | 101 | self.assertIsInstance(dummy_data_upy, list) 102 | self.assertEqual(len(dummy_data_upy), 2) 103 | 104 | for net in dummy_data_upy: 105 | self.assertIsInstance(net, tuple) 106 | 107 | # check first network 108 | self.assertEqual(dummy_data_upy[0][0], 'TP-LINK_FBFC3C') 109 | self.assertEqual(dummy_data_upy[0][1], 'a0f3c1fbfc3c') 110 | self.assertEqual(dummy_data_upy[0][2], 1) 111 | self.assertEqual(dummy_data_upy[0][3], -mocked_rssi[0]) 112 | self.assertEqual(dummy_data_upy[0][4], 'WPA/WPA2-PSK') 113 | self.assertEqual(dummy_data_upy[0][5], False) 114 | 115 | # check second network 116 | self.assertEqual(dummy_data_upy[1][0], 'FRITZ!Box 7490') 117 | self.assertEqual(dummy_data_upy[1][1], '3810d517eb39') 118 | self.assertEqual(dummy_data_upy[1][2], 11) 119 | self.assertEqual(dummy_data_upy[1][3], -mocked_rssi[1]) 120 | self.assertEqual(dummy_data_upy[1][4], 'WPA2-PSK') 121 | self.assertEqual(dummy_data_upy[1][5], False) 122 | 123 | def test__convert_scan_to_upy_format(self) -> None: 124 | """Test converting the dummy data to same style as Micropython""" 125 | # get two random values for RSSI in range of 0 to 100 126 | mocked_rssi = random.sample(range(0, 100), 2) 127 | 128 | # patch call of random.randint inside of _dummy_data with mocked_rssi 129 | with patch('random.randint') as mock_randint: 130 | mock_randint.side_effect = mocked_rssi 131 | dummy_data = NetworkHelper._dummy_data() 132 | 133 | dummy_data_upy = NetworkHelper._convert_scan_to_upy_format( 134 | data=dummy_data) 135 | 136 | self.assertIsInstance(dummy_data_upy, list) 137 | self.assertEqual(len(dummy_data_upy), 2) 138 | 139 | for net in dummy_data_upy: 140 | self.assertIsInstance(net, tuple) 141 | 142 | # check first network 143 | self.assertEqual(dummy_data_upy[0][0], 'TP-LINK_FBFC3C') 144 | self.assertEqual(dummy_data_upy[0][1], 'a0f3c1fbfc3c') 145 | self.assertEqual(dummy_data_upy[0][2], 1) 146 | self.assertEqual(dummy_data_upy[0][3], -mocked_rssi[0]) 147 | self.assertEqual(dummy_data_upy[0][4], 'WPA/WPA2-PSK') 148 | self.assertEqual(dummy_data_upy[0][5], False) 149 | 150 | # check second network 151 | self.assertEqual(dummy_data_upy[1][0], 'FRITZ!Box 7490') 152 | self.assertEqual(dummy_data_upy[1][1], '3810d517eb39') 153 | self.assertEqual(dummy_data_upy[1][2], 11) 154 | self.assertEqual(dummy_data_upy[1][3], -mocked_rssi[1]) 155 | self.assertEqual(dummy_data_upy[1][4], 'WPA2-PSK') 156 | self.assertEqual(dummy_data_upy[1][5], False) 157 | 158 | @unittest.skip("Not yet implemented") 159 | def test__gather_ifconfig_data(self) -> None: 160 | pass 161 | 162 | 163 | class TestStation(unittest.TestCase): 164 | def setUp(self) -> None: 165 | pass 166 | 167 | def tearDown(self) -> None: 168 | pass 169 | 170 | @unittest.skip("Not yet implemented") 171 | def test__init__(self) -> None: 172 | pass 173 | 174 | @unittest.skip("Not yet implemented") 175 | def test_active(self) -> None: 176 | pass 177 | 178 | @unittest.skip("Not yet implemented") 179 | def test_connect(self) -> None: 180 | pass 181 | 182 | @unittest.skip("Not yet implemented") 183 | def test_disconnect(self) -> None: 184 | pass 185 | 186 | @unittest.skip("Not yet implemented") 187 | def test_isconnected(self) -> None: 188 | pass 189 | 190 | @unittest.skip("Not yet implemented") 191 | def test_scan(self) -> None: 192 | pass 193 | 194 | @unittest.skip("Not yet implemented") 195 | def test_status(self) -> None: 196 | pass 197 | 198 | @unittest.skip("Not yet implemented") 199 | def test_ifconfig(self) -> None: 200 | pass 201 | 202 | @unittest.skip("Not yet implemented") 203 | def test_config(self) -> None: 204 | pass 205 | 206 | @unittest.skip("Not yet implemented") 207 | def test_connected(self) -> None: 208 | pass 209 | 210 | 211 | class TestClient(unittest.TestCase): 212 | def setUp(self) -> None: 213 | pass 214 | 215 | def tearDown(self) -> None: 216 | pass 217 | 218 | def test__init__(self) -> None: 219 | ap = Client() 220 | 221 | self.assertIsNone(ap._essid) 222 | self.assertIsNone(ap._authmode) 223 | self.assertIsNone(ap._password) 224 | self.assertIsNone(ap._channel) 225 | self.assertFalse(ap._active) 226 | 227 | @unittest.skip("Not yet implemented") 228 | def test_ifconfig(self) -> None: 229 | pass 230 | 231 | @unittest.skip("Not yet implemented") 232 | def test_config(self) -> None: 233 | pass 234 | 235 | 236 | class TestWLAN(unittest.TestCase): 237 | def setUp(self) -> None: 238 | pass 239 | 240 | def tearDown(self) -> None: 241 | pass 242 | 243 | @unittest.skip("Not yet implemented") 244 | def test__init__(self) -> None: 245 | pass 246 | 247 | 248 | if __name__ == '__main__': 249 | unittest.main() 250 | -------------------------------------------------------------------------------- /simulation/tests/test_path_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Path Helper""" 5 | 6 | from pathlib import Path 7 | import unittest 8 | from unittest.mock import patch 9 | 10 | # custom imports 11 | from path_helper import PathHelper 12 | 13 | 14 | class TestPathHelper(unittest.TestCase): 15 | def setUp(self) -> None: 16 | pass 17 | 18 | def tearDown(self) -> None: 19 | pass 20 | 21 | def test_exists(self) -> None: 22 | """Test existance of file at given path""" 23 | with patch.object(Path, 'exists') as mock_exists: 24 | # mock file existance 25 | mock_exists.return_value = True 26 | result = PathHelper.exists(path='/some/path') 27 | self.assertTrue(result) 28 | 29 | # mock file unexistance 30 | mock_exists.return_value = False 31 | result = PathHelper.exists(path='/some/path') 32 | self.assertFalse(result) 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /simulation/tests/test_pin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Machine Pin""" 5 | 6 | from nose2.tools import params 7 | import unittest 8 | 9 | from typing import Any 10 | 11 | # custom imports 12 | from machine import Pin 13 | 14 | 15 | class TestPin(unittest.TestCase): 16 | def setUp(self) -> None: 17 | pass 18 | 19 | def tearDown(self) -> None: 20 | pass 21 | 22 | def test__init__(self) -> None: 23 | pin = Pin(pin=12, mode=Pin.IN) 24 | 25 | self.assertEqual(pin._pin, 12) 26 | self.assertEqual(pin._mode, 1) 27 | self.assertEqual(pin._value, 0) 28 | 29 | pin = Pin(pin=34, mode=Pin.OUT) 30 | 31 | self.assertEqual(pin._pin, 34) 32 | self.assertEqual(pin._mode, 2) 33 | self.assertEqual(pin._value, 0) 34 | 35 | @params( 36 | (None, False), # new pin state, expectation 37 | (True, True), 38 | (False, False), 39 | (1, True), 40 | (0, False), 41 | ('asdf', True), 42 | ('', False), 43 | ) 44 | def test_value_output_pin(self, new_state: Any, expectation: bool) -> None: 45 | """ 46 | Test setting and getting the output pin state 47 | 48 | :param new_state: New desired pin state 49 | :type new_state: Any 50 | :param expectation: Expected pin state after change 51 | :type expectation: bool 52 | """ 53 | pin = Pin(pin=12, mode=Pin.OUT) 54 | 55 | # set new pin state 56 | pin.value(new_state) 57 | self.assertEqual(pin.value(), expectation) 58 | 59 | @params( 60 | (None), 61 | (True), 62 | (False), 63 | (1), 64 | (0), 65 | ('asdf'), 66 | (''), 67 | ) 68 | def test_value_input_pin(self, new_state: Any) -> None: 69 | """ 70 | Test setting and getting the input pin state 71 | 72 | :param new_state: New desired pin state 73 | :type new_state: Any 74 | """ 75 | pin = Pin(pin=12, mode=Pin.IN) 76 | 77 | # check default pin state 78 | self.assertEqual(pin.value(), False) 79 | 80 | # set new pin state 81 | pin.value(new_state) 82 | self.assertEqual(pin.value(), False) 83 | 84 | def test_on_off(self) -> None: 85 | """Test setting pin to "0" or "1" output level""" 86 | # input pins can not be changed 87 | input_pin = Pin(pin=12, mode=Pin.IN) 88 | 89 | # check default pin state 90 | self.assertEqual(input_pin.value(), False) 91 | 92 | # set new pin state 93 | input_pin.on() 94 | self.assertEqual(input_pin.value(), False) 95 | 96 | # set new pin state 97 | input_pin.off() 98 | self.assertEqual(input_pin.value(), False) 99 | 100 | input_pin.on() 101 | self.assertEqual(input_pin.value(), False) 102 | 103 | # output pins can be changed 104 | output_pin = Pin(pin=12, mode=Pin.OUT) 105 | 106 | # check default pin state 107 | self.assertEqual(output_pin.value(), False) 108 | 109 | # set new pin state 110 | output_pin.on() 111 | self.assertEqual(output_pin.value(), True) 112 | 113 | # set new pin state 114 | output_pin.off() 115 | self.assertEqual(output_pin.value(), False) 116 | 117 | # set new pin state 118 | output_pin.on() 119 | self.assertEqual(output_pin.value(), True) 120 | 121 | 122 | if __name__ == '__main__': 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /simulation/tests/test_rtc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Machine RTC""" 5 | 6 | from datetime import datetime 7 | import unittest 8 | import time 9 | 10 | # custom imports 11 | from machine import RTC 12 | 13 | 14 | class TestRTC(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.rtc = RTC() 17 | 18 | def tearDown(self) -> None: 19 | pass 20 | 21 | def test__init(self) -> None: 22 | """Test initial values""" 23 | time_tuple = self.rtc._time_tuple 24 | self.assertIsInstance(time_tuple, tuple) 25 | self.assertTrue(all(isinstance(ele, int) for ele in time_tuple)) 26 | self.assertEqual(time_tuple, (2000, 1, 1, 0, 0, 0, 0, 0)) 27 | 28 | @unittest.skip("Failing on CI") 29 | def test_init(self) -> None: 30 | """Test initialization of RTC""" 31 | timezone = 0 32 | tm = time.localtime() 33 | # now = datetime.fromtimestamp(time.mktime(tm)) 34 | now = datetime.fromtimestamp(time.time()) 35 | 36 | self.rtc.init( 37 | (tm[0], tm[1], tm[2], tm[3], tm[4], tm[5], 0, timezone) 38 | ) 39 | 40 | time_tuple = self.rtc._time_tuple 41 | self.assertEqual(time_tuple, 42 | (now.year, now.month, now.day, 43 | now.weekday(), 44 | now.hour, now.minute, now.second, 0)) 45 | 46 | @unittest.skip("Failing on CI") 47 | def test_datetime(self) -> None: 48 | """Test getting current datetime""" 49 | datetime = self.rtc.datetime() 50 | self.assertIsInstance(datetime, tuple) 51 | self.assertTrue(all(isinstance(ele, int) for ele in datetime)) 52 | self.assertEqual(datetime, (2000, 1, 1, 0, 0, 0, 0, 0)) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /simulation/tests/test_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of ClassName""" 5 | 6 | from nose2.tools import params 7 | from typing import Union 8 | import unittest 9 | 10 | # custom imports 11 | # from xxx import ClassName 12 | 13 | 14 | class TestTemplate(unittest.TestCase): 15 | def setUp(self) -> None: 16 | pass 17 | 18 | def tearDown(self) -> None: 19 | pass 20 | 21 | @params( 22 | (None), 23 | ('Sauerkraut') 24 | ) 25 | def test_asdf(self, var: Union[None, str]) -> None: 26 | """ 27 | Test asdf function 28 | 29 | :param var: Variable description 30 | :type var: Union[None, str] 31 | """ 32 | pass 33 | 34 | @unittest.skip("Not yet implemented") 35 | def test_qwertz(self) -> None: 36 | """Test quertz function""" 37 | pass 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /simulation/tests/test_time_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of TimeHelper""" 5 | 6 | import time 7 | import unittest 8 | from unittest.mock import Mock 9 | 10 | # custom imports 11 | from time_helper import TimeHelper 12 | 13 | 14 | class TestTimeHelper(unittest.TestCase): 15 | def setUp(self) -> None: 16 | time.localtime = Mock( 17 | # (year, m, day, h, min, sec, weekday, yearday) 18 | return_value=(2022, 11, 3, 18, 15, 31, 3, 307) 19 | # 0, 1, 2, 3, 4, 5, 6, 7 20 | ) 21 | self.th = TimeHelper() 22 | self.th.sync_time() 23 | 24 | def tearDown(self) -> None: 25 | pass 26 | 27 | @unittest.skip("Not yet implemented") 28 | def test_sync_time(self) -> None: 29 | """Test sync_time""" 30 | pass 31 | 32 | def test_timezone(self) -> None: 33 | """Test getting current timezone""" 34 | self.assertEqual(self.th.timezone, 1) 35 | 36 | self.th.timezone = 2 37 | self.assertEqual(self.th.timezone, 2) 38 | 39 | def test_year(self) -> None: 40 | """Test getting current year""" 41 | self.assertEqual(self.th.year, 2022) 42 | 43 | def test_month(self) -> None: 44 | """Test getting current month""" 45 | self.assertEqual(self.th.month, 11) 46 | 47 | def test_day(self) -> None: 48 | """Test getting current day""" 49 | self.assertEqual(self.th.day, 3) 50 | 51 | def test_weekday(self) -> None: 52 | """Test getting current weekday""" 53 | self.assertEqual(self.th.weekday, 3) 54 | 55 | def test_hour(self) -> None: 56 | """Test getting current hour""" 57 | self.assertEqual(self.th.hour, 18 + self.th.timezone) 58 | 59 | def test_minute(self) -> None: 60 | """Test getting current minute""" 61 | self.assertEqual(self.th.minute, 15) 62 | 63 | def test_second(self) -> None: 64 | """Test getting current second""" 65 | self.assertEqual(self.th.second, 31) 66 | 67 | def test_current_timestamp(self) -> None: 68 | """Test getting current timestamp""" 69 | self.assertEqual(self.th.current_timestamp, 70 | (2022, 11, 3, 18, 15, 31, 3, 307)) 71 | 72 | def test_current_timestamp_iso8601(self) -> None: 73 | """Test getting current timestamp in ISO8601 format""" 74 | self.assertEqual(self.th.current_timestamp_iso8601, 75 | '18:15:31 2022-11-03') 76 | 77 | def test_current_timestamp_human(self) -> None: 78 | """Test getting current timestamp in human readable ISO8601 format""" 79 | self.assertEqual(self.th.current_timestamp_human, 80 | '18:15:31 2022-11-03') 81 | 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /simulation/tests/test_timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of Machine Timer""" 5 | 6 | import time 7 | import unittest 8 | 9 | # custom imports 10 | import machine 11 | 12 | 13 | class TestTimer(unittest.TestCase): 14 | def setUp(self) -> None: 15 | self.my_var = 0 16 | 17 | def tearDown(self) -> None: 18 | pass 19 | 20 | def increment_var(self, tim: machine.Timer = None) -> None: 21 | """ 22 | Internal test function called by Timer callback to increment variable 23 | 24 | :param tim: The MicroPython compliant Timer argument 25 | :type tim: machine.Timer 26 | """ 27 | self.my_var += 1 28 | 29 | def test__init__(self) -> None: 30 | """Test init of Timer""" 31 | this_timer = machine.Timer(-1) 32 | self.assertIsNone(this_timer._timer) 33 | 34 | def test_init_oneshot(self) -> None: 35 | """Test initializing timer in one shot mode""" 36 | self.assertEqual(self.my_var, 0) 37 | 38 | this_timer = machine.Timer(-1) 39 | cb_period = 1000 # milliseconds 40 | 41 | this_timer.init(mode=machine.Timer.ONE_SHOT, 42 | period=cb_period, 43 | callback=self.increment_var) 44 | 45 | # give the timer enough space to run and perform the callback call 46 | time.sleep((cb_period / 1000) * 2) 47 | 48 | self.assertEqual(self.my_var, 1) 49 | 50 | def test_init_periodic(self) -> None: 51 | """Test initializing timer in periodic mode""" 52 | self.assertEqual(self.my_var, 0) 53 | 54 | this_timer = machine.Timer(-1) 55 | cb_period = 1000 # milliseconds 56 | iterations = 10 # let timer execute n times 57 | 58 | this_timer.init(mode=machine.Timer.PERIODIC, 59 | period=cb_period, 60 | callback=self.increment_var) 61 | 62 | # give the timer enough space to run and perform the callback call 63 | time.sleep((cb_period / 1000) * iterations + 1) 64 | this_timer.deinit() 65 | 66 | self.assertGreaterEqual(self.my_var, iterations) 67 | 68 | def test_init_unsupported(self) -> None: 69 | """Test initializing timer with unsupported mode""" 70 | this_timer = machine.Timer(-1) 71 | cb_period = 1000 # milliseconds 72 | unsupported_mode = 3 73 | 74 | with self.assertRaises(machine.TimerError) as context: 75 | this_timer.init(mode=unsupported_mode, 76 | period=cb_period, 77 | callback=self.increment_var) 78 | 79 | self.assertEqual('Unsupported Timer mode: {}'.format(unsupported_mode), 80 | str(context.exception)) 81 | 82 | @unittest.skip("Covered by test_init_periodic") 83 | def test_deinit(self) -> None: 84 | pass 85 | 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /simulation/tests/test_wifi_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest of WiFi Helper""" 5 | 6 | from nose2.tools import params 7 | from typing import List, Union 8 | import unittest 9 | from unittest.mock import patch 10 | 11 | # custom imports 12 | from machine import machine 13 | from wifi_helper import network 14 | from wifi_helper import WifiHelper 15 | 16 | 17 | class TestWifiHelper(unittest.TestCase): 18 | # Set maximum size of the assertion error message when Unit Test fail 19 | maxDiff = None 20 | 21 | def setUp(self) -> None: 22 | self.wh = WifiHelper() 23 | 24 | # activate WiFi before usage 25 | self.wh.station.active(True) 26 | 27 | def tearDown(self) -> None: 28 | self.wh.station.active(False) 29 | self.wh.station.connected = False 30 | 31 | def test__init(self) -> None: 32 | """Test initial values if WifiHelper""" 33 | wh = WifiHelper() 34 | 35 | required_keys = [ 36 | 'ssid', 'bssid', 'channel', 'RSSI', 'authmode', 'hidden' 37 | ] 38 | self.assertIsInstance(wh._scan_info, list) 39 | self.assertEqual(len(wh._scan_info), len(required_keys)) 40 | self.assertTrue(all(name in required_keys for name in wh._scan_info)) 41 | 42 | self.assertIsInstance(wh._network_list, list) 43 | self.assertEqual(len(wh._network_list), 0) 44 | 45 | self.assertIsInstance(wh._station, network.WLAN) 46 | 47 | def test__do_connect_connected(self) -> None: 48 | """ 49 | Test _do_connect in case a SOFT_RESET occured and system is already 50 | connected to a network 51 | """ 52 | # mock SOFT_RESET 53 | with patch('machine.machine.reset_cause') as mock_reset_cause: 54 | mock_reset_cause.return_value = machine.SOFT_RESET 55 | # mock already connected 56 | with patch('wifi_helper.network.Station.isconnected', 57 | return_value=True): 58 | station = network.WLAN(network.STA_IF) 59 | result = WifiHelper._do_connect(station=station, 60 | ssid='qwertz', 61 | password=1234, 62 | timeout=10) 63 | self.assertEqual(result, True) 64 | 65 | @params( 66 | (10, 0.3), 67 | (10, 0.5), 68 | (10, 0.9) 69 | ) 70 | def test__do_connect_unconnected(self, timeout: int, ratio: int) -> None: 71 | """ 72 | Test _do_connect within a given timeout 73 | 74 | :param timeout: The timeout to wait for a successul connection 75 | :type timeout: int 76 | :param ratio: The ratio after which a connection is established 77 | :type ratio: int 78 | """ 79 | # mock unknown reset cause 80 | with patch('machine.machine.reset_cause') as mock_reset_cause: 81 | mock_reset_cause.return_value = machine.UNKNOWN_RESET 82 | # mock not yet connected and change to success on second call 83 | # mock unconnected status for n calls 84 | side_effect_unconnected = [False] * int(timeout * 10 * ratio) 85 | side_effect_connected = [True] * int(timeout * 10 * (1 - ratio)) 86 | side_effect = side_effect_unconnected + side_effect_connected 87 | with patch('wifi_helper.network.Station.isconnected', 88 | side_effect=side_effect): 89 | station = network.WLAN(network.STA_IF) 90 | 91 | with patch.object(station, 'disconnect') as mock_disconnect: 92 | result = WifiHelper._do_connect(station=station, 93 | ssid='qwertz', 94 | password=1234, 95 | timeout=timeout) 96 | 97 | mock_disconnect.assert_called_once() 98 | self.assertEqual(result, True) 99 | 100 | @params( 101 | (1), 102 | (10), 103 | (20) 104 | ) 105 | def test__do_connect_timeout(self, timeout: int) -> None: 106 | """ 107 | Test _do_connect without a successful connection within a given timeout 108 | 109 | The device sleeps for 100ms after each connection check call, it is 110 | called 100 times at a timeout of 10 sec. 111 | 112 | Let connection timeout trigger and fail the connection 113 | 114 | :param timeout: The timeout to wait for a successul connection 115 | :type timeout: int 116 | """ 117 | # mock unknown reset cause 118 | with patch('machine.machine.reset_cause') as mock_reset_cause: 119 | mock_reset_cause.return_value = machine.UNKNOWN_RESET 120 | # mock unconnected status for n+1 calls 121 | side_effect = [False] * (timeout * 10 + 1) 122 | 123 | with patch('wifi_helper.network.Station.isconnected', 124 | side_effect=side_effect): 125 | station = network.WLAN(network.STA_IF) 126 | 127 | with patch.object(station, 'disconnect') as mock_disconnect: 128 | result = WifiHelper._do_connect(station=station, 129 | ssid='qwertz', 130 | password=1234, 131 | timeout=timeout) 132 | 133 | mock_disconnect.assert_called_once() 134 | self.assertEqual(result, False) 135 | 136 | @params( 137 | # single network, 10 sec timeout, no reconnection 138 | ('qwertz', 'asdf', 10, False), 139 | ('qwertz', 'asdf', 10, True), 140 | ) 141 | def test_connect_already_connected(self, 142 | ssid: Union[None, List[str], str], 143 | password: Union[None, List[str], str], 144 | timeout: int, 145 | reconnect: bool) -> None: 146 | with patch('wifi_helper.network.Station.isconnected', 147 | return_value=True): 148 | with patch('wifi_helper.network.Station.config', 149 | return_value=ssid): 150 | if reconnect: 151 | # check call of disonnect 152 | with patch.object(network.Station, 153 | 'disconnect') as mock_disconnect: 154 | result = WifiHelper.connect(ssid=ssid, 155 | password=password, 156 | timeout=timeout, 157 | reconnect=reconnect) 158 | mock_disconnect.assert_called_once() 159 | else: 160 | # only if no reconnection is required, function will return 161 | result = WifiHelper.connect(ssid=ssid, 162 | password=password, 163 | timeout=timeout, 164 | reconnect=reconnect) 165 | self.assertEqual(result, True) 166 | 167 | def test_isconnected(self) -> None: 168 | is_connected = self.wh.isconnected 169 | self.assertFalse(is_connected) 170 | 171 | with patch('wifi_helper.network.Station.isconnected', 172 | return_value=True): 173 | is_connected = self.wh.isconnected 174 | self.assertTrue(is_connected) 175 | 176 | def test_station(self) -> None: 177 | station = self.wh.station 178 | 179 | self.assertIsInstance(station, network.WLAN) 180 | 181 | @params( 182 | ('qwertz', '1234', 9, 10) 183 | ) 184 | def test_create_ap(self, 185 | ssid: str, 186 | password: str, 187 | channel: int, 188 | timeout: int) -> None: 189 | """ 190 | Test creation of an AccessPoint 191 | 192 | :param ssid: The ssid 193 | :type ssid: str 194 | :param password: The password 195 | :type password: str 196 | :param channel: The channel 197 | :type channel: int 198 | :param timeout: The timeout 199 | :type timeout: int 200 | """ 201 | pass 202 | """ 203 | client = network.WLAN(network.AP_IF) 204 | self.assertTrue(client.dummy()) 205 | 206 | client.config(essid=ssid) 207 | print(client._essid) 208 | 209 | with patch.object(network.WLAN(2), 'dummy') as mock_dummy: 210 | client = network.WLAN(network.AP_IF) 211 | self.assertTrue(client.dummy()) 212 | 213 | mock_dummy.return_value = False 214 | self.assertFalse(client.dummy()) 215 | """ 216 | 217 | """ 218 | with patch.object(network.WLAN(2), 'dummy') as mock_essid: 219 | # result = WifiHelper.create_ap(ssid=ssid, 220 | # password=password, 221 | # channel=channel, 222 | # timeout=timeout) 223 | # 224 | # self.assertEqual(result, True) 225 | 226 | client = network.WLAN(network.AP_IF) 227 | self.assertTrue(client.dummy) 228 | 229 | # self.assertEqual(mock_essid._essid, ssid) 230 | # print(dir(mock_essid)) 231 | # print(mock_essid.return_value) 232 | """ 233 | 234 | def test_scan_info(self) -> None: 235 | expectation = [ 236 | 'ssid', 'bssid', 'channel', 'RSSI', 'authmode', 'hidden' 237 | ] 238 | 239 | result = self.wh.scan_info 240 | 241 | self.assertEqual(result, expectation) 242 | 243 | def test_auth_modes(self) -> None: 244 | expectation = { 245 | 0: "open", 246 | 1: "WEP", 247 | 2: "WPA-PSK", 248 | 3: "WPA2-PSK", 249 | 4: "WPA/WPA2-PSK" 250 | } 251 | 252 | result = self.wh.auth_modes 253 | 254 | self.assertEqual(result, expectation) 255 | 256 | @unittest.skip("Checked by test_networks") 257 | def test_scan_networks(self) -> None: 258 | pass 259 | 260 | def test_networks(self) -> None: 261 | # perform scan 262 | self.wh.scan_networks() 263 | 264 | result = self.wh.networks 265 | 266 | print(result) 267 | 268 | self.assertIsInstance(result, list) 269 | 270 | # check networks have been found 271 | if len(result): 272 | for net in result: 273 | self.assertEqual(self.isinstance_namedtuple(net), True) 274 | scan_info = self.wh.scan_info.copy() 275 | scan_info.append('quality') 276 | self.assertEqual(list(net._fields), scan_info) 277 | 278 | @unittest.skip("Not yet implemented") 279 | def test_get_wifi_networks_sorted(self) -> None: 280 | pass 281 | 282 | @params( 283 | (-96, 8), # dBm, percentage 284 | ) 285 | def test_dbm_to_quality(self, dBm: int, expectation: int) -> None: 286 | result = self.wh.dbm_to_quality(dBm=dBm) 287 | self.assertEqual(result, expectation) 288 | 289 | @params( 290 | (50, -75), # percentage, dBm 291 | ) 292 | def test_quality_to_dbm(self, quality: int, expectation: int) -> None: 293 | result = self.wh.quality_to_dbm(quality=quality) 294 | self.assertEqual(result, expectation) 295 | 296 | def test_ifconfig_client(self) -> None: 297 | # fake a connection to a network 298 | self.wh.station.connected = True 299 | 300 | result = self.wh.ifconfig_client 301 | self.assertEqual(self.isinstance_namedtuple(result), True) 302 | self.assertEqual(list(result._fields), 303 | ['ip', 'subnet', 'gateway', 'dns']) 304 | 305 | def test_ifconfig_ap(self) -> None: 306 | # fake a connection to a network 307 | self.wh.station.connected = True 308 | 309 | result = self.wh.ifconfig_client 310 | self.assertEqual(self.isinstance_namedtuple(result), True) 311 | self.assertEqual(list(result._fields), 312 | ['ip', 'subnet', 'gateway', 'dns']) 313 | 314 | def isinstance_namedtuple(self, obj: tuple) -> bool: 315 | return ( 316 | isinstance(obj, tuple) and 317 | hasattr(obj, '_asdict') and 318 | hasattr(obj, '_fields') 319 | ) 320 | 321 | 322 | if __name__ == '__main__': 323 | unittest.main() 324 | -------------------------------------------------------------------------------- /simulation/tests/unittest.cfg: -------------------------------------------------------------------------------- 1 | [unittest] 2 | # start-dir = tests 3 | # code-directories = src 4 | plugins = nose2.plugins.junitxml 5 | # plugins = nose2.plugins.attrib 6 | 7 | [coverage] 8 | always-on = True 9 | coverage = 10 | coverage-config = 11 | coverage-report = html 12 | 13 | [discovery] 14 | always-on = True 15 | 16 | [functions] 17 | always-on = True 18 | 19 | [output-buffer] 20 | # set "always-on" to False to see print content in console 21 | always-on = True 22 | stderr = False 23 | stdout = True 24 | 25 | [parameters] 26 | always-on = True 27 | 28 | [pretty-assert] 29 | always-on = True 30 | 31 | [test-result] 32 | always-on = True 33 | descriptions = True 34 | 35 | [junit-xml] 36 | always-on = True 37 | keep_restricted = False 38 | path = reports/test_results/nose2-junit.xml 39 | test_fullname = True -------------------------------------------------------------------------------- /simulation/tests/unittest_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Unittest helper 5 | 6 | Common modules used for unittesting 7 | """ 8 | 9 | import logging 10 | from pathlib import Path 11 | 12 | # custom imports 13 | from generic_helper import GenericHelper 14 | 15 | 16 | class UnitTestHelper(object): 17 | """docstring for UnitTestHelper""" 18 | def __init__(self, logger: logging.Logger = None, quiet: bool = False): 19 | if logger is None: 20 | logger_name = self.__class__.__name__ 21 | logger = GenericHelper.create_logger(logger_name=logger_name) 22 | self.logger = logger 23 | self.logger.disabled = quiet 24 | 25 | @staticmethod 26 | def get_current_path() -> Path: 27 | """ 28 | Get the path to this file. 29 | 30 | :returns: The path of this file 31 | :rtype: Path object 32 | """ 33 | here = Path(__file__).parent.resolve() 34 | 35 | return here 36 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # Static webpage files 2 | 3 | JavaScript and CSS files 4 | 5 | ## General 6 | 7 | This package and the simulation uses [bootstrap 5.1.3][ref-bootstrap] 8 | 9 | ## Create compressed version 10 | 11 | ### Why 12 | 13 | To speed up the data transfer between the device and a browser, many of them 14 | accept CSS and JS files as compressed `.gz` files. 15 | 16 | ### Additional informations 17 | 18 | In some cases the following warning can be seen in the web console of the page 19 | 20 | Layout rendering was forced before the page was fully loaded. 21 | 22 | This might be due to the reason of loading multiple (CSS) files within a given 23 | time frame. As a less powerfull device, such as an ESP32 or ESP8266, will have 24 | difficulties to provide these data in the expected time frame, the layout 25 | rendering might be forced, leading to a not as expected view. 26 | 27 | To avoid such issues, try to serve as less files as possible in the most 28 | compact way. To do so, use minified versions of CSS files (`*.min.css`) and 29 | combine multiple files into one. An even better performance is reached by 30 | compressing the file as shown onwards. 31 | 32 | To minify custom CSS files, search online for a CSS minifier or use 33 | [this one][ref-css-minifier] 34 | 35 | ### How to 36 | 37 | To compress a file use `gzip` instead of `tar`. `tar` seems to break something 38 | in the compressed file. As a result the style might not be as with the non 39 | compressed version. 40 | 41 | This example shows how to compress `bootstrap.min.css` to a new file called 42 | `bootstrap.min.css.gz` 43 | 44 | ```bash 45 | cd css 46 | 47 | gzip -r bootstrap.min.css -c > bootstrap.min.css.gz 48 | ``` 49 | 50 | 94 | 95 | 96 | [ref-bootstrap]: https://getbootstrap.com/docs/5.1/getting-started/download/ 97 | [ref-css-minifier]: https://www.toptal.com/developers/cssminifier/ 98 | [ref-stackoverflow-sed]: https://stackoverflow.com/questions/5410757/how-to-delete-from-a-text-file-all-lines-that-contain-a-specific-string 99 | -------------------------------------------------------------------------------- /static/css/bootstrap.min.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/static/css/bootstrap.min.css.gz -------------------------------------------------------------------------------- /static/css/list-groups.css: -------------------------------------------------------------------------------- 1 | .b-example-divider { 2 | height: 3rem; 3 | background-color: rgba(0, 0, 0, .1); 4 | border: solid rgba(0, 0, 0, .15); 5 | border-width: 1px 0; 6 | box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); 7 | } 8 | 9 | .bi { 10 | vertical-align: -.125em; 11 | fill: currentColor; 12 | } 13 | 14 | .opacity-50 { opacity: .5; } 15 | .opacity-75 { opacity: .75; } 16 | 17 | .list-group { 18 | width: auto; 19 | max-width: 460px; 20 | margin: 4rem auto; 21 | } 22 | 23 | .form-check-input:checked + .form-checked-content { 24 | opacity: .5; 25 | } 26 | 27 | .form-check-input-placeholder { 28 | pointer-events: none; 29 | border-style: dashed; 30 | } 31 | [contenteditable]:focus { 32 | outline: 0; 33 | } 34 | 35 | .list-group-checkable { 36 | display: grid; 37 | gap: .5rem; 38 | border: 0; 39 | } 40 | .list-group-checkable .list-group-item { 41 | cursor: pointer; 42 | border-radius: .5rem; 43 | } 44 | .list-group-item-check { 45 | position: absolute; 46 | clip: rect(0, 0, 0, 0); 47 | pointer-events: none; 48 | } 49 | .list-group-item-check:hover + .list-group-item { 50 | background-color: var(--bs-light); 51 | } 52 | .list-group-item-check:checked + .list-group-item { 53 | color: #fff; 54 | background-color: var(--bs-blue); 55 | } 56 | .list-group-item-check[disabled] + .list-group-item, 57 | .list-group-item-check:disabled + .list-group-item { 58 | pointer-events: none; 59 | filter: none; 60 | opacity: .5; 61 | } 62 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | .top-pad { 6 | padding: 40px 15px; 7 | text-align: center; 8 | } -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position:fixed; 3 | top:0; 4 | left:0; 5 | right:0; 6 | bottom:0; 7 | background-color: gray; 8 | color: white; 9 | opacity: 1; 10 | transition: 0.5s; 11 | visibility: visible; 12 | } 13 | .overlay.hidden { 14 | opacity: 0; 15 | visibility: hidden; 16 | } 17 | .loader { 18 | position: absolute; 19 | left: 50%; 20 | top: 50%; 21 | z-index: 1; 22 | width: 120px; 23 | height: 120px; 24 | margin: -76px 0 0 -76px; 25 | border: 16px solid #f3f3f3; 26 | border-radius: 50%; 27 | border-top: 16px solid #3498db; 28 | -webkit-animation: spin 2s linear infinite; 29 | animation: spin 2s linear infinite; 30 | } 31 | @-webkit-keyframes spin { 32 | 0% { -webkit-transform: rotate(0deg); } 33 | 100% { -webkit-transform: rotate(360deg); } 34 | } 35 | @keyframes spin { 36 | 0% { transform: rotate(0deg); } 37 | 100% { transform: rotate(360deg); } 38 | } 39 | .animate-bottom { 40 | position: relative; 41 | -webkit-animation-name: animatebottom; 42 | -webkit-animation-duration: 1s; 43 | animation-name: animatebottom; 44 | animation-duration: 1s 45 | } 46 | @-webkit-keyframes animatebottom { 47 | from { bottom:-100px; opacity:0 } 48 | to { bottom:0px; opacity:1 } 49 | } 50 | @keyframes animatebottom { 51 | from{ bottom:-100px; opacity:0 } 52 | to{ bottom:0px; opacity:1 } 53 | } 54 | 55 | body { 56 | padding:50px 80px; 57 | font-family:"Lucida Grande","bitstream vera sans","trebuchet ms",sans-serif,verdana; 58 | } 59 | 60 | /* get rid of those system borders being generated for A tags */ 61 | a:active { 62 | outline:none; 63 | } 64 | 65 | :focus { 66 | -moz-outline-style:none; 67 | } 68 | 69 | input[type=text], input[type=password], select { 70 | width: 200px; 71 | padding: 12px 20px; 72 | margin: 8px 0; 73 | margin-left: auto; 74 | margin-right: auto; 75 | box-sizing: border-box; 76 | border: 3px solid #ccc; 77 | -webkit-transition: 0.5s; 78 | transition: 0.5s; 79 | outline: none; 80 | border-radius:10px; 81 | } 82 | 83 | input[type=text]:focus, input[type=password]:focus, select:focus { 84 | border: 3px solid #4CAF50; 85 | } 86 | 87 | /* 88 | input[type=button], input[type=submit], input[type=reset] { 89 | background-color: #4CAF50; 90 | border: none; 91 | color: white; 92 | padding: 16px 32px; 93 | text-decoration: none; 94 | margin: 8px 0px; 95 | margin-left: auto; 96 | margin-right: auto; 97 | cursor: pointer; 98 | width: 200px; 99 | height: 45px; 100 | border-radius:10px; 101 | display: inline-block; 102 | box-shadow: 0 9px #999; 103 | } 104 | */ 105 | 106 | table { 107 | font-family: arial, sans-serif; 108 | border-collapse: collapse; 109 | width: 100%; 110 | } 111 | 112 | td, th { 113 | border: 1px solid #dddddd; 114 | text-align: left; 115 | padding: 8px; 116 | } 117 | 118 | tr:nth-child(even) { 119 | background-color: #dddddd; 120 | } 121 | 122 | .center { 123 | margin: auto; 124 | width: 200px; 125 | 126 | margin: 0; 127 | position: absolute; 128 | top: 50%; 129 | left: 50%; 130 | -ms-transform: translate(-50%, -50%); 131 | transform: translate(-50%, -50%); 132 | } 133 | 134 | .button:hover {background-color: #3e8e41} 135 | 136 | .button:active { 137 | background-color: #3e8e41; 138 | box-shadow: 0 5px #666; 139 | transform: translateY(4px); 140 | } -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/static/favicon.ico -------------------------------------------------------------------------------- /static/js/bootstrap.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/static/js/bootstrap.min.js.gz -------------------------------------------------------------------------------- /static/js/toast.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * toast.js v0.1.0 3 | * (c) 2022 brainelectronics 4 | * Released under the MIT License 5 | */ 6 | function createToast(t,e,i,s){var n=document.createElement("div");n.classList.add("alert"),t&&n.classList.add(t),n.style.position="relative",n.style.width="100%",n.style.maxWidth="300px",n.style.minWidth="280px",n.style.borderRadius="5px",n.style.padding="15px",n.innerHTML=""+e+" "+i,document.getElementById("alert_container").appendChild(n),setTimeout(function(){n.style.opacity=1,n.style.visibility="visible"},1),s>0?setTimeout(function(){n.style.opacity=0,n.style.visibility="hidden",setTimeout(function(){n.remove()},350)},s):null==s&&setTimeout(function(){n.style.opacity=0,n.style.visibility="hidden",setTimeout(function(){n.remove()},350)},3e3)} -------------------------------------------------------------------------------- /static/js/toast.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainelectronics/Micropython-ESP-WiFi-Manager/f38cb22b63546f0b607389f4be34c5d3a42742be/static/js/toast.js.gz -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position:fixed; 3 | top:0; 4 | left:0; 5 | right:0; 6 | bottom:0; 7 | background-color: gray; 8 | color: white; 9 | opacity: 1; 10 | transition: 0.5s; 11 | visibility: visible; 12 | } 13 | .overlay.hidden { 14 | opacity: 0; 15 | visibility: hidden; 16 | } 17 | .loader { 18 | position: absolute; 19 | left: 50%; 20 | top: 50%; 21 | z-index: 1; 22 | width: 120px; 23 | height: 120px; 24 | margin: -76px 0 0 -76px; 25 | border: 16px solid #f3f3f3; 26 | border-radius: 50%; 27 | border-top: 16px solid #3498db; 28 | -webkit-animation: spin 2s linear infinite; 29 | animation: spin 2s linear infinite; 30 | } 31 | @-webkit-keyframes spin { 32 | 0% { -webkit-transform: rotate(0deg); } 33 | 100% { -webkit-transform: rotate(360deg); } 34 | } 35 | @keyframes spin { 36 | 0% { transform: rotate(0deg); } 37 | 100% { transform: rotate(360deg); } 38 | } 39 | .animate-bottom { 40 | position: relative; 41 | -webkit-animation-name: animatebottom; 42 | -webkit-animation-duration: 1s; 43 | animation-name: animatebottom; 44 | animation-duration: 1s 45 | } 46 | @-webkit-keyframes animatebottom { 47 | from { bottom:-100px; opacity:0 } 48 | to { bottom:0px; opacity:1 } 49 | } 50 | @keyframes animatebottom { 51 | from{ bottom:-100px; opacity:0 } 52 | to{ bottom:0px; opacity:1 } 53 | } 54 | 55 | body { 56 | padding:50px 80px; 57 | font-family:"Lucida Grande","bitstream vera sans","trebuchet ms",sans-serif,verdana; 58 | } 59 | 60 | /* get rid of those system borders being generated for A tags */ 61 | a:active { 62 | outline:none; 63 | } 64 | 65 | :focus { 66 | -moz-outline-style:none; 67 | } 68 | 69 | input[type=text], input[type=password], select { 70 | width: 200px; 71 | padding: 12px 20px; 72 | margin: 8px 0; 73 | margin-left: auto; 74 | margin-right: auto; 75 | box-sizing: border-box; 76 | border: 3px solid #ccc; 77 | -webkit-transition: 0.5s; 78 | transition: 0.5s; 79 | outline: none; 80 | border-radius:10px; 81 | } 82 | 83 | input[type=text]:focus, input[type=password]:focus, select:focus { 84 | border: 3px solid #4CAF50; 85 | } 86 | 87 | /* 88 | input[type=button], input[type=submit], input[type=reset] { 89 | background-color: #4CAF50; 90 | border: none; 91 | color: white; 92 | padding: 16px 32px; 93 | text-decoration: none; 94 | margin: 8px 0px; 95 | margin-left: auto; 96 | margin-right: auto; 97 | cursor: pointer; 98 | width: 200px; 99 | height: 45px; 100 | border-radius:10px; 101 | display: inline-block; 102 | box-shadow: 0 9px #999; 103 | } 104 | */ 105 | 106 | table { 107 | font-family: arial, sans-serif; 108 | border-collapse: collapse; 109 | width: 100%; 110 | } 111 | 112 | td, th { 113 | border: 1px solid #dddddd; 114 | text-align: left; 115 | padding: 8px; 116 | } 117 | 118 | tr:nth-child(even) { 119 | background-color: #dddddd; 120 | } 121 | 122 | .center { 123 | margin: auto; 124 | width: 200px; 125 | 126 | margin: 0; 127 | position: absolute; 128 | top: 50%; 129 | left: 50%; 130 | -ms-transform: translate(-50%, -50%); 131 | transform: translate(-50%, -50%); 132 | } 133 | 134 | .button:hover {background-color: #3e8e41} 135 | 136 | .button:active { 137 | background-color: #3e8e41; 138 | box-shadow: 0 5px #666; 139 | transform: translateY(4px); 140 | } -------------------------------------------------------------------------------- /templates/index.tpl: -------------------------------------------------------------------------------- 1 | {% args req, content %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Setup 10 | 11 | 12 | 19 | 20 | 21 |
22 |
23 |
24 | 27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /templates/remove.tpl: -------------------------------------------------------------------------------- 1 | {% args req, wifi_nets, button_mode %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Remove WiFi Network 10 | 11 | 12 | 16 | 23 | 34 | 37 | 38 | 39 |
40 |
41 |
42 | 43 | 75 |
76 | 77 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /templates/select.tpl: -------------------------------------------------------------------------------- 1 | {% args req, content %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Select WiFi 10 | 11 | 12 | 16 | 23 | 34 | 37 | 38 | 39 |
40 |
41 |
42 | 43 | 64 |
65 | 66 | 97 | 98 | -------------------------------------------------------------------------------- /templates/wifi_configs.tpl: -------------------------------------------------------------------------------- 1 | {% args req, wifi_nets %} 2 | 3 | 4 | 5 | 6 | 7 | Remove WiFi Network 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 | 33 |
34 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /templates/wifi_select_loader.tpl: -------------------------------------------------------------------------------- 1 | {% args req, wifi_nets %} 2 | 3 | 4 | 5 | 6 | 7 | Select WiFi 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 | 32 |
33 | 34 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /utemplate/compiled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | This file has been copied from micropython-lib 6 | 7 | https://github.com/pfalcon/utemplate/tree/181b974fb41c726f4f1a27d9d9fffac7407623f4/utemplate 8 | """ 9 | 10 | 11 | class Loader: 12 | 13 | def __init__(self, pkg, dir): 14 | if dir == ".": 15 | dir = "" 16 | else: 17 | dir = dir.replace("/", ".") + "." 18 | if pkg and pkg != "__main__": 19 | dir = pkg + "." + dir 20 | self.p = dir 21 | 22 | def load(self, name): 23 | name = name.replace(".", "_") 24 | return __import__(self.p + name, None, None, (name,)).render 25 | -------------------------------------------------------------------------------- /utemplate/recompile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | This file has been copied from micropython-lib 6 | 7 | https://github.com/pfalcon/utemplate/tree/181b974fb41c726f4f1a27d9d9fffac7407623f4/utemplate 8 | 9 | (c) 2014-2020 Paul Sokolovsky. MIT license. 10 | """ 11 | 12 | try: 13 | from uos import stat, remove 14 | except ImportError: 15 | from os import stat, remove 16 | from . import source 17 | 18 | 19 | class Loader(source.Loader): 20 | 21 | def load(self, name): 22 | o_path = self.pkg_path + self.compiled_path(name) 23 | i_path = self.pkg_path + self.dir + "/" + name 24 | try: 25 | o_stat = stat(o_path) 26 | i_stat = stat(i_path) 27 | if i_stat[8] > o_stat[8]: 28 | # input file is newer, remove output to force recompile 29 | remove(o_path) 30 | finally: 31 | return super().load(name) 32 | -------------------------------------------------------------------------------- /utemplate/source.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | This file has been copied from micropython-lib 6 | 7 | https://github.com/pfalcon/utemplate/tree/181b974fb41c726f4f1a27d9d9fffac7407623f4/utemplate 8 | 9 | (c) 2014-2019 Paul Sokolovsky. MIT license. 10 | """ 11 | 12 | from . import compiled 13 | 14 | 15 | class Compiler: 16 | 17 | START_CHAR = "{" 18 | STMNT = "%" 19 | STMNT_END = "%}" 20 | EXPR = "{" 21 | EXPR_END = "}}" 22 | 23 | def __init__(self, file_in, file_out, indent=0, seq=0, loader=None): 24 | self.file_in = file_in 25 | self.file_out = file_out 26 | self.loader = loader 27 | self.seq = seq 28 | self._indent = indent 29 | self.stack = [] 30 | self.in_literal = False 31 | self.flushed_header = False 32 | self.args = "*a, **d" 33 | 34 | def indent(self, adjust=0): 35 | if not self.flushed_header: 36 | self.flushed_header = True 37 | self.indent() 38 | self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args)) 39 | self.stack.append("def") 40 | self.file_out.write(" " * (len(self.stack) + self._indent + adjust)) 41 | 42 | def literal(self, s): 43 | if not s: 44 | return 45 | if not self.in_literal: 46 | self.indent() 47 | self.file_out.write('yield """') 48 | self.in_literal = True 49 | self.file_out.write(s.replace('"', '\\"')) 50 | 51 | def close_literal(self): 52 | if self.in_literal: 53 | self.file_out.write('"""\n') 54 | self.in_literal = False 55 | 56 | def render_expr(self, e): 57 | self.indent() 58 | self.file_out.write('yield str(' + e + ')\n') 59 | 60 | def parse_statement(self, stmt): 61 | tokens = stmt.split(None, 1) 62 | if tokens[0] == "args": 63 | if len(tokens) > 1: 64 | self.args = tokens[1] 65 | else: 66 | self.args = "" 67 | elif tokens[0] == "set": 68 | self.indent() 69 | self.file_out.write(stmt[3:].strip() + "\n") 70 | elif tokens[0] == "include": 71 | if not self.flushed_header: 72 | # If there was no other output, we still need a header now 73 | self.indent() 74 | tokens = tokens[1].split(None, 1) 75 | args = "" 76 | if len(tokens) > 1: 77 | args = tokens[1] 78 | if tokens[0][0] == "{": 79 | self.indent() 80 | # "1" as fromlist param is uPy hack 81 | self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2]) 82 | self.indent() 83 | self.file_out.write("yield from _.render(%s)\n" % args) 84 | return 85 | 86 | with self.loader.input_open(tokens[0][1:-1]) as inc: 87 | self.seq += 1 88 | c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq) 89 | inc_id = self.seq 90 | self.seq = c.compile() 91 | self.indent() 92 | self.file_out.write("yield from render%d(%s)\n" % (inc_id, args)) 93 | elif len(tokens) > 1: 94 | if tokens[0] == "elif": 95 | assert self.stack[-1] == "if" 96 | self.indent(-1) 97 | self.file_out.write(stmt + ":\n") 98 | else: 99 | self.indent() 100 | self.file_out.write(stmt + ":\n") 101 | self.stack.append(tokens[0]) 102 | else: 103 | if stmt.startswith("end"): 104 | assert self.stack[-1] == stmt[3:] 105 | self.stack.pop(-1) 106 | elif stmt == "else": 107 | assert self.stack[-1] == "if" 108 | self.indent(-1) 109 | self.file_out.write("else:\n") 110 | else: 111 | assert False 112 | 113 | def parse_line(self, l): 114 | while l: 115 | start = l.find(self.START_CHAR) 116 | if start == -1: 117 | self.literal(l) 118 | return 119 | self.literal(l[:start]) 120 | self.close_literal() 121 | sel = l[start + 1] 122 | #print("*%s=%s=" % (sel, EXPR)) 123 | if sel == self.STMNT: 124 | end = l.find(self.STMNT_END) 125 | assert end > 0 126 | stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip() 127 | self.parse_statement(stmt) 128 | end += len(self.STMNT_END) 129 | l = l[end:] 130 | if not self.in_literal and l == "\n": 131 | break 132 | elif sel == self.EXPR: 133 | # print("EXPR") 134 | end = l.find(self.EXPR_END) 135 | assert end > 0 136 | expr = l[start + len(self.START_CHAR + self.EXPR):end].strip() 137 | self.render_expr(expr) 138 | end += len(self.EXPR_END) 139 | l = l[end:] 140 | else: 141 | self.literal(l[start]) 142 | l = l[start + 1:] 143 | 144 | def header(self): 145 | self.file_out.write("# Autogenerated file\n") 146 | 147 | def compile(self): 148 | self.header() 149 | for l in self.file_in: 150 | self.parse_line(l) 151 | self.close_literal() 152 | return self.seq 153 | 154 | 155 | class Loader(compiled.Loader): 156 | 157 | def __init__(self, pkg, dir): 158 | super().__init__(pkg, dir) 159 | self.dir = dir 160 | if pkg == "__main__": 161 | # if pkg isn't really a package, don't bother to use it 162 | # it means we're running from "filesystem directory", not 163 | # from a package. 164 | pkg = None 165 | 166 | self.pkg_path = "" 167 | if pkg: 168 | p = __import__(pkg) 169 | if isinstance(p.__path__, str): 170 | # uPy 171 | self.pkg_path = p.__path__ 172 | else: 173 | # CPy 174 | self.pkg_path = p.__path__[0] 175 | self.pkg_path += "/" 176 | 177 | def input_open(self, template): 178 | path = self.pkg_path + self.dir + "/" + template 179 | return open(path) 180 | 181 | def compiled_path(self, template): 182 | return self.dir + "/" + template.replace(".", "_") + ".py" 183 | 184 | def load(self, name): 185 | try: 186 | return super().load(name) 187 | except (OSError, ImportError): 188 | pass 189 | 190 | compiled_path = self.pkg_path + self.compiled_path(name) 191 | 192 | f_in = self.input_open(name) 193 | f_out = open(compiled_path, "w") 194 | c = Compiler(f_in, f_out, loader=self) 195 | c.compile() 196 | f_in.close() 197 | f_out.close() 198 | return super().load(name) 199 | -------------------------------------------------------------------------------- /wifi_manager/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .wifi_manager import WiFiManager 5 | 6 | from .version import __version__ 7 | -------------------------------------------------------------------------------- /wifi_manager/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | __version_info__ = ("0", "0", "0") 5 | __version__ = '.'.join(__version_info__) 6 | --------------------------------------------------------------------------------