├── .github └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── README.md ├── _config.yml ├── assets │ ├── img │ │ ├── mac-open-right-click-warning.png │ │ ├── mac-open-right-click.png │ │ ├── mac-open-unsigned-warning.png │ │ ├── screenshot-macos.png │ │ ├── screenshot-ubuntu.png │ │ ├── screenshot-windows.png │ │ ├── screenshots-grey.png │ │ ├── screenshots-white.png │ │ ├── screenshots.xcf │ │ ├── terminal-recording.svg │ │ ├── windows-open-more-info.png │ │ └── windows-open.png │ ├── read-intel-gui.png │ └── save-hex-gui.png ├── contributing.md ├── development.md ├── index.md ├── installation.md └── usage.md ├── make.py ├── package ├── pyinstaller-cli.spec ├── pyinstaller-gui.spec └── svd_data.zip ├── poetry.lock ├── pyproject.toml ├── tests ├── test_cli.py ├── test_cmds.py ├── test_gui.py ├── test_programmer.py └── test_system_cli.py └── ubittool ├── __init__.py ├── __main__.py ├── cli.py ├── cmds.py ├── gui.py └── programmer.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Executables 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | pyinstaller-build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-20.04, macos-11, windows-2019] 10 | fail-fast: false 11 | name: ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v4 17 | with: 18 | # Only Python version known to work with GH Action runner macOS with 19 | # Tkinder & have a Windows build. More info in the test.yml file. 20 | python-version: "3.9.13" 21 | cache: 'pip' 22 | - name: Install Poetry 23 | run: python -m pip install poetry 24 | - name: Force poetry install without lock file when not macOS 25 | if: runner.os != 'macOS' 26 | run: rm poetry.lock 27 | - name: Install ubittool dependencies 28 | run: poetry install 29 | - name: Run PyInstaller 30 | run: poetry run python make.py build 31 | # GitHub actions upload artifact breaks permissions, so tar workaround 32 | # https://github.com/actions/upload-artifact/issues/38 33 | - name: Tar files 34 | run: tar -cvf ubittool-${{ runner.os }}.tar dist 35 | - uses: actions/upload-artifact@v1 36 | with: 37 | name: ubittool-${{ runner.os }} 38 | path: ubittool-${{ runner.os }}.tar 39 | # Run the tests as the last step, we might still want the installed to be 40 | # uploaded, but we should catch issues runnning the tests, 41 | # specifically the GitHub Python builds with tkinker in macOS 42 | - name: Run tests 43 | if: runner.os != 'Linux' 44 | run: poetry run python make.py check 45 | - name: Run tests (Linux) 46 | if: runner.os == 'Linux' 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y libxkbcommon-x11-0 xvfb 50 | xvfb-run poetry run python make.py check 51 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 14 * * 5" 8 | 9 | jobs: 10 | analyze: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | security-events: write 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | language: ["python"] 18 | name: Analyze ${{ matrix.language }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v1 25 | with: 26 | languages: ${{ matrix.language }} 27 | 28 | - name: Perform CodeQL Analysis 29 | uses: github/codeql-action/analyze@v1 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-tests: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-20.04, macos-11, windows-2019] 10 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 11 | # There are issues with Tkinter & Python in the GH Actions environment 12 | # So we control which exact minor versions are used, which are known to 13 | # work, but unfortunately don't have a release for the Windows runner 14 | # The latest Python 3.7 to 3.10 minor releases have this tkinter error: 15 | # RuntimeError: tk.h version (8.5) doesn't match libtk.a version (8.6) 16 | # https://github.com/actions/setup-python/issues/649 17 | exclude: 18 | - os: macos-11 19 | include: 20 | - os: macos-11 21 | python-version: "3.7.15" 22 | - os: macos-11 23 | python-version: "3.8.15" 24 | - os: macos-11 25 | python-version: "3.9.15" 26 | - os: macos-11 27 | python-version: "3.10.10" 28 | - os: macos-11 29 | python-version: "3.11" 30 | fail-fast: false 31 | name: Py ${{ matrix.python-version }} - ${{ matrix.os }} 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python ${{ matrix.python-version }} 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | cache: 'pip' 40 | - name: Install Poetry 41 | run: python -m pip install poetry 42 | - name: Install ubittool dependencies 43 | run: poetry install --verbose 44 | - name: Prepare Ubuntu xvfb 45 | if: runner.os == 'Linux' 46 | run: | 47 | sudo apt-get update 48 | sudo apt-get install -y libxkbcommon-x11-0 xvfb 49 | - name: Run tests (Ubuntu) 50 | if: runner.os == 'Linux' 51 | run: xvfb-run poetry run python make.py check 52 | - name: Run tests 53 | if: runner.os != 'Linux' 54 | run: poetry run python make.py check 55 | - name: Upload coverage to Codecov 56 | uses: codecov/codecov-action@v3 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #################### 2 | # Project Specific # 3 | #################### 4 | ignoreme/* 5 | 6 | ######## 7 | # IDEs # 8 | ######## 9 | .vscode/* 10 | 11 | ################### 12 | # Python Specific # 13 | ################### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | env/ 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # dotenv 69 | .env 70 | 71 | # virtualenv 72 | .venv/ 73 | venv/ 74 | ENV/ 75 | 76 | ##################### 77 | # OS Specific files # 78 | ##################### 79 | 80 | # Windows image file caches 81 | Thumbs.db 82 | ehthumbs.db 83 | 84 | # Folder config file 85 | Desktop.ini 86 | 87 | # Recycle Bin used on file shares 88 | $RECYCLE.BIN/ 89 | 90 | # Windows Installer files 91 | *.cab 92 | *.msi 93 | *.msm 94 | *.msp 95 | 96 | # Windows shortcuts 97 | *.lnk 98 | 99 | # Linux 100 | *~ 101 | 102 | # temporary files which can be created if a process still has a handle open of a deleted file 103 | .fuse_hidden* 104 | 105 | # KDE directory preferences 106 | .directory 107 | 108 | # Linux trash folder which might appear on any partition or disk 109 | .Trash-* 110 | 111 | # OS X 112 | .DS_Store 113 | .AppleDouble 114 | .LSOverride 115 | 116 | # Icon must end with two \r 117 | Icon 118 | 119 | # Thumbnails 120 | ._* 121 | 122 | # Files that might appear in the root of a volume 123 | .DocumentRevisions-V100 124 | .fseventsd 125 | .Spotlight-V100 126 | .TemporaryItems 127 | .Trashes 128 | .VolumeIcon.icns 129 | 130 | # Directories potentially created on remote AFP share 131 | .AppleDB 132 | .AppleDesktop 133 | Network Trash Folder 134 | Temporary Items 135 | .apdisk 136 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "stopOnEntry": true, 12 | "python": "${command:python.interpreterPath}", 13 | "program": "${file}", 14 | "cwd": "${workspaceFolder}", 15 | "env": {}, 16 | "envFile": "${workspaceFolder}/.env", 17 | "debugOptions": ["RedirectOutput"] 18 | }, 19 | { 20 | "name": "Python: Terminal (integrated)", 21 | "type": "python", 22 | "request": "launch", 23 | "stopOnEntry": true, 24 | "python": "${command:python.interpreterPath}", 25 | "program": "${file}", 26 | "cwd": "", 27 | "console": "integratedTerminal", 28 | "env": {}, 29 | "envFile": "${workspaceFolder}/.env", 30 | "debugOptions": [], 31 | "internalConsoleOptions": "neverOpen" 32 | }, 33 | { 34 | "name": "Python: Terminal (external)", 35 | "type": "python", 36 | "request": "launch", 37 | "stopOnEntry": true, 38 | "python": "${command:python.interpreterPath}", 39 | "program": "${file}", 40 | "cwd": "", 41 | "console": "externalTerminal", 42 | "env": {}, 43 | "envFile": "${workspaceFolder}/.env", 44 | "debugOptions": [], 45 | "internalConsoleOptions": "neverOpen" 46 | }, 47 | { 48 | "name": "Python: uBit CLI", 49 | "type": "python", 50 | "request": "launch", 51 | "stopOnEntry": false, 52 | "python": "${command:python.interpreterPath}", 53 | "program": "${workspaceFolder}/ubittool/cli.py", 54 | "args": [], 55 | "cwd": "${workspaceFolder}", 56 | "console": "integratedTerminal", 57 | "env": {}, 58 | "envFile": "${workspaceFolder}/.env", 59 | "debugOptions": [], 60 | "internalConsoleOptions": "neverOpen" 61 | }, 62 | { 63 | "name": "Python: uBit GUI", 64 | "type": "python", 65 | "request": "launch", 66 | "stopOnEntry": false, 67 | "python": "${command:python.interpreterPath}", 68 | "program": "${workspaceFolder}/ubittool/gui.py", 69 | "args": [], 70 | "cwd": "${workspaceFolder}", 71 | "console": "integratedTerminal", 72 | "env": {}, 73 | "envFile": "${workspaceFolder}/.env", 74 | "debugOptions": [], 75 | "internalConsoleOptions": "neverOpen" 76 | }, 77 | { 78 | "name": "Python: uBit CLI module", 79 | "type": "python", 80 | "request": "launch", 81 | "stopOnEntry": false, 82 | "python": "${command:python.interpreterPath}", 83 | "module": "ubittool", 84 | "args": [], 85 | "cwd": "${workspaceFolder}", 86 | "console": "integratedTerminal", 87 | "env": {}, 88 | "envFile": "${workspaceFolder}/.env", 89 | "debugOptions": [], 90 | "internalConsoleOptions": "neverOpen" 91 | }, 92 | { 93 | "name": "Python: uBit GUI module", 94 | "type": "python", 95 | "request": "launch", 96 | "stopOnEntry": false, 97 | "python": "${command:python.interpreterPath}", 98 | "module": "ubittool", 99 | "args": ["gui"], 100 | "cwd": "${workspaceFolder}", 101 | "console": "integratedTerminal", 102 | "env": {}, 103 | "envFile": "${workspaceFolder}/.env", 104 | "debugOptions": [], 105 | "internalConsoleOptions": "neverOpen" 106 | }, 107 | { 108 | "name": "Python: tests", 109 | "type": "python", 110 | "request": "launch", 111 | "stopOnEntry": false, 112 | "python": "${command:python.interpreterPath}", 113 | "program": "${workspaceFolder}/make.py", 114 | "args": ["test"], 115 | "cwd": "${workspaceFolder}", 116 | "console": "integratedTerminal", 117 | "env": {}, 118 | "envFile": "${workspaceFolder}/.env", 119 | "debugOptions": [], 120 | "internalConsoleOptions": "neverOpen" 121 | } 122 | ] 123 | } 124 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestEnabled": false, 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.nosetestsEnabled": false, 5 | "python.formatting.provider": "black", 6 | "editor.formatOnSave": true, 7 | "python.linting.enabled": true, 8 | "python.linting.lintOnSave": true, 9 | "python.linting.flake8Enabled": true, 10 | "python.linting.flake8Path": "flake8" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2023 Carlos Pereira Atencio 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uBitTool 2 | 3 | [![Code coverage](https://codecov.io/gh/carlosperate/ubittool/branch/master/graph/badge.svg)](https://codecov.io/gh/carlosperate/ubittool) 4 | [![CI: Tests](https://github.com/carlosperate/ubittool/actions/workflows/test.yml/badge.svg)](https://github.com/carlosperate/ubittool/actions/workflows/test.yml) 5 | [![CI: Build](https://github.com/carlosperate/ubittool/actions/workflows/build.yml/badge.svg)](https://github.com/carlosperate/ubittool/actions/workflows/build.yml) 6 | [![PyPI versions](https://img.shields.io/pypi/pyversions/ubittool.svg)](https://pypi.org/project/ubittool/) 7 | ![Supported Platforms](https://img.shields.io/badge/platform-Windows%20%7C%20macOs%20%7C%20Linux-blue) 8 | [![Code style Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 9 | [![PyPI - License](https://img.shields.io/pypi/l/ubittool.svg)](LICENSE) 10 | 11 | uBitTool is a command line and GUI application to interface with the micro:bit. 12 | 13 | It can: 14 | 15 | - Read the micro:bit flash contents 16 | - Extract user Python code from the micro:bit flash 17 | - Flash the micro:bit 18 | - Compare the contents of the micro:bit flash against a local hex file 19 | 20 | ![screenshots](https://www.embeddedlog.com/ubittool/assets/img/screenshots-white.png) 21 | 22 |

23 | terminal recording demo 24 |

25 | 26 | ## Docs 27 | 28 | The documentation is online at 29 | [https://carlosperate.github.io/ubittool/](https://carlosperate.github.io/ubittool/), 30 | and its source can be found in `docs` directory. 31 | 32 | ## Basic Introduction 33 | 34 | The easiest way to use uBitTool is via the application GUI. 35 | 36 | - Download one of the latest GUI executables for macOS or Windows from the 37 | [GitHub Releases Page](https://github.com/carlosperate/ubittool/releases). 38 | - Plug-in your micro:bit to the computer via USB 39 | - Open the GUI executable file 40 | - On the application menu click "nrf > Read Full Flash contents (Intel Hex)". 41 | - A full image of the micro:bit flash should now be displayed in the GUI :) 42 | 43 | For more information and instructions for other platforms please visit the 44 | [Documentation](https://carlosperate.github.io/ubittool/). 45 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # uBitTool Docs 2 | 3 | ## Set up local environment 4 | 5 | With Ruby v2.3: 6 | 7 | ``` 8 | gem install github-pages 9 | gem install jekyll-remote-theme 10 | ``` 11 | 12 | ## Serve 13 | 14 | ``` 15 | cd docs/ 16 | jekyll server --host=0.0.0.0 --watch -P 3232 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # General Jekyll options https://jekyllrb.com/docs/configuration/ 2 | exclude: 3 | - README.md 4 | - Gemfile 5 | - Gemfile.lock 6 | plugins: 7 | - jekyll-remote-theme 8 | 9 | # Theme and theme options 10 | remote_theme: carlosperate/jekyll-theme-rtd 11 | -------------------------------------------------------------------------------- /docs/assets/img/mac-open-right-click-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/mac-open-right-click-warning.png -------------------------------------------------------------------------------- /docs/assets/img/mac-open-right-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/mac-open-right-click.png -------------------------------------------------------------------------------- /docs/assets/img/mac-open-unsigned-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/mac-open-unsigned-warning.png -------------------------------------------------------------------------------- /docs/assets/img/screenshot-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshot-macos.png -------------------------------------------------------------------------------- /docs/assets/img/screenshot-ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshot-ubuntu.png -------------------------------------------------------------------------------- /docs/assets/img/screenshot-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshot-windows.png -------------------------------------------------------------------------------- /docs/assets/img/screenshots-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshots-grey.png -------------------------------------------------------------------------------- /docs/assets/img/screenshots-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshots-white.png -------------------------------------------------------------------------------- /docs/assets/img/screenshots.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/screenshots.xcf -------------------------------------------------------------------------------- /docs/assets/img/windows-open-more-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/windows-open-more-info.png -------------------------------------------------------------------------------- /docs/assets/img/windows-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/img/windows-open.png -------------------------------------------------------------------------------- /docs/assets/read-intel-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/read-intel-gui.png -------------------------------------------------------------------------------- /docs/assets/save-hex-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/docs/assets/save-hex-gui.png -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Contributing 4 | nav_order: 5 5 | --- 6 | 7 | # Contributing 8 | 9 | ## How To Contribute 10 | 11 | This part of the documentation has not yet been written. 12 | 13 | If you'd like to contribute to the documentation PRs are welcomed! 14 | 15 | ## Code Of Conduct 16 | 17 | WIP. 18 | 19 | ## Bugs 20 | 21 | Issue tracker: 22 | [https://github.com/carlosperate/ubittool/issues](https://github.com/carlosperate/ubittool/issues) 23 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Set Up A Development Environment 4 | nav_order: 4 5 | --- 6 | 7 | # Set Up A Development Environment 8 | 9 | ## Installing from Source 10 | 11 | This project uses Poetry. You can install it using their 12 | [installation instructions](https://poetry.eustace.io/docs/#installation). 13 | 14 | Then clone the repository and install the development dependencies using 15 | `poetry`: 16 | 17 | ``` 18 | $ git clone https://github.com/carlosperate/ubittool.git 19 | $ cd ubittool 20 | $ poetry install 21 | ``` 22 | 23 | If you prefer to only install the dependencies necessary to run the tool and 24 | skip all the development dependencies you can replace the last command with: 25 | 26 | ``` 27 | $ poetry install --no-dev 28 | ``` 29 | 30 | Then to run uBitTool: 31 | 32 | ``` 33 | $ poetry run ubit --help 34 | ``` 35 | 36 | ## make.py 37 | 38 | Rather than having a Makefile, which can be difficult to get running on 39 | Windows, there is a make.py file that can execute the type of commands that 40 | would normally go in a Makefile. 41 | 42 | To get an up-to-date overview of what commands are available you can run the 43 | `--help` flag: 44 | 45 | ``` 46 | $ python make.py --help 47 | Usage: python make.py [OPTIONS] COMMAND [ARGS]... 48 | 49 | Run make-like commands from a Python script instead of a MakeFile. 50 | 51 | No dependencies outside of what is on the Pipfile, so it works on all 52 | platforms without installing other stuff (e.g. Make on Windows). 53 | 54 | Options: 55 | --help Show this message and exit. 56 | 57 | Commands: 58 | build Build the CLI and GUI executables. 59 | check Run all the checkers and tests. 60 | clean Remove unnecessary files (like build outputs). 61 | linter Run Flake8 linter with all its plugins. 62 | test Run PyTests with the coverage plugin. 63 | ``` 64 | 65 | These docs will only cover the most important commands for development, but 66 | feel free to explore the other commands with the `--help` flag. 67 | 68 | ### Check 69 | 70 | Run all the checkers (`linter`, `test`, and `style`): 71 | 72 | ``` 73 | $ python make.py check 74 | ``` 75 | 76 | ### Build 77 | 78 | Builds the CLI and GUI executables using PyInstaller: 79 | 80 | ``` 81 | $ python make.py build 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | homepage: true 4 | nav_order: 1 5 | --- 6 | 7 | # Intro 8 | 9 | uBitTool is a command line and GUI application to interface with the micro:bit. 10 | 11 | It can: 12 | 13 | - Read the micro:bit flash contents 14 | - Extract user Python code from the micro:bit flash 15 | - Flash the micro:bit 16 | - Compare the contents of the micro:bit flash against a local hex file 17 | 18 | ![screenshots](assets/img/screenshots-grey.png) 19 | 20 | ![terminal recording demo](assets/img/terminal-recording.svg) 21 | 22 | These docs are still a WIP. 23 | 24 | ## Basic Introduction 25 | 26 | ### Basic Installation 27 | 28 | The easiest way to use uBitTool is to download ane execute the application GUI. 29 | 30 | Downloaded the latest version of the app for your Operating System from the 31 | [GitHub Releases page](https://github.com/carlosperate/ubittool/releases), 32 | you can then double click on the gui file. 33 | 34 | If you are using the command line application you can open the GUI with: 35 | 36 | ``` 37 | ubit gui 38 | ``` 39 | 40 | The command line help flag will provide information about how to use the 41 | uBitTool in the terminal: 42 | 43 | ``` 44 | $ ubit --help 45 | Usage: ubit [OPTIONS] COMMAND [ARGS]... 46 | 47 | uBitTool v0.8.0. 48 | 49 | CLI and GUI utility to read content from the micro:bit. 50 | 51 | Options: 52 | --help Show this message and exit. 53 | 54 | Commands: 55 | batch-flash Flash any micro:bit connected until Ctrl+C is pressed. 56 | compare Compare the micro:bit flash contents with a hex file. 57 | flash-compare Copy a hex file into the MICROBIT drive, read back the 58 | flash contents, and compare them with a hex file. 59 | gui Launch the GUI version of this app (has more options). 60 | read-code Extract the MicroPython code to a file or print it. 61 | read-flash Read the micro:bit flash contents into a hex file or 62 | console. 63 | read-flash-uicr Read the micro:bit flash and UICR into a hex file or 64 | console. 65 | ``` 66 | 67 | ## Run 68 | 69 | To see the available commands: 70 | 71 | ``` 72 | ubit --help 73 | ``` 74 | 75 | Or from this directory if you have clone and installed the repository: 76 | 77 | ``` 78 | python -m ubit --help 79 | ``` 80 | 81 | To retrieve the user Python code: 82 | 83 | ``` 84 | ubit read-code -f extracted_script.py 85 | ``` 86 | 87 | To read the entire flash contents: 88 | 89 | ``` 90 | ubit read-flash 91 | ``` 92 | 93 | To compare the flash contents with a hex file: 94 | 95 | ``` 96 | ubit compare-flash file-to-compare-against.hex 97 | ``` 98 | 99 | To run the GUI: 100 | 101 | ``` 102 | ubit gui 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: How To Install uBitTool 4 | nav_order: 2 5 | --- 6 | 7 | # How To Install uBitTool 8 | 9 | There are three ways to install uBitTool: 10 | 11 | - Download the executables (no installation required, they are ready to run) 12 | - Install as a Python 3 package from PyPI 13 | - Install the Python 3 package from source 14 | 15 | ## Executables 16 | 17 | The latest version of the executables can be downloaded from the 18 | [GitHub Releases page](https://github.com/carlosperate/ubittool/releases). 19 | 20 | Only macOS and Windows executables are currently build. If you are using Linux 21 | you can follow any of the other installation methods. 22 | 23 | There is no need to install these applications, they are self-contain 24 | executables that can be run simply by double clicking them. 25 | 26 | However, the applications have not been signed as that is a process that 27 | costs money, and this is a personal open source project. So, as unsigned 28 | applications your Operating System might show you a warning about this. 29 | 30 | ### macOS 31 | 32 | Download the latest version for `mac` from the 33 | [GitHub Releases page](https://github.com/carlosperate/ubittool/releases). 34 | 35 | If you double click on the executable in macOS you might see a warning like 36 | this one: 37 | ![Unsigned app warning](assets/img/mac-open-unsigned-warning.png) 38 | 39 | The first time you open the application you will have follow these steps: 40 | 41 | - Right click on the ubittool-gui.app file 42 | - Select Open 43 | ![Open with right click](assets/img/mac-open-right-click.png) 44 | - A different warning window now offers more options 45 | - Click "Open" and the app will now work 46 | ![Unsigned app warning](assets/img/mac-open-right-click-warning.png) 47 | 48 | This is only required the first time, any subsequent double clicks will 49 | instantly open uBitTool without these warnings. 50 | 51 | More information about these steps and why this is necessary can be found in 52 | the Apple support website: 53 | [https://support.apple.com/en-gb/guide/mac-help/mh40616/mac](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac) 54 | 55 | ### Windows 56 | 57 | - Download the latest version for `win32` from the 58 | [GitHub Releases page](https://github.com/carlosperate/ubittool/releases). 59 | - Double click on the .exe file 60 | - A warning will appear indicating that "Windows protected your PC". This is 61 | because the application is not signed, as explained in the into of the parent 62 | section. 63 | ![Unsigned app warning](assets/img/windows-open.png) 64 | - Click on the "More info" link 65 | - A button should appear to "Run anyway" 66 | ![Unsigned app warning](assets/img/windows-open-more-info.png) 67 | - Click the "Run anyway" button and the app should open 68 | 69 | ## Python Package 70 | 71 | This application is provided as a Python 3 (>=3.6) package. 72 | 73 | Using [pipx](https://pipxproject.github.io/pipx/) is not necessary, but highly 74 | encouraged, as it will automatically create a virtual environment, install 75 | uBitTool and add its executable to the system path. This way the command can 76 | be used from any terminal session without the need to manually activate a 77 | virtual environment. 78 | 79 | If you don't have `pipx` already installed, follow the 80 | [pipx installation instructions](https://pipxproject.github.io/pipx/installation/). 81 | 82 | Then: 83 | 84 | ``` 85 | $ pipx install ubittool 86 | ``` 87 | 88 | Alternatively, create a Python 3 (>=3.6) virtual environment and install the 89 | `ubittool` package inside: 90 | 91 | ``` 92 | $ pip install ubittool 93 | ``` 94 | 95 | You can pip install uBitTool without a virtual environment, but there are a lot 96 | of reason why that is not a good idea. A bit more info can be found 97 | [here](https://stackoverflow.com/a/41972262/775259). 98 | 99 | ## Installing from source 100 | 101 | For information about how to install uBitTool from source please consult the 102 | [uBitTool Development documentation](development.html). 103 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Usage 4 | nav_order: 3 5 | --- 6 | 7 | # How to use uBitTool 8 | 9 | ## GUI 10 | 11 | ### Read out the contents of a .hex file 12 | 13 | 1. From the **nrf** menu select **Read full flash contents (Intel Hex)** 14 | 15 | ![Read intel hex](./assets/read-intel-gui.png) 16 | 17 | A full image of the .hex file will appear in the GUI. 18 | 19 | 2. From the **File** menu, select **Save as** and choose the destination to 20 | save your file. Add .hex as the suffix. 21 | 22 | ![Save hex file](./assets/save-hex-gui.png) 23 | 24 | ## Command Line 25 | 26 | ### Read out the contents of a .hex file 27 | 28 | Use `read-flash` which will output the contents to the console. You can 29 | optionally specify a file with the `-f`/`--file_path` flag. 30 | 31 | ``` 32 | ubit read-flash -f ~/Downloads/microbit-hex.hex 33 | ``` 34 | -------------------------------------------------------------------------------- /make.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Run make-like commands from a Python script instead of a MakeFile. 4 | 5 | No dependencies outside of what is on the Pipfile, so it works on all platforms 6 | without installing other stuff (e.g. Make on Windows). 7 | """ 8 | from __future__ import print_function 9 | import os 10 | import sys 11 | import shutil 12 | import subprocess 13 | 14 | import click 15 | 16 | 17 | def _run_cli_cmd(cmd_list): 18 | """Run a shell command and return the error code. 19 | 20 | :param cmd_list: A list of strings that make up the command to execute. 21 | """ 22 | try: 23 | return subprocess.call(cmd_list) 24 | except Exception as e: 25 | print(str(e)) 26 | sys.exit(1) 27 | 28 | 29 | def _this_file_dir(): 30 | """:return: Path to this file directory.""" 31 | return os.path.dirname(os.path.realpath(__file__)) 32 | 33 | 34 | def _set_cwd(): 35 | """Set cwd to the path necessary for these commands to work correctly. 36 | 37 | All commands depend on this file folder being the current working 38 | directory. 39 | """ 40 | os.chdir(_this_file_dir()) 41 | 42 | 43 | def _rm_dir(dir_to_remove): 44 | """:param dir_to_remove: Directory to remove.""" 45 | if os.path.isdir(dir_to_remove): 46 | print("Removing directory: {}".format(dir_to_remove)) 47 | shutil.rmtree(dir_to_remove) 48 | else: 49 | print("Directory {} was not found.".format(dir_to_remove)) 50 | 51 | 52 | def _rm_folder_named(scan_path, folder_name): 53 | """Remove all folders named folder_name from the given directory tree. 54 | 55 | :param scan_path: Directory to scan for folders with specific name. 56 | """ 57 | for root, dirs, files in os.walk(scan_path, topdown=False): 58 | for name in dirs: 59 | if name == folder_name: 60 | _rm_dir(os.path.join(root, name)) 61 | 62 | 63 | def _rm_file(file_to_remove): 64 | """:param file_to_remove: File to remove.""" 65 | if os.path.isfile(file_to_remove): 66 | print("Removing file: {}".format(file_to_remove)) 67 | os.remove(file_to_remove) 68 | else: 69 | print("File {} was not found.".format(file_to_remove)) 70 | 71 | 72 | def _rm_file_extension(scan_path, file_extension): 73 | """Remove all files with an specific extension from a given directory. 74 | 75 | :param scan_path: Directory to scan for file removal. 76 | :param file_extension: File extension of the files to remove 77 | """ 78 | for root, dirs, files in os.walk(scan_path, topdown=False): 79 | for file_ in files: 80 | if file_.endswith(".{}".format(file_extension)): 81 | file_path = os.path.join(root, file_) 82 | _rm_file(file_path) 83 | 84 | 85 | @click.group(help=__doc__) 86 | def make(): 87 | """Click entry point.""" 88 | pass 89 | 90 | 91 | @make.command() 92 | def linter(): 93 | """Run Flake8 linter with all its plugins.""" 94 | _set_cwd() 95 | print("---------------") 96 | print("Running linter:") 97 | print("---------------") 98 | return_code = _run_cli_cmd(["flake8", "ubittool/", "tests/"]) 99 | if return_code != 0: 100 | sys.exit(return_code) 101 | print("All good :)") 102 | return return_code 103 | 104 | 105 | @make.command() 106 | def style(): 107 | """Run Black as a linter without automatic formatting.""" 108 | _set_cwd() 109 | print("----------------------") 110 | print("Running Style Checker:") 111 | print("----------------------") 112 | try: 113 | import black 114 | except ImportError: 115 | print("Black Python module not found, style check skipped.") 116 | return 0 117 | black_cmd = ["black", ".", "--check", "--diff"] 118 | return_code = _run_cli_cmd(black_cmd) 119 | if return_code != 0: 120 | sys.exit(return_code) 121 | return return_code 122 | 123 | 124 | @make.command() 125 | def test(): 126 | """Run PyTests with the coverage plugin.""" 127 | _set_cwd() 128 | # Only create an xml report in the CI for codecov SaaS to consume 129 | report = "--cov-report=xml" if os.getenv("CI") else "" 130 | return_code = _run_cli_cmd( 131 | [ 132 | sys.executable, 133 | "-m", 134 | "pytest", 135 | "-vv", 136 | "--cov=ubittool", 137 | report, 138 | "tests/", 139 | ] 140 | ) 141 | if return_code != 0: 142 | sys.exit(return_code) 143 | return 0 144 | 145 | 146 | @make.command() 147 | @click.pass_context 148 | def check(ctx): 149 | """Run all the checkers and tests.""" 150 | commands = [linter, test, style] 151 | for cmd in commands: 152 | ctx.invoke(cmd) 153 | return 0 154 | 155 | 156 | @make.command() 157 | @click.pass_context 158 | def build(ctx): 159 | """Build the CLI and GUI executables.""" 160 | ctx.invoke(clean) 161 | _set_cwd() 162 | print("------------------------") 163 | print("Building CLI executable:") 164 | print("------------------------") 165 | rtn_code = _run_cli_cmd(["pyinstaller", "package/pyinstaller-cli.spec"]) 166 | if rtn_code != 0: 167 | sys.exit(rtn_code) 168 | print("------------------------") 169 | print("Building GUI executable:") 170 | print("------------------------") 171 | rtn_code = _run_cli_cmd(["pyinstaller", "package/pyinstaller-gui.spec"]) 172 | if rtn_code != 0: 173 | sys.exit(rtn_code) 174 | return 0 175 | 176 | 177 | @make.command() 178 | def package(): 179 | """Build the Python Package.""" 180 | _set_cwd() 181 | print("------------------------") 182 | print("Building Python Package:") 183 | print("------------------------") 184 | rtn_code = _run_cli_cmd(["poetry", "build"]) 185 | if rtn_code != 0: 186 | sys.exit(rtn_code) 187 | return 0 188 | 189 | 190 | @make.command() 191 | @click.pass_context 192 | def publish_test(ctx): 193 | """Publish the Python Package to the TestPyPI repository.""" 194 | ctx.invoke(clean) 195 | ctx.invoke(package) 196 | _set_cwd() 197 | print("-----------------------------") 198 | print("Publish package to test PyPI:") 199 | print("-----------------------------") 200 | rtn_code = _run_cli_cmd( 201 | [ 202 | "poetry", 203 | "config", 204 | "repositories.testpypi", 205 | "https://test.pypi.org/legacy/", 206 | ] 207 | ) 208 | if rtn_code != 0: 209 | sys.exit(rtn_code) 210 | rtn_code = _run_cli_cmd(["poetry", "publish", "-r", "testpypi"]) 211 | if rtn_code != 0: 212 | sys.exit(rtn_code) 213 | return 0 214 | 215 | 216 | @make.command() 217 | @click.pass_context 218 | def publish(ctx): 219 | """Publish the Python Package to PyPI.""" 220 | ctx.invoke(clean) 221 | ctx.invoke(package) 222 | _set_cwd() 223 | print("-----------------------------") 224 | print("Publish package to test PyPI:") 225 | print("-----------------------------") 226 | rtn_code = _run_cli_cmd( 227 | [ 228 | "poetry", 229 | "config", 230 | "repositories.testpypi", 231 | "https://test.pypi.org/legacy/", 232 | ] 233 | ) 234 | if rtn_code != 0: 235 | sys.exit(rtn_code) 236 | rtn_code = _run_cli_cmd(["poetry", "publish"]) 237 | if rtn_code != 0: 238 | sys.exit(rtn_code) 239 | return 0 240 | 241 | 242 | @make.command() 243 | def clean(): 244 | """Remove unnecessary files (like build outputs).""" 245 | _set_cwd() 246 | print("---------") 247 | print("Cleaning:") 248 | print("---------") 249 | folders_to_remove = [ 250 | ".pytest_cache", 251 | "build", 252 | "dist", 253 | "ubittool.egg-info", 254 | "pip-wheel-metadata", 255 | ] 256 | files_to_remove = [".coverage", "coverage.xml"] 257 | for folder in folders_to_remove: 258 | _rm_dir(folder) 259 | for f in files_to_remove: 260 | _rm_file(f) 261 | 262 | _rm_folder_named(".", "__pycache__") 263 | _rm_file_extension(".", "pyc") 264 | return 0 265 | 266 | 267 | def main(): 268 | """Script entry point, launches click.""" 269 | make(prog_name="python make.py") 270 | return 0 271 | 272 | 273 | if __name__ == "__main__": 274 | sys.exit(main()) 275 | -------------------------------------------------------------------------------- /package/pyinstaller-cli.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # -*- mode: python -*- 3 | # PyInstaller additional datas and hidden import from: 4 | # https://github.com/pyocd/pyOCD/issues/1529#issuecomment-1758960044 5 | import os 6 | import pathlib 7 | 8 | from PyInstaller.utils.hooks import get_package_paths, collect_entry_point 9 | 10 | 11 | datas_probe, hiddenimports_probe = collect_entry_point('pyocd.probe') 12 | datas_rtos, hiddenimports_rtos = collect_entry_point('pyocd.rtos') 13 | datas = [ 14 | (get_package_paths('pyocd')[1], 'pyocd'), 15 | (get_package_paths('pylink')[1], 'pylink') 16 | ] 17 | 18 | # Right now we exclude cmsis_pack_manager, but could be needed in the future 19 | # datas.append((get_package_paths('cmsis_pack_manager')[1], 'cmsis_pack_manager')) 20 | excludes = ['cmsis_pack_manager'] 21 | 22 | # For the CLI version we don't want to include the gui command 23 | excludes.append('tkinter') 24 | 25 | 26 | a = Analysis(['../ubittool/cli.py'], 27 | pathex=['../'], 28 | binaries=None, 29 | datas=datas + datas_probe + datas_rtos, 30 | hiddenimports=hiddenimports_probe + hiddenimports_rtos, 31 | hookspath=[], 32 | runtime_hooks=[], 33 | excludes=excludes, 34 | win_no_prefer_redirects=False, 35 | win_private_assemblies=False, 36 | cipher=None) 37 | 38 | # There isn't a way to exclude files, so remove them from collected datas. 39 | # This removes the pyocd/target/builtin/target_xxxx.py files unless for Nordic. 40 | # And replaces the svd_data.zip file with a slimmer version manually modified. 41 | # Datas format: ("", "", 'DATA') 42 | modified_datas = [d for d in a.datas if d[0] != 'pyocd/debug/svd/svd_data.zip' and 43 | not (d[0].startswith("pyocd/target/builtin/target_") and not d[0].startswith("pyocd/target/builtin/target_nRF"))] 44 | 45 | modified_datas.append(( 46 | 'pyocd/debug/svd/svd_data.zip', 47 | str(pathlib.Path(os.getcwd()).resolve() / 'package' / 'svd_data.zip'), 48 | 'DATA') 49 | ) 50 | a.datas = tuple(modified_datas) 51 | 52 | pyz = PYZ(a.pure, 53 | a.zipped_data, 54 | cipher=None) 55 | 56 | exe = EXE(pyz, 57 | a.scripts, 58 | a.binaries, 59 | a.zipfiles, 60 | a.datas, 61 | name='ubittool-cli', 62 | strip=False, 63 | upx=False, 64 | # False hides the cli window, useful ON to debug 65 | console=True, 66 | debug=False) 67 | -------------------------------------------------------------------------------- /package/pyinstaller-gui.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # PyInstaller additional datas and hidden import from: 3 | # https://github.com/pyocd/pyOCD/issues/1529#issuecomment-1758960044 4 | import os 5 | import pathlib 6 | 7 | from PyInstaller.utils.hooks import get_package_paths, collect_entry_point 8 | 9 | 10 | datas_probe, hiddenimports_probe = collect_entry_point('pyocd.probe') 11 | datas_rtos, hiddenimports_rtos = collect_entry_point('pyocd.rtos') 12 | datas = [ 13 | (get_package_paths('pyocd')[1], 'pyocd'), 14 | (get_package_paths('pylink')[1], 'pylink') 15 | ] 16 | 17 | # Right now we exclude cmsis_pack_manager, but could be needed in the future 18 | # datas.append((get_package_paths('cmsis_pack_manager')[1], 'cmsis_pack_manager')) 19 | excludes = ['cmsis_pack_manager'] 20 | 21 | a = Analysis(['../ubittool/gui.py'], 22 | pathex=['../'], 23 | binaries=None, 24 | datas=datas + datas_probe + datas_rtos, 25 | hiddenimports=hiddenimports_probe + hiddenimports_rtos, 26 | hookspath=[], 27 | runtime_hooks=[], 28 | excludes=excludes, 29 | win_no_prefer_redirects=False, 30 | win_private_assemblies=False, 31 | cipher=None) 32 | 33 | # There isn't a way to exclude files, so remove them from collected datas. 34 | # This removes the pyocd/target/builtin/target_xxxx.py files unless for Nordic. 35 | # And replaces the svd_data.zip file with a slimmer version manually modified. 36 | # Datas format: ("", "", 'DATA') 37 | modified_datas = [d for d in a.datas if d[0] != 'pyocd/debug/svd/svd_data.zip' and 38 | not (d[0].startswith("pyocd/target/builtin/target_") and not d[0].startswith("pyocd/target/builtin/target_nRF"))] 39 | 40 | modified_datas.append(( 41 | 'pyocd/debug/svd/svd_data.zip', 42 | str(pathlib.Path(os.getcwd()).resolve() / 'package' / 'svd_data.zip'), 43 | 'DATA') 44 | ) 45 | a.datas = tuple(modified_datas) 46 | 47 | pyz = PYZ(a.pure, 48 | a.zipped_data, 49 | cipher=None) 50 | 51 | exe = EXE(pyz, 52 | a.scripts, 53 | a.binaries, 54 | a.zipfiles, 55 | a.datas, 56 | name='ubittool-gui', 57 | strip=False, 58 | upx=False, 59 | # False hides the cli window, useful ON to debug 60 | console=False, 61 | debug=False) 62 | 63 | app = BUNDLE(exe, 64 | name='ubittool-gui.app', 65 | bundle_identifier=None, 66 | info_plist={'NSHighResolutionCapable': 'True'}) 67 | 68 | # Uncomment this for debugging purposes 69 | # coll = COLLECT(exe, 70 | # a.binaries, 71 | # a.zipfiles, 72 | # a.datas, 73 | # strip=False, 74 | # upx=True, 75 | # upx_exclude=[], 76 | # name='ubittool-gui-bundle') 77 | -------------------------------------------------------------------------------- /package/svd_data.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosperate/ubittool/72c628c6eef75bc243ec8bacc53016941cddf497/package/svd_data.zip -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "altgraph" 3 | version = "0.17.4" 4 | description = "Python graph (network) package" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "appdirs" 11 | version = "1.4.4" 12 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 13 | category = "main" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "22.2.0" 20 | description = "Classes Without Boilerplate" 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=3.6" 24 | 25 | [package.extras] 26 | cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 27 | dev = ["attrs"] 28 | docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 29 | tests = ["attrs", "zope.interface"] 30 | tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 31 | tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 32 | 33 | [[package]] 34 | name = "black" 35 | version = "19.10b0" 36 | description = "The uncompromising code formatter." 37 | category = "dev" 38 | optional = false 39 | python-versions = ">=3.6" 40 | 41 | [package.dependencies] 42 | appdirs = "*" 43 | attrs = ">=18.1.0" 44 | click = ">=6.5" 45 | pathspec = ">=0.6,<1" 46 | regex = "*" 47 | toml = ">=0.9.4" 48 | typed-ast = ">=1.4.0" 49 | 50 | [package.extras] 51 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 52 | 53 | [[package]] 54 | name = "capstone" 55 | version = "4.0.2" 56 | description = "Capstone disassembly engine" 57 | category = "main" 58 | optional = false 59 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 60 | 61 | [[package]] 62 | name = "cffi" 63 | version = "1.15.1" 64 | description = "Foreign Function Interface for Python calling C code." 65 | category = "main" 66 | optional = false 67 | python-versions = "*" 68 | 69 | [package.dependencies] 70 | pycparser = "*" 71 | 72 | [[package]] 73 | name = "click" 74 | version = "7.1.2" 75 | description = "Composable command line interface toolkit" 76 | category = "main" 77 | optional = false 78 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 79 | 80 | [[package]] 81 | name = "cmsis-pack-manager" 82 | version = "0.5.3" 83 | description = "Python manager for CMSIS-Pack index and cache with fast Rust backend" 84 | category = "main" 85 | optional = false 86 | python-versions = ">=3.6" 87 | 88 | [package.dependencies] 89 | appdirs = ">=1.4,<2.0" 90 | cffi = "*" 91 | pyyaml = ">=6.0,<7.0" 92 | 93 | [package.extras] 94 | test = ["pytest (>=6.0)", "hypothesis", "jinja2"] 95 | 96 | [[package]] 97 | name = "colorama" 98 | version = "0.4.5" 99 | description = "Cross-platform colored terminal text." 100 | category = "main" 101 | optional = false 102 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 103 | 104 | [[package]] 105 | name = "coverage" 106 | version = "7.2.7" 107 | description = "Code coverage measurement for Python" 108 | category = "dev" 109 | optional = false 110 | python-versions = ">=3.7" 111 | 112 | [package.dependencies] 113 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 114 | 115 | [package.extras] 116 | toml = ["tomli"] 117 | 118 | [[package]] 119 | name = "exceptiongroup" 120 | version = "1.2.0" 121 | description = "Backport of PEP 654 (exception groups)" 122 | category = "dev" 123 | optional = false 124 | python-versions = ">=3.7" 125 | 126 | [package.extras] 127 | test = ["pytest (>=6)"] 128 | 129 | [[package]] 130 | name = "flake8" 131 | version = "4.0.1" 132 | description = "the modular source code checker: pep8 pyflakes and co" 133 | category = "dev" 134 | optional = false 135 | python-versions = ">=3.6" 136 | 137 | [package.dependencies] 138 | importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} 139 | mccabe = ">=0.6.0,<0.7.0" 140 | pycodestyle = ">=2.8.0,<2.9.0" 141 | pyflakes = ">=2.4.0,<2.5.0" 142 | 143 | [[package]] 144 | name = "flake8-bugbear" 145 | version = "22.9.23" 146 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 147 | category = "dev" 148 | optional = false 149 | python-versions = ">=3.6" 150 | 151 | [package.dependencies] 152 | attrs = ">=19.2.0" 153 | flake8 = ">=3.0.0" 154 | 155 | [package.extras] 156 | dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] 157 | 158 | [[package]] 159 | name = "flake8-builtins" 160 | version = "2.0.0" 161 | description = "Check for python builtins being used as variables or parameters." 162 | category = "dev" 163 | optional = false 164 | python-versions = "*" 165 | 166 | [package.dependencies] 167 | flake8 = "*" 168 | 169 | [package.extras] 170 | test = ["pytest"] 171 | 172 | [[package]] 173 | name = "flake8-docstrings" 174 | version = "1.6.0" 175 | description = "Extension for flake8 which uses pydocstyle to check docstrings" 176 | category = "dev" 177 | optional = false 178 | python-versions = "*" 179 | 180 | [package.dependencies] 181 | flake8 = ">=3" 182 | pydocstyle = ">=2.1" 183 | 184 | [[package]] 185 | name = "hidapi" 186 | version = "0.14.0" 187 | description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" 188 | category = "main" 189 | optional = false 190 | python-versions = "*" 191 | 192 | [[package]] 193 | name = "importlib-metadata" 194 | version = "4.2.0" 195 | description = "Read metadata from Python packages" 196 | category = "main" 197 | optional = false 198 | python-versions = ">=3.6" 199 | 200 | [package.dependencies] 201 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 202 | zipp = ">=0.5" 203 | 204 | [package.extras] 205 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 206 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 207 | 208 | [[package]] 209 | name = "importlib-resources" 210 | version = "5.12.0" 211 | description = "Read resources from Python packages" 212 | category = "main" 213 | optional = false 214 | python-versions = ">=3.7" 215 | 216 | [package.dependencies] 217 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 218 | 219 | [package.extras] 220 | docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] 221 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"] 222 | 223 | [[package]] 224 | name = "iniconfig" 225 | version = "1.1.1" 226 | description = "iniconfig: brain-dead simple config-ini parsing" 227 | category = "dev" 228 | optional = false 229 | python-versions = "*" 230 | 231 | [[package]] 232 | name = "intelhex" 233 | version = "2.3.0" 234 | description = "Python library for Intel HEX files manipulations" 235 | category = "main" 236 | optional = false 237 | python-versions = "*" 238 | 239 | [[package]] 240 | name = "intervaltree" 241 | version = "3.1.0" 242 | description = "Editable interval tree data structure for Python 2 and 3" 243 | category = "main" 244 | optional = false 245 | python-versions = "*" 246 | 247 | [package.dependencies] 248 | sortedcontainers = ">=2.0,<3.0" 249 | 250 | [[package]] 251 | name = "lark" 252 | version = "1.1.8" 253 | description = "a modern parsing library" 254 | category = "main" 255 | optional = false 256 | python-versions = ">=3.6" 257 | 258 | [package.extras] 259 | atomic_cache = ["atomicwrites"] 260 | interegular = ["interegular (>=0.3.1,<0.4.0)"] 261 | nearley = ["js2py"] 262 | regex = ["regex"] 263 | 264 | [[package]] 265 | name = "libusb-package" 266 | version = "1.0.26.2" 267 | description = "Package containing libusb so it can be installed via Python package managers" 268 | category = "main" 269 | optional = false 270 | python-versions = ">=3.7" 271 | 272 | [package.dependencies] 273 | importlib-resources = "*" 274 | 275 | [[package]] 276 | name = "macholib" 277 | version = "1.16.3" 278 | description = "Mach-O header analysis and editing" 279 | category = "dev" 280 | optional = false 281 | python-versions = "*" 282 | 283 | [package.dependencies] 284 | altgraph = ">=0.17" 285 | 286 | [[package]] 287 | name = "mccabe" 288 | version = "0.6.1" 289 | description = "McCabe checker, plugin for flake8" 290 | category = "dev" 291 | optional = false 292 | python-versions = "*" 293 | 294 | [[package]] 295 | name = "natsort" 296 | version = "8.4.0" 297 | description = "Simple yet flexible natural sorting in Python." 298 | category = "main" 299 | optional = false 300 | python-versions = ">=3.7" 301 | 302 | [package.extras] 303 | fast = ["fastnumbers (>=2.0.0)"] 304 | icu = ["PyICU (>=1.0.0)"] 305 | 306 | [[package]] 307 | name = "packaging" 308 | version = "21.3" 309 | description = "Core utilities for Python packages" 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=3.6" 313 | 314 | [package.dependencies] 315 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 316 | 317 | [[package]] 318 | name = "pathspec" 319 | version = "0.9.0" 320 | description = "Utility library for gitignore style pattern matching of file paths." 321 | category = "dev" 322 | optional = false 323 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 324 | 325 | [[package]] 326 | name = "pefile" 327 | version = "2023.2.7" 328 | description = "Python PE parsing module" 329 | category = "dev" 330 | optional = false 331 | python-versions = ">=3.6.0" 332 | 333 | [[package]] 334 | name = "pluggy" 335 | version = "1.0.0" 336 | description = "plugin and hook calling mechanisms for python" 337 | category = "dev" 338 | optional = false 339 | python-versions = ">=3.6" 340 | 341 | [package.dependencies] 342 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 343 | 344 | [package.extras] 345 | dev = ["pre-commit", "tox"] 346 | testing = ["pytest", "pytest-benchmark"] 347 | 348 | [[package]] 349 | name = "prettytable" 350 | version = "3.7.0" 351 | description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" 352 | category = "main" 353 | optional = false 354 | python-versions = ">=3.7" 355 | 356 | [package.dependencies] 357 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 358 | wcwidth = "*" 359 | 360 | [package.extras] 361 | tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] 362 | 363 | [[package]] 364 | name = "psutil" 365 | version = "5.9.6" 366 | description = "Cross-platform lib for process and system monitoring in Python." 367 | category = "main" 368 | optional = false 369 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 370 | 371 | [package.extras] 372 | test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] 373 | 374 | [[package]] 375 | name = "pycodestyle" 376 | version = "2.8.0" 377 | description = "Python style guide checker" 378 | category = "dev" 379 | optional = false 380 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 381 | 382 | [[package]] 383 | name = "pycparser" 384 | version = "2.21" 385 | description = "C parser in Python" 386 | category = "main" 387 | optional = false 388 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 389 | 390 | [[package]] 391 | name = "pydocstyle" 392 | version = "6.3.0" 393 | description = "Python docstring style checker" 394 | category = "dev" 395 | optional = false 396 | python-versions = ">=3.6" 397 | 398 | [package.dependencies] 399 | importlib-metadata = {version = ">=2.0.0,<5.0.0", markers = "python_version < \"3.8\""} 400 | snowballstemmer = ">=2.2.0" 401 | 402 | [package.extras] 403 | toml = ["tomli (>=1.2.3)"] 404 | 405 | [[package]] 406 | name = "pyelftools" 407 | version = "0.30" 408 | description = "Library for analyzing ELF files and DWARF debugging information" 409 | category = "main" 410 | optional = false 411 | python-versions = "*" 412 | 413 | [[package]] 414 | name = "pyflakes" 415 | version = "2.4.0" 416 | description = "passive checker of Python programs" 417 | category = "dev" 418 | optional = false 419 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 420 | 421 | [[package]] 422 | name = "pyinstaller" 423 | version = "5.13.2" 424 | description = "PyInstaller bundles a Python application and all its dependencies into a single package." 425 | category = "dev" 426 | optional = false 427 | python-versions = "<3.13,>=3.7" 428 | 429 | [package.dependencies] 430 | altgraph = "*" 431 | importlib-metadata = {version = ">=1.4", markers = "python_version < \"3.8\""} 432 | macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} 433 | pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} 434 | pyinstaller-hooks-contrib = ">=2021.4" 435 | pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} 436 | 437 | [package.extras] 438 | encryption = ["tinyaes (>=1.0.0)"] 439 | hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] 440 | 441 | [[package]] 442 | name = "pyinstaller-hooks-contrib" 443 | version = "2022.0" 444 | description = "Community maintained hooks for PyInstaller" 445 | category = "dev" 446 | optional = false 447 | python-versions = "*" 448 | 449 | [[package]] 450 | name = "pylink-square" 451 | version = "1.2.0" 452 | description = "Python interface for SEGGER J-Link." 453 | category = "main" 454 | optional = false 455 | python-versions = "*" 456 | 457 | [package.dependencies] 458 | psutil = ">=5.2.2" 459 | six = "*" 460 | 461 | [[package]] 462 | name = "pyocd" 463 | version = "0.36.0" 464 | description = "Cortex-M debugger for Python" 465 | category = "main" 466 | optional = false 467 | python-versions = ">=3.7.0" 468 | 469 | [package.dependencies] 470 | capstone = ">=4.0,<5.0" 471 | cmsis-pack-manager = ">=0.5.2,<1.0" 472 | colorama = "<1.0" 473 | hidapi = {version = ">=0.10.1,<1.0", markers = "platform_system != \"Linux\""} 474 | importlib-metadata = ">=3.6" 475 | importlib-resources = "*" 476 | intelhex = ">=2.0,<3.0" 477 | intervaltree = ">=3.0.2,<4.0" 478 | lark = ">=1.1.5,<2.0" 479 | libusb-package = ">=1.0,<2.0" 480 | natsort = ">=8.0.0,<9.0" 481 | prettytable = ">=2.0,<4.0" 482 | pyelftools = "<1.0" 483 | pylink-square = ">=1.0,<2.0" 484 | pyusb = ">=1.2.1,<2.0" 485 | pyyaml = ">=6.0,<7.0" 486 | six = ">=1.15.0,<2.0" 487 | typing-extensions = ">=4.0,<5.0" 488 | 489 | [package.extras] 490 | pemicro = ["pyocd-pemicro (>=1.0.6)"] 491 | test = ["pytest (>=6.2)", "pytest-cov", "coverage", "flake8", "pylint", "tox"] 492 | 493 | [[package]] 494 | name = "pyparsing" 495 | version = "3.0.7" 496 | description = "Python parsing module" 497 | category = "dev" 498 | optional = false 499 | python-versions = ">=3.6" 500 | 501 | [package.extras] 502 | diagrams = ["jinja2", "railroad-diagrams"] 503 | 504 | [[package]] 505 | name = "pytest" 506 | version = "7.4.3" 507 | description = "pytest: simple powerful testing with Python" 508 | category = "dev" 509 | optional = false 510 | python-versions = ">=3.7" 511 | 512 | [package.dependencies] 513 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 514 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 515 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 516 | iniconfig = "*" 517 | packaging = "*" 518 | pluggy = ">=0.12,<2.0" 519 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 520 | 521 | [package.extras] 522 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 523 | 524 | [[package]] 525 | name = "pytest-cov" 526 | version = "4.1.0" 527 | description = "Pytest plugin for measuring coverage." 528 | category = "dev" 529 | optional = false 530 | python-versions = ">=3.7" 531 | 532 | [package.dependencies] 533 | coverage = {version = ">=5.2.1", extras = ["toml"]} 534 | pytest = ">=4.6" 535 | 536 | [package.extras] 537 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 538 | 539 | [[package]] 540 | name = "pyusb" 541 | version = "1.2.1" 542 | description = "Python USB access module" 543 | category = "main" 544 | optional = false 545 | python-versions = ">=3.6.0" 546 | 547 | [[package]] 548 | name = "pywin32" 549 | version = "303" 550 | description = "Python for Window Extensions" 551 | category = "dev" 552 | optional = false 553 | python-versions = "*" 554 | 555 | [[package]] 556 | name = "pywin32-ctypes" 557 | version = "0.2.2" 558 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 559 | category = "dev" 560 | optional = false 561 | python-versions = ">=3.6" 562 | 563 | [[package]] 564 | name = "pyyaml" 565 | version = "6.0.1" 566 | description = "YAML parser and emitter for Python" 567 | category = "main" 568 | optional = false 569 | python-versions = ">=3.6" 570 | 571 | [[package]] 572 | name = "regex" 573 | version = "2023.8.8" 574 | description = "Alternative regular expression module, to replace re." 575 | category = "dev" 576 | optional = false 577 | python-versions = ">=3.6" 578 | 579 | [[package]] 580 | name = "setuptools-scm" 581 | version = "6.4.2" 582 | description = "the blessed package to manage your versions by scm tags" 583 | category = "dev" 584 | optional = false 585 | python-versions = ">=3.6" 586 | 587 | [package.dependencies] 588 | packaging = ">=20.0" 589 | tomli = ">=1.0.0" 590 | 591 | [package.extras] 592 | test = ["pytest (>=6.2)", "virtualenv (>20)"] 593 | toml = ["setuptools (>=42)"] 594 | 595 | [[package]] 596 | name = "six" 597 | version = "1.16.0" 598 | description = "Python 2 and 3 compatibility utilities" 599 | category = "main" 600 | optional = false 601 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 602 | 603 | [[package]] 604 | name = "snowballstemmer" 605 | version = "2.2.0" 606 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 607 | category = "dev" 608 | optional = false 609 | python-versions = "*" 610 | 611 | [[package]] 612 | name = "sortedcontainers" 613 | version = "2.4.0" 614 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 615 | category = "main" 616 | optional = false 617 | python-versions = "*" 618 | 619 | [[package]] 620 | name = "toml" 621 | version = "0.10.2" 622 | description = "Python Library for Tom's Obvious, Minimal Language" 623 | category = "dev" 624 | optional = false 625 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 626 | 627 | [[package]] 628 | name = "tomli" 629 | version = "1.2.3" 630 | description = "A lil' TOML parser" 631 | category = "dev" 632 | optional = false 633 | python-versions = ">=3.6" 634 | 635 | [[package]] 636 | name = "typed-ast" 637 | version = "1.5.5" 638 | description = "a fork of Python 2 and 3 ast modules with type comment support" 639 | category = "dev" 640 | optional = false 641 | python-versions = ">=3.6" 642 | 643 | [[package]] 644 | name = "typing-extensions" 645 | version = "4.1.1" 646 | description = "Backported and Experimental Type Hints for Python 3.6+" 647 | category = "main" 648 | optional = false 649 | python-versions = ">=3.6" 650 | 651 | [[package]] 652 | name = "uflash" 653 | version = "1.2.0" 654 | description = "A module and utility to flash Python onto the BBC micro:bit." 655 | category = "main" 656 | optional = false 657 | python-versions = "*" 658 | 659 | [[package]] 660 | name = "wcwidth" 661 | version = "0.2.12" 662 | description = "Measures the displayed width of unicode strings in a terminal" 663 | category = "main" 664 | optional = false 665 | python-versions = "*" 666 | 667 | [[package]] 668 | name = "zipp" 669 | version = "3.6.0" 670 | description = "Backport of pathlib-compatible object wrapper for zip files" 671 | category = "main" 672 | optional = false 673 | python-versions = ">=3.6" 674 | 675 | [package.extras] 676 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 677 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 678 | 679 | [metadata] 680 | lock-version = "1.1" 681 | python-versions = "^3.7,<3.12" 682 | content-hash = "107483eef753f320c066dad6a954f3c896732a26847d35293f71c6dc3bac6c0b" 683 | 684 | [metadata.files] 685 | altgraph = [] 686 | appdirs = [] 687 | attrs = [] 688 | black = [] 689 | capstone = [] 690 | cffi = [] 691 | click = [] 692 | cmsis-pack-manager = [] 693 | colorama = [] 694 | coverage = [] 695 | exceptiongroup = [] 696 | flake8 = [] 697 | flake8-bugbear = [] 698 | flake8-builtins = [] 699 | flake8-docstrings = [] 700 | hidapi = [] 701 | importlib-metadata = [] 702 | importlib-resources = [] 703 | iniconfig = [] 704 | intelhex = [] 705 | intervaltree = [] 706 | lark = [] 707 | libusb-package = [] 708 | macholib = [] 709 | mccabe = [] 710 | natsort = [] 711 | packaging = [] 712 | pathspec = [] 713 | pefile = [] 714 | pluggy = [] 715 | prettytable = [] 716 | psutil = [] 717 | pycodestyle = [] 718 | pycparser = [] 719 | pydocstyle = [] 720 | pyelftools = [] 721 | pyflakes = [] 722 | pyinstaller = [] 723 | pyinstaller-hooks-contrib = [] 724 | pylink-square = [] 725 | pyocd = [] 726 | pyparsing = [] 727 | pytest = [] 728 | pytest-cov = [] 729 | pyusb = [] 730 | pywin32 = [] 731 | pywin32-ctypes = [] 732 | pyyaml = [] 733 | regex = [] 734 | setuptools-scm = [] 735 | six = [] 736 | snowballstemmer = [] 737 | sortedcontainers = [] 738 | toml = [] 739 | tomli = [] 740 | typed-ast = [] 741 | typing-extensions = [] 742 | uflash = [] 743 | wcwidth = [] 744 | zipp = [] 745 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "poetry_core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | # Poetry settings 6 | [tool.poetry] 7 | name = "ubittool" 8 | version = "0.8.0" 9 | description = "Tool to interface with the BBC micro:bit." 10 | authors = ["Carlos Pereira Atencio "] 11 | license = "MIT" 12 | homepage = "https://carlosperate.github.io/ubittool/" 13 | repository = "https://github.com/carlosperate/ubittool/" 14 | documentation = "https://carlosperate.github.io/ubittool/" 15 | readme = "README.md" 16 | keywords = ["microbit", "micro:bit", "bbcmicrobit", "ubittool"] 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "Environment :: Console", 20 | "Environment :: MacOS X", 21 | "Environment :: Win32 (MS Windows)", 22 | "Environment :: X11 Applications", 23 | "Intended Audience :: Developers", 24 | "Intended Audience :: Education", 25 | "Intended Audience :: End Users/Desktop", 26 | "Operating System :: MacOS", 27 | "Operating System :: Microsoft :: Windows", 28 | "Operating System :: POSIX", 29 | "Topic :: Education", 30 | "Topic :: Software Development :: Embedded Systems", 31 | ] 32 | 33 | [tool.poetry.dependencies] 34 | # Currently libusb-package is incompatible to newer Python versions 35 | # https://github.com/pyocd/libusb-package/issues/16 36 | python = "^3.7,<3.12" 37 | IntelHex = "^2.2.1" 38 | uflash = ">=1.1.0,<1.2.1" 39 | # This version of PyOCD fails in Python 3.10+ 40 | pyocd = "0.36.0" 41 | click = "^7.0" 42 | 43 | [tool.poetry.dev-dependencies] 44 | # Packaging, PyInstaller needs macholib for macOS, pywin32 for Windows 45 | pyinstaller = "^5.13.2" 46 | macholib = { version = "^1.8", platform = "darwin" } 47 | pywin32 = { version = "^303", platform = "windows" } 48 | pywin32-ctypes = { version = "^0.2.0", platform = "windows" } 49 | # PyOCD needs this as a setup_requires, for some reason 'pipenv install' in 50 | # Travis doesn't pick it up and PyOCD fails to install or run 51 | setuptools_scm = "*" 52 | # Linting 53 | flake8 = "*" 54 | flake8-bugbear = { version = "*", python = "^3.5" } 55 | flake8-builtins = "*" 56 | flake8-docstrings = "*" 57 | black = { version = "19.10b0", python = "^3.6" } 58 | # Tests 59 | pytest = "^7.4.3" 60 | pytest-cov = "^4.1.0" 61 | 62 | [tool.poetry.scripts] 63 | ubit = 'ubittool.__main__:main' 64 | 65 | # Black settings 66 | [tool.black] 67 | line-length = 79 68 | target-version = ['py35'] 69 | exclude = ''' 70 | /( 71 | \.eggs 72 | | \.git 73 | | \.hg 74 | | \.mypy_cache 75 | | \.tox 76 | | \.venv 77 | | _build 78 | | buck-out 79 | | build 80 | | dist 81 | # Above this line are defaults and below are project specific 82 | | ignoreme 83 | )/ 84 | ''' 85 | 86 | # Coverage settings 87 | [tool.coverage.run] 88 | branch = false 89 | 90 | [tool.coverage.report] 91 | show_missing = true 92 | # Regexes for lines to exclude from consideration 93 | exclude_lines = [ 94 | # Have to re-enable the standard pragma 95 | "pragma: no cover", 96 | # Don't complain about missing debug-only code: 97 | "def __repr__", 98 | "if self.debug", 99 | # Don't complain if tests don't hit defensive assertion code: 100 | "raise AssertionError", 101 | "raise NotImplementedError", 102 | # Don't complain if non-runnable code isn't run: 103 | "if 0:", 104 | "if __name__ == .__main__.:", 105 | ] 106 | 107 | [tool.coverage.html] 108 | directory = "htmlcov" 109 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Tests for cli.py.""" 4 | import os 5 | from unittest import mock 6 | 7 | from click.testing import CliRunner 8 | import pytest 9 | 10 | from ubittool import cli, cmds 11 | 12 | 13 | @pytest.fixture 14 | def check_no_board_connected(): 15 | """Check that there is no mbed board that PyOCD can connect to.""" 16 | try: 17 | cmds._read_continuous_memory(address=0x00, count=16) 18 | except Exception: 19 | # Good: Exception raised if no board is found 20 | pass 21 | else: 22 | raise Exception("Found an Mbed device connected, please unplug.") 23 | 24 | 25 | @mock.patch("ubittool.cli.os.path.exists", autospec=True) 26 | @mock.patch("ubittool.cli.click.echo", autospec=True) 27 | @mock.patch("ubittool.cli.sys.exit", autospec=True) 28 | def test_file_checker(mock_exit, mock_echo, mock_exists): 29 | """Test the file checker perform the required checks and prints info.""" 30 | mock_exists.return_value = False 31 | 32 | cli._file_checker("subject", "file/path.py") 33 | 34 | mock_exists.assert_called_once_with("file/path.py") 35 | assert mock_echo.call_count == 1 36 | assert "subject will be written to: file/path.py" in mock_echo.call_args[0] 37 | assert mock_exit.call_count == 0 38 | 39 | 40 | @mock.patch("ubittool.cli.os.path.exists", autospec=True) 41 | @mock.patch("ubittool.cli.click.echo", autospec=True) 42 | @mock.patch("ubittool.cli.sys.exit", autospec=True) 43 | def test_file_checker_existing_path(mock_exit, mock_echo, mock_exists): 44 | """Test file checker exits with error if the file exists.""" 45 | mock_exists.return_value = True 46 | 47 | cli._file_checker("subject", "file/path.py") 48 | 49 | mock_exists.assert_called_once_with("file/path.py") 50 | assert mock_echo.call_count == 1 51 | assert ( 52 | "Abort: The file/path.py file already exists." 53 | in mock_echo.call_args[0][0] 54 | ) 55 | mock_exit.assert_called_once_with(1) 56 | 57 | 58 | @mock.patch("ubittool.cli.click.echo", autospec=True) 59 | @mock.patch("ubittool.cli.sys.exit", autospec=True) 60 | def test_file_checker_no_path(mock_exit, mock_echo): 61 | """Test the file check informs about console output if no file is given.""" 62 | cli._file_checker("subject", None) 63 | 64 | assert mock_echo.call_count == 1 65 | assert "subject will be output to console." in mock_echo.call_args[0] 66 | assert mock_exit.call_count == 0 67 | 68 | 69 | @mock.patch("ubittool.cli.read_python_code", autospec=True) 70 | def test_read_code(mock_read_python_code, check_no_board_connected): 71 | """Test the read-code command without a file option.""" 72 | python_code = "Python code here" 73 | mock_read_python_code.return_value = python_code 74 | runner = CliRunner() 75 | 76 | result = runner.invoke(cli.read_code) 77 | 78 | assert "MicroPython code will be output to console." in result.output 79 | assert "Printing the MicroPython code" in result.output 80 | assert python_code in result.output 81 | assert "Finished successfully" in result.output 82 | assert result.exit_code == 0 83 | 84 | 85 | def test_read_code_no_board(check_no_board_connected): 86 | """Test the read-code command when no board is connected.""" 87 | runner = CliRunner() 88 | 89 | result = runner.invoke(cli.read_code) 90 | 91 | assert result.exit_code != 0 92 | assert "MicroPython code will be output to console." in result.output 93 | assert "Did not find any connected boards." in result.output 94 | 95 | 96 | @mock.patch("ubittool.cli.read_python_code", autospec=True) 97 | def test_read_code_path(mock_read_python_code, check_no_board_connected): 98 | """Test the read-code command with a file option.""" 99 | mock_read_python_code.return_value = "Python code here" 100 | runner = CliRunner() 101 | 102 | with mock.patch("ubittool.cli.open", mock.mock_open()) as m_open: 103 | result = runner.invoke(cli.read_code, ["--file_path", "thisfile.py"]) 104 | 105 | m_open.assert_called_once_with("thisfile.py", "w") 106 | m_open().write.assert_called_once_with("Python code here") 107 | assert "MicroPython code will be written to: thisfile.py" in result.output 108 | assert "Saving the MicroPython code..." in result.output 109 | assert "Finished successfully" in result.output 110 | assert result.exit_code == 0 111 | 112 | 113 | def test_read_code_path_no_board(check_no_board_connected): 114 | """Test read-code command with a file option and no board connected.""" 115 | file_name = "thisfile.py" 116 | runner = CliRunner() 117 | 118 | results = [ 119 | runner.invoke(cli.read_code, ["--file_path", file_name]), 120 | runner.invoke(cli.read_code, ["-f", file_name]), 121 | ] 122 | 123 | for result in results: 124 | assert result.exit_code != 0, "Exit code non-zero" 125 | assert ( 126 | "MicroPython code will be written to: {}".format(file_name) 127 | in result.output 128 | ), "Message written to file" 129 | assert ( 130 | "Did not find any connected boards." in result.output 131 | ), "Message error, board not found" 132 | # File not mocked, so checking command hasn't created it 133 | assert not os.path.isfile(file_name), "File does not exist" 134 | 135 | 136 | @mock.patch("ubittool.cli.read_flash_hex", autospec=True) 137 | def test_read_flash(mock_read_flash_hex, check_no_board_connected): 138 | """Test the read-flash command without a file option.""" 139 | flash_hex_content = "Intel Hex lines here" 140 | mock_read_flash_hex.return_value = flash_hex_content 141 | runner = CliRunner() 142 | 143 | result = runner.invoke(cli.read_flash) 144 | 145 | assert "micro:bit flash hex will be output to console." in result.output 146 | assert "Printing the flash contents" in result.output 147 | assert flash_hex_content in result.output 148 | assert "Finished successfully" in result.output 149 | assert result.exit_code == 0 150 | 151 | 152 | def test_read_flash_no_board(check_no_board_connected): 153 | """Test the read-flash command when no board is connected.""" 154 | runner = CliRunner() 155 | 156 | result = runner.invoke(cli.read_flash) 157 | 158 | assert result.exit_code != 0 159 | assert "micro:bit flash hex will be output to console." in result.output 160 | assert "Did not find any connected boards." in result.output 161 | 162 | 163 | @mock.patch("ubittool.cli.read_flash_hex", autospec=True) 164 | def test_read_flash_path(mock_read_flash_hex, check_no_board_connected): 165 | """Test the read-code command with a file option.""" 166 | flash_hex_content = "Intel Hex lines here" 167 | mock_read_flash_hex.return_value = flash_hex_content 168 | file_name = "thisfile.py" 169 | runner = CliRunner() 170 | 171 | with mock.patch("ubittool.cli.open", mock.mock_open()) as m_open: 172 | results = [runner.invoke(cli.read_flash, ["--file_path", file_name])] 173 | with mock.patch("ubittool.cli.open", mock.mock_open()) as m_open2: 174 | results.append(runner.invoke(cli.read_flash, ["-f", file_name])) 175 | 176 | m_open.assert_called_once_with(file_name, "w") 177 | m_open2.assert_called_once_with(file_name, "w") 178 | m_open().write.assert_called_once_with(flash_hex_content) 179 | m_open2().write.assert_called_once_with(flash_hex_content) 180 | for result in results: 181 | assert ( 182 | "micro:bit flash hex will be written to: {}".format(file_name) 183 | in result.output 184 | ) 185 | assert "Saving the flash contents..." in result.output 186 | assert "Finished successfully" in result.output 187 | assert result.exit_code == 0 188 | 189 | 190 | def test_read_flash_path_no_board(check_no_board_connected): 191 | """Test read-flash command with a file option and no board connected.""" 192 | file_name = "thisfile.py" 193 | runner = CliRunner() 194 | 195 | results = [ 196 | runner.invoke(cli.read_flash, ["--file_path", file_name]), 197 | runner.invoke(cli.read_flash, ["-f", file_name]), 198 | ] 199 | 200 | for result in results: 201 | assert result.exit_code != 0, "Exit code non-zero" 202 | assert ( 203 | "micro:bit flash hex will be written to: {}".format(file_name) 204 | in result.output 205 | ), "Message written to file" 206 | assert ( 207 | "Did not find any connected boards." in result.output 208 | ), "Message error, board not found" 209 | # File not mocked, so checking command hasn't created it 210 | assert not os.path.isfile(file_name), "File does not exist" 211 | 212 | 213 | @mock.patch("ubittool.cli.read_flash_uicr_hex", autospec=True) 214 | def test_read_flash_uicr(mock_read_flash_uicr_hex, check_no_board_connected): 215 | """Test the read-flash-uicr command without a file option.""" 216 | flash_hex_content = "Intel Hex lines here" 217 | mock_read_flash_uicr_hex.return_value = flash_hex_content 218 | runner = CliRunner() 219 | 220 | result = runner.invoke(cli.read_flash_uicr) 221 | 222 | assert ( 223 | "micro:bit flash and UICR hex will be output to console." 224 | in result.output 225 | ) 226 | assert "Printing the flash and UICR contents" in result.output 227 | assert flash_hex_content in result.output 228 | assert "Finished successfully" in result.output 229 | assert result.exit_code == 0 230 | 231 | 232 | def test_read_flash_uicr_no_board(check_no_board_connected): 233 | """Test the read-flash-uicr command when no board is connected.""" 234 | runner = CliRunner() 235 | 236 | result = runner.invoke(cli.read_flash_uicr) 237 | 238 | assert result.exit_code != 0 239 | assert ( 240 | "micro:bit flash and UICR hex will be output to console." 241 | in result.output 242 | ) 243 | assert "Did not find any connected boards." in result.output 244 | 245 | 246 | @mock.patch("ubittool.cli.read_flash_uicr_hex", autospec=True) 247 | def test_read_flash_uicr_path( 248 | mock_read_flash_uicr_hex, check_no_board_connected 249 | ): 250 | """Test the read-code-uicr command with a file option.""" 251 | flash_hex_content = "Intel Hex lines here" 252 | mock_read_flash_uicr_hex.return_value = flash_hex_content 253 | file_name = "thisfile.py" 254 | runner = CliRunner() 255 | 256 | with mock.patch("ubittool.cli.open", mock.mock_open()) as m_open: 257 | results = [ 258 | runner.invoke(cli.read_flash_uicr, ["--file_path", file_name]) 259 | ] 260 | with mock.patch("ubittool.cli.open", mock.mock_open()) as m_open2: 261 | results.append(runner.invoke(cli.read_flash_uicr, ["-f", file_name])) 262 | 263 | m_open.assert_called_once_with(file_name, "w") 264 | m_open2.assert_called_once_with(file_name, "w") 265 | m_open().write.assert_called_once_with(flash_hex_content) 266 | m_open2().write.assert_called_once_with(flash_hex_content) 267 | for result in results: 268 | assert ( 269 | "micro:bit flash and UICR hex will be written to: {}".format( 270 | file_name 271 | ) 272 | in result.output 273 | ) 274 | assert "Saving the flash and UICR contents..." in result.output 275 | assert "Finished successfully" in result.output 276 | assert result.exit_code == 0 277 | 278 | 279 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 280 | @mock.patch("ubittool.cli.compare_full_flash_hex", autospec=True) 281 | def test_compare_flash(mock_compare, mock_isfile, check_no_board_connected): 282 | """Test the compare-flash command.""" 283 | file_name = "random_file_name.hex" 284 | mock_isfile.return_value = True 285 | mock_compare.return_value = 0 286 | runner = CliRunner() 287 | 288 | results = [ 289 | runner.invoke(cli.compare, ["-f", file_name]), 290 | runner.invoke(cli.compare, ["--file_path", file_name]), 291 | ] 292 | 293 | assert mock_compare.call_count == len(results) 294 | for result in results: 295 | assert "Diff output loaded in the default browser." in result.output 296 | assert "Finished successfully." in result.output 297 | assert result.exit_code == 0, "Exit code 0" 298 | 299 | 300 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 301 | @mock.patch("ubittool.cli.compare_full_flash_hex", autospec=True) 302 | def test_compare_flash_diffs( 303 | mock_compare, mock_isfile, check_no_board_connected 304 | ): 305 | """Test the compare-flash command.""" 306 | file_name = "random_file_name.hex" 307 | mock_isfile.return_value = True 308 | mock_compare.return_value = 1 309 | runner = CliRunner() 310 | 311 | results = [ 312 | runner.invoke(cli.compare, ["-f", file_name]), 313 | runner.invoke(cli.compare, ["--file_path", file_name]), 314 | ] 315 | 316 | assert mock_compare.call_count == len(results) 317 | for result in results: 318 | assert "Diff output loaded in the default browser." in result.output 319 | assert ( 320 | "There are some differences in the micro:bit flash!" 321 | in result.output 322 | ) 323 | assert result.exit_code != 0, "Exit code non-zero" 324 | 325 | 326 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 327 | def test_compare_flash_no_board(mock_isfile, check_no_board_connected): 328 | """Test the compare-flash command when no board is connected.""" 329 | file_name = "random_file_name.hex" 330 | file_content = "Intel Hex lines here" 331 | mock_isfile.return_value = True 332 | runner = CliRunner() 333 | 334 | with mock.patch( 335 | "ubittool.cmds.open", mock.mock_open(read_data=file_content) 336 | ) as m_open: 337 | results = [ 338 | runner.invoke(cli.compare, ["-f", file_name]), 339 | runner.invoke(cli.compare, ["--file_path", file_name]), 340 | ] 341 | 342 | assert m_open.call_count == len(results) 343 | for result in results: 344 | assert result.exit_code != 0, "Exit code non-zero" 345 | assert "Did not find any connected boards." in result.output 346 | 347 | 348 | def test_compare_flash_invalid_file(): 349 | """Check error is thrown when compare-flash file does not exist.""" 350 | file_name = "random_file_does_not_exist.hex" 351 | runner = CliRunner() 352 | 353 | results = [ 354 | runner.invoke(cli.compare, ["--file_path", file_name]), 355 | runner.invoke(cli.compare, ["-f", file_name]), 356 | ] 357 | 358 | for result in results: 359 | assert result.exit_code != 0, "Exit code non-zero" 360 | assert "Abort: File does not exists" in result.output 361 | 362 | 363 | def test_compare_flash_no_file(): 364 | """Test there is an error when compare-flash doesn't have a file arg.""" 365 | runner = CliRunner() 366 | 367 | result = runner.invoke(cli.compare) 368 | 369 | assert result.exit_code != 0, "Exit code non-zero" 370 | assert "Error: Missing option '-f' / '--file_path'." in result.output 371 | 372 | 373 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 374 | @mock.patch("ubittool.cli.flash_drag_n_drop", autospec=True) 375 | @mock.patch("ubittool.cli.compare_full_flash_hex", autospec=True) 376 | def test_flash_compare( 377 | mock_compare, mock_flash, mock_isfile, check_no_board_connected 378 | ): 379 | """Test the compare-flash command.""" 380 | file_input = "random_input_file.hex" 381 | file_compare = "compare_file.hex" 382 | mock_isfile.return_value = True 383 | mock_compare.return_value = 0 384 | runner = CliRunner() 385 | 386 | results = [ 387 | runner.invoke( 388 | cli.flash_compare, ["-i", file_input, "-c", file_compare] 389 | ), 390 | runner.invoke( 391 | cli.flash_compare, 392 | [ 393 | "--input_file_path", 394 | file_input, 395 | "--compare_file_path", 396 | file_compare, 397 | ], 398 | ), 399 | ] 400 | 401 | assert mock_compare.call_count == len(results) 402 | for result in results: 403 | assert file_input + "' file to MICROBIT drive" in result.output 404 | assert "Diff output loaded in the default browser" in result.output 405 | assert "Finished successfully" in result.output 406 | assert result.exit_code == 0, "Exit code 0" 407 | 408 | 409 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 410 | def test_flash_compare_no_file(mock_isfile, check_no_board_connected): 411 | """Test the compare-flash command.""" 412 | mock_isfile.side_effect = [False, True, False] 413 | runner = CliRunner() 414 | 415 | results = [ 416 | runner.invoke( 417 | cli.flash_compare, 418 | ["-i", "does_not_exist.hex", "-c", "does_not_matter.hex"], 419 | ), 420 | runner.invoke( 421 | cli.flash_compare, 422 | ["-i", "exists.hex", "-c", "does_not_exist.hex"], 423 | ), 424 | ] 425 | 426 | for result in results: 427 | assert "does not exists" in result.output 428 | assert result.exit_code != 0, "Exit code non-zero" 429 | 430 | 431 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 432 | def test_flash_compare_exception(mock_isfile, check_no_board_connected): 433 | """Test the compare-flash command.""" 434 | file_input = "random_input_file.hex" 435 | file_compare = "compare_file.hex" 436 | mock_isfile.return_value = True 437 | runner = CliRunner() 438 | 439 | result = runner.invoke( 440 | cli.flash_compare, ["-i", file_input, "-c", file_compare] 441 | ) 442 | 443 | assert result.exit_code != 0, "Exit code non-zero" 444 | assert "Error: Could not find a MICROBIT" in result.output 445 | 446 | 447 | @mock.patch("ubittool.cli.open_gui", autospec=True) 448 | def test_gui(mock_open_gui, check_no_board_connected): 449 | """Test the gui command.""" 450 | runner = CliRunner() 451 | 452 | result = runner.invoke(cli.gui) 453 | 454 | assert result.exit_code == 0, "Exit code 0" 455 | assert mock_open_gui.call_count == 1, "open_gui() function called" 456 | 457 | 458 | def test_batch_flash(check_no_board_connected): 459 | """Test the batch-flash command.""" 460 | runner = CliRunner() 461 | file_path = "/path/to/hex/file.hex" 462 | 463 | result = runner.invoke(cli.batch_flash, ["--file-path", file_path]) 464 | 465 | assert result.exit_code != 0, "Exit code non-zero" 466 | assert "Batch flash of hex file" in result.output 467 | 468 | 469 | @mock.patch("ubittool.cli.os.path.isfile", autospec=True) 470 | def test_batch_flash_exit_keyboardexception( 471 | mock_isfile, check_no_board_connected 472 | ): 473 | """Test the batch-flash command.""" 474 | runner = CliRunner() 475 | file_path = "/path/to/hex/file.hex" 476 | 477 | # Inject KeyboardException to stop the command 478 | with mock.patch( 479 | "ubittool.cli.batch_flash_hex", autospec=True 480 | ) as mock_batch_flash_hex: 481 | mock_batch_flash_hex.side_effect = KeyboardInterrupt 482 | result = runner.invoke(cli.batch_flash, ["--file-path", file_path]) 483 | 484 | assert result.exit_code == 0, "Exit code zero" 485 | assert "Batch flash of hex file" in result.output 486 | -------------------------------------------------------------------------------- /tests/test_cmds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Tests for cmds.py module.""" 4 | import os 5 | from io import StringIO 6 | from unittest import mock 7 | 8 | import pytest 9 | from intelhex import IntelHex 10 | 11 | from ubittool import cmds 12 | 13 | 14 | ############################################################################### 15 | # Helpers 16 | ############################################################################### 17 | INTEL_HEX_EOF = ":00000001FF\n" 18 | 19 | 20 | def ihex_to_str(ih): 21 | """Convert an Intel Hex instance into an Intel Hex string.""" 22 | sio = StringIO() 23 | ih.write_hex_file(sio) 24 | hex_str = sio.getvalue() 25 | sio.close() 26 | return hex_str 27 | 28 | 29 | ############################################################################### 30 | # Data format conversions 31 | ############################################################################### 32 | def test_bytes_to_intel_hex(): 33 | """Test the data to Intel Hex string conversion.""" 34 | data = [1, 2, 3, 4, 5] 35 | expected_hex_str = "\n".join([":050000000102030405EC", INTEL_HEX_EOF]) 36 | 37 | result = cmds._bytes_to_intel_hex( 38 | [cmds.DataAndOffset(data=data, offset=0)] 39 | ) 40 | 41 | assert expected_hex_str == result 42 | 43 | 44 | def test_bytes_to_intel_hex_offset(): 45 | """Test the data to Intel Hex string conversion with an offset.""" 46 | data = [1, 2, 3, 4, 5] 47 | offset = 0x2000000 48 | expected_hex_str = "\n".join( 49 | [":020000040200F8", ":050000000102030405EC", INTEL_HEX_EOF] 50 | ) 51 | 52 | result = cmds._bytes_to_intel_hex( 53 | [cmds.DataAndOffset(data=data, offset=offset)] 54 | ) 55 | 56 | assert expected_hex_str == result 57 | 58 | 59 | @mock.patch("ubittool.cmds.sys.stderr.write", autospec=True) 60 | @mock.patch("ubittool.cmds.StringIO", autospec=True) 61 | def test_bytes_to_intel_hex_io_error(mock_string_io, mock_stderr): 62 | """Test the exception handling when an IOError is encountered.""" 63 | data = [1, 2, 3, 4, 5] 64 | mock_string_io.return_value.write.side_effect = IOError() 65 | 66 | result = cmds._bytes_to_intel_hex( 67 | [cmds.DataAndOffset(data=data, offset=0)] 68 | ) 69 | 70 | assert result is None 71 | assert mock_stderr.call_count == 1 72 | 73 | 74 | def test_bytes_to_intel_hex_invalid_data(): 75 | """Test there is an error thrown if the input data is invalid.""" 76 | data = [1, 2, 3, 4, "500"] 77 | 78 | try: 79 | cmds._bytes_to_intel_hex([cmds.DataAndOffset(data=data, offset=0)]) 80 | except Exception: 81 | # The exception that bubbles up from IntelHex is implementation detail 82 | # from that library, so it could be anything 83 | assert True, "Exception raised" 84 | else: 85 | raise AssertionError("Exception NOT raised") 86 | 87 | 88 | def test_bytes_to_pretty_hex(): 89 | """Test the data to Intel Hex string conversion.""" 90 | data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] 91 | expected = ( 92 | "0000 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 " 93 | "|................|\n" 94 | ) 95 | 96 | result = cmds._bytes_to_pretty_hex( 97 | [cmds.DataAndOffset(data=data, offset=0)] 98 | ) 99 | 100 | assert expected == result 101 | 102 | 103 | def test_bytes_to_pretty_hex_offset(): 104 | """Test the data to Intel Hex string conversion with an offset.""" 105 | data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] 106 | offset = 0x2000001 107 | expected = ( 108 | "2000000 -- 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F " 109 | "| ...............|\n" 110 | "2000010 10 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- " 111 | "|. |\n" 112 | ) 113 | 114 | result = cmds._bytes_to_pretty_hex( 115 | [cmds.DataAndOffset(data=data, offset=offset)] 116 | ) 117 | 118 | assert expected == result 119 | 120 | 121 | @mock.patch("ubittool.cmds.sys.stderr.write", autospec=True) 122 | @mock.patch("ubittool.cmds.StringIO", autospec=True) 123 | def test_bytes_to_pretty_hex_io_error(mock_string_io, mock_stderr): 124 | """Test the exception handling when an IOError is encountered.""" 125 | data = [1, 2, 3, 4, 5] 126 | mock_string_io.return_value.write.side_effect = IOError() 127 | 128 | result = cmds._bytes_to_pretty_hex( 129 | [cmds.DataAndOffset(data=data, offset=0)] 130 | ) 131 | 132 | assert result is None 133 | assert mock_stderr.call_count == 1 134 | 135 | 136 | def test_bytes_to_pretty_hexinvalid_data(): 137 | """Test there is an error thrown if the input data is invalid.""" 138 | try: 139 | cmds._bytes_to_pretty_hex( 140 | [cmds.DataAndOffset(data=[1, 2, 3, 4, "500"], offset=0)] 141 | ) 142 | except Exception: 143 | # The exception that bubbles up from IntelHex is implementation detail 144 | # from that library, so it could be anything 145 | assert True, "Exception raised" 146 | else: 147 | raise AssertionError("Exception NOT raised") 148 | 149 | 150 | ############################################################################### 151 | # Reading data commands 152 | ############################################################################### 153 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 154 | def test_read_flash_hex(mock_read_flash): 155 | """Test read_flash_hex() with default arguments.""" 156 | data_bytes = bytes([x for x in range(256)] * 4) 157 | intel_hex = IntelHex() 158 | intel_hex.frombytes(data_bytes) 159 | ihex_str = ihex_to_str(intel_hex) 160 | mock_read_flash.return_value = (0, data_bytes) 161 | 162 | result = cmds.read_flash_hex() 163 | 164 | assert result == ihex_str 165 | 166 | 167 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 168 | def test_read_flash_hex_decoded(mock_read_flash): 169 | """Test read_flash_hex() with decoding hex.""" 170 | data_bytes = bytes([x for x in range(1, 17)]) 171 | expected = ( 172 | "0000 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 " 173 | "|................|\n" 174 | ) 175 | mock_read_flash.return_value = (0, data_bytes) 176 | 177 | result = cmds.read_flash_hex(decode_hex=True) 178 | 179 | assert result == expected 180 | 181 | 182 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_uicr", autospec=True) 183 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 184 | def test_read_flash_uicr_hex(mock_read_flash, mock_read_uicr): 185 | """Test read_flash_uicr_hex() with default arguments.""" 186 | flash_data_bytes = bytes([x for x in range(256)] * 4) 187 | mock_read_flash.return_value = (0, flash_data_bytes) 188 | uicr_data_bytes = bytes([x for x in range(64)]) 189 | mock_read_uicr.return_value = (0x10001000, uicr_data_bytes) 190 | intel_hex = IntelHex() 191 | intel_hex.frombytes(flash_data_bytes) 192 | intel_hex.frombytes(uicr_data_bytes, 0x10001000) 193 | flash_and_uicr_ihex_str = ihex_to_str(intel_hex) 194 | 195 | result = cmds.read_flash_uicr_hex() 196 | 197 | assert result == flash_and_uicr_ihex_str 198 | 199 | 200 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 201 | def test_read_flash_hex_non_zero_address(mock_read_flash): 202 | """Test read_flash_hex() with given address and count.""" 203 | data_bytes = bytes([x for x in range(128)]) 204 | data_dict = {x + 1024: x for x in range(256)} 205 | intel_hex = IntelHex() 206 | intel_hex.fromdict(data_dict) 207 | ihex_str = ihex_to_str(intel_hex) 208 | # Remove the last 128 bytes, 8 lines, 9 with EoF 209 | ihex_str = "\n".join(ihex_str.split()[:-9] + [INTEL_HEX_EOF]) 210 | mock_read_flash.return_value = (1024, data_bytes) 211 | 212 | result = cmds.read_flash_hex(address=1024, count=128) 213 | 214 | assert result == ihex_str 215 | 216 | 217 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_uicr", autospec=True) 218 | def test_read_uicr_hex(mock_read_uicr): 219 | """Test read_uicr_hex() with default arguments.""" 220 | data_bytes = bytes([x for x in range(256)]) 221 | ihex_str = ihex_to_str(IntelHex({x + 0x10001000: x for x in range(256)})) 222 | mock_read_uicr.return_value = (0x10001000, data_bytes) 223 | 224 | result = cmds.read_uicr_hex() 225 | 226 | assert result == ihex_str 227 | 228 | 229 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_ram", autospec=True) 230 | def test_read_ram_hex(mock_read_uicr): 231 | """Test read_ram_hex() with default arguments.""" 232 | data_bytes = bytes([x for x in range(16)] * 1024) 233 | ihex = IntelHex( 234 | {x + 0x20000000: data_bytes[x] for x in range(len(data_bytes))} 235 | ) 236 | ihex_str = ihex_to_str(ihex) 237 | mock_read_uicr.return_value = (0x20000000, data_bytes) 238 | 239 | result = cmds.read_ram_hex() 240 | 241 | assert result == ihex_str 242 | 243 | 244 | @mock.patch.object( 245 | cmds.programmer.MicrobitMcu, "read_uicr_customer", autospec=True 246 | ) 247 | def test_read_uicr_customer_hex(mock_read_uicr_customer): 248 | """Test read_uicr_customer_hex() with default arguments.""" 249 | data_bytes = bytes([x for x in range(128)]) 250 | ihex_str = ihex_to_str(IntelHex({x + 0x10001080: x for x in range(128)})) 251 | mock_read_uicr_customer.return_value = (0x10001080, data_bytes) 252 | 253 | result = cmds.read_uicr_customer_hex() 254 | 255 | assert result == ihex_str 256 | 257 | 258 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 259 | def test_read_micropython(mock_read_flash): 260 | """Test read_micropython() with default arguments.""" 261 | data_bytes = bytes([x for x in range(256)] * 4) 262 | intel_hex = IntelHex() 263 | intel_hex.frombytes(data_bytes) 264 | ihex_str = ihex_to_str(intel_hex) 265 | mock_read_flash.return_value = (0, data_bytes) 266 | 267 | result = cmds.read_micropython() 268 | 269 | assert result == ihex_str 270 | 271 | 272 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 273 | @mock.patch("ubittool.cmds._bytes_to_intel_hex", autospec=True) 274 | def test_read_python_code(mock_bytes_to_intel_hex, mock_read_flash): 275 | """.""" 276 | python_code_hex = "\n".join( 277 | [ 278 | ":020000040003F7", 279 | ":10E000004D509600232041646420796F7572205032", 280 | ":10E010007974686F6E20636F646520686572652E21", 281 | ":10E0200020452E672E0A66726F6D206D6963726FD0", 282 | ":10E0300062697420696D706F7274202A0A7768694A", 283 | ":10E040006C6520547275653A0A202020206469733B", 284 | ":10E05000706C61792E7363726F6C6C282748656CE5", 285 | ":10E060006C6F2C20576F726C642127290A202020A6", 286 | ":10E0700020646973706C61792E73686F7728496DBD", 287 | ":10E080006167652E4845415254290A20202020739B", 288 | ":10E090006C656570283230303029000000000000C7", 289 | ":10E0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80", 290 | ] 291 | ) 292 | python_code = "\n".join( 293 | [ 294 | "# Add your Python code here. E.g.", 295 | "from microbit import *", 296 | "while True:", 297 | " display.scroll('Hello, World!')", 298 | " display.show(Image.HEART)", 299 | " sleep(2000)", 300 | ] 301 | ) 302 | mock_read_flash.return_value = (0, "") 303 | mock_bytes_to_intel_hex.return_value = python_code_hex 304 | 305 | result = cmds.read_python_code() 306 | 307 | assert result == python_code 308 | 309 | 310 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 311 | def test_read_python_code_empty(mock_read_flash): 312 | """Check an emptry Python code is returned on an empty flash.""" 313 | data_bytes = bytes([x for x in range(64)]) 314 | mock_read_flash.return_value = (0x3E000, data_bytes) 315 | 316 | result = cmds.read_python_code() 317 | 318 | assert result == "" 319 | 320 | 321 | @mock.patch.object(cmds.programmer.MicrobitMcu, "read_flash", autospec=True) 322 | @mock.patch("ubittool.cmds.uflash.extract_script", autospec=True) 323 | def test_read_python_code_exception(mock_extract_script, mock_read_flash): 324 | """Check error thrown if failing to find Python code in flash.""" 325 | data_bytes = bytes([x for x in range(64)]) 326 | mock_read_flash.return_value = (0x3E000, data_bytes) 327 | 328 | mock_extract_script.side_effect = Exception("Boom") 329 | 330 | try: 331 | cmds.read_python_code() 332 | except Exception as e: 333 | assert str(e) == "Could not decode the MicroPython code from flash" 334 | else: 335 | raise AssertionError("Expected excepion not thrown.") 336 | 337 | 338 | ############################################################################### 339 | # Hex comparison commands 340 | ############################################################################### 341 | @mock.patch("ubittool.cmds.Timer", autospec=True) 342 | @mock.patch("ubittool.cmds.webbrowser.open", autospec=True) 343 | def test_open_temp_html(mock_browser_open, mock_timer): 344 | """Check browser is requested with a file containing the given text.""" 345 | html_content = "hello world" 346 | 347 | cmds._open_temp_html(html_content) 348 | 349 | # Get the URL sent to the browser and check the content 350 | assert mock_browser_open.call_count == 1 351 | url = mock_browser_open.call_args[0][0] 352 | assert url.startswith("file://") 353 | file_path = url[7:] 354 | with open(file_path, "r") as tmp_file: 355 | read_content = tmp_file.read() 356 | assert read_content == html_content 357 | 358 | # Timer was mocked, so remove file manually 359 | os.remove(file_path) 360 | 361 | 362 | @mock.patch("ubittool.cmds.HtmlDiff", autospec=True) 363 | def test_gen_diff_html(mock_diff): 364 | """Check the HTML returned contains the provided inputs.""" 365 | from_title = "from_title_content" 366 | from_lines = "left content here" 367 | to_title = "to_title_content" 368 | to_lines = "different content on the right here" 369 | mock_diff.return_value.make_table.return_value = "{} {}".format( 370 | from_lines, to_lines 371 | ) 372 | 373 | html = cmds._gen_diff_html(from_title, [from_lines], to_title, [to_lines]) 374 | 375 | assert html.count(from_title) == 2 376 | assert html.count(from_lines) == 1 377 | assert html.count(to_title) == 2 378 | assert html.count(to_lines) == 1 379 | 380 | 381 | @mock.patch("ubittool.cmds.read_flash_hex", autospec=True) 382 | @mock.patch("ubittool.cmds._gen_diff_html", autospec=True) 383 | @mock.patch("ubittool.cmds._open_temp_html", autospec=True) 384 | def test_compare_full_flash_hex( 385 | mock_open_temp_html, mock_gen_diff_html, mock_read_flash_hex 386 | ): 387 | """Check that file contents.""" 388 | file_hex_path = os.path.join("path", "to", "file.hex") 389 | file_hex_content = "This is the hex file content" 390 | flash_hex_content = "This is the flash hex content" 391 | mock_read_flash_hex.return_value = flash_hex_content 392 | 393 | with mock.patch( 394 | "ubittool.cmds.open", mock.mock_open(read_data=file_hex_content) 395 | ) as m_open: 396 | cmds.compare_full_flash_hex(file_hex_path) 397 | 398 | m_open.assert_called_once_with(file_hex_path, encoding="utf-8") 399 | assert mock_read_flash_hex.call_count == 1 400 | assert mock_read_flash_hex.call_args[1] == {"decode_hex": False} 401 | assert mock_gen_diff_html.call_count == 1 402 | assert mock_gen_diff_html.call_args[0] == ( 403 | "micro:bit", 404 | [flash_hex_content], 405 | "Hex file", 406 | [file_hex_content], 407 | ) 408 | assert mock_open_temp_html.call_count == 1 409 | 410 | 411 | @mock.patch("ubittool.cmds.read_uicr_customer_hex", autospec=True) 412 | @mock.patch("ubittool.cmds._gen_diff_html", autospec=True) 413 | @mock.patch("ubittool.cmds._open_temp_html", autospec=True) 414 | def test_compare_uicr_customer( 415 | mock_open_temp_html, mock_gen_diff_html, mock_read_uicr_customer 416 | ): 417 | """Check that file contents.""" 418 | file_hex_path = os.path.join("path", "to", "file.hex") 419 | file_hex_content = "This is the hex file content" 420 | flash_hex_content = "This is the flash hex content" 421 | mock_read_uicr_customer.return_value = flash_hex_content 422 | 423 | with mock.patch( 424 | "ubittool.cmds.open", mock.mock_open(read_data=file_hex_content) 425 | ) as m_open: 426 | cmds.compare_uicr_customer(file_hex_path) 427 | 428 | m_open.assert_called_once_with(file_hex_path, encoding="utf-8") 429 | assert mock_read_uicr_customer.call_count == 1 430 | assert mock_read_uicr_customer.call_args[1] == {"decode_hex": False} 431 | assert mock_gen_diff_html.call_count == 1 432 | assert mock_gen_diff_html.call_args[0] == ( 433 | "micro:bit", 434 | [flash_hex_content], 435 | "Hex file", 436 | [file_hex_content], 437 | ) 438 | assert mock_open_temp_html.call_count == 1 439 | 440 | 441 | ############################################################################### 442 | # Flash commands 443 | ############################################################################### 444 | @mock.patch("ubittool.cmds.os.fsync", autospec=True) 445 | @mock.patch("ubittool.cmds.uflash.find_microbit", autospec=True) 446 | def test_flash_drag_n_drop(mock_find_microbit, mock_fsync): 447 | """Check the file copy flash function.""" 448 | fake_mb_path = "./not_a_real_MICROBIT_path" 449 | fake_hex_path = "not_a_real.hex" 450 | mock_find_microbit.return_value = fake_mb_path 451 | 452 | with mock.patch("builtins.open", mock.mock_open()) as m_open: 453 | cmds.flash_drag_n_drop(fake_hex_path) 454 | 455 | m_open.assert_any_call(fake_hex_path, "rb") 456 | m_open.assert_any_call(os.path.join(fake_mb_path, "input.hex"), "wb") 457 | 458 | 459 | @mock.patch("ubittool.cmds.uflash.find_microbit", autospec=True) 460 | def test_flash_drag_n_drop_no_mb(mock_find_microbit): 461 | """Check the file copy flash function.""" 462 | mock_find_microbit.return_value = None 463 | 464 | with pytest.raises(Exception) as exc_info: 465 | cmds.flash_drag_n_drop("not_a_real.hex") 466 | 467 | assert "Could not find a MICROBIT drive" in str(exc_info.value) 468 | 469 | 470 | @mock.patch.object(cmds.programmer.MicrobitMcu, "flash_hex", autospec=True) 471 | def test_flash_pyocd(mock_microbit_mcu_flash_hex): 472 | """Check the flash with PyOCD function.""" 473 | cmds.flash_pyocd("path/to/hex_file.hex") 474 | 475 | assert ( 476 | mock_microbit_mcu_flash_hex.call_args[0][1] == "path/to/hex_file.hex" 477 | ) 478 | -------------------------------------------------------------------------------- /tests/test_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Tests for GUI.""" 4 | import sys 5 | from unittest import mock 6 | import tkinter 7 | 8 | import pytest 9 | 10 | from ubittool.gui import UBitToolWindow, open_gui 11 | 12 | 13 | @pytest.fixture() 14 | def gui_window(): 15 | """Fixture to create and destroy GUI window.""" 16 | app = UBitToolWindow() 17 | app.wait_visibility() 18 | yield app 19 | if app: 20 | try: 21 | app.winfo_exists() 22 | except tkinter.TclError: 23 | # App destroyed, nothing left to do 24 | pass 25 | else: 26 | app.update() 27 | app.destroy() 28 | 29 | 30 | def test_window_console(capsys): 31 | """Test the std our and err go to the console widget.""" 32 | std_out_content = "This is content in the std out\n" 33 | std_err_content = "And this goes to the std err\n" 34 | 35 | with capsys.disabled(): 36 | app = UBitToolWindow() 37 | # app.wait_visibility() 38 | 39 | sys.stdout.write(std_out_content) 40 | sys.stderr.write(std_err_content) 41 | console_widget_content = app.console.get(1.0, "end") 42 | 43 | app.update() 44 | app.destroy() 45 | 46 | assert std_out_content in console_widget_content 47 | assert std_err_content in console_widget_content 48 | 49 | 50 | def test_menu_bar_presence(gui_window): 51 | """Test that the window menu is present with all expected options.""" 52 | file_index = 0 53 | microbit_index = 1 54 | nrf_index = 2 55 | 56 | def get_labels(menu): 57 | """Get all the labels from a menu.""" 58 | menu_len = menu.index("end") + 1 59 | labels = [] 60 | for x in range(menu_len): 61 | try: 62 | label = menu.entrycget(x, "label") 63 | except tkinter.TclError: 64 | pass 65 | else: 66 | labels.append(label) 67 | return labels 68 | 69 | menu_bar = gui_window.menu_bar 70 | assert menu_bar.winfo_exists(), "Menu bar exists" 71 | 72 | top_labels = get_labels(menu_bar) 73 | assert "File" == top_labels[file_index], "File present in window menu" 74 | assert ( 75 | "micro:bit" == top_labels[microbit_index] 76 | ), "micro:bit present in window menu" 77 | assert "nrf" == top_labels[nrf_index], "nrf present in window menu" 78 | 79 | file_labels = get_labels(menu_bar.winfo_children()[file_index]) 80 | assert len(file_labels) == 3, "File menu has 3 items" 81 | assert "Open" == file_labels[0], "Open present in File menu" 82 | assert "Save As" == file_labels[1], "Save As present in File menu" 83 | assert "Exit" == file_labels[2], "Exit present in File menu" 84 | 85 | microbit_labels = get_labels(menu_bar.winfo_children()[microbit_index]) 86 | assert len(microbit_labels) == 2, "micro:bit menu has 3 items" 87 | assert ( 88 | "Read MicroPython code" == microbit_labels[0] 89 | ), "Read Code present in micro:bit menu" 90 | assert ( 91 | "Read MicroPython runtime" == microbit_labels[1] 92 | ), "Read Runtime present in micro:bit menu" 93 | 94 | nrf_labels = get_labels(menu_bar.winfo_children()[nrf_index]) 95 | assert len(nrf_labels) == 9, "nrf menu has 8 items" 96 | assert ( 97 | "Read full flash contents (Intel Hex)" == nrf_labels[0] 98 | ), "Read Flash Hex present in nrf menu" 99 | assert ( 100 | "Read full flash contents (Pretty Hex)" == nrf_labels[1] 101 | ), "Read Flash UICR Pretty present in nrf menu" 102 | assert ( 103 | "Read full RAM contents (Intel Hex)" == nrf_labels[2] 104 | ), "Read RAM Hex present in nrf menu" 105 | assert ( 106 | "Read full RAM contents (Pretty Hex)" == nrf_labels[3] 107 | ), "Read RAM Pretty present in nrf menu" 108 | assert "Read UICR" == nrf_labels[4], "Read UICR present in nrf menu" 109 | assert ( 110 | "Read UICR Customer" == nrf_labels[5] 111 | ), "Read UICR Customer present in nrf menu" 112 | assert ( 113 | "Read full flash + UICR" == nrf_labels[6] 114 | ), "Read Flash UICR Hex present in nrf menu" 115 | assert ( 116 | "Compare full flash contents (Intel Hex)" == nrf_labels[7] 117 | ), "Compare Flash present in nrf menu" 118 | assert ( 119 | "Compare UICR Customer (Intel Hex)" == nrf_labels[8] 120 | ), "Compare UICR in nrf menu" 121 | 122 | 123 | @mock.patch("ubittool.gui.cmds.read_python_code", autospec=True) 124 | def test_read_python_code(mock_read_python_code, gui_window): 125 | """Tests the READ_CODE command.""" 126 | python_code = "The Python code from the flash" 127 | mock_read_python_code.return_value = python_code 128 | 129 | gui_window.ubit_menu.invoke(0) 130 | 131 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 132 | assert python_code == editor_content 133 | assert mock_read_python_code.call_count == 1 134 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 135 | gui_window.CMD_READ_CODE 136 | ) 137 | 138 | 139 | @mock.patch("ubittool.gui.cmds.read_micropython", autospec=True) 140 | def test_read_micropython(mock_read_micropython, gui_window): 141 | """Tests the READ_UPY command.""" 142 | upy_hex = "The MicroPython runtime in Intel Hex format data" 143 | mock_read_micropython.return_value = upy_hex 144 | 145 | gui_window.ubit_menu.invoke(1) 146 | 147 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 148 | assert upy_hex == editor_content 149 | assert mock_read_micropython.call_count == 1 150 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 151 | gui_window.CMD_READ_UPY 152 | ) 153 | 154 | 155 | @mock.patch("ubittool.gui.cmds.read_flash_hex", autospec=True) 156 | def test_read_full_flash_intel(mock_read_flash_hex, gui_window): 157 | """Tests the READ_FLASH_HEX command.""" 158 | flash_data = "The full flash in Intel Hex format data" 159 | mock_read_flash_hex.return_value = flash_data 160 | 161 | gui_window.nrf_menu.invoke(0) 162 | 163 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 164 | assert flash_data == editor_content 165 | assert mock_read_flash_hex.call_count == 1 166 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 167 | gui_window.CMD_READ_FLASH_HEX 168 | ) 169 | 170 | 171 | @mock.patch("ubittool.gui.cmds.read_flash_hex", autospec=True) 172 | def test_read_full_flash_pretty(mock_read_flash_hex, gui_window): 173 | """Tests the READ_FLASH_PRETTY command.""" 174 | flash_data = "The full flash in pretty format data" 175 | mock_read_flash_hex.return_value = flash_data 176 | 177 | gui_window.nrf_menu.invoke(1) 178 | 179 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 180 | assert flash_data == editor_content 181 | assert mock_read_flash_hex.call_count == 1 182 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 183 | gui_window.CMD_READ_FLASH_PRETTY 184 | ) 185 | 186 | 187 | @mock.patch("ubittool.gui.cmds.read_flash_uicr_hex", autospec=True) 188 | def test_read_full_flash_uicr_intel(mock_read_flash_uicr_hex, gui_window): 189 | """Tests the READ_FLASH_HEX command.""" 190 | flash_data = "The full flash in Intel Hex format data" 191 | mock_read_flash_uicr_hex.return_value = flash_data 192 | 193 | gui_window.nrf_menu.invoke(6) 194 | 195 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 196 | assert flash_data == editor_content 197 | assert mock_read_flash_uicr_hex.call_count == 1 198 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 199 | gui_window.CMD_READ_FLASH_UICR_HEX 200 | ) 201 | 202 | 203 | @mock.patch("ubittool.gui.cmds.read_ram_hex", autospec=True) 204 | def test_read_ram_intel(mock_read_ram_hex, gui_window): 205 | """Tests the CMD_READ_RAM_HEX command.""" 206 | ram_data = "The full RAM in Intel Hex format data" 207 | mock_read_ram_hex.return_value = ram_data 208 | 209 | gui_window.nrf_menu.invoke(2) 210 | 211 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 212 | assert ram_data == editor_content 213 | assert mock_read_ram_hex.call_count == 1 214 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 215 | gui_window.CMD_READ_RAM_HEX 216 | ) 217 | 218 | 219 | @mock.patch("ubittool.gui.cmds.read_ram_hex", autospec=True) 220 | def test_read_ram_pretty(mock_read_ram_hex, gui_window): 221 | """Tests the CMD_READ_RAM_PRETTY command.""" 222 | ram_data = "The full RAM in pretty format data" 223 | mock_read_ram_hex.return_value = ram_data 224 | 225 | gui_window.nrf_menu.invoke(3) 226 | 227 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 228 | assert ram_data == editor_content 229 | assert mock_read_ram_hex.call_count == 1 230 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 231 | gui_window.CMD_READ_RAM_PRETTY 232 | ) 233 | 234 | 235 | @mock.patch("ubittool.gui.cmds.read_uicr_hex", autospec=True) 236 | def test_read_uicr(mock_read_uicr, gui_window): 237 | """Tests the READ_UICR command.""" 238 | uicr_data = "The UICR data" 239 | mock_read_uicr.return_value = uicr_data 240 | 241 | gui_window.nrf_menu.invoke(4) 242 | 243 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 244 | assert uicr_data == editor_content 245 | assert mock_read_uicr.call_count == 1 246 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 247 | gui_window.CMD_READ_UICR 248 | ) 249 | 250 | 251 | @mock.patch("ubittool.gui.cmds.read_uicr_customer_hex", autospec=True) 252 | def test_read_uicr_customer(mock_read_uicr_customer, gui_window): 253 | """Tests the READ_UICR_CUSTOMER command.""" 254 | uicr_data = "The UICR CUSTOMER data" 255 | mock_read_uicr_customer.return_value = uicr_data 256 | 257 | gui_window.nrf_menu.invoke(5) 258 | 259 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 260 | assert uicr_data == editor_content 261 | assert mock_read_uicr_customer.call_count == 1 262 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 263 | gui_window.CMD_READ_UICR_CUSTOMER 264 | ) 265 | 266 | 267 | @mock.patch("ubittool.gui.cmds.compare_full_flash_hex", autospec=True) 268 | @mock.patch("ubittool.gui.tkFileDialog.askopenfilename", autospec=True) 269 | def test_compare_full_flash_intel( 270 | mock_compare_full_flash, mock_open_file, gui_window 271 | ): 272 | """Tests the COMPARE_FULL_FLASH command.""" 273 | mock_open_file.return_value = "/some/path" 274 | 275 | gui_window.nrf_menu.invoke(8) 276 | 277 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 278 | assert mock_compare_full_flash.call_count == 1 279 | assert editor_content == "Diff content loaded in default browser." 280 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 281 | gui_window.CMD_COMPARE_FLASH 282 | ) 283 | 284 | 285 | @mock.patch("ubittool.gui.cmds.compare_uicr_customer", autospec=True) 286 | @mock.patch("ubittool.gui.tkFileDialog.askopenfilename", autospec=True) 287 | def test_compare_uicr_customer( 288 | mock_compare_uicr_customer, mock_open_file, gui_window 289 | ): 290 | """Tests the READ_UICR command.""" 291 | mock_open_file.return_value = "/some/path" 292 | 293 | gui_window.nrf_menu.invoke(9) 294 | 295 | editor_content = gui_window.text_viewer.get(1.0, "end-1c") 296 | assert mock_compare_uicr_customer.call_count == 1 297 | assert editor_content == "Diff content loaded in default browser." 298 | assert gui_window.cmd_title.cmd_title.get() == "Command: {}".format( 299 | gui_window.CMD_COMPARE_UICR 300 | ) 301 | 302 | 303 | @mock.patch("ubittool.gui.UBitToolWindow", autospec=True) 304 | def test_open_gui(mock_window): 305 | """Test the app instance is created and main loop invoked.""" 306 | open_gui() 307 | 308 | assert mock_window.return_value.lift.call_count == 1 309 | assert mock_window.return_value.mainloop.call_count == 1 310 | 311 | 312 | def test_quit(): 313 | """Test that when the window is closed it deactivates the console.""" 314 | app = UBitToolWindow() 315 | app.wait_visibility() 316 | 317 | assert sys.stdout != sys.__stdout__ 318 | assert sys.stderr != sys.__stderr__ 319 | 320 | app.app_quit() 321 | 322 | assert sys.stdout == sys.__stdout__ 323 | assert sys.stderr == sys.__stderr__ 324 | try: 325 | app.winfo_exists() 326 | except tkinter.TclError: 327 | # App destroyed, nothing left to do 328 | assert True, "Window was already destroyed" 329 | else: 330 | raise AssertionError("Window is not destroyed") 331 | -------------------------------------------------------------------------------- /tests/test_programmer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Tests for programmer.py module.""" 4 | import types 5 | from unittest import mock 6 | 7 | import pytest 8 | 9 | from ubittool import programmer 10 | 11 | 12 | ############################################################################### 13 | # Helpers 14 | ############################################################################### 15 | def MicrobitMcu_instance(v=1): 16 | """Patched version for v1.""" 17 | 18 | def _connec_v1(self): 19 | self.mem = programmer.MEM_REGIONS_MB_V1 20 | 21 | def _connec_v2(self): 22 | self.mem = programmer.MEM_REGIONS_MB_V2 23 | 24 | mb = programmer.MicrobitMcu() 25 | if v == 1: 26 | mb._connect = types.MethodType(_connec_v1, mb) 27 | elif v == 2: 28 | mb._connect = types.MethodType(_connec_v2, mb) 29 | return mb 30 | 31 | 32 | ############################################################################### 33 | # MicrobitMcu.read_flash() 34 | ############################################################################### 35 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 36 | def test_read_flash(mock_read_memory): 37 | """Test read_flash() with default arguments.""" 38 | data_bytes = bytes([x for x in range(256)] * 4) 39 | mock_read_memory.return_value = data_bytes 40 | mb1 = MicrobitMcu_instance(v=1) 41 | mb2 = MicrobitMcu_instance(v=2) 42 | 43 | start_addres1, result_data1 = mb1.read_flash() 44 | start_addres2, result_data2 = mb2.read_flash() 45 | 46 | assert start_addres1 == 0 47 | assert result_data2 == data_bytes 48 | assert start_addres1 == 0 49 | assert result_data2 == data_bytes 50 | 51 | 52 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 53 | def test_read_flash_with_args(mock_read_memory): 54 | """Test read_flash() with providedarguments.""" 55 | data_bytes = bytes([x for x in range(256)] * 4) 56 | mock_read_memory.return_value = data_bytes 57 | mb1 = MicrobitMcu_instance(v=1) 58 | mb2 = MicrobitMcu_instance(v=2) 59 | 60 | start_addres1, result_data1 = mb1.read_flash(address=0, count=256 * 1024) 61 | start_addres2, result_data2 = mb2.read_flash(address=0, count=512 * 1024) 62 | 63 | assert start_addres1 == 0 64 | assert result_data1 == data_bytes 65 | assert start_addres2 == 0 66 | assert result_data2 == data_bytes 67 | 68 | 69 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 70 | def test_read_flash_bad_address(mock_read_memory): 71 | """Test read_flash() with bad address arguments.""" 72 | data_bytes = bytes([x for x in range(256)] * 4) 73 | mock_read_memory.return_value = data_bytes 74 | mb1 = MicrobitMcu_instance(v=1) 75 | mb2 = MicrobitMcu_instance(v=2) 76 | 77 | with pytest.raises(ValueError) as execinfo11: 78 | start_addres, result_data = mb1.read_flash(address=-1, count=1) 79 | with pytest.raises(ValueError) as execinfo12: 80 | start_addres, result_data = mb1.read_flash( 81 | address=(256 * 1024) + 1, count=1 82 | ) 83 | with pytest.raises(ValueError) as execinfo21: 84 | start_addres, result_data = mb2.read_flash(address=-1, count=1) 85 | with pytest.raises(ValueError) as execinfo22: 86 | start_addres, result_data = mb2.read_flash( 87 | address=(512 * 1024) + 1, count=1 88 | ) 89 | 90 | assert "Cannot read a flash address out of" in str(execinfo11.value) 91 | assert "Cannot read a flash address out of" in str(execinfo12.value) 92 | assert "Cannot read a flash address out of" in str(execinfo21.value) 93 | assert "Cannot read a flash address out of" in str(execinfo22.value) 94 | 95 | 96 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 97 | def test_read_flash_bad_count(mock_read_memory): 98 | """Test read_flash() with bad values in the count argument.""" 99 | data_bytes = bytes([x for x in range(256)] * 4) 100 | mock_read_memory.return_value = data_bytes 101 | mb1 = MicrobitMcu_instance(v=1) 102 | mb2 = MicrobitMcu_instance(v=2) 103 | 104 | with pytest.raises(ValueError) as execinfo11: 105 | start_addres, result_data = mb1.read_flash( 106 | address=0, count=(256 * 1024) + 1 107 | ) 108 | with pytest.raises(ValueError) as execinfo12: 109 | start_addres, result_data = mb1.read_flash( 110 | address=(256 * 1024) - 10, count=11 111 | ) 112 | with pytest.raises(ValueError) as execinfo21: 113 | start_addres, result_data = mb2.read_flash( 114 | address=0, count=(512 * 1024) + 1 115 | ) 116 | with pytest.raises(ValueError) as execinfo22: 117 | start_addres, result_data = mb2.read_flash( 118 | address=(512 * 1024) - 10, count=11 119 | ) 120 | 121 | assert "Cannot read a flash address out of" in str(execinfo11.value) 122 | assert "Cannot read a flash address out of" in str(execinfo12.value) 123 | assert "Cannot read a flash address out of" in str(execinfo21.value) 124 | assert "Cannot read a flash address out of" in str(execinfo22.value) 125 | 126 | 127 | ############################################################################### 128 | # MicrobitMcu.read_ram() 129 | ############################################################################### 130 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 131 | def test_read_ram(mock_read_memory): 132 | """Test read_ram() with default arguments.""" 133 | data_bytes = bytes([x for x in range(256)] * 4) 134 | mock_read_memory.return_value = data_bytes 135 | mb1 = MicrobitMcu_instance(v=1) 136 | mb2 = MicrobitMcu_instance(v=2) 137 | 138 | start_addres1, result_data1 = mb1.read_ram() 139 | start_addres2, result_data2 = mb2.read_ram() 140 | 141 | assert start_addres1 == 0x2000_0000 142 | assert result_data2 == data_bytes 143 | assert start_addres1 == 0x2000_0000 144 | assert result_data2 == data_bytes 145 | 146 | 147 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 148 | def test_read_ram_with_args(mock_read_memory): 149 | """Test read_ram() with providedarguments.""" 150 | data_bytes = bytes([x for x in range(256)] * 4) 151 | mock_read_memory.return_value = data_bytes 152 | mb1 = MicrobitMcu_instance(v=1) 153 | mb2 = MicrobitMcu_instance(v=2) 154 | 155 | start_addres1, result_data1 = mb1.read_ram( 156 | address=0x2000_0000, count=16 * 1024 157 | ) 158 | start_addres2, result_data2 = mb2.read_ram( 159 | address=0x2000_0000, count=128 * 1024 160 | ) 161 | 162 | assert start_addres1 == 0x2000_0000 163 | assert result_data1 == data_bytes 164 | assert start_addres2 == 0x2000_0000 165 | assert result_data2 == data_bytes 166 | 167 | 168 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 169 | def test_read_ram_bad_address(mock_read_memory): 170 | """Test read_ram() with bad address arguments.""" 171 | data_bytes = bytes([x for x in range(256)] * 4) 172 | mock_read_memory.return_value = data_bytes 173 | mb1 = MicrobitMcu_instance(v=1) 174 | mb2 = MicrobitMcu_instance(v=2) 175 | 176 | with pytest.raises(ValueError) as execinfo11: 177 | start_addres, result_data = mb1.read_ram( 178 | address=0x2000_0000 - 1, count=1 179 | ) 180 | with pytest.raises(ValueError) as execinfo12: 181 | start_addres, result_data = mb1.read_ram( 182 | address=(16 * 1024) + 1, count=1 183 | ) 184 | with pytest.raises(ValueError) as execinfo21: 185 | start_addres, result_data = mb2.read_ram( 186 | address=0x2000_0000 - 1, count=1 187 | ) 188 | with pytest.raises(ValueError) as execinfo22: 189 | start_addres, result_data = mb2.read_ram( 190 | address=(128 * 1024) + 1, count=1 191 | ) 192 | 193 | assert "Cannot read a RAM location out of" in str(execinfo11.value) 194 | assert "Cannot read a RAM location out of" in str(execinfo12.value) 195 | assert "Cannot read a RAM location out of" in str(execinfo21.value) 196 | assert "Cannot read a RAM location out of" in str(execinfo22.value) 197 | 198 | 199 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 200 | def test_read_ram_bad_count(mock_read_memory): 201 | """Test read_ram() with bad values in the count argument.""" 202 | data_bytes = bytes([x for x in range(256)] * 4) 203 | mock_read_memory.return_value = data_bytes 204 | mb1 = MicrobitMcu_instance(v=1) 205 | mb2 = MicrobitMcu_instance(v=2) 206 | 207 | with pytest.raises(ValueError) as execinfo11: 208 | start_addres, result_data = mb1.read_ram( 209 | address=0x2000_0000, count=(16 * 1024) + 1 210 | ) 211 | with pytest.raises(ValueError) as execinfo12: 212 | start_addres, result_data = mb1.read_ram( 213 | address=0x2000_0000 + (16 * 1024) - 10, count=11 214 | ) 215 | with pytest.raises(ValueError) as execinfo21: 216 | start_addres, result_data = mb2.read_ram( 217 | address=0x2000_0000, count=(128 * 1024) + 1 218 | ) 219 | with pytest.raises(ValueError) as execinfo22: 220 | start_addres, result_data = mb2.read_ram( 221 | address=0x2000_0000 + (128 * 1024) - 10, count=11 222 | ) 223 | 224 | assert "Cannot read a RAM location out of" in str(execinfo11.value) 225 | assert "Cannot read a RAM location out of" in str(execinfo12.value) 226 | assert "Cannot read a RAM location out of" in str(execinfo21.value) 227 | assert "Cannot read a RAM location out of" in str(execinfo22.value) 228 | 229 | 230 | ############################################################################### 231 | # MicrobitMcu.read_uicr() 232 | ############################################################################### 233 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 234 | def test_read_uicr(mock_read_memory): 235 | """Test read_uicr() with default arguments.""" 236 | data_bytes = bytes([x for x in range(256)] * 4) 237 | mock_read_memory.return_value = data_bytes 238 | mb1 = MicrobitMcu_instance(v=1) 239 | mb2 = MicrobitMcu_instance(v=2) 240 | 241 | start_addres1, result_data1 = mb1.read_uicr() 242 | start_addres2, result_data2 = mb2.read_uicr() 243 | 244 | assert start_addres1 == 0x1000_1000 245 | assert result_data1 == data_bytes 246 | assert start_addres2 == 0x1000_1000 247 | assert result_data2 == data_bytes 248 | 249 | 250 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 251 | def test_read_uicr_with_args(mock_read_memory): 252 | """Test read_uicr() with providedarguments.""" 253 | data_bytes = bytes([x for x in range(256)] * 4) 254 | mock_read_memory.return_value = data_bytes 255 | mb1 = MicrobitMcu_instance(v=1) 256 | mb2 = MicrobitMcu_instance(v=2) 257 | 258 | start_addres1, result_data1 = mb1.read_uicr( 259 | address=0x1000_1000, count=0x100 260 | ) 261 | start_addres2, result_data2 = mb2.read_uicr( 262 | address=0x1000_1000, count=0x308 263 | ) 264 | 265 | assert start_addres1 == 0x1000_1000 266 | assert result_data1 == data_bytes 267 | assert start_addres2 == 0x1000_1000 268 | assert result_data2 == data_bytes 269 | 270 | 271 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 272 | def test_read_uicr_bad_address(mock_read_memory): 273 | """Test read_uicr() with bad address arguments.""" 274 | data_bytes = bytes([x for x in range(256)] * 4) 275 | mock_read_memory.return_value = data_bytes 276 | mb1 = MicrobitMcu_instance(v=1) 277 | mb2 = MicrobitMcu_instance(v=2) 278 | 279 | with pytest.raises(ValueError) as execinfo11: 280 | start_addres, result_data = mb1.read_uicr( 281 | address=0x1000_1000 - 1, count=1 282 | ) 283 | with pytest.raises(ValueError) as execinfo12: 284 | start_addres, result_data = mb1.read_uicr(address=0x100 + 1, count=1) 285 | with pytest.raises(ValueError) as execinfo21: 286 | start_addres, result_data = mb2.read_uicr( 287 | address=0x1000_1000 - 1, count=1 288 | ) 289 | with pytest.raises(ValueError) as execinfo22: 290 | start_addres, result_data = mb2.read_uicr(address=0x308 + 1, count=1) 291 | 292 | assert "Cannot read a UICR location out of" in str(execinfo11.value) 293 | assert "Cannot read a UICR location out of" in str(execinfo12.value) 294 | assert "Cannot read a UICR location out of" in str(execinfo21.value) 295 | assert "Cannot read a UICR location out of" in str(execinfo22.value) 296 | 297 | 298 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 299 | def test_read_uicr_bad_count(mock_read_memory): 300 | """Test read_uicr() with bad values in the count argument.""" 301 | data_bytes = bytes([x for x in range(256)] * 4) 302 | mock_read_memory.return_value = data_bytes 303 | mb1 = MicrobitMcu_instance(v=1) 304 | mb2 = MicrobitMcu_instance(v=2) 305 | 306 | with pytest.raises(ValueError) as execinfo11: 307 | start_addres, result_data = mb1.read_uicr( 308 | address=0x1000_1000, count=(16 * 1024) + 1 309 | ) 310 | with pytest.raises(ValueError) as execinfo12: 311 | start_addres, result_data = mb1.read_uicr( 312 | address=0x1000_1000 + (16 * 1024) - 10, count=11 313 | ) 314 | with pytest.raises(ValueError) as execinfo21: 315 | start_addres, result_data = mb2.read_uicr( 316 | address=0x1000_1000, count=(128 * 1024) + 1 317 | ) 318 | with pytest.raises(ValueError) as execinfo22: 319 | start_addres, result_data = mb2.read_uicr( 320 | address=0x1000_1000 + (128 * 1024) - 10, count=11 321 | ) 322 | 323 | assert "Cannot read a UICR location out of" in str(execinfo11.value) 324 | assert "Cannot read a UICR location out of" in str(execinfo12.value) 325 | assert "Cannot read a UICR location out of" in str(execinfo21.value) 326 | assert "Cannot read a UICR location out of" in str(execinfo22.value) 327 | 328 | 329 | ############################################################################### 330 | # MicrobitMcu.read_uicr_customer() 331 | ############################################################################### 332 | @mock.patch.object(programmer.MicrobitMcu, "_read_memory", autospec=True) 333 | def test_read_uicr_customer(mock_read_memory): 334 | """Test read_uiread_uicr_customercr() with default arguments.""" 335 | data_bytes = bytes([x for x in range(256)] * 4) 336 | mock_read_memory.return_value = data_bytes 337 | mb1 = MicrobitMcu_instance(v=1) 338 | mb2 = MicrobitMcu_instance(v=2) 339 | 340 | start_addres1, result_data1 = mb1.read_uicr_customer() 341 | start_addres2, result_data2 = mb2.read_uicr_customer() 342 | 343 | assert start_addres1 == 0x1000_1080 344 | assert result_data1 == data_bytes 345 | assert start_addres2 == 0x1000_1080 346 | assert result_data2 == data_bytes 347 | 348 | 349 | ############################################################################### 350 | # find_microbit_ids() 351 | ############################################################################### 352 | @mock.patch( 353 | "ubittool.programmer.ConnectHelper.get_all_connected_probes", autospec=True 354 | ) 355 | def test_find_microbit_ids(mock_get_all_connected_probes): 356 | """Test find_microbit_ids().""" 357 | 358 | class MockProbe: 359 | def __init__(self, _id): 360 | self.unique_id = _id 361 | 362 | mock_get_all_connected_probes.return_value = [ 363 | MockProbe("9900"), 364 | MockProbe("9901"), 365 | MockProbe("9903"), 366 | MockProbe("9904"), 367 | MockProbe("9905"), 368 | ] 369 | 370 | ids = programmer.find_microbit_ids() 371 | 372 | assert ids == ("9900", "9901", "9903", "9904", "9905") 373 | 374 | 375 | @mock.patch( 376 | "ubittool.programmer.ConnectHelper.get_all_connected_probes", autospec=True 377 | ) 378 | def test_find_microbit_ids_empty(mock_get_all_connected_probes): 379 | """Test find_microbit_ids().""" 380 | mock_get_all_connected_probes.return_value = [] 381 | 382 | ids = programmer.find_microbit_ids() 383 | 384 | assert ids == tuple() 385 | -------------------------------------------------------------------------------- /tests/test_system_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Run simple system tests on the CLI program. 4 | 5 | The actual unit tests for the cli module are in the test_cli.py file. 6 | Here we only do a simple command line invocation to ensure we can access the 7 | app via terminal, and we trust that the library click implements all the 8 | commands as it is a fully tested package. 9 | """ 10 | import subprocess 11 | import sys 12 | 13 | import pytest 14 | 15 | from ubittool import cli, cmds, __version__ 16 | 17 | 18 | def _run_cli_cmd(cmd_list): 19 | """Run a shell command and return the output. 20 | 21 | :param cmd_list: A list of strings that make up the command to execute. 22 | """ 23 | try: 24 | return subprocess.check_output(cmd_list, stderr=subprocess.STDOUT) 25 | except subprocess.CalledProcessError as e: 26 | return e.output 27 | 28 | 29 | def _ubittool_cmd(cmd_list): 30 | """Invoke the uBitTool app using different methods and return outputs. 31 | 32 | :param cmd_list: List of cli argument to add to ubittool invocation. 33 | """ 34 | module = [sys.executable, "-m", "ubittool"] 35 | module.extend(cmd_list) 36 | script = [sys.executable, "ubittool/cli.py"] 37 | script.extend(cmd_list) 38 | return [_run_cli_cmd(module), _run_cli_cmd(script)] 39 | 40 | 41 | @pytest.fixture 42 | def check_no_board_connected(): 43 | """Check that there is no mbed board that PyOCD can connect to.""" 44 | try: 45 | cmds._read_continuous_memory(address=0x00, count=16) 46 | except Exception: 47 | # Good: Exception raised if no board is found 48 | pass 49 | else: 50 | raise Exception("Found an Mbed device connected, please unplug.") 51 | 52 | 53 | def test_help(): 54 | """Check the help option works.""" 55 | outputs = _ubittool_cmd(["--help"]) 56 | for output in outputs: 57 | assert b"Usage: ubit [OPTIONS] COMMAND [ARGS]..." in output 58 | assert str.encode("uBitTool v{}".format(__version__)) in output 59 | assert str.encode(cli.__doc__) in output 60 | 61 | 62 | def test_read_code(check_no_board_connected): 63 | """Check the read-code command returns an error when no board connected.""" 64 | outputs = _ubittool_cmd(["read-code"]) 65 | for output in outputs: 66 | assert b"Executing: Extract the MicroPython code" in output 67 | assert b"Did not find any connected boards." in output 68 | -------------------------------------------------------------------------------- /ubittool/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Package init.""" 4 | 5 | # Semantic Versioning 2.0.0 https://semver.org/spec/v2.0.0.html 6 | __version__ = "0.8.0" 7 | -------------------------------------------------------------------------------- /ubittool/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Entry point for the program, checks to run GUI or CLI.""" 4 | from ubittool.cli import main as cli 5 | 6 | 7 | def main(): 8 | """Launch the command line interface.""" 9 | cli() 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /ubittool/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """CLI and GUI utility to read content from the micro:bit.""" 4 | import os 5 | import sys 6 | 7 | import click 8 | 9 | from ubittool import __version__ 10 | from ubittool.cmds import ( 11 | read_flash_hex, 12 | read_flash_uicr_hex, 13 | read_python_code, 14 | flash_drag_n_drop, 15 | batch_flash_hex, 16 | compare_full_flash_hex, 17 | ) 18 | 19 | # GUI depends on tkinter, which could be packaged separately from Python or 20 | # excluded from CLI-only packing, but the other CLI commands should still work 21 | try: 22 | from ubittool.gui import open_gui 23 | 24 | GUI_AVAILABLE = True 25 | except ImportError: # pragma: no cover 26 | GUI_AVAILABLE = False 27 | 28 | 29 | @click.group(help="uBitTool v{}.\n\n{}".format(__version__, __doc__)) 30 | def cli(): 31 | """Click entry point.""" 32 | pass 33 | 34 | 35 | def _file_checker(subject, file_path): 36 | """Check if a file exists and informs user about content output. 37 | 38 | :param subject: Very short file description, subject for printed sentences. 39 | :param file_path: Path to the file to check. 40 | """ 41 | if file_path: 42 | if os.path.exists(file_path): 43 | click.echo( 44 | click.style( 45 | "Abort: The {} file already exists.", fg="red" 46 | ).format(file_path), 47 | err=True, 48 | ) 49 | sys.exit(1) 50 | else: 51 | click.echo("{} will be written to: {}".format(subject, file_path)) 52 | else: 53 | click.echo("{} will be output to console.".format(subject)) 54 | 55 | 56 | @cli.command() 57 | @click.option( 58 | "-f", 59 | "--file_path", 60 | "file_path", 61 | type=click.Path(), 62 | help="Path to the output file to write the MicroPython code.", 63 | ) 64 | def read_code(file_path=None): 65 | """Extract the MicroPython code to a file or print it.""" 66 | click.echo("Executing: {}\n".format(read_code.__doc__)) 67 | _file_checker("MicroPython code", file_path) 68 | 69 | click.echo("Reading the micro:bit flash contents...") 70 | try: 71 | python_code = read_python_code() 72 | except Exception as e: 73 | click.echo(click.style("Error: {}", fg="red").format(e), err=True) 74 | sys.exit(1) 75 | 76 | if file_path: 77 | click.echo("Saving the MicroPython code...") 78 | with open(file_path, "w") as python_file: 79 | python_file.write(python_code) 80 | else: 81 | click.echo("Printing the MicroPython code") 82 | click.echo("----------------------------------------") 83 | click.echo(python_code) 84 | click.echo("----------------------------------------") 85 | 86 | click.echo("\nFinished successfully!") 87 | 88 | 89 | @cli.command( 90 | short_help="Read the micro:bit flash contents into a hex file or console." 91 | ) 92 | @click.option( 93 | "-f", 94 | "--file_path", 95 | "file_path", 96 | type=click.Path(), 97 | help="Path to the output file to write micro:bit flash content.", 98 | ) 99 | def read_flash(file_path=None): 100 | """Read the micro:bit flash contents into a hex file or console.""" 101 | click.echo("Executing: {}\n".format(read_flash.__doc__)) 102 | _file_checker("micro:bit flash hex", file_path) 103 | 104 | click.echo("Reading the micro:bit flash contents...") 105 | try: 106 | flash_data = read_flash_hex() 107 | except Exception as e: 108 | click.echo(click.style("Error: {}", fg="red").format(e), err=True) 109 | sys.exit(1) 110 | 111 | if file_path: 112 | click.echo("Saving the flash contents...") 113 | with open(file_path, "w") as hex_file: 114 | hex_file.write(flash_data) 115 | else: 116 | click.echo("Printing the flash contents") 117 | click.echo("----------------------------------------") 118 | click.echo(flash_data) 119 | click.echo("----------------------------------------") 120 | 121 | click.echo("\nFinished successfully!") 122 | 123 | 124 | @cli.command( 125 | short_help="Read the micro:bit flash and UICR into a hex file or console." 126 | ) 127 | @click.option( 128 | "-f", 129 | "--file_path", 130 | "file_path", 131 | type=click.Path(), 132 | help="Path to the output file to write micro:bit flash content.", 133 | ) 134 | def read_flash_uicr(file_path=None): 135 | """Read the micro:bit flash and UICR into a hex file or console.""" 136 | click.echo("Executing: {}\n".format(read_flash_uicr.__doc__)) 137 | _file_checker("micro:bit flash and UICR hex", file_path) 138 | 139 | click.echo("Reading the micro:bit flash and UICR contents...") 140 | try: 141 | flash_data = read_flash_uicr_hex() 142 | except Exception as e: 143 | click.echo(click.style("Error: {}", fg="red").format(e), err=True) 144 | sys.exit(1) 145 | 146 | if file_path: 147 | click.echo("Saving the flash and UICR contents...") 148 | with open(file_path, "w") as hex_file: 149 | hex_file.write(flash_data) 150 | else: 151 | click.echo("Printing the flash and UICR contents") 152 | click.echo("----------------------------------------") 153 | click.echo(flash_data) 154 | click.echo("----------------------------------------") 155 | 156 | click.echo("\nFinished successfully!") 157 | 158 | 159 | @cli.command() 160 | @click.option( 161 | "-f", 162 | "--file_path", 163 | "file_path", 164 | type=click.Path(), 165 | required=True, 166 | help="Path to the hex file to compare against the micro:bit.", 167 | ) 168 | def compare(file_path): 169 | """Compare the micro:bit flash contents with a hex file. 170 | 171 | Opens the default browser to display an HTML page with the comparison 172 | output. 173 | """ 174 | click.echo("Executing: Compare the micro:bit flash with a hex file.\n") 175 | if not file_path or not os.path.isfile(file_path): 176 | click.echo( 177 | click.style("Abort: File does not exists", fg="red"), err=True 178 | ) 179 | sys.exit(1) 180 | 181 | click.echo("Reading the micro:bit flash contents...") 182 | try: 183 | exit_code = compare_full_flash_hex(file_path) 184 | except Exception as e: 185 | click.echo(click.style("Error: {}", fg="red").format(e), err=True) 186 | sys.exit(1) 187 | click.echo("Diff output loaded in the default browser.") 188 | 189 | if exit_code: 190 | click.echo("\nThere are some differences in the micro:bit flash!") 191 | sys.exit(exit_code) 192 | else: 193 | click.echo("\nNo diffs between micro:bit flash and hex file :)") 194 | click.echo("Finished successfully.") 195 | 196 | 197 | @cli.command( 198 | short_help="Copy a hex file into the MICROBIT drive, read back the " 199 | "flash contents, and compare them with a hex file." 200 | ) 201 | @click.option( 202 | "-i", 203 | "--input_file_path", 204 | "input_file_path", 205 | type=click.Path(), 206 | required=True, 207 | help="Path to the hex file to flash into the micro:bit.", 208 | ) 209 | @click.option( 210 | "-c", 211 | "--compare_file_path", 212 | "compare_file_path", 213 | type=click.Path(), 214 | required=True, 215 | help="Path to the hex file to compare against the micro:bit flash.", 216 | ) 217 | def flash_compare(compare_file_path, input_file_path): 218 | """Flash the micro:bit and compare its flash contents with a hex file. 219 | 220 | Opens the default browser to display an HTML page with the comparison 221 | output. 222 | """ 223 | click.echo("Executing: Compare the micro:bit flash with a hex file.\n") 224 | abort = "Abort: File '{}' does not exists" 225 | if not input_file_path or not os.path.isfile(input_file_path): 226 | click.echo( 227 | click.style(abort.format(input_file_path), fg="red"), err=True 228 | ) 229 | sys.exit(1) 230 | if not compare_file_path or not os.path.isfile(compare_file_path): 231 | click.echo( 232 | click.style(abort.format(compare_file_path), fg="red"), err=True 233 | ) 234 | sys.exit(1) 235 | 236 | try: 237 | click.echo( 238 | "Copying '{}' file to MICROBIT drive...".format(input_file_path) 239 | ) 240 | flash_drag_n_drop(input_file_path) 241 | click.echo("Reading the micro:bit flash contents...") 242 | compare_full_flash_hex(compare_file_path) 243 | except Exception as e: 244 | click.echo(click.style("Error: {}", fg="red").format(e), err=True) 245 | sys.exit(1) 246 | click.echo("Diff output loaded in the default browser.") 247 | 248 | click.echo("\nFinished successfully!") 249 | 250 | 251 | @cli.command() 252 | @click.option( 253 | "-f", 254 | "--file-path", 255 | "file_path", 256 | type=click.Path(), 257 | required=True, 258 | help="Path to the hex file to flash into all micro:bits.", 259 | ) 260 | def batch_flash(file_path): 261 | """Flash any micro:bit connected until Ctrl+C is pressed.""" 262 | click.echo("Executing: Batch flash of hex files") 263 | if not file_path or not os.path.isfile(file_path): 264 | click.echo( 265 | click.style("Abort: File does not exists", fg="red"), err=True 266 | ) 267 | sys.exit(1) 268 | 269 | click.echo( 270 | f"Any micro:bit connected via USB will be flashed with {file_path}" 271 | ) 272 | try: 273 | batch_flash_hex(file_path) 274 | except KeyboardInterrupt: 275 | click.echo(click.style("Aborted by user.", fg="red"), err=True) 276 | sys.exit(0) 277 | 278 | 279 | if GUI_AVAILABLE: 280 | 281 | @cli.command() 282 | def gui(): 283 | """Launch the GUI version of this app (has more options).""" 284 | open_gui() 285 | 286 | 287 | def main(): 288 | """Command line interface entry point.""" 289 | cli(prog_name="ubit") 290 | 291 | 292 | if __name__ == "__main__": 293 | main() 294 | -------------------------------------------------------------------------------- /ubittool/cmds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Commands to carry out all the actions for uBitTool control. 5 | 6 | This module internal functions can read any memory location from the micro:bit 7 | via PyOCD, which uses the CMSIS interface provided by DAPLink. 8 | 9 | The exposed function for the command line and GUI interfaces can safely read 10 | areas of Flash (full flash, MicroPython, Python code) and UICR (to read the 11 | customer data), and format the output into Intel Hex, a nicely decoded string 12 | format, or human readable text (for the Python code). 13 | """ 14 | import os 15 | import sys 16 | import time 17 | import tempfile 18 | import webbrowser 19 | import multiprocessing 20 | from io import StringIO 21 | from threading import Timer 22 | from collections import namedtuple 23 | from difflib import HtmlDiff, unified_diff 24 | from traceback import format_exc 25 | 26 | import uflash 27 | from intelhex import IntelHex 28 | 29 | from ubittool import programmer 30 | 31 | 32 | DataAndOffset = namedtuple("DataAndOffset", ["data", "offset"]) 33 | 34 | 35 | # 36 | # Data format conversions 37 | # 38 | def _bytes_to_intel_hex(data_offsets): 39 | """Take a list of bytes and return a string in the Intel Hex format. 40 | 41 | :param data: List of integers, each representing a single byte. 42 | :param offset: Start address offset. 43 | :return: A string with the Intel Hex encoded data. 44 | """ 45 | i_hex = IntelHex() 46 | for do in data_offsets: 47 | i_hex.frombytes(do.data, do.offset) 48 | 49 | fake_file = StringIO() 50 | try: 51 | i_hex.tofile(fake_file, format="hex", byte_count=16) 52 | except IOError as e: 53 | sys.stderr.write("ERROR: File write: {}\n{}".format(fake_file, str(e))) 54 | return 55 | 56 | intel_hex_str = fake_file.getvalue() 57 | fake_file.close() 58 | return intel_hex_str 59 | 60 | 61 | def _bytes_to_pretty_hex(data_offsets): 62 | """Convert a list of bytes to a nicely formatted ASCII decoded hex string. 63 | 64 | :param data: List of integers, each representing a single byte. 65 | :param offset: Start address offset. 66 | :return: A string with the formatted hex data. 67 | """ 68 | i_hex = IntelHex() 69 | for do in data_offsets: 70 | i_hex.frombytes(do.data, do.offset) 71 | 72 | fake_file = StringIO() 73 | try: 74 | i_hex.dump(tofile=fake_file, width=16, withpadding=False) 75 | except IOError as e: 76 | sys.stderr.write("ERROR: File write: {}\n{}".format(fake_file, str(e))) 77 | return 78 | 79 | pretty_hex_str = fake_file.getvalue() 80 | fake_file.close() 81 | return pretty_hex_str 82 | 83 | 84 | # 85 | # Reading data commands 86 | # 87 | def read_flash_hex(decode_hex=False, **kwargs): 88 | """Read data from the flash memory and return as a hex string. 89 | 90 | Read as a number of bytes of the micro:bit flash from the given address. 91 | Can return it in Intel Hex format or a pretty formatted and decoded hex 92 | string. 93 | 94 | :param address: Integer indicating the start address to read. 95 | :param count: Integer indicating hoy many bytes to read. 96 | :param decode_hex: True selects nice decoded format, False selects Intel 97 | Hex format. 98 | :return: String with the hex formatted as indicated. 99 | """ 100 | with programmer.MicrobitMcu() as mb: 101 | start_address, flash_data = mb.read_flash(**kwargs) 102 | to_hex = _bytes_to_pretty_hex if decode_hex else _bytes_to_intel_hex 103 | return to_hex([DataAndOffset(flash_data, start_address)]) 104 | 105 | 106 | def read_flash_uicr_hex(decode_hex=False, **kwargs): 107 | """Read data from the flash memory and the UICR and return as a hex string. 108 | 109 | Read as a number of bytes of the micro:bit flash from the given address. 110 | Can return it in Intel Hex format or a pretty formatted and decoded hex 111 | string. 112 | 113 | :param address: Integer indicating the start address to read. 114 | :param count: Integer indicating hoy many bytes to read. 115 | :param decode_hex: True selects nice decoded format, False selects Intel 116 | Hex format. 117 | :return: String with the hex formatted as indicated. 118 | """ 119 | with programmer.MicrobitMcu() as mb: 120 | flash_start, flash_data = mb.read_flash(**kwargs) 121 | uicr_start, uicr_data = mb.read_uicr() 122 | to_hex = _bytes_to_pretty_hex if decode_hex else _bytes_to_intel_hex 123 | return to_hex( 124 | [ 125 | DataAndOffset(flash_data, flash_start), 126 | DataAndOffset(uicr_data, uicr_start), 127 | ] 128 | ) 129 | 130 | 131 | def read_ram_hex(decode_hex=False, **kwargs): 132 | """Read data from RAM and return as a hex string. 133 | 134 | Read as a number of bytes of the micro:bit RAM from the given address. 135 | Can return it in Intel Hex format or a pretty formatted and decoded hex 136 | string. 137 | 138 | :param address: Integer indicating the start address to read. 139 | :param count: Integer indicating hoy many bytes to read. 140 | :param decode_hex: True selects nice decoded format, False selects Intel 141 | Hex format. 142 | :return: String with the hex formatted as indicated. 143 | """ 144 | with programmer.MicrobitMcu() as mb: 145 | start_address, ram_data = mb.read_ram(**kwargs) 146 | to_hex = _bytes_to_pretty_hex if decode_hex else _bytes_to_intel_hex 147 | return to_hex([DataAndOffset(ram_data, start_address)]) 148 | 149 | 150 | def read_uicr_hex(decode_hex=False): 151 | """Read the full UICR data. 152 | 153 | :return: String with the nicely decoded UICR area data. 154 | """ 155 | with programmer.MicrobitMcu() as mb: 156 | start_address, uicr_data = mb.read_uicr() 157 | to_hex = _bytes_to_pretty_hex if decode_hex else _bytes_to_intel_hex 158 | return to_hex([DataAndOffset(uicr_data, start_address)]) 159 | 160 | 161 | def read_uicr_customer_hex(decode_hex=False): 162 | """Read the UICR Customer data. 163 | 164 | :return: String with the nicely decoded UICR Customer area data. 165 | """ 166 | with programmer.MicrobitMcu() as mb: 167 | start_address, uicr_data = mb.read_uicr_customer() 168 | to_hex = _bytes_to_pretty_hex if decode_hex else _bytes_to_intel_hex 169 | return to_hex([DataAndOffset(uicr_data, start_address)]) 170 | 171 | 172 | def read_micropython(): 173 | """Read the MicroPython runtime from the micro:bit flash. 174 | 175 | :return: String with Intel Hex format for the MicroPython runtime. 176 | """ 177 | with programmer.MicrobitMcu() as mb: 178 | start_address, flash_data = mb.read_flash( 179 | address=programmer.MICROPYTHON_START, 180 | count=programmer.MICROPYTHON_END - programmer.MICROPYTHON_START, 181 | ) 182 | return _bytes_to_intel_hex([DataAndOffset(flash_data, start_address)]) 183 | 184 | 185 | def read_python_code(): 186 | """Read the MicroPython user code from the micro:bit flash. 187 | 188 | :return: String with the MicroPython code. 189 | """ 190 | with programmer.MicrobitMcu() as mb: 191 | start_address, flash_data = mb.read_flash( 192 | address=programmer.PYTHON_CODE_START, 193 | count=(programmer.PYTHON_CODE_END - programmer.PYTHON_CODE_START), 194 | ) 195 | py_code_hex = _bytes_to_intel_hex( 196 | [DataAndOffset(flash_data, start_address)] 197 | ) 198 | try: 199 | python_code = uflash.extract_script(py_code_hex) 200 | except Exception: 201 | sys.stderr.write(format_exc() + "\n" + "-" * 70 + "\n") 202 | raise Exception("Could not decode the MicroPython code from flash") 203 | return python_code 204 | 205 | 206 | # 207 | # Flashing commands 208 | # 209 | def flash_drag_n_drop(hex_path): 210 | """Flash the micro:bit via a file transfer to the MICROBIT drive. 211 | 212 | :param hex_path: Path to the hex file to flash to the micro:bit. 213 | """ 214 | microbit_path = uflash.find_microbit() 215 | if not microbit_path: 216 | raise Exception("Could not find a MICROBIT drive to flash hex.") 217 | with open(hex_path, "rb") as hex_file: 218 | hex_bytes = hex_file.read() 219 | with open(os.path.join(microbit_path, "input.hex"), "wb") as hex_write: 220 | hex_write.write(hex_bytes) 221 | # Trying to force the OS to flush and sync in a blocking manner 222 | hex_write.flush() 223 | os.fsync(hex_write.fileno()) 224 | # After flashing the MICROBIT drive needs some time to remount 225 | time.sleep(1) 226 | 227 | 228 | def flash_pyocd(path_to_hex, unique_id=None): 229 | """Flash the micro:bit with the given hex file using PyOCD. 230 | 231 | :param path_to_hex: Path to the hex file to flash to the micro:bit. 232 | :param unique_id: Optional USB Serial number of a micro:bit to flash. 233 | """ 234 | with programmer.MicrobitMcu(unique_id=unique_id) as mb: 235 | mb.flash_hex(path_to_hex) 236 | 237 | 238 | def batch_flash_hex(hex_path): 239 | """Flash the micro:bit with the given hex file using multiprocessing. 240 | 241 | :param hex_path: Path to the hex file to flash to the micro:bit. 242 | """ 243 | found_microbits = set() 244 | flash_processes = [] 245 | 246 | multiprocessing.set_start_method("spawn") 247 | 248 | while True: 249 | time.sleep(1) 250 | connected_microbit_ids = programmer.find_microbit_ids() 251 | if not connected_microbit_ids: 252 | continue 253 | 254 | for microbit_id in connected_microbit_ids: 255 | if microbit_id not in found_microbits: 256 | print(f"\nNew micro:bit found: {microbit_id}") 257 | found_microbits.add(microbit_id) 258 | flash_process = multiprocessing.Process( 259 | target=flash_pyocd, args=(hex_path, microbit_id), 260 | ) 261 | flash_processes.append((flash_process, microbit_id)) 262 | flash_process.start() 263 | 264 | # Check exit code for all processes 265 | # Remove the ones that finished and retry the ones that failed 266 | for flash_process_tuple in list(flash_processes): 267 | flash_process, microbit_id = flash_process_tuple 268 | if flash_process.exitcode is not None: 269 | flash_processes.remove(flash_process_tuple) 270 | if flash_process.exitcode != 0: 271 | print(f"\nFlashing of {microbit_id} failed, retrying...") 272 | found_microbits.remove(microbit_id) 273 | else: 274 | print(f"\nFlashing of {microbit_id} finished successfully") 275 | 276 | 277 | # 278 | # Hex comparison commands 279 | # 280 | def _open_temp_html(html_str): 281 | """Create a temporary html file, open it in a browser and delete it. 282 | 283 | :param html_str: String to write to the temporary file. 284 | """ 285 | fd, path = tempfile.mkstemp(suffix=".html") 286 | try: 287 | with os.fdopen(fd, "w") as tmp: 288 | # do stuff with temp file 289 | tmp.write(html_str) 290 | webbrowser.open("file://{}".format(os.path.realpath(path))) 291 | finally: 292 | # It can take a bit of time for the browser to open the file, 293 | # so wait some time before deleting it 294 | t = Timer(30.0, lambda del_f: os.remove(del_f), args=[path]) 295 | t.start() 296 | 297 | 298 | def _gen_diff_html(from_title, from_lines, to_title, to_lines): 299 | """Compare two strings and string of HTML code with the output. 300 | 301 | :param from_title: Title of the left content to compare. 302 | :param from_lines: List of lines to compare. 303 | :param to_title: Title of the right content to compare. 304 | :param to_lines: List of lines to compare. 305 | :return: String of HTML code with the comparison output. 306 | """ 307 | html_template = """ 308 | 309 | 310 | 311 | Diff {from_title} vs. {to_title} 312 | 322 | 323 | 324 | 325 | 340 |
326 | 327 | 328 | 329 | 330 |
Colors
Added
Changed
Deleted
331 | 332 | 333 | 334 | 335 |
Links
(f)irst change
(n)ext change
(t)op
336 | 337 | 338 | 339 |
Files
Left: {from_title}
Right: {to_title}
341 | {diff_table} 342 | 343 | """ 344 | differ = HtmlDiff() 345 | filled_template = html_template.format( 346 | from_title=from_title, 347 | to_title=to_title, 348 | diff_table=differ.make_table(from_lines, to_lines), 349 | ) 350 | return filled_template 351 | 352 | 353 | def compare_full_flash_hex(hex_file_path): 354 | """Compare the micro:bit flash contents with a hex file. 355 | 356 | Opens the default browser to display an HTML page with the comparison 357 | output. 358 | 359 | :param hex_file_path: File path to the hex file to compare against. 360 | """ 361 | with open(hex_file_path, encoding="utf-8") as f: 362 | file_hex_lines = f.read().splitlines() 363 | flash_hex_lines = read_flash_hex(decode_hex=False).splitlines() 364 | 365 | html_code = _gen_diff_html( 366 | "micro:bit", flash_hex_lines, "Hex file", file_hex_lines, 367 | ) 368 | _open_temp_html(html_code) 369 | 370 | diffs = list(unified_diff(flash_hex_lines, file_hex_lines)) 371 | return 1 if len(diffs) else 0 372 | 373 | 374 | def compare_uicr_customer(hex_file_path): 375 | """Compare the micro:bit User UICR contents with a hex file. 376 | 377 | Opens the default browser to display an HTML page with the comparison 378 | output. 379 | 380 | :param hex_file_path: File path to the hex file to compare against. 381 | """ 382 | with open(hex_file_path, encoding="utf-8") as f: 383 | file_hex_str = f.readlines() 384 | flash_hex_str = read_uicr_customer_hex(decode_hex=False) 385 | 386 | html_code = _gen_diff_html( 387 | "micro:bit", flash_hex_str.splitlines(), "Hex file", file_hex_str 388 | ) 389 | _open_temp_html(html_code) 390 | -------------------------------------------------------------------------------- /ubittool/gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """A GUI to display the content from performing the uBitTool actions.""" 4 | import sys 5 | import logging 6 | import platform 7 | import tkinter as tk 8 | from tkinter import filedialog as tkFileDialog 9 | from tkinter.scrolledtext import ScrolledText as tkScrolledText 10 | 11 | try: 12 | from idlelib.WidgetRedirector import WidgetRedirector 13 | except ImportError: 14 | from idlelib.redirector import WidgetRedirector 15 | 16 | from ubittool import __version__ 17 | from ubittool import cmds 18 | 19 | 20 | class ReadOnlyEditor(tkScrolledText): 21 | """Implement a read only mode text editor class with scroll bar. 22 | 23 | Done by replacing the bindings for the insert and delete events. From: 24 | http://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only 25 | """ 26 | 27 | def __init__(self, *args, **kwargs): 28 | """Init the class and set the insert and delete event bindings.""" 29 | super().__init__(*args, **kwargs) 30 | self.redirector = WidgetRedirector(self) 31 | self.insert = self.redirector.register( 32 | "insert", lambda *args, **kw: "break" 33 | ) 34 | self.delete = self.redirector.register( 35 | "delete", lambda *args, **kw: "break" 36 | ) 37 | 38 | def clear(self): 39 | """Clear the contents of the text area.""" 40 | self.delete(1.0, "end") 41 | 42 | def replace(self, new_content): 43 | """Remove all editor content and inserts the new content. 44 | 45 | :param new_content: String to insert. 46 | """ 47 | self.clear() 48 | self.insert(1.0, new_content) 49 | 50 | 51 | class StdoutRedirector(object): 52 | """A class to redirect stdout to a text widget.""" 53 | 54 | def __init__(self, text_area, text_color=None): 55 | """Get the text area widget as a reference and configure its colour.""" 56 | self.text_area = text_area 57 | self.text_color = text_color 58 | self.tag = "colour_change_{}".format(text_color) 59 | if self.text_color: 60 | self.text_area.tag_configure(self.tag, foreground=text_color) 61 | 62 | def write(self, string): 63 | """Write text to the fake stream.""" 64 | start_position = self.text_area.index("insert") 65 | self.text_area.insert("end", string) 66 | if self.text_color: 67 | self.text_area.tag_add( 68 | self.tag, start_position, self.text_area.index("insert") 69 | ) 70 | 71 | def flush(self): # pragma: no cover 72 | """All flushed immediately on each write call.""" 73 | pass 74 | 75 | 76 | class TextViewer(ReadOnlyEditor): 77 | """A read-only text editor for viewing text.""" 78 | 79 | def __init__(self, *args, **kwargs): 80 | """Construct the editor widget. 81 | 82 | :param frame: A Frame() instance to set the text editor. 83 | """ 84 | super().__init__(*args, **kwargs) 85 | self.pack(side="left", fill="both", expand=1) 86 | self.config(wrap="char", width=1) 87 | 88 | 89 | class ConsoleOutput(ReadOnlyEditor): 90 | """A read-only editor to display std out and err streams.""" 91 | 92 | def __init__(self, *args, **kwargs): 93 | """Construct the read-only editor widget. 94 | 95 | :param frame: A Frame() instance to set this text editor. 96 | """ 97 | super().__init__(background="#222", foreground="#DDD", *args, **kwargs) 98 | self.config(wrap="char", width=1) 99 | self.activate() 100 | 101 | def activate(self): 102 | """Configure std out/in to send to write to the console text widget.""" 103 | sys.stdout = StdoutRedirector(self, text_color="#0D4") 104 | sys.stderr = StdoutRedirector(self, text_color="#D00") 105 | logger = logging.getLogger() 106 | logger.setLevel(level=logging.INFO) 107 | logging_handler_out = logging.StreamHandler(sys.stdout) 108 | logging_handler_out.setLevel(logging.INFO) 109 | logger.addHandler(logging_handler_out) 110 | logging_handler_err = logging.StreamHandler(sys.stderr) 111 | logging_handler_err.setLevel(logging.WARNING) 112 | logger.addHandler(logging_handler_err) 113 | 114 | def deactivate(self): 115 | """Restore std out/in.""" 116 | sys.stdout = sys.__stdout__ 117 | sys.stderr = sys.__stderr__ 118 | 119 | 120 | class CmdLabel(tk.Label): 121 | """A text label to contain the name of the last command executed.""" 122 | 123 | def __init__(self, parent, default_text, *args, **kwargs): 124 | """Set the colours in the constructor. 125 | 126 | :param frame: A Frame() instance to set this text editor. 127 | """ 128 | self.bg_colour = "#E5E5E5" 129 | self.cmd_title = tk.StringVar(value="Command: Select from the Menu") 130 | parent.config(borderwidth=1, background=self.bg_colour) 131 | super().__init__( 132 | parent, background=self.bg_colour, textvariable=self.cmd_title 133 | ) 134 | self.set_text(default_text) 135 | self.pack(side="left", fill="x") 136 | parent.pack(fill="x", expand=False) 137 | 138 | def set_text(self, new_text): 139 | """Set the text of the command.""" 140 | self.cmd_title.set("Command: {}".format(new_text)) 141 | 142 | 143 | class UBitToolWindow(tk.Tk): 144 | """Main app window. 145 | 146 | Creates a TK window with a text viewer, console viewer, and menus for 147 | executing actions. 148 | """ 149 | 150 | CMD_OPEN = "Open" 151 | CMD_SAVE = "Save As" 152 | CMD_EXIT = "Exit" 153 | CMD_READ_CODE = "Read MicroPython code" 154 | CMD_READ_UPY = "Read MicroPython runtime" 155 | CMD_READ_FLASH_HEX = "Read full flash contents (Intel Hex)" 156 | CMD_READ_FLASH_PRETTY = "Read full flash contents (Pretty Hex)" 157 | CMD_READ_RAM_HEX = "Read full RAM contents (Intel Hex)" 158 | CMD_READ_RAM_PRETTY = "Read full RAM contents (Pretty Hex)" 159 | CMD_READ_UICR = "Read UICR" 160 | CMD_READ_UICR_CUSTOMER = "Read UICR Customer" 161 | CMD_READ_FLASH_UICR_HEX = "Read full flash + UICR" 162 | CMD_COMPARE_FLASH = "Compare full flash contents (Intel Hex)" 163 | CMD_COMPARE_UICR = "Compare UICR Customer (Intel Hex)" 164 | 165 | def __init__(self, *args, **kwargs): 166 | """Initialise the window.""" 167 | super().__init__(*args, **kwargs) 168 | self.title("uBitTool v{}".format(__version__)) 169 | self.geometry("{}x{}".format(600, 480)) 170 | 171 | self.menu_bar = tk.Menu(self) 172 | self.set_menu_bar(self.menu_bar) 173 | self.bind_shortcuts() 174 | 175 | self.frame_title = tk.Frame(self) 176 | self.cmd_title = CmdLabel(self.frame_title, "Select from the Menu") 177 | 178 | self.paned_window = tk.PanedWindow( 179 | orient=tk.VERTICAL, 180 | sashrelief="groove", 181 | sashpad=0, 182 | sashwidth=5, 183 | showhandle=True, 184 | handlesize=10, 185 | ) 186 | self.paned_window.pack(fill=tk.BOTH, expand=1) 187 | 188 | self.text_viewer = TextViewer() 189 | self.paned_window.add(self.text_viewer) 190 | 191 | self.console = ConsoleOutput() 192 | self.paned_window.add(self.console) 193 | 194 | # instead of closing the window, execute a function 195 | self.protocol("WM_DELETE_WINDOW", self.app_quit) 196 | 197 | def set_menu_bar(self, menu): 198 | """Create the menu bar with all user options. 199 | 200 | :param menu: A Menu() instance to attach all options. 201 | """ 202 | # In macOS we use the command key instead of option 203 | cmd_key = "Command" if platform.system() == "Darwin" else "Ctrl" 204 | # Menu item File 205 | self.file_menu = tk.Menu(menu, tearoff=0) 206 | self.file_menu.add_command( 207 | label=self.CMD_OPEN, 208 | command=self.file_open, 209 | accelerator="{}+O".format(cmd_key), 210 | underline=1, 211 | ) 212 | self.file_menu.add_command( 213 | label=self.CMD_SAVE, 214 | command=self.file_save_as, 215 | accelerator="{}+S".format(cmd_key), 216 | underline=1, 217 | ) 218 | self.file_menu.add_separator() 219 | self.file_menu.add_command( 220 | label=self.CMD_EXIT, command=self.app_quit, accelerator="Alt+F4" 221 | ) 222 | menu.add_cascade(label="File", underline=0, menu=self.file_menu) 223 | 224 | # Helper function to execute commands and display their output 225 | def execute_cmd(cmd_str, cmd_function, *args, **kwargs): 226 | self.set_next_cmd(cmd_str) 227 | self.update() 228 | output_str = cmd_function(*args, **kwargs) 229 | self.text_viewer.replace(output_str) 230 | 231 | # Menu item micro:bit 232 | self.ubit_menu = tk.Menu(menu, tearoff=0) 233 | self.ubit_menu.add_command( 234 | label=self.CMD_READ_CODE, 235 | command=lambda: execute_cmd( 236 | self.CMD_READ_CODE, cmds.read_python_code 237 | ), 238 | ) 239 | self.ubit_menu.add_command( 240 | label=self.CMD_READ_UPY, 241 | command=lambda: execute_cmd( 242 | self.CMD_READ_UPY, cmds.read_micropython 243 | ), 244 | ) 245 | menu.add_cascade(label="micro:bit", underline=0, menu=self.ubit_menu) 246 | # Menu item nrf 247 | self.nrf_menu = tk.Menu(menu, tearoff=0) 248 | self.nrf_menu.add_command( 249 | label=self.CMD_READ_FLASH_HEX, 250 | command=lambda: execute_cmd( 251 | self.CMD_READ_FLASH_HEX, cmds.read_flash_hex, decode_hex=False 252 | ), 253 | ) 254 | self.nrf_menu.add_command( 255 | label=self.CMD_READ_FLASH_PRETTY, 256 | command=lambda: execute_cmd( 257 | self.CMD_READ_FLASH_PRETTY, cmds.read_flash_hex, True 258 | ), 259 | ) 260 | self.nrf_menu.add_command( 261 | label=self.CMD_READ_RAM_HEX, 262 | command=lambda: execute_cmd( 263 | self.CMD_READ_RAM_HEX, cmds.read_ram_hex, decode_hex=False 264 | ), 265 | ) 266 | self.nrf_menu.add_command( 267 | label=self.CMD_READ_RAM_PRETTY, 268 | command=lambda: execute_cmd( 269 | self.CMD_READ_RAM_PRETTY, cmds.read_ram_hex, decode_hex=True 270 | ), 271 | ) 272 | self.nrf_menu.add_command( 273 | label=self.CMD_READ_UICR, 274 | command=lambda: execute_cmd( 275 | self.CMD_READ_UICR, cmds.read_uicr_hex, decode_hex=False 276 | ), 277 | ) 278 | self.nrf_menu.add_command( 279 | label=self.CMD_READ_UICR_CUSTOMER, 280 | command=lambda: execute_cmd( 281 | self.CMD_READ_UICR_CUSTOMER, cmds.read_uicr_customer_hex, False 282 | ), 283 | ) 284 | self.nrf_menu.add_command( 285 | label=self.CMD_READ_FLASH_UICR_HEX, 286 | command=lambda: execute_cmd( 287 | self.CMD_READ_FLASH_UICR_HEX, cmds.read_flash_uicr_hex, False 288 | ), 289 | ) 290 | self.nrf_menu.add_separator() 291 | self.nrf_menu.add_command( 292 | label=self.CMD_COMPARE_FLASH, command=self.compare_full_flash_intel 293 | ) 294 | self.nrf_menu.add_command( 295 | label=self.CMD_COMPARE_UICR, 296 | command=self.compare_uicr_customer_intel, 297 | ) 298 | menu.add_cascade(label="nrf", underline=0, menu=self.nrf_menu) 299 | # display the menu 300 | self.config(menu=menu) 301 | 302 | def bind_shortcuts(self, event=None): 303 | """Bind shortcuts to operations.""" 304 | # In macOS we use the command key instead of option 305 | cmd_key = "Command" if platform.system() == "Darwin" else "Control" 306 | self.bind("<{}-o>".format(cmd_key), self.file_open) 307 | self.bind("<{}-O>".format(cmd_key), self.file_open) 308 | self.bind("<{}-S>".format(cmd_key), self.file_save_as) 309 | self.bind("<{}-s>".format(cmd_key), self.file_save_as) 310 | 311 | def set_next_cmd(self, cmd_name): 312 | """Prepare the window for the next command to be executed.""" 313 | self.text_viewer.clear() 314 | self.console.clear() 315 | self.cmd_title.set_text(cmd_name) 316 | 317 | def compare_full_flash_intel(self): 318 | """Compare a hex file with the micro:bit flash. 319 | 320 | Ask the user to select a hex file, compares it with flash contents and 321 | opens the default web browser to display the comparison results. 322 | """ 323 | self.set_next_cmd(self.CMD_COMPARE_FLASH) 324 | file_path = tkFileDialog.askopenfilename() 325 | if file_path: 326 | self.text_viewer.replace("Reading flash contents...") 327 | cmds.compare_full_flash_hex(file_path) 328 | self.text_viewer.replace("Diff content loaded in default browser.") 329 | 330 | def compare_uicr_customer_intel(self): 331 | """Compare a hex file with the micro:bit user UICR memory. 332 | 333 | Ask the user to select a hex file, compares it with the user UICR 334 | contents and opens the default web browser to display the comparison 335 | results. 336 | """ 337 | self.set_next_cmd(self.CMD_COMPARE_UICR) 338 | file_path = tkFileDialog.askopenfilename() 339 | if file_path: 340 | self.text_viewer.replace("Reading User UICR contents...") 341 | cmds.compare_uicr_customer(file_path) 342 | self.text_viewer.replace("Diff content loaded in default browser.") 343 | 344 | def file_open(self, event=None): 345 | """Open a file picker and load a file into the text viewer.""" 346 | file_path = tkFileDialog.askopenfilename() 347 | if file_path: 348 | self.set_next_cmd(self.CMD_OPEN) 349 | with open(file_path, encoding="utf-8") as f: 350 | file_contents = f.read() 351 | # Set current text to file contents 352 | self.text_viewer.replace(file_contents) 353 | 354 | def file_save_as(self, event=None): 355 | """Save the text from the text viewer into a file.""" 356 | file_path = tkFileDialog.asksaveasfilename( 357 | filetypes=(("Python files", "*.py *.pyw"), ("All files", "*.*")) 358 | ) 359 | if file_path: 360 | with open(file_path, "wb") as f: 361 | text = self.text_viewer.get(1.0, "end-1c") 362 | f.write(text.encode("utf-8")) 363 | return file_path 364 | else: 365 | return None 366 | 367 | def app_quit(self, event=None): 368 | """Quit the app.""" 369 | self.console.deactivate() 370 | self.destroy() 371 | 372 | 373 | def open_gui(): 374 | """Create the app window and launch it.""" 375 | app = UBitToolWindow() 376 | app.lift() 377 | app.attributes("-topmost", True) 378 | app.after_idle(app.attributes, "-topmost", False) 379 | app.mainloop() 380 | 381 | 382 | if __name__ == "__main__": 383 | open_gui() 384 | -------------------------------------------------------------------------------- /ubittool/programmer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Functions to read data from the micro:bit using PyOCD.""" 4 | from collections import namedtuple 5 | 6 | from pyocd.core.helpers import ConnectHelper 7 | from pyocd.flash.file_programmer import FileProgrammer 8 | 9 | 10 | MemoryRegions = namedtuple( 11 | "MemoryRegions", 12 | [ 13 | "flash_start", 14 | "flash_size", 15 | "ram_start", 16 | "ram_size", 17 | "uicr_start", 18 | "uicr_size", 19 | "uicr_customer_offset", 20 | "uicr_customer_size", 21 | ], 22 | ) 23 | 24 | MEM_REGIONS_MB_V1 = MemoryRegions( 25 | flash_start=0x0000_0000, 26 | flash_size=256 * 1024, 27 | ram_start=0x2000_0000, 28 | ram_size=16 * 1024, 29 | uicr_start=0x1000_1000, 30 | uicr_size=0x100, 31 | uicr_customer_offset=0x80, 32 | uicr_customer_size=0x100 - 0x80, 33 | ) 34 | MEM_REGIONS_MB_V2 = MemoryRegions( 35 | flash_start=0x0000_0000, 36 | flash_size=512 * 1024, 37 | ram_start=0x2000_0000, 38 | ram_size=128 * 1024, 39 | uicr_start=0x1000_1000, 40 | uicr_size=0x308, 41 | uicr_customer_offset=0x80, 42 | uicr_customer_size=0x200 - 0x80, 43 | ) 44 | MICROBIT_MEM_REGIONS = { 45 | "9900": MEM_REGIONS_MB_V1, 46 | "9901": MEM_REGIONS_MB_V1, 47 | "9903": MEM_REGIONS_MB_V2, 48 | "9904": MEM_REGIONS_MB_V2, 49 | "9905": MEM_REGIONS_MB_V2, 50 | "9906": MEM_REGIONS_MB_V2, 51 | } 52 | 53 | # Assumes code attached to fixed location instead of using the filesystem 54 | PYTHON_CODE_START = 0x3E000 55 | PYTHON_CODE_END = 0x40000 56 | 57 | # MicroPython will contain unnecessary empty space between end of interpreter 58 | # and begginning of code at a fixed location, assumes no file system used 59 | MICROPYTHON_START = 0x0 60 | MICROPYTHON_END = PYTHON_CODE_START 61 | 62 | 63 | class MicrobitMcu(object): 64 | """Read data from main microcontroller on the micro:bit board.""" 65 | 66 | def __init__(self, unique_id=None): 67 | """Declare all instance variables.""" 68 | self.unique_id = unique_id 69 | self.session = None 70 | self.board = None 71 | self.target = None 72 | self.board_id = None 73 | self.mem = None 74 | 75 | def _connect(self): 76 | """Connect PyOCD to the main micro:bit microcontroller.""" 77 | if not self.session: 78 | try: 79 | self.session = ConnectHelper.session_with_chosen_probe( 80 | unique_id=self.unique_id, 81 | blocking=False, 82 | auto_unlock=False, 83 | halt_on_connect=True, 84 | resume_on_disconnect=True, 85 | ) 86 | if self.session is None: 87 | raise Exception("Could not open the debugger session.") 88 | self.session.open() 89 | self.board = self.session.board 90 | self.target = self.board.target 91 | self.board_id = self.board.unique_id[:4] 92 | except Exception as e: 93 | raise Exception( 94 | "{}\n{}\n".format(str(e), "-" * 70) 95 | + "Error: Did not find any connected boards." 96 | ) 97 | if self.board_id not in MICROBIT_MEM_REGIONS: 98 | self._disconnect() 99 | raise Exception("Incompatible board ID from connected device.") 100 | self.mem = MICROBIT_MEM_REGIONS[self.board_id] 101 | 102 | def _disconnect(self): 103 | """.""" 104 | if self.session: 105 | self.session.close() 106 | self.session = None 107 | 108 | def __enter__(self): 109 | """.""" 110 | return self 111 | 112 | def __exit__(self, exc_type, exc_value, traceback): 113 | """.""" 114 | self._disconnect() 115 | 116 | def _read_memory(self, address=None, count=None): 117 | """Read any continuous memory area from the micro:bit. 118 | 119 | Reads the contents of any micro:bit continuous memory area, starting 120 | from the 'address' argument for as many bytes as indicated by 'count'. 121 | There is no input sanitation in this function and is the responsibility 122 | of the caller to input values within range of the target board or deal 123 | with any exceptions raised by PyOCD. 124 | 125 | :param address: Integer indicating the start address to read. 126 | :param count: Integer, how many bytes to read. 127 | :return: A list of integers, each representing a byte of data. 128 | """ 129 | self._connect() 130 | return self.target.read_memory_block8(address, count) 131 | 132 | def read_flash(self, address=None, count=None): 133 | """Read data from flash and returns it as a list of bytes. 134 | 135 | :param address: Integer indicating the start address to read. 136 | :param count: Integer indicating how many bytes to read. 137 | :return: The start address from the read and a list of integers, 138 | each representing a byte of data. 139 | """ 140 | self._connect() 141 | 142 | if address is None: 143 | address = self.mem.flash_start 144 | if count is None: 145 | count = self.mem.flash_size 146 | flash_end = self.mem.flash_start + self.mem.flash_size 147 | 148 | end = address + count 149 | if ( 150 | not (self.mem.flash_start <= address < flash_end) 151 | or end > flash_end 152 | ): 153 | raise ValueError( 154 | "Cannot read a flash address out of boundaries.\n" 155 | "Reading from {} to {},\nlimits are from {} to {}".format( 156 | address, end, self.mem.flash_start, flash_end, 157 | ) 158 | ) 159 | 160 | return address, self._read_memory(address=address, count=count) 161 | 162 | def read_ram(self, address=None, count=None): 163 | """Read the contents of the micro:bit RAM memory. 164 | 165 | :param address: Integer indicating the start address to read. 166 | :param count: Integer indicating how many bytes to read. 167 | :return: The start address from the read and a list of integers, 168 | each representing a byte of data. 169 | """ 170 | self._connect() 171 | 172 | if address is None: 173 | address = self.mem.ram_start 174 | if count is None: 175 | count = self.mem.ram_size 176 | ram_end = self.mem.ram_start + self.mem.ram_size 177 | 178 | last_byte = address + count 179 | if ( 180 | not (self.mem.ram_start <= address < ram_end) 181 | or last_byte > ram_end 182 | ): 183 | raise ValueError( 184 | "Cannot read a RAM location out of boundaries.\n" 185 | "Reading from {} to {},\nlimits from {} to {}".format( 186 | address, last_byte, self.mem.ram_start, ram_end 187 | ) 188 | ) 189 | return address, self._read_memory(address=address, count=count) 190 | 191 | def read_uicr(self, address=None, count=None): 192 | """Read data from UICR and returns it as a list of bytes. 193 | 194 | :param address: Integer indicating the start address to read. 195 | :param count: Integer indicating how many bytes to read. 196 | :return: The start address from the read and a list of integers, 197 | each representing a byte of data. 198 | """ 199 | self._connect() 200 | 201 | if address is None: 202 | address = self.mem.uicr_start 203 | if count is None: 204 | count = self.mem.uicr_size 205 | uicr_end = self.mem.uicr_start + self.mem.uicr_size 206 | 207 | end = address + count 208 | if not (self.mem.uicr_start <= address < uicr_end) or end > uicr_end: 209 | raise ValueError( 210 | "Cannot read a UICR location out of boundaries.\n" 211 | "Reading from {} to {},\nlimits are from {} to {}".format( 212 | address, end, self.mem.uicr_start, uicr_end, 213 | ) 214 | ) 215 | 216 | return address, self._read_memory(address=address, count=count) 217 | 218 | def read_uicr_customer(self): 219 | """Read all the UICR customer data and return it as a list of bytes. 220 | 221 | :param address: Integer indicating the start address to read. 222 | :param count: Integer indicating how many bytes to read. 223 | :return: The start address from the read and a list of integers, 224 | each representing a byte of data. 225 | """ 226 | self._connect() 227 | 228 | return self.read_uicr( 229 | address=self.mem.uicr_start + self.mem.uicr_customer_offset, 230 | count=self.mem.uicr_customer_size, 231 | ) 232 | 233 | def flash_hex(self, hex_path): 234 | """Flash the micro:bit with the provided hex file and reset it. 235 | 236 | :param hex_path: Path to the hex file to flash. 237 | """ 238 | self._connect() 239 | 240 | self.target.mass_erase() 241 | FileProgrammer(self.session).program(hex_path) 242 | self.target.reset() 243 | 244 | 245 | def find_microbit_ids(): 246 | """Find all connected micro:bit boards and return their USB unique IDs. 247 | 248 | :return: A tuple of strings, each containing a connected micro:bit USB ID. 249 | """ 250 | connected_boards = ConnectHelper.get_all_connected_probes( 251 | unique_id="990", blocking=False, print_wait_message=True 252 | ) 253 | return tuple( 254 | [connected_board.unique_id for connected_board in connected_boards] 255 | ) 256 | --------------------------------------------------------------------------------