├── Cargo.toml ├── hf2 ├── src │ ├── utils │ │ ├── testdata │ │ │ ├── sections │ │ │ ├── blinky_1.44.0 │ │ │ └── blinky_1.47.0 │ │ └── mod.rs │ ├── resetintoapp.rs │ ├── resetintobootloader.rs │ ├── startflash.rs │ ├── hidapi_trait.rs │ ├── writeflashpage.rs │ ├── writewords.rs │ ├── dmesg.rs │ ├── readwords.rs │ ├── checksumpages.rs │ ├── lib.rs │ ├── info.rs │ ├── bininfo.rs │ └── command.rs ├── Cargo.toml ├── LICENSE.md └── readme.md ├── .gitignore ├── readme.md ├── cargo-hf2 ├── Cargo.toml ├── LICENSE.md ├── readme.md └── src │ └── main.rs ├── hf2-cli ├── Cargo.toml ├── LICENSE.md ├── readme.md └── src │ └── main.rs └── .github └── workflows └── rust.yml /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cargo-hf2", 4 | "hf2-cli", 5 | "hf2", 6 | ] 7 | -------------------------------------------------------------------------------- /hf2/src/utils/testdata/sections: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobrosenthal/hf2-rs/HEAD/hf2/src/utils/testdata/sections -------------------------------------------------------------------------------- /hf2/src/utils/testdata/blinky_1.44.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobrosenthal/hf2-rs/HEAD/hf2/src/utils/testdata/blinky_1.44.0 -------------------------------------------------------------------------------- /hf2/src/utils/testdata/blinky_1.47.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobrosenthal/hf2-rs/HEAD/hf2/src/utils/testdata/blinky_1.47.0 -------------------------------------------------------------------------------- /hf2/src/resetintoapp.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{xmit, Command}; 2 | use crate::Error; 3 | 4 | ///Reset the device into user-space app. Empty tuple response. 5 | pub fn reset_into_app(d: &hidapi::HidDevice) -> Result<(), Error> { 6 | xmit(Command::new(0x0003, 0, vec![]), d) 7 | } 8 | -------------------------------------------------------------------------------- /hf2/src/resetintobootloader.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{xmit, Command}; 2 | use crate::Error; 3 | 4 | ///Reset the device into bootloader, usually for flashing. Empty tuple response. 5 | pub fn reset_into_bootloader(d: &hidapi::HidDevice) -> Result<(), Error> { 6 | xmit(Command::new(0x0004, 0, vec![]), d) 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk -------------------------------------------------------------------------------- /hf2/src/startflash.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command}; 2 | use crate::Error; 3 | 4 | /// When issued in bootloader mode, it has no effect. In user-space mode it causes handover to bootloader. A BININFO command can be issued to verify that. Empty tuple response. 5 | pub fn start_flash(d: &hidapi::HidDevice) -> Result<(), Error> { 6 | xmit(Command::new(0x0005, 0, vec![]), d)?; 7 | 8 | rx(d).map(|_| ()) 9 | } 10 | -------------------------------------------------------------------------------- /hf2/src/hidapi_trait.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, ReadWrite}; 2 | use hidapi::HidDevice; 3 | 4 | impl ReadWrite for HidDevice { 5 | fn hf2_write(&self, data: &[u8]) -> Result { 6 | self.write(data).map_err(|e| e.into()) 7 | } 8 | fn hf2_read(&self, buf: &mut [u8]) -> Result { 9 | self.read_timeout(buf, 1000).map_err(|e| e.into()) 10 | } 11 | } 12 | 13 | impl From for Error { 14 | fn from(_err: hidapi::HidError) -> Self { 15 | Error::Transmission 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hf2-rs 2 | 3 | Implements [Microsofts HID Flashing Format (HF2)](https://github.com/microsoft/uf2/blob/86e101e3a282553756161fe12206c7a609975e70/hf2.md) to upload firmware to UF2 bootloaders. UF2 is factory programmed extensively by [Microsoft MakeCode](https://www.microsoft.com/en-us/makecode) and [Adafruit](https://www.adafruit.com) hardware. 4 | 5 | * [hf2 library](https://github.com/jacobrosenthal/hf2-rs/tree/master/hf2) 6 | * [hf2 binary](https://github.com/jacobrosenthal/hf2-rs/tree/master/hf2-cli) 7 | * [hf2 cargo subcommand](https://github.com/jacobrosenthal/hf2-rs/tree/master/cargo-hf2) 8 | -------------------------------------------------------------------------------- /hf2/src/writeflashpage.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command}; 2 | use crate::Error; 3 | use scroll::Pwrite; 4 | 5 | ///Write a single page of flash memory. Empty tuple response. 6 | pub fn write_flash_page( 7 | d: &hidapi::HidDevice, 8 | target_address: u32, 9 | data: Vec, 10 | ) -> Result<(), Error> { 11 | let mut buffer = vec![0_u8; data.len() + 4]; 12 | let mut offset = 0; 13 | 14 | buffer.gwrite_with(target_address, &mut offset, scroll::LE)?; 15 | for i in &data { 16 | buffer.gwrite_with(i, &mut offset, scroll::LE)?; 17 | } 18 | 19 | xmit(Command::new(0x0006, 0, buffer), d)?; 20 | 21 | rx(d).map(|_| ()) 22 | } 23 | -------------------------------------------------------------------------------- /cargo-hf2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-hf2" 3 | version = "0.3.3" 4 | authors = ["Jacob Rosenthal <@jacobrosenthal>"] 5 | edition = "2018" 6 | description = "Cargo Subcommand for Microsoft HID Flashing Library for UF2 Bootloaders" 7 | repository = "https://github.com/jacobrosenthal/hf2-rs" 8 | keywords = ["uf2", "hid", "flash", "cargo", "subcommand"] 9 | categories = ["command-line-utilities", "development-tools", "embedded"] 10 | license = "MIT/Apache-2.0" 11 | readme = "readme.md" 12 | 13 | [dependencies] 14 | structopt = "0.3.2" 15 | colored = "2.0.0" 16 | hf2 = { version = "^0.3.0", path = "../hf2" } 17 | hidapi = "1.2.1" 18 | cargo-project = "0.2.4" 19 | pretty_env_logger = "0.3.0" 20 | maplit = "1.0.2" 21 | log = "0.4.6" 22 | -------------------------------------------------------------------------------- /hf2/src/writewords.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command}; 2 | use crate::Error; 3 | use scroll::Pwrite; 4 | 5 | ///Dual of READ WORDS, with the same constraints. Empty tuple response. 6 | pub fn write_words( 7 | d: &hidapi::HidDevice, 8 | target_address: u32, 9 | num_words: u32, 10 | words: Vec, 11 | ) -> Result<(), Error> { 12 | let mut buffer = vec![0_u8; words.len() * 4 + 8]; 13 | let mut offset = 0; 14 | 15 | buffer.gwrite_with(target_address, &mut offset, scroll::LE)?; 16 | buffer.gwrite_with(num_words, &mut offset, scroll::LE)?; 17 | for i in words { 18 | buffer.gwrite_with(i, &mut offset, scroll::LE)?; 19 | } 20 | 21 | xmit(Command::new(0x0009, 0, buffer), d)?; 22 | 23 | rx(d).map(|_| ()) 24 | } 25 | -------------------------------------------------------------------------------- /hf2-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hf2-cli" 3 | version = "0.3.3" 4 | authors = ["Jacob Rosenthal <@jacobrosenthal>"] 5 | edition = "2018" 6 | description = "CLI for Microsoft HID Flashing Library for UF2 Bootloaders" 7 | repository = "https://github.com/jacobrosenthal/hf2-rs" 8 | keywords = ["uf2", "makecode", "adafruit", "hid", "flash"] 9 | categories = ["command-line-utilities", "development-tools", "embedded"] 10 | license = "MIT/Apache-2.0" 11 | readme = "readme.md" 12 | 13 | [dependencies] 14 | structopt = "0.3.2" 15 | hf2 = { version = "^0.3.0", path = "../hf2" } 16 | hidapi = "1.2.1" 17 | pretty_env_logger = "0.4.0" 18 | maplit = "1.0.2" 19 | crc-any = { version = "2.2.3", default-features = false } 20 | log = "0.4.6" 21 | 22 | [[bin]] 23 | name = "hf2" 24 | path = "src/main.rs" 25 | -------------------------------------------------------------------------------- /hf2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hf2" 3 | version = "0.3.3" 4 | authors = ["Jacob Rosenthal <@jacobrosenthal>"] 5 | edition = "2018" 6 | description = "Microsoft HID Flashing Library for UF2 Bootloaders" 7 | repository = "https://github.com/jacobrosenthal/hf2-rs" 8 | keywords = ["uf2", "makecode", "adafruit", "hid", "flash"] 9 | license = "MIT/Apache-2.0" 10 | readme = "readme.md" 11 | 12 | [features] 13 | default = ["hidapi", "utils"] 14 | utils = ["maplit", "goblin", "crc-any"] 15 | 16 | [dependencies] 17 | scroll = { version = "0.10.0" } 18 | log = "0.4.6" 19 | hidapi = { version = "1.2.1", optional = true } 20 | maplit = { version = "1.0.2", optional = true } 21 | goblin = { version = "0.2.3", optional = true } 22 | crc-any = { version = "2.2.3", default-features = false, optional = true } 23 | -------------------------------------------------------------------------------- /hf2/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jacob Rosenthal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /cargo-hf2/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jacob Rosenthal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /hf2-cli/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jacob Rosenthal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /hf2/src/dmesg.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command, CommandResponse, CommandResponseStatus}; 2 | use crate::Error; 3 | use scroll::{ctx, Pread, LE}; 4 | 5 | ///Return internal log buffer if any. The result is a character array. 6 | 7 | pub fn dmesg(d: &hidapi::HidDevice) -> Result { 8 | xmit(Command::new(0x0010, 0, vec![]), d)?; 9 | 10 | match rx(d) { 11 | Ok(CommandResponse { 12 | status: CommandResponseStatus::Success, 13 | data, 14 | .. 15 | }) => (data.as_slice()).pread_with(0, LE), 16 | Ok(_) => Err(Error::CommandNotRecognized), 17 | Err(e) => Err(e), 18 | } 19 | } 20 | 21 | ///Response to the dmesg command 22 | #[derive(Debug, PartialEq)] 23 | pub struct DmesgResponse { 24 | pub logs: String, 25 | } 26 | 27 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for DmesgResponse { 28 | type Error = Error; 29 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 30 | let mut bytes = vec![0; this.len()]; 31 | 32 | let mut offset = 0; 33 | this.gread_inout_with(&mut offset, &mut bytes, le)?; 34 | 35 | let logs = core::str::from_utf8(&bytes)?; 36 | 37 | Ok((DmesgResponse { logs: logs.into() }, offset)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: install libusb 18 | run: sudo apt-get install libusb-1.0-0-dev 19 | - uses: actions/checkout@v2 20 | - name: Build - All Features 21 | run: cargo build --verbose --all-features 22 | - name: Build - Default 23 | run: cargo build --verbose 24 | - name: Build - Check Examples & Tests 25 | run: cargo test --all-features 26 | 27 | clippy: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: install libusb 32 | run: sudo apt-get install libusb-1.0-0-dev 33 | - uses: actions/checkout@v1 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: stable 37 | components: clippy 38 | override: true 39 | - uses: actions-rs/clippy-check@v1 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | args: --all-features 43 | 44 | format: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: install libusb 48 | run: sudo apt-get install libusb-1.0-0-dev 49 | - uses: actions/checkout@v2 50 | - uses: actions-rs/toolchain@v1 51 | with: 52 | toolchain: stable 53 | components: rustfmt 54 | override: true 55 | # rustfmt formats the project as a whole, so including only the entry points of each crate is sufficient 56 | - run: rustfmt --check --edition 2018 ./cargo-hf2/src/main.rs ./hf2/src/lib.rs ./hf2-cli/src/main.rs 57 | -------------------------------------------------------------------------------- /hf2/src/readwords.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command, CommandResponse, CommandResponseStatus}; 2 | use crate::Error; 3 | use scroll::{ctx, Pread, Pwrite, LE}; 4 | 5 | ///Read a number of words from memory. Memory is read word by word (and not byte by byte), and target_addr must be suitably aligned. This is to support reading of special IO regions. 6 | pub fn read_words( 7 | d: &hidapi::HidDevice, 8 | target_address: u32, 9 | num_words: u32, 10 | ) -> Result { 11 | let mut buffer = vec![0_u8; 8]; 12 | let mut offset = 0; 13 | 14 | buffer.gwrite_with(target_address, &mut offset, scroll::LE)?; 15 | buffer.gwrite_with(num_words, &mut offset, scroll::LE)?; 16 | 17 | xmit(Command::new(0x0008, 0, buffer), d)?; 18 | 19 | match rx(d) { 20 | Ok(CommandResponse { 21 | status: CommandResponseStatus::Success, 22 | data, 23 | .. 24 | }) => (data.as_slice()).pread_with(0, LE), 25 | Ok(_) => Err(Error::CommandNotRecognized), 26 | Err(e) => Err(e), 27 | } 28 | } 29 | 30 | ///Response to the read_words command 31 | #[derive(Debug, PartialEq)] 32 | pub struct ReadWordsResponse { 33 | pub words: Vec, 34 | } 35 | 36 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for ReadWordsResponse { 37 | type Error = Error; 38 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 39 | if this.len() < 4 { 40 | return Err(Error::Parse); 41 | } 42 | 43 | let mut words: Vec = vec![0; this.len() / 4]; 44 | 45 | let mut offset = 0; 46 | this.gread_inout_with(&mut offset, &mut words, le)?; 47 | 48 | Ok((ReadWordsResponse { words }, offset)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /hf2/src/checksumpages.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command, CommandResponse, CommandResponseStatus}; 2 | use crate::Error; 3 | use scroll::{ctx, Pread, Pwrite, LE}; 4 | 5 | ///Compute checksum of a number of pages. Maximum value for num_pages is max_message_size / 2 - 2. The checksum algorithm used is CRC-16-CCITT. 6 | pub fn checksum_pages( 7 | d: &hidapi::HidDevice, 8 | target_address: u32, 9 | num_pages: u32, 10 | ) -> Result { 11 | let mut buffer = vec![0_u8; 8]; 12 | let mut offset = 0; 13 | 14 | buffer.gwrite_with(target_address, &mut offset, scroll::LE)?; 15 | buffer.gwrite_with(num_pages, &mut offset, scroll::LE)?; 16 | 17 | xmit(Command::new(0x0007, 0, buffer), d)?; 18 | 19 | match rx(d) { 20 | Ok(CommandResponse { 21 | status: CommandResponseStatus::Success, 22 | data, 23 | .. 24 | }) => (data.as_slice()).pread_with(0, LE), 25 | Ok(_) => Err(Error::CommandNotRecognized), 26 | Err(e) => Err(e), 27 | } 28 | } 29 | 30 | ///Response to the checksum_pages command 31 | #[derive(Debug, PartialEq)] 32 | pub struct ChecksumPagesResponse { 33 | pub checksums: Vec, 34 | } 35 | 36 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for ChecksumPagesResponse { 37 | type Error = Error; 38 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 39 | if this.len() < 2 { 40 | return Err(Error::Parse); 41 | } 42 | 43 | let mut checksums: Vec = vec![0; this.len() / 2]; 44 | 45 | let mut offset = 0; 46 | this.gread_inout_with(&mut offset, &mut checksums, le)?; 47 | 48 | Ok((ChecksumPagesResponse { checksums }, offset)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /hf2/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// This command states the current mode of the device: 2 | mod bininfo; 3 | pub use bininfo::*; 4 | 5 | ///Compute checksum of a number of pages. Maximum value for num_pages is max_message_size / 2 - 2. The checksum algorithm used is CRC-16-CCITT. 6 | mod checksumpages; 7 | pub use checksumpages::*; 8 | 9 | ///Return internal log buffer if any. The result is a character array. 10 | mod dmesg; 11 | pub use dmesg::*; 12 | 13 | /// Various device information. The result is a character array. See INFO_UF2.TXT in UF2 format for details. 14 | mod info; 15 | pub use info::*; 16 | 17 | ///Read a number of words from memory. Memory is read word by word (and not byte by byte), and target_addr must be suitably aligned. This is to support reading of special IO regions. 18 | mod readwords; 19 | pub use readwords::*; 20 | 21 | ///Reset the device into user-space app. Usually, no response at all will arrive for this command. 22 | mod resetintoapp; 23 | pub use resetintoapp::*; 24 | 25 | ///Reset the device into bootloader, usually for flashing. Usually, no response at all will arrive for this command. 26 | mod resetintobootloader; 27 | pub use resetintobootloader::*; 28 | 29 | /// When issued in bootloader mode, it has no effect. In user-space mode it causes handover to bootloader. A BININFO command can be issued to verify that. 30 | mod startflash; 31 | pub use startflash::*; 32 | 33 | ///Write a single page of flash memory. No Result. 34 | mod writeflashpage; 35 | pub use writeflashpage::*; 36 | 37 | ///Dual of READ WORDS, with the same constraints. No Result. 38 | mod writewords; 39 | pub use writewords::*; 40 | 41 | /// Errors and traits to build a command 42 | mod command; 43 | 44 | #[derive(Clone, Debug)] 45 | pub enum Error { 46 | Arguments, 47 | Parse, 48 | CommandNotRecognized, 49 | Execution, 50 | Sequence, 51 | Transmission, 52 | } 53 | 54 | ///trait to implement HID devices 55 | pub trait ReadWrite { 56 | fn hf2_write(&self, data: &[u8]) -> Result; 57 | fn hf2_read(&self, buf: &mut [u8]) -> Result; 58 | } 59 | 60 | #[cfg(feature = "hidapi")] 61 | mod hidapi_trait; 62 | 63 | #[cfg(feature = "utils")] 64 | pub mod utils; 65 | -------------------------------------------------------------------------------- /hf2/src/info.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command, CommandResponse, CommandResponseStatus}; 2 | use crate::Error; 3 | use scroll::{ctx, Pread, LE}; 4 | 5 | /// Various device information. The result is a character array. See INFO_UF2.TXT in UF2 format for details. 6 | pub fn info(d: &hidapi::HidDevice) -> Result { 7 | xmit(Command::new(0x0002, 0, vec![]), d)?; 8 | 9 | match rx(d) { 10 | Ok(CommandResponse { 11 | status: CommandResponseStatus::Success, 12 | data, 13 | .. 14 | }) => (data.as_slice()).pread_with(0, LE), 15 | Ok(_) => Err(Error::CommandNotRecognized), 16 | Err(e) => Err(e), 17 | } 18 | } 19 | 20 | ///Response to the info command 21 | #[derive(Debug, PartialEq)] 22 | pub struct InfoResponse { 23 | pub info: String, 24 | } 25 | 26 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for InfoResponse { 27 | type Error = Error; 28 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 29 | let mut bytes = vec![0; this.len()]; 30 | 31 | let mut offset = 0; 32 | this.gread_inout_with(&mut offset, &mut bytes, le)?; 33 | 34 | let info = core::str::from_utf8(&bytes)?; 35 | 36 | Ok((InfoResponse { info: info.into() }, offset)) 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[test] 45 | fn parse_response() { 46 | let data: Vec = vec![ 47 | 0x55, 0x46, 0x32, 0x20, 0x42, 0x6F, 0x6F, 0x74, 0x6C, 0x6F, 0x61, 0x64, 0x65, 0x72, 48 | 0x20, 0x76, 0x33, 0x2E, 0x36, 0x2E, 0x30, 0x20, 0x53, 0x46, 0x48, 0x57, 0x52, 0x4F, 49 | 0x0D, 0x0A, 0x4D, 0x6F, 0x64, 0x65, 0x6C, 0x3A, 0x20, 0x50, 0x79, 0x47, 0x61, 0x6D, 50 | 0x65, 0x72, 0x0D, 0x0A, 0x42, 0x6F, 0x61, 0x72, 0x64, 0x2D, 0x49, 0x44, 0x3A, 0x20, 51 | 0x53, 0x41, 0x4D, 0x44, 0x35, 0x31, 0x4A, 0x31, 0x39, 0x41, 0x2D, 0x50, 0x79, 0x47, 52 | 0x61, 0x6D, 0x65, 0x72, 0x2D, 0x4D, 0x34, 0x0D, 0x0A, 53 | ]; 54 | 55 | let info_result = InfoResponse { 56 | info: "UF2 Bootloader v3.6.0 SFHWRO\r\nModel: PyGamer\r\nBoard-ID: SAMD51J19A-PyGamer-M4\r\n".into() 57 | }; 58 | 59 | let res: InfoResponse = (data.as_slice()).pread_with::(0, LE).unwrap(); 60 | 61 | assert_eq!(res, info_result); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /hf2/readme.md: -------------------------------------------------------------------------------- 1 | # hf2 2 | 3 | Implements [Microsofts HID Flashing Format (HF2)](https://github.com/microsoft/uf2/blob/86e101e3a282553756161fe12206c7a609975e70/hf2.md) to upload firmware to UF2 bootloaders. UF2 is factory programmed extensively by [Microsoft MakeCode](https://www.microsoft.com/en-us/makecode) and [Adafruit](https://www.adafruit.com) hardware. 4 | 5 | Unless you know otherwise, you probably want [cargo-hf2](https://github.com/jacobrosenthal/hf2-rs) 6 | 7 | ## prerequisites 8 | 9 | By default enables the hidapi feature and utilizes the [hidapi-sys crate](https://crates.io/crates/hidapi) which uses [libusb](https://github.com/libusb/hidapi). Presumably other transports could be added in the future that implement the ReadWrite trait internally. 10 | 11 | ### linux 12 | 13 | Youll need libusb depending on your distro you might do `sudo apt-get install libudev-dev libusb-1.0-0-dev`. 14 | 15 | If you'd like to not use sudo, you'll need udev rules. With your board plugged in and in bootloader mode, use `lsusb` to find your vendorid, seen here as 239a 16 | 17 | ```bash 18 | Bus 001 Device 087: ID 239a:001b Adafruit Industries Feather M0 19 | ``` 20 | 21 | Then put your vendorid below and save to something like /etc/udev/rules.d/99-adafruit-boards.rules 22 | 23 | ```bash 24 | ATTRS{idVendor}=="239a", ENV{ID_MM_DEVICE_IGNORE}="1" 25 | SUBSYSTEM=="usb", ATTRS{idVendor}=="239a", MODE="0666" 26 | SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", MODE="0666" 27 | ``` 28 | 29 | Then reboot or run 30 | 31 | ```bash 32 | sudo udevadm control --reload-rules 33 | sudo udevadm trigger 34 | ``` 35 | 36 | ### mac 37 | 38 | On mac, as of Catalina you will get a permissions prompt and must follow directions to allow "Input Monitoring" for the Terminal application. 39 | 40 | ## install 41 | 42 | `cargo install cargo-hf2` 43 | 44 | ## use 45 | 46 | Assuming using default feature hidapi and sloppily just grabbing the first device 47 | 48 | ```rust 49 | let api = HidApi::new().expect("Couldn't find system usb"); 50 | let dev = api.device_list().nth(0).unwrap().open_device(&api).unwrap(); 51 | let chk = hf2::checksum_pages(&dev, 0x4000, 1).unwrap(); 52 | dbg!(chk.checksums); 53 | ``` 54 | 55 | ## troubleshooting 56 | 57 | If it cant find a device, make sure your device is in a bootloader mode ready to receive firmware. 58 | 59 | ```bash 60 | thread 'main' panicked at 'Are you sure device is plugged in and in bootloader mode?: OpenHidDeviceError', src/libcore/result.rs:1165:5 61 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. 62 | ``` 63 | 64 | On the PyGamer, 2 button presses enables a blue and green screen that says PyGamer and also generally creates a flash drive which you should be able to see (though this doesn't use that method). 65 | 66 | If you find another error, be sure to run with debug to see where in the process it failed and include those logs when reporting 67 | 68 | ```bash 69 | RUST_LOG=debug cargo run 70 | ``` 71 | -------------------------------------------------------------------------------- /hf2-cli/readme.md: -------------------------------------------------------------------------------- 1 | # hf2-cli 2 | 3 | Command line implementation of the [hf2 flashing over hid protocol](https://github.com/jacobrosenthal/hf2-rs/tree/master/hf2) commonly used in by [Microsoft MakeCode](https://www.microsoft.com/en-us/makecode) and [Adafruit](https://www.adafruit.com) hardware. 4 | 5 | Unless you know otherwise, you probably want [cargo-hf2](https://github.com/jacobrosenthal/hf2-rs) 6 | 7 | ## prerequisites 8 | 9 | Utilizes the [hidapi-sys crate](https://crates.io/crates/hidapi) which uses [libusb](https://github.com/libusb/hidapi). 10 | 11 | ### linux 12 | 13 | Youll need libusb depending on your distro you might do `sudo apt-get install libudev-dev libusb-1.0-0-dev`. 14 | 15 | If you'd like to not use sudo, you'll need udev rules. With your board plugged in and in bootloader mode, use `lsusb` to find your vendorid, seen here as 239a 16 | 17 | ```bash 18 | Bus 001 Device 087: ID 239a:001b Adafruit Industries Feather M0 19 | ``` 20 | 21 | Then put your vendorid below and save to something like /etc/udev/rules.d/99-adafruit-boards.rules 22 | 23 | ```bash 24 | ATTRS{idVendor}=="239a", ENV{ID_MM_DEVICE_IGNORE}="1" 25 | SUBSYSTEM=="usb", ATTRS{idVendor}=="239a", MODE="0666" 26 | SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", MODE="0666" 27 | ``` 28 | 29 | Then reboot or run 30 | 31 | ```bash 32 | sudo udevadm control --reload-rules 33 | sudo udevadm trigger 34 | ``` 35 | 36 | ### mac 37 | 38 | On mac, as of Catalina you will get a permissions prompt and must follow directions to allow "Input Monitoring" for the Terminal application. 39 | 40 | ## install 41 | 42 | `cargo install hf2-cli` 43 | 44 | ## `hf2` flashing elf files, or as a cargo runner 45 | 46 | `cargo build --release --example blinky_basic` then `hf2 elf target/thumbv7em-none-eabihf/release/examples/blinky_basic` 47 | 48 | Hf2 will attempt to autodetect a device by sending the bininfo command to any whitelisted vid/pids it finds connected and using the first one that responds, or you can specify pid and vid (before the subcommand) instead. `hf2 --vid 0x239a --pid 0x003d elf target/thumbv7em-none-eabihf/release/examples/blinky_basic` 49 | 50 | However the optimal use is as a cargo runner. In your .cargo/config set hf2 as your runner 51 | 52 | ```toml 53 | [target.thumbv7em-none-eabihf] 54 | runner = "hf2 elf" 55 | ``` 56 | 57 | Then either `cargo run --release --example blinky_basic` Or use your ide's "run" button and it will build and upload. 58 | 59 | ## hf2 standalone to flash binaries 60 | 61 | The flash command deals in binaries, not elf files so you're going to have to get a bin with something like [cargo binutils](https://github.com/rust-embedded/cargo-binutils) `cargo objcopy --release --example blinky_basic -- -O binary blinky_basic.bin` 62 | 63 | Then all you need your bootloaders address offset. `hf2 blinky_basic.bin -a 0x4000` 64 | 65 | Hf2 will attempt to autodetect a device by sending the bininfo command to any whitelisted vid/pids it finds connected and using the first one that responds, or you can specify pid and vid (before the subcommand) instead. `hf2 -v 0x239a -p 0x003d flash -f blinky_basic.bin -a 0x4000` 66 | 67 | ## troubleshooting 68 | 69 | If it cant find a device, make sure your device is in a bootloader mode ready to receive firmware. 70 | 71 | ```bash 72 | thread 'main' panicked at 'Are you sure device is plugged in and in bootloader mode?: OpenHidDeviceError', src/libcore/result.rs:1165:5 73 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. 74 | ``` 75 | 76 | On the PyGamer, two button presses enables a blue and green screen that says PyGamer and also generally creates a flash drive which you should be able to see (though this doesn't use that method). 77 | 78 | If you find another error, be sure to run with debug to see where in the process it failed and include those logs when reporting 79 | 80 | ```bash 81 | RUST_LOG=debug hf2 -v 0x239a -p 0x003d flash -f neopixel_rainbow.bin -a 0x4000 82 | ``` 83 | -------------------------------------------------------------------------------- /cargo-hf2/readme.md: -------------------------------------------------------------------------------- 1 | # cargo-hf2 2 | 3 | Replaces the cargo build command to include flashing over usb to connected uf2 devices using [hf2 flashing over hid protocol](https://github.com/jacobrosenthal/hf2-rs/tree/master/hf2). 4 | 5 | ## prerequisites 6 | 7 | Utilizes the [hidapi-sys crate](https://crates.io/crates/hidapi) which uses [libusb](https://github.com/libusb/hidapi). 8 | 9 | ### linux 10 | 11 | Youll need libusb depending on your distro you might do `sudo apt-get install libudev-dev libusb-1.0-0-dev`. 12 | 13 | If you'd like to not use sudo, you'll need udev rules. With your board plugged in and in bootloader mode, use `lsusb` to find your vendorid, seen here as 239a 14 | 15 | ```bash 16 | Bus 001 Device 087: ID 239a:001b Adafruit Industries Feather M0 17 | ``` 18 | 19 | Then put your vendorid below and save to something like /etc/udev/rules.d/99-adafruit-boards.rules 20 | 21 | ```bash 22 | ATTRS{idVendor}=="239a", ENV{ID_MM_DEVICE_IGNORE}="1" 23 | SUBSYSTEM=="usb", ATTRS{idVendor}=="239a", MODE="0666" 24 | SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", MODE="0666" 25 | ``` 26 | 27 | Then reboot or run 28 | 29 | ```bash 30 | sudo udevadm control --reload-rules 31 | sudo udevadm trigger 32 | ``` 33 | 34 | ### mac 35 | 36 | On mac, as of Catalina you will get a permissions prompt and must follow directions to allow "Input Monitoring" for the Terminal application. 37 | 38 | ## install 39 | 40 | `cargo install cargo-hf2` 41 | 42 | ## use 43 | 44 | From a firmware directory you can run all the usual cargo build commands, --example and --release, with build replaced by hf2. Assuming the builds succeeds we open the usb device using a hardcoded whitelist and copy the file over. 45 | 46 | ```bash 47 | $ cargo hf2 --example ferris_img --release --pid 0x003d --vid 0x239a 48 | Finished release [optimized + debuginfo] target(s) in 0.28s 49 | Flashing "./target/thumbv7em-none-eabihf/release/examples/ferris_img" 50 | Success 51 | Finished in 0.037s 52 | ``` 53 | 54 | Optionally you can leave off pid and vid and it'll attempt to query any hid devices with the bininfo packet and write to the first one that responds 55 | 56 | ```bash 57 | $ cargo hf2 --example ferris_img --release 58 | Finished release [optimized + debuginfo] target(s) in 0.24s 59 | no vid/pid provided.. 60 | trying "" "Apple Internal Keyboard / Trackpad" 61 | trying "Adafruit Industries" "PyGamer" 62 | Flashing "./target/thumbv7em-none-eabihf/release/examples/ferris_img" 63 | Success 64 | Finished in 0.034s 65 | ``` 66 | 67 | If it cant find a device, make sure your device is in a bootloader mode. On the PyGamer, 2 button presses enables a blue and green screen that says PyGamer. 68 | 69 | ```bash 70 | $ cargo hf2 --example ferris_img --release 71 | Finished release [optimized + debuginfo] target(s) in 0.20s 72 | no vid/pid provided.. 73 | trying "" "Apple Internal Keyboard / Trackpad" 74 | trying "" "Keyboard Backlight" 75 | trying "" "Apple Internal Keyboard / Trackpad" 76 | trying "" "Apple Internal Keyboard / Trackpad" 77 | thread 'main' panicked at 'Are you sure device is plugged in and in bootloader mode?', src/libcore/option.rs:1166:5 78 | 79 | ``` 80 | 81 | ## troubleshooting 82 | 83 | If it cant find a device, make sure your device is in a bootloader mode ready to receive firmware. 84 | 85 | ```bash 86 | thread 'main' panicked at 'Are you sure device is plugged in and in bootloader mode?: OpenHidDeviceError', src/libcore/result.rs:1165:5 87 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. 88 | ``` 89 | 90 | On the PyGamer, 2 button presses enables a blue and green screen that says PyGamer and also generally creates a flash drive which you should be able to see (though this doesn't use that method). 91 | 92 | If you find another error, be sure to run with debug to see where in the process it failed and include those logs when reporting 93 | 94 | ```bash 95 | RUST_LOG=debug cargo hf2 --vid 0x239a --pid 0x003d 96 | ``` 97 | -------------------------------------------------------------------------------- /hf2/src/bininfo.rs: -------------------------------------------------------------------------------- 1 | use crate::command::{rx, xmit, Command, CommandResponse, CommandResponseStatus}; 2 | use crate::Error; 3 | use core::convert::TryFrom; 4 | use scroll::{ctx, Pread, LE}; 5 | 6 | #[derive(Debug, PartialEq)] 7 | pub enum BinInfoMode { 8 | //bootloader, and thus flashing of user-space programs is allowed 9 | Bootloader = 0x0001, 10 | //user-space mode. It also returns the size of flash page size (flashing needs to be done on page-by-page basis), and the maximum size of message. It is always the case that max_message_size >= flash_page_size + 64. 11 | User = 0x0002, 12 | } 13 | 14 | impl TryFrom for BinInfoMode { 15 | type Error = Error; 16 | 17 | fn try_from(value: u32) -> Result { 18 | match value { 19 | 1 => Ok(BinInfoMode::Bootloader), 20 | 2 => Ok(BinInfoMode::User), 21 | _ => Err(Error::Parse), 22 | } 23 | } 24 | } 25 | 26 | /// This command states the current mode of the device: 27 | pub fn bin_info(d: &hidapi::HidDevice) -> Result { 28 | xmit(Command::new(0x0001, 0, vec![]), d)?; 29 | 30 | match rx(d) { 31 | Ok(CommandResponse { 32 | status: CommandResponseStatus::Success, 33 | data, 34 | .. 35 | }) => (data.as_slice()).pread_with(0, LE), 36 | Ok(_) => Err(Error::CommandNotRecognized), 37 | Err(e) => Err(e), 38 | } 39 | } 40 | 41 | ///Response to the bin_info command 42 | #[derive(Debug, PartialEq)] 43 | pub struct BinInfoResponse { 44 | pub mode: BinInfoMode, // uint32_t mode; 45 | pub flash_page_size: u32, 46 | pub flash_num_pages: u32, 47 | pub max_message_size: u32, 48 | pub family_id: Option, 49 | } 50 | 51 | #[allow(non_camel_case_types)] 52 | #[derive(Debug, Copy, Clone, PartialEq)] 53 | pub enum FamilyId { 54 | ATSAMD21, 55 | ATSAMD51, 56 | NRF52840, 57 | STM32F103, 58 | STM32F401, 59 | ATMEGA32, 60 | CYPRESS_FX2, 61 | UNKNOWN(u32), 62 | } 63 | 64 | impl From for FamilyId { 65 | fn from(val: u32) -> Self { 66 | match val { 67 | 0x68ed_2b88 => Self::ATSAMD21, 68 | 0x5511_4460 => Self::ATSAMD51, 69 | 0x1b57_745f => Self::NRF52840, 70 | 0x5ee2_1072 => Self::STM32F103, 71 | 0x5775_5a57 => Self::STM32F401, 72 | 0x1657_3617 => Self::ATMEGA32, 73 | 0x5a18_069b => Self::CYPRESS_FX2, 74 | _ => Self::UNKNOWN(val), 75 | } 76 | } 77 | } 78 | 79 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for BinInfoResponse { 80 | type Error = Error; 81 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 82 | if this.len() < 16 { 83 | return Err(Error::Parse); 84 | } 85 | 86 | //does it give me offset somehow??? or just slice appropriately for me?s 87 | let mut offset = 0; 88 | let mode: u32 = this.gread_with::(&mut offset, le)?; 89 | let mode: Result = BinInfoMode::try_from(mode); 90 | let mode: BinInfoMode = mode?; 91 | let flash_page_size = this.gread_with::(&mut offset, le)?; 92 | let flash_num_pages = this.gread_with::(&mut offset, le)?; 93 | let max_message_size = this.gread_with::(&mut offset, le)?; 94 | 95 | let family_id = if this.len() >= 20 { 96 | let family_id: FamilyId = this.gread_with::(&mut offset, le)?.into(); 97 | Some(family_id) 98 | } else { 99 | None 100 | }; 101 | 102 | Ok(( 103 | BinInfoResponse { 104 | mode, 105 | flash_page_size, 106 | flash_num_pages, 107 | max_message_size, 108 | family_id, 109 | }, 110 | offset, 111 | )) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /hf2-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use hf2::utils::{elf_to_bin, flash_bin, vendor_map, verify_bin}; 2 | use hidapi::{HidApi, HidDevice}; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::PathBuf; 6 | use structopt::StructOpt; 7 | 8 | fn main() { 9 | pretty_env_logger::init(); 10 | 11 | let args = Opt::from_args(); 12 | 13 | let api = HidApi::new().expect("Couldn't find system usb"); 14 | 15 | let d = if let (Some(v), Some(p)) = (args.vid, args.pid) { 16 | api.open(v, p) 17 | .expect("Are you sure device is plugged in and in bootloader mode?") 18 | } else { 19 | println!("no vid/pid provided.."); 20 | 21 | let mut device: Option = None; 22 | 23 | let vendor = vendor_map(); 24 | 25 | for device_info in api.device_list() { 26 | if let Some(products) = vendor.get(&device_info.vendor_id()) { 27 | if products.contains(&device_info.product_id()) { 28 | if let Ok(d) = device_info.open_device(&api) { 29 | device = Some(d); 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | device.expect("Are you sure device is plugged in and in bootloader mode?") 36 | }; 37 | 38 | println!( 39 | "found {:?} {:?}", 40 | d.get_manufacturer_string(), 41 | d.get_product_string() 42 | ); 43 | 44 | match args.cmd { 45 | Cmd::resetIntoApp => hf2::reset_into_app(&d).unwrap(), 46 | Cmd::resetIntoBootloader => hf2::reset_into_bootloader(&d).unwrap(), 47 | Cmd::info => info(&d), 48 | Cmd::bininfo => bininfo(&d), 49 | Cmd::dmesg => dmesg(&d), 50 | Cmd::flash { file, address } => { 51 | let binary = get_binary(file); 52 | let bininfo = hf2::bin_info(&d).expect("bin_info failed"); 53 | log::debug!("{:?}", bininfo); 54 | 55 | flash_bin(&binary, address, &bininfo, &d).unwrap(); 56 | println!("Success") 57 | } 58 | Cmd::verify { file, address } => { 59 | let binary = get_binary(file); 60 | let bininfo = hf2::bin_info(&d).expect("bin_info failed"); 61 | log::debug!("{:?}", bininfo); 62 | 63 | verify_bin(&binary, address, &bininfo, &d).unwrap(); 64 | println!("Success") 65 | } 66 | Cmd::elf { path } => { 67 | let (binary, address) = elf_to_bin(path).unwrap(); 68 | 69 | let bininfo = hf2::bin_info(&d).expect("bin_info failed"); 70 | log::debug!("{:?}", bininfo); 71 | 72 | flash_bin(&binary, address, &bininfo, &d).unwrap(); 73 | } 74 | } 75 | } 76 | 77 | fn info(d: &HidDevice) { 78 | let info = hf2::info(d).expect("info failed"); 79 | println!("{:?}", info); 80 | } 81 | 82 | fn bininfo(d: &HidDevice) { 83 | let bininfo = hf2::bin_info(d).expect("bin_info failed"); 84 | println!( 85 | "{:?} {:?}kb", 86 | bininfo, 87 | bininfo.flash_num_pages * bininfo.flash_page_size / 1024 88 | ); 89 | } 90 | 91 | fn dmesg(d: &HidDevice) { 92 | // todo, test. not supported on my board 93 | let dmesg = hf2::dmesg(d).expect("dmesg failed"); 94 | println!("{:?}", dmesg); 95 | } 96 | 97 | fn get_binary(file: PathBuf) -> Vec { 98 | //shouldnt there be a chunking interator for this? 99 | let mut f = File::open(file).unwrap(); 100 | let mut binary = Vec::new(); 101 | f.read_to_end(&mut binary).unwrap(); 102 | binary 103 | } 104 | 105 | fn parse_hex_32(input: &str) -> Result { 106 | if let Some(stripped) = input.strip_prefix("0x") { 107 | u32::from_str_radix(stripped, 16) 108 | } else { 109 | input.parse::() 110 | } 111 | } 112 | 113 | fn parse_hex_16(input: &str) -> Result { 114 | if let Some(stripped) = input.strip_prefix("0x") { 115 | u16::from_str_radix(stripped, 16) 116 | } else { 117 | input.parse::() 118 | } 119 | } 120 | 121 | #[allow(non_camel_case_types)] 122 | #[derive(StructOpt, Debug, PartialEq)] 123 | pub enum Cmd { 124 | ///Reset the device into user-space app. 125 | resetIntoApp, 126 | ///Reset the device into bootloader, usually for flashing 127 | resetIntoBootloader, 128 | 129 | /// Various device information. The result is a character array. See INFO_UF2.TXT in UF2 format for details. 130 | info, 131 | 132 | /// This command states the current mode of the device 133 | bininfo, 134 | 135 | ///Return internal log buffer if any. The result is a character array. 136 | dmesg, 137 | 138 | /// flash binary, note includes a verify and reset into app 139 | flash { 140 | #[structopt(short = "f", name = "file", long = "file")] 141 | file: PathBuf, 142 | #[structopt(short = "a", name = "address", long = "address", parse(try_from_str = parse_hex_32))] 143 | address: u32, 144 | }, 145 | 146 | /// verify binary 147 | verify { 148 | #[structopt(short = "f", name = "file", long = "file")] 149 | file: PathBuf, 150 | #[structopt(short = "a", name = "address", long = "address", parse(try_from_str = parse_hex_32))] 151 | address: u32, 152 | }, 153 | 154 | /// flash elf, note includes a verify and reset into app 155 | elf { 156 | #[structopt(parse(from_os_str))] 157 | path: PathBuf, 158 | }, 159 | } 160 | 161 | #[derive(Debug, StructOpt)] 162 | #[structopt(name = "hf2", about = "Microsoft HID Flashing Format")] 163 | struct Opt { 164 | #[structopt(subcommand)] 165 | cmd: Cmd, 166 | 167 | #[structopt(short = "p", name = "pid", long = "pid", parse(try_from_str = parse_hex_16))] 168 | pid: Option, 169 | #[structopt(short = "v", name = "vid", long = "vid", parse(try_from_str = parse_hex_16))] 170 | vid: Option, 171 | } 172 | -------------------------------------------------------------------------------- /cargo-hf2/src/main.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use hf2::utils::{elf_to_bin, flash_bin, vendor_map}; 3 | use hidapi::{HidApi, HidDevice}; 4 | use std::path::PathBuf; 5 | use std::process::{Command, Stdio}; 6 | use std::time::Instant; 7 | use structopt::StructOpt; 8 | 9 | fn main() { 10 | // Initialize the logging backend. 11 | pretty_env_logger::init(); 12 | 13 | // Get commandline options. 14 | // Skip the first arg which is the calling application name. 15 | let opt = Opt::from_iter(std::env::args().skip(1)); 16 | 17 | // Try and get the cargo project information. 18 | let project = cargo_project::Project::query(".").expect("Couldn't parse the Cargo.toml"); 19 | 20 | // Decide what artifact to use. 21 | let artifact = if let Some(bin) = &opt.bin { 22 | cargo_project::Artifact::Bin(bin) 23 | } else if let Some(example) = &opt.example { 24 | cargo_project::Artifact::Example(example) 25 | } else { 26 | cargo_project::Artifact::Bin(project.name()) 27 | }; 28 | 29 | // Decide what profile to use. 30 | let profile = if opt.release { 31 | cargo_project::Profile::Release 32 | } else { 33 | cargo_project::Profile::Dev 34 | }; 35 | 36 | // Try and get the artifact path. 37 | let path = project 38 | .path( 39 | artifact, 40 | profile, 41 | opt.target.as_deref(), 42 | "x86_64-unknown-linux-gnu", 43 | ) 44 | .expect("Couldn't find the build result"); 45 | 46 | // Remove first two args which is the calling application name and the `hf2` command from cargo. 47 | let mut args: Vec<_> = std::env::args().skip(2).collect(); 48 | 49 | // todo, keep as iter. difficult because we want to filter map remove two items at once. 50 | // Remove our args as cargo build does not understand them. 51 | let flags = ["--pid", "--vid"].iter(); 52 | for flag in flags { 53 | if let Some(index) = args.iter().position(|x| x == flag) { 54 | args.remove(index); 55 | args.remove(index); 56 | } 57 | } 58 | 59 | let status = Command::new("cargo") 60 | .arg("build") 61 | .args(args) 62 | .stdout(Stdio::inherit()) 63 | .stderr(Stdio::inherit()) 64 | .spawn() 65 | .unwrap() 66 | .wait() 67 | .unwrap(); 68 | 69 | if !status.success() { 70 | exit_with_process_status(status) 71 | } 72 | 73 | let api = HidApi::new().expect("Couldn't find system usb"); 74 | 75 | let d = if let (Some(v), Some(p)) = (opt.vid, opt.pid) { 76 | api.open(v, p) 77 | .expect("Are you sure device is plugged in and in bootloader mode?") 78 | } else { 79 | println!( 80 | " {} for a connected device with known vid/pid pair.", 81 | "Searching".green().bold(), 82 | ); 83 | 84 | let mut device: Option = None; 85 | 86 | let vendor = vendor_map(); 87 | 88 | for device_info in api.device_list() { 89 | if let Some(products) = vendor.get(&device_info.vendor_id()) { 90 | if products.contains(&device_info.product_id()) { 91 | if let Ok(d) = device_info.open_device(&api) { 92 | device = Some(d); 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | device.expect("Are you sure device is plugged in and in bootloader mode?") 99 | }; 100 | 101 | println!( 102 | " {} {:?} {:?}", 103 | "Trying ".green().bold(), 104 | d.get_manufacturer_string(), 105 | d.get_product_string() 106 | ); 107 | 108 | println!(" {} {:?}", "Flashing".green().bold(), path); 109 | 110 | let (binary, address) = elf_to_bin(path).unwrap(); 111 | 112 | // Start timer. 113 | let instant = Instant::now(); 114 | 115 | let bininfo = hf2::bin_info(&d).expect("bin_info failed"); 116 | log::debug!("{:?}", bininfo); 117 | 118 | flash_bin(&binary, address, &bininfo, &d).unwrap(); 119 | 120 | // Stop timer. 121 | let elapsed = instant.elapsed(); 122 | println!( 123 | " {} in {}s", 124 | "Finished".green().bold(), 125 | elapsed.as_millis() as f32 / 1000.0 126 | ); 127 | } 128 | 129 | #[cfg(unix)] 130 | fn exit_with_process_status(status: std::process::ExitStatus) -> ! { 131 | use std::os::unix::process::ExitStatusExt; 132 | let status = status.code().or_else(|| status.signal()).unwrap_or(1); 133 | std::process::exit(status) 134 | } 135 | 136 | #[cfg(not(unix))] 137 | fn exit_with_process_status(status: std::process::ExitStatus) -> ! { 138 | let status = status.code().unwrap_or(1); 139 | std::process::exit(status) 140 | } 141 | 142 | fn parse_hex_16(input: &str) -> Result { 143 | if let Some(stripped) = input.strip_prefix("0x") { 144 | u16::from_str_radix(stripped, 16) 145 | } else { 146 | input.parse::() 147 | } 148 | } 149 | 150 | #[derive(Debug, StructOpt)] 151 | struct Opt { 152 | // `cargo build` arguments 153 | #[structopt(name = "binary", long = "bin")] 154 | bin: Option, 155 | #[structopt(name = "example", long = "example")] 156 | example: Option, 157 | #[structopt(name = "package", short = "p", long = "package")] 158 | package: Option, 159 | #[structopt(name = "release", long = "release")] 160 | release: bool, 161 | #[structopt(name = "target", long = "target")] 162 | target: Option, 163 | #[structopt(name = "PATH", long = "manifest-path", parse(from_os_str))] 164 | manifest_path: Option, 165 | #[structopt(long)] 166 | no_default_features: bool, 167 | #[structopt(long)] 168 | all_features: bool, 169 | #[structopt(long)] 170 | features: Vec, 171 | 172 | #[structopt(name = "pid", long = "pid", parse(try_from_str = parse_hex_16))] 173 | pid: Option, 174 | #[structopt(name = "vid", long = "vid", parse(try_from_str = parse_hex_16))] 175 | vid: Option, 176 | } 177 | -------------------------------------------------------------------------------- /hf2/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | checksum_pages, reset_into_app, start_flash, write_flash_page, BinInfoMode, BinInfoResponse, 3 | Error, 4 | }; 5 | use crc_any::CRCu16; 6 | use goblin::elf::program_header::*; 7 | use hidapi::HidDevice; 8 | use std::path::PathBuf; 9 | use std::{fs::File, io::Read}; 10 | 11 | #[derive(Debug)] 12 | pub enum UtilError { 13 | File, 14 | InvalidBinary, 15 | Elf, 16 | Internal, 17 | Communication, 18 | ContentsDifferent, 19 | } 20 | 21 | impl From for UtilError { 22 | fn from(err: Error) -> UtilError { 23 | match err { 24 | Error::Parse | Error::Transmission => UtilError::Communication, 25 | _ => UtilError::Internal, 26 | } 27 | } 28 | } 29 | 30 | /// Returns a contiguous bin with 0s between non-contiguous sections and starting address from an elf. 31 | pub fn elf_to_bin(path: PathBuf) -> Result<(Vec, u32), UtilError> { 32 | let mut file = File::open(path).map_err(|_| UtilError::File)?; 33 | let mut buffer = vec![]; 34 | file.read_to_end(&mut buffer).map_err(|_| UtilError::File)?; 35 | 36 | let binary = goblin::elf::Elf::parse(buffer.as_slice()).map_err(|_| UtilError::Elf)?; 37 | 38 | let mut start_address: u64 = 0; 39 | let mut last_address: u64 = 0; 40 | 41 | let mut data = vec![]; 42 | for (i, ph) in binary 43 | .program_headers 44 | .iter() 45 | .filter(|ph| { 46 | ph.p_type == PT_LOAD 47 | && ph.p_filesz > 0 48 | && ph.p_offset >= binary.header.e_ehsize as u64 49 | && ph.is_read() 50 | }) 51 | .enumerate() 52 | { 53 | // first time through grab the starting physical address 54 | if i == 0 { 55 | start_address = ph.p_paddr; 56 | } 57 | // on subsequent passes, if there's a gap between this section and the 58 | // previous one, fill it with zeros 59 | else { 60 | let difference = (ph.p_paddr - last_address) as usize; 61 | data.resize(data.len() + difference, 0x0); 62 | } 63 | 64 | data.extend_from_slice(&buffer[ph.p_offset as usize..][..ph.p_filesz as usize]); 65 | 66 | last_address = ph.p_paddr + ph.p_filesz; 67 | } 68 | 69 | Ok((data, start_address as u32)) 70 | } 71 | 72 | /// Flash, Verify and restart into app. 73 | pub fn flash_bin( 74 | binary: &[u8], 75 | address: u32, 76 | bininfo: &BinInfoResponse, 77 | d: &HidDevice, 78 | ) -> Result<(), UtilError> { 79 | if binary.is_empty() { 80 | return Err(UtilError::InvalidBinary); 81 | } 82 | 83 | let mut binary = binary.to_owned(); 84 | 85 | // pad zeros to page size 86 | // add divisor-1 to dividend to round up 87 | let padded_num_pages = 88 | (binary.len() as u32 + (bininfo.flash_page_size - 1)) / bininfo.flash_page_size; 89 | 90 | let padded_size = padded_num_pages * bininfo.flash_page_size; 91 | log::debug!( 92 | "binary is {} bytes, padding to {} bytes", 93 | binary.len(), 94 | padded_size 95 | ); 96 | for _i in 0..(padded_size as usize - binary.len()) { 97 | binary.push(0x0); 98 | } 99 | 100 | if bininfo.mode != BinInfoMode::Bootloader { 101 | let _ = start_flash(d).map_err(UtilError::from)?; 102 | } 103 | flash(&binary, address, bininfo, d)?; 104 | 105 | match verify(&binary, address, bininfo, d) { 106 | Ok(false) => return Err(UtilError::ContentsDifferent), 107 | Err(e) => return Err(e), 108 | Ok(true) => (), 109 | }; 110 | 111 | reset_into_app(d).map_err(UtilError::from) 112 | } 113 | 114 | /// Flashes binary writing a single page at a time. 115 | fn flash( 116 | binary: &[u8], 117 | address: u32, 118 | bininfo: &BinInfoResponse, 119 | d: &HidDevice, 120 | ) -> Result<(), UtilError> { 121 | for (page_index, page) in binary.chunks(bininfo.flash_page_size as usize).enumerate() { 122 | let target_address = address + bininfo.flash_page_size * page_index as u32; 123 | 124 | let _ = write_flash_page(d, target_address, page.to_vec()).map_err(UtilError::from)?; 125 | } 126 | Ok(()) 127 | } 128 | 129 | pub fn verify_bin( 130 | binary: &[u8], 131 | address: u32, 132 | bininfo: &BinInfoResponse, 133 | d: &HidDevice, 134 | ) -> Result<(), UtilError> { 135 | let mut binary = binary.to_owned(); 136 | 137 | // pad zeros to page size 138 | // add divisor-1 to dividend to round up 139 | let padded_num_pages = 140 | (binary.len() as u32 + (bininfo.flash_page_size - 1)) / bininfo.flash_page_size; 141 | 142 | let padded_size = padded_num_pages * bininfo.flash_page_size; 143 | 144 | for _i in 0..(padded_size as usize - binary.len()) { 145 | binary.push(0x0); 146 | } 147 | 148 | match verify(&binary, address, bininfo, d) { 149 | Ok(false) => Err(UtilError::ContentsDifferent), 150 | Err(e) => Err(e), 151 | Ok(true) => Ok(()), 152 | } 153 | } 154 | 155 | /// Verifys checksum of binary. 156 | fn verify( 157 | binary: &[u8], 158 | address: u32, 159 | bininfo: &BinInfoResponse, 160 | d: &HidDevice, 161 | ) -> Result { 162 | // get checksums of existing pages 163 | 164 | let top_address = address + binary.len() as u32; 165 | 166 | let max_pages = bininfo.max_message_size / 2 - 2; 167 | let steps = max_pages * bininfo.flash_page_size; 168 | let mut device_checksums = vec![]; 169 | 170 | for target_address in (address..top_address).step_by(steps as usize) { 171 | // add divisor-1 to dividend to round up 172 | let pages_left = (top_address - target_address + (bininfo.flash_page_size - 1)) 173 | / bininfo.flash_page_size; 174 | 175 | let num_pages = if pages_left < max_pages { 176 | pages_left 177 | } else { 178 | max_pages 179 | }; 180 | 181 | let chk = checksum_pages(d, target_address, num_pages).map_err(UtilError::from)?; 182 | device_checksums.extend_from_slice(&chk.checksums); 183 | } 184 | 185 | let mut binary_checksums = vec![]; 186 | 187 | //collect and sums so we can view all mismatches, not just first 188 | for page in binary.chunks(bininfo.flash_page_size as usize) { 189 | let mut xmodem = CRCu16::crc16xmodem(); 190 | xmodem.digest(&page); 191 | 192 | binary_checksums.push(xmodem.get_crc()); 193 | } 194 | 195 | Ok(binary_checksums.eq(&device_checksums)) 196 | } 197 | 198 | pub fn vendor_map() -> std::collections::HashMap> { 199 | maplit::hashmap! { 200 | 0x1D50 => vec![0x6110, 0x6112], 201 | 0x239A => vec![0x007F, 0x00B3, 0x003F, 0x0051, 0x0093, 0x0087, 0x003B, 0x0071, 0x0045, 0x0063, 0x0029, 0x0079, 0x0061, 0xE005, 0x0095, 0x004D, 0x006B, 0x0057, 0x00B5, 0x007D, 0x00B9, 0x0065, 0x0047, 0x0049, 0x00AF, 0x00CD, 0x00BF, 0x00C3, 0x00CB, 0x00AB, 0x00C5, 0x00A5, 0x00A7, 0x00C7, 0x002D, 0x0015, 0x001B, 0xB000, 0x0024, 0x000F, 0x0013, 0x0021, 0x0031, 0x0037, 0x0035, 0x002F, 0x002B, 0x0033, 0x0034, 0x003D, 0x0018, 0x001C, 0x001E, 0x0027, 0x0022, 0x00EF], 202 | 0x04D8 => vec![0xEC44, 0xEC64, 0xEC63, 0xEDB3, 0xEDBE, 0xEF66], 203 | 0x2341 => vec![0x0057, 0x024E, 0x8053, 0x024D], 204 | 0x16D0 => vec![0x0CDA], 205 | 0x03EB => vec![0x2402], 206 | 0x2886 => vec![0x002D, 0x000D, 0x002F], 207 | 0x1B4F => vec![0x0D23, 0x0D22, 0x0016], 208 | 0x1209 => vec![0x805A, 0x7102, 0x4D44, 0x2017], 209 | 0x3171 => vec![0x0100], 210 | 0x1915 => vec![0x521F], 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | #[test] 217 | fn elf_rustc_1_44_0() { 218 | let (_, start_addr) = super::elf_to_bin( 219 | [ 220 | env!("CARGO_MANIFEST_DIR"), 221 | "src/utils/testdata/blinky_1.44.0", 222 | ] 223 | .iter() 224 | .collect(), 225 | ) 226 | .unwrap(); 227 | assert_eq!(start_addr, 0x4000); 228 | } 229 | 230 | #[test] 231 | fn elf_rustc_1_47_0() { 232 | let (_, start_addr) = super::elf_to_bin( 233 | [ 234 | env!("CARGO_MANIFEST_DIR"), 235 | "src/utils/testdata/blinky_1.47.0", 236 | ] 237 | .iter() 238 | .collect(), 239 | ) 240 | .unwrap(); 241 | assert_eq!(start_addr, 0x4000); 242 | } 243 | 244 | #[test] 245 | fn elf_sections() { 246 | let (data, start_addr) = super::elf_to_bin( 247 | [env!("CARGO_MANIFEST_DIR"), "src/utils/testdata/sections"] 248 | .iter() 249 | .collect(), 250 | ) 251 | .unwrap(); 252 | println!("{:?}", data); 253 | assert_eq!(start_addr, 0); 254 | assert_eq!(data[0], 1); 255 | assert_eq!(data[12], 2); 256 | assert_eq!(data[20], 3); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /hf2/src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, ReadWrite}; 2 | use core::convert::TryFrom; 3 | 4 | use scroll::{ctx, Pread, Pwrite, LE}; 5 | 6 | impl From for Error { 7 | fn from(_err: scroll::Error) -> Self { 8 | Error::Parse 9 | } 10 | } 11 | 12 | impl From for Error { 13 | fn from(_err: core::str::Utf8Error) -> Self { 14 | Error::Parse 15 | } 16 | } 17 | 18 | impl From for Error { 19 | fn from(_err: std::io::Error) -> Self { 20 | Error::Arguments 21 | } 22 | } 23 | 24 | #[derive(Debug, PartialEq)] 25 | pub(crate) struct CommandResponse { 26 | ///arbitrary number set by the host, for example as sequence number. The response should repeat the tag. 27 | pub(crate) tag: u16, 28 | pub(crate) status: CommandResponseStatus, // uint8_t status; 29 | ///additional information In case of non-zero status 30 | pub(crate) status_info: u8, // optional? 31 | ///LE bytes 32 | pub(crate) data: Vec, 33 | } 34 | 35 | #[derive(Debug, PartialEq)] 36 | pub(crate) enum CommandResponseStatus { 37 | //command understood and executed correctly 38 | Success = 0x00, 39 | //command not understood 40 | ParseError = 0x01, 41 | //command execution error 42 | ExecutionError = 0x02, 43 | } 44 | 45 | impl TryFrom for CommandResponseStatus { 46 | type Error = Error; 47 | 48 | fn try_from(val: u8) -> Result { 49 | match val { 50 | 0 => Ok(CommandResponseStatus::Success), 51 | 1 => Ok(CommandResponseStatus::ParseError), 52 | 2 => Ok(CommandResponseStatus::ExecutionError), 53 | _ => Err(Error::Parse), 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, PartialEq)] 59 | enum PacketType { 60 | //Inner packet of a command message 61 | Inner = 0, 62 | //Final packet of a command message 63 | Final = 1, 64 | //Serial stdout 65 | StdOut = 2, 66 | //Serial stderr 67 | Stderr = 3, 68 | } 69 | 70 | impl TryFrom for PacketType { 71 | type Error = Error; 72 | 73 | fn try_from(val: u8) -> Result { 74 | match val { 75 | 0 => Ok(PacketType::Inner), 76 | 1 => Ok(PacketType::Final), 77 | 2 => Ok(PacketType::StdOut), 78 | 3 => Ok(PacketType::Stderr), 79 | _ => Err(Error::Parse), 80 | } 81 | } 82 | } 83 | 84 | // doesnt know what the data is supposed to be decoded as 85 | // thats linked via the seq number outside, so we cant decode here 86 | impl<'a> ctx::TryFromCtx<'a, scroll::Endian> for CommandResponse { 87 | type Error = Error; 88 | fn try_from_ctx(this: &'a [u8], le: scroll::Endian) -> Result<(Self, usize), Self::Error> { 89 | if this.len() < 4 { 90 | return Err(Error::Parse); 91 | } 92 | 93 | let mut offset = 0; 94 | let tag = this.gread_with::(&mut offset, le)?; 95 | let status: u8 = this.gread_with::(&mut offset, le)?; 96 | let status = CommandResponseStatus::try_from(status)?; 97 | let status_info = this.gread_with::(&mut offset, le)?; 98 | 99 | Ok(( 100 | CommandResponse { 101 | tag, 102 | status, 103 | status_info, 104 | data: this[offset..].to_vec(), 105 | }, 106 | offset, 107 | )) 108 | } 109 | } 110 | 111 | #[derive(Debug)] 112 | pub(crate) struct Command { 113 | ///Command ID 114 | id: u32, 115 | ///arbitrary number set by the host, for example as sequence number. The response should repeat the tag. 116 | tag: u16, 117 | ///reserved bytes in the command should be sent as zero and ignored by the device 118 | _reserved0: u8, 119 | ///reserved bytes in the command should be sent as zero and ignored by the device 120 | _reserved1: u8, 121 | ///LE bytes 122 | data: Vec, 123 | } 124 | impl Command { 125 | pub(crate) fn new(id: u32, tag: u16, data: Vec) -> Self { 126 | Self { 127 | id, 128 | tag, 129 | _reserved0: 0, 130 | _reserved1: 0, 131 | data, 132 | } 133 | } 134 | } 135 | 136 | ///Transmit a Command, command.data should already have been LE converted 137 | pub(crate) fn xmit(cmd: Command, d: &impl ReadWrite) -> Result<(), Error> { 138 | log::debug!("{:?}", cmd); 139 | 140 | //Packets are up to 64 bytes long + first byte is Report ID, 141 | let buffer = &mut [0_u8; 65]; 142 | 143 | // Report ID at 0, hardcoded to 0, header at 1 filled in later, so start at 2 144 | let mut offset = 2; 145 | 146 | //command struct is 8 bytes 147 | buffer.gwrite_with(cmd.id, &mut offset, LE)?; 148 | buffer.gwrite_with(cmd.tag, &mut offset, LE)?; 149 | buffer.gwrite_with(cmd._reserved0, &mut offset, LE)?; 150 | buffer.gwrite_with(cmd._reserved1, &mut offset, LE)?; 151 | 152 | //copy up to the first 55 bytes 153 | let mut count = if cmd.data.len() > 55 { 154 | 55 155 | } else { 156 | cmd.data.len() 157 | }; 158 | buffer.gwrite(&cmd.data[..count], &mut offset)?; 159 | 160 | //subtract header from offset for packet size 161 | if count == cmd.data.len() { 162 | buffer[1] = (PacketType::Final as u8) << 6 | (offset - 2) as u8; 163 | log::debug!("tx: {:02X?}", &buffer[..offset]); 164 | 165 | return d.hf2_write(&buffer[..offset]).map(|_| ()); 166 | } else { 167 | buffer[1] = (PacketType::Inner as u8) << 6 | (offset - 2) as u8; 168 | log::debug!("tx: {:02X?}", &buffer[..offset]); 169 | 170 | d.hf2_write(&buffer[..offset])?; 171 | } 172 | 173 | //send the rest in chunks up to 63 174 | for chunk in cmd.data[count..].chunks(63) { 175 | count += chunk.len(); 176 | 177 | if count == cmd.data.len() { 178 | buffer[1] = (PacketType::Final as u8) << 6 | chunk.len() as u8; 179 | } else { 180 | buffer[1] = (PacketType::Inner as u8) << 6 | chunk.len() as u8; 181 | } 182 | 183 | buffer[2..(chunk.len() + 2)].copy_from_slice(chunk); 184 | 185 | log::debug!("tx: {:02X?}", &buffer[..(chunk.len() + 2)]); 186 | d.hf2_write(&buffer[..(chunk.len() + 2)])?; 187 | } 188 | Ok(()) 189 | } 190 | 191 | ///Receive a CommandResponse, CommandResponse.data is not interpreted in any way. 192 | pub(crate) fn rx(d: &impl ReadWrite) -> Result { 193 | let mut bitsnbytes: Vec = vec![]; 194 | 195 | let buffer = &mut [0_u8; 64]; 196 | let mut retries = 5; 197 | 198 | // keep reading until Final packet 199 | 'outer: loop { 200 | let count = d.hf2_read(buffer)?; 201 | 202 | log::debug!("rx count: {:?}", count); 203 | 204 | if count < 1 { 205 | if retries <= 0 { 206 | return Err(Error::Parse); 207 | } else { 208 | retries -= 1; 209 | continue 'outer; 210 | } 211 | } 212 | 213 | let ptype = PacketType::try_from(buffer[0] >> 6)?; 214 | 215 | log::debug!("rx ptype: {:?}", ptype); 216 | 217 | let len: usize = (buffer[0] & 0x3F) as usize; 218 | 219 | log::debug!("rx len: {:?}", len); 220 | 221 | if len >= count { 222 | return Err(Error::Parse); 223 | } 224 | 225 | log::debug!( 226 | "rx header: {:02X?} data: {:02X?}", 227 | &buffer[0], 228 | &buffer[1..(len + 1)] 229 | ); 230 | 231 | //skip the header byte and strip excess bytes remote is allowed to send 232 | bitsnbytes.extend_from_slice(&buffer[1..(len + 1)]); 233 | 234 | if ptype != PacketType::Inner { 235 | break; 236 | } 237 | } 238 | 239 | let resp = bitsnbytes.as_slice().pread_with(0, LE)?; 240 | 241 | log::debug!("{:?}", resp); 242 | 243 | Ok(resp) 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::*; 249 | 250 | #[allow(dead_code)] 251 | pub struct MyMock 252 | where 253 | R: Fn() -> Vec, 254 | W: Fn(&[u8]) -> usize, 255 | { 256 | pub reader: R, 257 | pub writer: W, 258 | } 259 | 260 | impl ReadWrite for MyMock 261 | where 262 | R: Fn() -> Vec, 263 | W: Fn(&[u8]) -> usize, 264 | { 265 | fn hf2_write(&self, data: &[u8]) -> Result { 266 | let len = (&self.writer)(data); 267 | 268 | Ok(len) 269 | } 270 | fn hf2_read(&self, buf: &mut [u8]) -> Result { 271 | let data = (self.reader)(); 272 | 273 | for (i, val) in data.iter().enumerate() { 274 | buf[i] = *val 275 | } 276 | 277 | Ok(data.len()) 278 | } 279 | } 280 | 281 | #[test] 282 | fn send_fragmented() { 283 | let data: Vec> = vec![ 284 | vec![ 285 | 0x00, 0x3f, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 286 | 0x00, 0x00, 0x03, 0x20, 0xd7, 0x5e, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x51, 0x5f, 287 | 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 288 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 289 | 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 290 | ], 291 | vec![ 292 | 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 293 | 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 294 | 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 295 | 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 296 | 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 297 | ], 298 | vec![ 299 | 0x00, 0x3f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 300 | 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 301 | 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 302 | 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 303 | 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 304 | ], 305 | vec![ 306 | 0x00, 0x3f, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 307 | 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 308 | 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 309 | 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 310 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 311 | ], 312 | vec![ 313 | 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 0x4d, 0x5f, 0x00, 0x00, 314 | 0x4d, 0x5f, 0x00, 0x00, 315 | ], 316 | ]; 317 | 318 | let le_page: Vec = vec![ 319 | 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x03, 0x20, 0xD7, 0x5E, 0x00, 0x00, 0x4D, 0x5F, 320 | 0x00, 0x00, 0x51, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 321 | 0x4D, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 323 | 0x00, 0x00, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 324 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 325 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 326 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 327 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 328 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 329 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 330 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 331 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 332 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 333 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 334 | 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 335 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 0x00, 0x00, 336 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 337 | 0x4D, 0x5F, 0x00, 0x00, 0x4D, 0x5F, 0x00, 0x00, 338 | ]; 339 | 340 | let writer = |v: &[u8]| -> usize { 341 | static mut I: usize = 0; 342 | 343 | let res: &Vec = unsafe { 344 | let res = &data[I]; 345 | I += 1; 346 | res 347 | }; 348 | 349 | assert_eq!(res.as_slice(), v); 350 | 351 | v.len() 352 | }; 353 | 354 | let mock = MyMock { 355 | reader: || vec![], 356 | writer, 357 | }; 358 | 359 | let command = Command::new(0x0006, 4, le_page); 360 | 361 | xmit(command, &mock).unwrap(); 362 | } 363 | 364 | #[test] 365 | fn receive_fragmented() { 366 | let data: Vec> = vec![ 367 | vec![ 368 | 0x3F, 0x04, 0x00, 0x00, 0x00, 0x55, 0x46, 0x32, 0x20, 0x42, 0x6F, 0x6F, 0x74, 0x6C, 369 | 0x6F, 0x61, 0x64, 0x65, 0x72, 0x20, 0x76, 0x33, 0x2E, 0x36, 0x2E, 0x30, 0x20, 0x53, 370 | 0x46, 0x48, 0x57, 0x52, 0x4F, 0x0D, 0x0A, 0x4D, 0x6F, 0x64, 0x65, 0x6C, 0x3A, 0x20, 371 | 0x50, 0x79, 0x47, 0x61, 0x6D, 0x65, 0x72, 0x0D, 0x0A, 0x42, 0x6F, 0x61, 0x72, 0x64, 372 | 0x2D, 0x49, 0x44, 0x3A, 0x20, 0x53, 0x41, 0x4D, 373 | ], 374 | vec![ 375 | 0x54, 0x44, 0x35, 0x31, 0x4A, 0x31, 0x39, 0x41, 0x2D, 0x50, 0x79, 0x47, 0x61, 0x6D, 376 | 0x65, 0x72, 0x2D, 0x4D, 0x34, 0x0D, 0x0A, 377 | ], 378 | ]; 379 | 380 | let result: Vec = vec![ 381 | 0x55, 0x46, 0x32, 0x20, 0x42, 0x6F, 0x6F, 0x74, 0x6C, 0x6F, 0x61, 0x64, 0x65, 0x72, 382 | 0x20, 0x76, 0x33, 0x2E, 0x36, 0x2E, 0x30, 0x20, 0x53, 0x46, 0x48, 0x57, 0x52, 0x4F, 383 | 0x0D, 0x0A, 0x4D, 0x6F, 0x64, 0x65, 0x6C, 0x3A, 0x20, 0x50, 0x79, 0x47, 0x61, 0x6D, 384 | 0x65, 0x72, 0x0D, 0x0A, 0x42, 0x6F, 0x61, 0x72, 0x64, 0x2D, 0x49, 0x44, 0x3A, 0x20, 385 | 0x53, 0x41, 0x4D, 0x44, 0x35, 0x31, 0x4A, 0x31, 0x39, 0x41, 0x2D, 0x50, 0x79, 0x47, 386 | 0x61, 0x6D, 0x65, 0x72, 0x2D, 0x4D, 0x34, 0x0D, 0x0A, 387 | ]; 388 | 389 | let reader = || -> Vec { 390 | static mut I: usize = 0; 391 | 392 | let res: &Vec = unsafe { 393 | let res = &data[I]; 394 | I += 1; 395 | res 396 | }; 397 | 398 | res.to_vec() 399 | }; 400 | 401 | let mock = MyMock { 402 | reader, 403 | writer: |_v| 0, 404 | }; 405 | 406 | let response = CommandResponse { 407 | tag: 0x0004, 408 | status: CommandResponseStatus::Success, 409 | status_info: 0x00, 410 | data: result.to_vec(), 411 | }; 412 | 413 | let rsp = rx(&mock).unwrap(); 414 | assert_eq!(rsp, response); 415 | } 416 | } 417 | --------------------------------------------------------------------------------