├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── 99-axdl.rules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.ja.md ├── README.md ├── axdl-cli ├── Cargo.toml └── src │ └── main.rs ├── axdl-gui ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── index.html ├── src │ └── main.rs └── ui │ └── app-window.slint ├── axdl ├── Cargo.toml └── src │ ├── communication.rs │ ├── frame.rs │ ├── lib.rs │ ├── partition.rs │ └── transport │ ├── mod.rs │ ├── serial.rs │ ├── usb.rs │ ├── webserial.rs │ └── webusb.rs ├── doc └── axdl-gui.drawio.svg └── wireshark └── axdl.lua /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | override: true 23 | components: rustfmt, clippy 24 | target: wasm32-unknown-unknown 25 | 26 | - name: Install wasm-pack 27 | run: cargo install wasm-pack 28 | 29 | - name: Install system dependencies 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y libudev-dev libusb-1.0-0-dev 33 | 34 | - name: Check axdl 35 | run: cd axdl && cargo check 36 | 37 | - name: Check axdl-cli 38 | run: cd axdl-cli && cargo check 39 | 40 | - name: Check axdl-gui 41 | run: cd axdl-gui && cargo check --target wasm32-unknown-unknown 42 | 43 | - name: Run tests 44 | run: cd axdl && cargo test 45 | 46 | - name: Clippy 47 | run: cargo clippy --workspace --exclude axdl-gui -- -A warnings 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /M5_LLM_ubuntu22.04_not_fixed 3 | *.axp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.features": "all", 3 | "rust-analyzer.cargo.features": "all" 4 | } -------------------------------------------------------------------------------- /99-axdl.rules: -------------------------------------------------------------------------------- 1 | # Axera downloader VID:PID 2 | ATTRS{idVendor}=="32c9", ATTRS{idProduct}=="1000", MODE="664", GROUP="plugdev", TAG+="uaccess" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | 5 | members = ["axdl", "axdl-cli", "axdl-gui"] 6 | 7 | [workspace.package] 8 | version = "0.1.2" 9 | authors = ["Kenta Ida"] 10 | edition = "2021" 11 | license = "Apache-2.0" 12 | repository = "https://github.com/ciniml/axdl-rs" 13 | 14 | [workspace.dependencies] 15 | anyhow = { version = "1.0.95", features = ["backtrace"] } 16 | bincode = "1.3.3" 17 | byteorder = "1.5.0" 18 | clap = { version = "4.5.28", features = ["derive"] } 19 | hex = { version = "0.4.3", features = ["serde"] } 20 | rusb = "0.9.4" 21 | serde = { version = "1.0.217", features = ["derive"] } 22 | serde-xml-rs = "0.6.0" 23 | serde_bytes = "0.11.15" 24 | thiserror = "2.0.11" 25 | tracing = "0.1.41" 26 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 27 | tracing-wasm = "0.2.1" 28 | 29 | zip = { version = "2.2.2", default-features = false, features = ["deflate"] } 30 | async_zip = { version = "0.0.17", default-features = false, features = ["full-wasm"] } 31 | futures-util = "0.3.31" 32 | futures-io = "0.3.31" 33 | pin-project = "1.1.9" 34 | 35 | hex-literal = "0.4.1" 36 | indicatif = "0.17.11" 37 | serialport = "4.7.0" 38 | wasm-bindgen = "0.2.100" 39 | webusb-web = { version = "0.3.0" } 40 | wasm-bindgen-futures = "0.4.50" 41 | web-sys = "0.3.77" 42 | js-sys = "0.3.77" 43 | pin-utils = "0.1.0" 44 | wasm-streams = "0.4.2" 45 | rfd = "0.15.2" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # axdl-rs 非公式のAxeraイメージダウンローダーのRust実装 2 | 3 | これは、Axera SoCにイメージファイルを書き込むための非公式のAxeraイメージダウンローダーのRust実装です。 4 | 5 | [English](./README.md) 6 | 7 | ## 目次 8 | 9 | - [準備](#準備) 10 | - [インストール](#インストール) 11 | - [Webブラウザ版](#Webブラウザ版) 12 | - [ビルド](#ビルド) 13 | - [使用方法](#使用方法) 14 | - [ライセンス](#ライセンス) 15 | 16 | ## 準備 17 | 18 | ### Linux (Debian系) 19 | 20 | 通常のユーザーがデバイスにアクセスできるようにするためには、udevを設定して通常のユーザーがデバイスにアクセスできるようにする必要があります。 21 | udevを設定するには、`99-axdl.rules`を`/etc/udev/rules.d`にコピーし、udevの設定をリロードします。 22 | 23 | ``` 24 | sudo cp 99-axdl.rules /etc/udev/rules.d/ 25 | sudo udevadm control --reload 26 | ``` 27 | 28 | ユーザーが `plugdev` に属していないなら、 `plugdev` に追加しててログインしなおします。 (ログインしなおさないとグループの変更が有効にならない) 29 | 30 | ``` 31 | id 32 | # 結果に ...,(plugdev),... が含まれているか確認する 33 | ``` 34 | 35 | ``` 36 | # plugdevグループにユーザーを追加 37 | sudo usermod -a -G plugdev $USER 38 | ``` 39 | 40 | libusbとlibudevに依存しているのでインストールしておきます。 41 | 42 | ``` 43 | sudo apt install -y libudev-dev libusb-1.0-0-dev 44 | ``` 45 | 46 | ## インストール 47 | 48 | `axdl-cli` は `cargo install` にてインストールできます。 49 | 50 | ``` 51 | cargo install axdl-cli 52 | ``` 53 | 54 | ## Webブラウザ版 55 | 56 | Webブラウザ版は [https://www.fugafuga.org/axdl-rs/axdl-gui/latest/](https://www.fugafuga.org/axdl-rs/axdl-gui/latest/) から実行できます。 57 | 58 | ![axdl-gui](./doc/axdl-gui.drawio.svg) 59 | 60 | 1. `Open Image` を押して書き込みたい `.axp` ファイルを選択します。 61 | 2. rootfsを書き込みたくないなら `Exclude rootfs` にチェックを入れます。 62 | 3. `Open Device` を押してUSBデバイス選択画面を表示します 63 | 4. Axera SoCをダウンロードモードでホストに接続します。(M5Stack Module LLMの場合は、BOOTボタンを押しながらUSBケーブルを挿しこみます) 64 | 5. Axera SoCがダウンロードモードで動作している間に `Download` ボタンを押します。 (10秒くらいでダウンロードモードから抜けてしまうので、その場合は (3) からやり直します。) 65 | 66 | ## ビルド 67 | 68 | ### 準備 69 | 70 | プロジェクトをビルドする前に、rustupを使用してRustツールチェーンをインストールします。 71 | 72 | ```bash 73 | # リポジトリをクローン 74 | git clone https://github.com/ciniml/axdl-rs.git 75 | 76 | # ディレクトリを変更 77 | cd axdl-rs 78 | ``` 79 | 80 | ### コマンドライン版のビルド 81 | 82 | ``` 83 | # ビルド 84 | cargo build --bin axdl-cli --package axdl-cli 85 | ``` 86 | 87 | ### Webブラウザ版のビルド 88 | 89 | Webブラウザ版のビルドには `wasm-pack` が必要なのでインストールします。 90 | 91 | ``` 92 | cargo install wasm-pack 93 | ``` 94 | 95 | `wasm-pack` を使ってビルドします。 96 | 97 | ``` 98 | cd axdl-gui 99 | wasm-pack build --target web --release 100 | ``` 101 | 102 | ## 使用方法 103 | 104 | ### コマンドライン版 105 | 106 | *.axpイメージを書き込むには、以下のコマンドを実行し、ダウンロードモードでAxera SoCデバイスを接続します。 107 | M5Stack Module LLMの場合、BOOTボタンを押し続けながらUSBケーブルをデバイスに接続します。 108 | 109 | ```shell 110 | cargo run --bin axdl-cli --package axdl-cli --release -- --file /path/to/image.axp --wait-for-device 111 | ``` 112 | 113 | rootfsを書き込みたくない場合は、`--exclude-rootfs`オプションを指定します。 114 | 115 | ```shell 116 | cargo run --bin axdl-cli --package axdl-cli --release -- --file /path/to/image.axp --wait-for-device --exclude-rootfs 117 | ``` 118 | 119 | Windows上など、AxeraのAXDL用公式ドライバをインストールしている環境で使用するには、 `--transport serial` を指定してシリアルポート経由でアクセスするようにします。 120 | 121 | ```shell 122 | cargo run --bin axdl-cli --package axdl-cli --release -- --file /path/to/image.axp --wait-for-device --transport serial 123 | ``` 124 | 125 | ### Webブラウザ版 126 | 127 | Webブラウザ版を実行するにはビルド後、ローカルでHTTPサーバーを立ち上げるなどをしてブラウザからアクセスします。 128 | Chrome等、WebUSBに対応したブラウザが必要です。 129 | pythonのHTTPモジュールを使ってHTTPサーバーを立ち上げてる例を示します。 130 | 131 | ``` 132 | # Webブラウザ版のビルド 133 | cd axdl-gui 134 | wasm-pack build --target web --release 135 | # HTTPサーバーを立ち上げる 136 | python -m http.server 8000 137 | ``` 138 | 139 | [http://localhost:8000](http://localhost:8000) にアクセスするとWebブラウザ版が開きます。 140 | 141 | ## ライセンス 142 | 143 | このプロジェクトはApache License 2.0の下でライセンスされています。詳細については[LICENSE](LICENSE)ファイルを参照してください。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axdl-rs Unofficial Axera image downloader implementation in Rust 2 | 3 | This is an unofficial Axera image downloader implementation in Rust to write image file into Axera SoCs. 4 | 5 | [日本語](./README.ja.md) 6 | 7 | ## Table of Contents 8 | 9 | - [Prepare](#Prepare) 10 | - [Install](#Install) 11 | - [Web Browser Version](#web-browser-version) 12 | - [Build](#build) 13 | - [Usage](#usage) 14 | - [License](#license) 15 | 16 | ## Prepare 17 | 18 | ### Linux (Debian based) 19 | 20 | In order to access to the device from a normal user, you have to configure udev to allow a normal user to access the device. 21 | To configure udev, copy `99-axdl.rules` into `/etc/udev/rules.d` and reload the configuration of udev. 22 | 23 | ``` 24 | sudo cp 99-axdl.rules /etc/udev/rules.d/ 25 | sudo udevadm control --reload 26 | ``` 27 | 28 | If the user is not in the plugdev group, add them to it and re-login. (Group membership changes require a re-login to take effect.) 29 | 30 | ``` 31 | id 32 | # Confirm that ...,(plugdev),... is included in the output 33 | ``` 34 | 35 | ``` 36 | # Add the user to the plugdev group 37 | sudo usermod -a -G plugdev $USER 38 | ``` 39 | 40 | Since this tool depends on libusb and libudev, install them in advance: 41 | 42 | ``` 43 | sudo apt install -y libudev-dev libusb-1.0-0-dev 44 | ``` 45 | 46 | ## Install 47 | 48 | `axdl-cli` can be installed via `cargo install`. 49 | 50 | ``` 51 | cargo install axdl-cli 52 | ``` 53 | 54 | ## Web Browser Version 55 | 56 | You can run the web browser version from [https://www.fugafuga.org/axdl-rs/axdl-gui/latest/](https://www.fugafuga.org/axdl-rs/axdl-gui/latest/). 57 | 58 | ![axdl-gui](./doc/axdl-gui.drawio.svg) 59 | 60 | 1. Click `Open Image` and select the `.axp` file you want to flash. 61 | 2. If you don’t want to flash the rootfs, check `Exclude rootfs`. 62 | 3. Click `Open Device` to open the USB device selection screen. 63 | 4. Connect the Axera SoC to the host in download mode. (For M5Stack Module LLM, hold down the BOOT button while plugging in the USB cable.) 64 | 5. While the Axera SoC is in download mode, click `Download`. (If it exits download mode within about 10 seconds, redo step (3).) 65 | 66 | ## Build 67 | 68 | Before building the project, install the Rust toolchain via rustup. 69 | 70 | ```bash 71 | # Clone the repository 72 | git clone https://github.com/ciniml/axdl-rs.git 73 | 74 | # Change directory 75 | cd axdl-rs 76 | ``` 77 | ### Building the Command-Line Version 78 | 79 | ``` 80 | # Build 81 | cargo build --bin axdl-cli --package axdl-cli 82 | ``` 83 | 84 | ### Building the Web Browser Version 85 | 86 | To build the web browser version, install wasm-pack: 87 | 88 | ``` 89 | cargo install wasm-pack 90 | ``` 91 | 92 | Then build with wasm-pack: 93 | 94 | ``` 95 | cd axdl-gui 96 | wasm-pack build --target web --release 97 | ``` 98 | 99 | ## Usage 100 | 101 | To burn a *.axp image, run the command below and plug the Axera SoC device with download mode. 102 | For M5Stack Module LLM, keep press the BOOT button and plug the USB cable into the device. 103 | 104 | ```shell 105 | cargo run --bin axdl-cli --package axdl-cli -- --file /path/to/image.axp --wait-for-device 106 | ``` 107 | 108 | If you don't want to burn the rootfs, specify `--exclude-rootfs` option. 109 | 110 | ```shell 111 | cargo run --bin axdl-cli --package axdl-cli -- --file /path/to/image.axp --wait-for-device --exclude-rootfs 112 | ``` 113 | 114 | On Windows or other platforms where the official Axera AXDL driver is installed, you can use serial port access by specifying the --transport serial option: 115 | 116 | ```shell 117 | cargo run --bin axdl-cli --package axdl-cli -- --file /path/to/image.axp --wait-for-device --transport serial 118 | ``` 119 | 120 | ### Web Browser Version 121 | 122 | After building, start a local HTTP server and access it from your browser. 123 | A browser supporting WebUSB (such as Chrome) is required. 124 | Below is an example of using Python’s HTTP module: 125 | 126 | ``` 127 | # Build the web browser version 128 | cd axdl-gui 129 | wasm-pack build --target web --release 130 | # Start the HTTP server 131 | python -m http.server 8000 132 | ``` 133 | 134 | Access http://localhost:8000 to open the web browser version. 135 | 136 | ## License 137 | 138 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /axdl-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axdl-cli" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | description = "Unofficial CLI image download tool for Axera SoCs" 9 | keywords = ["cli", "tool", "axera"] 10 | categories = ["command-line-utilities"] 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | axdl = { path = "../axdl", version = "0.1.1", default-features = false, features = ["usb", "serial"] } 15 | 16 | anyhow = { workspace = true, features = ["backtrace"] } 17 | clap = { workspace = true, features = ["derive"] } 18 | tracing = { workspace = true } 19 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 20 | indicatif = { workspace = true } -------------------------------------------------------------------------------- /axdl-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | use std::time::Duration; 17 | 18 | use axdl::{ 19 | download_image, 20 | transport::{DynDevice, Transport as _}, 21 | DownloadConfig, DownloadProgress, 22 | }; 23 | 24 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 25 | pub enum Transport { 26 | #[default] 27 | Usb, 28 | Serial, 29 | } 30 | impl std::str::FromStr for Transport { 31 | type Err = String; 32 | fn from_str(s: &str) -> Result { 33 | match s { 34 | "usb" => Ok(Self::Usb), 35 | "serial" => Ok(Self::Serial), 36 | _ => Err(format!("Unknown transport method: {}", s)), 37 | } 38 | } 39 | } 40 | 41 | /// command line arguments 42 | #[derive(Debug, clap::Parser)] 43 | struct Args { 44 | #[clap(short, long, help = "AXP image file")] 45 | file: std::path::PathBuf, 46 | #[clap( 47 | short, 48 | long, 49 | help = "Exclude root filesystem from the download operation" 50 | )] 51 | exclude_rootfs: bool, 52 | #[clap(short, long, help = "Wait for the device to be ready")] 53 | wait_for_device: bool, 54 | #[clap(long, help = "Timeout for waiting for the device to be ready")] 55 | wait_for_device_timeout_secs: Option, 56 | #[clap( 57 | short, 58 | long, 59 | help = "Specify the transport method", 60 | default_value = "usb" 61 | )] 62 | transport: Transport, 63 | } 64 | 65 | struct CliProgress { 66 | pb: Option, 67 | last_description: String, 68 | } 69 | 70 | impl CliProgress { 71 | fn new() -> Self { 72 | Self { 73 | pb: None, 74 | last_description: String::new(), 75 | } 76 | } 77 | } 78 | 79 | impl axdl::DownloadProgress for CliProgress { 80 | fn is_cancelled(&self) -> bool { 81 | false 82 | } 83 | fn report_progress(&mut self, description: &str, progress: Option) { 84 | if let Some(progress) = progress { 85 | if self.pb.is_none() { 86 | let pb = indicatif::ProgressBar::new(100); 87 | pb.set_style( 88 | indicatif::ProgressStyle::with_template( 89 | "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}]", 90 | ) 91 | .unwrap() 92 | .progress_chars("#>-"), 93 | ); 94 | self.pb = Some(pb); 95 | } 96 | self.pb 97 | .as_ref() 98 | .unwrap() 99 | .set_position((progress * 100.0) as u64); 100 | } else { 101 | if let Some(pb) = self.pb.take() { 102 | pb.finish(); 103 | } 104 | tracing::info!("{}", description); 105 | } 106 | self.last_description = description.to_string(); 107 | } 108 | } 109 | 110 | fn main() -> anyhow::Result<()> { 111 | tracing_subscriber::fmt() 112 | .with_env_filter( 113 | tracing_subscriber::EnvFilter::builder() 114 | .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) 115 | .from_env_lossy(), 116 | ) 117 | .with_file(true) 118 | .with_line_number(true) 119 | .init(); 120 | 121 | // Parse command line arguments. 122 | let args: Args = ::parse(); 123 | 124 | // Open the specified image file and find the configuration XML file. 125 | let mut file = std::fs::File::open(&args.file)?; 126 | let config = DownloadConfig { 127 | exclude_rootfs: args.exclude_rootfs, 128 | }; 129 | 130 | let mut progress = CliProgress::new(); 131 | 132 | if args.wait_for_device { 133 | if let Some(timeout) = args.wait_for_device_timeout_secs { 134 | tracing::debug!( 135 | "Waiting for the device to be ready (timeout={}s)...", 136 | timeout 137 | ); 138 | progress.report_progress( 139 | &format!("Waiting for the device to be ready (timeout={}s)", timeout), 140 | None, 141 | ); 142 | } else { 143 | tracing::debug!("Waiting for the device to be ready..."); 144 | progress.report_progress("Waiting for the device to be ready", None); 145 | } 146 | } 147 | 148 | let wait_start = std::time::Instant::now(); 149 | let mut device = loop { 150 | let device: Option = match args.transport { 151 | Transport::Serial => axdl::transport::serial::SerialTransport::list_devices()? 152 | .iter() 153 | .next() 154 | .map(|path| axdl::transport::serial::SerialTransport::open_device(path).ok()) 155 | .flatten() 156 | .map(|device| { 157 | let device: DynDevice = Box::new(device); 158 | device 159 | }), 160 | Transport::Usb => axdl::transport::usb::UsbTransport::list_devices()? 161 | .iter() 162 | .next() 163 | .map(|path| axdl::transport::usb::UsbTransport::open_device(path).ok()) 164 | .flatten() 165 | .map(|device| { 166 | let device: DynDevice = Box::new(device); 167 | device 168 | }), 169 | }; 170 | 171 | if let Some(device) = device { 172 | break device; 173 | } 174 | 175 | if args.wait_for_device { 176 | if let Some(timeout) = args.wait_for_device_timeout_secs { 177 | if wait_start.elapsed() > Duration::from_secs(timeout) { 178 | return Err(anyhow::anyhow!("Timeout waiting for the device")); 179 | } 180 | } 181 | std::thread::sleep(Duration::from_secs(1)); 182 | } else { 183 | return Err(anyhow::anyhow!("Device not found")); 184 | } 185 | }; 186 | 187 | // Perform download 188 | download_image(&mut file, &mut device, &config, &mut progress)?; 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /axdl-gui/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ 3 | "--cfg=web_sys_unstable_apis", 4 | ] -------------------------------------------------------------------------------- /axdl-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axdl-gui" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | description = "Unofficial GUI image download tool for Axera SoCs" 9 | keywords = ["gui", "tool", "axera"] # Updated keywords to reflect GUI 10 | categories = ["graphical-user-interfaces"] # Updated category for GUI 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | axdl = { path = "../axdl", version = "0.1.1", default-features = false, features = ["webusb", "webserial"] } 15 | 16 | anyhow = { workspace = true, features = ["backtrace"] } 17 | clap = { workspace = true, features = ["derive"] } 18 | tracing = { workspace = true } 19 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 20 | indicatif = { workspace = true } 21 | slint = { version = "1.8.0" } 22 | getrandom = { version = "0.2.15", features = ["js"] } 23 | 24 | webusb-web = { workspace = true } 25 | wasm-bindgen-futures = { workspace = true} 26 | web-sys = { workspace = true, features = ["Usb", "UsbDevice", "UsbDeviceFilter", "Serial", "SerialPort", "SerialPortInfo", "SerialOptions", "SerialPortRequestOptions", "Blob", "File", "FileReaderSync"] } 27 | js-sys = { workspace = true } 28 | 29 | tracing-wasm = { workspace = true } 30 | rfd = { workspace = true, features = ["file-handle-inner"] } 31 | futures-io = { workspace = true } 32 | pin-project = { workspace = true } 33 | 34 | [build-dependencies] 35 | slint-build = "1.8.0" 36 | 37 | [target.'cfg(target_arch = "wasm32")'.dependencies] 38 | wasm-bindgen = { workspace = true } 39 | 40 | [lib] 41 | path = "src/main.rs" 42 | crate-type = ["cdylib", "rlib"] 43 | -------------------------------------------------------------------------------- /axdl-gui/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | slint_build::compile("ui/app-window.slint").expect("Slint build failed"); 3 | } 4 | -------------------------------------------------------------------------------- /axdl-gui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /axdl-gui/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 17 | 18 | use std::{cell::RefCell, mem::forget, rc::Rc, time::Duration}; 19 | 20 | use axdl::{ 21 | download_image, 22 | transport::{AsyncTransport, DynDevice, Transport as _}, 23 | AxdlError, DownloadConfig, DownloadProgress, 24 | }; 25 | use js_sys::wasm_bindgen::{self, JsCast}; 26 | use tracing_subscriber::layer::SubscriberExt; 27 | use tracing_wasm::WASMLayerConfig; 28 | 29 | slint::include_modules!(); 30 | 31 | struct GuiProgress { 32 | ui: slint::Weak, 33 | cancelled: bool, 34 | } 35 | 36 | impl GuiProgress { 37 | fn new(ui: slint::Weak) -> Self { 38 | Self { 39 | ui, 40 | cancelled: false, 41 | } 42 | } 43 | 44 | fn set_cancelled(&mut self, cancelled: bool) { 45 | self.cancelled = cancelled; 46 | } 47 | } 48 | 49 | impl axdl::DownloadProgress for GuiProgress { 50 | fn is_cancelled(&self) -> bool { 51 | self.cancelled 52 | } 53 | fn report_progress(&mut self, description: &str, progress: Option) { 54 | let ui = self.ui.clone(); 55 | let description = description.to_string(); 56 | let _ = slint::invoke_from_event_loop(move || { 57 | let ui = ui.unwrap(); 58 | let progress = progress.unwrap_or(-1.0); 59 | ui.invoke_set_progress(description.into(), progress); 60 | }); 61 | } 62 | } 63 | 64 | enum AxdlDevice { 65 | Serial(axdl::transport::webserial::WebSerialDevice), 66 | Usb(webusb_web::OpenUsbDevice), 67 | } 68 | 69 | impl axdl::transport::AsyncDevice for AxdlDevice { 70 | async fn read(&mut self, buf: &mut [u8]) -> Result { 71 | match self { 72 | AxdlDevice::Serial(device) => device.read(buf).await, 73 | AxdlDevice::Usb(device) => device.read(buf).await, 74 | } 75 | } 76 | 77 | async fn write(&mut self, buf: &[u8]) -> Result { 78 | match self { 79 | AxdlDevice::Serial(device) => device.write(buf).await, 80 | AxdlDevice::Usb(device) => device.write(buf).await, 81 | } 82 | } 83 | } 84 | 85 | #[pin_project::pin_project] 86 | struct BufReader { 87 | #[pin] 88 | reader: R, 89 | buf: Vec, 90 | pos: usize, 91 | filled: usize, 92 | } 93 | 94 | impl BufReader { 95 | fn new(reader: R, buffer_size: usize) -> Self { 96 | let mut buf = Vec::with_capacity(buffer_size); 97 | buf.resize(buffer_size, 0); 98 | Self { 99 | reader, 100 | buf, 101 | pos: 0, 102 | filled: 0, 103 | } 104 | } 105 | } 106 | 107 | impl futures_io::AsyncRead for BufReader { 108 | fn poll_read( 109 | mut self: std::pin::Pin<&mut Self>, 110 | cx: &mut std::task::Context<'_>, 111 | buf: &mut [u8], 112 | ) -> std::task::Poll> { 113 | let bytes_remaining = self.filled - self.pos; 114 | tracing::debug!( 115 | "poll_read size: {}, bytes_remaining: {}", 116 | buf.len(), 117 | bytes_remaining 118 | ); 119 | 120 | let this = self.project(); 121 | if bytes_remaining > 0 { 122 | let bytes_to_copy = bytes_remaining.min(buf.len()); 123 | buf[..bytes_to_copy].copy_from_slice(&this.buf[*this.pos..*this.pos + bytes_to_copy]); 124 | *this.pos += bytes_to_copy; 125 | if *this.pos == *this.filled { 126 | *this.filled = 0; 127 | *this.pos = 0; 128 | } 129 | return std::task::Poll::Ready(Ok(bytes_to_copy)); 130 | } 131 | this.reader.poll_read(cx, buf) 132 | } 133 | } 134 | 135 | impl futures_io::AsyncSeek for BufReader { 136 | fn poll_seek( 137 | self: std::pin::Pin<&mut Self>, 138 | cx: &mut std::task::Context<'_>, 139 | pos: std::io::SeekFrom, 140 | ) -> std::task::Poll> { 141 | tracing::debug!("poll_seek: {:?}", pos); 142 | let this = self.project(); 143 | *this.filled = 0; 144 | *this.pos = 0; 145 | 146 | this.reader.poll_seek(cx, pos) 147 | } 148 | } 149 | 150 | impl futures_io::AsyncBufRead for BufReader { 151 | fn poll_fill_buf( 152 | self: std::pin::Pin<&mut Self>, 153 | cx: &mut std::task::Context<'_>, 154 | ) -> std::task::Poll> { 155 | tracing::debug!("poll_fill_buf"); 156 | let bytes_remaining_in_buf = self.filled - self.pos; 157 | let mut this = self.project(); 158 | if bytes_remaining_in_buf > 0 { 159 | std::task::Poll::Ready(Ok(&this.buf[*this.pos..*this.filled])) 160 | } else { 161 | *this.filled = 0; 162 | *this.pos = 0; 163 | 164 | match this.reader.poll_read(cx, &mut this.buf) { 165 | std::task::Poll::Ready(Ok(bytes_read)) => { 166 | *this.filled = bytes_read; 167 | std::task::Poll::Ready(Ok(&this.buf[..*this.filled])) 168 | } 169 | std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Err(e)), 170 | std::task::Poll::Pending => std::task::Poll::Pending, 171 | } 172 | } 173 | } 174 | fn consume(self: std::pin::Pin<&mut Self>, amt: usize) { 175 | tracing::debug!("consume: {}", amt); 176 | let this = self.project(); 177 | *this.pos += amt; 178 | } 179 | } 180 | 181 | #[pin_project::pin_project] 182 | struct FileWrapper<'a> { 183 | reader: web_sys::FileReader, 184 | file: &'a web_sys::File, 185 | position: u64, 186 | slice: Option, 187 | } 188 | 189 | impl<'a> FileWrapper<'a> { 190 | fn new(file: &'a web_sys::File) -> Self { 191 | Self { 192 | reader: web_sys::FileReader::new().unwrap(), 193 | file, 194 | position: 0, 195 | slice: None, 196 | } 197 | } 198 | } 199 | 200 | impl<'a> futures_io::AsyncRead for FileWrapper<'a> { 201 | fn poll_read( 202 | self: std::pin::Pin<&mut Self>, 203 | cx: &mut std::task::Context<'_>, 204 | buf: &mut [u8], 205 | ) -> std::task::Poll> { 206 | let size = self.file.size() as u64; 207 | let remaining = size.saturating_sub(self.position); 208 | let bytes_to_read = remaining.min(buf.len() as u64); 209 | 210 | tracing::debug!( 211 | "poll_read position: {}, bytes_to_read: {}, remaining: {}", 212 | self.position, 213 | bytes_to_read, 214 | remaining 215 | ); 216 | 217 | if bytes_to_read == 0 { 218 | return std::task::Poll::Ready(Ok(0)); 219 | } 220 | 221 | let this = self.project(); 222 | if this.slice.is_some() { 223 | match this.reader.ready_state() { 224 | web_sys::FileReader::LOADING => { 225 | tracing::debug!("poll_read: LOADING"); 226 | return std::task::Poll::Pending; 227 | } 228 | web_sys::FileReader::DONE => { 229 | tracing::debug!("poll_read: DONE"); 230 | *this.slice = None; 231 | let result = this.reader.result().unwrap(); 232 | let array = js_sys::Uint8Array::new(&result); 233 | let bytes_read = array.length() as usize; 234 | let bytes_to_copy = bytes_read.min(buf.len()); 235 | array.copy_to(&mut buf[..bytes_to_copy]); 236 | tracing::debug!( 237 | "poll_read: DONE bytes_read {} bytes_to_copy {}", 238 | bytes_read, 239 | bytes_to_copy 240 | ); 241 | *this.position += bytes_to_copy as u64; 242 | return std::task::Poll::Ready(Ok(bytes_to_copy)); 243 | } 244 | _ => unreachable!(), 245 | } 246 | } 247 | 248 | tracing::debug!("poll_read: EMPTY"); 249 | 250 | let slice = this 251 | .file 252 | .slice_with_f64_and_f64( 253 | *this.position as f64, 254 | (*this.position + bytes_to_read) as f64, 255 | ) 256 | .map_err(|e| { 257 | std::io::Error::new( 258 | std::io::ErrorKind::InvalidData, 259 | format!("failed to create slice - {:?}", e), 260 | ) 261 | }); 262 | let slice = match slice { 263 | Ok(slice) => slice, 264 | Err(e) => return std::task::Poll::Ready(Err(e)), 265 | }; 266 | let waker = cx.waker().clone(); 267 | let wake_closure = wasm_bindgen::closure::Closure::wrap(Box::new(move || { 268 | //tracing::debug!("poll_read: onloadend"); 269 | waker.wake_by_ref(); 270 | }) as Box); 271 | this.reader 272 | .set_onloadend(Some(wake_closure.as_ref().unchecked_ref())); 273 | forget(wake_closure); 274 | 275 | if let Err(e) = this.reader.read_as_array_buffer(&slice) { 276 | return std::task::Poll::Ready(Err(std::io::Error::new( 277 | std::io::ErrorKind::InvalidData, 278 | format!("read error - {:?}", e), 279 | ))); 280 | } 281 | *this.slice = Some(slice); 282 | std::task::Poll::Pending 283 | } 284 | } 285 | 286 | impl<'a> futures_io::AsyncSeek for FileWrapper<'a> { 287 | fn poll_seek( 288 | mut self: std::pin::Pin<&mut Self>, 289 | cx: &mut std::task::Context<'_>, 290 | pos: std::io::SeekFrom, 291 | ) -> std::task::Poll> { 292 | let size = self.file.size() as u64; 293 | match pos { 294 | std::io::SeekFrom::Start(pos) => { 295 | self.position = pos.min(size); 296 | } 297 | std::io::SeekFrom::End(pos) => { 298 | self.position = size.saturating_add_signed(pos).min(size); 299 | } 300 | std::io::SeekFrom::Current(pos) => { 301 | self.position = self.position.saturating_add_signed(pos).min(size); 302 | } 303 | } 304 | std::task::Poll::Ready(Ok(self.position)) 305 | } 306 | } 307 | 308 | impl<'a> std::io::Read for FileWrapper<'a> { 309 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 310 | let size = self.file.size() as u64; 311 | let remaining = size.saturating_sub(self.position); 312 | let bytes_to_read = remaining.min(buf.len() as u64); 313 | 314 | if bytes_to_read == 0 { 315 | return Ok(0); 316 | } 317 | 318 | let slice = self 319 | .file 320 | .slice_with_f64_and_f64(self.position as f64, (self.position + bytes_to_read) as f64) 321 | .map_err(|e| { 322 | std::io::Error::new( 323 | std::io::ErrorKind::InvalidData, 324 | format!("failed to create slice - {:?}", e), 325 | ) 326 | })?; 327 | let reader = web_sys::FileReaderSync::new().map_err(|e| { 328 | std::io::Error::new( 329 | std::io::ErrorKind::Other, 330 | format!("failed to create FileReaderSync - {:?}", e), 331 | ) 332 | })?; 333 | let data = reader.read_as_array_buffer(&slice).map_err(|e| { 334 | std::io::Error::new( 335 | std::io::ErrorKind::InvalidData, 336 | format!("read error - {:?}", e), 337 | ) 338 | })?; 339 | let data = js_sys::Uint8Array::new(&data); 340 | let bytes_read = data.byte_length() as usize; 341 | let bytes_to_copy = bytes_read.min(buf.len()); 342 | data.copy_to(&mut buf[..bytes_to_copy]); 343 | 344 | self.position += bytes_to_copy as u64; 345 | Ok(bytes_to_copy) 346 | } 347 | } 348 | 349 | impl<'a> std::io::Seek for FileWrapper<'a> { 350 | fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { 351 | let size = self.file.size() as u64; 352 | let last = size.saturating_sub(1); 353 | match pos { 354 | std::io::SeekFrom::Start(pos) => { 355 | self.position = pos.min(last); 356 | } 357 | std::io::SeekFrom::End(pos) => { 358 | self.position = size.saturating_add_signed(pos).min(last); 359 | } 360 | std::io::SeekFrom::Current(pos) => { 361 | self.position = self.position.saturating_add_signed(pos).min(last); 362 | } 363 | } 364 | Ok(self.position) 365 | } 366 | } 367 | 368 | fn gui_main() -> Result<(), Box> { 369 | let tracing_layer = tracing_wasm::WASMLayer::new( 370 | tracing_wasm::WASMLayerConfigBuilder::default() 371 | .set_max_level(tracing::Level::INFO) 372 | .build(), 373 | ); 374 | let subscriber = tracing_subscriber::registry().with(tracing_layer); 375 | tracing::subscriber::set_global_default(subscriber).unwrap(); 376 | 377 | let usb = Rc::new(webusb_web::Usb::new().unwrap()); 378 | let serial = Rc::new(axdl::transport::webserial::new_serial().unwrap()); 379 | let axdl_device: Rc>> = Rc::new(RefCell::new(None)); 380 | let image_file = Rc::new(RefCell::new(None)); 381 | 382 | let ui = AppWindow::new()?; 383 | 384 | { 385 | let usb = usb.clone(); 386 | let axdl_device = axdl_device.clone(); 387 | let ui_handle = ui.as_weak(); 388 | ui.on_open_usb_device(move || { 389 | let usb = usb.clone(); 390 | let axdl_device = axdl_device.clone(); 391 | let ui = ui_handle.unwrap(); 392 | slint::spawn_local(async move { 393 | let result: Result<(), Box> = async { 394 | let device = usb 395 | .request_device([axdl::transport::webusb::axdl_device_filter()]) 396 | .await?; 397 | tracing::info!("Device selected: {:?}", device); 398 | let open_device = device.open().await?; 399 | tracing::info!("Device opened: {:?}", open_device); 400 | open_device.claim_interface(0).await?; 401 | axdl_device.replace(Some(AxdlDevice::Usb(open_device))); 402 | ui.set_device_opened(true); 403 | Ok(()) 404 | } 405 | .await; 406 | 407 | if let Err(e) = result { 408 | tracing::error!("Failed to open device: {:?}", e); 409 | ui.set_device_opened(false); 410 | } 411 | }); 412 | }); 413 | } 414 | 415 | { 416 | let serial = serial.clone(); 417 | let axdl_device = axdl_device.clone(); 418 | let ui_handle = ui.as_weak(); 419 | ui.on_open_serial_device(move || { 420 | let serial = serial.clone(); 421 | let axdl_device = axdl_device.clone(); 422 | let ui = ui_handle.unwrap(); 423 | slint::spawn_local(async move { 424 | let result: Result<(), Box> = async { 425 | let options = web_sys::SerialPortRequestOptions::new(); 426 | options.set_filters(&js_sys::Array::of1( 427 | &axdl::transport::webserial::axdl_device_filter(), 428 | )); 429 | let promise = serial.request_port_with_options(&options); 430 | let device = web_sys::SerialPort::from( 431 | wasm_bindgen_futures::JsFuture::from(promise) 432 | .await 433 | .map_err(AxdlError::WebSerialError)?, 434 | ); 435 | tracing::info!("Device selected: {:?}", device); 436 | let options = web_sys::SerialOptions::new(115200); 437 | options.set_buffer_size(48000); 438 | wasm_bindgen_futures::JsFuture::from(device.open(&options)) 439 | .await 440 | .map_err(AxdlError::WebSerialError)?; 441 | tracing::info!("Device opened: {:?}", device); 442 | axdl_device.replace(Some(AxdlDevice::Serial( 443 | axdl::transport::webserial::WebSerialDevice::new(device), 444 | ))); 445 | ui.set_device_opened(true); 446 | Ok(()) 447 | } 448 | .await; 449 | 450 | if let Err(e) = result { 451 | tracing::error!("Failed to open device: {:?}", e); 452 | ui.set_device_opened(false); 453 | } 454 | }); 455 | }); 456 | } 457 | 458 | { 459 | let ui_handle = ui.as_weak(); 460 | let image_file = image_file.clone(); 461 | ui.on_open_image(move || { 462 | let ui = ui_handle.unwrap(); 463 | let image_file = image_file.clone(); 464 | slint::spawn_local(async move { 465 | let result: Result<(), Box> = async { 466 | let file = rfd::AsyncFileDialog::new() 467 | .add_filter("AXDL Image", &["*.axp"]) 468 | .pick_file() 469 | .await 470 | .inspect(|path| { 471 | tracing::info!("Selected file: {}", path.file_name()); 472 | }); 473 | 474 | ui.set_image_file_opened(file.is_some()); 475 | ui.set_image_file( 476 | file.as_ref() 477 | .map(|f| f.file_name()) 478 | .unwrap_or_default() 479 | .into(), 480 | ); 481 | *image_file.borrow_mut() = file; 482 | Ok(()) 483 | } 484 | .await; 485 | 486 | if let Err(e) = result { 487 | tracing::error!("Failed to open image file: {:?}", e); 488 | ui.set_image_file_opened(false); 489 | } 490 | }); 491 | }); 492 | } 493 | 494 | { 495 | let ui_handle = ui.as_weak(); 496 | let image_file = image_file.clone(); 497 | let axdl_device = axdl_device.clone(); 498 | 499 | ui.on_download(move || { 500 | let ui_handle = ui_handle.clone(); 501 | let ui = ui_handle.unwrap(); 502 | if axdl_device.borrow().is_none() || image_file.borrow().is_none() { 503 | tracing::error!("Device or image file is not selected"); 504 | return; 505 | } 506 | 507 | let image_file = image_file.clone(); 508 | let axdl_device = axdl_device.clone(); 509 | 510 | ui.set_downloading(true); 511 | 512 | slint::spawn_local(async move { 513 | let result: Result<(), Box> = async { 514 | let mut progress = GuiProgress::new(ui_handle.clone()); 515 | let config = DownloadConfig { 516 | exclude_rootfs: ui.get_exclude_rootfs(), 517 | }; 518 | let image_file_ref = image_file.borrow(); 519 | let file = FileWrapper::new(image_file_ref.as_ref().unwrap().inner()); 520 | let mut buf_file = BufReader::new(file, 1048576); 521 | 522 | tracing::info!("Start downloading image file"); 523 | let result = axdl::download_image_async( 524 | &mut buf_file, 525 | axdl_device.borrow_mut().as_mut().unwrap(), 526 | &config, 527 | &mut progress, 528 | ) 529 | .await?; 530 | Ok(()) 531 | } 532 | .await; 533 | 534 | ui.set_downloading(false); 535 | 536 | if let Err(e) = result { 537 | tracing::error!("Failed to download image file: {:?}", e); 538 | ui.invoke_set_progress( 539 | format!("Failed to download image file: {:?}", e).into(), 540 | -1.0, 541 | ); 542 | } else { 543 | ui.invoke_set_progress("Done".into(), -1.0); 544 | } 545 | }); 546 | }); 547 | } 548 | 549 | ui.run()?; 550 | 551 | Ok(()) 552 | } 553 | 554 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen(start))] 555 | fn main() { 556 | gui_main().unwrap(); 557 | } 558 | -------------------------------------------------------------------------------- /axdl-gui/ui/app-window.slint: -------------------------------------------------------------------------------- 1 | import { Button, VerticalBox, HorizontalBox, ProgressIndicator, CheckBox, AboutSlint } from "std-widgets.slint"; 2 | 3 | export component AppWindow inherits Window { 4 | in-out property serial_port_supported: false; 5 | in-out property device_opened: false; 6 | in-out property image_file_opened: false; 7 | in-out property image_file; 8 | in-out property downloading: false; 9 | in-out property exclude_rootfs: false; 10 | in-out property description; 11 | in-out property show_progress; 12 | in-out property progress: -1.0; 13 | 14 | callback open-usb-device(); 15 | callback open-serial-device(); 16 | callback open-image(); 17 | callback download(); 18 | 19 | public function set_progress(description:string, progress: float) { 20 | root.description = description; 21 | root.progress = progress; 22 | root.show_progress = true; 23 | } 24 | 25 | public function clear_progress() { 26 | root.show_progress = false; 27 | } 28 | 29 | VerticalBox { 30 | HorizontalBox { 31 | VerticalBox { 32 | Text { 33 | text: "Device: \{root.device_opened ? "Opened" : "Closed"}"; 34 | } 35 | 36 | Button { 37 | text: "Open Device"; 38 | enabled: !root.downloading; 39 | clicked => { 40 | root.open-usb-device(); 41 | } 42 | } 43 | 44 | if root.serial_port_supported: Button { 45 | text: "Open Serial Device"; 46 | enabled: !root.downloading; 47 | clicked => { 48 | root.open-serial-device(); 49 | } 50 | } 51 | 52 | } 53 | 54 | VerticalBox { 55 | Text { 56 | text: root.image_file; 57 | } 58 | Button { 59 | text: "Open Image"; 60 | enabled: !root.downloading; 61 | clicked => { 62 | root.open-image(); 63 | } 64 | } 65 | CheckBox { 66 | text: "Exclude rootfs"; 67 | enabled: !root.downloading; 68 | checked <=> root.exclude_rootfs; 69 | } 70 | } 71 | 72 | Button { 73 | text: "Download"; 74 | enabled: root.device_opened && root.image_file_opened && !root.downloading; 75 | clicked => { 76 | root.download(); 77 | } 78 | } 79 | AboutSlint { 80 | width: 100px; 81 | } 82 | } 83 | if root.show_progress: VerticalBox { 84 | Text { 85 | text: root.description; 86 | } 87 | ProgressIndicator { 88 | visible: root.progress >= 0.0; 89 | width: 100%; 90 | height: 32px; 91 | progress: root.progress; 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /axdl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axdl" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | description = "Unofficial implementation of Axera SoC image download protocol" 9 | keywords = ["protocol", "axera"] 10 | categories = ["command-line-utilities"] 11 | readme = "../README.md" 12 | 13 | [features] 14 | 15 | default = ["usb", "serial"] 16 | 17 | usb = ["dep:rusb"] 18 | web = ["async", "dep:wasm-bindgen-futures", "dep:web-sys", "dep:js-sys"] 19 | webusb = ["web", "dep:webusb-web", "web-sys/Usb", "web-sys/UsbDevice", "web-sys/UsbDeviceFilter"] 20 | webserial = ["web", "web-sys/Serial", "web-sys/SerialPort", "web-sys/SerialPortInfo", "web-sys/SerialPortFilter", "web-sys/SerialOptions", "web-sys/ReadableStream", "web-sys/WritableStream", "dep:wasm-streams"] 21 | serial = ["dep:serialport"] 22 | async = ["dep:async_zip", "dep:futures-io", "dep:futures-util", "dep:pin-project", "dep:pin-utils"] 23 | 24 | [dependencies] 25 | bincode = { workspace = true } 26 | byteorder = { workspace = true } 27 | clap = { workspace = true, features = ["derive"] } 28 | hex = { workspace = true, features = ["serde"] } 29 | rusb = { workspace = true, optional = true } 30 | serde = { workspace = true, features = ["derive"] } 31 | serde-xml-rs = { workspace = true } 32 | serde_bytes = { workspace = true } 33 | serialport = { workspace = true, optional = true } 34 | thiserror = { workspace = true } 35 | tracing = { workspace = true } 36 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 37 | zip = { workspace = true, default-features = false, features = ["deflate"] } 38 | webusb-web = { workspace = true, optional = true } 39 | wasm-bindgen-futures = { workspace = true, optional = true } 40 | web-sys = { workspace = true, optional = true, features = ["Window", "Navigator"] } 41 | js-sys = { workspace = true, optional = true } 42 | pin-utils = { workspace = true, optional = true } 43 | wasm-streams = { workspace = true, optional = true} 44 | async_zip = { workspace = true, optional = true, default-features = false, features = ["full-wasm"] } 45 | futures-io = { workspace = true, optional = true } 46 | futures-util = { workspace = true, optional = true } 47 | pin-project = { workspace = true, optional = true} 48 | 49 | [dev-dependencies] 50 | hex-literal = { workspace = true } 51 | -------------------------------------------------------------------------------- /axdl/src/communication.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | use std::time::Duration; 17 | 18 | use crate::AxdlError; 19 | 20 | const HANDSHAKE_REQUEST: [u8; 3] = [0x3c, 0x3c, 0x3c]; 21 | pub const TIMEOUT: Duration = Duration::from_secs(10*60); 22 | pub const TIMEOUT_WRITE_IMAGE: Duration = TIMEOUT; 23 | 24 | pub fn wait_handshake( 25 | device: &mut crate::transport::DynDevice, 26 | expected_handshake: &str, 27 | ) -> Result<(), AxdlError> { 28 | device.write_timeout(&HANDSHAKE_REQUEST, TIMEOUT)?; 29 | let mut buf = [0u8; 64]; 30 | let length = device.read_timeout(&mut buf, TIMEOUT)?; 31 | 32 | tracing::debug!("received: {:02X?}", &buf[..length]); 33 | let view = crate::frame::AxdlFrameView::new(&buf[..length]); 34 | tracing::debug!( 35 | "view: {}, checksum={:04X}", 36 | view, 37 | view.calculate_checksum().unwrap_or(0) 38 | ); 39 | if !view.is_valid() { 40 | return Err(AxdlError::InvalidFrame); 41 | } 42 | let handshake = view 43 | .payload() 44 | .map(|payload| { 45 | std::str::from_utf8(payload) 46 | .map_err(AxdlError::HandshakeDecodeError) 47 | .map(|s| s.to_string()) 48 | }) 49 | .transpose()? 50 | .ok_or(AxdlError::NoPayload)?; 51 | 52 | tracing::debug!("handshake: {}", handshake); 53 | if !handshake.contains(expected_handshake) { 54 | return Err(AxdlError::UnexpectedHandshake(handshake)); 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub fn receive_response( 60 | device: &mut crate::transport::DynDevice, 61 | timeout: Duration, 62 | ) -> Result, AxdlError> { 63 | let mut buf = Vec::with_capacity(65536); 64 | buf.resize(buf.capacity(), 0); 65 | let length = device.read_timeout(&mut buf, timeout)?; 66 | 67 | tracing::debug!("received: {:02X?}", &buf[..length]); 68 | let view = crate::frame::AxdlFrameView::new(&buf[..length]); 69 | tracing::debug!( 70 | "view: {}, checksum={:04X}", 71 | view, 72 | view.calculate_checksum().unwrap_or(0) 73 | ); 74 | if !view.is_valid() { 75 | return Err(AxdlError::InvalidFrame); 76 | } 77 | 78 | buf.resize(length, 0); 79 | Ok(buf) 80 | } 81 | 82 | pub fn start_ram_download(device: &mut crate::transport::DynDevice) -> Result<(), AxdlError> { 83 | tracing::debug!("start_ram_download"); 84 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 85 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 86 | frame.init(); 87 | frame.finalize(); 88 | 89 | device.write_timeout(&buf, TIMEOUT)?; 90 | 91 | let response = receive_response(device, TIMEOUT)?; 92 | let response_view = crate::frame::AxdlFrameView::new(&response); 93 | if response_view.command_response() != Some(0x0080) { 94 | return Err(AxdlError::UnexpectedResponse( 95 | response_view.command_response().unwrap(), 96 | )); 97 | } 98 | Ok(()) 99 | } 100 | 101 | pub fn start_partition_absolute_32( 102 | device: &mut crate::transport::DynDevice, 103 | start_address: u32, 104 | partition_length: u32, 105 | ) -> Result<(), AxdlError> { 106 | tracing::debug!( 107 | "start_partition_absolute: start_address={:#X}, partition_length={}", 108 | start_address, 109 | partition_length 110 | ); 111 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 8]; 112 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 113 | frame.init(); 114 | frame.set_command_response(0x0001); // Start partition 115 | { 116 | let payload = frame.payload_mut(); 117 | payload[0..4].copy_from_slice(&start_address.to_le_bytes()); 118 | payload[4..8].copy_from_slice(&partition_length.to_le_bytes()); 119 | } 120 | frame.finalize(); 121 | 122 | device.write_timeout(&buf, TIMEOUT)?; 123 | 124 | let response = receive_response(device, TIMEOUT)?; 125 | let response_view = crate::frame::AxdlFrameView::new(&response); 126 | if response_view.command_response() != Some(0x0080) { 127 | return Err(AxdlError::UnexpectedResponse( 128 | response_view.command_response().unwrap(), 129 | )); 130 | } 131 | Ok(()) 132 | } 133 | 134 | pub fn start_partition_absolute( 135 | device: &mut crate::transport::DynDevice, 136 | start_address: u64, 137 | partition_length: u64, 138 | ) -> Result<(), AxdlError> { 139 | tracing::debug!( 140 | "start_partition_absolute: start_address={:#X}, partition_length={}", 141 | start_address, 142 | partition_length 143 | ); 144 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 16]; 145 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 146 | frame.init(); 147 | frame.set_command_response(0x0001); // Start partition 148 | { 149 | let payload = frame.payload_mut(); 150 | payload[0..8].copy_from_slice(&start_address.to_le_bytes()); 151 | payload[8..16].copy_from_slice(&partition_length.to_le_bytes()); 152 | } 153 | frame.finalize(); 154 | 155 | device.write_timeout(&buf, TIMEOUT)?; 156 | 157 | let response = receive_response(device, TIMEOUT)?; 158 | let response_view = crate::frame::AxdlFrameView::new(&response); 159 | if response_view.command_response() != Some(0x0080) { 160 | return Err(AxdlError::UnexpectedResponse( 161 | response_view.command_response().unwrap(), 162 | )); 163 | } 164 | Ok(()) 165 | } 166 | 167 | pub fn start_partition_id( 168 | device: &mut crate::transport::DynDevice, 169 | partition_name: &str, 170 | total_length: u64, 171 | ) -> Result<(), AxdlError> { 172 | tracing::debug!( 173 | "start_partition_id: partition_name={}, total_length={}", 174 | partition_name, 175 | total_length 176 | ); 177 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 88]; 178 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 179 | frame.init(); 180 | frame.set_command_response(0x0001); // Start partition 181 | { 182 | let payload = frame.payload_mut(); 183 | let partition_name_bytes = partition_name 184 | .encode_utf16() 185 | .map(|c| c.to_le_bytes()) 186 | .flatten() 187 | .collect::>(); 188 | payload[0..partition_name_bytes.len()].copy_from_slice(&partition_name_bytes); 189 | payload[72..80].copy_from_slice(&total_length.to_le_bytes()); 190 | } 191 | frame.finalize(); 192 | 193 | device.write_timeout(&buf, TIMEOUT)?; 194 | 195 | let response = receive_response(device, TIMEOUT)?; 196 | let response_view = crate::frame::AxdlFrameView::new(&response); 197 | if response_view.command_response() != Some(0x0080) { 198 | return Err(AxdlError::UnexpectedResponse( 199 | response_view.command_response().unwrap(), 200 | )); 201 | } 202 | Ok(()) 203 | } 204 | 205 | pub fn start_block( 206 | device: &mut crate::transport::DynDevice, 207 | block_size: u16, 208 | ) -> Result<(), AxdlError> { 209 | tracing::debug!("start_block: block_size={}", block_size); 210 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 12]; 211 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 212 | frame.init(); 213 | frame.set_command_response(0x0002); // Start block 214 | { 215 | let payload = frame.payload_mut(); 216 | payload[0..2].copy_from_slice(&block_size.to_le_bytes()); 217 | } 218 | frame.finalize(); 219 | 220 | device.write_timeout(&buf, TIMEOUT)?; 221 | 222 | let response = receive_response(device, TIMEOUT)?; 223 | let response_view = crate::frame::AxdlFrameView::new(&response); 224 | if response_view.command_response() != Some(0x0080) { 225 | return Err(AxdlError::UnexpectedResponse( 226 | response_view.command_response().unwrap(), 227 | )); 228 | } 229 | Ok(()) 230 | } 231 | 232 | pub fn end_partition( 233 | device: &mut crate::transport::DynDevice, 234 | timeout: Duration, 235 | ) -> Result<(), AxdlError> { 236 | tracing::debug!("end_partition"); 237 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 238 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 239 | frame.init(); 240 | frame.set_command_response(0x0003); // End partition 241 | frame.finalize(); 242 | 243 | device.write_timeout(&buf, timeout)?; 244 | 245 | let response = receive_response(device, timeout)?; 246 | let response_view = crate::frame::AxdlFrameView::new(&response); 247 | if response_view.command_response() != Some(0x0080) { 248 | return Err(AxdlError::UnexpectedResponse( 249 | response_view.command_response().unwrap(), 250 | )); 251 | } 252 | Ok(()) 253 | } 254 | 255 | pub fn end_ram_download(device: &mut crate::transport::DynDevice) -> Result<(), AxdlError> { 256 | tracing::debug!("end_ram_download"); 257 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 258 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 259 | frame.init(); 260 | frame.set_command_response(0x0004); // End RAM download 261 | frame.finalize(); 262 | 263 | device.write_timeout(&buf, TIMEOUT)?; 264 | 265 | let response = receive_response(device, TIMEOUT)?; 266 | let response_view = crate::frame::AxdlFrameView::new(&response); 267 | if response_view.command_response() != Some(0x0080) { 268 | return Err(AxdlError::UnexpectedResponse( 269 | response_view.command_response().unwrap(), 270 | )); 271 | } 272 | Ok(()) 273 | } 274 | 275 | pub fn set_partition_table( 276 | device: &mut crate::transport::DynDevice, 277 | partition_table: &crate::partition::PartitionTable, 278 | ) -> Result<(), AxdlError> { 279 | tracing::debug!("set_partition_table: {:?}", partition_table); 280 | let partition_table_image = partition_table.to_bytes(); 281 | let mut buf = Vec::with_capacity(crate::frame::MINIMUM_LENGTH + partition_table_image.len()); 282 | buf.resize( 283 | crate::frame::MINIMUM_LENGTH + partition_table_image.len(), 284 | 0, 285 | ); 286 | 287 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 288 | frame.init(); 289 | frame.set_command_response(0x000b); // Set partition table 290 | { 291 | let payload = frame.payload_mut(); 292 | payload.copy_from_slice(&partition_table_image); 293 | } 294 | frame.finalize(); 295 | 296 | device.write_timeout(&buf, TIMEOUT)?; 297 | 298 | let response = receive_response(device, TIMEOUT)?; 299 | let response_view = crate::frame::AxdlFrameView::new(&response); 300 | if response_view.command_response() != Some(0x0080) { 301 | return Err(AxdlError::UnexpectedResponse( 302 | response_view.command_response().unwrap(), 303 | )); 304 | } 305 | Ok(()) 306 | } 307 | 308 | pub fn write_image( 309 | device: &mut crate::transport::DynDevice, 310 | reader: &mut R, 311 | chunk_size: usize, 312 | image_name: &str, 313 | image_size: usize, 314 | report_every: Option, 315 | progress: &mut impl crate::DownloadProgress, 316 | ) -> Result<(), AxdlError> { 317 | let mut buffer = Vec::with_capacity(chunk_size); 318 | buffer.resize(chunk_size, 0); 319 | 320 | let mut report_every_counter = 0; 321 | let mut bytes_transferred: usize = 0; 322 | loop { 323 | progress.check_is_cancelled()?; 324 | 325 | let bytes_read = reader 326 | .read(&mut buffer) 327 | .map_err(|e| AxdlError::IoError("read error".to_string(), e))?; 328 | if bytes_read == 0 { 329 | break; 330 | } 331 | let chunk = &buffer[..bytes_read]; 332 | start_block(device, chunk.len() as u16)?; 333 | device.write_timeout(chunk, TIMEOUT_WRITE_IMAGE)?; 334 | let response = receive_response(device, TIMEOUT_WRITE_IMAGE)?; 335 | let response_view = crate::frame::AxdlFrameView::new(&response); 336 | if response_view.command_response() != Some(0x0080) { 337 | return Err(AxdlError::UnexpectedResponse( 338 | response_view.command_response().unwrap(), 339 | )); 340 | } 341 | bytes_transferred += chunk.len(); 342 | if let Some(report_every) = report_every { 343 | report_every_counter += 1; 344 | if report_every_counter >= report_every { 345 | report_every_counter = 0; 346 | tracing::debug!("{}/{} bytes sent", bytes_transferred, image_size); 347 | progress.report_progress( 348 | &format!("Downloading image {}", image_name), 349 | Some(bytes_transferred as f32 / image_size as f32), 350 | ); 351 | } 352 | } 353 | } 354 | Ok(()) 355 | } 356 | 357 | #[cfg(feature = "async")] 358 | pub mod r#async { 359 | use crate::{communication::HANDSHAKE_REQUEST, transport::AsyncDevice, AxdlError}; 360 | 361 | pub async fn wait_handshake( 362 | device: &mut D, 363 | expected_handshake: &str, 364 | ) -> Result<(), AxdlError> { 365 | device.write(&HANDSHAKE_REQUEST).await?; 366 | let mut buf = [0u8; 64]; 367 | let length = device.read(&mut buf).await?; 368 | 369 | tracing::debug!("received: {:02X?}", &buf[..length]); 370 | let view = crate::frame::AxdlFrameView::new(&buf[..length]); 371 | tracing::debug!( 372 | "view: {}, checksum={:04X}", 373 | view, 374 | view.calculate_checksum().unwrap_or(0) 375 | ); 376 | if !view.is_valid() { 377 | return Err(AxdlError::InvalidFrame); 378 | } 379 | let handshake = view 380 | .payload() 381 | .map(|payload| { 382 | std::str::from_utf8(payload) 383 | .map_err(AxdlError::HandshakeDecodeError) 384 | .map(|s| s.to_string()) 385 | }) 386 | .transpose()? 387 | .ok_or(AxdlError::NoPayload)?; 388 | 389 | tracing::debug!("handshake: {}", handshake); 390 | if !handshake.contains(expected_handshake) { 391 | return Err(AxdlError::UnexpectedHandshake(handshake)); 392 | } 393 | Ok(()) 394 | } 395 | 396 | pub async fn receive_response( 397 | device: &mut D, 398 | ) -> Result, AxdlError> { 399 | let mut buf = Vec::with_capacity(65536); 400 | buf.resize(buf.capacity(), 0); 401 | let length = device.read(&mut buf).await?; 402 | 403 | tracing::debug!("received: {:02X?}", &buf[..length]); 404 | let view = crate::frame::AxdlFrameView::new(&buf[..length]); 405 | tracing::debug!( 406 | "view: {}, checksum={:04X}", 407 | view, 408 | view.calculate_checksum().unwrap_or(0) 409 | ); 410 | if !view.is_valid() { 411 | return Err(AxdlError::InvalidFrame); 412 | } 413 | 414 | buf.resize(length, 0); 415 | Ok(buf) 416 | } 417 | 418 | pub async fn start_ram_download(device: &mut D) -> Result<(), AxdlError> { 419 | tracing::debug!("start_ram_download"); 420 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 421 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 422 | frame.init(); 423 | frame.finalize(); 424 | 425 | device.write(&buf).await?; 426 | 427 | let response = receive_response(device).await?; 428 | let response_view = crate::frame::AxdlFrameView::new(&response); 429 | if response_view.command_response() != Some(0x0080) { 430 | return Err(AxdlError::UnexpectedResponse( 431 | response_view.command_response().unwrap(), 432 | )); 433 | } 434 | Ok(()) 435 | } 436 | 437 | pub async fn start_partition_absolute_32( 438 | device: &mut D, 439 | start_address: u32, 440 | partition_length: u32, 441 | ) -> Result<(), AxdlError> { 442 | tracing::debug!( 443 | "start_partition_absolute: start_address={:#X}, partition_length={}", 444 | start_address, 445 | partition_length 446 | ); 447 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 8]; 448 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 449 | frame.init(); 450 | frame.set_command_response(0x0001); // Start partition 451 | { 452 | let payload = frame.payload_mut(); 453 | payload[0..4].copy_from_slice(&start_address.to_le_bytes()); 454 | payload[4..8].copy_from_slice(&partition_length.to_le_bytes()); 455 | } 456 | frame.finalize(); 457 | 458 | device.write(&buf).await?; 459 | 460 | let response = receive_response(device).await?; 461 | let response_view = crate::frame::AxdlFrameView::new(&response); 462 | if response_view.command_response() != Some(0x0080) { 463 | return Err(AxdlError::UnexpectedResponse( 464 | response_view.command_response().unwrap(), 465 | )); 466 | } 467 | Ok(()) 468 | } 469 | 470 | pub async fn start_partition_absolute( 471 | device: &mut D, 472 | start_address: u64, 473 | partition_length: u64, 474 | ) -> Result<(), AxdlError> { 475 | tracing::debug!( 476 | "start_partition_absolute: start_address={:#X}, partition_length={}", 477 | start_address, 478 | partition_length 479 | ); 480 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 16]; 481 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 482 | frame.init(); 483 | frame.set_command_response(0x0001); // Start partition 484 | { 485 | let payload = frame.payload_mut(); 486 | payload[0..8].copy_from_slice(&start_address.to_le_bytes()); 487 | payload[8..16].copy_from_slice(&partition_length.to_le_bytes()); 488 | } 489 | frame.finalize(); 490 | 491 | device.write(&buf).await?; 492 | 493 | let response = receive_response(device).await?; 494 | let response_view = crate::frame::AxdlFrameView::new(&response); 495 | if response_view.command_response() != Some(0x0080) { 496 | return Err(AxdlError::UnexpectedResponse( 497 | response_view.command_response().unwrap(), 498 | )); 499 | } 500 | Ok(()) 501 | } 502 | 503 | pub async fn start_partition_id( 504 | device: &mut D, 505 | partition_name: &str, 506 | total_length: u64, 507 | ) -> Result<(), AxdlError> { 508 | tracing::debug!( 509 | "start_partition_id: partition_name={}, total_length={}", 510 | partition_name, 511 | total_length 512 | ); 513 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 88]; 514 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 515 | frame.init(); 516 | frame.set_command_response(0x0001); // Start partition 517 | { 518 | let payload = frame.payload_mut(); 519 | let partition_name_bytes = partition_name 520 | .encode_utf16() 521 | .map(|c| c.to_le_bytes()) 522 | .flatten() 523 | .collect::>(); 524 | payload[0..partition_name_bytes.len()].copy_from_slice(&partition_name_bytes); 525 | payload[72..80].copy_from_slice(&total_length.to_le_bytes()); 526 | } 527 | frame.finalize(); 528 | 529 | device.write(&buf).await?; 530 | 531 | let response = receive_response(device).await?; 532 | let response_view = crate::frame::AxdlFrameView::new(&response); 533 | if response_view.command_response() != Some(0x0080) { 534 | return Err(AxdlError::UnexpectedResponse( 535 | response_view.command_response().unwrap(), 536 | )); 537 | } 538 | Ok(()) 539 | } 540 | 541 | pub async fn start_block( 542 | device: &mut D, 543 | block_size: u16, 544 | ) -> Result<(), AxdlError> { 545 | tracing::debug!("start_block: block_size={}", block_size); 546 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH + 12]; 547 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 548 | frame.init(); 549 | frame.set_command_response(0x0002); // Start block 550 | { 551 | let payload = frame.payload_mut(); 552 | payload[0..2].copy_from_slice(&block_size.to_le_bytes()); 553 | } 554 | frame.finalize(); 555 | 556 | device.write(&buf).await?; 557 | 558 | let response = receive_response(device).await?; 559 | let response_view = crate::frame::AxdlFrameView::new(&response); 560 | if response_view.command_response() != Some(0x0080) { 561 | return Err(AxdlError::UnexpectedResponse( 562 | response_view.command_response().unwrap(), 563 | )); 564 | } 565 | Ok(()) 566 | } 567 | 568 | pub async fn end_partition( 569 | device: &mut D, 570 | ) -> Result<(), AxdlError> { 571 | tracing::debug!("end_partition"); 572 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 573 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 574 | frame.init(); 575 | frame.set_command_response(0x0003); // End partition 576 | frame.finalize(); 577 | 578 | device.write(&buf).await?; 579 | 580 | let response = receive_response(device).await?; 581 | let response_view = crate::frame::AxdlFrameView::new(&response); 582 | if response_view.command_response() != Some(0x0080) { 583 | return Err(AxdlError::UnexpectedResponse( 584 | response_view.command_response().unwrap(), 585 | )); 586 | } 587 | Ok(()) 588 | } 589 | 590 | pub async fn end_ram_download( 591 | device: &mut D, 592 | ) -> Result<(), AxdlError> { 593 | tracing::debug!("end_ram_download"); 594 | let mut buf = [0u8; crate::frame::MINIMUM_LENGTH]; 595 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 596 | frame.init(); 597 | frame.set_command_response(0x0004); // End RAM download 598 | frame.finalize(); 599 | 600 | device.write(&buf).await?; 601 | 602 | let response = receive_response(device).await?; 603 | let response_view = crate::frame::AxdlFrameView::new(&response); 604 | if response_view.command_response() != Some(0x0080) { 605 | return Err(AxdlError::UnexpectedResponse( 606 | response_view.command_response().unwrap(), 607 | )); 608 | } 609 | Ok(()) 610 | } 611 | 612 | pub async fn set_partition_table( 613 | device: &mut D, 614 | partition_table: &crate::partition::PartitionTable, 615 | ) -> Result<(), AxdlError> { 616 | tracing::debug!("set_partition_table: {:?}", partition_table); 617 | let partition_table_image = partition_table.to_bytes(); 618 | let mut buf = 619 | Vec::with_capacity(crate::frame::MINIMUM_LENGTH + partition_table_image.len()); 620 | buf.resize( 621 | crate::frame::MINIMUM_LENGTH + partition_table_image.len(), 622 | 0, 623 | ); 624 | 625 | let mut frame = crate::frame::AxdlFrameViewMut::new(&mut buf); 626 | frame.init(); 627 | frame.set_command_response(0x000b); // Set partition table 628 | { 629 | let payload = frame.payload_mut(); 630 | payload.copy_from_slice(&partition_table_image); 631 | } 632 | frame.finalize(); 633 | 634 | device.write(&buf).await?; 635 | 636 | let response = receive_response(device).await?; 637 | let response_view = crate::frame::AxdlFrameView::new(&response); 638 | if response_view.command_response() != Some(0x0080) { 639 | return Err(AxdlError::UnexpectedResponse( 640 | response_view.command_response().unwrap(), 641 | )); 642 | } 643 | Ok(()) 644 | } 645 | 646 | pub async fn write_image( 647 | device: &mut D, 648 | reader: &mut R, 649 | chunk_size: usize, 650 | image_name: &str, 651 | image_size: usize, 652 | report_every: Option, 653 | progress: &mut impl crate::DownloadProgress, 654 | ) -> Result<(), AxdlError> { 655 | use futures_util::io::AsyncReadExt; 656 | 657 | let mut buffer = Vec::with_capacity(chunk_size); 658 | buffer.resize(chunk_size, 0); 659 | 660 | let mut report_every_counter = 0; 661 | let mut bytes_transferred: usize = 0; 662 | loop { 663 | progress.check_is_cancelled()?; 664 | 665 | let bytes_read = reader 666 | .read(&mut buffer) 667 | .await 668 | .map_err(|e| AxdlError::IoError("read error".to_string(), e))?; 669 | if bytes_read == 0 { 670 | break; 671 | } 672 | let chunk = &buffer[..bytes_read]; 673 | start_block(device, chunk.len() as u16).await?; 674 | let bytes_written = device.write(chunk).await?; 675 | if bytes_written != chunk.len() { 676 | return Err(AxdlError::IoError( 677 | "write error".to_string(), 678 | std::io::Error::new(std::io::ErrorKind::Other, "short write for data packet"), 679 | )); 680 | } 681 | let response = receive_response(device).await?; 682 | let response_view = crate::frame::AxdlFrameView::new(&response); 683 | if response_view.command_response() != Some(0x0080) { 684 | return Err(AxdlError::UnexpectedResponse( 685 | response_view.command_response().unwrap(), 686 | )); 687 | } 688 | bytes_transferred += chunk.len(); 689 | if let Some(report_every) = report_every { 690 | report_every_counter += 1; 691 | if report_every_counter >= report_every { 692 | report_every_counter = 0; 693 | tracing::debug!("{}/{} bytes sent", bytes_transferred, image_size); 694 | progress.report_progress( 695 | &format!("Downloading image {}", image_name), 696 | Some(bytes_transferred as f32 / image_size as f32), 697 | ); 698 | } 699 | } 700 | } 701 | Ok(()) 702 | } 703 | } 704 | -------------------------------------------------------------------------------- /axdl/src/frame.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | use serde::{Deserialize, Serialize}; 17 | use serde_bytes::ByteBuf; 18 | use thiserror::Error; 19 | 20 | /// USBフレーム構造体 21 | #[derive(Debug, Serialize, Deserialize, Clone)] 22 | pub struct UsbFrame { 23 | pub raw: ByteBuf, 24 | } 25 | 26 | #[derive(Debug, Error)] 27 | pub enum UsbFrameError { 28 | #[error("Invalid signature")] 29 | Signature, 30 | #[error("Invalid frame length")] 31 | Length, 32 | #[error("Invalid checksum")] 33 | Checksum, 34 | } 35 | 36 | pub const MINIMUM_LENGTH: usize = 4 + 2 + 2 + 2; // signature + length + command_response + checksum 37 | pub const SIGNATURE: u32 = 0x5c6d8e9f; 38 | 39 | #[derive(Debug)] 40 | pub struct AxdlFrameView<'a> { 41 | data: &'a [u8], 42 | } 43 | 44 | impl<'a> std::fmt::Display for AxdlFrameView<'a> { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | write!( 47 | f, 48 | "AxdlFrameView({:08X}, {}, {:04X}, {:02X?}, {:04X})", 49 | self.signature().unwrap_or(0), 50 | self.length().unwrap_or(0), 51 | self.command_response().unwrap_or(0), 52 | self.payload().unwrap_or(&[]), 53 | self.checksum().unwrap_or(0) 54 | ) 55 | } 56 | } 57 | 58 | impl<'a> AxdlFrameView<'a> { 59 | pub fn new(data: &'a [u8]) -> Self { 60 | Self { data } 61 | } 62 | 63 | pub fn signature(&self) -> Option { 64 | if self.data.len() < 4 { 65 | return None; 66 | } 67 | Some(u32::from_le_bytes([ 68 | self.data[0], 69 | self.data[1], 70 | self.data[2], 71 | self.data[3], 72 | ])) 73 | } 74 | 75 | pub fn length(&self) -> Option { 76 | if self.data.len() < 4 + 2 { 77 | return None; 78 | } 79 | Some(u16::from_le_bytes([self.data[4], self.data[5]])) 80 | } 81 | 82 | pub fn command_response(&self) -> Option { 83 | if self.data.len() < 4 + 2 + 2 { 84 | return None; 85 | } 86 | Some(u16::from_le_bytes([self.data[6], self.data[7]])) 87 | } 88 | 89 | pub fn payload(&self) -> Option<&[u8]> { 90 | let payload_length = self.length()? as usize; 91 | 92 | if self.data.len() < 4 + 2 + 2 + payload_length + 2 { 93 | return None; 94 | } 95 | 96 | Some(&self.data[4 + 2 + 2..4 + 2 + 2 + payload_length]) 97 | } 98 | 99 | pub fn payload_unchecked(&self) -> Option<&[u8]> { 100 | if self.data.len() < 4 + 2 + 2 { 101 | return None; 102 | } 103 | Some(&self.data[4 + 2 + 2..self.data.len() - 2]) 104 | } 105 | 106 | pub fn checksum(&self) -> Option { 107 | if self.data.len() < 4 + 2 + 2 + 2 { 108 | return None; 109 | } 110 | Some(u16::from_le_bytes([ 111 | self.data[self.data.len() - 2], 112 | self.data[self.data.len() - 1], 113 | ])) 114 | } 115 | 116 | fn ones_complement_add(lhs: u16, rhs: u16) -> u16 { 117 | let mut sum = lhs as u32 + rhs as u32; 118 | 119 | while sum > 0xffff { 120 | sum = (sum & 0xffff) + (sum >> 16); 121 | } 122 | sum as u16 123 | } 124 | pub fn calculate_checksum(&self) -> Option { 125 | let payload = if let Some(payload) = self.payload() { 126 | payload 127 | } else { 128 | return None; 129 | }; 130 | 131 | let length = self.length().unwrap(); 132 | let command_response = self.command_response().unwrap(); 133 | let mut checksum = self.checksum().unwrap(); 134 | checksum = Self::ones_complement_add(checksum, length); 135 | checksum = Self::ones_complement_add(checksum, command_response); 136 | for i in 0..payload.len() / 2 { 137 | let value = u16::from_le_bytes([payload[i * 2], payload[i * 2 + 1]]); 138 | checksum = Self::ones_complement_add(checksum, value); 139 | } 140 | if payload.len() % 2 == 1 { 141 | checksum = Self::ones_complement_add( 142 | checksum, 143 | u16::from_le_bytes([payload[payload.len() - 1], 0]), 144 | ); 145 | } 146 | 147 | Some(checksum) 148 | } 149 | 150 | pub fn verify_checksum(&self) -> bool { 151 | self.calculate_checksum() 152 | .map(|checksum| checksum == 0xffff) 153 | .unwrap_or(false) 154 | } 155 | 156 | pub fn is_valid(&self) -> bool { 157 | self.signature() == Some(SIGNATURE) && self.verify_checksum() 158 | } 159 | } 160 | 161 | pub struct AxdlFrameViewMut<'a> { 162 | buffer: &'a mut [u8], 163 | } 164 | 165 | impl<'a> AxdlFrameViewMut<'a> { 166 | pub fn new(buffer: &'a mut [u8]) -> Self { 167 | Self { buffer } 168 | } 169 | 170 | pub fn init(&mut self) -> &mut Self { 171 | let length = self.buffer.len(); 172 | self.set_signature(SIGNATURE); 173 | self.set_length((length - MINIMUM_LENGTH) as u16); 174 | 175 | self 176 | } 177 | 178 | pub fn signature(&self) -> u32 { 179 | AxdlFrameView::new(self.buffer).signature().unwrap() 180 | } 181 | 182 | pub fn length(&self) -> u16 { 183 | AxdlFrameView::new(self.buffer).length().unwrap() 184 | } 185 | 186 | pub fn command_response(&self) -> u16 { 187 | AxdlFrameView::new(self.buffer).command_response().unwrap() 188 | } 189 | 190 | pub fn checksum(&self) -> u16 { 191 | AxdlFrameView::new(self.buffer).checksum().unwrap() 192 | } 193 | 194 | pub fn set_signature(&mut self, signature: u32) -> &mut Self { 195 | self.buffer[0] = (signature & 0xff) as u8; 196 | self.buffer[1] = ((signature >> 8) & 0xff) as u8; 197 | self.buffer[2] = ((signature >> 16) & 0xff) as u8; 198 | self.buffer[3] = ((signature >> 24) & 0xff) as u8; 199 | self 200 | } 201 | pub fn set_length(&mut self, length: u16) -> &mut Self { 202 | assert!(length as usize + 4 + 2 + 2 + 2 <= self.buffer.len()); 203 | 204 | self.buffer[4] = (length & 0xff) as u8; 205 | self.buffer[5] = ((length >> 8) & 0xff) as u8; 206 | 207 | self 208 | } 209 | pub fn set_command_response(&mut self, command_response: u16) -> &mut Self { 210 | self.buffer[6] = (command_response & 0xff) as u8; 211 | self.buffer[7] = ((command_response >> 8) & 0xff) as u8; 212 | 213 | self 214 | } 215 | 216 | pub fn payload_mut(&mut self) -> &mut [u8] { 217 | let length = self.length() as usize; 218 | &mut self.buffer[4 + 2 + 2..4 + 2 + 2 + length] 219 | } 220 | 221 | pub fn set_checksum(&mut self, checksum: u16) -> &mut Self { 222 | let length = self.length() as usize; 223 | self.buffer[4 + 2 + 2 + length + 0] = (checksum & 0xff) as u8; 224 | self.buffer[4 + 2 + 2 + length + 1] = (checksum >> 8) as u8; 225 | 226 | self 227 | } 228 | 229 | pub fn finalize(mut self) { 230 | self.set_checksum(0); 231 | let checksum = AxdlFrameView::new(self.buffer) 232 | .calculate_checksum() 233 | .unwrap(); 234 | self.set_checksum(!checksum); 235 | } 236 | } 237 | 238 | #[cfg(test)] 239 | mod test { 240 | use super::*; 241 | 242 | #[test] 243 | fn test_axdl_frame_view_empty() { 244 | let data = hex_literal::hex!("9f 8e 6d 5c 00 00 01 00 fe ff"); 245 | let view = AxdlFrameView::new(&data); 246 | assert_eq!(view.signature(), Some(SIGNATURE)); 247 | assert_eq!(view.length(), Some(0)); 248 | assert_eq!(view.command_response(), Some(0x0001)); 249 | assert_eq!(view.payload(), Some(&data[4 + 2 + 2..4 + 2 + 2 as usize])); 250 | assert_eq!(view.checksum(), Some(0xfffe)); 251 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 252 | assert_eq!(view.verify_checksum(), true); 253 | assert_eq!(view.is_valid(), true); 254 | } 255 | 256 | #[test] 257 | fn test_axdl_frame_view_command_1() { 258 | let data = hex_literal::hex!("9f 8e 6d 5c 08 00 01 00 00 00 00 03 00 68 01 00 f5 94"); 259 | let view = AxdlFrameView::new(&data); 260 | assert_eq!(view.signature(), Some(SIGNATURE)); 261 | assert_eq!(view.length(), Some(8)); 262 | assert_eq!(view.command_response(), Some(0x0001)); 263 | assert_eq!( 264 | view.payload(), 265 | Some(&data[4 + 2 + 2..4 + 2 + 8 + 2 as usize]) 266 | ); 267 | assert_eq!(view.checksum(), Some(0x94f5)); 268 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 269 | assert_eq!(view.verify_checksum(), true); 270 | assert_eq!(view.is_valid(), true); 271 | } 272 | 273 | #[test] 274 | fn test_axdl_frame_view_command_2() { 275 | let data = hex_literal::hex!( 276 | "9F 8E 6D 5C 10 00 81 00 72 6F 6D 63 6F 64 65 20 76 31 2E 30 3B 72 61 77 79 5C" 277 | ); 278 | let view = AxdlFrameView::new(&data); 279 | assert_eq!(view.signature(), Some(SIGNATURE)); 280 | assert_eq!(view.length(), Some(16)); 281 | assert_eq!(view.command_response(), Some(0x0081)); 282 | assert_eq!( 283 | view.payload(), 284 | Some(&data[4 + 2 + 2..4 + 2 + 16 + 2 as usize]) 285 | ); 286 | assert_eq!(view.checksum(), Some(0x5c79)); 287 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 288 | assert_eq!(view.verify_checksum(), true); 289 | assert_eq!(view.is_valid(), true); 290 | } 291 | 292 | #[test] 293 | fn test_axdl_frame_view_mut() { 294 | let mut data = [0u8; 12]; 295 | 296 | let mut view_mut = AxdlFrameViewMut::new(&mut data); 297 | view_mut 298 | .init() 299 | .set_command_response(0x1234) 300 | .set_checksum(0x5678) 301 | .payload_mut() 302 | .copy_from_slice(&[0x9a, 0xbc]); 303 | drop(view_mut); 304 | 305 | let view = AxdlFrameView::new(&data); 306 | assert_eq!(view.signature(), Some(SIGNATURE)); 307 | assert_eq!(view.length(), Some(2)); 308 | assert_eq!(view.command_response(), Some(0x1234)); 309 | assert_eq!(view.payload(), Some(&[0x9au8, 0xbc][..])); 310 | assert_eq!(view.checksum(), Some(0x5678)); 311 | } 312 | 313 | #[test] 314 | fn test_axdl_frame_view_mut_empty() { 315 | let mut data = [0u8; 10]; 316 | let mut view_mut = AxdlFrameViewMut::new(&mut data); 317 | view_mut.init(); 318 | view_mut.finalize(); 319 | 320 | let view = AxdlFrameView::new(&data); 321 | assert_eq!(view.signature(), Some(SIGNATURE)); 322 | assert_eq!(view.length(), Some(0)); 323 | assert_eq!(view.command_response(), Some(0x0000)); 324 | assert_eq!(view.payload(), Some(&data[4 + 2 + 2..4 + 2 + 2 as usize])); 325 | assert_eq!(view.checksum(), Some(0xffff)); 326 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 327 | assert_eq!(view.verify_checksum(), true); 328 | assert_eq!(view.is_valid(), true); 329 | } 330 | 331 | #[test] 332 | fn test_axdl_frame_view_mut_empty_command() { 333 | let mut data = [0u8; 10]; 334 | let mut view_mut = AxdlFrameViewMut::new(&mut data); 335 | view_mut.init().set_command_response(0xcafe); 336 | view_mut.finalize(); 337 | 338 | let view = AxdlFrameView::new(&data); 339 | assert_eq!(view.signature(), Some(SIGNATURE)); 340 | assert_eq!(view.length(), Some(0)); 341 | assert_eq!(view.command_response(), Some(0xcafe)); 342 | assert_eq!(view.payload(), Some(&data[4 + 2 + 2..4 + 2 + 2 as usize])); 343 | assert_eq!(view.checksum(), Some(!0xcafe)); 344 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 345 | assert_eq!(view.verify_checksum(), true); 346 | assert_eq!(view.is_valid(), true); 347 | } 348 | 349 | #[test] 350 | fn test_axdl_frame_view_mut_with_payload() { 351 | let mut data = [0u8; 12]; 352 | let mut view_mut = AxdlFrameViewMut::new(&mut data); 353 | view_mut.init().set_command_response(0xcafe); 354 | view_mut.payload_mut().copy_from_slice(&[0x01, 0x02]); 355 | view_mut.finalize(); 356 | 357 | let view = AxdlFrameView::new(&data); 358 | assert_eq!(view.signature(), Some(SIGNATURE)); 359 | assert_eq!(view.length(), Some(2)); 360 | assert_eq!(view.command_response(), Some(0xcafe)); 361 | assert_eq!(view.payload(), Some(&[0x01, 0x02][..])); 362 | assert_eq!(view.checksum(), Some(!(0xcafe + 0x0002 + 0x0201))); 363 | assert_eq!(view.calculate_checksum(), Some(0xffff)); 364 | assert_eq!(view.verify_checksum(), true); 365 | assert_eq!(view.is_valid(), true); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /axdl/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | use std::time::Duration; 17 | 18 | pub mod communication; 19 | pub mod frame; 20 | pub mod partition; 21 | pub mod transport; 22 | 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum AxdlError { 25 | #[cfg(feature = "usb")] 26 | #[error("USB error: {0}")] 27 | UsbError(rusb::Error), 28 | #[cfg(feature = "serial")] 29 | #[error("Serial communication error: {0}")] 30 | SerialError(serialport::Error), 31 | #[cfg(feature = "webusb")] 32 | #[error("WebUSB error: {0}")] 33 | WebUsbError(webusb_web::Error), 34 | #[cfg(feature = "webusb")] 35 | #[error("WebSerial error: {0:?}")] 36 | WebSerialError(js_sys::wasm_bindgen::JsValue), 37 | #[error("Invalid frame received")] 38 | InvalidFrame, 39 | #[error("Failed to decode handshake: {0}")] 40 | HandshakeDecodeError(std::str::Utf8Error), 41 | #[error("Unexpected handshake: {0}")] 42 | UnexpectedHandshake(String), 43 | #[error("Frame has no payload")] 44 | NoPayload, 45 | #[error("Unexpected response: {0:02X}")] 46 | UnexpectedResponse(u16), 47 | #[error("IO Error: {0}, {1}")] 48 | IoError(String, std::io::Error), 49 | #[error("AXP image zip error: {0}")] 50 | ImageZipError(#[from] zip::result::ZipError), 51 | #[cfg(feature = "async")] 52 | #[error("AXP image zip error: {0}")] 53 | ImageAsyncZipError(#[from] async_zip::error::ZipError), 54 | #[error("Image error: {0}")] 55 | ImageError(String), 56 | #[error("Device not found")] 57 | DeviceNotFound, 58 | #[error("Device timeout")] 59 | DeviceTimeout, 60 | #[error("User cancelled the operation")] 61 | UserCancelled, 62 | #[error("Unsupported: {0}")] 63 | Unsupported(String), 64 | } 65 | 66 | #[derive(Debug)] 67 | pub struct DownloadConfig { 68 | pub exclude_rootfs: bool, 69 | } 70 | 71 | pub trait DownloadProgress { 72 | fn is_cancelled(&self) -> bool; 73 | fn report_progress(&mut self, description: &str, progress: Option); 74 | 75 | fn check_is_cancelled(&self) -> Result<(), AxdlError> { 76 | if self.is_cancelled() { 77 | Err(AxdlError::UserCancelled) 78 | } else { 79 | Ok(()) 80 | } 81 | } 82 | } 83 | 84 | pub fn download_image( 85 | image_reader: &mut R, 86 | device: &mut transport::DynDevice, 87 | config: &DownloadConfig, 88 | progress: &mut Progress, 89 | ) -> Result<(), AxdlError> { 90 | // Open the specified image file and find the configuration XML file. 91 | let mut archive = zip::ZipArchive::new(image_reader).map_err(AxdlError::ImageZipError)?; 92 | let mut config_string = None; 93 | 94 | progress.report_progress("Loading the AXP image configuration", None); 95 | // Load the axp image configuration. 96 | let project = { 97 | for i in 0..archive.len() { 98 | let mut file = archive.by_index(i)?; 99 | if file.name().ends_with(".xml") { 100 | config_string = Some(String::new()); 101 | std::io::Read::read_to_string(&mut file, config_string.as_mut().unwrap()).map_err( 102 | |e| AxdlError::ImageError(format!("failed to read configuration file: {}", e)), 103 | )?; 104 | break; 105 | } 106 | } 107 | let config_string = config_string.ok_or(AxdlError::ImageError( 108 | "configuration file not found in the image".into(), 109 | ))?; 110 | let config: partition::deserialize::Config = serde_xml_rs::from_str(&config_string) 111 | .map_err(|e| { 112 | AxdlError::ImageError(format!("failed to parse the configuration file: {}", e)) 113 | })?; 114 | partition::Project::from(config.project) 115 | }; 116 | 117 | tracing::debug!("{:#?}", project); 118 | let partition_table = project.partition_table(); 119 | tracing::debug!("{:#?}", partition_table); 120 | 121 | tracing::debug!("Starting the download process..."); 122 | progress.report_progress("Start download", None); 123 | 124 | // Check if romcode is running on the device. 125 | progress.report_progress("Handshaking with the device", None); 126 | communication::wait_handshake(device, "romcode")?; 127 | 128 | progress.report_progress("Downloading the flash downloaders", None); 129 | if project.is2_level_fdl() { 130 | // Find the FDL1 image and download it. 131 | let fdl1_image = project 132 | .images() 133 | .iter() 134 | .find(|image| image.name() == "FDL1") 135 | .ok_or(AxdlError::ImageError("FDL1 image not found".into()))?; 136 | let fdl1_image_file = fdl1_image.file().ok_or(AxdlError::ImageError( 137 | "FDL1 image file not specified in the project".into(), 138 | ))?; 139 | let mut fdl1 = archive.by_name(fdl1_image_file).map_err(|e| { 140 | AxdlError::ImageError(format!("FDL1 image was not found in the image file: {}", e)) 141 | })?; 142 | let fdl1_address = match fdl1_image.block() { 143 | partition::Block::Absolute(address) => address, 144 | _ => return Err(AxdlError::ImageError("FDL1 block is not absolute".into())), 145 | }; 146 | 147 | // Start the RAM download (FDL1) 148 | communication::start_ram_download(device)?; 149 | let fdl1_image_size = fdl1.size(); 150 | communication::start_partition_absolute_32( 151 | device, 152 | *fdl1_address as u32, 153 | fdl1_image_size as u32, 154 | )?; 155 | communication::write_image( 156 | device, 157 | &mut fdl1, 158 | 1000, 159 | "FDL1", 160 | fdl1_image_size as usize, 161 | Some(100), 162 | progress, 163 | )?; 164 | drop(fdl1); 165 | communication::end_partition(device, communication::TIMEOUT)?; 166 | communication::end_ram_download(device)?; 167 | 168 | communication::wait_handshake(device, "fdl1")?; 169 | 170 | // Find the FDL2 image and download it. 171 | let fdl2_image = project 172 | .images() 173 | .iter() 174 | .find(|image| image.name() == "FDL2") 175 | .ok_or(AxdlError::ImageError("FDL2 image not found".into()))?; 176 | let fdl2_image_file = fdl2_image.file().ok_or(AxdlError::ImageError( 177 | "FDL2 image file not specified in the project".into(), 178 | ))?; 179 | let mut fdl2 = archive.by_name(fdl2_image_file).map_err(|e| { 180 | AxdlError::ImageError(format!("FDL2 image was not found in the image file: {}", e)) 181 | })?; 182 | let fdl2_address = match fdl2_image.block() { 183 | partition::Block::Absolute(address) => address, 184 | _ => return Err(AxdlError::ImageError("FDL2 block is not absolute".into())), 185 | }; 186 | // Start the RAM download (FDL2) 187 | communication::start_ram_download(device)?; 188 | 189 | let fdl2_image_size = fdl2.size(); 190 | communication::start_partition_absolute(device, *fdl2_address, fdl2_image_size)?; 191 | communication::write_image( 192 | device, 193 | &mut fdl2, 194 | 1000, 195 | "FDL2", 196 | fdl2_image_size as usize, 197 | Some(100), 198 | progress, 199 | )?; 200 | drop(fdl2); 201 | communication::end_partition(device, communication::TIMEOUT)?; 202 | communication::end_ram_download(device)?; 203 | }else{ 204 | let fdl1_image = project 205 | .images() 206 | .iter() 207 | .find(|image| image.name() == "FDL") 208 | .ok_or(AxdlError::ImageError("FDL image not found".into()))?; 209 | let fdl1_image_file = fdl1_image.file().ok_or(AxdlError::ImageError( 210 | "FDL image file not specified in the project".into(), 211 | ))?; 212 | let mut fdl1 = archive.by_name(fdl1_image_file).map_err(|e| { 213 | AxdlError::ImageError(format!("FDL image was not found in the image file: {}", e)) 214 | })?; 215 | let fdl1_address = match fdl1_image.block() { 216 | partition::Block::Absolute(address) => address, 217 | _ => return Err(AxdlError::ImageError("FDL block is not absolute".into())), 218 | }; 219 | 220 | // Start the RAM download (FDL1) 221 | communication::start_ram_download(device)?; 222 | let fdl1_image_size = fdl1.size(); 223 | communication::start_partition_absolute_32( 224 | device, 225 | *fdl1_address as u32, 226 | fdl1_image_size as u32, 227 | )?; 228 | communication::write_image( 229 | device, 230 | &mut fdl1, 231 | 1000, 232 | "FDL", 233 | fdl1_image_size as usize, 234 | Some(100), 235 | progress, 236 | )?; 237 | drop(fdl1); 238 | communication::end_partition(device, communication::TIMEOUT)?; 239 | communication::end_ram_download(device)?; 240 | 241 | communication::wait_handshake(device, "fdl2")?; 242 | } 243 | 244 | // Download the partition table. 245 | progress.report_progress("Downloading the partition table", None); 246 | communication::set_partition_table(device, &partition_table)?; 247 | 248 | // Download all of "CODE" images 249 | for image in project.images().iter().filter(|image| { 250 | image.r#type() == partition::ImageType::Code 251 | && (!config.exclude_rootfs || image.name() != "ROOTFS") 252 | }) { 253 | tracing::debug!("Downloading image: {}", image.name()); 254 | progress.report_progress(&format!("Downloading image {}", image.name()), None); 255 | 256 | progress.check_is_cancelled()?; 257 | 258 | let image_file_name = image.file().ok_or(AxdlError::ImageError(format!( 259 | "image {} file not specified in the project", 260 | image.name() 261 | )))?; 262 | let mut image_data = archive.by_name(&image_file_name).map_err(|e| { 263 | AxdlError::ImageError(format!( 264 | "image {} was not found in the archive: {}", 265 | image.name(), 266 | e 267 | )) 268 | })?; 269 | let image_id = match image.block() { 270 | partition::Block::Partition(id) => id, 271 | _ => { 272 | return Err(AxdlError::ImageError(format!( 273 | "image {} block is not partition", 274 | image.name() 275 | ))) 276 | } 277 | }; 278 | let image_data_size = image_data.size(); 279 | communication::start_partition_id(device, &image_id, image_data_size)?; 280 | communication::write_image( 281 | device, 282 | &mut image_data, 283 | 48000, 284 | image.name(), 285 | image_data_size as usize, 286 | Some(100), 287 | progress, 288 | )?; 289 | communication::end_partition(device, Duration::from_secs(60))?; 290 | } 291 | tracing::info!("Done"); 292 | Ok(()) 293 | } 294 | 295 | #[cfg(feature = "async")] 296 | mod r#async { 297 | use crate::{AxdlError, DownloadProgress, DownloadConfig, communication, partition, transport::AsyncDevice}; 298 | 299 | type AsyncZipEntryReaderWithEntry<'a, R> = 300 | async_zip::base::read::ZipEntryReader<'a, R, async_zip::base::read::WithEntry<'a>>; 301 | 302 | async fn read_zip_entry_as_string< 303 | R: futures_io::AsyncBufRead + futures_io::AsyncSeek + Unpin, 304 | F: Fn(&async_zip::ZipEntry) -> bool, 305 | >( 306 | archive: &mut async_zip::base::read::seek::ZipFileReader, 307 | predicate: F, 308 | ) -> Result, AxdlError> { 309 | for i in 0.. { 310 | match archive.reader_with_entry(i).await { 311 | Ok(mut reader) => { 312 | if predicate(reader.entry()) { 313 | let mut config_string = String::new(); 314 | reader 315 | .read_to_string_checked(&mut config_string) 316 | .await 317 | .map_err(AxdlError::ImageAsyncZipError)?; 318 | return Ok(Some(config_string)); 319 | } 320 | } 321 | Err(async_zip::error::ZipError::EntryIndexOutOfBounds) => break, 322 | Err(e) => return Err(AxdlError::ImageAsyncZipError(e.into())), 323 | } 324 | } 325 | Ok(None) 326 | } 327 | 328 | enum WriteImagePartition { 329 | Absolute32(u32), 330 | Absolute64(u64), 331 | PartitionId(String), 332 | } 333 | 334 | async fn write_partition_from_zip_file_async< 335 | R: futures_io::AsyncBufRead + futures_io::AsyncSeek + Unpin, 336 | D: AsyncDevice, 337 | >( 338 | device: &mut D, 339 | archive: &mut async_zip::base::read::seek::ZipFileReader, 340 | image_name: &str, 341 | partition: &WriteImagePartition, 342 | file_name: &str, 343 | chunk_size: usize, 344 | report_every: Option, 345 | progress: &mut impl DownloadProgress, 346 | ) -> Result<(), AxdlError> { 347 | for i in 0.. { 348 | match archive.reader_with_entry(i).await { 349 | Ok(mut reader) => { 350 | if reader 351 | .entry() 352 | .filename() 353 | .as_str() 354 | .map(|s| s == file_name) 355 | .unwrap_or(false) 356 | { 357 | let image_size = reader.entry().uncompressed_size(); 358 | match partition { 359 | WriteImagePartition::Absolute32(address) => { 360 | communication::r#async::start_partition_absolute_32( 361 | device, 362 | *address, 363 | image_size as u32, 364 | ) 365 | .await?; 366 | } 367 | WriteImagePartition::Absolute64(address) => { 368 | communication::r#async::start_partition_absolute( 369 | device, *address, image_size, 370 | ) 371 | .await?; 372 | } 373 | WriteImagePartition::PartitionId(id) => { 374 | communication::r#async::start_partition_id(device, id, image_size) 375 | .await?; 376 | } 377 | } 378 | communication::r#async::write_image( 379 | device, 380 | &mut reader, 381 | chunk_size, 382 | image_name, 383 | image_size as usize, 384 | report_every, 385 | progress, 386 | ) 387 | .await?; 388 | communication::r#async::end_partition(device).await?; 389 | return Ok(()); 390 | } 391 | } 392 | Err(async_zip::error::ZipError::EntryIndexOutOfBounds) => break, 393 | Err(e) => return Err(AxdlError::ImageAsyncZipError(e.into())), 394 | } 395 | } 396 | Err(AxdlError::ImageError(format!( 397 | "image was not found in the image file: {}", 398 | file_name 399 | ))) 400 | } 401 | 402 | #[cfg(feature = "async")] 403 | pub async fn download_image_async< 404 | R: futures_io::AsyncBufRead + futures_io::AsyncSeek + Unpin, 405 | D: AsyncDevice, 406 | Progress: DownloadProgress, 407 | >( 408 | image_reader: &mut R, 409 | device: &mut D, 410 | config: &DownloadConfig, 411 | progress: &mut Progress, 412 | ) -> Result<(), AxdlError> { 413 | tracing::info!("download_image_async"); 414 | // Open the specified image file and find the configuration XML file. 415 | let mut archive = async_zip::base::read::seek::ZipFileReader::new(image_reader) 416 | .await 417 | .map_err(AxdlError::ImageAsyncZipError)?; 418 | tracing::info!("image file opened"); 419 | progress.report_progress("Loading the AXP image configuration", None); 420 | // Load the axp image configuration. 421 | let project = { 422 | let config_string = read_zip_entry_as_string(&mut archive, |entry| { 423 | entry 424 | .filename() 425 | .as_str() 426 | .map(|s| s.ends_with(".xml")) 427 | .unwrap_or(false) 428 | }) 429 | .await? 430 | .ok_or(AxdlError::ImageError( 431 | "configuration file not found in the image".into(), 432 | ))?; 433 | let config: partition::deserialize::Config = serde_xml_rs::from_str(&config_string) 434 | .map_err(|e| { 435 | AxdlError::ImageError(format!("failed to parse the configuration file: {}", e)) 436 | })?; 437 | partition::Project::from(config.project) 438 | }; 439 | 440 | tracing::debug!("{:#?}", project); 441 | let partition_table = project.partition_table(); 442 | tracing::debug!("{:#?}", partition_table); 443 | 444 | tracing::debug!("Starting the download process..."); 445 | progress.report_progress("Start download", None); 446 | 447 | // Check if romcode is running on the device. 448 | progress.report_progress("Handshaking with the device", None); 449 | communication::r#async::wait_handshake(device, "romcode").await?; 450 | 451 | progress.report_progress("Downloading the flash downloaders", None); 452 | // Find the FDL1 image and download it. 453 | let fdl1_image = project 454 | .images() 455 | .iter() 456 | .find(|image| image.name() == "FDL1") 457 | .ok_or(AxdlError::ImageError("FDL1 image not found".into()))?; 458 | let fdl1_image_file = fdl1_image.file().ok_or(AxdlError::ImageError( 459 | "FDL1 image file not specified in the project".into(), 460 | ))?; 461 | let fdl1_address = match fdl1_image.block() { 462 | partition::Block::Absolute(address) => address, 463 | _ => return Err(AxdlError::ImageError("FDL1 block is not absolute".into())), 464 | }; 465 | 466 | // Start the RAM download (FDL1) 467 | communication::r#async::start_ram_download(device).await?; 468 | write_partition_from_zip_file_async( 469 | device, 470 | &mut archive, 471 | "FDL1", 472 | &WriteImagePartition::Absolute32(*fdl1_address as u32), 473 | fdl1_image_file, 474 | 1000, 475 | Some(100), 476 | progress, 477 | ) 478 | .await?; 479 | communication::r#async::end_ram_download(device).await?; 480 | 481 | communication::r#async::wait_handshake(device, "fdl1").await?; 482 | 483 | // Find the FDL2 image and download it. 484 | let fdl2_image = project 485 | .images() 486 | .iter() 487 | .find(|image| image.name() == "FDL2") 488 | .ok_or(AxdlError::ImageError("FDL2 image not found".into()))?; 489 | let fdl2_image_file = fdl2_image.file().ok_or(AxdlError::ImageError( 490 | "FDL2 image file not specified in the project".into(), 491 | ))?; 492 | let fdl2_address = match fdl2_image.block() { 493 | partition::Block::Absolute(address) => address, 494 | _ => return Err(AxdlError::ImageError("FDL2 block is not absolute".into())), 495 | }; 496 | // Start the RAM download (FDL2) 497 | communication::r#async::start_ram_download(device).await?; 498 | write_partition_from_zip_file_async( 499 | device, 500 | &mut archive, 501 | "FDL2", 502 | &WriteImagePartition::Absolute64(*fdl2_address), 503 | fdl2_image_file, 504 | 1000, 505 | Some(100), 506 | progress, 507 | ) 508 | .await?; 509 | communication::r#async::end_ram_download(device).await?; 510 | 511 | // Download the partition table. 512 | progress.report_progress("Downloading the partition table", None); 513 | communication::r#async::set_partition_table(device, &partition_table).await?; 514 | 515 | // Download all of "CODE" images 516 | for image in project.images().iter().filter(|image| { 517 | image.r#type() == partition::ImageType::Code 518 | && (!config.exclude_rootfs || image.name() != "ROOTFS") 519 | }) { 520 | tracing::debug!("Downloading image: {}", image.name()); 521 | progress.report_progress(&format!("Downloading image {}", image.name()), None); 522 | 523 | progress.check_is_cancelled()?; 524 | 525 | let image_file_name = image.file().ok_or(AxdlError::ImageError(format!( 526 | "image {} file not specified in the project", 527 | image.name() 528 | )))?; 529 | 530 | let image_id = match image.block() { 531 | partition::Block::Partition(id) => id, 532 | _ => { 533 | return Err(AxdlError::ImageError(format!( 534 | "image {} block is not partition", 535 | image.name() 536 | ))) 537 | } 538 | }; 539 | 540 | write_partition_from_zip_file_async( 541 | device, 542 | &mut archive, 543 | image.name(), 544 | &WriteImagePartition::PartitionId(image_id.clone()), 545 | image_file_name, 546 | 48000, 547 | Some(100), 548 | progress, 549 | ) 550 | .await?; 551 | } 552 | tracing::info!("Done"); 553 | Ok(()) 554 | } 555 | } 556 | 557 | #[cfg(feature = "async")] 558 | pub use r#async::*; 559 | -------------------------------------------------------------------------------- /axdl/src/partition.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2025 Kenta Ida 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | use std::str::FromStr; 17 | 18 | #[derive(Debug)] 19 | pub struct PartitionTable { 20 | strategy: u8, 21 | unit: u8, 22 | partitions: Vec, 23 | } 24 | 25 | impl PartitionTable { 26 | pub fn new(strategy: u8, unit: u8) -> Self { 27 | Self { 28 | strategy, 29 | unit, 30 | partitions: Vec::new(), 31 | } 32 | } 33 | 34 | pub fn strategy(&self) -> u8 { 35 | self.strategy 36 | } 37 | 38 | pub fn unit(&self) -> u8 { 39 | self.unit 40 | } 41 | 42 | pub fn add_partition(&mut self, partition: Partition) { 43 | self.partitions.push(partition); 44 | } 45 | 46 | pub fn partitions(&self) -> &[Partition] { 47 | &self.partitions 48 | } 49 | 50 | pub fn to_bytes(&self) -> Vec { 51 | let mut bytes = Vec::new(); 52 | // Add header 53 | bytes.extend_from_slice(&[0x70, 0x61, 0x72, 0x3a, self.strategy, self.unit]); //"par:"" strategy, unit 54 | bytes.extend_from_slice(&(self.partitions.len() as u16).to_le_bytes()); 55 | for partition in &self.partitions { 56 | bytes.extend_from_slice(&partition.to_bytes()); 57 | } 58 | bytes 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub struct Partition { 64 | name: String, 65 | gap: u64, 66 | size: u64, 67 | } 68 | 69 | impl Partition { 70 | pub fn new(name: String, gap: u64, size: u64) -> Self { 71 | Self { name, gap, size } 72 | } 73 | 74 | pub fn name(&self) -> &str { 75 | &self.name 76 | } 77 | 78 | pub fn gap(&self) -> u64 { 79 | self.gap 80 | } 81 | 82 | pub fn size(&self) -> u64 { 83 | self.size 84 | } 85 | 86 | pub fn to_bytes(&self) -> [u8; 0x58] { 87 | let mut bytes = [0u8; 0x58]; 88 | let name_utf16: Vec = str::encode_utf16(&self.name) 89 | .map(|c| [(c & 0xff) as u8, (c >> 8) as u8]) 90 | .flatten() 91 | .collect(); 92 | if name_utf16.len() > 0x40 { 93 | panic!("Partition name is too long"); 94 | } 95 | bytes[..name_utf16.len()].copy_from_slice(&name_utf16); 96 | bytes[0x40..0x48].copy_from_slice(&self.gap.to_le_bytes()); 97 | bytes[0x48..0x50].copy_from_slice(&self.size.to_le_bytes()); 98 | bytes 99 | } 100 | } 101 | 102 | #[derive(Debug, Copy, Clone, PartialEq)] 103 | pub enum ImageType { 104 | Init, 105 | Eip, 106 | Fdl1, 107 | Fdl2, 108 | EraseFlash, 109 | Code, 110 | } 111 | 112 | impl FromStr for ImageType { 113 | type Err = (); 114 | 115 | fn from_str(s: &str) -> Result { 116 | match s { 117 | "INIT" => Ok(Self::Init), 118 | "EIP" => Ok(Self::Eip), 119 | "FDL1" => Ok(Self::Fdl1), 120 | "FDL2" => Ok(Self::Fdl2), 121 | "FDL" => Ok(Self::Fdl2), //Single level FDL , AX650N 122 | "ERASEFLASH" => Ok(Self::EraseFlash), 123 | "CODE" => Ok(Self::Code), 124 | _ => Err(()), 125 | } 126 | } 127 | } 128 | 129 | #[derive(Debug, PartialEq)] 130 | pub enum Block { 131 | Absolute(u64), 132 | Partition(String), 133 | } 134 | 135 | #[derive(Debug)] 136 | pub struct Image { 137 | flag: u32, 138 | name: String, 139 | r#type: ImageType, 140 | block: Block, 141 | file: Option, 142 | description: String, 143 | } 144 | impl Image { 145 | pub(crate) fn name(&self) -> &str { 146 | self.name.as_str() 147 | } 148 | 149 | pub(crate) fn r#type(&self) -> ImageType { 150 | self.r#type 151 | } 152 | 153 | pub(crate) fn block(&self) -> &Block { 154 | &self.block 155 | } 156 | 157 | pub(crate) fn description(&self) -> &str { 158 | &self.description 159 | } 160 | 161 | pub fn file(&self) -> Option<&str> { 162 | self.file.as_deref() 163 | } 164 | } 165 | 166 | #[derive(Debug)] 167 | pub struct Project { 168 | partition_table: PartitionTable, 169 | images: Vec, 170 | fdl_level: u32 171 | } 172 | 173 | impl Project { 174 | pub fn partition_table(&self) -> &PartitionTable { 175 | &self.partition_table 176 | } 177 | 178 | pub fn images(&self) -> &[Image] { 179 | &self.images 180 | } 181 | 182 | pub fn is2_level_fdl(&self) -> bool { 183 | self.fdl_level == 2 184 | } 185 | } 186 | 187 | pub mod deserialize { 188 | use serde::Deserialize; 189 | 190 | #[derive(Debug, Deserialize)] 191 | #[serde(rename = "Config")] 192 | pub struct Config { 193 | #[serde(rename = "Project")] 194 | pub project: Project, 195 | } 196 | 197 | #[derive(Debug, Deserialize)] 198 | pub struct Project { 199 | #[serde(rename = "alias")] 200 | alias: String, 201 | #[serde(rename = "name")] 202 | name: String, 203 | #[serde(rename = "version")] 204 | version: String, 205 | 206 | #[serde(rename = "FDLLevel")] 207 | fdl_level: u32, 208 | 209 | #[serde(rename = "Partitions")] 210 | partitions: Partitions, 211 | 212 | #[serde(rename = "ImgList")] 213 | img_list: ImgList, 214 | } 215 | 216 | impl From for super::Project { 217 | fn from(project: Project) -> super::Project { 218 | let partition_table = project.partitions.into(); 219 | let mut images = Vec::new(); 220 | for img in project.img_list.images { 221 | images.push(img.into()); 222 | } 223 | super::Project { 224 | partition_table, 225 | images, 226 | fdl_level: project.fdl_level 227 | } 228 | } 229 | } 230 | 231 | #[derive(Debug, Deserialize)] 232 | struct Partitions { 233 | #[serde(rename = "strategy")] 234 | strategy: u32, 235 | #[serde(rename = "unit")] 236 | unit: u32, 237 | 238 | #[serde(rename = "$value")] 239 | partitions: Vec, 240 | } 241 | 242 | impl From for super::PartitionTable { 243 | fn from(partitions: Partitions) -> super::PartitionTable { 244 | let mut partition_table = 245 | super::PartitionTable::new(partitions.strategy as u8, partitions.unit as u8); 246 | for partition in partitions.partitions { 247 | partition_table.add_partition(partition.into()); 248 | } 249 | partition_table 250 | } 251 | } 252 | 253 | #[derive(Debug, Deserialize)] 254 | struct Partition { 255 | #[serde(rename = "gap")] 256 | gap: u64, 257 | #[serde(rename = "id")] 258 | id: String, 259 | #[serde(rename = "size")] 260 | size: String, 261 | } 262 | 263 | fn hex_to_u64(hex_string: &str) -> Option { 264 | match u64::from_str_radix(hex_string, 16) { 265 | Ok(parsed_int) => Some(parsed_int), 266 | Err(_) => None, 267 | } 268 | } 269 | 270 | impl From for super::Partition { 271 | fn from(partition: Partition) -> Self { 272 | if partition.size.starts_with("0x") { 273 | 274 | super::Partition::new(partition.id, partition.gap, hex_to_u64(&partition.size[2..]).unwrap()) 275 | } else { 276 | super::Partition::new(partition.id, partition.gap, partition.size.parse::().unwrap()) 277 | } 278 | } 279 | } 280 | 281 | #[derive(Debug, Deserialize)] 282 | struct ImgList { 283 | #[serde(rename = "Img")] 284 | images: Vec, 285 | } 286 | 287 | #[derive(Debug, Deserialize)] 288 | struct Img { 289 | #[serde(rename = "flag")] 290 | flag: u32, 291 | #[serde(rename = "name")] 292 | name: String, 293 | #[serde(rename = "select")] 294 | select: u32, 295 | 296 | #[serde(rename = "ID")] 297 | id: String, 298 | #[serde(rename = "Type")] 299 | img_type: String, 300 | 301 | #[serde(rename = "Block")] 302 | block: Block, 303 | 304 | #[serde(rename = "File", deserialize_with = "empty_string_to_none")] 305 | file: Option, 306 | 307 | #[serde(rename = "Auth")] 308 | auth: Auth, 309 | 310 | #[serde(rename = "Description")] 311 | description: String, 312 | } 313 | 314 | impl From for super::Image { 315 | fn from(img: Img) -> super::Image { 316 | super::Image { 317 | flag: img.flag, 318 | name: img.name, 319 | r#type: img.img_type.parse().unwrap(), 320 | block: img.block.into(), 321 | file: img.file, 322 | description: img.description, 323 | } 324 | } 325 | } 326 | 327 | fn from_hex<'de, D>(deserializer: D) -> Result 328 | where 329 | D: serde::Deserializer<'de>, 330 | { 331 | let s: String = Deserialize::deserialize(deserializer)?; 332 | u64::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) 333 | } 334 | 335 | fn empty_string_to_none<'de, D>(deserializer: D) -> Result, D::Error> 336 | where 337 | D: serde::Deserializer<'de>, 338 | { 339 | let s: Option = Option::deserialize(deserializer)?; 340 | match s { 341 | Some(ref value) if value.is_empty() => Ok(None), 342 | other => Ok(other), 343 | } 344 | } 345 | 346 | #[derive(Debug, Deserialize)] 347 | struct Block { 348 | #[serde(rename = "id")] 349 | id: Option, 350 | 351 | #[serde(rename = "Base", deserialize_with = "from_hex")] 352 | base: u64, 353 | 354 | #[serde(rename = "Size", deserialize_with = "from_hex")] 355 | size: u64, 356 | } 357 | 358 | impl From for super::Block { 359 | fn from(block: Block) -> super::Block { 360 | if let Some(id) = block.id { 361 | super::Block::Partition(id) 362 | } else { 363 | super::Block::Absolute(block.base) 364 | } 365 | } 366 | } 367 | 368 | #[derive(Debug, Deserialize)] 369 | struct Auth { 370 | #[serde(rename = "algo")] 371 | algo: u32, 372 | } 373 | 374 | #[cfg(test)] 375 | mod test { 376 | use super::*; 377 | 378 | #[test] 379 | fn test_deserialize() { 380 | let xml_data = r#" 381 | 382 | 383 | 2 384 | 385 | 386 | 387 | 388 | 389 | 390 | INIT 391 | INIT 392 | 393 | 0x0 394 | 0x0 395 | 396 | 397 | 398 | Handshake with romcode 399 | 400 | 401 | 402 | 403 | "#; 404 | 405 | let config: Config = serde_xml_rs::from_str(xml_data).unwrap(); 406 | println!("{:#?}", config); 407 | 408 | let project = super::super::Project::from(config.project); 409 | println!("{:#?}", project); 410 | assert_eq!(project.partition_table().strategy(), 1); 411 | assert_eq!(project.partition_table().unit(), 2); 412 | assert_eq!(project.partition_table().partitions().len(), 2); 413 | assert_eq!(project.partition_table().partitions()[0].name(), "spl"); 414 | assert_eq!(project.partition_table().partitions()[0].gap(), 0); 415 | assert_eq!(project.partition_table().partitions()[0].size(), 768); 416 | assert_eq!(project.partition_table().partitions()[1].name(), "ddrinit"); 417 | assert_eq!(project.partition_table().partitions()[1].gap(), 0); 418 | assert_eq!(project.partition_table().partitions()[1].size(), 512); 419 | assert_eq!(project.images().len(), 1); 420 | assert_eq!(project.images()[0].flag, 2); 421 | assert_eq!(project.images()[0].name, "INIT"); 422 | assert_eq!(project.images()[0].r#type, super::super::ImageType::Init); 423 | assert_eq!(project.images()[0].block, super::super::Block::Absolute(0)); 424 | assert_eq!(project.images()[0].file, None); 425 | assert_eq!(project.images()[0].description, "Handshake with romcode"); 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /axdl/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::AxdlError; 4 | 5 | #[cfg(feature = "serial")] 6 | pub mod serial; 7 | #[cfg(feature = "usb")] 8 | pub mod usb; 9 | #[cfg(feature = "webserial")] 10 | pub mod webserial; 11 | #[cfg(feature = "webusb")] 12 | pub mod webusb; 13 | 14 | /// Device trait for reading and writing data. 15 | pub trait Device { 16 | fn read_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> Result; 17 | fn write_timeout(&mut self, buf: &[u8], timeout: Duration) -> Result; 18 | } 19 | 20 | /// Transport trait for listing devices and opening devices. 21 | pub trait Transport { 22 | type DeviceId; 23 | type DeviceType: Device; 24 | fn list_devices() -> Result, AxdlError>; 25 | fn open_device(path: &Self::DeviceId) -> Result; 26 | } 27 | 28 | pub type DynDevice = Box; 29 | 30 | #[cfg(feature = "webusb")] 31 | mod async_transport { 32 | use crate::AxdlError; 33 | 34 | pub trait AsyncDevice { 35 | fn read( 36 | &mut self, 37 | buf: &mut [u8], 38 | ) -> impl std::future::Future>; 39 | fn write( 40 | &mut self, 41 | buf: &[u8], 42 | ) -> impl std::future::Future>; 43 | } 44 | 45 | pub trait AsyncTransport { 46 | type DeviceId; 47 | type DeviceType: AsyncDevice; 48 | fn list_devices( 49 | ) -> impl std::future::Future, AxdlError>>; 50 | fn open_device( 51 | path: &Self::DeviceId, 52 | ) -> impl std::future::Future>; 53 | } 54 | } 55 | 56 | #[cfg(feature = "webusb")] 57 | pub use async_transport::*; 58 | -------------------------------------------------------------------------------- /axdl/src/transport/serial.rs: -------------------------------------------------------------------------------- 1 | use crate::AxdlError; 2 | use std::time::Duration; 3 | 4 | use super::{Device, Transport}; 5 | 6 | pub const VENDOR_ID: u16 = 0x32c9; 7 | pub const PRODUCT_ID: u16 = 0x1000; 8 | 9 | /// Transport implementation for serial ports 10 | pub struct SerialTransport; 11 | 12 | /// Device path for serial ports. 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub struct SerialDevicePath { 15 | port_name: String, 16 | } 17 | 18 | impl SerialDevicePath { 19 | pub fn is_match(&self, port_name: &str) -> bool { 20 | self.port_name == port_name 21 | } 22 | } 23 | 24 | impl std::fmt::Display for SerialDevicePath { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!(f, "{}", self.port_name) 27 | } 28 | } 29 | 30 | impl Transport for SerialTransport { 31 | type DeviceId = SerialDevicePath; 32 | type DeviceType = SerialDevice; 33 | 34 | fn list_devices() -> Result, AxdlError> { 35 | let list = serialport::available_ports() 36 | .map_err(AxdlError::SerialError)? 37 | .iter() 38 | .filter_map(|port_info| match &port_info.port_type { 39 | serialport::SerialPortType::UsbPort(usb) => { 40 | if usb.vid == VENDOR_ID && usb.pid == PRODUCT_ID { 41 | Some(SerialDevicePath { 42 | port_name: port_info.port_name.clone(), 43 | }) 44 | } else { 45 | None 46 | } 47 | } 48 | _ => None, 49 | }) 50 | .collect(); 51 | Ok(list) 52 | } 53 | fn open_device(path: &Self::DeviceId) -> Result { 54 | let port = serialport::new(&path.port_name, 115200) 55 | .open() 56 | .map_err(AxdlError::SerialError)?; 57 | Ok(SerialDevice { port }) 58 | } 59 | } 60 | 61 | #[derive(Debug)] 62 | pub struct SerialDevice { 63 | port: Box, 64 | } 65 | 66 | impl Device for SerialDevice { 67 | fn read_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> Result { 68 | self.port 69 | .set_timeout(timeout) 70 | .map_err(AxdlError::SerialError)?; 71 | self.port 72 | .read(buf) 73 | .map_err(|e| AxdlError::IoError("read error".into(), e)) 74 | } 75 | fn write_timeout(&mut self, buf: &[u8], timeout: Duration) -> Result { 76 | self.port 77 | .set_timeout(timeout) 78 | .map_err(AxdlError::SerialError)?; 79 | self.port 80 | .write(buf) 81 | .map_err(|e| AxdlError::IoError("write error".into(), e)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /axdl/src/transport/usb.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use rusb::DeviceHandle; 4 | 5 | use crate::AxdlError; 6 | 7 | use super::{Device, Transport}; 8 | 9 | pub const VENDOR_ID: u16 = 0x32c9; 10 | pub const PRODUCT_ID: u16 = 0x1000; 11 | pub const ENDPOINT_OUT: u8 = 0x01; 12 | pub const ENDPOINT_IN: u8 = 0x81; 13 | 14 | /// Transport implementation to use the USB device directly via libusb. 15 | pub struct UsbTransport; 16 | 17 | /// Device path for USB devices. 18 | #[derive(Debug, Clone, PartialEq)] 19 | pub struct UsbDevicePath { 20 | port_numbers: Vec, 21 | } 22 | 23 | impl std::fmt::Display for UsbDevicePath { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | // Concat port number with dot. 26 | for (i, port_number) in self.port_numbers.iter().enumerate() { 27 | if i > 0 { 28 | write!(f, ".")?; 29 | } 30 | write!(f, "{}", port_number)?; 31 | } 32 | Ok(()) 33 | } 34 | } 35 | 36 | impl Transport for UsbTransport { 37 | type DeviceId = UsbDevicePath; 38 | type DeviceType = UsbDevice; 39 | 40 | fn list_devices() -> Result, AxdlError> { 41 | let list = rusb::devices() 42 | .map_err(AxdlError::UsbError)? 43 | .iter() 44 | .filter_map(|device| { 45 | if let Ok(device_desc) = device.device_descriptor() { 46 | if device_desc.vendor_id() == VENDOR_ID 47 | && device_desc.product_id() == PRODUCT_ID 48 | { 49 | device 50 | .port_numbers() 51 | .ok() 52 | .map(|port_numbers| UsbDevicePath { port_numbers }) 53 | } else { 54 | None 55 | } 56 | } else { 57 | None 58 | } 59 | }) 60 | .collect(); 61 | Ok(list) 62 | } 63 | fn open_device(path: &Self::DeviceId) -> Result { 64 | let device = rusb::devices() 65 | .map_err(AxdlError::UsbError)? 66 | .iter() 67 | .find(|device| { 68 | if let Ok(device_desc) = device.device_descriptor() { 69 | if device_desc.vendor_id() == VENDOR_ID 70 | && device_desc.product_id() == PRODUCT_ID 71 | { 72 | if let Ok(port_numbers) = device.port_numbers() { 73 | return port_numbers == path.port_numbers; 74 | } 75 | } 76 | } 77 | false 78 | }) 79 | .ok_or(AxdlError::DeviceNotFound)?; 80 | 81 | let handle = device.open().map_err(AxdlError::UsbError)?; 82 | handle.claim_interface(0).map_err(AxdlError::UsbError)?; 83 | Ok(UsbDevice { handle }) 84 | } 85 | } 86 | 87 | #[derive(Debug)] 88 | pub struct UsbDevice { 89 | handle: DeviceHandle, 90 | } 91 | 92 | impl Device for UsbDevice { 93 | fn read_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> Result { 94 | self.handle 95 | .read_bulk(ENDPOINT_IN, buf, timeout) 96 | .map_err(AxdlError::UsbError) 97 | } 98 | fn write_timeout(&mut self, buf: &[u8], timeout: Duration) -> Result { 99 | self.handle 100 | .write_bulk(ENDPOINT_OUT, buf, timeout) 101 | .map_err(AxdlError::UsbError) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /axdl/src/transport/webserial.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use wasm_streams::{ReadableStream, WritableStream}; 4 | use webusb_web; 5 | 6 | use crate::AxdlError; 7 | 8 | use super::AsyncDevice; 9 | 10 | pub const VENDOR_ID: u16 = 0x32c9; 11 | pub const PRODUCT_ID: u16 = 0x1000; 12 | pub const ENDPOINT_OUT: u8 = 0x01; 13 | pub const ENDPOINT_IN: u8 = 0x81; 14 | 15 | pub fn new_serial() -> Result { 16 | web_sys::window() 17 | .map(|window| window.navigator().serial()) 18 | .ok_or(AxdlError::Unsupported("WebSerial".to_string())) 19 | } 20 | 21 | /// Returns a device filter for Axera devices. 22 | pub fn axdl_device_filter() -> web_sys::SerialPortFilter { 23 | let mut filter = web_sys::SerialPortFilter::new(); 24 | filter.set_usb_vendor_id(VENDOR_ID); 25 | filter.set_usb_product_id(PRODUCT_ID); 26 | filter 27 | } 28 | 29 | pub struct WebSerialDevice { 30 | port: web_sys::SerialPort, 31 | read_buffer: Vec, 32 | read_position: usize, 33 | } 34 | 35 | impl WebSerialDevice { 36 | pub fn new(port: web_sys::SerialPort) -> Self { 37 | let read_buffer = Vec::new(); 38 | let read_position = 0; 39 | Self { 40 | port, 41 | read_buffer, 42 | read_position, 43 | } 44 | } 45 | } 46 | 47 | impl AsyncDevice for WebSerialDevice { 48 | async fn read(&mut self, buf: &mut [u8]) -> Result { 49 | if buf.len() == 0 { 50 | return Ok(0); 51 | } 52 | let bytes_remaining = self.read_buffer.len() - self.read_position; 53 | if bytes_remaining < buf.len() { 54 | let mut stream = ReadableStream::from_raw(self.port.readable()); 55 | let mut reader = stream.get_reader(); 56 | pin_utils::pin_mut!(reader); 57 | let result = reader.read().await; 58 | if let Ok(Some(chunk)) = result { 59 | if let Ok(buffer) = js_sys::Uint8Array::try_from(chunk) { 60 | let length = buffer.length() as usize; 61 | let prev_len = self.read_buffer.len(); 62 | self.read_buffer.resize(prev_len + length, 0); 63 | buffer.copy_to(&mut self.read_buffer[prev_len..]); 64 | } 65 | } 66 | } 67 | 68 | let bytes_remaining = self.read_buffer.len() - self.read_position; 69 | if bytes_remaining == 0 { 70 | return Ok(0); 71 | } else if bytes_remaining < buf.len() { 72 | buf[..bytes_remaining].copy_from_slice(&self.read_buffer[self.read_position..]); 73 | self.read_position = 0; 74 | self.read_buffer.clear(); 75 | Ok(bytes_remaining) 76 | } else { 77 | buf.copy_from_slice( 78 | &self.read_buffer[self.read_position..self.read_position + buf.len()], 79 | ); 80 | self.read_position += buf.len(); 81 | Ok(buf.len()) 82 | } 83 | } 84 | 85 | async fn write(&mut self, buf: &[u8]) -> Result { 86 | let buffer = js_sys::Uint8Array::from(buf); 87 | let mut stream = WritableStream::from_raw(self.port.writable()); 88 | let writer = stream.get_writer(); 89 | pin_utils::pin_mut!(writer); 90 | tracing::debug!("webserial: write {} bytes", buffer.byte_length()); 91 | writer 92 | .write(buffer.into()) 93 | .await 94 | .map_err(AxdlError::WebSerialError)?; 95 | Ok(buf.len()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /axdl/src/transport/webusb.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use webusb_web; 4 | 5 | use crate::AxdlError; 6 | 7 | use super::AsyncDevice; 8 | 9 | pub const VENDOR_ID: u16 = 0x32c9; 10 | pub const PRODUCT_ID: u16 = 0x1000; 11 | pub const ENDPOINT_OUT: u8 = 0x01; 12 | pub const ENDPOINT_IN: u8 = 0x01; 13 | 14 | /// Returns a device filter for Axera devices. 15 | pub fn axdl_device_filter() -> webusb_web::UsbDeviceFilter { 16 | webusb_web::UsbDeviceFilter::new() 17 | .with_vendor_id(VENDOR_ID) 18 | .with_product_id(PRODUCT_ID) 19 | } 20 | 21 | impl AsyncDevice for webusb_web::OpenUsbDevice { 22 | async fn read(&mut self, buf: &mut [u8]) -> Result { 23 | let result = self 24 | .transfer_in(ENDPOINT_IN, buf.len() as u32) 25 | .await 26 | .map_err(AxdlError::WebUsbError)?; 27 | let bytes_to_copy = result.len().min(buf.len()); 28 | 29 | buf[..bytes_to_copy].copy_from_slice(&result[..bytes_to_copy]); 30 | Ok(bytes_to_copy) 31 | } 32 | 33 | async fn write(&mut self, buf: &[u8]) -> Result { 34 | let bytes_written = self 35 | .transfer_out(ENDPOINT_OUT, buf) 36 | .await 37 | .map_err(AxdlError::WebUsbError)?; 38 | Ok(bytes_written as usize) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /wireshark/axdl.lua: -------------------------------------------------------------------------------- 1 | axdl_protocol = Proto("AXDL", "AXDL download protocol") 2 | 3 | local marker = ProtoField.uint24("axdl.marker", "Marker", base.HEX) 4 | local signature = ProtoField.uint32("axdl.signature", "Signature", base.HEX) 5 | local frame_length = ProtoField.uint16("axdl.length", "Length", base.DEC) 6 | local command = ProtoField.uint16("axdl.command", "Command", base.HEX) 7 | local data = ProtoField.bytes("axdl.data", "Data") 8 | local start_address = ProtoField.uint32("axdl.start_address", "Start Address", base.HEX) 9 | local total_length = ProtoField.uint32("axdl.total_length", "Total Length", base.DEC) 10 | local start_address_64 = ProtoField.uint64("axdl.start_address", "Start Address", base.HEX) 11 | local total_length_64 = ProtoField.uint64("axdl.total_length", "Total Length", base.DEC) 12 | local partition_name = ProtoField.string("axdl.partition_name", "Partition Name") 13 | local partition_table_header = ProtoField.uint64("axdl.partition_table_header", "Partition Table Header", base.HEX) 14 | local partition_table_entry = ProtoField.bytes("axdl.partition_table_entry", "Partition Table Entry") 15 | local partition_table_entry_name = ProtoField.string("axdl.partition_table_entry_name", "Partition Table Entry Name") 16 | local partition_table_entry_gap = ProtoField.uint64("axdl.partition_table_entry_gap", "Partition Table Entry Gap") 17 | local partition_table_entry_size = ProtoField.uint64("axdl.partition_table_entry_size", "Partition Table Entry Size") 18 | local checksum = ProtoField.uint16("axdl.checksum", "Check Sum", base.HEX) 19 | 20 | 21 | axdl_protocol.fields = { marker, signature, frame_length, command, data, start_address, total_length, start_address_64, total_length_64, partition_name, partition_table_header, partition_table_entry, partition_table_entry_name, partition_table_entry_gap, partition_table_entry_size, checksum } 22 | 23 | -- Referenced USB URB dissector fields. 24 | local f_urb_type = Field.new("usb.urb_type") 25 | local f_transfer_type = Field.new("usb.transfer_type") 26 | local f_endpoint = Field.new("usb.endpoint_address.number") 27 | local f_direction = Field.new("usb.endpoint_address.direction") 28 | 29 | function axdl_protocol.dissector(buffer, pinfo, tree) 30 | local transfer_type = tonumber(tostring(f_transfer_type())) 31 | if not(transfer_type == 3) then return 0 end 32 | 33 | -- print("debug: " .. f_urb_type()) 34 | -- local urb_type = tonumber(tostring(f_urb_type())) 35 | -- local endpoint = tonumber(tostring(f_endpoint())) 36 | -- local direction = tonumber(tostring(f_direction())) 37 | 38 | -- if not(urb_type == 83 and endpoint == 1) -- 'S' - Submit 39 | -- and not(urb_type == 67 and endpoint == 1) -- 'C' - Complete 40 | -- then 41 | -- return 0 42 | -- end 43 | 44 | length = buffer:len() 45 | if length < 3 then return end 46 | 47 | pinfo.cols.protocol = axdl_protocol.name 48 | 49 | local subtree = tree:add(axdl_protocol, buffer(), "AXDL") 50 | 51 | if length == 3 then 52 | subtree:add_le(marker, buffer(0, 3)) 53 | return 54 | end 55 | 56 | if length < 10 then return end 57 | 58 | if buffer(0, 4):le_uint() ~= 0x5c6d8e9f then 59 | -- Data packet 60 | subtree:add(data, buffer(0, length)) 61 | return 62 | end 63 | 64 | local n_command = buffer(6, 2):le_uint() 65 | local n_frame_length = buffer(4, 2):le_uint() 66 | local payload_length = length - 10 67 | 68 | subtree:add_le(signature, buffer(0, 4)) 69 | subtree:add_le(frame_length, buffer(4, 2)) 70 | subtree:add_le(command, buffer(6, 2)) 71 | local data_subtree = subtree:add (data, buffer(8, payload_length)) 72 | if n_command == 0x0001 then 73 | if payload_length == 8 then 74 | data_subtree:add_le(start_address, buffer(8, 4)) 75 | data_subtree:add_le(total_length, buffer(12, 4)) 76 | end 77 | if payload_length == 16 then 78 | data_subtree:add_le(start_address_64, buffer(8, 8)) 79 | data_subtree:add_le(total_length_64, buffer(16, 8)) 80 | end 81 | if payload_length == 88 then 82 | data_subtree:add(partition_name, buffer(8, 72), buffer(8, 72):le_ustring()) 83 | data_subtree:add_le(total_length, buffer(80, 4)) 84 | end 85 | end 86 | if n_command == 0x000b then -- Partition Table 87 | data_subtree:add_le(partition_table_header, buffer(8, 8)) 88 | for offset = 8, payload_length, 0x58 do 89 | if payload_length - offset < 0x58 then break end 90 | local entry = data_subtree:add(partition_table_entry, buffer(8 + offset, 0x58)) 91 | entry:add(partition_table_entry_name, buffer(8 + offset, 0x40), buffer(8 + offset, 0x40):le_ustring()) 92 | entry:add_le(partition_table_entry_gap, buffer(8 + offset + 0x40, 8)) 93 | entry:add_le(partition_table_entry_size, buffer(8 + offset + 0x48, 8)) 94 | end 95 | end 96 | subtree:add_le(checksum, buffer(length - 2, 2)) 97 | end 98 | 99 | function axdl_protocol.init() 100 | local usb_product_dissectors = DissectorTable.get("usb.product") 101 | 102 | -- Dissection by vendor+product ID requires that Wireshark can get the 103 | -- the device descriptor. Making a USB device available inside a VM 104 | -- will make it inaccessible from Linux, so Wireshark cannot fetch the 105 | -- descriptor by itself. However, it is sufficient if the guest requests 106 | -- the descriptor once while Wireshark is capturing. 107 | usb_product_dissectors:add(0x32c91000, axdl_protocol) 108 | 109 | -- Addendum: Protocol registration based on product ID does not always 110 | -- work as desired. Register the protocol on the interface class instead. 111 | -- The downside is that it would be a bad idea to put this into the global 112 | -- configuration, so one has to make do with -X lua_script: for now. 113 | -- local usb_bulk_dissectors = DissectorTable.get("usb.bulk") 114 | 115 | -- For some reason the "unknown" class ID is sometimes 0xFF and sometimes 116 | -- 0xFFFF. Register both to make it work all the time. 117 | -- usb_bulk_dissectors:add(0xFF, p_logic16) 118 | -- usb_bulk_dissectors:add(0xFFFF, p_logic16) 119 | end 120 | 121 | -- DissectorTable.get("usb.bulk"):add(0xffff, axdl_protocol) --------------------------------------------------------------------------------