├── .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 | [](https://codecov.io/gh/carlosperate/ubittool)
4 | [](https://github.com/carlosperate/ubittool/actions/workflows/test.yml)
5 | [](https://github.com/carlosperate/ubittool/actions/workflows/build.yml)
6 | [](https://pypi.org/project/ubittool/)
7 | 
8 | [](https://github.com/ambv/black)
9 | [](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 | 
21 |
22 |
23 |
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 | 
19 |
20 | 
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 | 
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 | 
44 | - A different warning window now offers more options
45 | - Click "Open" and the app will now work
46 | 
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 | 
64 | - Click on the "More info" link
65 | - A button should appear to "Run anyway"
66 | 
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 | 
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 | 
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 |
326 | Colors |
327 | Added |
328 | Changed |
329 | Deleted |
330 | |
331 | Links | |
332 | (f)irst change |
333 | (n)ext change |
334 | (t)op |
335 | |
336 | Files | |
337 | Left: {from_title} |
338 | Right: {to_title} |
339 | |
340 |
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 |
--------------------------------------------------------------------------------