├── .gitattributes ├── .github └── workflows │ ├── build-and-release.yml │ ├── build-linux.yml │ ├── build-macos.yml │ ├── build-python.yml │ ├── build-windows.yml │ └── non-release-build.yml ├── .gitignore ├── DESCRIPTION.md ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── artemis_upload.py ├── artemis_uploader ├── __init__.py ├── artemis_svl.py ├── artemis_uploader.py ├── asb │ ├── __init__.py │ ├── am_defines.py │ ├── asb.py │ └── keys_info.py ├── au_act_artasb.py ├── au_act_artfrmw.py ├── au_action.py ├── au_worker.py └── resource │ ├── _version.py │ ├── artemis-icon-blk.png │ ├── artemis-icon.png │ ├── artemis-logo-rounded.png │ ├── artemis-uploader.ico │ ├── artemis_svl.bin │ ├── sfe_logo_med.png │ └── sparkdisk.icns ├── examples └── Blink.bin ├── images ├── artemis-linux.png ├── artemis-macos-1.png ├── artemis-macos-2.png ├── artemis-macos-install-1.png ├── artemis-macos-install-2.png ├── artemis-macos-install-3.png ├── artemis-macos-install-4.png ├── artemis-macos-install-5.png ├── artemis-macos-install-6.png ├── artemis-uploader-banner.png ├── artemis-windows-1.png ├── artemis-windows-2.png ├── artemis-windows.png ├── bootloader-upload.png ├── firmware-upload.png └── macos-finder.png ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build-and-release 4 | 5 | # Controls when the workflow will run 6 | on: 7 | 8 | # Trigger on a push 9 | #push: 10 | 11 | # Trigger on a published release 12 | release: 13 | types: [published] 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | # Build the installer on mac 21 | call-macos-build: 22 | uses: ./.github/workflows/build-macos.yml 23 | 24 | call-linux-build: 25 | uses: ./.github/workflows/build-linux.yml 26 | 27 | call-windows-build: 28 | uses: ./.github/workflows/build-windows.yml 29 | 30 | call-python-build: 31 | uses: ./.github/workflows/build-python.yml 32 | 33 | # Using the outputs of the build 34 | deploy-builds: 35 | 36 | # Only do this on a release - note - filtering release types in the above "on:" statement 37 | if: github.event_name == 'release' 38 | runs-on: ubuntu-latest 39 | needs: [call-macos-build, call-linux-build, call-windows-build, call-python-build] 40 | steps: 41 | # Download the generated app files that are part of the release 42 | - uses: actions/download-artifact@v4 43 | with: 44 | name: ${{ needs.call-macos-build.outputs.build-file }} 45 | - uses: actions/download-artifact@v4 46 | with: 47 | name: ${{ needs.call-linux-build.outputs.build-file }} 48 | - uses: actions/download-artifact@v4 49 | with: 50 | name: ${{ needs.call-windows-build.outputs.build-file }} 51 | - uses: actions/download-artifact@v4 52 | with: 53 | name: ${{ needs.call-python-build.outputs.build-file }} 54 | - name: Output Listing 55 | run: ls -la 56 | 57 | - name: Publish Release 58 | uses: softprops/action-gh-release@v2 59 | with: 60 | files: | 61 | ${{ needs.call-macos-build.outputs.build-file }} 62 | ${{ needs.call-linux-build.outputs.build-file }} 63 | ${{ needs.call-windows-build.outputs.build-file }} 64 | ${{ needs.call-python-build.outputs.build-package }} 65 | 66 | -------------------------------------------------------------------------------- /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build-linux 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # this is a called workflow 8 | workflow_call: 9 | outputs: 10 | build-file: 11 | description: "The output of this build procsss" 12 | value: ${{ jobs.linux-build-job.outputs.install-file }} 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # Build the installer on mac 17 | linux-build-job: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Output 22 | outputs: 23 | install-file: ${{ steps.output-installer.outputs.filename }} 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-python@v5 30 | with: 31 | python-version: '3.12' 32 | 33 | # Setup python 34 | - name: System Setup 35 | run: | 36 | pip3 install pyserial pycryptodome pyinstaller pyqt5 darkdetect 37 | 38 | # Build the installer 39 | - name: Build Linux Installer 40 | run: | 41 | pyinstaller --onefile --name ArtemisUploader --noconsole --distpath=. --icon=artemis_uploader/resource/artemis-uploader.ico --add-data="artemis_uploader/resource/*:resource/" artemis_upload.py 42 | gzip ArtemisUploader 43 | mv ArtemisUploader.gz ArtemisUploader.linux.gz 44 | 45 | - uses: actions/upload-artifact@v4 46 | with: 47 | name: ArtemisUploader.linux.gz 48 | path: ArtemisUploader.linux.gz 49 | 50 | - id: output-installer 51 | run: echo "filename=ArtemisUploader.linux.gz" >> $GITHUB_OUTPUT 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build-macos 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # this is a called workflow 8 | workflow_call: 9 | outputs: 10 | build-file: 11 | description: "The output of this build procsss" 12 | value: ${{ jobs.macos-build-job.outputs.install-file }} 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # Build the installer on mac 17 | macos-build-job: 18 | # The type of runner that the job will run on 19 | runs-on: macos-latest 20 | 21 | # Output 22 | outputs: 23 | install-file: ${{ steps.output-installer.outputs.filename }} 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-python@v5 30 | with: 31 | python-version: '3.12' 32 | 33 | # Setup python 34 | - name: System Setup 35 | run: | 36 | pip3 install pyserial pycryptodome pyinstaller Pillow pyqt5 darkdetect 37 | brew install create-dmg 38 | 39 | # Build the installer 40 | - name: Build Mac Installer 41 | run: | 42 | pyinstaller --windowed -n ArtemisUploader --noconsole --distpath=. --icon=artemis_uploader/resource/artemis-uploader.ico --add-data="artemis_uploader/resource/*:resource/" artemis_upload.py 43 | mkdir tmp 44 | mv "ArtemisUploader.app" "tmp/" 45 | create-dmg --volicon "artemis_uploader/resource/sparkdisk.icns" --background "artemis_uploader/resource/sfe_logo_med.png" --hide-extension "ArtemisUploader.app" --icon "ArtemisUploader.app" 100 100 --window-size 600 440 --app-drop-link 400 100 "ArtemisUploader.dmg" "tmp/" 46 | 47 | - uses: actions/upload-artifact@v4 48 | with: 49 | name: ArtemisUploader.dmg 50 | path: ArtemisUploader.dmg 51 | 52 | - id: output-installer 53 | run: echo "filename=ArtemisUploader.dmg" >> $GITHUB_OUTPUT 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/build-python.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build-python 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # this is a called workflow 8 | workflow_call: 9 | outputs: 10 | build-file: 11 | description: "The output of this build procsss" 12 | value: ${{ jobs.python-build-job.outputs.install-file }} 13 | build-package: 14 | description: "The output of this build procsss" 15 | value: ${{ jobs.python-build-job.outputs.install-package }} 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | # Build the installer on mac 20 | python-build-job: 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | 24 | # Output 25 | outputs: 26 | install-file: ${{ steps.output-installer.outputs.filename }} 27 | install-package: ${{ steps.output-installer.outputs.packagename }} 28 | 29 | # Steps represent a sequence of tasks that will be executed as part of the job 30 | steps: 31 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: '3.12' 36 | 37 | # Setup python 38 | - name: System Setup 39 | run: | 40 | pip3 install setuptools 41 | 42 | # Build the installer 43 | - name: Build Python Installer 44 | run: | 45 | python setup.py sdist 46 | 47 | - uses: actions/upload-artifact@v4 48 | with: 49 | name: python-install-package 50 | path: dist 51 | 52 | - name: Extract package name 53 | run: | 54 | cd dist 55 | echo "PACKAGE_NAME=$(ls *.tar.gz)" >> $GITHUB_ENV 56 | 57 | - id: output-installer 58 | run: | 59 | echo "filename=python-install-package" >> $GITHUB_OUTPUT 60 | echo "packagename=${{ env.PACKAGE_NAME }}" >> $GITHUB_OUTPUT 61 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build-windows 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # this is a called workflow 8 | workflow_call: 9 | outputs: 10 | build-file: 11 | description: "The output of this build procsss" 12 | value: ${{ jobs.windows-build-job.outputs.install-file }} 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # Build the installer on mac 17 | windows-build-job: 18 | # The type of runner that the job will run on 19 | runs-on: windows-latest 20 | 21 | # Output 22 | outputs: 23 | install-file: ${{ steps.output-installer.outputs.filename }} 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-python@v5 30 | with: 31 | python-version: '3.12' 32 | 33 | # Setup python 34 | - name: System Setup 35 | run: | 36 | pip3 install pyserial pycryptodome pyinstaller pyqt5 darkdetect 37 | 38 | # Build the installer 39 | - name: Build Windows Installer 40 | run: | 41 | pyinstaller --onefile --name ArtemisUploader --noconsole --distpath=. --icon=artemis_uploader\resource\artemis-uploader.ico --add-data="artemis_uploader\resource\*;resource\" artemis_upload.py 42 | 43 | - name: Compress Installer 44 | shell: powershell 45 | run: | 46 | $compress = @{ 47 | Path = ".\ArtemisUploader.exe" 48 | CompressionLevel = "Fastest" 49 | DestinationPath = ".\ArtemisUploader.win.zip" 50 | } 51 | Compress-Archive @compress 52 | 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: ArtemisUploader.win.zip 56 | path: ArtemisUploader.win.zip 57 | 58 | - id: output-installer 59 | run: echo "filename=ArtemisUploader.win.zip" >> $env:GITHUB_OUTPUT 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /.github/workflows/non-release-build.yml: -------------------------------------------------------------------------------- 1 | # Workflow that builds the application, but doens't add to a release. 2 | # 3 | # This will run on a push that doesn't have a vesion (release) tag 4 | 5 | name: non-release-build 6 | 7 | # Controls when the workflow will run 8 | on: 9 | # Trigger on push - when a version tag isn't set 10 | # push: 11 | # branches: 12 | # - master 13 | # tags-ignore: 14 | # - "v*.*.*" 15 | 16 | # Allows you to run this workflow manually from the Actions tab 17 | workflow_dispatch: 18 | 19 | # Run each plaform build workflows as seperate jobs 20 | jobs: 21 | # Build the installer on mac 22 | call-macos-build: 23 | uses: ./.github/workflows/build-macos.yml 24 | 25 | call-linux-build: 26 | uses: ./.github/workflows/build-linux.yml 27 | 28 | call-windows-build: 29 | uses: ./.github/workflows/build-windows.yml 30 | 31 | call-python-build: 32 | uses: ./.github/workflows/build-python.yml 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #VSCode 2 | 3 | /.vscode/* 4 | 5 | # Windows image file caches 6 | Thumbs.db 7 | ehthumbs.db 8 | 9 | #Eagle Backup files 10 | *.s#? 11 | *.b#? 12 | *.l#? 13 | *.lck 14 | 15 | # Folder config file 16 | Desktop.ini 17 | 18 | # Recycle Bin used on file shares 19 | $RECYCLE.BIN/ 20 | 21 | # Windows Installer files 22 | *.cab 23 | *.msi 24 | *.msm 25 | *.msp 26 | 27 | # ========================= 28 | # Operating System Files 29 | # ========================= 30 | 31 | # OSX 32 | # ========================= 33 | 34 | .DS_Store 35 | .AppleDouble 36 | .LSOverride 37 | 38 | # Icon must ends with two \r. 39 | Icon 40 | 41 | 42 | # Thumbnails 43 | ._* 44 | 45 | # Files that might appear on external disk 46 | .Spotlight-V100 47 | .Trashes 48 | -------------------------------------------------------------------------------- /DESCRIPTION.md: -------------------------------------------------------------------------------- 1 | The Artemis Firmware Uploader (AFU) is a simple to use GUI for updating firmware and the bootloader on Artemis based products. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The software is released under the [MIT License](http://opensource.org/licenses/MIT). 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2020 SparkFun Electronics 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include artemis_uploader/resource/*.* 2 | include DESCRIPTION.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SparkFun Artemis Uploader App 2 | ======================================== 3 | 4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | follow on Twitter 13 |

14 | 15 | ![macOS Artemis Uploader](images/artemis-uploader-banner.png) 16 | 17 | The Artemis Uploader App is a simple, easy to use method for updating the firmware and bootloader on SparkFun Artemis based products. Available on all major platforms, as well as a Python package, the Artemis Uploader App simplifies working with SparkFun Artemis. 18 | 19 | If you need to install the application, see the [Installation Section](#installation) of this page. 20 | 21 | 22 | # Using the Artemis Uploader 23 | 24 | ## Upload Firmware 25 | 26 | * Click ```Browse``` and select the firmware file you'd like to upload (should end in *.bin*) 27 | * Attach the Artemis target board over USB 28 | * Select the COM port from the dropdown menu 29 | * Adjust the Baud Rate as desired 30 | * Click the ```Upload Firmware``` Button in the lower right corner of the app. 31 | 32 | The selected firmware is then uploaded to the connected SparkFun Artemis product. Upload information and progress are displayed in the output portion of the interface. 33 | 34 | ![Firmware Upload](images/firmware-upload.png) 35 | 36 | ## Update Bootloader 37 | 38 | Clicking the ```Update Bootloader``` button on the lower left of the application will erase all firmware on the Artemis and load the latest bootloader firmware. This is helpful when SparkFun releases updates to the [SVL](https://github.com/sparkfun/SparkFun_Apollo3_AmbiqSuite_BSPs/blob/master/common/examples/artemis_svl/src/main.c). 39 | 40 | ![Bootloader Upload](images/bootloader-upload.png) 41 | 42 | **Note:** the bootloader update sometimes fails to start correctly. You may need to repeat the update more than once until it succeeds. 43 | 44 | ## Installation 45 | Installation binaries are available for all major platforms (macOS, Window, and Linux) on the release page of the Artemis Uploader App github repository. 46 | 47 | [Artemis Uploader Release Page](https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases) 48 | 49 | ### Windows 50 | * Download the [github release](https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases) zip file - *ArtemisUploader.win.zip* 51 | * Unzip the release file - *ArtemisUploader.zip* 52 | * This results in the application executable *ArtemisUploader.exe* 53 | * Double-click *ArtemisUploader.exe* to start the application 54 | * The Windows EXE isn't signed, so you will see the following warning. Click **More info**: 55 | 56 | ![Artemis Uploader on Windows - Warning 1](images/artemis-windows-1.png) 57 | 58 | * Click **Run anyway** to start the GUI: 59 | 60 | ![Artemis Uploader on Windows - Warning 2](images/artemis-windows-2.png) 61 | 62 | ![Artemis Uploader on Windows](images/artemis-windows.png) 63 | 64 | ### macOS 65 | 66 | * Check that you have the latest WCH drivers installed for the CH340 interface chip. 67 | * Full instructions can be found in our [CH340 Tutorial](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all#mac-osx) 68 | * Here is a link to the WCH downloads page for the [CH340 / CH341 macOS driver](https://www.wch-ic.com/downloads/CH341SER_MAC_ZIP.html) 69 | * The Zip file contains more instructions: CH34X_DRV_INSTAL_INSTRUCTIONS.pdf 70 | 71 | * Download the [github release](https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases) file - *ArtemisUploader.dmg* 72 | * Click on the downloads icon 73 | * Click the *ArtemisUploader.dmg* file to mount the disk image 74 | * The following Finder window, with the contents of the file will open 75 | 76 | ![Artemis Uploader macOS Finder](images/macos-finder.png) 77 | 78 | * Install the *ArtemisUploader.app* by dragging it onto the *Applications* icon in the ArtemisUploader Finder Window, or copying the file to a desired location. 79 | * Once complete, unmount the ArtemisUploader disk image by clicking on the disk eject in Finder. 80 | 81 | ![Artemis Uploader macOS Finder](images/artemis-macos-install-1.png) 82 | 83 | To launch the Artemis Uploader application: 84 | 85 | * Double-click ArtemisUploader.app to launch the application 86 | 87 | ![Artemis Uploader macOS Finder](images/artemis-macos-install-2.png) 88 | 89 | * The ArtemisUploader.app isn't signed, so macOS won't run the application, and will display a warning dialog. Click **Done**. 90 | 91 | ![Artemis Uploader macOS Finder](images/artemis-macos-install-3.png) 92 | 93 | * To approve app execution bring up the macOS *System Settings* and navigate to *Privacy & Security*. 94 | * On this page, select the **Open Anyway** button to launch the ArtemisUploader application. 95 | 96 | ![Artemis Uploader macOS System Settings](images/artemis-macos-install-4.png) 97 | 98 | * Once selected, macOS will present one last dialog. Select **Open Anyway** to run the application. 99 | 100 | ![Artemis Uploader macOS System Settings](images/artemis-macos-install-5.png) 101 | 102 | * Enter your password and click The ArtemisUploader will now start. 103 | 104 | ![Artemis Uploader macOS System Settings](images/artemis-macos-install-6.png) 105 | 106 | * Ensure you select the correct *COM Port*. The port name should begin with **cu.wchusbserial**. 107 | 108 | ![macOS Artemis Uploader](images/artemis-macos-1.png) 109 | 110 | * When you select the *Firmware File*, click **Allow** to allow the app to open the file. 111 | 112 | ![macOS Artemis Uploader](images/artemis-macos-2.png) 113 | 114 | ### Linux 115 | 116 | * Download the [github release](https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases) file - *ArtemisUploader.linux.gz* 117 | * Unzip the release file - *ArtemisUploader.linux.gz* 118 | * Un-gzip the file, either by double-clicking in on the desktop, or using the `gunzip` command in a terminal window. This results in the file *ArtemisUploader* 119 | * To run the application, the file must have *execute* permission. This is performed by selecting *Properties* from the file right-click menu, and then selecting permissions. You can also change permissions using the `chmod` command in a terminal window. 120 | * Once the application has execute permission, you can start the application a terminal window. Change directory's to the application location and issue `./ArtemisUploader` 121 | 122 | ![Linux Artemis Uploader](images/artemis-linux.png) 123 | 124 | ### Python Package 125 | The Artemis Uploader App is also provided as an installable Python package. This is advantageous for platforms that lack a pre-compiled application. 126 | 127 | To install the Python package: 128 | * Download the [package file](https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases) - *artemis_uploader-3.0.0.tar.gz* (note - the version number might vary) 129 | 130 | At a command line - issue the package install command: 131 | 132 | * `pip install artemis_uploader-3.0.0.tar.gz` 133 | * Once installed, you can start the Artemis Uploader App by issuing the command `./artemis_upload` at the command line. (To see the command, you might need to start a new terminal, or issue a command like `rehash` depending on your platform/shell) 134 | 135 | Notes: 136 | * A path might be needed to specify the install file location. 137 | * Depending on your platform, this command might need to be run as admin/root. 138 | * Depending on your system, you might need to use the command `pip3` 139 | 140 | The uploader is uninstalled by issuing this pip command: 141 | * `pip uninstall artemis-uploader` 142 | 143 | ### Raspberry Pi 144 | We've tested the GUI on 64-bit Raspberry Pi Debian. You will need to use the **Python Package** to install it. 145 | 146 | Notes: 147 | * On 32-bit Raspberry Pi, with both Python 2 and Python 3 installed, use `sudo pip3 install artemis_uploader-3.0.0.tar.gz` 148 | * On 64-bit Raspberry Pi, use `sudo pip install artemis_uploader-3.0.0.tar.gz` 149 | * By default, the executable will be placed in `/usr/local/bin` 150 | * The `sudo` is required to let `setup.py` install `python3-pyqt5` and `python3-pyqt5.qtserialport` using `sudo apt-get install` 151 | 152 | ### Example Firmware 153 | In the applications github repo, an example *Blink.bin* firmware file is included in the repo. This firmware will cause these LEDs to blink at 1Hz: 154 | * the D5 LED on the [SparkFun RedBoard Artemis ATP](https://www.sparkfun.com/products/15442) 155 | * the D13 LED on the [SparkFun RedBoard Artemis](https://www.sparkfun.com/products/15444) 156 | * the D18 LED on the [SparkFun Thing Plus - Artemis](https://www.sparkfun.com/products/15574) 157 | * the D19 LED on the [SparkFun RedBoard Artemis Nano](https://www.sparkfun.com/products/15443) 158 | * the Green LED on the [SparkFun Edge Development Board - Apollo3 Blue](https://www.sparkfun.com/products/15170) 159 | * the STAT LED on the [OpenLog Artemis](https://www.sparkfun.com/products/15846) 160 | * the D19 and GNSS LEDs on the [Artemis Global Tracker](https://www.sparkfun.com/products/16469) 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /artemis_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #----------------------------------------------------------------------------- 3 | # artemis_upload.py 4 | # 5 | #------------------------------------------------------------------------ 6 | # 7 | # Written/Update by SparkFun Electronics, Fall 2022 8 | # 9 | # This python package implements a GUI Qt application that supports 10 | # firmware and boot loader uploading to the SparkFun Artemis module 11 | # 12 | # This file is part of the job dispatch system, which runs "jobs" 13 | # in a background thread for the artemis_uploader package/application. 14 | # 15 | # This file is the main command line entry point, which calls into 16 | # the 'artemis_uploader' package to launch the application 17 | # 18 | # More information on qwiic is at https://www.sparkfun.com/artemis 19 | # 20 | # Do you like this library? Help support SparkFun. Buy a board! 21 | # 22 | #================================================================================== 23 | # Copyright (c) 2022 SparkFun Electronics 24 | # 25 | # Permission is hereby granted, free of charge, to any person obtaining a copy 26 | # of this software and associated documentation files (the "Software"), to deal 27 | # in the Software without restriction, including without limitation the rights 28 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | # copies of the Software, and to permit persons to whom the Software is 30 | # furnished to do so, subject to the following conditions: 31 | # 32 | # The above copyright notice and this permission notice shall be included in all 33 | # copies or substantial portions of the Software. 34 | # 35 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | # SOFTWARE. 42 | #================================================================================== 43 | # 44 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 45 | # 46 | #----------------------------------------------------------------------------- 47 | import artemis_uploader 48 | 49 | # Call the app entry point in the package 50 | artemis_uploader.startArtemisUploader() 51 | 52 | -------------------------------------------------------------------------------- /artemis_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # __init__.py 3 | # 4 | #------------------------------------------------------------------------ 5 | # 6 | # Written/Update by SparkFun Electronics, Fall 2022 7 | # 8 | # This python package implements a GUI Qt application that supports 9 | # firmware and boot loader uploading to the SparkFun Artemis module 10 | # 11 | # This file is defines this directory as the 'artemis_uploader' package. 12 | # 13 | # More information on qwiic is at https://www.sparkfun.com/artemis 14 | # 15 | # Do you like this library? Help support SparkFun. Buy a board! 16 | # 17 | #================================================================================== 18 | # Copyright (c) 2022 SparkFun Electronics 19 | # 20 | # Permission is hereby granted, free of charge, to any person obtaining a copy 21 | # of this software and associated documentation files (the "Software"), to deal 22 | # in the Software without restriction, including without limitation the rights 23 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | # copies of the Software, and to permit persons to whom the Software is 25 | # furnished to do so, subject to the following conditions: 26 | # 27 | # The above copyright notice and this permission notice shall be included in all 28 | # copies or substantial portions of the Software. 29 | # 30 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | # SOFTWARE. 37 | #================================================================================== 38 | # 39 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 40 | # 41 | #----------------------------------------------------------------------------- 42 | # init file to support artemis uploader as a package install 43 | 44 | from .artemis_uploader import startArtemisUploader 45 | -------------------------------------------------------------------------------- /artemis_uploader/artemis_svl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SparkFun Variable Loader 4 | # Variable baud rate bootloader for Artemis Apollo3 modules 5 | 6 | # Immediately upon reset the Artemis module will search for the timing character 7 | # to auto-detect the baud rate. If a valid baud rate is found the Artemis will 8 | # respond with the bootloader version packet 9 | # If the computer receives a well-formatted version number packet at the desired 10 | # baud rate it will send a command to begin bootloading. The Artemis shall then 11 | # respond with the a command asking for the next frame. 12 | # The host will then send a frame packet. If the CRC is OK the Artemis will write 13 | # that to memory and request the next frame. If the CRC fails the Artemis will 14 | # discard that data and send a request to re-send the previous frame. 15 | # This cycle repeats until the Artemis receives a done command in place of the 16 | # requested frame data command. 17 | # The initial baud rate determination must occur within some small timeout. Once 18 | # baud rate detection has completed all additional communication will have a 19 | # universal timeout value. Once the Artemis has begun requesting data it may no 20 | # no longer exit the bootloader. If the host detects a timeout at any point it 21 | # will stop bootloading. 22 | 23 | # Notes about PySerial timeout: 24 | # The timeout operates on whole functions - that is to say that a call to 25 | # ser.read(10) will return after ser.timeout, just as will ser.read(1) (assuming 26 | # that the necessary bytes were not found) 27 | # If there are no incoming bytes (on the line or in the buffer) then two calls to 28 | # ser.read(n) will time out after 2*ser.timeout 29 | # Incoming UART data is buffered behind the scenes, probably by the OS. 30 | 31 | # *********************************************************************************** 32 | # 33 | # Imports 34 | # 35 | # *********************************************************************************** 36 | 37 | import argparse 38 | import serial 39 | import serial.tools.list_ports as list_ports 40 | import sys 41 | import time 42 | import math 43 | import os.path 44 | from sys import exit 45 | 46 | 47 | _verbose = False 48 | 49 | def set_verbose(bVerbose): 50 | global _verbose 51 | 52 | _verbose = bVerbose 53 | 54 | def verboseprint(*args): 55 | 56 | if not _verbose: 57 | return 58 | 59 | # Print each argument separately so caller doesn't need to 60 | # stuff everything to be printed into a single string 61 | for arg in args: 62 | print(arg, end='', flush=True), 63 | print() 64 | 65 | #------------------------------------------------------------------------------------- 66 | 67 | 68 | SCRIPT_VERSION_MAJOR = "1" 69 | SCRIPT_VERSION_MINOR = "7" 70 | 71 | # *********************************************************************************** 72 | # 73 | # Commands 74 | # 75 | # *********************************************************************************** 76 | SVL_CMD_VER = 0x01 # version 77 | SVL_CMD_BL = 0x02 # enter bootload mode 78 | SVL_CMD_NEXT = 0x03 # request next chunk 79 | SVL_CMD_FRAME = 0x04 # indicate app data frame 80 | SVL_CMD_RETRY = 0x05 # request re-send frame 81 | SVL_CMD_DONE = 0x06 # finished - all data sent 82 | 83 | barWidthInCharacters = 40 # Width of progress bar, ie [###### % complete 84 | 85 | crcTable = ( 86 | 0x0000, 0x8005, 0x800F, 0x000A, 0x801B, 0x001E, 0x0014, 0x8011, 87 | 0x8033, 0x0036, 0x003C, 0x8039, 0x0028, 0x802D, 0x8027, 0x0022, 88 | 0x8063, 0x0066, 0x006C, 0x8069, 0x0078, 0x807D, 0x8077, 0x0072, 89 | 0x0050, 0x8055, 0x805F, 0x005A, 0x804B, 0x004E, 0x0044, 0x8041, 90 | 0x80C3, 0x00C6, 0x00CC, 0x80C9, 0x00D8, 0x80DD, 0x80D7, 0x00D2, 91 | 0x00F0, 0x80F5, 0x80FF, 0x00FA, 0x80EB, 0x00EE, 0x00E4, 0x80E1, 92 | 0x00A0, 0x80A5, 0x80AF, 0x00AA, 0x80BB, 0x00BE, 0x00B4, 0x80B1, 93 | 0x8093, 0x0096, 0x009C, 0x8099, 0x0088, 0x808D, 0x8087, 0x0082, 94 | 0x8183, 0x0186, 0x018C, 0x8189, 0x0198, 0x819D, 0x8197, 0x0192, 95 | 0x01B0, 0x81B5, 0x81BF, 0x01BA, 0x81AB, 0x01AE, 0x01A4, 0x81A1, 96 | 0x01E0, 0x81E5, 0x81EF, 0x01EA, 0x81FB, 0x01FE, 0x01F4, 0x81F1, 97 | 0x81D3, 0x01D6, 0x01DC, 0x81D9, 0x01C8, 0x81CD, 0x81C7, 0x01C2, 98 | 0x0140, 0x8145, 0x814F, 0x014A, 0x815B, 0x015E, 0x0154, 0x8151, 99 | 0x8173, 0x0176, 0x017C, 0x8179, 0x0168, 0x816D, 0x8167, 0x0162, 100 | 0x8123, 0x0126, 0x012C, 0x8129, 0x0138, 0x813D, 0x8137, 0x0132, 101 | 0x0110, 0x8115, 0x811F, 0x011A, 0x810B, 0x010E, 0x0104, 0x8101, 102 | 0x8303, 0x0306, 0x030C, 0x8309, 0x0318, 0x831D, 0x8317, 0x0312, 103 | 0x0330, 0x8335, 0x833F, 0x033A, 0x832B, 0x032E, 0x0324, 0x8321, 104 | 0x0360, 0x8365, 0x836F, 0x036A, 0x837B, 0x037E, 0x0374, 0x8371, 105 | 0x8353, 0x0356, 0x035C, 0x8359, 0x0348, 0x834D, 0x8347, 0x0342, 106 | 0x03C0, 0x83C5, 0x83CF, 0x03CA, 0x83DB, 0x03DE, 0x03D4, 0x83D1, 107 | 0x83F3, 0x03F6, 0x03FC, 0x83F9, 0x03E8, 0x83ED, 0x83E7, 0x03E2, 108 | 0x83A3, 0x03A6, 0x03AC, 0x83A9, 0x03B8, 0x83BD, 0x83B7, 0x03B2, 109 | 0x0390, 0x8395, 0x839F, 0x039A, 0x838B, 0x038E, 0x0384, 0x8381, 110 | 0x0280, 0x8285, 0x828F, 0x028A, 0x829B, 0x029E, 0x0294, 0x8291, 111 | 0x82B3, 0x02B6, 0x02BC, 0x82B9, 0x02A8, 0x82AD, 0x82A7, 0x02A2, 112 | 0x82E3, 0x02E6, 0x02EC, 0x82E9, 0x02F8, 0x82FD, 0x82F7, 0x02F2, 113 | 0x02D0, 0x82D5, 0x82DF, 0x02DA, 0x82CB, 0x02CE, 0x02C4, 0x82C1, 114 | 0x8243, 0x0246, 0x024C, 0x8249, 0x0258, 0x825D, 0x8257, 0x0252, 115 | 0x0270, 0x8275, 0x827F, 0x027A, 0x826B, 0x026E, 0x0264, 0x8261, 116 | 0x0220, 0x8225, 0x822F, 0x022A, 0x823B, 0x023E, 0x0234, 0x8231, 117 | 0x8213, 0x0216, 0x021C, 0x8219, 0x0208, 0x820D, 0x8207, 0x0202) 118 | # *********************************************************************************** 119 | # 120 | # Compute CRC on a byte array 121 | # 122 | # *********************************************************************************** 123 | 124 | 125 | def get_crc16(data): 126 | 127 | # Table and code ported from Artemis SVL bootloader 128 | crc = 0x0000 129 | data = bytearray(data) 130 | for ch in data: 131 | tableAddr = ch ^ (crc >> 8) 132 | CRCH = (crcTable[tableAddr] >> 8) ^ (crc & 0xFF) 133 | CRCL = crcTable[tableAddr] & 0x00FF 134 | crc = CRCH << 8 | CRCL 135 | return crc 136 | 137 | 138 | # *********************************************************************************** 139 | # 140 | # Wait for a packet 141 | # 142 | # *********************************************************************************** 143 | def wait_for_packet(ser): 144 | 145 | packet = {'len': 0, 'cmd': 0, 'data': 0, 'crc': 1, 'timeout': 1} 146 | 147 | n = ser.read(2) # get the number of bytes 148 | if(len(n) < 2): 149 | return packet 150 | 151 | packet['len'] = int.from_bytes(n, byteorder='big', signed=False) # 152 | payload = ser.read(packet['len']) 153 | 154 | if(len(payload) != packet['len']): 155 | return packet 156 | 157 | # all bytes received, so timeout is not true 158 | packet['timeout'] = 0 159 | # cmd is the first byte of the payload 160 | packet['cmd'] = payload[0] 161 | # the data is the part of the payload that is not cmd or crc 162 | packet['data'] = payload[1:packet['len']-2] 163 | # performing the crc on the whole payload should return 0 164 | packet['crc'] = get_crc16(payload) 165 | 166 | return packet 167 | 168 | # *********************************************************************************** 169 | # 170 | # Send a packet 171 | # 172 | # *********************************************************************************** 173 | 174 | 175 | def send_packet(ser, cmd, data): 176 | data = bytearray(data) 177 | num_bytes = 3 + len(data) 178 | payload = bytearray(cmd.to_bytes(1, 'big')) 179 | payload.extend(data) 180 | crc = get_crc16(payload) 181 | payload.extend(bytearray(crc.to_bytes(2, 'big'))) 182 | 183 | ser.write(num_bytes.to_bytes(2, 'big')) 184 | ser.write(bytes(payload)) 185 | 186 | 187 | # *********************************************************************************** 188 | # 189 | # Setup: signal baud rate, get version, and command BL enter 190 | # 191 | # *********************************************************************************** 192 | def phase_setup(ser): 193 | 194 | baud_detect_byte = b'U' 195 | 196 | verboseprint('\nPhase:\tSetup') 197 | 198 | # Handle the serial startup blip 199 | ser.reset_input_buffer() 200 | verboseprint('\tCleared startup blip') 201 | 202 | ser.write(baud_detect_byte) # send the baud detection character 203 | 204 | packet = wait_for_packet(ser) 205 | if(packet['timeout'] or packet['crc']): 206 | return False # failed to enter bootloader 207 | 208 | verboseprint('\t') 209 | print(' - Version: ' + str(int.from_bytes(packet['data'], 'big') ) ) 210 | print('') 211 | verboseprint('\tSending \'enter bootloader\' command') 212 | 213 | send_packet(ser, SVL_CMD_BL, b'') 214 | 215 | return True 216 | 217 | # Now enter the bootload phase 218 | 219 | 220 | # *********************************************************************************** 221 | # 222 | # Bootloader phase (Artemis is locked in) 223 | # 224 | # *********************************************************************************** 225 | def phase_bootload(ser, binfile): 226 | 227 | startTime = time.time() 228 | frame_size = 512*4 229 | 230 | resend_max = 4 231 | resend_count = 0 232 | 233 | verboseprint('\nPhase:\tBootload') 234 | 235 | with open(binfile, mode='rb') as binfile: 236 | application = binfile.read() 237 | total_len = len(application) 238 | 239 | total_frames = math.ceil(total_len/frame_size) 240 | curr_frame = 0 241 | progressChars = 0 242 | 243 | if (not _verbose): 244 | print("[Uploading] 0%", end='') 245 | 246 | verboseprint('\thave ' + str(total_len) + 247 | ' bytes to send in ' + str(total_frames) + ' frames') 248 | 249 | bl_done = False 250 | bl_succeeded = True 251 | 252 | while((bl_done == False) and (bl_succeeded == True)): 253 | 254 | # wait for indication by Artemis 255 | packet = wait_for_packet(ser) 256 | if(packet['timeout'] or packet['crc']): 257 | verboseprint('\n\tError receiving packet') 258 | verboseprint(packet) 259 | verboseprint('\n') 260 | bl_succeeded = False 261 | bl_done = True 262 | 263 | if(packet['cmd'] == SVL_CMD_NEXT): 264 | # verboseprint('\tgot frame request') 265 | curr_frame += 1 266 | resend_count = 0 267 | elif(packet['cmd'] == SVL_CMD_RETRY): 268 | verboseprint('\t\tRetrying...') 269 | resend_count += 1 270 | if(resend_count >= resend_max): 271 | bl_succeeded = False 272 | bl_done = True 273 | else: 274 | print('Timeout or unknown error') 275 | bl_succeeded = False 276 | bl_done = True 277 | 278 | if(curr_frame <= total_frames): 279 | frame_data = application[( 280 | (curr_frame-1)*frame_size):((curr_frame-1+1)*frame_size)] 281 | 282 | if _verbose: 283 | 284 | verboseprint('\tSending frame #'+str(curr_frame) + 285 | ', length: '+str(len(frame_data))) 286 | else: 287 | percentComplete = curr_frame * 100 / total_frames 288 | percentCompleteInChars = math.floor( 289 | percentComplete / 100 * barWidthInCharacters) 290 | while(progressChars <= percentCompleteInChars): 291 | progressChars = progressChars + 1 292 | print(u'\b\b\b\b\u2588 {:2d}%'.format(int(percentComplete)), end='', flush=True) # bright block 293 | 294 | send_packet(ser, SVL_CMD_FRAME, frame_data) 295 | 296 | else: 297 | send_packet(ser, SVL_CMD_DONE, b'') 298 | bl_done = True 299 | 300 | print('\n') 301 | if(bl_succeeded == True): 302 | verboseprint('\n\t') 303 | print('Upload Successful') 304 | endTime = time.time() 305 | bps = total_len / (endTime - startTime) 306 | verboseprint('\n\tNominal bootload bps: ' + str(round(bps, 2))) 307 | else: 308 | verboseprint('\n\t') 309 | print('Upload Failed') 310 | 311 | return bl_succeeded 312 | 313 | 314 | # *********************************************************************************** 315 | # 316 | # Help if serial port could not be opened 317 | # 318 | # *********************************************************************************** 319 | def phase_serial_port_help( port ): 320 | 321 | devices = list_ports.comports() 322 | 323 | # First check to see if user has the given port open 324 | for dev in devices: 325 | if(dev.device.upper() == port.upper()): 326 | print(dev.device + " is currently open. Please close any other terminal programs that may be using " + 327 | dev.device + " and try again.") 328 | exit() 329 | 330 | # otherwise, give user a list of possible com ports 331 | print(port.upper() + 332 | " not found but we detected the following serial ports:") 333 | for dev in devices: 334 | if 'CH340' in dev.description: 335 | print( 336 | dev.description + ": Likely an Arduino or derivative. Try " + dev.device + ".") 337 | elif 'FTDI' in dev.description: 338 | print( 339 | dev.description + ": Likely an Arduino or derivative. Try " + dev.device + ".") 340 | elif 'USB Serial Device' in dev.description: 341 | print( 342 | dev.description + ": Possibly an Arduino or derivative.") 343 | else: 344 | print(dev.description) 345 | 346 | 347 | # *********************************************************************************** 348 | # 349 | # Upload function 350 | # 351 | # *********************************************************************************** 352 | def upload_firmware(binfile, port, baud, timeout=0.5): 353 | try: 354 | num_tries = 3 355 | 356 | print('\nArtemis SVL Bootloader', end='') 357 | 358 | verboseprint("Script version " + SCRIPT_VERSION_MAJOR + 359 | "." + SCRIPT_VERSION_MINOR) 360 | 361 | if not os.path.exists(binfile): 362 | print("Bin file {} does not exist.".format(binfile)) 363 | exit() 364 | 365 | bl_success = False 366 | entered_bootloader = False 367 | 368 | # Instantiate ser here and set dtr and rts before opening the port 369 | ser = serial.Serial() 370 | ser.port = port 371 | ser.baudrate = baud 372 | ser.timeout = timeout 373 | 374 | attempt = 0 375 | 376 | while (attempt < num_tries) and (bl_success == False): 377 | 378 | # Set dtr and rts before opening the port 379 | # https://community.sparkfun.com/t/unable-to-flash-artemis-thing-plus-on-macos-sequoia/60766/6 380 | ser.dtr=False 381 | ser.rts=False 382 | 383 | ser.open() 384 | 385 | ser.dtr=True # True sets the CH340 RTS pin low 386 | ser.rts=True 387 | 388 | # startup time for Artemis bootloader (experimentally determined - 0.095 sec min delay) 389 | t_su = 0.15 390 | 391 | time.sleep(t_su) # Allow Artemis to come out of reset 392 | 393 | ser.reset_input_buffer() # reset the input bufer to discard any UART traffic that the device may have generated 394 | 395 | # Perform baud rate negotiation 396 | entered_bootloader = phase_setup(ser) 397 | 398 | if(entered_bootloader == True): 399 | bl_success = phase_bootload(ser, binfile) 400 | else: 401 | verboseprint("Failed to enter bootload phase") 402 | 403 | ser.close() 404 | attempt = attempt + 1 405 | 406 | if (entered_bootloader == False): 407 | print("Target failed to enter bootload mode. Verify the right COM port is selected and that your board has the SVL bootloader.") 408 | elif (bl_success == False): 409 | print("Target entered bootloader mode but firmware upload failed. Verify the right COM port is selected and that your board has the SVL bootloader.") 410 | else: 411 | print("Success!") 412 | 413 | except serial.SerialException: 414 | phase_serial_port_help(port) 415 | 416 | 417 | # ****************************************************************************** 418 | # 419 | # Main program flow 420 | # 421 | # ****************************************************************************** 422 | if __name__ == '__main__': 423 | 424 | parser = argparse.ArgumentParser( 425 | description='SparkFun Serial Bootloader for Artemis') 426 | 427 | parser.add_argument('port', help='Serial COMx Port') 428 | 429 | parser.add_argument('-b', dest='baud', default=115200, type=int, 430 | help='Baud Rate (default is 115200)') 431 | 432 | parser.add_argument('-f', dest='binfile', default='', 433 | help='Binary file to program into the target device') 434 | 435 | parser.add_argument("-v", "--verbose", default=False, help="Enable verbose output", 436 | action="store_true") 437 | 438 | parser.add_argument("-t", "--timeout", default=0.50, help="Communication timeout in seconds (default 0.5)", 439 | type=float) 440 | 441 | if len(sys.argv) < 2: 442 | print("No port selected. Detected Serial Ports:") 443 | devices = list_ports.comports() 444 | for dev in devices: 445 | print(dev.description) 446 | 447 | args = parser.parse_args() 448 | 449 | set_verbose(args.verbose) 450 | 451 | # call upload 452 | 453 | upload_firmware(args.binfile, args.port, args.baud, args.timeout) 454 | -------------------------------------------------------------------------------- /artemis_uploader/artemis_uploader.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # artemis_uploader.py 3 | # 4 | # ------------------------------------------------------------------------ 5 | # 6 | # Written/Update by SparkFun Electronics, Fall 2022 7 | # 8 | # This python package implements a GUI Qt application that supports 9 | # firmware and bootloader uploading to the SparkFun Artemis module 10 | # 11 | # This file is the main application implementation - creating the pyQt 12 | # interface and event handlers. 13 | # 14 | # More information on qwiic is at https://www.sparkfun.com/artemis 15 | # 16 | # Do you like this library? Help support SparkFun. Buy a board! 17 | # 18 | # ================================================================================== 19 | # Copyright (c) 2022 SparkFun Electronics 20 | # 21 | # Permission is hereby granted, free of charge, to any person obtaining a copy 22 | # of this software and associated documentation files (the "Software"), to deal 23 | # in the Software without restriction, including without limitation the rights 24 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | # copies of the Software, and to permit persons to whom the Software is 26 | # furnished to do so, subject to the following conditions: 27 | # 28 | # The above copyright notice and this permission notice shall be included in all 29 | # copies or substantial portions of the Software. 30 | # 31 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | # SOFTWARE. 38 | # ================================================================================== 39 | # 40 | # pylint: disable=missing-docstring, wrong-import-position, no-name-in-module, syntax-error, invalid-name, global-statement 41 | # pylint: disable=unused-variable, too-few-public-methods, too-many-instance-attributes, too-many-locals, too-many-statements 42 | # ----------------------------------------------------------------------------- 43 | 44 | from .au_worker import AUxWorker 45 | from .au_act_artfrmw import AUxArtemisUploadFirmware 46 | from .au_act_artasb import AUxArtemisBurnBootloader 47 | from .au_action import AxJob 48 | 49 | import darkdetect 50 | import sys 51 | import os 52 | import os.path 53 | import platform 54 | 55 | from typing import Iterator, Tuple 56 | from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt 57 | from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QGridLayout, \ 58 | QPushButton, QApplication, QLineEdit, QFileDialog, QPlainTextEdit, \ 59 | QAction, QActionGroup, QMainWindow, QMessageBox 60 | from PyQt5.QtGui import QCloseEvent, QTextCursor, QIcon, QFont, QPixmap 61 | from PyQt5.QtSerialPort import QSerialPortInfo 62 | 63 | 64 | _APP_NAME = "Artemis Firmware Uploader" 65 | 66 | # sub folder for our resource files 67 | _RESOURCE_DIRECTORY = "resource" 68 | # --------------------------------------------------------------------------------------- 69 | # resource_path() 70 | # 71 | # Get the runtime path of app resources. This changes depending on how the app is 72 | # run -> locally, or via pyInstaller 73 | # 74 | # https://stackoverflow.com/a/50914550 75 | 76 | def resource_path(relative_path): 77 | """ Get absolute path to resource, works for dev and for PyInstaller """ 78 | base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))) 79 | return os.path.join(base_path, _RESOURCE_DIRECTORY, relative_path) 80 | 81 | def get_version(rel_path: str) -> str: 82 | try: 83 | with open(resource_path(rel_path), encoding='utf-8') as fp: 84 | for line in fp.read().splitlines(): 85 | if line.startswith("__version__"): 86 | delim = '"' if '"' in line else "'" 87 | return line.split(delim)[1] 88 | raise RuntimeError("Unable to find version string.") 89 | except: 90 | raise RuntimeError("Unable to find _version.py.") 91 | 92 | _APP_VERSION = get_version("_version.py") 93 | 94 | # determine the current GUI style 95 | 96 | # import action things - the .syntax is used since these are part of the package 97 | 98 | # ---------------------------------------------------------------- 99 | # hack to know when a combobox menu is being shown. Helpful if contents 100 | # of list are dynamic -- like serial ports. 101 | 102 | class AUxComboBox(QComboBox): 103 | 104 | popupAboutToBeShown = pyqtSignal() 105 | 106 | def showPopup(self): 107 | self.popupAboutToBeShown.emit() 108 | super().showPopup() 109 | 110 | 111 | # ---------------------------------------------------------------- 112 | # ux_is_darkmode() 113 | # 114 | # Helpful function used during setup to determine if the Ux is in 115 | # dark mode 116 | _is_darkmode = None 117 | 118 | 119 | def ux_is_darkmode() -> bool: 120 | global _is_darkmode 121 | 122 | if _is_darkmode is not None: 123 | return _is_darkmode 124 | 125 | osName = platform.system() 126 | 127 | if osName == "Darwin": 128 | _is_darkmode = darkdetect.isDark() 129 | 130 | elif osName == "Windows": 131 | # it appears that the Qt interface on Windows doesn't apply DarkMode 132 | # So, just keep it light 133 | _is_darkmode = False 134 | elif osName == "Linux": 135 | # Need to check this on Linux at some pont 136 | _is_darkmod = False 137 | 138 | else: 139 | _is_darkmode = False 140 | 141 | return _is_darkmode 142 | 143 | # -------------------------------------------------------------------------------------- 144 | 145 | 146 | BOOTLOADER_VERSION = 5 # << Change this to match the version of artemis_svl.bin 147 | 148 | # Setting constants 149 | SETTING_PORT_NAME = 'port_name' 150 | SETTING_FILE_LOCATION = 'file_location' 151 | SETTING_BAUD_RATE = 'baud' 152 | SETTING_ARTEMIS = 'artemis' 153 | 154 | 155 | def gen_serial_ports() -> Iterator[Tuple[str, str, str]]: 156 | """Return all available serial ports.""" 157 | ports = QSerialPortInfo.availablePorts() 158 | return ((p.description(), p.portName(), p.systemLocation()) for p in ports) 159 | 160 | # noinspection PyArgumentList 161 | 162 | # --------------------------------------------------------------------------------------- 163 | 164 | 165 | class MainWindow(QMainWindow): 166 | """Main Window""" 167 | 168 | sig_message = pyqtSignal(str) 169 | sig_finished = pyqtSignal(int, str, int) 170 | 171 | def __init__(self, parent: QMainWindow = None) -> None: 172 | super().__init__(parent) 173 | 174 | self.installed_bootloader = -1 # Use this to record the bootloader version 175 | 176 | # 177 | self.appFile = 'artemis_svl.bin' # --bin Bootloader binary file 178 | # --load-address-wired dest=loadaddress_blob default=0x60000 179 | self.load_address_blob = 0xC000 180 | # --load-address-blob dest=loadaddress_image 181 | # default=AM_SECBOOT_DEFAULT_NONSECURE_MAIN=0xC000 182 | self.load_address_image = 0x20000 183 | # --magic-num Magic Num (AM_IMAGE_MAGIC_NONSECURE) 184 | self.magic_num = 0xCB 185 | 186 | # File location line edit 187 | msg_label = QLabel(self.tr('Firmware File:')) 188 | self.fileLocation_lineedit = QLineEdit() 189 | msg_label.setBuddy(self.fileLocation_lineedit) 190 | self.fileLocation_lineedit.setEnabled(False) 191 | self.fileLocation_lineedit.returnPressed.connect( 192 | self.on_browse_btn_pressed) 193 | 194 | # Browse for new file button 195 | browse_btn = QPushButton(self.tr('Browse')) 196 | browse_btn.setEnabled(True) 197 | browse_btn.pressed.connect(self.on_browse_btn_pressed) 198 | 199 | # Port Combobox 200 | port_label = QLabel(self.tr('COM Port:')) 201 | self.port_combobox = AUxComboBox() 202 | port_label.setBuddy(self.port_combobox) 203 | self.update_com_ports() 204 | self.port_combobox.popupAboutToBeShown.connect(self.on_port_combobox) 205 | 206 | # Baudrate Combobox 207 | baud_label = QLabel(self.tr('Baud Rate:')) 208 | self.baud_combobox = QComboBox() 209 | baud_label.setBuddy(self.baud_combobox) 210 | self.update_baud_rates() 211 | 212 | # Upload Button 213 | myFont = QFont() 214 | myFont.setBold(True) 215 | self.upload_btn = QPushButton(self.tr(' Upload Firmware ')) 216 | self.upload_btn.setFont(myFont) 217 | self.upload_btn.pressed.connect(self.on_upload_btn_pressed) 218 | 219 | # Upload Button 220 | self.updateBootloader_btn = QPushButton(self.tr(' Update Bootloader ')) 221 | self.updateBootloader_btn.pressed.connect( 222 | self.on_update_bootloader_btn_pressed) 223 | 224 | # Messages Bar 225 | messages_label = QLabel(self.tr('Status / Warnings:')) 226 | 227 | # Messages/Console Window 228 | self.messages = QPlainTextEdit() 229 | color = "C0C0C0" if ux_is_darkmode() else "424242" 230 | self.messages.setStyleSheet("QPlainTextEdit { color: #" + color + ";}") 231 | 232 | # Attempting to reduce window size 233 | #self.messages.setMinimumSize(1, 2) 234 | #self.messages.resize(1, 2) 235 | 236 | # Menu Bar 237 | menubar = self.menuBar() 238 | boardMenu = menubar.addMenu('Board Type') 239 | 240 | boardGroup = QActionGroup(self) 241 | 242 | self.artemis = QAction('Artemis', self, checkable=True) 243 | self.artemis.setStatusTip( 244 | 'Artemis-based boards including the OLA and AGT') 245 | self.artemis.setChecked(True) # Default to artemis. _load_settings will override this 246 | a = boardGroup.addAction(self.artemis) 247 | boardMenu.addAction(a) 248 | 249 | self.apollo3 = QAction('Apollo3', self, checkable=True) 250 | self.apollo3.setStatusTip( 251 | 'Apollo3 Blue development boards including the SparkFun Edge') 252 | self.apollo3.setChecked(False) # Default to artemis. _load_settings will override this 253 | a = boardGroup.addAction(self.apollo3) 254 | boardMenu.addAction(a) 255 | 256 | # Add an artemis logo to the user interface 257 | logo = QLabel(self) 258 | icon = "artemis-icon.png" if ux_is_darkmode() else "artemis-icon-blk.png" 259 | pixmap = QPixmap(resource_path(icon)) 260 | logo.setPixmap(pixmap) 261 | 262 | # Arrange Layout 263 | layout = QGridLayout() 264 | 265 | layout.addWidget(msg_label, 1, 0) 266 | layout.addWidget(self.fileLocation_lineedit, 1, 1) 267 | layout.addWidget(browse_btn, 1, 2) 268 | 269 | layout.addWidget(port_label, 2, 0) 270 | layout.addWidget(self.port_combobox, 2, 1) 271 | 272 | layout.addWidget(logo, 2, 2, 2, 3, alignment=Qt.AlignCenter) 273 | 274 | layout.addWidget(baud_label, 3, 0) 275 | layout.addWidget(self.baud_combobox, 3, 1) 276 | 277 | layout.addWidget(messages_label, 4, 0) 278 | layout.addWidget(self.messages, 5, 0, 5, 3) 279 | 280 | layout.addWidget(self.upload_btn, 15, 2) 281 | layout.addWidget(self.updateBootloader_btn, 15, 0) 282 | 283 | widget = QWidget() 284 | widget.setLayout(layout) 285 | self.setCentralWidget(widget) 286 | 287 | self.settings = QSettings() 288 | self._load_settings() 289 | 290 | # Make the text edit window read-only 291 | self.messages.setReadOnly(True) 292 | self.messages.clear() # Clear the message window 293 | 294 | self.setWindowTitle(_APP_NAME + " - " + _APP_VERSION) 295 | 296 | # Initial Status Bar 297 | self.statusBar().showMessage(_APP_NAME + " - " + _APP_VERSION, 10000) 298 | 299 | # setup our background worker thread ... 300 | 301 | # connect the signals from the background processor to callback 302 | # methods/slots. This makes it thread safe 303 | self.sig_message.connect(self.log_message) 304 | self.sig_finished.connect(self.on_finished) 305 | 306 | # Create our background worker object, which also will do work in it's 307 | # own thread. 308 | self._worker = AUxWorker(self.on_worker_callback) 309 | 310 | # add the actions/commands for this app to the background processing thread. 311 | # These actions are passed jobs to execute. 312 | self._worker.add_action( 313 | AUxArtemisUploadFirmware(), AUxArtemisBurnBootloader()) 314 | 315 | # -------------------------------------------------------------- 316 | # callback function for the background worker. 317 | # 318 | # It is assumed that this method is called by the background thread 319 | # so signals and used to relay the call to the GUI running on the 320 | # main thread 321 | 322 | def on_worker_callback(self, *args): #msg_type, arg): 323 | 324 | # need a min of 2 args (id, arg) 325 | if len(args) < 2: 326 | self.log_message("Invalid parameters from the uploader.") 327 | return 328 | 329 | msg_type = args[0] 330 | if msg_type == AUxWorker.TYPE_MESSAGE: 331 | self.sig_message.emit(args[1]) 332 | elif msg_type == AUxWorker.TYPE_FINISHED: 333 | # finished takes 3 args - status, job type, and job id 334 | if len(args) < 4: 335 | self.log_message("Invalid parameters from the uploader."); 336 | return; 337 | 338 | self.sig_finished.emit(args[1], args[2], args[3]) 339 | 340 | # -------------------------------------------------------------- 341 | @pyqtSlot(str) 342 | def log_message(self, msg: str) -> None: 343 | """Add msg to the messages window, ensuring that it is visible""" 344 | 345 | # The passed in text is inserted *raw* at the end of the console 346 | # text area. The insert method doesn't add any newlines. Most of the 347 | # text being recieved originates in a print() call, which adds newlines. 348 | 349 | self.messages.moveCursor(QTextCursor.End) 350 | 351 | # Backspace ("\b")?? 352 | tmp = msg 353 | while len(tmp) > 2 and tmp.startswith('\b'): 354 | 355 | # remove the "\b" from the input string, and delete the 356 | # previous character from the cursor in the text console 357 | tmp = tmp[1:] 358 | self.messages.textCursor().deletePreviousChar() 359 | self.messages.moveCursor(QTextCursor.End) 360 | 361 | # insert the new text at the end of the console 362 | self.messages.insertPlainText(tmp) 363 | 364 | # make sure cursor is at end of text and it's visible 365 | self.messages.moveCursor(QTextCursor.End) 366 | self.messages.ensureCursorVisible() 367 | self.messages.repaint() # Update/refresh the message window 368 | 369 | # -------------------------------------------------------------- 370 | # on_finished() 371 | # 372 | # Slot for sending the "on finished" signal from the background thread 373 | # 374 | # Called when the backgroudn job is finished and includes a status value 375 | @pyqtSlot(int, str, int) 376 | def on_finished(self, status, action_type, job_id) -> None: 377 | 378 | # re-enable the UX 379 | self.disable_interface(False) 380 | 381 | # update the status message 382 | msg = "" # if status == 0 else "with an error" 383 | self.statusBar().showMessage("The upload process finished " + msg, 2000) 384 | 385 | # -------------------------------------------------------------- 386 | # on_port_combobox() 387 | # 388 | # Called when the combobox pop-up menu is about to be shown 389 | # 390 | # Use this event to dynamically update the displayed ports 391 | # 392 | @pyqtSlot() 393 | def on_port_combobox(self): 394 | self.statusBar().showMessage("Updating ports...", 500) 395 | self.update_com_ports() 396 | 397 | # --------------------------------------------------------------- 398 | 399 | def _load_settings(self) -> None: 400 | """Load settings on startup.""" 401 | 402 | port_name = self.settings.value(SETTING_PORT_NAME) 403 | if port_name is not None: 404 | index = self.port_combobox.findData(port_name) 405 | if index > -1: 406 | self.port_combobox.setCurrentIndex(index) 407 | 408 | lastFile = self.settings.value(SETTING_FILE_LOCATION) 409 | if lastFile is not None: 410 | self.fileLocation_lineedit.setText(lastFile) 411 | 412 | baud = self.settings.value(SETTING_BAUD_RATE) 413 | if baud is not None: 414 | index = self.baud_combobox.findData(baud) 415 | if index > -1: 416 | self.baud_combobox.setCurrentIndex(index) 417 | 418 | checked = self.settings.value(SETTING_ARTEMIS) 419 | if checked is not None: 420 | if checked == 'True': 421 | self.artemis.setChecked(True) 422 | self.apollo3.setChecked(False) 423 | else: 424 | self.artemis.setChecked(False) 425 | self.apollo3.setChecked(True) 426 | 427 | # -------------------------------------------------------------- 428 | def _save_settings(self) -> None: 429 | """Save settings on shutdown.""" 430 | 431 | self.settings.setValue(SETTING_PORT_NAME, self.port) 432 | self.settings.setValue(SETTING_FILE_LOCATION, self.theFileName) 433 | self.settings.setValue(SETTING_BAUD_RATE, self.baudRate) 434 | if self.artemis.isChecked(): # Convert isChecked to str 435 | checkedStr = 'True' 436 | else: 437 | checkedStr = 'False' 438 | self.settings.setValue(SETTING_ARTEMIS, checkedStr) 439 | 440 | # -------------------------------------------------------------- 441 | def _clean_settings(self) -> None: 442 | """Clean (remove) all existing settings.""" 443 | settings = QSettings() 444 | settings.clear() 445 | 446 | # -------------------------------------------------------------- 447 | def show_error_message(self, msg: str) -> None: 448 | """Show a Message Box with the error message.""" 449 | QMessageBox.critical(self, QApplication.applicationName(), str(msg)) 450 | 451 | # -------------------------------------------------------------- 452 | def update_com_ports(self) -> None: 453 | """Update COM Port list in GUI.""" 454 | previousPort = self.port # Record the previous port before we clear the combobox 455 | 456 | self.port_combobox.clear() 457 | 458 | index = 0 459 | indexOfCH340 = -1 460 | indexOfPrevious = -1 461 | for desc, name, nsys in gen_serial_ports(): 462 | 463 | longname = desc + " (" + name + ")" 464 | self.port_combobox.addItem(longname, nsys) 465 | if "CH340" in longname: 466 | # Select the first available CH340 467 | # This is likely to only work on Windows. Linux port names are different. 468 | if indexOfCH340 == -1: 469 | indexOfCH340 = index 470 | # it could be too early to call 471 | #self.log_message("CH340 found at index " + str(indexOfCH340)) 472 | # as the GUI might not exist yet 473 | if nsys == previousPort: # Previous port still exists so record it 474 | indexOfPrevious = index 475 | index = index + 1 476 | 477 | if indexOfPrevious > -1: # Restore the previous port if it still exists 478 | self.port_combobox.setCurrentIndex(indexOfPrevious) 479 | if indexOfCH340 > -1: # If we found a CH340, let that take priority 480 | self.port_combobox.setCurrentIndex(indexOfCH340) 481 | 482 | # -------------------------------------------------------------- 483 | # Is a port still valid? 484 | 485 | def verify_port(self, port) -> bool: 486 | 487 | # Valid inputs - Check the port 488 | for desc, name, nsys in gen_serial_ports(): 489 | if nsys == port: 490 | return True 491 | 492 | return False 493 | # -------------------------------------------------------------- 494 | 495 | def update_baud_rates(self) -> None: 496 | """Update baud rate list in GUI.""" 497 | # Lowest speed first so code defaults to that 498 | # if settings.value(SETTING_BAUD_RATE) is None 499 | self.baud_combobox.clear() 500 | self.baud_combobox.addItem("115200", 115200) 501 | self.baud_combobox.addItem("460800", 460800) 502 | self.baud_combobox.addItem("921600", 921600) 503 | 504 | # -------------------------------------------------------------- 505 | @property 506 | def port(self) -> str: 507 | """Return the current serial port.""" 508 | return self.port_combobox.currentData() 509 | 510 | # -------------------------------------------------------------- 511 | @property 512 | def baudRate(self) -> str: 513 | """Return the current baud rate.""" 514 | return self.baud_combobox.currentData() 515 | 516 | # -------------------------------------------------------------- 517 | @property 518 | def theFileName(self) -> str: 519 | """Return the current file location.""" 520 | return self.fileLocation_lineedit.text() 521 | 522 | # -------------------------------------------------------------- 523 | def closeEvent(self, event: QCloseEvent) -> None: 524 | """Handle Close event of the Widget.""" 525 | self._save_settings() 526 | 527 | # shutdown the background worker/stop it so the app exits correctly 528 | self._worker.shutdown() 529 | 530 | event.accept() 531 | 532 | # -------------------------------------------------------------- 533 | # disable_interface() 534 | # 535 | # Enable/Disable portions of the ux - often used when a job is running 536 | # 537 | def disable_interface(self, bDisable=False): 538 | 539 | self.upload_btn.setDisabled(bDisable) 540 | self.updateBootloader_btn.setDisabled(bDisable) 541 | 542 | # -------------------------------------------------------------- 543 | # on_upload_btn_pressed() 544 | # 545 | 546 | def on_upload_btn_pressed(self) -> None: 547 | 548 | # Valid inputs - Check the port 549 | if not self.verify_port(self.port): 550 | self.log_message("Port No Longer Available") 551 | return 552 | 553 | # Does the upload file exist? 554 | fmwFile = self.fileLocation_lineedit.text() 555 | if not os.path.exists(fmwFile): 556 | self.log_message("The firmware file was not found: " + fmwFile) 557 | return 558 | 559 | # Create a job and add it to the job queue. The worker thread will pick this up and 560 | # process the job. Can set job values using dictionary syntax, or attribute assignments 561 | # 562 | # Note - the job is defined with the ID of the target action 563 | theJob = AxJob(AUxArtemisUploadFirmware.ACTION_ID, 564 | {"port": self.port, "baud": self.baudRate, "file": fmwFile}) 565 | 566 | # Send the job to the worker to process 567 | job_id = self._worker.add_job(theJob) 568 | 569 | self.disable_interface(True) 570 | 571 | # -------------------------------------------------------------- 572 | def on_update_bootloader_btn_pressed(self) -> None: 573 | 574 | # Valid inputs - Check the port 575 | if not self.verify_port(self.port): 576 | self.log_message("Port No Longer Available") 577 | return 578 | 579 | # Does the bootloader file exist? 580 | blFile = resource_path(self.appFile) 581 | if not os.path.exists(blFile): 582 | self.log_message("The bootloader file was not found: " + blFile) 583 | return 584 | 585 | # Make up a job and add it to the job queue. The worker thread will pick this up and 586 | # process the job. Can set job values using dictionary syntax, or attribute assignments 587 | theJob = AxJob(AUxArtemisBurnBootloader.ACTION_ID, 588 | {"port": self.port, "baud": self.baudRate, "file": blFile}) 589 | 590 | # Send the job to the worker to process 591 | job_id = self._worker.add_job(theJob) 592 | 593 | self.disable_interface(True) 594 | 595 | # -------------------------------------------------------------- 596 | def on_browse_btn_pressed(self) -> None: 597 | """Open dialog to select bin file.""" 598 | 599 | self.statusBar().showMessage("Select firmware file for upload...", 4000) 600 | options = QFileDialog.Options() 601 | fileName, _ = QFileDialog.getOpenFileName( 602 | None, 603 | "Select Firmware to Upload", 604 | "", 605 | "Firmware Files (*.bin);;All Files (*)", 606 | options=options) 607 | if fileName: 608 | self.fileLocation_lineedit.setText(fileName) 609 | 610 | # ------------------------------------------------------------------ 611 | # startArtemisUploader() 612 | # 613 | # This is the main entry point function to start the application GUI 614 | # 615 | # This is called from the command line script that launches the application 616 | # 617 | 618 | 619 | def startArtemisUploader(): 620 | 621 | app = QApplication([]) 622 | app.setOrganizationName('SparkFun Electronics') 623 | app.setApplicationName(_APP_NAME + ' - ' + _APP_VERSION) 624 | app.setWindowIcon(QIcon(resource_path("artemis-logo-rounded.png"))) 625 | app.setApplicationVersion(_APP_VERSION) 626 | w = MainWindow() 627 | w.show() 628 | sys.exit(app.exec_()) 629 | 630 | 631 | # ------------------------------------------------------------------ 632 | # This is probably not needed/working now that this is file is part of a package, 633 | # but leaving here anyway ... 634 | if __name__ == '__main__': 635 | startArtemisUploader() 636 | -------------------------------------------------------------------------------- /artemis_uploader/asb/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Make a package out of asb.py. Just export main() from asb.p 4 | 5 | from .asb import main -------------------------------------------------------------------------------- /artemis_uploader/asb/am_defines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Utility functioins 3 | 4 | import sys 5 | from Crypto.Cipher import AES 6 | from Crypto.PublicKey import RSA 7 | from Crypto.Signature import PKCS1_v1_5 8 | from Crypto.Hash import SHA256 9 | import array 10 | import hashlib 11 | import hmac 12 | import os 13 | import binascii 14 | 15 | 16 | ivVal0 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 17 | 18 | FLASH_PAGE_SIZE = 0x2000 # 8K 19 | MAX_DOWNLOAD_SIZE = 0x48000 # 288K 20 | AM_SECBOOT_DEFAULT_NONSECURE_MAIN = 0xC000 21 | 22 | AM_SECBOOT_AESCBC_BLOCK_SIZE_WORDS = 4 23 | AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES = 4*AM_SECBOOT_AESCBC_BLOCK_SIZE_WORDS 24 | 25 | AM_SECBOOT_MIN_KEYIDX_INFO0 = 8 ## KeyIdx 8 - 15 26 | AM_SECBOOT_MAX_KEYIDX_INFO0 = 15 27 | AM_SECBOOT_MIN_KEYIDX_INFO1 = 0 ## KeyIdx 0 - 7 28 | AM_SECBOOT_MAX_KEYIDX_INFO1 = 7 29 | AM_SECBOOT_KEYIDX_BYTES = 16 30 | 31 | # Encryption Algorithm 32 | AM_SECBOOT_ENC_ALGO_NONE = 0 33 | AM_SECBOOT_ENC_ALGO_AES128 = 1 34 | AM_SECBOOT_ENC_ALGO_MAX = AM_SECBOOT_ENC_ALGO_AES128 35 | # String constants 36 | helpEncAlgo = 'Encryption Algo? (0(default) = none, 1 = AES128)' 37 | 38 | # Authentication Algorithm 39 | AM_SECBOOT_AUTH_ALGO_NONE = 0 40 | AM_SECBOOT_AUTH_ALGO_SHA256HMAC = 1 41 | AM_SECBOOT_AUTH_ALGO_MAX = AM_SECBOOT_AUTH_ALGO_SHA256HMAC 42 | # String constants 43 | helpAuthAlgo = 'Authentication Algo? (0(default) = none, 1 = SHA256)' 44 | 45 | 46 | FLASH_INVALID = 0xFFFFFFFF 47 | 48 | # KeyWrap Mode 49 | AM_SECBOOT_KEYWRAP_NONE = 0 50 | AM_SECBOOT_KEYWRAP_XOR = 1 51 | AM_SECBOOT_KEYWRAP_AES128 = 2 52 | AM_SECBOOT_KEYWRAP_MAX = AM_SECBOOT_KEYWRAP_AES128 53 | 54 | #****************************************************************************** 55 | # 56 | # Magic Numbers 57 | # 58 | #****************************************************************************** 59 | AM_IMAGE_MAGIC_MAIN = 0xC0 60 | AM_IMAGE_MAGIC_CHILD = 0xCC 61 | AM_IMAGE_MAGIC_NONSECURE = 0xCB 62 | AM_IMAGE_MAGIC_INFO0 = 0xCF 63 | 64 | # Dummy for creating images for customer - not understood by SBL 65 | # This could be any value from the definition: 66 | # #define AM_IMAGE_MAGIC_CUST(x) ((((x) & 0xF0) == 0xC0) && ((x) != 0xC0) && ((x) != 0xCC) && ((x) != 0xCB) && ((x) != 0xCF)) 67 | AM_IMAGE_MAGIC_CUSTPATCH = 0xC1 68 | 69 | #****************************************************************************** 70 | # 71 | # Image Types 72 | # 73 | #****************************************************************************** 74 | AM_SECBOOT_WIRED_IMAGETYPE_SBL = 0 75 | AM_SECBOOT_WIRED_IMAGETYPE_AM3P = 1 76 | AM_SECBOOT_WIRED_IMAGETYPE_PATCH = 2 77 | AM_SECBOOT_WIRED_IMAGETYPE_MAIN = 3 78 | AM_SECBOOT_WIRED_IMAGETYPE_CHILD = 4 79 | AM_SECBOOT_WIRED_IMAGETYPE_CUSTPATCH = 5 80 | AM_SECBOOT_WIRED_IMAGETYPE_NONSECURE = 6 81 | AM_SECBOOT_WIRED_IMAGETYPE_INFO0 = 7 82 | AM_SECBOOT_WIRED_IMAGETYPE_INFO0_NOOTA = 32 83 | AM_SECBOOT_WIRED_IMAGETYPE_INVALID = 0xFF 84 | 85 | 86 | #****************************************************************************** 87 | # 88 | # Wired Message Types 89 | # 90 | #****************************************************************************** 91 | AM_SECBOOT_WIRED_MSGTYPE_HELLO = 0 92 | AM_SECBOOT_WIRED_MSGTYPE_STATUS = 1 93 | AM_SECBOOT_WIRED_MSGTYPE_OTADESC = 2 94 | AM_SECBOOT_WIRED_MSGTYPE_UPDATE = 3 95 | AM_SECBOOT_WIRED_MSGTYPE_ABORT = 4 96 | AM_SECBOOT_WIRED_MSGTYPE_RECOVER = 5 97 | AM_SECBOOT_WIRED_MSGTYPE_RESET = 6 98 | AM_SECBOOT_WIRED_MSGTYPE_ACK = 7 99 | AM_SECBOOT_WIRED_MSGTYPE_DATA = 8 100 | 101 | 102 | #****************************************************************************** 103 | # 104 | # Wired Message ACK Status 105 | # 106 | #****************************************************************************** 107 | AM_SECBOOT_WIRED_ACK_STATUS_SUCCESS = 0 108 | AM_SECBOOT_WIRED_ACK_STATUS_FAILURE = 1 109 | AM_SECBOOT_WIRED_ACK_STATUS_INVALID_INFO0 = 2 110 | AM_SECBOOT_WIRED_ACK_STATUS_CRC = 3 111 | AM_SECBOOT_WIRED_ACK_STATUS_SEC = 4 112 | AM_SECBOOT_WIRED_ACK_STATUS_MSG_TOO_BIG = 5 113 | AM_SECBOOT_WIRED_ACK_STATUS_UNKNOWN_MSGTYPE = 6 114 | AM_SECBOOT_WIRED_ACK_STATUS_INVALID_ADDR = 7 115 | AM_SECBOOT_WIRED_ACK_STATUS_INVALID_OPERATION = 8 116 | AM_SECBOOT_WIRED_ACK_STATUS_INVALID_PARAM = 9 117 | AM_SECBOOT_WIRED_ACK_STATUS_SEQ = 10 118 | AM_SECBOOT_WIRED_ACK_STATUS_TOO_MUCH_DATA = 11 119 | 120 | #****************************************************************************** 121 | # 122 | # Definitions related to Image Headers 123 | # 124 | #****************************************************************************** 125 | AM_HMAC_SIG_SIZE = 32 126 | AM_KEK_SIZE = 16 127 | AM_CRC_SIZE = 4 128 | 129 | AM_MAX_UART_MSG_SIZE = 8192 # 8K buffer in SBL 130 | 131 | # Wiredupdate Image Header 132 | AM_WU_IMAGEHDR_OFFSET_SIG = 16 133 | AM_WU_IMAGEHDR_OFFSET_IV = 48 134 | AM_WU_IMAGEHDR_OFFSET_KEK = 64 135 | AM_WU_IMAGEHDR_OFFSET_IMAGETYPE = (AM_WU_IMAGEHDR_OFFSET_KEK + AM_KEK_SIZE) 136 | AM_WU_IMAGEHDR_OFFSET_OPTIONS = (AM_WU_IMAGEHDR_OFFSET_IMAGETYPE + 1) 137 | AM_WU_IMAGEHDR_OFFSET_KEY = (AM_WU_IMAGEHDR_OFFSET_IMAGETYPE + 4) 138 | AM_WU_IMAGEHDR_OFFSET_ADDR = (AM_WU_IMAGEHDR_OFFSET_KEY + 4) 139 | AM_WU_IMAGEHDR_OFFSET_SIZE = (AM_WU_IMAGEHDR_OFFSET_ADDR + 4) 140 | 141 | AM_WU_IMAGEHDR_START_HMAC = (AM_WU_IMAGEHDR_OFFSET_SIG + AM_HMAC_SIG_SIZE) 142 | AM_WU_IMAGEHDR_START_ENCRYPT = (AM_WU_IMAGEHDR_OFFSET_KEK + AM_KEK_SIZE) 143 | AM_WU_IMAGEHDR_SIZE = (AM_WU_IMAGEHDR_OFFSET_KEK + AM_KEK_SIZE + 16) 144 | 145 | 146 | # Image Header 147 | AM_IMAGEHDR_SIZE_MAIN = 256 148 | AM_IMAGEHDR_SIZE_AUX = (112 + AM_KEK_SIZE) 149 | 150 | AM_IMAGEHDR_OFFSET_CRC = 4 151 | AM_IMAGEHDR_OFFSET_SIG = 16 152 | AM_IMAGEHDR_OFFSET_IV = 48 153 | AM_IMAGEHDR_OFFSET_KEK = 64 154 | AM_IMAGEHDR_OFFSET_SIGCLR = (AM_IMAGEHDR_OFFSET_KEK + AM_KEK_SIZE) 155 | AM_IMAGEHDR_START_CRC = (AM_IMAGEHDR_OFFSET_CRC + AM_CRC_SIZE) 156 | AM_IMAGEHDR_START_HMAC_INST = (AM_IMAGEHDR_OFFSET_SIG + AM_HMAC_SIG_SIZE) 157 | AM_IMAGEHDR_START_ENCRYPT = (AM_IMAGEHDR_OFFSET_KEK + AM_KEK_SIZE) 158 | AM_IMAGEHDR_START_HMAC = (AM_IMAGEHDR_OFFSET_SIGCLR + AM_HMAC_SIG_SIZE) 159 | AM_IMAGEHDR_OFFSET_ADDR = AM_IMAGEHDR_START_HMAC 160 | AM_IMAGEHDR_OFFSET_VERKEY = (AM_IMAGEHDR_OFFSET_ADDR + 4) 161 | AM_IMAGEHDR_OFFSET_CHILDPTR = (AM_IMAGEHDR_OFFSET_VERKEY + 4) 162 | 163 | # Recover message 164 | AM_WU_RECOVERY_HDR_SIZE = 44 165 | AM_WU_RECOVERY_HDR_OFFSET_CUSTID = 8 166 | AM_WU_RECOVERY_HDR_OFFSET_RECKEY = (AM_WU_RECOVERY_HDR_OFFSET_CUSTID + 4) 167 | AM_WU_RECOVERY_HDR_OFFSET_NONCE = (AM_WU_RECOVERY_HDR_OFFSET_RECKEY + 16) 168 | AM_WU_RECOVERY_HDR_OFFSET_RECBLOB = (AM_WU_RECOVERY_HDR_OFFSET_NONCE + 16) 169 | 170 | 171 | #****************************************************************************** 172 | # 173 | # INFOSPACE related definitions 174 | # 175 | #****************************************************************************** 176 | AM_SECBOOT_INFO0_SIGN_PROGRAMMED0 = 0x48EAAD88 177 | AM_SECBOOT_INFO0_SIGN_PROGRAMMED1 = 0xC9705737 178 | AM_SECBOOT_INFO0_SIGN_PROGRAMMED2 = 0x0A6B8458 179 | AM_SECBOOT_INFO0_SIGN_PROGRAMMED3 = 0xE41A9D74 180 | 181 | AM_SECBOOT_INFO0_SIGN_UINIT0 = 0x5B75A5FA 182 | AM_SECBOOT_INFO0_SIGN_UINIT1 = 0x7B9C8674 183 | AM_SECBOOT_INFO0_SIGN_UINIT2 = 0x869A96FE 184 | AM_SECBOOT_INFO0_SIGN_UINIT3 = 0xAEC90860 185 | 186 | INFO_SIZE_BYTES = (8 * 1024) 187 | INFO_MAX_AUTH_KEY_WORDS = 32 188 | INFO_MAX_ENC_KEY_WORDS = 32 189 | 190 | INFO_MAX_AUTH_KEYS = (INFO_MAX_AUTH_KEY_WORDS*4//AM_SECBOOT_KEYIDX_BYTES) 191 | INFO_MAX_ENC_KEYS = (INFO_MAX_ENC_KEY_WORDS*4//AM_SECBOOT_KEYIDX_BYTES) 192 | 193 | INFO0_SIGNATURE0_O = 0x00000000 194 | INFO0_SIGNATURE1_O = 0x00000004 195 | INFO0_SIGNATURE2_O = 0x00000008 196 | INFO0_SIGNATURE3_O = 0x0000000c 197 | INFO0_SECURITY_O = 0x00000010 198 | INFO0_CUSTOMER_TRIM_O = 0x00000014 199 | INFO0_CUSTOMER_TRIM2_O = 0x00000018 200 | INFO0_SECURITY_OVR_O = 0x00000020 201 | INFO0_SECURITY_WIRED_CFG_O = 0x00000024 202 | INFO0_SECURITY_WIRED_IFC_CFG0_O = 0x00000028 203 | INFO0_SECURITY_WIRED_IFC_CFG1_O = 0x0000002C 204 | INFO0_SECURITY_WIRED_IFC_CFG2_O = 0x00000030 205 | INFO0_SECURITY_WIRED_IFC_CFG3_O = 0x00000034 206 | INFO0_SECURITY_WIRED_IFC_CFG4_O = 0x00000038 207 | INFO0_SECURITY_WIRED_IFC_CFG5_O = 0x0000003C 208 | INFO0_SECURITY_VERSION_O = 0x00000040 209 | INFO0_SECURITY_SRAM_RESV_O = 0x00000050 210 | AM_REG_INFO0_SECURITY_SRAM_RESV_SRAM_RESV_M = 0x0000FFFF 211 | INFO0_WRITE_PROTECT_L_O = 0x000001f8 212 | INFO0_WRITE_PROTECT_H_O = 0x000001fc 213 | INFO0_COPY_PROTECT_L_O = 0x00000200 214 | INFO0_COPY_PROTECT_H_O = 0x00000204 215 | INFO0_WRITE_PROTECT_SBL_L_O = 0x000009f8 216 | INFO0_WRITE_PROTECT_SBL_H_O = 0x000009fc 217 | INFO0_COPY_PROTECT_SBL_L_O = 0x00000A00 218 | INFO0_COPY_PROTECT_SBL_H_O = 0x00000A04 219 | INFO0_MAIN_PTR1_O = 0x00000C00 220 | INFO0_MAIN_PTR2_O = 0x00000C04 221 | INFO0_KREVTRACK_O = 0x00000C08 222 | INFO0_AREVTRACK_O = 0x00000C0C 223 | INFO0_MAIN_CNT0_O = 0x00000FF8 224 | INFO0_MAIN_CNT1_O = 0x00000FFC 225 | 226 | INFO0_CUST_KEK_W0_O = 0x00001800 227 | INFO0_CUST_KEK_W1_O = 0x00001804 228 | INFO0_CUST_KEK_W2_O = 0x00001808 229 | INFO0_CUST_KEK_W3_O = 0x0000180c 230 | INFO0_CUST_KEK_W4_O = 0x00001810 231 | INFO0_CUST_KEK_W5_O = 0x00001814 232 | INFO0_CUST_KEK_W6_O = 0x00001818 233 | INFO0_CUST_KEK_W7_O = 0x0000181c 234 | INFO0_CUST_KEK_W8_O = 0x00001820 235 | INFO0_CUST_KEK_W9_O = 0x00001824 236 | INFO0_CUST_KEK_W10_O = 0x00001828 237 | INFO0_CUST_KEK_W11_O = 0x0000182c 238 | INFO0_CUST_KEK_W12_O = 0x00001830 239 | INFO0_CUST_KEK_W13_O = 0x00001834 240 | INFO0_CUST_KEK_W14_O = 0x00001838 241 | INFO0_CUST_KEK_W15_O = 0x0000183c 242 | INFO0_CUST_KEK_W16_O = 0x00001840 243 | INFO0_CUST_KEK_W17_O = 0x00001844 244 | INFO0_CUST_KEK_W18_O = 0x00001848 245 | INFO0_CUST_KEK_W19_O = 0x0000184c 246 | INFO0_CUST_KEK_W20_O = 0x00001850 247 | INFO0_CUST_KEK_W21_O = 0x00001854 248 | INFO0_CUST_KEK_W22_O = 0x00001858 249 | INFO0_CUST_KEK_W23_O = 0x0000185c 250 | INFO0_CUST_KEK_W24_O = 0x00001860 251 | INFO0_CUST_KEK_W25_O = 0x00001864 252 | INFO0_CUST_KEK_W26_O = 0x00001868 253 | INFO0_CUST_KEK_W27_O = 0x0000186c 254 | INFO0_CUST_KEK_W28_O = 0x00001870 255 | INFO0_CUST_KEK_W29_O = 0x00001874 256 | INFO0_CUST_KEK_W30_O = 0x00001878 257 | INFO0_CUST_KEK_W31_O = 0x0000187c 258 | INFO0_CUST_AUTH_W0_O = 0x00001880 259 | INFO0_CUST_AUTH_W1_O = 0x00001884 260 | INFO0_CUST_AUTH_W2_O = 0x00001888 261 | INFO0_CUST_AUTH_W3_O = 0x0000188c 262 | INFO0_CUST_AUTH_W4_O = 0x00001890 263 | INFO0_CUST_AUTH_W5_O = 0x00001894 264 | INFO0_CUST_AUTH_W6_O = 0x00001898 265 | INFO0_CUST_AUTH_W7_O = 0x0000189c 266 | INFO0_CUST_AUTH_W8_O = 0x000018a0 267 | INFO0_CUST_AUTH_W9_O = 0x000018a4 268 | INFO0_CUST_AUTH_W10_O = 0x000018a8 269 | INFO0_CUST_AUTH_W11_O = 0x000018ac 270 | INFO0_CUST_AUTH_W12_O = 0x000018b0 271 | INFO0_CUST_AUTH_W13_O = 0x000018b4 272 | INFO0_CUST_AUTH_W14_O = 0x000018b8 273 | INFO0_CUST_AUTH_W15_O = 0x000018bc 274 | INFO0_CUST_AUTH_W16_O = 0x000018c0 275 | INFO0_CUST_AUTH_W17_O = 0x000018c4 276 | INFO0_CUST_AUTH_W18_O = 0x000018c8 277 | INFO0_CUST_AUTH_W19_O = 0x000018cc 278 | INFO0_CUST_AUTH_W20_O = 0x000018d0 279 | INFO0_CUST_AUTH_W21_O = 0x000018d4 280 | INFO0_CUST_AUTH_W22_O = 0x000018d8 281 | INFO0_CUST_AUTH_W23_O = 0x000018dc 282 | INFO0_CUST_AUTH_W24_O = 0x000018e0 283 | INFO0_CUST_AUTH_W25_O = 0x000018e4 284 | INFO0_CUST_AUTH_W26_O = 0x000018e8 285 | INFO0_CUST_AUTH_W27_O = 0x000018ec 286 | INFO0_CUST_AUTH_W28_O = 0x000018f0 287 | INFO0_CUST_AUTH_W29_O = 0x000018f4 288 | INFO0_CUST_AUTH_W30_O = 0x000018f8 289 | INFO0_CUST_AUTH_W31_O = 0x000018fc 290 | INFO0_CUST_PUBKEY_W0_O = 0x00001900 291 | INFO0_CUST_PUBKEY_W1_O = 0x00001904 292 | INFO0_CUST_PUBKEY_W2_O = 0x00001908 293 | INFO0_CUST_PUBKEY_W3_O = 0x0000190c 294 | INFO0_CUST_PUBKEY_W4_O = 0x00001910 295 | INFO0_CUST_PUBKEY_W5_O = 0x00001914 296 | INFO0_CUST_PUBKEY_W6_O = 0x00001918 297 | INFO0_CUST_PUBKEY_W7_O = 0x0000191c 298 | INFO0_CUST_PUBKEY_W8_O = 0x00001920 299 | INFO0_CUST_PUBKEY_W9_O = 0x00001924 300 | INFO0_CUST_PUBKEY_W10_O = 0x00001928 301 | INFO0_CUST_PUBKEY_W11_O = 0x0000192c 302 | INFO0_CUST_PUBKEY_W12_O = 0x00001930 303 | INFO0_CUST_PUBKEY_W13_O = 0x00001934 304 | INFO0_CUST_PUBKEY_W14_O = 0x00001938 305 | INFO0_CUST_PUBKEY_W15_O = 0x0000193c 306 | INFO0_CUST_PUBKEY_W16_O = 0x00001940 307 | INFO0_CUST_PUBKEY_W17_O = 0x00001944 308 | INFO0_CUST_PUBKEY_W18_O = 0x00001948 309 | INFO0_CUST_PUBKEY_W19_O = 0x0000194c 310 | INFO0_CUST_PUBKEY_W20_O = 0x00001950 311 | INFO0_CUST_PUBKEY_W21_O = 0x00001954 312 | INFO0_CUST_PUBKEY_W22_O = 0x00001958 313 | INFO0_CUST_PUBKEY_W23_O = 0x0000195c 314 | INFO0_CUST_PUBKEY_W24_O = 0x00001960 315 | INFO0_CUST_PUBKEY_W25_O = 0x00001964 316 | INFO0_CUST_PUBKEY_W26_O = 0x00001968 317 | INFO0_CUST_PUBKEY_W27_O = 0x0000196c 318 | INFO0_CUST_PUBKEY_W28_O = 0x00001970 319 | INFO0_CUST_PUBKEY_W29_O = 0x00001974 320 | INFO0_CUST_PUBKEY_W30_O = 0x00001978 321 | INFO0_CUST_PUBKEY_W31_O = 0x0000197c 322 | INFO0_CUST_PUBKEY_W32_O = 0x00001980 323 | INFO0_CUST_PUBKEY_W33_O = 0x00001984 324 | INFO0_CUST_PUBKEY_W34_O = 0x00001988 325 | INFO0_CUST_PUBKEY_W35_O = 0x0000198c 326 | INFO0_CUST_PUBKEY_W36_O = 0x00001990 327 | INFO0_CUST_PUBKEY_W37_O = 0x00001994 328 | INFO0_CUST_PUBKEY_W38_O = 0x00001998 329 | INFO0_CUST_PUBKEY_W39_O = 0x0000199c 330 | INFO0_CUST_PUBKEY_W40_O = 0x000019a0 331 | INFO0_CUST_PUBKEY_W41_O = 0x000019a4 332 | INFO0_CUST_PUBKEY_W42_O = 0x000019a8 333 | INFO0_CUST_PUBKEY_W43_O = 0x000019ac 334 | INFO0_CUST_PUBKEY_W44_O = 0x000019b0 335 | INFO0_CUST_PUBKEY_W45_O = 0x000019b4 336 | INFO0_CUST_PUBKEY_W46_O = 0x000019b8 337 | INFO0_CUST_PUBKEY_W47_O = 0x000019bc 338 | INFO0_CUST_PUBKEY_W48_O = 0x000019c0 339 | INFO0_CUST_PUBKEY_W49_O = 0x000019c4 340 | INFO0_CUST_PUBKEY_W50_O = 0x000019c8 341 | INFO0_CUST_PUBKEY_W51_O = 0x000019cc 342 | INFO0_CUST_PUBKEY_W52_O = 0x000019d0 343 | INFO0_CUST_PUBKEY_W53_O = 0x000019d4 344 | INFO0_CUST_PUBKEY_W54_O = 0x000019d8 345 | INFO0_CUST_PUBKEY_W55_O = 0x000019dc 346 | INFO0_CUST_PUBKEY_W56_O = 0x000019e0 347 | INFO0_CUST_PUBKEY_W57_O = 0x000019e4 348 | INFO0_CUST_PUBKEY_W58_O = 0x000019e8 349 | INFO0_CUST_PUBKEY_W59_O = 0x000019ec 350 | INFO0_CUST_PUBKEY_W60_O = 0x000019f0 351 | INFO0_CUST_PUBKEY_W61_O = 0x000019f4 352 | INFO0_CUST_PUBKEY_W62_O = 0x000019f8 353 | INFO0_CUST_PUBKEY_W63_O = 0x000019fc 354 | INFO0_CUSTOMER_KEY0_O = 0x00001a00 355 | INFO0_CUSTOMER_KEY1_O = 0x00001a04 356 | INFO0_CUSTOMER_KEY2_O = 0x00001a08 357 | INFO0_CUSTOMER_KEY3_O = 0x00001a0c 358 | INFO0_CUST_PUBHASH_W0_O = 0x00001a10 359 | INFO0_CUST_PUBHASH_W1_O = 0x00001a14 360 | INFO0_CUST_PUBHASH_W2_O = 0x00001a18 361 | INFO0_CUST_PUBHASH_W3_O = 0x00001a1c 362 | 363 | 364 | #****************************************************************************** 365 | # 366 | # CRC using ethernet poly, as used by Corvette hardware for validation 367 | # 368 | #****************************************************************************** 369 | def crc32(L): 370 | return (binascii.crc32(L) & 0xFFFFFFFF) 371 | 372 | #****************************************************************************** 373 | # 374 | # Pad the text to the block_size. bZeroPad determines how to handle text which 375 | # is already multiple of block_size 376 | # 377 | #****************************************************************************** 378 | def pad_to_block_size(text, block_size, bZeroPad): 379 | text_length = len(text) 380 | amount_to_pad = block_size - (text_length % block_size) 381 | if (amount_to_pad == block_size): 382 | if (bZeroPad == 0): 383 | amount_to_pad = 0 384 | for i in range(0, amount_to_pad, 1): 385 | text += bytes(chr(amount_to_pad), 'ascii') 386 | return text 387 | 388 | 389 | #****************************************************************************** 390 | # 391 | # AES CBC encryption 392 | # 393 | #****************************************************************************** 394 | def encrypt_app_aes(cleartext, encKey, iv): 395 | key = array.array('B', encKey).tostring() 396 | ivVal = array.array('B', iv).tostring() 397 | plaintext = array.array('B', cleartext).tostring() 398 | 399 | encryption_suite = AES.new(key, AES.MODE_CBC, ivVal) 400 | cipher_text = encryption_suite.encrypt(plaintext) 401 | 402 | return cipher_text 403 | 404 | #****************************************************************************** 405 | # 406 | # AES 128 CBC encryption 407 | # 408 | #****************************************************************************** 409 | def encrypt_app_aes128(cleartext, encKey, iv): 410 | key = array.array('B', encKey).tostring() 411 | ivVal = array.array('B', iv).tostring() 412 | plaintext = array.array('B', cleartext).tostring() 413 | 414 | encryption_suite = AES.new(key, AES.MODE_CBC, ivVal) 415 | cipher_text = encryption_suite.encrypt(plaintext) 416 | 417 | return cipher_text 418 | 419 | #****************************************************************************** 420 | # 421 | # SHA256 HMAC 422 | # 423 | #****************************************************************************** 424 | def compute_hmac(key, data): 425 | sig = hmac.new(array.array('B', key).tostring(), array.array('B', data).tostring(), hashlib.sha256).digest() 426 | return sig 427 | 428 | #****************************************************************************** 429 | # 430 | # RSA PKCS1_v1_5 sign 431 | # 432 | #****************************************************************************** 433 | def compute_rsa_sign(prvKeyFile, data): 434 | key = open(prvKeyFile, "r").read() 435 | rsakey = RSA.importKey(key) 436 | signer = PKCS1_v1_5.new(rsakey) 437 | digest = SHA256.new() 438 | digest.update(bytes(data)) 439 | sign = signer.sign(digest) 440 | return sign 441 | 442 | #****************************************************************************** 443 | # 444 | # RSA PKCS1_v1_5 sign verification 445 | # 446 | #****************************************************************************** 447 | def verify_rsa_sign(pubKeyFile, data, sign): 448 | key = open(pubKeyFile, "r").read() 449 | rsakey = RSA.importKey(key) 450 | #print(hex(rsakey.n)) 451 | verifier = PKCS1_v1_5.new(rsakey) 452 | digest = SHA256.new() 453 | digest.update(bytes(data)) 454 | return verifier.verify(digest, sign) 455 | 456 | #****************************************************************************** 457 | # 458 | # Fill one word in bytearray 459 | # 460 | #****************************************************************************** 461 | def fill_word(barray, offset, w): 462 | barray[offset + 0] = (w >> 0) & 0x000000ff; 463 | barray[offset + 1] = (w >> 8) & 0x000000ff; 464 | barray[offset + 2] = (w >> 16) & 0x000000ff; 465 | barray[offset + 3] = (w >> 24) & 0x000000ff; 466 | 467 | 468 | #****************************************************************************** 469 | # 470 | # Turn a 32-bit number into a series of bytes for transmission. 471 | # 472 | # This command will split a 32-bit integer into an array of bytes, ordered 473 | # LSB-first for transmission over the UART. 474 | # 475 | #****************************************************************************** 476 | def int_to_bytes(n): 477 | A = [n & 0xFF, 478 | (n >> 8) & 0xFF, 479 | (n >> 16) & 0xFF, 480 | (n >> 24) & 0xFF] 481 | 482 | return A 483 | 484 | #****************************************************************************** 485 | # 486 | # Extract a word from a byte array 487 | # 488 | #****************************************************************************** 489 | def word_from_bytes(B, n): 490 | return (B[n] + (B[n + 1] << 8) + (B[n + 2] << 16) + (B[n + 3] << 24)) 491 | 492 | 493 | #****************************************************************************** 494 | # 495 | # automatically figure out the integer format (base 10 or 16) 496 | # 497 | #****************************************************************************** 498 | def auto_int(x): 499 | return int(x, 0) 500 | 501 | #****************************************************************************** 502 | # 503 | # User controllable Prints control 504 | # 505 | #****************************************************************************** 506 | # Defined print levels 507 | AM_PRINT_LEVEL_MIN = 0 508 | AM_PRINT_LEVEL_NONE = AM_PRINT_LEVEL_MIN 509 | AM_PRINT_LEVEL_ERROR = 1 510 | AM_PRINT_LEVEL_INFO = 2 511 | AM_PRINT_LEVEL_VERBOSE = 4 512 | AM_PRINT_LEVEL_DEBUG = 5 513 | AM_PRINT_LEVEL_MAX = AM_PRINT_LEVEL_DEBUG 514 | 515 | # Global variable to control the prints 516 | AM_PRINT_VERBOSITY = AM_PRINT_LEVEL_INFO 517 | 518 | helpPrintLevel = 'Set Log Level (0: None), (1: Error), (2: INFO), (4: Verbose), (5: Debug) [Default = Info]' 519 | 520 | def am_set_print_level(level): 521 | global AM_PRINT_VERBOSITY 522 | AM_PRINT_VERBOSITY = level 523 | 524 | def am_print(*args, level=AM_PRINT_LEVEL_INFO, **kwargs): 525 | global AM_PRINT_VERBOSITY 526 | if (AM_PRINT_VERBOSITY >= level): 527 | print(*args, **kwargs) 528 | -------------------------------------------------------------------------------- /artemis_uploader/asb/asb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Combination of the three steps to take an 'application.bin' file and run it on a SparkFun Artemis module 3 | 4 | # Information: 5 | # This script performs the three main tasks: 6 | # 1. Convert 'application.bin' to an OTA update blob 7 | # 2. Convert the OTA blob into a wired update blob 8 | # 3. Push the wired update blob into the Artemis module 9 | 10 | import argparse 11 | import sys 12 | from Crypto.Cipher import AES 13 | import array 14 | import hashlib 15 | import hmac 16 | import os 17 | import binascii 18 | import serial 19 | import serial.tools.list_ports as list_ports 20 | import time 21 | # from sf_am_defines import * 22 | from sys import exit 23 | 24 | from .am_defines import * 25 | from .keys_info import keyTblAes, keyTblHmac, minAesKeyIdx, maxAesKeyIdx, minHmacKeyIdx, maxHmacKeyIdx, INFO_KEY, FLASH_KEY 26 | 27 | #****************************************************************************** 28 | # 29 | # Global Variables 30 | # 31 | #****************************************************************************** 32 | loadTries = 0 #If we fail, try again. Tracks the number of tries we've attempted 33 | loadSuccess = False 34 | blob2wiredfile = '' 35 | uploadbinfile = '' 36 | 37 | 38 | #****************************************************************************** 39 | # 40 | # Generate the image blob as per command line parameters 41 | # 42 | #****************************************************************************** 43 | def bin2blob_process(loadaddress, appFile, magicNum, crcI, crcB, authI, authB, protection, authKeyIdx, output, encKeyIdx, version, erasePrev, child0, child1, authalgo, encalgo): 44 | 45 | global blob2wiredfile 46 | 47 | app_binarray = bytearray() 48 | # Open the file, and read it into an array of integers. 49 | with appFile as f_app: 50 | app_binarray.extend(f_app.read()) 51 | f_app.close() 52 | 53 | encVal = 0 54 | if (encalgo != 0): 55 | encVal = 1 56 | if ((encKeyIdx < minAesKeyIdx) or (encKeyIdx > maxAesKeyIdx)): 57 | am_print("Invalid encKey Idx ", encKeyIdx, level=AM_PRINT_LEVEL_ERROR); 58 | return 59 | if (encalgo == 2): 60 | if (encKeyIdx & 0x1): 61 | am_print("Invalid encKey Idx ", encKeyIdx, level=AM_PRINT_LEVEL_ERROR); 62 | return 63 | keySize = 32 64 | else: 65 | keySize = 16 66 | if (authalgo != 0): 67 | if ((authKeyIdx < minHmacKeyIdx) or (authKeyIdx > maxHmacKeyIdx) or (authKeyIdx & 0x1)): 68 | am_print("Invalid authKey Idx ", authKeyIdx, level=AM_PRINT_LEVEL_ERROR); 69 | return 70 | 71 | if (magicNum == AM_IMAGE_MAGIC_MAIN): 72 | hdr_length = AM_IMAGEHDR_SIZE_MAIN; #fixed header length 73 | elif ((magicNum == AM_IMAGE_MAGIC_CHILD) or (magicNum == AM_IMAGE_MAGIC_CUSTPATCH) or (magicNum == AM_IMAGE_MAGIC_NONSECURE) or (magicNum == AM_IMAGE_MAGIC_INFO0)): 74 | hdr_length = AM_IMAGEHDR_SIZE_AUX; #fixed header length 75 | else: 76 | am_print("magic number", hex(magicNum), " not supported", level=AM_PRINT_LEVEL_ERROR) 77 | return 78 | am_print("Header Size = ", hex(hdr_length)) 79 | 80 | #generate mutable byte array for the header 81 | hdr_binarray = bytearray([0x00]*hdr_length); 82 | 83 | orig_app_length = (len(app_binarray)) 84 | am_print("original app_size ",hex(orig_app_length), "(",orig_app_length,")") 85 | 86 | am_print("load_address ",hex(loadaddress), "(",loadaddress,")") 87 | if (loadaddress & 0x3): 88 | am_print("load address needs to be word aligned", level=AM_PRINT_LEVEL_ERROR) 89 | return 90 | 91 | if (magicNum == AM_IMAGE_MAGIC_INFO0): 92 | if (orig_app_length & 0x3): 93 | am_print("INFO0 blob length needs to be multiple of 4", level=AM_PRINT_LEVEL_ERROR) 94 | return 95 | if ((loadaddress + orig_app_length) > INFO_SIZE_BYTES): 96 | am_print("INFO0 Offset and length exceed size", level=AM_PRINT_LEVEL_ERROR) 97 | return 98 | 99 | if (encVal == 1): 100 | block_size = AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES 101 | app_binarray = pad_to_block_size(app_binarray, block_size, 1) 102 | else: 103 | # Add Padding 104 | app_binarray = pad_to_block_size(app_binarray, 4, 0) 105 | 106 | app_length = (len(app_binarray)) 107 | am_print("app_size ",hex(app_length), "(",app_length,")") 108 | 109 | # Create Image blobs 110 | 111 | # w0 112 | blobLen = hdr_length + app_length 113 | w0 = (magicNum << 24) | ((encVal & 0x1) << 23) | blobLen 114 | 115 | am_print("w0 =", hex(w0)) 116 | fill_word(hdr_binarray, 0, w0) 117 | 118 | # w2 119 | securityVal = ((authI << 1) | crcI) << 4 | (authB << 1) | crcB 120 | am_print("Security Value ", hex(securityVal)) 121 | w2 = ((securityVal << 24) & 0xff000000) | ((authalgo) & 0xf) | ((authKeyIdx << 4) & 0xf0) | ((encalgo << 8) & 0xf00) | ((encKeyIdx << 12) & 0xf000) 122 | fill_word(hdr_binarray, 8, w2) 123 | am_print("w2 = ",hex(w2)) 124 | 125 | 126 | if (magicNum == AM_IMAGE_MAGIC_INFO0): 127 | # Insert the INFO0 size and offset 128 | addrWord = ((orig_app_length>>2) << 16) | ((loadaddress>>2) & 0xFFFF) 129 | versionKeyWord = INFO_KEY 130 | else: 131 | # Insert the application binary load address. 132 | addrWord = loadaddress | (protection & 0x3) 133 | # Initialize versionKeyWord 134 | versionKeyWord = (version & 0x7FFF) | ((erasePrev & 0x1) << 15) 135 | 136 | am_print("addrWord = ",hex(addrWord)) 137 | fill_word(hdr_binarray, AM_IMAGEHDR_OFFSET_ADDR, addrWord) 138 | 139 | am_print("versionKeyWord = ",hex(versionKeyWord)) 140 | fill_word(hdr_binarray, AM_IMAGEHDR_OFFSET_VERKEY, versionKeyWord) 141 | 142 | # Initialize child (Child Ptr/ Feature key) 143 | am_print("child0/feature = ",hex(child0)) 144 | fill_word(hdr_binarray, AM_IMAGEHDR_OFFSET_CHILDPTR, child0) 145 | am_print("child1 = ",hex(child1)) 146 | fill_word(hdr_binarray, AM_IMAGEHDR_OFFSET_CHILDPTR + 4, child1) 147 | 148 | authKeyIdx = authKeyIdx - minHmacKeyIdx 149 | if (authB != 0): # Authentication needed 150 | am_print("Boot Authentication Enabled") 151 | # am_print("Key used for HMAC") 152 | # am_print([hex(keyTblHmac[authKeyIdx*AM_SECBOOT_KEYIDX_BYTES + n]) for n in range (0, AM_HMAC_SIG_SIZE)]) 153 | # Initialize the clear image HMAC 154 | sigClr = compute_hmac(keyTblHmac[authKeyIdx*AM_SECBOOT_KEYIDX_BYTES:(authKeyIdx*AM_SECBOOT_KEYIDX_BYTES+AM_HMAC_SIG_SIZE)], (hdr_binarray[AM_IMAGEHDR_START_HMAC:hdr_length] + app_binarray)) 155 | am_print("HMAC Clear") 156 | am_print([hex(n) for n in sigClr]) 157 | # Fill up the HMAC 158 | for x in range(0, AM_HMAC_SIG_SIZE): 159 | hdr_binarray[AM_IMAGEHDR_OFFSET_SIGCLR + x] = sigClr[x] 160 | 161 | # All the header fields part of the encryption are now final 162 | if (encVal == 1): 163 | am_print("Encryption Enabled") 164 | encKeyIdx = encKeyIdx - minAesKeyIdx 165 | ivValAes = os.urandom(AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES) 166 | am_print("Initialization Vector") 167 | am_print([hex(ivValAes[n]) for n in range (0, AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES)]) 168 | keyAes = os.urandom(keySize) 169 | am_print("AES Key used for encryption") 170 | am_print([hex(keyAes[n]) for n in range (0, keySize)]) 171 | # Encrypted Part 172 | am_print("Encrypting blob of size " , (hdr_length - AM_IMAGEHDR_START_ENCRYPT + app_length)) 173 | enc_binarray = encrypt_app_aes((hdr_binarray[AM_IMAGEHDR_START_ENCRYPT:hdr_length] + app_binarray), keyAes, ivValAes) 174 | # am_print("Key used for encrypting AES Key") 175 | # am_print([hex(keyTblAes[encKeyIdx*keySize + n]) for n in range (0, keySize)]) 176 | # Encrypted Key 177 | enc_key = encrypt_app_aes(keyAes, keyTblAes[encKeyIdx*keySize:encKeyIdx*keySize + keySize], ivVal0) 178 | am_print("Encrypted Key") 179 | am_print([hex(enc_key[n]) for n in range (0, keySize)]) 180 | # Fill up the IV 181 | for x in range(0, AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES): 182 | hdr_binarray[AM_IMAGEHDR_OFFSET_IV + x] = ivValAes[x] 183 | # Fill up the Encrypted Key 184 | for x in range(0, keySize): 185 | hdr_binarray[AM_IMAGEHDR_OFFSET_KEK + x] = enc_key[x] 186 | else: 187 | enc_binarray = hdr_binarray[AM_IMAGEHDR_START_ENCRYPT:hdr_length] + app_binarray 188 | 189 | 190 | if (authI != 0): # Install Authentication needed 191 | am_print("Install Authentication Enabled") 192 | # am_print("Key used for HMAC") 193 | # am_print([hex(keyTblHmac[authKeyIdx*AM_SECBOOT_KEYIDX_BYTES + n]) for n in range (0, AM_HMAC_SIG_SIZE)]) 194 | # Initialize the top level HMAC 195 | sig = compute_hmac(keyTblHmac[authKeyIdx*AM_SECBOOT_KEYIDX_BYTES:(authKeyIdx*AM_SECBOOT_KEYIDX_BYTES+AM_HMAC_SIG_SIZE)], (hdr_binarray[AM_IMAGEHDR_START_HMAC_INST:AM_IMAGEHDR_START_ENCRYPT] + enc_binarray)) 196 | am_print("Generated Signature") 197 | am_print([hex(n) for n in sig]) 198 | # Fill up the HMAC 199 | for x in range(0, AM_HMAC_SIG_SIZE): 200 | hdr_binarray[AM_IMAGEHDR_OFFSET_SIG + x] = sig[x] 201 | # compute the CRC for the blob - this is done on a clear image 202 | crc = crc32(hdr_binarray[AM_IMAGEHDR_START_CRC:hdr_length] + app_binarray) 203 | am_print("crc = ",hex(crc)); 204 | w1 = crc 205 | fill_word(hdr_binarray, AM_IMAGEHDR_OFFSET_CRC, w1) 206 | 207 | # now output all three binary arrays in the proper order 208 | output = output + '_OTA_blob.bin' 209 | blob2wiredfile = output # save the output of bin2blob for use by blob2wired 210 | am_print("Writing to file ", output) 211 | with open(output, mode = 'wb') as out: 212 | out.write(hdr_binarray[0:AM_IMAGEHDR_START_ENCRYPT]) 213 | out.write(enc_binarray) 214 | 215 | 216 | #****************************************************************************** 217 | # 218 | # Generate the image blob as per command line parameters 219 | # 220 | #****************************************************************************** 221 | def blob2wired_process(appFile, imagetype, loadaddress, authalgo, encalgo, authKeyIdx, encKeyIdx, optionsVal, maxSize, output): 222 | global uploadbinfile 223 | 224 | app_binarray = bytearray() 225 | # Open the file, and read it into an array of integers. 226 | print('testing: ' + appFile ) 227 | with open(appFile,'rb') as f_app: 228 | app_binarray.extend(f_app.read()) 229 | f_app.close() 230 | 231 | # Make sure it is page multiple 232 | if ((maxSize & (FLASH_PAGE_SIZE - 1)) != 0): 233 | am_print ("split needs to be multiple of flash page size", level=AM_PRINT_LEVEL_ERROR) 234 | return 235 | 236 | if (encalgo != 0): 237 | if ((encKeyIdx < minAesKeyIdx) or (encKeyIdx > maxAesKeyIdx)): 238 | am_print("Invalid encKey Idx ", encKeyIdx, level=AM_PRINT_LEVEL_ERROR) 239 | return 240 | if (encalgo == 2): 241 | if (encKeyIdx & 0x1): 242 | am_print("Invalid encKey Idx ", encKeyIdx, level=AM_PRINT_LEVEL_ERROR); 243 | return 244 | keySize = 32 245 | else: 246 | keySize = 16 247 | if (authalgo != 0): 248 | if ((authKeyIdx < minHmacKeyIdx) or (authKeyIdx > maxHmacKeyIdx) or (authKeyIdx & 0x1)): 249 | am_print("Invalid authKey Idx ", authKeyIdx, level=AM_PRINT_LEVEL_ERROR); 250 | return 251 | 252 | hdr_length = AM_WU_IMAGEHDR_SIZE; #fixed header length 253 | am_print("Header Size = ", hex(hdr_length)) 254 | 255 | orig_app_length = (len(app_binarray)) 256 | 257 | if (encalgo != 0): 258 | block_size = keySize 259 | app_binarray = pad_to_block_size(app_binarray, block_size, 1) 260 | else: 261 | # Add Padding 262 | app_binarray = pad_to_block_size(app_binarray, 4, 0) 263 | 264 | app_length = (len(app_binarray)) 265 | am_print("app_size ",hex(app_length), "(",app_length,")") 266 | 267 | if (app_length + hdr_length > maxSize): 268 | am_print("Image size bigger than max - Creating Split image") 269 | 270 | start = 0 271 | # now output all three binary arrays in the proper order 272 | output = output + '_Wired_OTA_blob.bin' 273 | uploadbinfile = output; # save the name of the output from blob2wired 274 | out = open(output, mode = 'wb') 275 | 276 | while (start < app_length): 277 | #generate mutable byte array for the header 278 | hdr_binarray = bytearray([0x00]*hdr_length); 279 | 280 | if (app_length - start > maxSize): 281 | end = start + maxSize 282 | else: 283 | end = app_length 284 | 285 | if (imagetype == AM_SECBOOT_WIRED_IMAGETYPE_INFO0_NOOTA): 286 | key = INFO_KEY 287 | # word offset 288 | fill_word(hdr_binarray, AM_WU_IMAGEHDR_OFFSET_ADDR, loadaddress>>2) 289 | else: 290 | key = FLASH_KEY 291 | # load address 292 | fill_word(hdr_binarray, AM_WU_IMAGEHDR_OFFSET_ADDR, loadaddress) 293 | # Create imageType & options 294 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_IMAGETYPE] = imagetype 295 | # Set the options only for the first block 296 | if (start == 0): 297 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_OPTIONS] = optionsVal 298 | else: 299 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_OPTIONS] = 0 300 | 301 | # Create Info0 Update Blob for wired update 302 | fill_word(hdr_binarray, AM_WU_IMAGEHDR_OFFSET_KEY, key) 303 | # update size 304 | fill_word(hdr_binarray, AM_WU_IMAGEHDR_OFFSET_SIZE, end-start) 305 | 306 | w0 = ((authalgo & 0xf) | ((authKeyIdx << 8) & 0xf00) | ((encalgo << 16) & 0xf0000) | ((encKeyIdx << 24) & 0x0f000000)) 307 | 308 | fill_word(hdr_binarray, 0, w0) 309 | 310 | if (encalgo != 0): 311 | keyIdx = encKeyIdx - minAesKeyIdx 312 | ivValAes = os.urandom(AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES) 313 | am_print("Initialization Vector") 314 | am_print([hex(n) for n in ivValAes]) 315 | keyAes = os.urandom(keySize) 316 | am_print("AES Key used for encryption") 317 | am_print([hex(keyAes[n]) for n in range (0, keySize)]) 318 | # Encrypted Part - after security header 319 | enc_binarray = encrypt_app_aes((hdr_binarray[AM_WU_IMAGEHDR_START_ENCRYPT:hdr_length] + app_binarray[start:end]), keyAes, ivValAes) 320 | # am_print("Key used for encrypting AES Key") 321 | # am_print([hex(keyTblAes[keyIdx*AM_SECBOOT_KEYIDX_BYTES + n]) for n in range (0, keySize)]) 322 | # Encrypted Key 323 | enc_key = encrypt_app_aes(keyAes, keyTblAes[keyIdx*AM_SECBOOT_KEYIDX_BYTES:(keyIdx*AM_SECBOOT_KEYIDX_BYTES + keySize)], ivVal0) 324 | am_print("Encrypted Key") 325 | am_print([hex(enc_key[n]) for n in range (0, keySize)]) 326 | # Fill up the IV 327 | for x in range(0, AM_SECBOOT_AESCBC_BLOCK_SIZE_BYTES): 328 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_IV + x] = ivValAes[x] 329 | # Fill up the Encrypted Key 330 | for x in range(0, keySize): 331 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_KEK + x] = enc_key[x] 332 | else: 333 | enc_binarray = hdr_binarray[AM_WU_IMAGEHDR_START_ENCRYPT:hdr_length] + app_binarray[start:end] 334 | 335 | 336 | if (authalgo != 0): # Authentication needed 337 | keyIdx = authKeyIdx - minHmacKeyIdx 338 | # am_print("Key used for HMAC") 339 | # am_print([hex(keyTblHmac[keyIdx*AM_SECBOOT_KEYIDX_BYTES + n]) for n in range (0, AM_HMAC_SIG_SIZE)]) 340 | # Initialize the HMAC - Sign is computed on image following the signature 341 | sig = compute_hmac(keyTblHmac[keyIdx*AM_SECBOOT_KEYIDX_BYTES:(keyIdx*AM_SECBOOT_KEYIDX_BYTES+AM_HMAC_SIG_SIZE)], hdr_binarray[AM_WU_IMAGEHDR_START_HMAC:AM_WU_IMAGEHDR_START_ENCRYPT] + enc_binarray) 342 | am_print("HMAC") 343 | am_print([hex(n) for n in sig]) 344 | # Fill up the HMAC 345 | for x in range(0, AM_HMAC_SIG_SIZE): 346 | hdr_binarray[AM_WU_IMAGEHDR_OFFSET_SIG + x] = sig[x] 347 | 348 | am_print("Writing to file ", output) 349 | am_print("Image from ", str(hex(start)), " to ", str(hex(end)), " will be loaded at", str(hex(loadaddress))) 350 | out.write(hdr_binarray[0:AM_WU_IMAGEHDR_START_ENCRYPT]) 351 | out.write(enc_binarray) 352 | 353 | # Reset start for next chunk 354 | start = end 355 | loadaddress = loadaddress + maxSize 356 | 357 | 358 | #****************************************************************************** 359 | # 360 | # Main function 361 | # 362 | #****************************************************************************** 363 | def upload(args, verboseprint): 364 | 365 | global loadTries 366 | global loadSuccess 367 | 368 | # Open a serial port, and communicate with Device 369 | # 370 | # Max flashing time depends on the amount of SRAM available. 371 | # For very large images, the flashing happens page by page. 372 | # However if the image can fit in the free SRAM, it could take a long time 373 | # for the whole image to be flashed at the end. 374 | # The largest image which can be stored depends on the max SRAM. 375 | # Assuming worst case ~100 ms/page of flashing time, and allowing for the 376 | # image to be close to occupying full SRAM (256K) which is 128 pages. 377 | 378 | connection_timeout = 5 379 | 380 | print('Connecting over serial port {}...'.format(args.port), flush=True) 381 | print('Requested baud rate:',args.baud) 382 | 383 | useBaud = int(args.baud) 384 | if (useBaud > 115200): 385 | print('Limiting baud rate to 115200') 386 | useBaud = 115200 387 | useBaud = str(useBaud) 388 | 389 | #Check to see if the com port is available 390 | try: 391 | with serial.Serial(args.port, useBaud, timeout=connection_timeout) as ser: 392 | pass 393 | except: 394 | 395 | #Show a list of com ports and recommend one 396 | print("Detected Serial Ports:") 397 | devices = list_ports.comports() 398 | port = None 399 | for dev in devices: 400 | print(dev.description) 401 | # The SparkFun BlackBoard has CH340 in the description 402 | if 'CH340' in dev.description: 403 | print("The port you selected was not found. But we did detect a CH340 on " + dev.device + " so you might try again on that port.") 404 | break 405 | elif 'FTDI' in dev.description: 406 | print("The port you selected was not found. But we did detect an FTDI on " + dev.device + " so you might try again on that port.") 407 | break 408 | elif 'USB Serial Device' in dev.description: 409 | print("The port you selected was not found. But we did detect a USB Serial Device on " + dev.device + " so you might try again on that port.") 410 | break 411 | else: 412 | print("Com Port not found - Did you select the right one?") 413 | 414 | exit() 415 | 416 | #Begin talking over com port 417 | 418 | #The auto-bootload sequence is good but not fullproof. The bootloader 419 | #fails to correctly catch the BOOT signal about 1 out of ten times. 420 | #Auto-retry this number of times before we give up. 421 | 422 | # Instantiate ser here and set dtr and rts before opening the port 423 | ser = serial.Serial() 424 | ser.port = args.port 425 | ser.baudrate = useBaud 426 | ser.timeout = connection_timeout 427 | 428 | loadTries = 0 429 | 430 | while loadTries < 3: 431 | loadSuccess = False 432 | 433 | # Set dtr and rts before opening the port 434 | ser.dtr=True 435 | ser.rts=True 436 | 437 | ser.open() 438 | 439 | # RTS behaves differently on macOS. Use a double-reset 440 | time.sleep(0.01) 441 | ser.dtr=False # Set RTS and DTR high 442 | ser.rts=False 443 | time.sleep(0.01) 444 | ser.dtr=True # Set RTS and DTR low 445 | ser.rts=True 446 | 447 | time.sleep(0.008) #3ms and 10ms work well. Not 50, and not 0. 448 | 449 | # Set RTS and DTR high 450 | # This causes BOOT to go high - and then decay back to zero 451 | ser.dtr=False 452 | ser.rts=False 453 | 454 | #Give bootloader a chance to run and check BOOT pin before communication begins. But must initiate com before bootloader timeout of 250ms. 455 | time.sleep(0.1) # 100ms works well 456 | 457 | ser.reset_input_buffer() # reset the input bufer to discard any UART traffic that the device may have generated 458 | 459 | connect_device(ser, args, verboseprint) 460 | 461 | loadTries = loadTries + 1 462 | 463 | if(loadSuccess == True): 464 | ser.close() 465 | print("Tries =", loadTries) 466 | print('Upload complete') 467 | exit() 468 | else: 469 | print("Fail") 470 | 471 | ser.close() 472 | 473 | print("Tries =", loadTries) 474 | print("Upload failed") 475 | exit() 476 | 477 | 478 | #****************************************************************************** 479 | # 480 | # Communicate with Device 481 | # 482 | # Given a serial port, connects to the target device using the 483 | # UART. 484 | # 485 | #****************************************************************************** 486 | def connect_device(ser, args, verboseprint): 487 | 488 | global loadSuccess 489 | 490 | # Send Hello 491 | #generate mutable byte array for the header 492 | hello = bytearray([0x00]*4) 493 | fill_word(hello, 0, ((8 << 16) | AM_SECBOOT_WIRED_MSGTYPE_HELLO)) 494 | verboseprint('Sending Hello.') 495 | response, success = send_command(hello, 88, ser, verboseprint) 496 | 497 | #Check if response failed 498 | if success == False: 499 | verboseprint("Failed to respond") 500 | return 501 | 502 | verboseprint("Received response for Hello") 503 | word = word_from_bytes(response, 4) 504 | if ((word & 0xFFFF) == AM_SECBOOT_WIRED_MSGTYPE_STATUS): 505 | # Received Status 506 | print("Bootloader connected") 507 | 508 | verboseprint("Received Status") 509 | verboseprint("length = ", hex((word >> 16))) 510 | verboseprint("version = ", hex(word_from_bytes(response, 8))) 511 | verboseprint("Max Storage = ", hex(word_from_bytes(response, 12))) 512 | verboseprint("Status = ", hex(word_from_bytes(response, 16))) 513 | verboseprint("State = ", hex(word_from_bytes(response, 20))) 514 | verboseprint("AMInfo = ") 515 | for x in range(24, 88, 4): 516 | verboseprint(hex(word_from_bytes(response, x))) 517 | 518 | abort = args.abort 519 | if (abort != -1): 520 | # Send OTA Desc 521 | verboseprint('Sending Abort command.') 522 | abortMsg = bytearray([0x00]*8); 523 | fill_word(abortMsg, 0, ((12 << 16) | AM_SECBOOT_WIRED_MSGTYPE_ABORT)) 524 | fill_word(abortMsg, 4, abort) 525 | response, success = send_ackd_command(abortMsg, ser, verboseprint) 526 | if success == False: 527 | verboseprint("Failed to ack command") 528 | return 529 | 530 | 531 | otadescaddr = args.otadesc 532 | if (otadescaddr != 0xFFFFFFFF): 533 | # Send OTA Desc 534 | verboseprint('Sending OTA Descriptor = ', hex(otadescaddr)) 535 | otaDesc = bytearray([0x00]*8); 536 | fill_word(otaDesc, 0, ((12 << 16) | AM_SECBOOT_WIRED_MSGTYPE_OTADESC)) 537 | fill_word(otaDesc, 4, otadescaddr) 538 | response, success = send_ackd_command(otaDesc, ser, verboseprint) 539 | if success == False: 540 | verboseprint("Failed to ack command") 541 | return 542 | 543 | 544 | imageType = args.imagetype 545 | if (uploadbinfile != ''): 546 | 547 | # Read the binary file from the command line. 548 | with open(uploadbinfile, mode='rb') as binfile: 549 | application = binfile.read() 550 | # Gather the important binary metadata. 551 | totalLen = len(application) 552 | # Send Update command 553 | verboseprint('Sending Update Command.') 554 | 555 | # It is assumed that maxSize is 256b multiple 556 | maxImageSize = args.split 557 | if ((maxImageSize & (FLASH_PAGE_SIZE - 1)) != 0): 558 | verboseprint ("split needs to be multiple of flash page size") 559 | return 560 | 561 | # Each Block of image consists of AM_WU_IMAGEHDR_SIZE Bytes Image header and the Image blob 562 | maxUpdateSize = AM_WU_IMAGEHDR_SIZE + maxImageSize 563 | numUpdates = (totalLen + maxUpdateSize - 1) // maxUpdateSize # Integer division 564 | verboseprint("number of updates needed = ", numUpdates) 565 | 566 | end = totalLen 567 | for numUpdates in range(numUpdates, 0 , -1): 568 | start = (numUpdates-1)*maxUpdateSize 569 | crc = crc32(application[start:end]) 570 | applen = end - start 571 | verboseprint("Sending block of size ", str(hex(applen)), " from ", str(hex(start)), " to ", str(hex(end))) 572 | end = end - applen 573 | 574 | update = bytearray([0x00]*16); 575 | fill_word(update, 0, ((20 << 16) | AM_SECBOOT_WIRED_MSGTYPE_UPDATE)) 576 | fill_word(update, 4, applen) 577 | fill_word(update, 8, crc) 578 | # Size = 0 => We're not piggybacking any data to IMAGE command 579 | fill_word(update, 12, 0) 580 | response, success = send_ackd_command(update, ser, verboseprint) 581 | if success == False: 582 | verboseprint("Failed to ack command") 583 | return 584 | 585 | # Loop over the bytes in the image, and send them to the target. 586 | resp = 0 587 | # Max chunk size is AM_MAX_UART_MSG_SIZE adjusted for the header for Data message 588 | maxChunkSize = AM_MAX_UART_MSG_SIZE - 12 589 | for x in range(0, applen, maxChunkSize): 590 | # Split the application into chunks of maxChunkSize bytes. 591 | # This is the max chunk size supported by the UART bootloader 592 | if ((x + maxChunkSize) > applen): 593 | chunk = application[start+x:start+applen] 594 | # print(str(hex(start+x)), " to ", str(hex(applen))) 595 | else: 596 | chunk = application[start+x:start+x+maxChunkSize] 597 | # print(str(hex(start+x)), " to ", str(hex(start + x + maxChunkSize))) 598 | 599 | chunklen = len(chunk) 600 | 601 | # Build a data packet with a "data command" a "length" and the actual 602 | # payload bytes, and send it to the target. 603 | dataMsg = bytearray([0x00]*8); 604 | fill_word(dataMsg, 0, (((chunklen + 12) << 16) | AM_SECBOOT_WIRED_MSGTYPE_DATA)) 605 | # seqNo 606 | fill_word(dataMsg, 4, x) 607 | 608 | verboseprint("Sending Data Packet of length ", chunklen) 609 | response, success = send_ackd_command(dataMsg + chunk, ser, verboseprint) 610 | if success == False: 611 | verboseprint("Failed to ack command") 612 | return 613 | 614 | if (args.raw != ''): 615 | 616 | # Read the binary file from the command line. 617 | with open(args.raw, mode='rb') as rawfile: 618 | blob = rawfile.read() 619 | # Send Raw command 620 | verboseprint('Sending Raw Command.') 621 | ser.write(blob) 622 | 623 | if (args.reset != 0): 624 | # Send reset 625 | verboseprint('Sending Reset Command.') 626 | resetmsg = bytearray([0x00]*8); 627 | fill_word(resetmsg, 0, ((12 << 16) | AM_SECBOOT_WIRED_MSGTYPE_RESET)) 628 | # options 629 | fill_word(resetmsg, 4, args.reset) 630 | 631 | response, success = send_ackd_command(resetmsg, ser, verboseprint) 632 | if success == False: 633 | verboseprint("Failed to ack command") 634 | return 635 | 636 | 637 | #Success! We're all done 638 | loadSuccess = True 639 | else: 640 | # Received Wrong message 641 | verboseprint("Received Unknown Message") 642 | word = word_from_bytes(response, 4) 643 | verboseprint("msgType = ", hex(word & 0xFFFF)) 644 | verboseprint("Length = ", hex(word >> 16)) 645 | verboseprint([hex(n) for n in response]) 646 | #print("!!!Wired Upgrade Unsuccessful!!!....Terminating the script") 647 | 648 | #exit() 649 | 650 | #****************************************************************************** 651 | # 652 | # Send ACK'd command 653 | # 654 | # Sends a command, and waits for an ACK. 655 | # 656 | #****************************************************************************** 657 | def send_ackd_command(command, ser, verboseprint): 658 | 659 | response, success = send_command(command, 20, ser, verboseprint) 660 | 661 | #Check if response failed 662 | if success == False: 663 | verboseprint("Response not valid") 664 | return False #Return error 665 | 666 | word = word_from_bytes(response, 4) 667 | if ((word & 0xFFFF) == AM_SECBOOT_WIRED_MSGTYPE_ACK): 668 | # Received ACK 669 | if (word_from_bytes(response, 12) != AM_SECBOOT_WIRED_ACK_STATUS_SUCCESS): 670 | verboseprint("Received NACK") 671 | verboseprint("msgType = ", hex(word_from_bytes(response, 8))) 672 | verboseprint("error = ", hex(word_from_bytes(response, 12))) 673 | verboseprint("seqNo = ", hex(word_from_bytes(response, 16))) 674 | #print("!!!Wired Upgrade Unsuccessful!!!....Terminating the script") 675 | verboseprint("Upload failed: No ack to command") 676 | 677 | return (b'', False) #Return error 678 | 679 | return (response, True) 680 | 681 | #****************************************************************************** 682 | # 683 | # Send command 684 | # 685 | # Sends a command, and waits for the response. 686 | # 687 | #****************************************************************************** 688 | def send_command(params, response_len, ser, verboseprint): 689 | 690 | # Compute crc 691 | crc = crc32(params) 692 | # print([hex(n) for n in int_to_bytes(crc)]) 693 | # print([hex(n) for n in params]) 694 | # send crc first 695 | ser.write(int_to_bytes(crc)) 696 | 697 | # Next, send the parameters. 698 | ser.write(params) 699 | 700 | response = '' 701 | response = ser.read(response_len) 702 | 703 | # Make sure we got the number of bytes we asked for. 704 | if len(response) != response_len: 705 | verboseprint('No response for command 0x{:08X}'.format(word_from_bytes(params, 0) & 0xFFFF)) 706 | n = len(response) 707 | if (n != 0): 708 | verboseprint("received bytes ", len(response)) 709 | verboseprint([hex(n) for n in response]) 710 | return (b'', False) 711 | 712 | return (response, True) 713 | 714 | #****************************************************************************** 715 | # 716 | # Send a command that uses an array of bytes as its parameters. 717 | # 718 | #****************************************************************************** 719 | def send_bytewise_command(command, params, response_len, ser): 720 | # Send the command first. 721 | ser.write(int_to_bytes(command)) 722 | 723 | # Next, send the parameters. 724 | ser.write(params) 725 | 726 | response = '' 727 | response = ser.read(response_len) 728 | 729 | # Make sure we got the number of bytes we asked for. 730 | if len(response) != response_len: 731 | print("Upload failed: No reponse to command") 732 | verboseprint('No response for command 0x{:08X}'.format(command)) 733 | exit() 734 | 735 | return response 736 | 737 | #****************************************************************************** 738 | # 739 | # Errors 740 | # 741 | #****************************************************************************** 742 | class BootError(Exception): 743 | pass 744 | 745 | class NoAckError(BootError): 746 | pass 747 | 748 | 749 | def parse_arguments(): 750 | parser = argparse.ArgumentParser(description = 751 | 'Combination script to upload application binaries to Artemis module. Includes:\n\t\'- bin2blob: create OTA blob from binary image\'\n\t\'- blob2wired: create wired update image from OTA blob\'\n\t\'- upload: send wired update image to Apollo3 Artemis module via serial port\'\n\nThere are many command-line arguments. They have been labeled by which steps they apply to\n') 752 | 753 | parser.add_argument('-a', dest = 'abort', default=-1, type=int, choices = [0,1,-1], 754 | help = 'upload: Should it send abort command? (0 = abort, 1 = abort and quit, -1 = no abort) (default is -1)') 755 | 756 | parser.add_argument('--authalgo', dest = 'authalgo', type=auto_int, default=0, choices=range(0, AM_SECBOOT_AUTH_ALGO_MAX+1), 757 | help = 'bin2blob, blob2wired: ' + str(helpAuthAlgo)) 758 | 759 | parser.add_argument('--authI', dest = 'authI', type=auto_int, default=0, choices=[0,1], 760 | help = 'bin2blob: Install Authentication check enabled (Default = N)?') 761 | 762 | parser.add_argument('--authB', dest = 'authB', type=auto_int, default=0, choices=[0,1], 763 | help = 'bin2blob: Boot Authentication check enabled (Default = N)?') 764 | 765 | parser.add_argument('--authkey', dest = 'authkey', type=auto_int, default=(minHmacKeyIdx), choices = range(minHmacKeyIdx, maxHmacKeyIdx + 1), 766 | help = 'bin2blob, blob2wired: Authentication Key Idx? (' + str(minHmacKeyIdx) + ' to ' + str(maxHmacKeyIdx) + ')') 767 | 768 | parser.add_argument('-b', dest='baud', default=115200, type=int, 769 | help = 'upload: Baud Rate (default is 115200)') 770 | 771 | parser.add_argument('--bin', dest='appFile', type=argparse.FileType('rb'), 772 | help='bin2blob: binary file (blah.bin)') 773 | 774 | parser.add_argument('-clean', dest='clean', default=0, type=int, 775 | help = 'All: whether or not to remove intermediate files') 776 | 777 | parser.add_argument('--child0', dest = 'child0', type=auto_int, default=hex(0xFFFFFFFF), 778 | help = 'bin2blob: child (blobPtr#0 for Main / feature key for AM3P)') 779 | 780 | parser.add_argument('--child1', dest = 'child1', type=auto_int, default=hex(0xFFFFFFFF), 781 | help = 'bin2blob: child (blobPtr#1 for Main)') 782 | 783 | parser.add_argument('--crcI', dest = 'crcI', type=auto_int, default=1, choices=[0,1], 784 | help = 'bin2blob: Install CRC check enabled (Default = Y)?') 785 | 786 | parser.add_argument('--crcB', dest = 'crcB', type=auto_int, default=0, choices=[0,1], 787 | help = 'bin2blob: Boot CRC check enabled (Default = N)?') 788 | 789 | parser.add_argument('--encalgo', dest = 'encalgo', type=auto_int, default=0, choices = range(0, AM_SECBOOT_ENC_ALGO_MAX+1), 790 | help = 'bin2blob, blob2wired: ' + str(helpEncAlgo)) 791 | 792 | parser.add_argument('--erasePrev', dest = 'erasePrev', type=auto_int, default=0, choices=[0,1], 793 | help = 'bin2blob: erasePrev (Valid only for main)') 794 | 795 | # parser.add_argument('-f', dest='binfile', default='', 796 | # help = 'upload: Binary file to program into the target device') 797 | 798 | parser.add_argument('-i', dest = 'imagetype', default=AM_SECBOOT_WIRED_IMAGETYPE_INVALID, type=auto_int, 799 | choices = [ 800 | (AM_SECBOOT_WIRED_IMAGETYPE_SBL), 801 | (AM_SECBOOT_WIRED_IMAGETYPE_AM3P), 802 | (AM_SECBOOT_WIRED_IMAGETYPE_PATCH), 803 | (AM_SECBOOT_WIRED_IMAGETYPE_MAIN), 804 | (AM_SECBOOT_WIRED_IMAGETYPE_CHILD), 805 | (AM_SECBOOT_WIRED_IMAGETYPE_CUSTPATCH), 806 | (AM_SECBOOT_WIRED_IMAGETYPE_NONSECURE), 807 | (AM_SECBOOT_WIRED_IMAGETYPE_INFO0), 808 | (AM_SECBOOT_WIRED_IMAGETYPE_INFO0_NOOTA), 809 | (AM_SECBOOT_WIRED_IMAGETYPE_INVALID) 810 | ], 811 | help = 'blob2wired, upload: ImageType (' 812 | + str(AM_SECBOOT_WIRED_IMAGETYPE_SBL) + ': SBL, ' 813 | + str(AM_SECBOOT_WIRED_IMAGETYPE_AM3P) + ': AM3P, ' 814 | + str(AM_SECBOOT_WIRED_IMAGETYPE_PATCH) + ': Patch, ' 815 | + str(AM_SECBOOT_WIRED_IMAGETYPE_MAIN) + ': Main, ' 816 | + str(AM_SECBOOT_WIRED_IMAGETYPE_CHILD) + ': Child, ' 817 | + str(AM_SECBOOT_WIRED_IMAGETYPE_CUSTPATCH) + ': CustOTA, ' 818 | + str(AM_SECBOOT_WIRED_IMAGETYPE_NONSECURE) + ': NonSecure, ' 819 | + str(AM_SECBOOT_WIRED_IMAGETYPE_INFO0) + ': Info0 ' 820 | + str(AM_SECBOOT_WIRED_IMAGETYPE_INFO0_NOOTA) + ': Info0_NOOTA) ' 821 | + str(AM_SECBOOT_WIRED_IMAGETYPE_INVALID) + ': Invalid) ' 822 | '- default[Invalid]') 823 | 824 | parser.add_argument('--kek', dest = 'kek', type=auto_int, default=(minAesKeyIdx), choices = range(minAesKeyIdx, maxAesKeyIdx+1), 825 | help = 'KEK index? (' + str(minAesKeyIdx) + ' to ' + str(maxAesKeyIdx) + ')') 826 | 827 | parser.add_argument('--load-address-wired', dest='loadaddress_blob', type=auto_int, default=hex(0x60000), 828 | help='blob2wired: Load address of the binary - Where in flash the blob will be stored (could be different than install address of binary within).') 829 | 830 | parser.add_argument('--load-address-blob', dest='loadaddress_image', type=auto_int, default=hex(AM_SECBOOT_DEFAULT_NONSECURE_MAIN), 831 | help='bin2blob: Load address of the binary.') 832 | 833 | parser.add_argument('--loglevel', dest='loglevel', type=auto_int, default=AM_PRINT_LEVEL_INFO, 834 | choices = range(AM_PRINT_LEVEL_MIN, AM_PRINT_LEVEL_MAX+1), 835 | help='bin2blob, blob2wired: ' + str(helpPrintLevel)) 836 | 837 | parser.add_argument('--magic-num', dest='magic_num', default=hex(AM_IMAGE_MAGIC_NONSECURE), 838 | type=lambda x: x.lower(), 839 | # type = str.lower, 840 | choices = [ 841 | hex(AM_IMAGE_MAGIC_MAIN), 842 | hex(AM_IMAGE_MAGIC_CHILD), 843 | hex(AM_IMAGE_MAGIC_CUSTPATCH), 844 | hex(AM_IMAGE_MAGIC_NONSECURE), 845 | hex(AM_IMAGE_MAGIC_INFO0) 846 | ], 847 | help = 'bin2blob: Magic Num (' 848 | + str(hex(AM_IMAGE_MAGIC_MAIN)) + ': Main, ' 849 | + str(hex(AM_IMAGE_MAGIC_CHILD)) + ': Child, ' 850 | + str(hex(AM_IMAGE_MAGIC_CUSTPATCH)) + ': CustOTA, ' 851 | + str(hex(AM_IMAGE_MAGIC_NONSECURE)) + ': NonSecure, ' 852 | + str(hex(AM_IMAGE_MAGIC_INFO0)) + ': Info0) ' 853 | '- default[Main]' 854 | ) 855 | 856 | parser.add_argument('-o', dest = 'output', default='wuimage', 857 | help = 'all: Output filename (without the extension) [also used for intermediate filenames]') 858 | 859 | parser.add_argument('-ota', dest = 'otadesc', type=auto_int, default=0xFE000, 860 | help = 'upload: OTA Descriptor Page address (hex) - (Default is 0xFE000 - at the end of main flash) - enter 0xFFFFFFFF to instruct SBL to skip OTA') 861 | 862 | parser.add_argument('--options', dest = 'options', type=auto_int, default=0x1, 863 | help = 'blob2wired: Options (16b hex value) - bit0 instructs to perform OTA of the image after wired download (set to 0 if only downloading & skipping OTA flow)') 864 | 865 | parser.add_argument('-p', dest = 'protection', type=auto_int, default=0, choices = [0x0, 0x1, 0x2, 0x3], 866 | help = 'bin2blob: protection info 2 bit C W') 867 | 868 | parser.add_argument('-port', dest = 'port', help = 'upload: Serial COMx Port') 869 | 870 | parser.add_argument('-r', dest = 'reset', default=1, type=auto_int, choices = [0,1,2], 871 | help = 'upload: Should it send reset command after image download? (0 = no reset, 1 = POI, 2 = POR) (default is 1)') 872 | 873 | parser.add_argument('--raw', dest='raw', default='', 874 | help = 'upload: Binary file for raw message') 875 | 876 | parser.add_argument('--split', dest='split', type=auto_int, default=hex(MAX_DOWNLOAD_SIZE), 877 | help='blob2wired, upload: Specify the max block size if the image will be downloaded in pieces') 878 | 879 | parser.add_argument('--version', dest = 'version', type=auto_int, default=0, 880 | help = 'bin2blob: version (15 bit)') 881 | 882 | parser.add_argument("-v", "--verbose", default=0, help="All: Enable verbose output", 883 | action="store_true") 884 | 885 | 886 | args = parser.parse_args() 887 | args.magic_num = int(args.magic_num, 16) 888 | 889 | 890 | return args 891 | 892 | 893 | 894 | #****************************************************************************** 895 | # 896 | # Main function. 897 | # 898 | #****************************************************************************** 899 | 900 | # example calling: 901 | # python artemis_bin_to_board.py --bin application.bin --load-address-blob 0x20000 --magic-num 0xCB -o application --version 0x0 --load-address-wired 0xC000 -i 6 --options 0x1 -b 921600 -port COM4 -r 1 -v 902 | 903 | def main(): 904 | # Read the arguments. 905 | args = parse_arguments() 906 | am_set_print_level(args.loglevel) 907 | 908 | global blob2wiredfile 909 | 910 | bin2blob_process(args.loadaddress_blob, args.appFile, args.magic_num, args.crcI, args.crcB, args.authI, args.authB, args.protection, args.authkey, args.output, args.kek, args.version, args.erasePrev, args.child0, args.child1, args.authalgo, args.encalgo) 911 | blob2wired_process( blob2wiredfile, args.imagetype, args.loadaddress_image, args.authalgo, args.encalgo, args.authkey, args.kek, args.options, args.split, args.output) 912 | 913 | # todo: link the bin2blob step with the blob2wired step by input/output files 914 | 915 | 916 | #Create print function for verbose output if caller deems it: https://stackoverflow.com/questions/5980042/how-to-implement-the-verbose-or-v-option-into-a-script 917 | if args.verbose: 918 | def verboseprint(*args): 919 | # Print each argument separately so caller doesn't need to 920 | # stuff everything to be printed into a single string 921 | for arg in args: 922 | print(arg, end=''), 923 | print() 924 | else: 925 | verboseprint = lambda *a: None # do-nothing function 926 | 927 | upload(args, verboseprint) 928 | 929 | if(args.clean == 1): 930 | print('Cleaning up intermediate files') # todo: why isnt this showing w/ -clean option? 931 | 932 | 933 | if __name__ == '__main__': 934 | main() 935 | -------------------------------------------------------------------------------- /artemis_uploader/asb/keys_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from .am_defines import * 3 | 4 | minAesKeyIdx = 8 5 | maxAesKeyIdx = 15 6 | minHmacKeyIdx = 8 7 | maxHmacKeyIdx = 15 8 | 9 | ###### Following are just dummy keys - Should be substituted with real keys ####### 10 | keyTblAes = [ 11 | # Info0 Keys - Starting at index 8 12 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 14 | 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 15 | 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 16 | 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 17 | 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 18 | 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 19 | 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 20 | ] 21 | 22 | keyTblHmac = [ 23 | # Info0 Keys - Starting at index 8 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 26 | 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 27 | 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 28 | ] 29 | 30 | custKey = [ 31 | 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 32 | ] 33 | 34 | # These are dummy values. Contact AMBIQ to get the real Recovery Key 35 | recoveryKey = [ 36 | 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 37 | ] 38 | 39 | ################################################################################### 40 | 41 | wrapKey = custKey 42 | minWrapMode = AM_SECBOOT_KEYWRAP_NONE 43 | 44 | INFO_KEY = 0xd894e09e 45 | FLASH_KEY = 0x12344321 46 | 47 | -------------------------------------------------------------------------------- /artemis_uploader/au_act_artasb.py: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------------------------------------------------------- 3 | # au_act_astasb.py 4 | # 5 | #------------------------------------------------------------------------ 6 | # 7 | # Written/Update by SparkFun Electronics, Fall 2022 8 | # 9 | # This python package implements a GUI Qt application that supports 10 | # firmware and bootloader uploading to the SparkFun Artemis module 11 | # 12 | # This file is part of the job dispatch system, which runs "jobs" 13 | # in a background thread for the artemis_uploader package/application. 14 | # 15 | # This file defines a "Action", which manages the uploading of a 16 | # bootloader to an artemis module 17 | # 18 | # More information on qwiic is at https://www.sparkfun.com/artemis 19 | # 20 | # Do you like this library? Help support SparkFun. Buy a board! 21 | # 22 | #================================================================================== 23 | # Copyright (c) 2022 SparkFun Electronics 24 | # 25 | # Permission is hereby granted, free of charge, to any person obtaining a copy 26 | # of this software and associated documentation files (the "Software"), to deal 27 | # in the Software without restriction, including without limitation the rights 28 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | # copies of the Software, and to permit persons to whom the Software is 30 | # furnished to do so, subject to the following conditions: 31 | # 32 | # The above copyright notice and this permission notice shall be included in all 33 | # copies or substantial portions of the Software. 34 | # 35 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | # SOFTWARE. 42 | #================================================================================== 43 | # 44 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 45 | # 46 | #----------------------------------------------------------------------------- 47 | from .au_action import AxAction, AxJob 48 | from .asb import main as asb_main 49 | import tempfile 50 | import sys 51 | #-------------------------------------------------------------------------------------- 52 | # Artemis Boot loader burn action 53 | class AUxArtemisBurnBootloader(AxAction): 54 | 55 | ACTION_ID = "artemis-burn-bootloader" 56 | NAME = "Artemis Bootloader Upload" 57 | 58 | def __init__(self) -> None: 59 | super().__init__(self.ACTION_ID, self.NAME) 60 | 61 | def run_job(self, job:AxJob): 62 | 63 | # fake command line args - since the apollo3 bootloader command will use 64 | # argparse 65 | sys.argv = ['./asb/asb.py', \ 66 | "--bin", job.file, \ 67 | "-port", job.port, \ 68 | "-b", str(job.baud), \ 69 | "-o", tempfile.gettempdir(), \ 70 | "--load-address-blob", "0x20000", \ 71 | "--magic-num", "0xCB", \ 72 | "--version", "0x0", \ 73 | "--load-address-wired", "0xC000", \ 74 | "-i", "6", \ 75 | "-v", \ 76 | "-clean", "1" ] 77 | 78 | # Call the ambiq command 79 | try: 80 | asb_main() 81 | 82 | except Exception: 83 | return 1 84 | 85 | return 0 86 | -------------------------------------------------------------------------------- /artemis_uploader/au_act_artfrmw.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # au_act_artfrmw.py 3 | # 4 | #------------------------------------------------------------------------ 5 | # 6 | # Written/Update by SparkFun Electronics, Fall 2022 7 | # 8 | # This python package implements a GUI Qt application that supports 9 | # firmware and bootloader uploading to the SparkFun Artemis module 10 | # 11 | # This file is part of the job dispatch system, which runs "jobs" 12 | # in a background thread for the artemis_uploader package/application. 13 | # 14 | # This file defines a "Action", which manages the uploading of a 15 | # firmware file to an artemis module 16 | # 17 | # More information on qwiic is at https://www.sparkfun.com/artemis 18 | # 19 | # Do you like this library? Help support SparkFun. Buy a board! 20 | # 21 | #================================================================================== 22 | # Copyright (c) 2022 SparkFun Electronics 23 | # 24 | # Permission is hereby granted, free of charge, to any person obtaining a copy 25 | # of this software and associated documentation files (the "Software"), to deal 26 | # in the Software without restriction, including without limitation the rights 27 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | # copies of the Software, and to permit persons to whom the Software is 29 | # furnished to do so, subject to the following conditions: 30 | # 31 | # The above copyright notice and this permission notice shall be included in all 32 | # copies or substantial portions of the Software. 33 | # 34 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | # SOFTWARE. 41 | #================================================================================== 42 | # 43 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 44 | # 45 | #----------------------------------------------------------------------------- 46 | from .au_action import AxAction, AxJob 47 | from .artemis_svl import upload_firmware 48 | 49 | #-------------------------------------------------------------------------------------- 50 | # action testing 51 | class AUxArtemisUploadFirmware(AxAction): 52 | 53 | ACTION_ID = "artemis-upload-firmware" 54 | NAME = "Artemis Firmware Upload" 55 | 56 | def __init__(self) -> None: 57 | super().__init__(self.ACTION_ID, self.NAME) 58 | 59 | def run_job(self, job:AxJob): 60 | 61 | try: 62 | upload_firmware(job.file, job.port, job.baud) 63 | 64 | except Exception: 65 | return 1 66 | 67 | return 0 68 | 69 | -------------------------------------------------------------------------------- /artemis_uploader/au_action.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # au_action.py 3 | # 4 | #------------------------------------------------------------------------ 5 | # 6 | # Written/Update by SparkFun Electronics, Fall 2022 7 | # 8 | # This python package implements a GUI Qt application that supports 9 | # firmware and bootloader uploading to the SparkFun Artemis module 10 | # 11 | # This file is part of the job dispatch system, which runs "jobs" 12 | # in a background thread for the artemis_uploader package/application. 13 | # 14 | # This file defines key data types for the background processing system. 15 | # A "Job" type and a "Action" type are defined in this file. 16 | # 17 | # Job - has a type and a list of parameter values for the Job to execute 18 | # 19 | # Action - defines a process type that runs a job 20 | # 21 | # More information on qwiic is at https://www.sparkfun.com/artemis 22 | # 23 | # Do you like this library? Help support SparkFun. Buy a board! 24 | # 25 | #================================================================================== 26 | # Copyright (c) 2022 SparkFun Electronics 27 | # 28 | # Permission is hereby granted, free of charge, to any person obtaining a copy 29 | # of this software and associated documentation files (the "Software"), to deal 30 | # in the Software without restriction, including without limitation the rights 31 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | # copies of the Software, and to permit persons to whom the Software is 33 | # furnished to do so, subject to the following conditions: 34 | # 35 | # The above copyright notice and this permission notice shall be included in all 36 | # copies or substantial portions of the Software. 37 | # 38 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | # SOFTWARE. 45 | #================================================================================== 46 | # 47 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 48 | # 49 | #----------------------------------------------------------------------------- 50 | # "actions" - commands that execute a command for the application 51 | # 52 | #-------------------------------------------------------------------------- 53 | # simple job class - list of parameters and an ID string. 54 | # 55 | # Sub-classes a dictionary (dict), and stores parameters in the dictionary. 56 | # Parameters can also be accessed as attributes. 57 | # 58 | # Example: 59 | # 60 | # myJob = AxJob('my-job-id') 61 | # 62 | # myJob['data'] = 1 63 | # 64 | # print(myJob.data) 65 | # 66 | # myJob.data=2 67 | # 68 | # print(myJob['data']) 69 | # 70 | # And can init the job using dictionary syntax 71 | # 72 | # myJob = AxJob('my-job-id', {"data":1, "sensor":"spectra1", "flight":33.3}) 73 | # 74 | # print(myJob.data) 75 | # print(myJob.sensor) 76 | # print(myJob.flight) 77 | # 78 | 79 | class AxJob(dict): 80 | 81 | # class variable for job ids 82 | _next_job_id =1 83 | 84 | def __init__(self, action_id:str, indict=None): 85 | 86 | if indict is None: 87 | indict = {} 88 | 89 | self.action_id = action_id 90 | 91 | self.job_id = AxJob._next_job_id; 92 | AxJob._next_job_id = AxJob._next_job_id+1; 93 | 94 | # super 95 | dict.__init__(self, indict) 96 | 97 | # flag 98 | self.__initialized = True 99 | 100 | def __getattr__(self, item): 101 | 102 | try: 103 | return self.__getitem__(item) 104 | except KeyError: 105 | raise AttributeError(item) 106 | 107 | def __setattr__(self, item, value): 108 | 109 | if '_AxJob__initialized' not in self.__dict__: # this test allows attributes to be set in the __init__ method 110 | return dict.__setattr__(self, item, value) 111 | 112 | else: 113 | self.__setitem__(item, value) 114 | 115 | #def __str__(self): 116 | # return "\"" + self.action_id + "\" :" + str(self._args) 117 | 118 | #-------------------------------------------------------------------------- 119 | # Base action class - defines method 120 | # 121 | # Sub-class this class to create a action 122 | 123 | class AxAction(object): 124 | 125 | def __init__(self, action_id:str, name="") -> None: 126 | object.__init__(self) 127 | self.action_id = action_id 128 | self.name = name 129 | 130 | def run_job(self, job:AxJob) -> int: 131 | return 1 # error 132 | -------------------------------------------------------------------------------- /artemis_uploader/au_worker.py: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------------------------------------------------------- 3 | # au_worker.py 4 | # 5 | #------------------------------------------------------------------------ 6 | # 7 | # Written/Update by SparkFun Electronics, Fall 2022 8 | # 9 | # This python package implements a GUI Qt application that supports 10 | # firmware and bootloader uploading to the SparkFun Artemis module 11 | # 12 | # This file is part of the job dispatch system, which runs "jobs" 13 | # in a background thread for the artemis_uploader package/application. 14 | # 15 | # This file implements the main logic of the background worker system. 16 | # 17 | # In general, the worker implements a background thread which waits for 18 | # "jobs" to be passed in for execution via a queue object. Once a job is 19 | # detected, it is sent to the target "action" object for execution. 20 | # 21 | # During job execution, messages are relayed to the main application 22 | # via a passed in callback function. 23 | # 24 | # When a job is executed, it is assumed that "command line" python 25 | # scripts are used for the underlying logic. As such stdout and stderr are 26 | # captured for output. Also, "exit()" calls are trapped, so the thread 27 | # will continue to execute. 28 | # 29 | # More information on qwiic is at https://www.sparkfun.com/artemis 30 | # 31 | # Do you like this library? Help support SparkFun. Buy a board! 32 | # 33 | #================================================================================== 34 | # Copyright (c) 2022 SparkFun Electronics 35 | # 36 | # Permission is hereby granted, free of charge, to any person obtaining a copy 37 | # of this software and associated documentation files (the "Software"), to deal 38 | # in the Software without restriction, including without limitation the rights 39 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | # copies of the Software, and to permit persons to whom the Software is 41 | # furnished to do so, subject to the following conditions: 42 | # 43 | # The above copyright notice and this permission notice shall be included in all 44 | # copies or substantial portions of the Software. 45 | # 46 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 52 | # SOFTWARE. 53 | #================================================================================== 54 | # 55 | # pylint: disable=old-style-class, missing-docstring, wrong-import-position 56 | # 57 | #----------------------------------------------------------------------------- 58 | import time 59 | import queue 60 | from threading import Thread 61 | from .au_action import AxAction, AxJob 62 | from contextlib import redirect_stdout, redirect_stderr 63 | 64 | #-------------------------------------------------------------------------------------- 65 | # AUxIOWedge 66 | # 67 | # Used to redirect/capture output chars from the print() function and redirect to our 68 | # console. Allows the use of command line routines in this GUI app 69 | 70 | 71 | from io import TextIOWrapper, BytesIO 72 | 73 | 74 | class AUxIOWedge(TextIOWrapper): 75 | def __init__(self, output_funct, suppress=False, newline="\n"): 76 | super(AUxIOWedge, self).__init__(BytesIO(), 77 | encoding="utf-8", 78 | errors="surrogatepass", 79 | newline=newline) 80 | 81 | self._output_func = output_funct 82 | self._suppress = suppress 83 | 84 | def write(self, buffer): 85 | 86 | # Just send buffer to our output console 87 | if not self._suppress: 88 | self._output_func(buffer) 89 | 90 | return len(buffer) 91 | #-------------------------------------------------------------------------------------- 92 | # Worker thread to manage background jobs passed in via a queue 93 | 94 | # define a worker class/thread 95 | 96 | class AUxWorker(object): 97 | 98 | TYPE_MESSAGE = 1 99 | TYPE_FINISHED = 2 100 | 101 | def __init__(self, cb_function): 102 | 103 | object.__init__(self) 104 | 105 | # create a standard python queue = the queue is used to communicate 106 | # work to the background thread in a safe manner. "Jobs" to do 107 | # are passed to the background thread via this queue 108 | self._queue = queue.Queue() 109 | 110 | self._cb_function = cb_function 111 | 112 | self._shutdown = False; 113 | 114 | # stash of registered actions 115 | self._actions = {} 116 | 117 | # throw the work/job into a thread 118 | self._thread = Thread(target = self.process_loop, args=(self._queue,)) 119 | self._thread.start() 120 | 121 | # Make sure the thread stops running in Destructor. And add shutdown user method 122 | def __del__(self): 123 | 124 | self._shutdown = True 125 | 126 | def shutdown(self): 127 | 128 | self._shutdown = True 129 | 130 | #------------------------------------------------------ 131 | # Add a execution type/object (an AxAction) to our available 132 | # job type list 133 | 134 | def add_action(self, *argv) -> None: 135 | 136 | for action in argv: 137 | if not isinstance(action, AxAction): 138 | print("Parameter is not of type AxAction" + str(type(action))) 139 | continue 140 | self._actions[action.action_id] = action 141 | 142 | 143 | #------------------------------------------------------ 144 | # Add a job for execution by the background thread. 145 | # 146 | def add_job(self, theJob:AxJob)->None: 147 | 148 | 149 | # get job ID 150 | job_id = theJob.job_id 151 | 152 | self._queue.put(theJob) 153 | 154 | return job_id 155 | 156 | #------------------------------------------------------ 157 | # call back function for output from the bootloader - called from our IO wedge class. 158 | # 159 | def message(self, message): 160 | 161 | # relay/post message to the GUI's console 162 | 163 | self._cb_function(self.TYPE_MESSAGE, message) 164 | #------------------------------------------------------ 165 | # Job dispatcher. Job should be an AxJob object instance. 166 | # 167 | # retval 0 = OKAY 168 | 169 | def dispatch_job(self, job): 170 | 171 | # make sure we have a job 172 | if not isinstance(job, AxJob): 173 | self.message("ERROR - invalid job dispatched\n") 174 | return 1 175 | 176 | # is the target action in our available actions dictionary? 177 | if job.action_id not in self._actions: 178 | self.message("Unknown job type. Aborting\n") 179 | return 1 180 | 181 | # write out the job 182 | # send a line break across the console - start of a new activity 183 | self.message('\n' + ('_'*70) + "\n") 184 | 185 | # Job details 186 | self.message(self._actions[job.action_id].name + "\n\n") 187 | for key in sorted(job.keys()): 188 | self.message(key.capitalize() + ":\t" + str(job[key]) + '\n') 189 | 190 | self.message('\n') 191 | 192 | # capture stdio and stderr outputs 193 | with redirect_stdout(AUxIOWedge(self.message)): 194 | with redirect_stderr(AUxIOWedge(self.message, suppress=True)): 195 | 196 | # catch any exit() calls the underlying system might make 197 | try: 198 | # run the action 199 | return self._actions[job.action_id].run_job(job) 200 | except SystemExit as error: 201 | # some scripts call exit(), even if not an error 202 | self.message("Complete.") 203 | 204 | return 1 205 | 206 | #------------------------------------------------------ 207 | # The thread processing loop 208 | 209 | def process_loop(self, inputQueue): 210 | 211 | # Wait on jobs .. forever... Exit when shutdown is true 212 | 213 | self._shutdown = False 214 | 215 | # run 216 | while not self._shutdown: 217 | 218 | if inputQueue.empty(): 219 | time.sleep(1) # no job, sleep a bit 220 | else: 221 | job = inputQueue.get() 222 | 223 | status = self.dispatch_job(job) 224 | 225 | # job is finished - let UX know -pass status, action type and job id 226 | self._cb_function(self.TYPE_FINISHED, status, job.action_id, job.job_id) 227 | 228 | -------------------------------------------------------------------------------- /artemis_uploader/resource/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.1.0" 2 | -------------------------------------------------------------------------------- /artemis_uploader/resource/artemis-icon-blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/artemis-icon-blk.png -------------------------------------------------------------------------------- /artemis_uploader/resource/artemis-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/artemis-icon.png -------------------------------------------------------------------------------- /artemis_uploader/resource/artemis-logo-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/artemis-logo-rounded.png -------------------------------------------------------------------------------- /artemis_uploader/resource/artemis-uploader.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/artemis-uploader.ico -------------------------------------------------------------------------------- /artemis_uploader/resource/artemis_svl.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/artemis_svl.bin -------------------------------------------------------------------------------- /artemis_uploader/resource/sfe_logo_med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/sfe_logo_med.png -------------------------------------------------------------------------------- /artemis_uploader/resource/sparkdisk.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/artemis_uploader/resource/sparkdisk.icns -------------------------------------------------------------------------------- /examples/Blink.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/examples/Blink.bin -------------------------------------------------------------------------------- /images/artemis-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-linux.png -------------------------------------------------------------------------------- /images/artemis-macos-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-1.png -------------------------------------------------------------------------------- /images/artemis-macos-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-2.png -------------------------------------------------------------------------------- /images/artemis-macos-install-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-1.png -------------------------------------------------------------------------------- /images/artemis-macos-install-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-2.png -------------------------------------------------------------------------------- /images/artemis-macos-install-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-3.png -------------------------------------------------------------------------------- /images/artemis-macos-install-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-4.png -------------------------------------------------------------------------------- /images/artemis-macos-install-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-5.png -------------------------------------------------------------------------------- /images/artemis-macos-install-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-macos-install-6.png -------------------------------------------------------------------------------- /images/artemis-uploader-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-uploader-banner.png -------------------------------------------------------------------------------- /images/artemis-windows-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-windows-1.png -------------------------------------------------------------------------------- /images/artemis-windows-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-windows-2.png -------------------------------------------------------------------------------- /images/artemis-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/artemis-windows.png -------------------------------------------------------------------------------- /images/bootloader-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/bootloader-upload.png -------------------------------------------------------------------------------- /images/firmware-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/firmware-upload.png -------------------------------------------------------------------------------- /images/macos-finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/Artemis-Firmware-Upload-GUI/876d7decda7099ace3c12750203a90af5e6bc56f/images/macos-finder.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = DESCRIPTION.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------------------------------------------------------- 3 | # setup.py 4 | # 5 | #------------------------------------------------------------------------ 6 | # 7 | # Written/Update by SparkFun Electronics, Fall 2022 8 | # 9 | # This python package implements a GUI Qt application that supports 10 | # firmware and boot loader uploading to the SparkFun Artemis module 11 | # 12 | # This file defines the python install package to be build for the 13 | # 'artemis_upload' package 14 | # 15 | # More information on qwiic is at https://www.sparkfun.com/artemis 16 | # 17 | # Do you like this library? Help support SparkFun. Buy a board! 18 | # 19 | #================================================================================== 20 | # Copyright (c) 2022 SparkFun Electronics 21 | # 22 | # Permission is hereby granted, free of charge, to any person obtaining a copy 23 | # of this software and associated documentation files (the "Software"), to deal 24 | # in the Software without restriction, including without limitation the rights 25 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 26 | # copies of the Software, and to permit persons to whom the Software is 27 | # furnished to do so, subject to the following conditions: 28 | # 29 | # The above copyright notice and this permission notice shall be included in all 30 | # copies or substantial portions of the Software. 31 | # 32 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | # SOFTWARE. 39 | #================================================================================== 40 | import setuptools 41 | from codecs import open # To use a consistent encoding 42 | from os import path 43 | from platform import system, machine 44 | import subprocess 45 | import sys 46 | 47 | # sub folder for our resource files 48 | _RESOURCE_DIRECTORY = "artemis_uploader/resource" 49 | 50 | #https://stackoverflow.com/a/50914550 51 | def resource_path(relative_path): 52 | """ Get absolute path to resource, works for dev and for PyInstaller """ 53 | base_path = getattr(sys, '_MEIPASS', path.dirname(path.abspath(__file__))) 54 | return path.join(base_path, _RESOURCE_DIRECTORY, relative_path) 55 | 56 | def get_version(rel_path: str) -> str: 57 | try: 58 | with open(resource_path(rel_path), encoding='utf-8') as fp: 59 | for line in fp.read().splitlines(): 60 | if line.startswith("__version__"): 61 | delim = '"' if '"' in line else "'" 62 | return line.split(delim)[1] 63 | raise RuntimeError("Unable to find version string.") 64 | except: 65 | raise RuntimeError("Unable to find _version.py.") 66 | 67 | _APP_VERSION = get_version("_version.py") 68 | 69 | here = path.abspath(path.dirname(__file__)) 70 | 71 | # Get the long description from the relevant file 72 | with open(path.join(here, 'DESCRIPTION.md'), encoding='utf-8') as f: 73 | long_description = f.read() 74 | 75 | install_deps = ['darkdetect', 'pyserial', 'pycryptodome'] 76 | 77 | # Raspberry Pi needs python3-pyqt5 and python3-pyqt5.qtserialport 78 | # which can only be installed with apt-get 79 | if (system() == "Linux") and ((machine() == "armv7l") or (machine() == "aarch64")): 80 | cmd = ['sudo','apt-get','install','python3-pyqt5','python3-pyqt5.qtserialport'] 81 | subprocess.run(cmd) 82 | else: 83 | install_deps.append('pyqt5') 84 | 85 | setuptools.setup( 86 | name='artemis_uploader', 87 | 88 | # Versions should comply with PEP440. For a discussion on single-sourcing 89 | # the version across setup.py and the project code, see 90 | # http://packaging.python.org/en/latest/tutorial.html#version 91 | version=_APP_VERSION, 92 | 93 | description='Application to upload firmware to SparkFun Artemis based products', 94 | long_description=long_description, 95 | 96 | # The project's main homepage. 97 | url='https://www.sparkfun.com/artemis', 98 | 99 | # Author details 100 | author='SparkFun Electronics', 101 | author_email='sales@sparkfun.com', 102 | 103 | project_urls = { 104 | "Bug Tracker" : "https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/issues", 105 | "Repository" : "https://github.com/sparkfun/Artemis-Firmware-Upload-GUI" 106 | }, 107 | # Choose your license 108 | license='MIT', 109 | 110 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 111 | classifiers=[ 112 | # How mature is this project? Common values are 113 | # 3 - Alpha 114 | # 4 - Beta 115 | # 5 - Production/Stable 116 | 'Production Stable :: 5', 117 | 118 | # Indicate who your project is intended for 119 | 'Intended Audience :: Developers', 120 | 'Topic :: Hardware Development :: Build Tools', 121 | 122 | # Pick your license as you wish (should match "license" above) 123 | 'License :: OSI Approved :: MIT License', 124 | 125 | # Specify the Python versions you support here. In particular, ensure 126 | # that you indicate whether you support Python 2, Python 3 or both. 127 | 'Programming Language :: Python :: 3', 128 | 'Programming Language :: Python :: 3.7', 129 | 'Programming Language :: Python :: 3.8', 130 | 'Programming Language :: Python :: 3.9', 131 | 'Programming Language :: Python :: 3.10', 132 | 'Programming Language :: Python :: 3.11', 133 | 'Programming Language :: Python :: 3.12', 134 | 135 | ], 136 | 137 | download_url="https://github.com/sparkfun/Artemis-Firmware-Upload-GUI/releases", 138 | 139 | # What does your project relate to? 140 | keywords='Firmware SparkFun Artemis Arduino', 141 | 142 | # You can just specify the packages manually here if your project is 143 | # simple. Or you can use find_packages(). 144 | packages=["artemis_uploader", "artemis_uploader/asb", "artemis_uploader/resource"], 145 | 146 | # List run-time dependencies here. These will be installed by pip when your 147 | # project is installed. For an analysis of "install_requires" vs pip's 148 | # requirements files see: 149 | # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files 150 | install_requires=install_deps, 151 | 152 | # If there are data files included in your packages that need to be 153 | # installed, specify them here. If using Python 2.6 or less, then these 154 | # have to be included in MANIFEST.in as well. 155 | package_data={ 156 | 'artemis_uploader/resource': ['*.png', '*.jpg', '*.ico', '*.bin', '*.icns'], 157 | }, 158 | 159 | 160 | 161 | # To provide executable scripts, use entry points in preference to the 162 | # "scripts" keyword. Entry points provide cross-platform support and allow 163 | # pip to create the appropriate form of executable for the target platform. 164 | entry_points={ 165 | 'console_scripts': ['artemis_upload=artemis_uploader:startArtemisUploader', 166 | ], 167 | }, 168 | ) 169 | --------------------------------------------------------------------------------