diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 775a011..b73bf1f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,3 +1,7 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + name: Rust Build & Test on: @@ -15,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install libudev + run: sudo apt-get install -y libudev-dev - name: Build run: cargo build --verbose - name: Run tests @@ -41,5 +47,7 @@ jobs: with: toolchain: stable components: clippy + - name: Install libudev + run: sudo apt-get install -y libudev-dev - name: ci-job-clippy run: make ci-job-clippy diff --git a/.gitignore b/.gitignore index 4fffb2f..48c27c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + /target /Cargo.lock +/.vscode \ No newline at end of file diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..74b9026 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,9 @@ +Tockloader-rs +Copyright 2024 OXIDOS AUTOMOTIVE + +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in this project may not be copied, +modified, or distributed except according to those terms. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 59905e5..b7a9385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,14 @@ -[package] -name = "tockloader" -version = "0.1.0" -edition = "2021" +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] -[dependencies] -clap = { version = "4.1.1", features = ["cargo"] } +resolver= "2" + +members = [ + "tockloader-cli", + "tock-process-console", + "tbf-parser", + "tockloader-lib", +] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 3dd5012..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 The Tock Project Developers - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..9b5e401 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31235ec --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2024 OXIDOS AUTOMOTIVE + +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: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +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. \ No newline at end of file diff --git a/Makefile b/Makefile index 9cc1259..e76a3e9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ -# This Makefile has been inspired by 'tock' -# Link to their Makefile here: https://github.com/tock/tock/blob/master/Makefile -# Naming scheme and conventions have been lifted from thier "Code Review" policy set. -# Reference: https://github.com/tock/tock/blob/master/doc/CodeReview.md#3-continuous-integration +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. .PHONY: ci-job-format ci-job-format: diff --git a/README.md b/README.md index 6cf6c89..68d054f 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,51 @@ This is a work-in-progress port to Rust for Tock Loader. Please use the original Python version of [TockLoader](https://www.github.com/tock/tockloader). -## Roadmap - -This is a non exhaustive list of functionalities that should be -implemented to make TockLoader usable. - - - [x] Setup the directory structure - - [x] Implement the command line arguments parser - - [ ] Implement the serial port listener - - [ ] Implement the tockloader serial protocol - - [ ] Implement the `openocd` transport interface - - [ ] Implement the `jlink` transport interface - - [ ] Implement the TBF Parser +## Adding support for a new board + +If you want to add support for a new board, you have 3 options: + +1. Implement support for the bootloader for your board. For this, please see the +[tock-bootloader](https://github.com/tock/tock-bootloader/tree/master) repo for more details. + > Note: this approach will limit you to use the bootloader for all operations (using the + > `--serial` flag). +2. Add support for your board in +[probe-rs](https://github.com/probe-rs/probe-rs?tab=readme-ov-file#adding-targets). This should be a +straight-forward process if a CMSIS packs is available for your board. +3. Implement a custom debug probe for your board. This is the most complex option, but it will give +you the most flexibility: + - First, add your debug probe to the `Connect` enum in `tockloader-lib/src/connection.rs`. + - Then, implement each command individually. There is no predefined interface for this, as debug probes + can be very different from each other. You can take a look at the existing implementations for inspiration, and feel free to contact us if you need help. + +## Install Dev Prerequisites + +### Linux + +```bash +sudo apt update +sudo apt install libudev-dev +``` + +### WSL + +```bash +sudo apt update +sudo apt install libudev-dev pkg-config +``` + +License +------- + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7c80977..0000000 --- a/src/main.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod cli; -use cli::make_cli; - -fn main() { - let matches = make_cli().get_matches(); - - if matches.get_flag("debug") { - println!("Debug mode enabled"); - } - - match matches.subcommand() { - Some(("listen", sub_matches)) => { - println!("Got the listen subcommand"); - let default_adr = "NONE".to_string(); - let adr = sub_matches - .get_one::("app-address") - .unwrap_or(&default_adr); - println!("With App Address {adr}"); - } - // If only the "--debug" flag is set, then this branch is executed - // Or, more likely at this stage, a subcommand hasn't been implemented yet. - _ => { - println!("Could not run the provided subcommand."); - _ = make_cli().print_help(); - } - } -} diff --git a/tbf-parser/Cargo.toml b/tbf-parser/Cargo.toml new file mode 100644 index 0000000..303f71e --- /dev/null +++ b/tbf-parser/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tbf-parser" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[features] +default = [] +std = [] \ No newline at end of file diff --git a/tbf-parser/src/lib.rs b/tbf-parser/src/lib.rs new file mode 100644 index 0000000..5c01366 --- /dev/null +++ b/tbf-parser/src/lib.rs @@ -0,0 +1,16 @@ +// Adapted from tock-tbf (https://github.com/tock/tock) +// === ORIGINAL LICENSE === +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2022. +// ======================== + +//! Tock Binary Format (TBF) header parsing library. + +// Parsing the headers does not require any unsafe operations. +#![forbid(unsafe_code)] +#![no_std] + +pub mod parse; +#[allow(dead_code)] // Some fields not read on device, but read when creating headers +pub mod types; diff --git a/tbf-parser/src/parse.rs b/tbf-parser/src/parse.rs new file mode 100644 index 0000000..e367603 --- /dev/null +++ b/tbf-parser/src/parse.rs @@ -0,0 +1,335 @@ +// Adapted from tock-tbf (https://github.com/tock/tock) +// === ORIGINAL LICENSE === +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2022. +// ======================== + +//! Tock Binary Format parsing code. + +use core::convert::TryInto; +use core::iter::Iterator; +use core::mem; + +use crate::types; + +/// Takes a value and rounds it up to be aligned % 4 +macro_rules! align4 { + ($e:expr $(,)?) => { + ($e) + ((4 - (($e) % 4)) % 4) + }; +} + +/// Parse the TBF header length and the entire length of the TBF binary. +/// +/// ## Return +/// +/// If all parsing is successful: +/// - Ok((Version, TBF header length, entire TBF length)) +/// +/// If we cannot parse the header because we have run out of flash, or the +/// values are entirely wrong we return `UnableToParse`. This means we have hit +/// the end of apps in flash. +/// - Err(InitialTbfParseError::UnableToParse) +/// +/// Any other error we return an error and the length of the entire app so that +/// we can skip over it and check for the next app. +/// - Err(InitialTbfParseError::InvalidHeader(app_length)) +pub fn parse_tbf_header_lengths( + app: &[u8; 8], +) -> Result<(u16, u16, u32), types::InitialTbfParseError> { + // Version is the first 16 bits of the app TBF contents. We need this to + // correctly parse the other lengths. + // + // ## Safety + // We trust that the version number has been checked prior to running this + // parsing code. That is, whatever loaded this application has verified that + // the version is valid and therefore we can trust it. + let version = u16::from_le_bytes([app[0], app[1]]); + + match version { + 2 => { + // In version 2, the next 16 bits after the version represent + // the size of the TBF header in bytes. + let tbf_header_size = u16::from_le_bytes([app[2], app[3]]); + + // The next 4 bytes are the size of the entire app's TBF space + // including the header. This also must be checked before parsing + // this header and we trust the value in flash. + let tbf_size = u32::from_le_bytes([app[4], app[5], app[6], app[7]]); + + // Check that the header length isn't greater than the entire app, + // and is at least as large as the v2 required header (which is 16 + // bytes). If that at least looks good then return the sizes. + if u32::from(tbf_header_size) > tbf_size || tbf_header_size < 16 { + Err(types::InitialTbfParseError::InvalidHeader(tbf_size)) + } else { + Ok((version, tbf_header_size, tbf_size)) + } + } + + // Since we have to trust the total size, and by extension the version + // number, if we don't know how to handle the version this must not be + // an actual app. Likely this is just the end of the app linked list. + _ => Err(types::InitialTbfParseError::UnableToParse), + } +} + +/// Parse a TBF header stored in flash. +/// +/// The `header` must be a slice that only contains the TBF header. The caller +/// should use the `parse_tbf_header_lengths()` function to determine this +/// length to create the correct sized slice. +pub fn parse_tbf_header( + header: &[u8], + version: u16, +) -> Result { + match version { + 2 => { + // Get the required base. This will succeed because we parsed the + // first bit of the header already in `parse_tbf_header_lengths()`. + let tbf_header_base: types::TbfHeaderV2Base = header.try_into()?; + + // Calculate checksum. The checksum is the XOR of each 4 byte word + // in the header. + let mut checksum: u32 = 0; + + // Get an iterator across 4 byte fields in the header. + let header_iter = header.chunks_exact(4); + + // Iterate all chunks and XOR the chunks to compute the checksum. + for (i, chunk) in header_iter.enumerate() { + let word = u32::from_le_bytes(chunk.try_into()?); + if i == 3 { + // Skip the checksum field. + } else { + checksum ^= word; + } + } + + // Verify the header matches. + if checksum != tbf_header_base.checksum { + return Err(types::TbfParseError::ChecksumMismatch( + tbf_header_base.checksum, + checksum, + )); + } + + // Get the rest of the header. The `remaining` variable will + // continue to hold the remainder of the header we have not + // processed. + let mut remaining = header + .get(16..) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + + // If there is nothing left in the header then this is just a + // padding "app" between two other apps. + if remaining.is_empty() { + // Just padding. + Ok(types::TbfHeader::Padding(tbf_header_base)) + } else { + // This is an actual app. + + // Places to save fields that we parse out of the header + // options. + let mut main_pointer: Option = None; + let mut program_pointer: Option = None; + let mut wfr_pointer: [Option; 4] = + Default::default(); + let mut package_name_pointer: Option> = None; + let mut fixed_address_pointer: Option = None; + let mut permissions_pointer: Option> = None; + let mut storage_permissions_pointer: Option< + types::TbfHeaderV2StoragePermissions<8>, + > = None; + let mut kernel_version: Option = None; + + // Iterate the remainder of the header looking for TLV entries. + while !remaining.is_empty() { + // Get the T and L portions of the next header (if it is + // there). + let tlv_header: types::TbfTlv = remaining + .get(0..4) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?; + remaining = remaining + .get(4..) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + + match tlv_header.tipe { + types::TbfHeaderTypes::TbfHeaderMain => { + let entry_len = mem::size_of::(); + // If there is already a header do nothing: if this is a second Main + // keep the first one, if it's a Program we ignore the Main + if main_pointer.is_none() { + if tlv_header.length as usize == entry_len { + main_pointer = Some( + remaining + .get(0..entry_len) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(types::TbfParseError::BadTlvEntry( + tlv_header.tipe as usize, + )); + } + } + } + types::TbfHeaderTypes::TbfHeaderProgram => { + let entry_len = mem::size_of::(); + if program_pointer.is_none() { + if tlv_header.length as usize == entry_len { + program_pointer = Some( + remaining + .get(0..entry_len) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(types::TbfParseError::BadTlvEntry( + tlv_header.tipe as usize, + )); + } + } + } + types::TbfHeaderTypes::TbfHeaderWriteableFlashRegions => { + // Length must be a multiple of the size of a region definition. + if tlv_header.length as usize + % mem::size_of::() + == 0 + { + // Calculate how many writeable flash regions + // there are specified in this header. + let wfr_len = + mem::size_of::(); + let mut number_regions = tlv_header.length as usize / wfr_len; + + // Capture a slice with just the wfr information. + let wfr_slice = remaining + .get(0..tlv_header.length as usize) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + + // To enable a static buffer, we only support up + // to four writeable flash regions. + if number_regions > 4 { + number_regions = 4; + } + + // Convert and store each wfr. + for (i, region) in + wfr_pointer.iter_mut().enumerate().take(number_regions) + { + *region = Some( + wfr_slice + .get(i * wfr_len..(i + 1) * wfr_len) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } + } else { + return Err(types::TbfParseError::BadTlvEntry( + tlv_header.tipe as usize, + )); + } + } + + types::TbfHeaderTypes::TbfHeaderPackageName => { + let name_buf = remaining + .get(0..tlv_header.length as usize) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + + package_name_pointer = Some(name_buf.try_into()?); + } + + types::TbfHeaderTypes::TbfHeaderFixedAddresses => { + let entry_len = mem::size_of::(); + if tlv_header.length as usize == entry_len { + fixed_address_pointer = Some( + remaining + .get(0..entry_len) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(types::TbfParseError::BadTlvEntry( + tlv_header.tipe as usize, + )); + } + } + + types::TbfHeaderTypes::TbfHeaderPermissions => { + permissions_pointer = Some(remaining.try_into()?); + } + + types::TbfHeaderTypes::TbfHeaderStoragePermissions => { + storage_permissions_pointer = Some(remaining.try_into()?); + } + + types::TbfHeaderTypes::TbfHeaderKernelVersion => { + let entry_len = mem::size_of::(); + if tlv_header.length as usize == entry_len { + kernel_version = Some( + remaining + .get(0..entry_len) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(types::TbfParseError::BadTlvEntry( + tlv_header.tipe as usize, + )); + } + } + + _ => {} + } + + // All TLV blocks are padded to 4 bytes, so we need to skip + // more if the length is not a multiple of 4. + let skip_len: usize = align4!(tlv_header.length as usize); + remaining = remaining + .get(skip_len..) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + } + + let tbf_header = types::TbfHeaderV2 { + base: tbf_header_base, + main: main_pointer, + program: program_pointer, + package_name: package_name_pointer, + writeable_regions: Some(wfr_pointer), + fixed_addresses: fixed_address_pointer, + permissions: permissions_pointer, + storage_permissions: storage_permissions_pointer, + kernel_version, + }; + + Ok(types::TbfHeader::TbfHeaderV2(tbf_header)) + } + } + _ => Err(types::TbfParseError::UnsupportedVersion(version)), + } +} + +pub fn parse_tbf_footer( + footers: &[u8], +) -> Result<(types::TbfFooterV2Credentials, u32), types::TbfParseError> { + let mut remaining = footers; + let tlv_header: types::TbfTlv = remaining.try_into()?; + remaining = remaining + .get(4..) + .ok_or(types::TbfParseError::NotEnoughFlash)?; + match tlv_header.tipe { + types::TbfHeaderTypes::TbfFooterCredentials => { + let credential: types::TbfFooterV2Credentials = remaining + .get(0..tlv_header.length as usize) + .ok_or(types::TbfParseError::NotEnoughFlash)? + .try_into()?; + // Check length here + let length = tlv_header.length; + Ok((credential, length as u32)) + } + _ => Err(types::TbfParseError::BadTlvEntry(tlv_header.tipe as usize)), + } +} diff --git a/tbf-parser/src/types.rs b/tbf-parser/src/types.rs new file mode 100644 index 0000000..12a0d97 --- /dev/null +++ b/tbf-parser/src/types.rs @@ -0,0 +1,1119 @@ +// Adapted from tock-tbf (https://github.com/tock/tock) +// === ORIGINAL LICENSE === +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2022. +// ======================== + +//! Types and Data Structures for TBFs. + +use core::convert::TryInto; +use core::mem::size_of; +use core::{fmt, str}; + +/// We only support up to a fixed number of storage permissions for each of read +/// and modify. This simplification enables us to use fixed sized buffers. +const NUM_STORAGE_PERMISSIONS: usize = 8; + +/// Error when parsing just the beginning of the TBF header. This is only used +/// when establishing the linked list structure of apps installed in flash. +pub enum InitialTbfParseError { + /// We were unable to parse the beginning of the header. This either means + /// we ran out of flash, or the trusted values are invalid meaning this is + /// just empty flash after the end of the last app. This error is fine, as + /// it just means we must have hit the end of the linked list of apps. + UnableToParse, + + /// Some length or value in the header is invalid. The header parsing has + /// failed at this point. However, the total app length value is a trusted + /// field, so we return that value with this error so that we can skip over + /// this invalid app and continue to check for additional apps. + InvalidHeader(u32), +} + +impl From for InitialTbfParseError { + // Convert a slice to a parsed type. Since we control how long we make our + // slices, this conversion should never fail. If it does, then this is a bug + // in this library that must be fixed. + fn from(_error: core::array::TryFromSliceError) -> Self { + InitialTbfParseError::UnableToParse + } +} + +/// Error when parsing an app's TBF header. +pub enum TbfParseError { + /// Not enough bytes in the buffer to parse the expected field. + NotEnoughFlash, + + /// Unknown version of the TBF header. + UnsupportedVersion(u16), + + /// Checksum calculation did not match what is stored in the TBF header. + /// First value is the checksum provided, second value is the checksum we + /// calculated. + ChecksumMismatch(u32, u32), + + /// One of the TLV entries did not parse correctly. This could happen if the + /// TLV.length does not match the size of a fixed-length entry. The `usize` + /// is the value of the "tipe" field. + BadTlvEntry(usize), + + /// The app name in the TBF header could not be successfully parsed as a + /// UTF-8 string. + BadProcessName, + + /// Internal kernel error. This is a bug inside of this library. Likely this + /// means that for some reason a slice was not sized properly for parsing a + /// certain type, which is something completely controlled by this library. + /// If the slice passed in is not long enough, then a `get()` call will + /// fail and that will trigger a different error. + InternalError, + + /// The number of variable length entries (for example the number of + /// `TbfHeaderDriverPermission` entries in `TbfHeaderV2Permissions`) is + /// too long for Tock to parse. + /// This can be fixed by increasing the number in `TbfHeaderV2`. + TooManyEntries(usize), + + /// The package name is too long for Tock to parse. + /// Consider a shorter name, or increasing the maximum size. + PackageNameTooLong, +} + +impl From for TbfParseError { + // Convert a slice to a parsed type. Since we control how long we make our + // slices, this conversion should never fail. If it does, then this is a bug + // in this library that must be fixed. + fn from(_error: core::array::TryFromSliceError) -> Self { + TbfParseError::InternalError + } +} + +impl fmt::Debug for TbfParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TbfParseError::NotEnoughFlash => write!(f, "Buffer too short to parse TBF header"), + TbfParseError::UnsupportedVersion(version) => { + write!(f, "TBF version {} unsupported", version) + } + TbfParseError::ChecksumMismatch(app, calc) => write!( + f, + "Checksum verification failed: app:{:#x}, calc:{:#x}", + app, calc + ), + TbfParseError::BadTlvEntry(tipe) => write!(f, "TLV entry type {} is invalid", tipe), + TbfParseError::BadProcessName => write!(f, "Process name not UTF-8"), + TbfParseError::InternalError => write!(f, "Internal kernel error. This is a bug."), + TbfParseError::TooManyEntries(tipe) => { + write!( + f, + "There are too many variable entries of {} for Tock to parse", + tipe + ) + } + TbfParseError::PackageNameTooLong => write!(f, "The package name is too long."), + } + } +} + +// TBF structure + +/// TBF fields that must be present in all v2 headers. +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2Base { + pub(crate) version: u16, + pub(crate) header_size: u16, + pub(crate) total_size: u32, + pub(crate) flags: u32, + pub(crate) checksum: u32, +} + +/// Types in TLV structures for each optional block of the header. +#[derive(Clone, Copy, Debug)] +pub enum TbfHeaderTypes { + TbfHeaderMain = 1, + TbfHeaderWriteableFlashRegions = 2, + TbfHeaderPackageName = 3, + TbfHeaderFixedAddresses = 5, + TbfHeaderPermissions = 6, + TbfHeaderStoragePermissions = 7, + TbfHeaderKernelVersion = 8, + TbfHeaderProgram = 9, + TbfFooterCredentials = 128, + + /// Some field in the header that we do not understand. Since the TLV format + /// specifies the length of each section, if we get a field we do not + /// understand we just skip it, rather than throwing an error. + Unknown, +} + +/// The TLV header (T and L). +#[derive(Clone, Copy, Debug)] +pub struct TbfTlv { + pub(crate) tipe: TbfHeaderTypes, + pub(crate) length: u16, +} + +/// The v2 Main Header for apps. +/// +/// All apps must have either a Main Header or a Program Header. Without +/// either, the TBF object is considered padding. Main and Program Headers +/// differ in whether they specify the endpoint of the process binary; Main +/// Headers do not, while Program Headers do. A TBF with a Main Header cannot +/// have any Credentials Footers, while a TBF with a Program Header can. +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2Main { + init_fn_offset: u32, + protected_trailer_size: u32, + minimum_ram_size: u32, +} + +/// The v2 Program Header for apps. +/// +/// All apps must have either a Main Header or a Program Header. Without +/// either, the TBF object is considered padding. Main and Program Headers +/// differ in whether they specify the endpoint of the process binary; Main +/// Headers do not, while Program Headers do. A Program Header includes +/// the binary end offset so that a Verifier knows where Credentials Headers +/// start. The region between the end of the binary and the end of the TBF +/// is reserved for Credentials Footers. +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2Program { + init_fn_offset: u32, + protected_trailer_size: u32, + minimum_ram_size: u32, + binary_end_offset: u32, + version: u32, +} + +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2PackageName { + size: u32, + buffer: [u8; L], +} + +/// Writeable flash regions only need an offset and size. +/// +/// There can be multiple (or zero) flash regions defined, so this is its own +/// struct. +#[derive(Clone, Copy, Debug, Default)] +pub struct TbfHeaderV2WriteableFlashRegion { + writeable_flash_region_offset: u32, + writeable_flash_region_size: u32, +} + +/// Optional fixed addresses for flash and RAM for this process. +/// +/// If a process is compiled for a specific address this header entry lets the +/// kernel know what those addresses are. +/// +/// If this header is omitted the kernel will assume that the process is +/// position-independent and can be loaded at any (reasonably aligned) flash +/// address and can be given any (reasonable aligned) memory segment. +/// +/// If this header is included, the kernel will check these values when setting +/// up the process. If a process wants to set one fixed address but not the other, the unused one +/// can be set to 0xFFFFFFFF. +#[derive(Clone, Copy, Debug, Default)] +pub struct TbfHeaderV2FixedAddresses { + /// The absolute address of the start of RAM that the process expects. For + /// example, if the process was linked with a RAM region starting at + /// address `0x00023000`, then this would be set to `0x00023000`. + start_process_ram: u32, + /// The absolute address of the start of the process binary. This does _not_ + /// include the TBF header. This is the address the process used for the + /// start of flash with the linker. + start_process_flash: u32, +} + +#[derive(Clone, Copy, Debug, Default)] +struct TbfHeaderDriverPermission { + driver_number: u32, + offset: u32, + allowed_commands: u64, +} + +/// A list of permissions for this app +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2Permissions { + length: u16, + perms: [TbfHeaderDriverPermission; L], +} + +/// A list of storage (read/write/modify) permissions for this app. +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2StoragePermissions { + write_id: Option, + read_length: u16, + read_ids: [u32; L], + modify_length: u16, + modify_ids: [u32; L], +} + +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2KernelVersion { + major: u16, + minor: u16, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TbfFooterV2CredentialsType { + Reserved = 0, + Rsa3072Key = 1, + Rsa4096Key = 2, + SHA256 = 3, + SHA384 = 4, + SHA512 = 5, +} + +/// Reference: https://github.com/tock/tock/blob/master/doc/reference/trd-appid.md#52-credentials-footer +#[derive(Clone, Copy, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum TbfFooterV2Credentials { + Reserved(u32), + Rsa3072Key(TbfFooterV2RSA<384>), + Rsa4096Key(TbfFooterV2RSA<512>), + SHA256(TbfFooterV2SHA<32>), + SHA384(TbfFooterV2SHA<48>), + SHA512(TbfFooterV2SHA<64>), +} + +#[derive(Clone, Copy, Debug)] +pub struct TbfFooterV2SHA { + hash: [u8; L], +} + +#[derive(Clone, Copy, Debug)] +pub struct TbfFooterV2RSA { + public_key: [u8; L], + signature: [u8; L], +} + +impl TbfFooterV2SHA { + pub fn get_format(&self) -> Result { + match L { + 32 => Ok(TbfFooterV2CredentialsType::SHA256), + 48 => Ok(TbfFooterV2CredentialsType::SHA384), + 64 => Ok(TbfFooterV2CredentialsType::SHA512), + _ => Err(TbfParseError::InternalError), + } + } + + pub fn get_hash(&self) -> &[u8; L] { + &self.hash + } +} + +impl TbfFooterV2RSA { + pub fn get_format(&self) -> Result { + match L { + 384 => Ok(TbfFooterV2CredentialsType::Rsa3072Key), + 512 => Ok(TbfFooterV2CredentialsType::Rsa4096Key), + _ => Err(TbfParseError::InternalError), + } + } + + pub fn get_public_key(&self) -> &[u8; L] { + &self.public_key + } + + pub fn get_signature(&self) -> &[u8; L] { + &self.signature + } +} + +// Conversion functions from slices to the various TBF fields. + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2Base { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + if b.len() < 16 { + return Err(TbfParseError::InternalError); + } + Ok(TbfHeaderV2Base { + version: u16::from_le_bytes( + b.get(0..2) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + header_size: u16::from_le_bytes( + b.get(2..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + total_size: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + flags: u32::from_le_bytes( + b.get(8..12) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + checksum: u32::from_le_bytes( + b.get(12..16) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom for TbfHeaderTypes { + type Error = TbfParseError; + + fn try_from(h: u16) -> Result { + match h { + 1 => Ok(TbfHeaderTypes::TbfHeaderMain), + 2 => Ok(TbfHeaderTypes::TbfHeaderWriteableFlashRegions), + 3 => Ok(TbfHeaderTypes::TbfHeaderPackageName), + 5 => Ok(TbfHeaderTypes::TbfHeaderFixedAddresses), + 6 => Ok(TbfHeaderTypes::TbfHeaderPermissions), + 7 => Ok(TbfHeaderTypes::TbfHeaderStoragePermissions), + 8 => Ok(TbfHeaderTypes::TbfHeaderKernelVersion), + 9 => Ok(TbfHeaderTypes::TbfHeaderProgram), + 128 => Ok(TbfHeaderTypes::TbfFooterCredentials), + _ => Ok(TbfHeaderTypes::Unknown), + } + } +} + +impl core::convert::TryFrom<&[u8]> for TbfTlv { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + Ok(TbfTlv { + tipe: u16::from_le_bytes( + b.get(0..2) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ) + .try_into()?, + length: u16::from_le_bytes( + b.get(2..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2Main { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + // For 3 or more fields, this shortcut check reduces code size + if b.len() < 12 { + return Err(TbfParseError::InternalError); + } + Ok(TbfHeaderV2Main { + init_fn_offset: u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + protected_trailer_size: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + minimum_ram_size: u32::from_le_bytes( + b.get(8..12) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2Program { + type Error = TbfParseError; + fn try_from(b: &[u8]) -> Result { + // For 3 or more fields, this shortcut check reduces code size + if b.len() < 20 { + return Err(TbfParseError::InternalError); + } + Ok(TbfHeaderV2Program { + init_fn_offset: u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + protected_trailer_size: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + minimum_ram_size: u32::from_le_bytes( + b.get(8..12) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + binary_end_offset: u32::from_le_bytes( + b.get(12..16) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + version: u32::from_le_bytes( + b.get(16..20) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2PackageName { + type Error = TbfParseError; + + fn try_from(value: &[u8]) -> Result { + if value.len() > L { + return Err(TbfParseError::PackageNameTooLong); + } + + if str::from_utf8(value).is_err() { + return Err(TbfParseError::BadProcessName); + } + + let mut buffer = [0u8; L]; + buffer[..value.len()].copy_from_slice(value); + + Ok(TbfHeaderV2PackageName { + size: value.len() as u32, + buffer, + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2WriteableFlashRegion { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + Ok(TbfHeaderV2WriteableFlashRegion { + writeable_flash_region_offset: u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + writeable_flash_region_size: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2FixedAddresses { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + Ok(TbfHeaderV2FixedAddresses { + start_process_ram: u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + start_process_flash: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderDriverPermission { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + // For 3 or more fields, this shortcut check reduces code size + if b.len() < 16 { + return Err(TbfParseError::InternalError); + } + Ok(TbfHeaderDriverPermission { + driver_number: u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + offset: u32::from_le_bytes( + b.get(4..8) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + allowed_commands: u64::from_le_bytes( + b.get(8..16) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2Permissions { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result, Self::Error> { + let number_perms = u16::from_le_bytes( + b.get(0..2) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + + let mut perms: [TbfHeaderDriverPermission; L] = [TbfHeaderDriverPermission { + driver_number: 0, + offset: 0, + allowed_commands: 0, + }; L]; + for i in 0..number_perms as usize { + let start = 2 + (i * size_of::()); + let end = start + size_of::(); + if let Some(perm) = perms.get_mut(i) { + *perm = b + .get(start..end) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?; + } else { + return Err(TbfParseError::BadTlvEntry( + TbfHeaderTypes::TbfHeaderPermissions as usize, + )); + } + } + + Ok(TbfHeaderV2Permissions { + length: number_perms, + perms, + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2StoragePermissions { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result, Self::Error> { + let mut read_end = 6; + + let write_id = core::num::NonZeroU32::new(u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + )); + + let read_length = u16::from_le_bytes( + b.get(4..6) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + + let mut read_ids: [u32; L] = [0; L]; + for i in 0..read_length as usize { + let start = 6 + (i * size_of::()); + read_end = start + size_of::(); + if let Some(read_id) = read_ids.get_mut(i) { + *read_id = u32::from_le_bytes( + b.get(start..read_end) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(TbfParseError::BadTlvEntry( + TbfHeaderTypes::TbfHeaderStoragePermissions as usize, + )); + } + } + + let modify_length = u16::from_le_bytes( + b.get(read_end..(read_end + 2)) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + + let mut modify_ids: [u32; L] = [0; L]; + for i in 0..modify_length as usize { + let start = read_end + 2 + (i * size_of::()); + let modify_end = start + size_of::(); + if let Some(modify_id) = modify_ids.get_mut(i) { + *modify_id = u32::from_le_bytes( + b.get(start..modify_end) + .ok_or(TbfParseError::NotEnoughFlash)? + .try_into()?, + ); + } else { + return Err(TbfParseError::BadTlvEntry( + TbfHeaderTypes::TbfHeaderStoragePermissions as usize, + )); + } + } + + Ok(TbfHeaderV2StoragePermissions { + write_id, + read_length, + read_ids, + modify_length, + modify_ids, + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfHeaderV2KernelVersion { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + Ok(TbfHeaderV2KernelVersion { + major: u16::from_le_bytes( + b.get(0..2) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + minor: u16::from_le_bytes( + b.get(2..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ), + }) + } +} + +impl core::convert::TryFrom<&[u8]> for TbfFooterV2Credentials { + type Error = TbfParseError; + + fn try_from(b: &[u8]) -> Result { + let format: u32 = u32::from_le_bytes( + b.get(0..4) + .ok_or(TbfParseError::InternalError)? + .try_into()?, + ); + let ftype = match format { + 0 => TbfFooterV2CredentialsType::Reserved, + 1 => TbfFooterV2CredentialsType::Rsa3072Key, + 2 => TbfFooterV2CredentialsType::Rsa4096Key, + 3 => TbfFooterV2CredentialsType::SHA256, + 4 => TbfFooterV2CredentialsType::SHA384, + 5 => TbfFooterV2CredentialsType::SHA512, + _ => { + return Err(TbfParseError::InternalError); + } + }; + let length = match ftype { + TbfFooterV2CredentialsType::Reserved => 0, + TbfFooterV2CredentialsType::Rsa3072Key => 768, + TbfFooterV2CredentialsType::Rsa4096Key => 1024, + TbfFooterV2CredentialsType::SHA256 => 32, + TbfFooterV2CredentialsType::SHA384 => 48, + TbfFooterV2CredentialsType::SHA512 => 64, + }; + + let data = b + .get(4..(length + 4)) + .ok_or(TbfParseError::NotEnoughFlash)?; + + match ftype { + TbfFooterV2CredentialsType::Reserved => { + Ok(TbfFooterV2Credentials::Reserved(b.len() as u32)) + } + TbfFooterV2CredentialsType::SHA256 => { + Ok(TbfFooterV2Credentials::SHA256(TbfFooterV2SHA { + hash: data.try_into().map_err(|_| TbfParseError::InternalError)?, + })) + } + TbfFooterV2CredentialsType::SHA384 => { + Ok(TbfFooterV2Credentials::SHA384(TbfFooterV2SHA { + hash: data.try_into().map_err(|_| TbfParseError::InternalError)?, + })) + } + TbfFooterV2CredentialsType::SHA512 => { + Ok(TbfFooterV2Credentials::SHA512(TbfFooterV2SHA { + hash: data.try_into().map_err(|_| TbfParseError::InternalError)?, + })) + } + TbfFooterV2CredentialsType::Rsa3072Key => { + Ok(TbfFooterV2Credentials::Rsa3072Key(TbfFooterV2RSA { + public_key: data[0..length / 2] + .try_into() + .map_err(|_| TbfParseError::InternalError)?, + signature: data[length / 2..] + .try_into() + .map_err(|_| TbfParseError::InternalError)?, + })) + } + TbfFooterV2CredentialsType::Rsa4096Key => { + Ok(TbfFooterV2Credentials::Rsa4096Key(TbfFooterV2RSA { + public_key: data[0..length / 2] + .try_into() + .map_err(|_| TbfParseError::InternalError)?, + signature: data[length / 2..] + .try_into() + .map_err(|_| TbfParseError::InternalError)?, + })) + } + } + } +} + +impl TbfFooterV2Credentials { + pub fn get_type(&self) -> &str { + match self { + TbfFooterV2Credentials::Reserved(_) => "Reserved", + TbfFooterV2Credentials::Rsa3072Key(_) => "Rsa3072Key", + TbfFooterV2Credentials::Rsa4096Key(_) => "Rsa4096Key", + TbfFooterV2Credentials::SHA256(_) => "SHA256", + TbfFooterV2Credentials::SHA384(_) => "SHA384", + TbfFooterV2Credentials::SHA512(_) => "SHA512", + } + } +} + +/// The command permissions specified by the TBF header. +/// +/// Use the `get_command_permissions()` function to retrieve these. +pub enum CommandPermissions { + /// The TBF header did not specify any permissions for any driver numbers. + NoPermsAtAll, + /// The TBF header did specify permissions for at least one driver number, + /// but not for the requested driver number. + NoPermsThisDriver, + /// The bitmask of allowed command numbers starting from the offset provided + /// when this enum was created. + Mask(u64), +} + +/// Single header that can contain all parts of a v2 header. +/// +/// Note, this struct limits the number of writeable regions an app can have to +/// four since we need to statically know the length of the array to store in +/// this type. +#[derive(Clone, Copy, Debug)] +pub struct TbfHeaderV2 { + pub(crate) base: TbfHeaderV2Base, + pub(crate) main: Option, + pub(crate) program: Option, + pub(crate) package_name: Option>, + pub(crate) writeable_regions: Option<[Option; 4]>, + pub(crate) fixed_addresses: Option, + pub(crate) permissions: Option>, + pub(crate) storage_permissions: Option>, + pub(crate) kernel_version: Option, +} + +/// Type that represents the fields of the Tock Binary Format header. +/// +/// This specifies the locations of the different code and memory sections +/// in the tock binary, as well as other information about the application. +/// The kernel can also use this header to keep persistent state about +/// the application. +#[derive(Debug)] +// Clippy suggests we box TbfHeaderV2. We can't really do that, since +// we are runnning under no_std, and I don't think it's that big of a issue. +#[allow(clippy::large_enum_variant)] +pub enum TbfHeader { + TbfHeaderV2(TbfHeaderV2), + Padding(TbfHeaderV2Base), +} + +impl TbfHeader { + /// Return the length of the header. + pub fn length(&self) -> u16 { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.base.header_size, + TbfHeader::Padding(base) => base.header_size, + } + } + + /// Return whether this is an app or just padding between apps. + pub fn is_app(&self) -> bool { + match *self { + TbfHeader::TbfHeaderV2(_) => true, + TbfHeader::Padding(_) => false, + } + } + + /// Return whether the application is enabled or not. + /// Disabled applications are not started by the kernel. + pub fn enabled(&self) -> bool { + match *self { + TbfHeader::TbfHeaderV2(hd) => { + // Bit 1 of flags is the enable/disable bit. + hd.base.flags & 0x00000001 == 1 + } + TbfHeader::Padding(_) => false, + } + } + + /// Return whether the application is sticky or not. + /// Sticky applications require additional confirmation to be erased. + pub fn sticky(&self) -> bool { + match *self { + TbfHeader::TbfHeaderV2(hd) => { + // Bit 2 of flags is the sticky bit. + hd.base.flags & 0x00000002 != 0 + } + TbfHeader::Padding(_) => false, + } + } + + /// Return total size of the application. + pub fn total_size(&self) -> u32 { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.base.total_size, + TbfHeader::Padding(_) => 0, + } + } + + /// Return checksum of the application. + pub fn checksum(&self) -> u32 { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.base.checksum, + TbfHeader::Padding(_) => 0, + } + } + + /// Return header size of the application. + pub fn header_size(&self) -> u16 { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.base.header_size, + TbfHeader::Padding(_) => 0, + } + } + + /// Add up all of the relevant fields in header version 1, or just used the + /// app provided value in version 2 to get the total amount of RAM that is + /// needed for this app. + pub fn get_minimum_app_ram_size(&self) -> u32 { + match *self { + TbfHeader::TbfHeaderV2(hd) => { + if hd.program.is_some() { + hd.program.map_or(0, |p| p.minimum_ram_size) + } else if hd.main.is_some() { + hd.main.map_or(0, |m| m.minimum_ram_size) + } else { + 0 + } + } + _ => 0, + } + } + + /// Get the number of bytes from the start of the app's region in flash that + /// is for kernel use only. The app cannot write this region. + pub fn get_protected_size(&self) -> u32 { + match *self { + TbfHeader::TbfHeaderV2(hd) => { + if hd.program.is_some() { + hd.program.map_or(0, |p| { + (hd.base.header_size as u32) + p.protected_trailer_size + }) + } else if hd.main.is_some() { + hd.main.map_or(0, |m| { + (hd.base.header_size as u32) + m.protected_trailer_size + }) + } else { + 0 + } + } + _ => 0, + } + } + + /// Get the start offset of the application binary from the beginning + /// of the process binary (start of the TBF header). Only valid if this + /// is an app. + pub fn get_app_start_offset(&self) -> u32 { + // The application binary starts after the header plus any + // additional protected space. + self.get_protected_size() + } + + /// Get the offset from the beginning of the app's flash region where the + /// app should start executing. + pub fn get_init_function_offset(&self) -> u32 { + match *self { + TbfHeader::TbfHeaderV2(hd) => { + if hd.program.is_some() { + hd.program + .map_or(0, |p| p.init_fn_offset + (hd.base.header_size as u32)) + } else if hd.main.is_some() { + hd.main + .map_or(0, |m| m.init_fn_offset + (hd.base.header_size as u32)) + } else { + 0 + } + } + _ => 0, + } + } + + /// Get the name of the app. + // Note: We could return Result instead. So far, no editing methods have been implemented, and when the PackageName struct is created + // the str::from_utf8 function is ran beforehand to make sure the bytes are valid UTF-8. + pub fn get_package_name(&self) -> Option<&str> { + match self { + TbfHeader::TbfHeaderV2(hd) => hd.package_name.as_ref().map(|name| { + str::from_utf8(&name.buffer[..name.size as usize]).expect("Package name is not valid UTF8. Conversion should have been checked beforehand.") + }), + _ => None, + } + } + + /// Get the number of flash regions this app has specified in its header. + pub fn number_writeable_flash_regions(&self) -> usize { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.writeable_regions.map_or(0, |wrs| { + wrs.iter() + .fold(0, |acc, wr| if wr.is_some() { acc + 1 } else { acc }) + }), + _ => 0, + } + } + + /// Get the offset and size of a given flash region. + pub fn get_writeable_flash_region(&self, index: usize) -> (u32, u32) { + match *self { + TbfHeader::TbfHeaderV2(hd) => hd.writeable_regions.map_or((0, 0), |wrs| { + wrs.get(index).unwrap_or(&None).map_or((0, 0), |wr| { + ( + wr.writeable_flash_region_offset, + wr.writeable_flash_region_size, + ) + }) + }), + _ => (0, 0), + } + } + + /// Get the address in RAM this process was specifically compiled for. If + /// the process is position independent, return `None`. + pub fn get_fixed_address_ram(&self) -> Option { + let hd = match self { + TbfHeader::TbfHeaderV2(hd) => hd, + _ => return None, + }; + match hd.fixed_addresses.as_ref()?.start_process_ram { + 0xFFFFFFFF => None, + start => Some(start), + } + } + + /// Get the address in flash this process was specifically compiled for. If + /// the process is position independent, return `None`. + pub fn get_fixed_address_flash(&self) -> Option { + let hd = match self { + TbfHeader::TbfHeaderV2(hd) => hd, + _ => return None, + }; + match hd.fixed_addresses.as_ref()?.start_process_flash { + 0xFFFFFFFF => None, + start => Some(start), + } + } + + /// Get the permissions for a specified driver and offset. + /// + /// - `driver_num`: The driver to lookup. + /// - `offset`: The offset for the driver to find. An offset value of 1 will + /// find a header with offset 1, so the `allowed_commands` will cover + /// command numbers 64 to 127. + /// + /// If permissions are found for the driver number, this function will + /// return `CommandPermissions::Mask`. If there are permissions in the + /// header but not for this driver the function will return + /// `CommandPermissions::NoPermsThisDriver`. If the process does not have + /// any permissions specified, return `CommandPermissions::NoPermsAtAll`. + pub fn get_command_permissions(&self, driver_num: usize, offset: usize) -> CommandPermissions { + match self { + TbfHeader::TbfHeaderV2(hd) => match hd.permissions { + Some(permissions) => { + let mut found_driver_num: bool = false; + for perm in permissions.perms { + if perm.driver_number == driver_num as u32 { + found_driver_num = true; + if perm.offset == offset as u32 { + return CommandPermissions::Mask(perm.allowed_commands); + } + } + } + if found_driver_num { + // We found this driver number but nothing matched the + // requested offset. Since permissions are default off, + // we can return a mask of all zeros. + CommandPermissions::Mask(0) + } else { + CommandPermissions::NoPermsThisDriver + } + } + _ => CommandPermissions::NoPermsAtAll, + }, + _ => CommandPermissions::NoPermsAtAll, + } + } + + /// Get the process `write_id`. + /// + /// Returns `None` if a `write_id` is not included. This indicates the TBF + /// does not have the ability to store new items. + pub fn get_storage_write_id(&self) -> Option { + match self { + TbfHeader::TbfHeaderV2(hd) => match hd.storage_permissions { + Some(permissions) => permissions.write_id, + _ => None, + }, + _ => None, + } + } + + /// Get the number of valid `read_ids` and the `read_ids`. + /// Returns `None` if a `read_ids` is not included. + pub fn get_storage_read_ids(&self) -> Option<(usize, [u32; NUM_STORAGE_PERMISSIONS])> { + match self { + TbfHeader::TbfHeaderV2(hd) => hd + .storage_permissions + .map(|permissions| (permissions.read_length.into(), permissions.read_ids)), + _ => None, + } + } + + /// Get the number of valid `access_ids` and the `access_ids`. + /// Returns `None` if a `access_ids` is not included. + pub fn get_storage_modify_ids(&self) -> Option<(usize, [u32; NUM_STORAGE_PERMISSIONS])> { + match self { + TbfHeader::TbfHeaderV2(hd) => hd + .storage_permissions + .map(|permissions| (permissions.modify_length.into(), permissions.modify_ids)), + _ => None, + } + } + + /// Get the minimum compatible kernel version this process requires. + /// Returns `None` if the kernel compatibility header is not included. + pub fn get_kernel_version(&self) -> Option<(u16, u16)> { + match self { + TbfHeader::TbfHeaderV2(hd) => hd + .kernel_version + .map(|kernel_version| (kernel_version.major, kernel_version.minor)), + _ => None, + } + } + + /// Return the offset where the binary ends in the TBF or 0 if there + /// is no binary. If there is a Main header the end offset is the size + /// of the TBF, while if there is a Program header it can be smaller. + pub fn get_binary_end(&self) -> u32 { + match self { + TbfHeader::TbfHeaderV2(hd) => hd + .program + .map_or(hd.base.total_size, |p| p.binary_end_offset), + _ => 0, + } + } + + /// Return the version number of the Userspace Binary in this TBF + /// Object, or 0 if there is no binary or no version number. + pub fn get_binary_version(&self) -> u32 { + match self { + TbfHeader::TbfHeaderV2(hd) => hd.program.map_or(0, |p| p.version), + _ => 0, + } + } +} diff --git a/tbf-parser/tests/flashes/RSA4096.key b/tbf-parser/tests/flashes/RSA4096.key new file mode 100755 index 0000000..bd9959c Binary files /dev/null and b/tbf-parser/tests/flashes/RSA4096.key differ diff --git a/tbf-parser/tests/flashes/RSA4096.sig b/tbf-parser/tests/flashes/RSA4096.sig new file mode 100755 index 0000000..44b7113 Binary files /dev/null and b/tbf-parser/tests/flashes/RSA4096.sig differ diff --git a/tbf-parser/tests/flashes/footerRSA4096.dat b/tbf-parser/tests/flashes/footerRSA4096.dat new file mode 100755 index 0000000..dc1f981 Binary files /dev/null and b/tbf-parser/tests/flashes/footerRSA4096.dat differ diff --git a/tbf-parser/tests/flashes/footerSHA256.dat b/tbf-parser/tests/flashes/footerSHA256.dat new file mode 100755 index 0000000..0c4a122 Binary files /dev/null and b/tbf-parser/tests/flashes/footerSHA256.dat differ diff --git a/tbf-parser/tests/flashes/simple.dat b/tbf-parser/tests/flashes/simple.dat new file mode 100755 index 0000000..8b3e671 Binary files /dev/null and b/tbf-parser/tests/flashes/simple.dat differ diff --git a/tbf-parser/tests/parse.rs b/tbf-parser/tests/parse.rs new file mode 100644 index 0000000..9a33de8 --- /dev/null +++ b/tbf-parser/tests/parse.rs @@ -0,0 +1,124 @@ +use tbf_parser::{ + parse::*, + types::{TbfFooterV2Credentials, TbfFooterV2CredentialsType}, +}; + +#[test] +fn simple_tbf() { + let buffer = include_bytes!("./flashes/simple.dat").to_vec(); + + let (ver, header_len, whole_len) = parse_tbf_header_lengths(&buffer[0..8].try_into().unwrap()) + .ok() + .unwrap(); + assert_eq!(ver, 2); + assert_eq!(header_len, 52); + assert_eq!(whole_len, 8192); + + let header = parse_tbf_header(&buffer[0..header_len as usize], 2).unwrap(); + dbg!(&header); + assert!(header.enabled()); + assert_eq!(header.get_minimum_app_ram_size(), 4848); + assert_eq!(header.get_init_function_offset(), 41 + header_len as u32); + assert_eq!(header.get_protected_size(), header_len as u32); + assert_eq!(header.get_package_name().unwrap(), "_heart"); + assert_eq!(header.get_kernel_version().unwrap(), (2, 0)); +} + +#[test] +fn footer_sha256() { + let buffer: Vec = include_bytes!("./flashes/footerSHA256.dat").to_vec(); + + let (ver, header_len, whole_len) = parse_tbf_header_lengths(&buffer[0..8].try_into().unwrap()) + .ok() + .unwrap(); + assert_eq!(ver, 2); + assert_eq!(header_len, 76); + assert_eq!(whole_len, 8192); + + let header = parse_tbf_header(&buffer[0..header_len as usize], 2).unwrap(); + dbg!(&header); + assert!(header.enabled()); + assert_eq!(header.get_minimum_app_ram_size(), 4848); + assert_eq!(header.get_init_function_offset(), 41 + header_len as u32); + assert_eq!(header.get_protected_size(), header_len as u32); + assert_eq!(header.get_package_name().unwrap(), "_heart"); + assert_eq!(header.get_kernel_version().unwrap(), (2, 0)); + let binary_offset = header.get_binary_end() as usize; + assert_eq!(binary_offset, 5836); + + let (footer, footer_size) = parse_tbf_footer(&buffer[binary_offset..]).unwrap(); + dbg!(footer); + assert_eq!(footer_size, 36); + let correct_sha256 = [ + 214u8, 17, 81, 32, 51, 178, 249, 35, 161, 33, 109, 184, 195, 46, 238, 158, 141, 54, 63, 94, + 60, 245, 50, 228, 239, 107, 231, 127, 220, 158, 77, 160, + ]; + if let TbfFooterV2Credentials::SHA256(creds) = footer { + assert_eq!( + creds.get_format().unwrap(), + TbfFooterV2CredentialsType::SHA256 + ); + assert_eq!(creds.get_hash(), &correct_sha256); + } else { + panic!("Footer is not of type SHA256!"); + } + + let second_footer_offset = binary_offset + footer_size as usize + 4; + let (footer, footer_size) = parse_tbf_footer(&buffer[second_footer_offset..]).unwrap(); + dbg!(footer); + assert_eq!(footer_size, 2312); + if let TbfFooterV2Credentials::Reserved(padding) = footer { + assert_eq!(padding, 2312); + } else { + panic!("Footer is not of type 'Reserved'!"); + } +} + +#[test] +fn footer_rsa4096() { + let buffer: Vec = include_bytes!("./flashes/footerRSA4096.dat").to_vec(); + + let (ver, header_len, whole_len) = parse_tbf_header_lengths(&buffer[0..8].try_into().unwrap()) + .ok() + .unwrap(); + assert_eq!(ver, 2); + assert_eq!(header_len, 76); + assert_eq!(whole_len, 4096); + + let header = parse_tbf_header(&buffer[0..header_len as usize], 2).unwrap(); + dbg!(&header); + assert!(header.enabled()); + assert_eq!(header.get_minimum_app_ram_size(), 4612); + assert_eq!(header.get_init_function_offset(), 41 + header_len as u32); + assert_eq!(header.get_protected_size(), header_len as u32); + assert_eq!(header.get_package_name().unwrap(), "c_hello"); + assert_eq!(header.get_kernel_version().unwrap(), (2, 0)); + let binary_offset = header.get_binary_end() as usize; + assert_eq!(binary_offset, 1168); + + let (footer, footer_size) = parse_tbf_footer(&buffer[binary_offset..]).unwrap(); + dbg!(footer); + assert_eq!(footer_size, 1028); + let correct_key = include_bytes!("./flashes/RSA4096.key"); + let correct_signature = include_bytes!("./flashes/RSA4096.sig"); + if let TbfFooterV2Credentials::Rsa4096Key(creds) = footer { + assert_eq!( + creds.get_format().unwrap(), + TbfFooterV2CredentialsType::Rsa4096Key + ); + assert_eq!(creds.get_public_key(), correct_key); + assert_eq!(creds.get_signature(), correct_signature); + } else { + panic!("Footer is not of type SHA256!"); + } + + let second_footer_offset = binary_offset + footer_size as usize + 4; + let (footer, footer_size) = parse_tbf_footer(&buffer[second_footer_offset..]).unwrap(); + dbg!(footer); + assert_eq!(footer_size, 1892); + if let TbfFooterV2Credentials::Reserved(padding) = footer { + assert_eq!(padding, 1892); + } else { + panic!("Footer is not of type 'Reserved'!"); + } +} diff --git a/tock-process-console/.gitignore b/tock-process-console/.gitignore new file mode 100644 index 0000000..271979a --- /dev/null +++ b/tock-process-console/.gitignore @@ -0,0 +1,7 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + +/target +*.log +.DS_Store diff --git a/tock-process-console/Cargo.toml b/tock-process-console/Cargo.toml new file mode 100644 index 0000000..92f706f --- /dev/null +++ b/tock-process-console/Cargo.toml @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + +[package] +name = "tock-process-console" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +crossterm = { version = "0.27.0", features = ["event-stream"] } +circular-queue = "0.2.6" +tokio = { version = "1.32.0", features = ["full"] } +anyhow = "1.0.75" +tokio-serial = {version = "5.4.4", features = ["libudev"]} +tokio-stream = { version = "0.1.14" } +tui-term = "0.1.6" +bytes = "1.5.0" +vt100 = "0.15.2" +queues = "1.0.2" +itertools = "0.12.1" +ratatui = "0.28.0" +probe-rs = "0.24.0" + diff --git a/tock-process-console/src/board/connection.rs b/tock-process-console/src/board/connection.rs new file mode 100644 index 0000000..3263ef9 --- /dev/null +++ b/tock-process-console/src/board/connection.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use anyhow::Error; +use bytes::Bytes; +use tokio::{ + io::{split, AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, +}; +use tokio_serial::{SerialPortBuilderExt, SerialStream}; + +use super::{ + decoder::{BoardMessage, Decoder}, + event::{Event, NewMessageEvent}, +}; + +pub struct ConnectionHandler { + port_reader: ReadHalf, + port_writer: WriteHalf, + event_writer: UnboundedSender, + command_reader: UnboundedReceiver, + decoder: Decoder, + decoded_receiver: UnboundedReceiver, +} + +impl ConnectionHandler { + pub async fn connection_init( + tty: &str, + ) -> Result<(UnboundedReceiver, UnboundedSender), Error> { + let mut port: SerialStream = tokio_serial::new(tty, 115200).open_native_async()?; + + #[cfg(unix)] + port.set_exclusive(false)?; + + let (port_reader, port_writer) = split(port); + + let (event_writer, event_reader) = mpsc::unbounded_channel::(); + let (command_writer, command_reader) = mpsc::unbounded_channel::(); + + // let (encoded_sender, encoded_receiver) = mpsc::unbounded_channel::(); + let (decoded_sender, decoded_receiver) = mpsc::unbounded_channel::(); + let decoder = Decoder::new(decoded_sender); + + let handler = ConnectionHandler { + port_reader, + port_writer, + event_writer, + command_reader, + decoder, + decoded_receiver, + }; + + handler.start().await; + Ok((event_reader, command_writer)) + } + + pub async fn start(mut self) { + tokio::spawn(async move { + let mut buffer = [0u8; 4096]; + + loop { + tokio::select! { + // We receive a command from the user + Some(command) = self.command_reader.recv() => { + let _ = self.port_writer.write(&command).await; + }, + // We read something from the serial port + port_read_result = self.port_reader.read(&mut buffer) => { + match port_read_result { + Ok(len) => { + let vec = buffer[..len].to_vec(); + self.decoder.send_encoded_message(vec); + }, + // We encountered an error while reading from the serial port + // Means thath we lost the connection + Err(err) => { + let _ = self.event_writer.send( + Event::LostConnection(err.to_string()) + ); + } + } + }, + received_decoded_result = self.decoded_receiver.recv() => { + if let Some(board_message) = received_decoded_result { + let app_name = if board_message.is_app { + format!("app_{}", board_message.pid) + } else if board_message.pid == 0 { + String::from("debug") + } else { + String::from("kernel") + }; + + let _ = self.event_writer.send( + Event::NewMessage( + NewMessageEvent{ + app: app_name, + pid: board_message.pid, + is_app: board_message.is_app, + payload: board_message.payload + } + ) + ); + } + } + } + } + }); + } +} diff --git a/tock-process-console/src/board/decoder.rs b/tock-process-console/src/board/decoder.rs new file mode 100644 index 0000000..ccf80f8 --- /dev/null +++ b/tock-process-console/src/board/decoder.rs @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use tokio::sync::mpsc::UnboundedSender; + +#[derive(Default)] +pub struct BoardMessage { + pub pid: u8, + pub is_app: bool, + pub payload: Vec, +} + +pub struct Decoder { + buffer: Vec, + decoded_sender: UnboundedSender, +} + +impl Decoder { + pub fn new(decoded_sender: UnboundedSender) -> Self { + Decoder { + buffer: Vec::new(), + decoded_sender, + } + } + + pub fn send_encoded_message(&mut self, mut buffer: Vec) { + while buffer.contains(&0xFF) { + let tail_index: usize = buffer.iter().position(|byte: &u8| byte.eq(&0xFF)).unwrap(); + + self.buffer.append(&mut buffer[0..tail_index].to_vec()); + self.decode_buffer(); + self.buffer.clear(); + + buffer = buffer[tail_index + 1..buffer.len()].to_vec(); + } + + self.buffer.append(&mut buffer); + } + + pub fn decode_buffer(&self) { + let is_app = self.buffer[0]; + let pid = self.buffer[1]; + + let _ = self.decoded_sender.send(BoardMessage { + pid, + is_app: is_app != 0, + payload: self.buffer.clone(), + }); + } +} diff --git a/tock-process-console/src/board/event.rs b/tock-process-console/src/board/event.rs new file mode 100644 index 0000000..b0e8194 --- /dev/null +++ b/tock-process-console/src/board/event.rs @@ -0,0 +1,17 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +#[derive(Debug, Clone)] +pub struct NewMessageEvent { + pub app: String, + pub pid: u8, + pub is_app: bool, + pub payload: Vec, +} + +#[derive(Debug, Clone)] +pub enum Event { + NewMessage(NewMessageEvent), + LostConnection(String), +} diff --git a/tock-process-console/src/board/mod.rs b/tock-process-console/src/board/mod.rs new file mode 100644 index 0000000..a8eb05a --- /dev/null +++ b/tock-process-console/src/board/mod.rs @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod connection; +pub mod decoder; +pub mod event; diff --git a/tock-process-console/src/lib.rs b/tock-process-console/src/lib.rs new file mode 100644 index 0000000..c9c4eaa --- /dev/null +++ b/tock-process-console/src/lib.rs @@ -0,0 +1,34 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod board; +mod state_store; +mod termination; +mod ui_management; + +use state_store::StateStore; +use termination::{create_terminator, Interrupted}; +use ui_management::UiManager; + +pub async fn run() -> anyhow::Result<()> { + let (terminator, mut interrupt_reader) = create_terminator(); + let (state_store, state_reader) = StateStore::new(); + let (ui_manager, action_reader) = UiManager::new(); + + tokio::try_join!( + state_store.main_loop(terminator, action_reader, interrupt_reader.resubscribe()), + ui_manager.main_loop(state_reader, interrupt_reader.resubscribe(),) + )?; + + if let Ok(reason) = interrupt_reader.recv().await { + match reason { + Interrupted::UserRequest => println!("Exited per user request"), + Interrupted::OsSignal => println!("Exited because of os signal"), + } + } else { + println!("Exited due to an unexpected error"); + } + + Ok(()) +} diff --git a/tock-process-console/src/state_store/action.rs b/tock-process-console/src/state_store/action.rs new file mode 100644 index 0000000..f69d1cc --- /dev/null +++ b/tock-process-console/src/state_store/action.rs @@ -0,0 +1,32 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use bytes::Bytes; + +#[derive(Debug, Clone)] +pub enum Action { + ConnectToBoard { + port: String, + }, + AddScreen { + screen_idx: usize, + }, + #[allow(dead_code)] + RemoveSreen { + screend_idx: usize, + }, + SelectApplication { + screen_idx: usize, + app_name: String, + }, + SendMessage { + content: Bytes, + }, + #[allow(dead_code)] + ResizeScreen { + rows: usize, + columns: usize, + }, + Exit, +} diff --git a/tock-process-console/src/state_store/mod.rs b/tock-process-console/src/state_store/mod.rs new file mode 100644 index 0000000..c38db45 --- /dev/null +++ b/tock-process-console/src/state_store/mod.rs @@ -0,0 +1,14 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod action; +pub use action::Action; + +mod state; +pub use state::AppData; +pub use state::BoardConnectionStatus; +pub use state::State; + +pub mod state_store_process; +pub use self::state_store_process::StateStore; diff --git a/tock-process-console/src/state_store/state.rs b/tock-process-console/src/state_store/state.rs new file mode 100644 index 0000000..36d12c2 --- /dev/null +++ b/tock-process-console/src/state_store/state.rs @@ -0,0 +1,173 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use circular_queue::CircularQueue; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; +use tui_term::vt100::Parser; + +use crate::board; + +/// AppData hold information about an application that is running on the board +/// and intercepted by the console application +#[derive(Debug, Clone)] +pub struct AppData { + // Name of the application running on the board + pub name: String, + // Process ID of the application running on the board + pub pid: u64, + pub is_app: bool, + // TODO: de vazut daca trebuie sa avem altceva in loc de string, sau daca nu avem de fapt nevoie sa fie o lista circulara + pub logs: CircularQueue>, + // Flag to indicate wheter there are new, unwritten logs for this app + pub has_new_logs: bool, +} +const MAX_LOGS_TO_STORE_PER_APP: usize = 1000; + +impl Default for AppData { + fn default() -> Self { + AppData { + name: String::from("kernel"), + pid: 0, + is_app: false, + logs: CircularQueue::with_capacity(MAX_LOGS_TO_STORE_PER_APP), + has_new_logs: false, + } + } +} + +impl AppData { + fn new(name: String, pid: u64, is_app: bool) -> Self { + AppData { + name, + pid, + is_app, + ..Default::default() + } + } +} + +#[derive(Debug, Clone)] +pub enum BoardConnectionStatus { + Uninitialized, + Connecting, + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + Connected { + port: String, + }, + Errored { + err: String, + }, +} + +/// State struct is holding the state of the entire application +#[derive(Clone)] +pub struct State { + pub board_connection_status: BoardConnectionStatus, + pub active_apps: Vec<(usize, Option)>, + // TODO: should be changed + pub apps_data_map: HashMap, + pub apps_parsers_map: HashMap>>, +} + +impl Default for State { + fn default() -> Self { + let mut default_apps: HashMap = HashMap::new(); + default_apps.insert("kernel".to_string(), AppData::default()); + + let mut default_parsers: HashMap>> = HashMap::new(); + default_parsers.insert( + "kernel".to_string(), + Arc::new(Mutex::new(Parser::new(40, 40, 100))), + ); + + State { + board_connection_status: BoardConnectionStatus::Uninitialized, + active_apps: Vec::new(), + apps_data_map: default_apps, + apps_parsers_map: default_parsers, + } + } +} + +impl State { + /// Handle events from the board + pub fn handle_board_event(&mut self, event: &board::event::Event) { + // TODO: should cover more types of events + match event { + board::event::Event::NewMessage(event) => { + if !self.apps_data_map.contains_key(&event.app) { + let new_app_data = + AppData::new(event.app.clone(), event.pid as u64, event.is_app); + self.apps_data_map.insert(event.app.clone(), new_app_data); + self.apps_parsers_map.insert( + event.app.clone(), + Arc::new(Mutex::new(Parser::new(4, 4, 100))), + ); + } + + let app_data = self.apps_data_map.get_mut(&event.app).unwrap(); + app_data.logs.push(event.payload.clone()); + + let app_parser = self.apps_parsers_map.get(&event.app).unwrap(); + app_parser.lock().unwrap().process(&event.payload); + + let mut is_in_focus = false; + for (_, active_app) in self.active_apps.clone() { + if let Some(active_app) = active_app { + if active_app.eq(&event.app) { + is_in_focus = true; + } + } + } + + if !is_in_focus { + app_data.has_new_logs = true; + } + } + board::event::Event::LostConnection(_err) => { + todo!() + } + } + } + + /// Marks the beggining of the connection process + pub fn mark_connection_request_start(&mut self) { + self.board_connection_status = BoardConnectionStatus::Connecting; + } + + /// Processes the result of the connection request and updates the state accordingly + pub fn process_connection_request_result(&mut self, result: anyhow::Result) { + self.board_connection_status = match result { + Ok(port) => BoardConnectionStatus::Connected { port: port.clone() }, + Err(error) => BoardConnectionStatus::Errored { + err: error.to_string(), + }, + } + } + + /// Is setting the currently active application that is visible in the UI + /// In case of failure, the function will return + pub fn try_set_active_room(&mut self, screen_idx: usize, app: &str) -> Option<&AppData> { + let app_data = self.apps_data_map.get_mut(app)?; + app_data.has_new_logs = false; + + if let Some(index) = self + .active_apps + .iter() + .enumerate() + .find(|(_index, (screen_index, _active_app))| screen_index == &screen_idx) + .map(|(index, _)| index) + { + self.active_apps[index].1 = Some(String::from(app)); + } else { + self.active_apps.push((screen_idx, Some(String::from(app)))); + } + + Some(app_data) + } +} diff --git a/tock-process-console/src/state_store/state_store_process.rs b/tock-process-console/src/state_store/state_store_process.rs new file mode 100644 index 0000000..1108733 --- /dev/null +++ b/tock-process-console/src/state_store/state_store_process.rs @@ -0,0 +1,132 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use super::{Action, State}; +use crate::{ + board::{connection::ConnectionHandler, event::Event}, + termination::{Interrupted, Terminator}, +}; +use bytes::Bytes; +use tokio::sync::{ + broadcast, + mpsc::{self, UnboundedReceiver, UnboundedSender}, +}; + +type ConnectionHandle = (UnboundedReceiver, UnboundedSender); + +pub struct StateStore { + state_sender: UnboundedSender, +} + +impl StateStore { + pub fn new() -> (Self, UnboundedReceiver) { + let (state_sender, state_receiver) = mpsc::unbounded_channel::(); + + (StateStore { state_sender }, state_receiver) + } + + // Will only return in case of interruption + pub async fn main_loop( + self, + mut terminator: Terminator, + mut action_receiver: UnboundedReceiver, + mut intrerrupt_receiver: broadcast::Receiver, + ) -> anyhow::Result { + let mut connection_handle: Option = None; + let mut state = State::default(); + + self.state_sender.send(state.clone())?; + + let result: Interrupted = loop { + // Check if we have a board connection + if let Some((ref mut event_receiver, ref command_writer)) = connection_handle { + // Treat after connection related actions + tokio::select! { + maybe_event = event_receiver.recv() => { + match maybe_event { + Some(event) => { + state.handle_board_event(&event); + }, + None => { + connection_handle = Option::None; + state = State::default(); + } + } + }, + Some(action) = action_receiver.recv() => match action { + Action::SendMessage { content } => { + if !state.active_apps.is_empty() { + command_writer.send( + content + ).expect("Expected command reader to be open."); + } + }, + Action::AddScreen { screen_idx } => { + state.active_apps.push((screen_idx, None)) + } + Action::SelectApplication { screen_idx, app_name } => { + state.try_set_active_room(screen_idx, app_name.as_str()); + }, + Action::Exit => { + let _ = terminator.terminate(Interrupted::UserRequest); + + break Interrupted::UserRequest; + } + _ => {} + } + } + } else { + // We are not connected to the board + // Treat before connection related actions + tokio::select! { + Some(action) = action_receiver.recv() => { + + match action { + // We received an action to connect to the board at a given port address + Action::ConnectToBoard { port } => { + state.mark_connection_request_start(); + + // Emit event to re-render any part depending on the connection status + self.state_sender.send(state.clone())?; + + match connect_to_board(&port).await { + Ok(connection_result) => { + connection_handle = Some(connection_result); + state.process_connection_request_result(Ok(port)); + }, + Err(err) => { + state.process_connection_request_result(Err(err)); + } + } + }, + Action::Exit => { + let _ = terminator.terminate(Interrupted::UserRequest); + + break Interrupted::UserRequest; + } + _ => {}, + } + }, + // Catch and handle interrupt signal to gracefully shutdown + Ok(interrupted) = intrerrupt_receiver.recv() => { + break interrupted; + } + } + } + + // Just modified the state object as per action received + // so we send the new state to the UI + self.state_sender.send(state.clone())?; + }; + + Ok(result) + } +} + +async fn connect_to_board(tty: &str) -> anyhow::Result { + match ConnectionHandler::connection_init(tty).await { + Ok((event_reader, command_writer)) => Ok((event_reader, command_writer)), + Err(err) => Err(err), + } +} diff --git a/tock-process-console/src/termination.rs b/tock-process-console/src/termination.rs new file mode 100644 index 0000000..4fba27f --- /dev/null +++ b/tock-process-console/src/termination.rs @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use tokio::sync::broadcast; + +#[derive(Debug, Clone)] +pub enum Interrupted { + OsSignal, + UserRequest, +} + +#[derive(Debug, Clone)] +pub struct Terminator { + interrupt_sender: broadcast::Sender, +} + +impl Terminator { + pub fn new(interrupt_sender: broadcast::Sender) -> Self { + Self { interrupt_sender } + } + + pub fn terminate(&mut self, interrupted: Interrupted) -> anyhow::Result<()> { + self.interrupt_sender.send(interrupted)?; + + Ok(()) + } +} + +#[cfg(unix)] +async fn terminate_by_unix_signal(mut terminator: Terminator) { + use tokio::signal::unix::signal; + + let mut interrupt_signal = signal(tokio::signal::unix::SignalKind::interrupt()) + .expect("failed to create interrupt signal stream"); + + interrupt_signal.recv().await; + + terminator + .terminate(Interrupted::OsSignal) + .expect("failed to send interrupt signal"); +} + +pub fn create_terminator() -> (Terminator, broadcast::Receiver) { + let (tx, rx) = broadcast::channel(1); + let terminator = Terminator::new(tx); + + #[cfg(unix)] + tokio::spawn(terminate_by_unix_signal(terminator.clone())); + + (terminator, rx) +} diff --git a/tock-process-console/src/ui_management/components/component.rs b/tock-process-console/src/ui_management/components/component.rs new file mode 100644 index 0000000..7308cfa --- /dev/null +++ b/tock-process-console/src/ui_management/components/component.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use crate::state_store::{Action, State}; +use crossterm::event::{KeyEvent, MouseEvent}; +use ratatui::Frame; +use tokio::sync::mpsc::UnboundedSender; + +pub trait Component { + fn new( + state: &State, + screen_idx: Option, + action_sender: UnboundedSender, + ) -> Self + where + Self: Sized; + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized; + + fn handle_key_event(&mut self, key: KeyEvent); + + fn handle_mouse_event(&mut self, event: MouseEvent); +} + +pub trait ComponentRender { + fn render(&mut self, frame: &mut Frame, properties: Properties); +} diff --git a/tock-process-console/src/ui_management/components/mod.rs b/tock-process-console/src/ui_management/components/mod.rs new file mode 100644 index 0000000..91bc5c7 --- /dev/null +++ b/tock-process-console/src/ui_management/components/mod.rs @@ -0,0 +1,6 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod component; +pub use component::{Component, ComponentRender}; diff --git a/tock-process-console/src/ui_management/mod.rs b/tock-process-console/src/ui_management/mod.rs new file mode 100644 index 0000000..abb5d12 --- /dev/null +++ b/tock-process-console/src/ui_management/mod.rs @@ -0,0 +1,9 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod components; +mod pages; +mod ui_manager; + +pub use ui_manager::UiManager; diff --git a/tock-process-console/src/ui_management/pages/mod.rs b/tock-process-console/src/ui_management/pages/mod.rs new file mode 100644 index 0000000..0a62272 --- /dev/null +++ b/tock-process-console/src/ui_management/pages/mod.rs @@ -0,0 +1,85 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. +use self::{setup_page::SetupPage, terminal_page::main_page::MainPage}; +use super::components::{Component, ComponentRender}; +use crate::state_store::{Action, BoardConnectionStatus, State}; +use tokio::sync::mpsc::UnboundedSender; + +mod setup_page; +mod terminal_page; + +enum ActivePage { + SetupPage, + MainPage, +} + +impl From<&State> for ActivePage { + fn from(state: &State) -> Self { + match state.board_connection_status { + BoardConnectionStatus::Connected { .. } => ActivePage::MainPage, + _ => ActivePage::SetupPage, + } + } +} + +pub struct AppRouter { + active_page: ActivePage, + setup_page: SetupPage, + main_page: MainPage, +} + +impl AppRouter { + fn get_active_page_component_mut(&mut self) -> &mut dyn Component { + match self.active_page { + ActivePage::SetupPage => &mut self.setup_page, + ActivePage::MainPage => &mut self.main_page, + } + } +} + +impl Component for AppRouter { + fn new( + state: &State, + _screen_idx: Option, + action_sender: UnboundedSender, + ) -> Self + where + Self: Sized, + { + AppRouter { + active_page: ActivePage::from(state), + setup_page: SetupPage::new(state, None, action_sender.clone()), + main_page: MainPage::new(state, None, action_sender.clone()), + } + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + self.get_active_page_component_mut().handle_key_event(key) + } + + fn handle_mouse_event(&mut self, event: crossterm::event::MouseEvent) { + self.get_active_page_component_mut() + .handle_mouse_event(event) + } + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized, + { + Self { + active_page: ActivePage::from(state), + setup_page: self.setup_page.update_with_state(state), + main_page: self.main_page.update_with_state(state), + } + } +} + +impl ComponentRender<()> for AppRouter { + fn render(&mut self, frame: &mut ratatui::prelude::Frame, properties: ()) { + match self.active_page { + ActivePage::SetupPage => self.setup_page.render(frame, properties), + ActivePage::MainPage => self.main_page.render(frame, properties), + } + } +} diff --git a/tock-process-console/src/ui_management/pages/setup_page/mod.rs b/tock-process-console/src/ui_management/pages/setup_page/mod.rs new file mode 100644 index 0000000..53dfcef --- /dev/null +++ b/tock-process-console/src/ui_management/pages/setup_page/mod.rs @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod setup_page_structure; + +pub use setup_page_structure::SetupPage; diff --git a/tock-process-console/src/ui_management/pages/setup_page/setup_page_structure.rs b/tock-process-console/src/ui_management/pages/setup_page/setup_page_structure.rs new file mode 100644 index 0000000..0275a97 --- /dev/null +++ b/tock-process-console/src/ui_management/pages/setup_page/setup_page_structure.rs @@ -0,0 +1,391 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use std::vec; + +use crate::{ + state_store::{Action, BoardConnectionStatus, State}, + ui_management::components::{Component, ComponentRender}, +}; +use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; +use ratatui::{ + layout::{Constraint, Layout}, + prelude::Direction, + style::{Color, Modifier, Style, Stylize}, + text::Text, + widgets::{Block, BorderType, Borders, List, ListDirection, ListState, Paragraph, Wrap}, +}; + +use tokio::sync::{mpsc::UnboundedSender, watch}; +use tokio_serial::SerialPortType; + +struct Properties { + error_message: Option, +} + +#[derive(PartialEq)] +pub enum ShowState { + ShowBoardsOnly, + ShowAllSerialPorts, +} + +impl From<&State> for Properties { + fn from(state: &State) -> Self { + let error_message = + if let BoardConnectionStatus::Errored { err } = &state.board_connection_status { + Some(err.clone()) + } else { + None + }; + + Properties { error_message } + } +} + +/// Struct that handles setup of the console application +pub struct SetupPage { + action_sender: UnboundedSender, + properties: Properties, + scrollbar_state_serial: ListState, + scrollbar_state_boards: ListState, + probeinfo_sender: watch::Sender>, + probeinfo_receiver: watch::Receiver>, + show_state: ShowState, +} + +impl SetupPage { + fn set_port(&mut self) { + let probeinfo = self.probeinfo_receiver.borrow_and_update(); + + let port_number = match self.show_state { + ShowState::ShowBoardsOnly => { + match self.scrollbar_state_boards.selected() { + Some(port) => port, + None => return, // Do nothing when there are no ports selected + } + } + ShowState::ShowAllSerialPorts => { + match self.scrollbar_state_serial.selected() { + Some(port) => port, + None => return, // Do nothing when there are no ports selected + } + } + }; + + let port = probeinfo[port_number].clone(); + self.action_sender + .send(Action::ConnectToBoard { port }) + .expect("Expected action receiver to be open."); + } +} + +impl Component for SetupPage { + fn new( + state: &State, + _screen_idx: Option, + action_sender: UnboundedSender, + ) -> Self + where + Self: Sized, + { + let mut scrollbar_state_serial = ListState::default(); + scrollbar_state_serial.select_first(); + + let mut scrollbar_state_boards = ListState::default(); + scrollbar_state_boards.select_first(); + + let collector: Vec = vec![]; + let (probeinfo_sender, probeinfo_receiver) = watch::channel(collector); + + let show_state = ShowState::ShowBoardsOnly; + + SetupPage { + action_sender: action_sender.clone(), + properties: Properties::from(state), + scrollbar_state_serial, + scrollbar_state_boards, + probeinfo_sender, + probeinfo_receiver, + show_state, + } + .update_with_state(state) + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + if key.kind != KeyEventKind::Press { + return; + } + + match key.code { + KeyCode::Enter => { + self.set_port(); + } + KeyCode::Char('c') => { + if key.modifiers == KeyModifiers::CONTROL { + let _ = self.action_sender.send(Action::Exit); + } + } + KeyCode::Char('a') => { + if self.show_state == ShowState::ShowBoardsOnly { + self.show_state = ShowState::ShowAllSerialPorts; + self.scrollbar_state_serial.select_first(); + } else if self.show_state == ShowState::ShowAllSerialPorts { + self.show_state = ShowState::ShowBoardsOnly; + self.scrollbar_state_boards.select_first(); + } + } + KeyCode::Up => { + if self.show_state == ShowState::ShowAllSerialPorts { + self.scrollbar_state_serial.select_previous() + } else if self.show_state == ShowState::ShowBoardsOnly { + self.scrollbar_state_boards.select_previous() + } + } + KeyCode::Down => { + if self.show_state == ShowState::ShowAllSerialPorts { + self.scrollbar_state_serial.select_next() + } else if self.show_state == ShowState::ShowBoardsOnly { + self.scrollbar_state_boards.select_next() + } + } + KeyCode::PageUp => { + if self.show_state == ShowState::ShowAllSerialPorts { + self.scrollbar_state_serial.select_previous() + } else if self.show_state == ShowState::ShowBoardsOnly { + self.scrollbar_state_boards.select_previous() + } + } + KeyCode::PageDown => { + if self.show_state == ShowState::ShowAllSerialPorts { + self.scrollbar_state_serial.select_next() + } else if self.show_state == ShowState::ShowBoardsOnly { + self.scrollbar_state_boards.select_next() + } + } + _ => {} + } + } + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized, + { + Self { + properties: Properties::from(state), + action_sender: self.action_sender, + scrollbar_state_serial: self.scrollbar_state_serial, + scrollbar_state_boards: self.scrollbar_state_boards, + probeinfo_sender: self.probeinfo_sender, + probeinfo_receiver: self.probeinfo_receiver, + show_state: self.show_state, + } + } + + fn handle_mouse_event(&mut self, _event: crossterm::event::MouseEvent) {} +} + +impl ComponentRender<()> for SetupPage { + fn render(&mut self, frame: &mut ratatui::prelude::Frame, _properties: ()) { + let temp_serial_position_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(35), + Constraint::Min(2), + Constraint::Percentage(35), + ]); + + let [_, serial_position_v, _] = temp_serial_position_v.areas(frame.area()); + + let temp_serial_position_h = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(18), + Constraint::Min(2), + Constraint::Percentage(18), + ]); + let [_, serial_position_h, _] = temp_serial_position_h.areas(serial_position_v); + + let available_ports = match tokio_serial::available_ports() { + Ok(ports) => ports, + Err(error) => panic!("Error while searching for ports: {}", error), + }; + + let mut vec_serial: Vec = vec![]; + let mut vec_boards: Vec = vec![]; + let mut board_ports: Vec = vec![]; + let mut serial_ports: Vec = vec![]; + for (port_index, port) in available_ports.iter().enumerate() { + let product = match &port.port_type { + SerialPortType::UsbPort(usb) => usb.product.clone(), + SerialPortType::PciPort => Some("PciPort".to_string()), + SerialPortType::BluetoothPort => Some("BluetoothPort".to_string()), + SerialPortType::Unknown => Some("Unknown".to_string()), + }; + let temp_serial = format! {"Port[{port_index}](Name:{:#?}, Type:{}), \n", port.port_name, product.unwrap_or("Unknown".to_string())}; + if let SerialPortType::UsbPort(_) = port.port_type { + // Add to boards only if its a USB type. + vec_boards.push(temp_serial.clone().into()); + board_ports.push(port.port_name.clone()); + } + // Fall-through, add to serial regardless of type. + vec_serial.push(temp_serial.into()); + serial_ports.push(port.port_name.clone()); + } + + if self.show_state == ShowState::ShowAllSerialPorts { + let list = List::new(vec_serial) + .style(Style::default().fg(Color::Cyan)) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .fg(Color::Yellow) + .title(format!(" Serial ports - {} ", available_ports.len())) + .title_style(Style::default().fg(Color::Blue)), + ) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(" > ") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, serial_position_h, &mut self.scrollbar_state_serial); + } + + if self.show_state == ShowState::ShowBoardsOnly { + let boards_found = vec_boards.len(); + + let list = List::new(vec_boards) + .style(Style::default().fg(Color::Cyan)) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .fg(Color::Yellow) + .title(format!(" Number of boards found: {} ", boards_found)) + .title_style(Style::default().fg(Color::Blue)), + ) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(" > ") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, serial_position_h, &mut self.scrollbar_state_boards); + } + + if self.show_state == ShowState::ShowBoardsOnly { + match self.probeinfo_sender.send(board_ports) { + Ok(data) => data, + Err(error) => println!("{}", error), + }; + } else if self.show_state == ShowState::ShowAllSerialPorts { + match self.probeinfo_sender.send(serial_ports) { + Ok(data) => data, + Err(error) => println!("{}", error), + }; + } + + let temp_help_text_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(65), + Constraint::Min(2), + Constraint::Percentage(10), + ]); + + let [_, help_text_v, _] = temp_help_text_v.areas(frame.area()); + + let temp_help_text_h = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(18), + Constraint::Min(2), + Constraint::Percentage(35), + ]); + + let [_, help_text_h, _] = temp_help_text_h.areas(help_text_v); + + let temp_panic_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(75), + Constraint::Min(2), + Constraint::Percentage(10), + ]); + + let [_, panic_v, _] = temp_panic_v.areas(frame.area()); + + let temp_panic_h = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(18), + Constraint::Min(2), + Constraint::Percentage(35), + ]); + + let [_, panic_h, _] = temp_panic_h.areas(panic_v); + + let temp_show_text_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(69), + Constraint::Min(2), + Constraint::Percentage(10), + ]); + + let [_, show_text_v, _] = temp_show_text_v.areas(frame.area()); + + let temp_show_text_h = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(18), + Constraint::Min(2), + Constraint::Percentage(35), + ]); + + let [_, show_text_h, _] = temp_show_text_h.areas(show_text_v); + + let show_text = Paragraph::new(Text::from("Press A to switch display mode.")); + frame.render_widget(show_text, show_text_h); + + let temp_enter_text_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(72), + Constraint::Min(2), + Constraint::Percentage(10), + ]); + + let [_, enter_text_v, _] = temp_enter_text_v.areas(frame.area()); + + let temp_enter_text_h = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(18), + Constraint::Min(2), + Constraint::Percentage(35), + ]); + + let [_, enter_text_h, _] = temp_enter_text_h.areas(enter_text_v); + + let help_text = Paragraph::new(Text::from("Press Enter to select highlighted port.")); + frame.render_widget(help_text, enter_text_h); + + let help_text = Paragraph::new(Text::from("Use ▲ ▼ PageUp PageDown to scroll. ")); + frame.render_widget(help_text, help_text_h); + + let error = if let Some(error) = &self.properties.error_message { + Text::from(format!("Error: {}", error)) + } else { + Text::from("") + }; + + let error_message = Paragraph::new(error).wrap(Wrap { trim: true }).style( + Style::default() + .fg(Color::Red) + .add_modifier(Modifier::SLOW_BLINK | Modifier::ITALIC), + ); + + frame.render_widget(error_message, panic_h); + } +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/applications_page.rs b/tock-process-console/src/ui_management/pages/terminal_page/applications_page.rs new file mode 100644 index 0000000..364eb46 --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/applications_page.rs @@ -0,0 +1,328 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use super::{ + components::{ + apps_list::{self, AppsList}, + terminal_box::{RenderProps, TerminalBox}, + }, + section::SectionActivation, +}; + +use crate::{ + state_store::{Action, AppData, State}, + ui_management::components::{Component, ComponentRender}, +}; +use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; +use ratatui::{ + layout::{Constraint, Direction, Layout}, + style::Color, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::collections::HashMap; +use tokio::sync::mpsc::UnboundedSender; + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum Section { + #[default] + AppsList, + Terminal, +} + +impl Section { + pub const COUNT: usize = 2; + + fn to_usize(&self) -> usize { + match self { + Section::AppsList => 0, + Section::Terminal => 1, + } + } +} + +impl TryFrom for Section { + type Error = (); + + fn try_from(value: usize) -> Result { + match value { + 0 => Ok(Section::AppsList), + 1 => Ok(Section::Terminal), + _ => Err(()), + } + } +} + +#[derive(Clone, Debug)] +struct Properties { + active_apps: Vec<(usize, Option)>, + app_data_map: HashMap, +} + +impl From<&State> for Properties { + fn from(state: &State) -> Self { + Self { + // TODO: the active_app should be different for each instnace of + // ApplicationsPage, but i need to know the index of the app in + // order to know which is the active_app for this screen + // active_app: state.active_apps.clone(), + active_apps: state.active_apps.clone(), + app_data_map: state.apps_data_map.clone(), + } + } +} + +#[derive(Clone)] +pub struct ApplicationsPage { + /// Action sender channel + pub action_sender: UnboundedSender, + /// The identificatior of the screen in focus + pub screen_idx: usize, + /// State of the terminal page + properties: Properties, + pub active_section: Option
, + /// Currently hovered section in the page + pub currently_hovered_section: Section, + /// Separate widget component that handles the listing of the applications on the board + pub app_list: AppsList, + /// Separate widget component that handles incoming messages from applications and user input + pub terminal: TerminalBox, +} + +impl ApplicationsPage { + fn get_app_data(&self, name: &str) -> Option<&AppData> { + self.properties.app_data_map.get(name) + } + + fn get_component_for_section_mut<'a>(&'a mut self, section: &Section) -> &'a mut dyn Component { + match section { + Section::Terminal => &mut self.terminal, + Section::AppsList => &mut self.app_list, + } + } + + fn get_section_activation_for_section<'a>( + &'a mut self, + section: &Section, + ) -> &'a mut dyn SectionActivation { + match section { + Section::Terminal => &mut self.terminal, + Section::AppsList => &mut self.app_list, + } + } + + fn hover_next(&mut self) { + let idx: usize = self.currently_hovered_section.to_usize(); + let next_idx = (idx + 1) % Section::COUNT; + self.currently_hovered_section = Section::try_from(next_idx).unwrap(); + } + + fn hover_previous(&mut self) { + let idx: usize = self.currently_hovered_section.to_usize(); + let previous_idx = if idx == 0 { + Section::COUNT - 1 + } else { + idx - 1 + }; + self.currently_hovered_section = Section::try_from(previous_idx).unwrap(); + } + + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + fn calculate_border_color(&self, section: Section) -> Color { + match ( + self.active_section.as_ref(), + &self.currently_hovered_section, + ) { + (Some(active_section), _) if active_section.eq(§ion) => Color::Yellow, + (_, hovered_section) if hovered_section.eq(§ion) => Color::Blue, + _ => Color::Reset, + } + } + + fn disable_section(&mut self, section: &Section) { + self.get_section_activation_for_section(section) + .deactivate(); + + self.active_section = None; + } + + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + pub fn set_screen_idx(&mut self, index: usize) { + self.screen_idx = index; + + self.terminal.set_screen_idx(index); + self.app_list.set_screen_idx(index); + } +} + +const DEFAULT_HOVERED_SECTION: Section = Section::AppsList; + +impl Component for ApplicationsPage { + fn new(state: &State, screen_idx: Option, action_sender: UnboundedSender) -> Self + where + Self: Sized, + { + ApplicationsPage { + action_sender: action_sender.clone(), + screen_idx: screen_idx.unwrap_or_default(), + properties: Properties::from(state), + active_section: Option::None, + currently_hovered_section: DEFAULT_HOVERED_SECTION, + app_list: AppsList::new(state, screen_idx, action_sender.clone()), + terminal: TerminalBox::new(state, screen_idx, action_sender.clone()), + } + .update_with_state(state) + } + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized, + { + ApplicationsPage { + properties: Properties::from(state), + app_list: self.app_list.update_with_state(state), + terminal: self.terminal.update_with_state(state), + ..self + } + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + if key.kind != KeyEventKind::Press { + return; + } + let active_section = self.active_section.clone(); + + match active_section { + // The action is destined for the ApplicationsPage overall as there is no component selected yet + None => match key.code { + KeyCode::Enter => { + let last_hovered_section = self.currently_hovered_section.clone(); + + self.active_section = Some(last_hovered_section.clone()); + self.get_section_activation_for_section(&last_hovered_section) + .activate(); + } + KeyCode::Left => self.hover_previous(), + KeyCode::Right => self.hover_next(), + KeyCode::Char('q') => { + let _ = self.action_sender.send(Action::Exit); + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + let _ = self.action_sender.send(Action::Exit); + } + _ => {} + }, + Some(section) => { + self.get_component_for_section_mut(§ion) + .handle_key_event(key); + + match section { + Section::AppsList if key.code == KeyCode::Enter => { + self.disable_section(§ion); + + self.active_section = Some(Section::Terminal); + self.get_section_activation_for_section(&Section::Terminal) + .activate(); + } + _ if key.code == KeyCode::BackTab => self.disable_section(§ion), + _ => (), + } + } + } + } + + fn handle_mouse_event(&mut self, event: crossterm::event::MouseEvent) { + let active_section = self.active_section.clone(); + + if let Some(active_section) = active_section { + self.get_component_for_section_mut(&active_section) + .handle_mouse_event(event); + } + } +} + +const NO_APP_SELECTED_MESSAE: &str = "Select an process to see its logs!"; + +impl ComponentRender for ApplicationsPage { + fn render( + &mut self, + frame: &mut ratatui::prelude::Frame, + properties: apps_list::RenderProperties, + ) { + let [container_active_app_header, container_content] = *Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(4), Constraint::Min(1)].as_ref()) + .split(properties.area) + else { + panic!("The left layout should have 2 chunks") + }; + + let top_line = if let Some(app_data) = self + .properties + .active_apps + .iter() + .find(|(screen_idx, _)| screen_idx == &self.screen_idx) + .map(|(_, active_app)| active_app) + .and_then(|active_app| active_app.clone()) + .and_then(|active_app| self.get_app_data(active_app.as_ref())) + { + let mut app_info = vec![Span::from(app_data.name.to_string())]; + + if app_data.is_app { + app_info.append(&mut vec![ + " --- PID ".into(), + Span::from(format!("{}", app_data.pid)), + ]) + } + + Line::from(app_info) + } else { + Line::from(NO_APP_SELECTED_MESSAE) + }; + let text = Text::from(top_line); + + let help_message = Paragraph::new(text).block( + Block::default() + .borders(Borders::ALL) + .title("Active Process Information"), + ); + frame.render_widget(help_message, container_active_app_header); + + if self.active_section.clone().unwrap_or_default() == Section::AppsList { + self.app_list.render( + frame, + apps_list::RenderProperties { + // border_color: self.calculate_border_color(Section::AppsList), + border_color: properties.border_color, + area: container_content, + }, + ); + } else { + self.terminal.render( + frame, + RenderProps { + area: container_content, + // border_color: self.calculate_border_color(Section::Terminal), + border_color: properties.border_color, + show_cursor: self + .active_section + .as_ref() + .map(|active_section| { + (active_section.to_usize()).eq(&Section::Terminal.to_usize()) + }) + .unwrap_or(false), + }, + ); + } + + // let usage_text: Text = widget_usage_to_text(self.usage_info()); + // let usage_text = usage_text.patch_style(Style::default()); + // let usage = Paragraph::new(usage_text) + // .wrap(Wrap { trim: true }) + // .block(Block::default().borders(Borders::ALL).title("Usage")); + // frame.render_widget(usage, right); + } +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/components/apps_list.rs b/tock-process-console/src/ui_management/pages/terminal_page/components/apps_list.rs new file mode 100644 index 0000000..ebb0453 --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/components/apps_list.rs @@ -0,0 +1,246 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use crate::{ + state_store::{Action, State}, + ui_management::{ + components::{Component, ComponentRender}, + pages::terminal_page::section::SectionActivation, + }, +}; +use crossterm::event::{KeyCode, KeyEventKind}; +use ratatui::{ + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState}, +}; +use tokio::sync::mpsc::UnboundedSender; + +#[derive(Clone)] +struct AppState { + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + pub pid: String, + pub name: String, + pub has_new_input: bool, +} + +#[derive(Clone)] +struct Properties { + apps: Vec, +} + +impl From<&State> for Properties { + fn from(state: &State) -> Self { + let mut apps = state + .apps_data_map + .iter() + .map(|(name, app_data)| AppState { + pid: app_data.pid.to_string(), + name: name.clone(), + has_new_input: app_data.has_new_logs, + }) + .collect::>(); + + apps.sort_by(|a, b| a.name.cmp(&b.name)); + + Self { apps } + } +} + +#[derive(Clone)] +pub struct AppsList { + action_sender: UnboundedSender, + properties: Properties, + pub list_state: ListState, + pub active_app: Option, + pub screen_idx: usize, +} + +impl AppsList { + fn next(&mut self) { + // Compute the value of the next index based on the currently selected one + let index = match self.list_state.selected() { + Some(idx) => { + if idx >= self.properties.apps.len() - 1 { + 0 + } else { + idx + 1 + } + } + None => 0, + }; + + self.list_state.select(Some(index)); + } + + fn previous(&mut self) { + // Compute the value of the previous index based on the currently selected one + let index = match self.list_state.selected() { + Some(idx) => { + if idx == 0 { + self.properties.apps.len() - 1 + } else { + idx - 1 + } + } + None => 0, + }; + + self.list_state.select(Some(index)); + } + + fn apps(&self) -> &Vec { + &self.properties.apps + } + + fn get_room_index(&self, name: &str) -> Option { + self.properties + .apps + .iter() + .enumerate() + .find_map(|(index, app_state)| { + if app_state.name == name { + Some(index) + } else { + None + } + }) + } + + pub fn set_screen_idx(&mut self, index: usize) { + self.screen_idx = index; + } +} + +impl Component for AppsList { + fn new(state: &State, screen_idx: Option, action_sender: UnboundedSender) -> Self + where + Self: Sized, + { + Self { + action_sender, + properties: Properties::from(state), + list_state: ListState::default(), + active_app: None, + screen_idx: screen_idx.unwrap_or_default(), + } + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + if key.kind != KeyEventKind::Press { + return; + } + + match key.code { + KeyCode::Up => { + self.previous(); + } + KeyCode::Down => { + self.next(); + } + KeyCode::Enter => { + if let Some(selected_app_index) = self.list_state.selected() { + let applications = self.apps(); + + if let Some(app_state) = applications.get(selected_app_index) { + let _ = self.action_sender.send(Action::SelectApplication { + screen_idx: self.screen_idx, + app_name: app_state.name.clone(), + }); + } else { + // TODO: should handle the error + } + } + } + _ => {} + } + } + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized, + { + Self { + properties: Properties::from(state), + // action_sender: self.action_sender, + // list_state: self.list_state, + ..self + } + } + + fn handle_mouse_event(&mut self, _event: crossterm::event::MouseEvent) {} +} + +pub struct RenderProperties { + pub border_color: Color, + pub area: Rect, +} + +impl ComponentRender for AppsList { + fn render(&mut self, frame: &mut ratatui::prelude::Frame, properties: RenderProperties) { + let active_application = self.active_app.clone(); + + let apps_list: Vec = self + .apps() + .iter() + .map(|app_state| { + let content = Line::from(Span::raw(app_state.name.clone())); + + let style = if self.list_state.selected().is_none() + && active_application.is_some() + && active_application.as_ref().unwrap().eq(&app_state.name) + { + Style::default().add_modifier(Modifier::BOLD) + } else if app_state.has_new_input { + Style::default().add_modifier(Modifier::SLOW_BLINK | Modifier::ITALIC) + } else { + Style::default() + }; + + ListItem::new(content).style(style.bg(Color::Reset)) + }) + .collect(); + + let apps_list_component = List::new(apps_list) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::new().fg(properties.border_color)) + .title("Running applications"), + ) + .highlight_style( + Style::default() + .bg(Color::Green) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">"); + + let mut applications_list_state = self.list_state.clone(); + frame.render_stateful_widget( + apps_list_component, + properties.area, + &mut applications_list_state, + ); + } +} + +impl SectionActivation for AppsList { + fn activate(&mut self) { + let idx = self + .active_app + .as_ref() + .and_then(|app_name| self.get_room_index(app_name)) + .unwrap_or(0); + + *self.list_state.offset_mut() = 0; + self.list_state.select(Some(idx)); + } + + fn deactivate(&mut self) { + *self.list_state.offset_mut() = 0; + self.list_state.select(None); + } +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/components/mod.rs b/tock-process-console/src/ui_management/pages/terminal_page/components/mod.rs new file mode 100644 index 0000000..651e1ee --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/components/mod.rs @@ -0,0 +1,6 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod apps_list; +pub mod terminal_box; diff --git a/tock-process-console/src/ui_management/pages/terminal_page/components/terminal_box.rs b/tock-process-console/src/ui_management/pages/terminal_page/components/terminal_box.rs new file mode 100644 index 0000000..3dd7f2a --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/components/terminal_box.rs @@ -0,0 +1,268 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use crate::ui_management::pages::Component; +use bytes::Bytes; +use crossterm::event::{KeyCode, KeyEventKind, MouseEventKind}; +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + widgets::block::Block, + widgets::{Borders, Paragraph}, +}; +use tokio::sync::mpsc::UnboundedSender; +use tui_term::{vt100::Parser, widget::PseudoTerminal}; + +use crate::{ + state_store::{Action, AppData, State}, + ui_management::{ + components::ComponentRender, pages::terminal_page::section::SectionActivation, + }, +}; + +#[derive(Clone)] +struct Properties { + /// Active application that is displayed to the user + active_apps: Vec<(usize, Option)>, + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + app_data_map: HashMap, + apps_parsers_map: HashMap>>, +} + +impl From<&State> for Properties { + fn from(state: &State) -> Self { + Self { + active_apps: state.active_apps.clone(), + app_data_map: state.apps_data_map.clone(), + // apps_parsers_map: state.apps_parsers_map.clone() + apps_parsers_map: state.apps_parsers_map.clone(), + } + } +} + +#[derive(Clone)] +pub struct TerminalBox { + action_sender: UnboundedSender, + screen_idx: usize, + properties: Properties, + scrollback_lines: usize, + // input: String, +} + +impl TerminalBox { + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + fn get_app_data(&self, name: &str) -> Option<&AppData> { + self.properties.app_data_map.get(name) + } + + pub fn set_screen_idx(&mut self, index: usize) { + self.screen_idx = index; + } +} + +impl Component for TerminalBox { + fn new( + state: &crate::state_store::State, + screen_idx: Option, + action_sender: UnboundedSender, + ) -> Self + where + Self: Sized, + { + Self { + action_sender: action_sender.clone(), + screen_idx: screen_idx.unwrap_or_default(), + properties: Properties::from(state), + scrollback_lines: 0, + } + } + + fn update_with_state(self, state: &crate::state_store::State) -> Self + where + Self: Sized, + { + Self { + properties: Properties::from(state), + ..self + } + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + if key.kind != KeyEventKind::Press { + return; + } + + match key.code { + KeyCode::Char(to_insert) => { + let _ = self.action_sender.send(Action::SendMessage { + content: Bytes::from(to_insert.to_string().into_bytes()), + }); + } + KeyCode::Enter => { + #[cfg(unix)] + let _ = self.action_sender.send(Action::SendMessage { + content: Bytes::from(vec![b'\n']), + }); + #[cfg(windows)] + let _ = self.action_sender.send(Action::SendMessage { + content: Bytes::from(vec![b'\r', b'\n']), + }); + } + KeyCode::Backspace => { + let _ = self.action_sender.send(Action::SendMessage { + content: Bytes::from(vec![8]), + }); + } + KeyCode::Left => self + .action_sender + .send(Action::SendMessage { + content: Bytes::from(vec![27, 91, 68]), + }) + .unwrap(), + KeyCode::Right => self + .action_sender + .send(Action::SendMessage { + content: Bytes::from(vec![27, 91, 67]), + }) + .unwrap(), + KeyCode::Up => self + .action_sender + .send(Action::SendMessage { + content: Bytes::from(vec![27, 91, 65]), + }) + .unwrap(), + KeyCode::Down => self + .action_sender + .send(Action::SendMessage { + content: Bytes::from(vec![27, 91, 66]), + }) + .unwrap(), + _ => {} + } + } + + fn handle_mouse_event(&mut self, event: crossterm::event::MouseEvent) { + match event.kind { + MouseEventKind::ScrollDown => { + let mut active_parser = self + .properties + .apps_parsers_map + .get( + self.properties + .active_apps + .iter() + .find(|(idx, _)| idx == &self.screen_idx) + .map(|(_, app)| app.as_ref().unwrap()) + .unwrap() + .as_str(), + ) + .unwrap() + .lock() + .unwrap(); + + self.scrollback_lines = + if self.scrollback_lines < active_parser.screen().size().0 as usize { + self.scrollback_lines + 1 + } else { + self.scrollback_lines + }; + active_parser.set_scrollback(self.scrollback_lines); + } + MouseEventKind::ScrollUp => { + let active_parser = self + .properties + .apps_parsers_map + .get( + self.properties + .active_apps + .iter() + .find(|(idx, _)| idx == &self.screen_idx) + .map(|(_, app)| app.as_ref().unwrap()) + .unwrap() + .as_str(), + ) + .unwrap(); + + self.scrollback_lines = self + .scrollback_lines + .checked_sub(1) + .unwrap_or(self.scrollback_lines); + active_parser + .lock() + .unwrap() + .set_scrollback(self.scrollback_lines); + } + _ => {} + } + } +} + +impl SectionActivation for TerminalBox { + fn activate(&mut self) {} + + fn deactivate(&mut self) {} +} + +pub struct RenderProps { + pub area: Rect, + pub border_color: Color, + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + pub show_cursor: bool, +} + +impl ComponentRender for TerminalBox { + fn render(&mut self, frame: &mut ratatui::prelude::Frame, properties: RenderProps) { + let chunks = ratatui::layout::Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .margin(1) + .constraints( + [ + ratatui::layout::Constraint::Percentage(100), + ratatui::layout::Constraint::Min(1), + ] + .as_ref(), + ) + .split(properties.area); + + if let Some(Some(active_app)) = self + .properties + .active_apps + .iter() + .find(|(idx, _)| idx == &self.screen_idx) + .map(|(_, app)| app) + { + // writeln!(file, "We have active app when rendering the terminal"); + if let Some(parser) = self.properties.apps_parsers_map.get(active_app) { + let mut parser = parser.lock().unwrap(); + parser.set_size( + (properties.area.rows().count() as u16) - 5, + properties.area.columns().count() as u16, + ); + + let pseudo_terminal = PseudoTerminal::new(parser.screen()).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::new().fg(properties.border_color)) + .style(Style::default().add_modifier(Modifier::BOLD)), + ); + frame.render_widget(pseudo_terminal, chunks[0]); + } + }; + + let explanation = "Press q to exit".to_string(); + let explanation = Paragraph::new(explanation) + .style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED)) + .alignment(Alignment::Center); + + frame.render_widget(explanation, chunks[1]); + } +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/main_page.rs b/tock-process-console/src/ui_management/pages/terminal_page/main_page.rs new file mode 100644 index 0000000..fdae89b --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/main_page.rs @@ -0,0 +1,235 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use std::collections::HashMap; + +use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; +use ratatui::{ + layout::{Constraint, Direction, Layout}, + style::Style, + widgets::{Block, Borders, Paragraph, Wrap}, +}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::{ + state_store::{Action, AppData, State}, + ui_management::components::{Component, ComponentRender}, +}; + +use super::{ + applications_page::ApplicationsPage, + components::apps_list, + section::usage::{widget_usage_to_text, UsageInfo, UsageInfoLine}, +}; + +struct Properties { + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + active_apps: Vec<(usize, Option)>, + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + app_data_map: HashMap, +} + +impl From<&State> for Properties { + fn from(state: &State) -> Self { + Self { + active_apps: state.active_apps.clone(), + app_data_map: state.apps_data_map.clone(), + } + } +} + +pub struct MainPage { + pub action_sender: UnboundedSender, + // TODO(NegrilaRares): investigate if we need port + #[allow(dead_code)] + properties: Properties, + pub hovered_screen: usize, + pub active_screen: Option, + pub screens: Vec, +} + +impl MainPage { + fn hover_next_screen(&mut self) { + let current_idx = self.hovered_screen; + let next_idx = (current_idx + 1) % self.screens.len(); + + self.hovered_screen = next_idx; + } + + fn hover_previous_screen(&mut self) { + let current_idx = self.hovered_screen; + let previous_idx = if current_idx == 0 { + self.screens.len() - 1 + } else { + current_idx - 1 + }; + + self.hovered_screen = previous_idx; + } + fn disable_active_screen(&mut self) { + self.active_screen = None; + } +} +impl Component for MainPage { + fn new( + state: &State, + _screen_idx: Option, + action_sender: UnboundedSender, + ) -> Self + where + Self: Sized, + { + MainPage { + action_sender: action_sender.clone(), + properties: Properties::from(state), + hovered_screen: 0, + active_screen: Option::None, + screens: vec![ApplicationsPage::new(state, Some(0), action_sender.clone())], + } + .update_with_state(state) + } + + fn update_with_state(self, state: &State) -> Self + where + Self: Sized, + { + let mut updated_screens = Vec::new(); + for screen in self.screens.clone() { + updated_screens.push(screen.update_with_state(state)) + } + + let action_sender = self.screens.last().unwrap().action_sender.clone(); + for (new_screen_idx, _) in state.active_apps.clone() { + if !self + .screens + .iter() + .any(|app| app.screen_idx == new_screen_idx) + { + updated_screens.push(ApplicationsPage::new( + state, + Some(new_screen_idx), + action_sender.clone(), + )); + } + } + + MainPage { + properties: Properties::from(state), + screens: updated_screens, + ..self + } + } + + fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) { + if key.kind != KeyEventKind::Press { + return; + } + + let active_screen = self.active_screen; + + match active_screen { + None => match key.code { + KeyCode::Tab => { + let _ = self.action_sender.send(Action::AddScreen { + screen_idx: self.screens.len(), + }); + } + KeyCode::Left => self.hover_previous_screen(), + KeyCode::Right => self.hover_next_screen(), + KeyCode::Enter => { + let last_hovered_screen = self.hovered_screen; + self.active_screen = Some(last_hovered_screen); + } + KeyCode::Char('q') => { + let _ = self.action_sender.send(Action::Exit); + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + let _ = self.action_sender.send(Action::Exit); + } + _ => {} + }, + Some(screen) => { + self.screens[screen].handle_key_event(key); + + if key.code == KeyCode::Esc { + self.disable_active_screen(); + } + } + } + } + + fn handle_mouse_event(&mut self, event: crossterm::event::MouseEvent) { + if let Some(active_screen) = self.active_screen { + self.screens[active_screen].handle_mouse_event(event); + } + } +} + +impl ComponentRender<()> for MainPage { + fn render(&mut self, frame: &mut ratatui::prelude::Frame, _properties: ()) { + let [left, right] = *Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(80), Constraint::Percentage(20)].as_ref()) + .split(frame.area()) + else { + panic!("The main layout should have 3 chunks") + }; + + let mut constraints: Vec = Vec::new(); + for _ in 0..self.screens.len() { + constraints.push(Constraint::Percentage(100 / (self.screens.len()) as u16)); + } + + let terminals = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints.as_slice()) + .split(left); + + for idx in 0..terminals.len() { + let is_hovered_screen = idx == self.hovered_screen; + let is_active_screen = idx == self.active_screen.unwrap_or(usize::MAX); + + self.screens[idx].render( + frame, + apps_list::RenderProperties { + border_color: if is_active_screen { + ratatui::style::Color::LightMagenta + } else if is_hovered_screen { + ratatui::style::Color::DarkGray + } else { + ratatui::style::Color::White + }, + area: terminals[idx], + }, + ) + } + + // let usage_text: Text = widget_usage_to_text(self.usage_info()); + let usage_info = UsageInfo { + description: Some("Select the running process to interact with".into()), + lines: vec![ + UsageInfoLine { + keys: vec!["Esc".into()], + description: "to cancel.".into(), + }, + UsageInfoLine { + keys: vec!["↑".into(), "↓".into()], + description: "to navigate".into(), + }, + UsageInfoLine { + keys: vec!["Enter".into()], + description: "to select a process.".into(), + }, + ], + }; + let usage_text = widget_usage_to_text(usage_info); + let usage_text = usage_text.patch_style(Style::default()); + let usage = Paragraph::new(usage_text) + .wrap(Wrap { trim: true }) + .block(Block::default().borders(Borders::ALL).title("Usage")); + frame.render_widget(usage, right); + } +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/mod.rs b/tock-process-console/src/ui_management/pages/terminal_page/mod.rs new file mode 100644 index 0000000..5f7517c --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/mod.rs @@ -0,0 +1,8 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod applications_page; +mod components; +pub mod main_page; +mod section; diff --git a/tock-process-console/src/ui_management/pages/terminal_page/section/mod.rs b/tock-process-console/src/ui_management/pages/terminal_page/section/mod.rs new file mode 100644 index 0000000..2b4f4d6 --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/section/mod.rs @@ -0,0 +1,9 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod usage; +pub trait SectionActivation { + fn activate(&mut self); + fn deactivate(&mut self); +} diff --git a/tock-process-console/src/ui_management/pages/terminal_page/section/usage.rs b/tock-process-console/src/ui_management/pages/terminal_page/section/usage.rs new file mode 100644 index 0000000..c6e20bc --- /dev/null +++ b/tock-process-console/src/ui_management/pages/terminal_page/section/usage.rs @@ -0,0 +1,63 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use ratatui::{ + style::Stylize, + text::{Line, Span, Text}, +}; + +pub struct UsageInfoLine { + pub keys: Vec, + pub description: String, +} + +pub struct UsageInfo { + pub description: Option, + pub lines: Vec, +} + +fn key_to_span<'a>(key: &String) -> Span<'a> { + Span::from(format!("( {} )", key)).bold() +} + +pub fn widget_usage_to_text<'a>(usage: UsageInfo) -> Text<'a> { + let mut lines: Vec = vec![]; + + if let Some(description) = usage.description { + lines.push(Line::from(description)) + } + + lines.push(Line::from("")); + + for command in usage.lines { + let mut bindings: Vec = match command.keys.len() { + 0 => vec![], + 1 => vec![key_to_span(&command.keys[0])], + 2 => vec![ + key_to_span(&command.keys[0]), + " or ".into(), + key_to_span(&command.keys[1]), + ], + _ => { + let mut bindings: Vec = Vec::with_capacity(command.keys.len() * 2); + + for key in command.keys.iter().take(command.keys.len() - 1) { + bindings.push(key_to_span(key)); + bindings.push(", ".into()); + } + + bindings.push("or".into()); + bindings.push(key_to_span(command.keys.last().unwrap())); + + bindings + } + }; + + bindings.push(Span::from(format!(" {}", command.description))); + + lines.push(Line::from(bindings)); + } + + Text::from(lines) +} diff --git a/tock-process-console/src/ui_management/ui_manager.rs b/tock-process-console/src/ui_management/ui_manager.rs new file mode 100644 index 0000000..c9a6022 --- /dev/null +++ b/tock-process-console/src/ui_management/ui_manager.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use crate::{ + state_store::{Action, State}, + termination::Interrupted, + ui_management::{ + components::{Component, ComponentRender}, + pages::AppRouter, + }, +}; +use anyhow::Context; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io::{self, Stdout}; +use tokio::sync::{ + broadcast, + mpsc::{self, UnboundedReceiver, UnboundedSender}, +}; +use tokio_stream::StreamExt; + +pub struct UiManager { + action_writer: UnboundedSender, +} + +impl UiManager { + pub fn new() -> (Self, UnboundedReceiver) { + let (action_writer, action_reader) = mpsc::unbounded_channel(); + + (Self { action_writer }, action_reader) + } + + pub async fn main_loop( + self, + mut state_reader: UnboundedReceiver, + mut interrupt_reader: broadcast::Receiver, + ) -> anyhow::Result { + let mut app_router = { + let state = state_reader.recv().await.unwrap(); + + AppRouter::new(&state, None, self.action_writer.clone()) + }; + + let mut terminal = setup_terminal()?; + let mut crossterm_events = EventStream::new(); + + let result: anyhow::Result = loop { + tokio::select! { + // Handle state updates + Some(state) = state_reader.recv() => { + app_router = app_router.update_with_state(&state); + }, + // Handle interruption signal + Ok(interrupted) = interrupt_reader.recv() => { + break Ok(interrupted); + }, + result = crossterm_events.next() => { + match result { + Some(Ok(Event::Key(key))) => { + app_router.handle_key_event(key); + }, + Some(Ok(Event::Mouse(event))) => { + app_router.handle_mouse_event(event); + } + None => break Ok(Interrupted::UserRequest), + _ => (), + } + } + } + + if let Err(err) = terminal + .draw(|frame| app_router.render(frame, ())) + .context("Could not render the terminal") + { + break Err(err); + } + }; + + let _ = close_terminal(&mut terminal); + + result + } +} + +fn setup_terminal() -> anyhow::Result>> { + let mut stdout = io::stdout(); + + enable_raw_mode()?; + + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + Ok(Terminal::new(CrosstermBackend::new(stdout))?) +} + +fn close_terminal(terminal: &mut Terminal>) -> anyhow::Result<()> { + disable_raw_mode()?; + + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + Ok(terminal.show_cursor()?) +} diff --git a/tockloader-cli/.gitignore b/tockloader-cli/.gitignore new file mode 100644 index 0000000..99bfb9a --- /dev/null +++ b/tockloader-cli/.gitignore @@ -0,0 +1,5 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + +/target \ No newline at end of file diff --git a/tockloader-cli/Cargo.toml b/tockloader-cli/Cargo.toml new file mode 100644 index 0000000..329e4ae --- /dev/null +++ b/tockloader-cli/Cargo.toml @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + +[package] +name = "tockloader" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1.73" +bytes = "1.5.0" +clap = { version = "4.1.1", features = ["cargo"] } +console = "0.15.7" +enum_dispatch = "0.3.12" +futures = "0.3.28" +tokio = { version = "1.32.0", features = ["full"] } +tokio-serial = "5.4.4" +tokio-util = { version = "0.7.8", features = ["full"] } +tock-process-console = { path = "../tock-process-console/" } +probe-rs = "0.24.0" +tbf-parser = { path = "../tbf-parser"} +glob = "0.3.1" +inquire = "0.7.5" +tockloader-lib = { path = "../tockloader-lib/" } +anyhow = "1.0.89" diff --git a/src/cli.rs b/tockloader-cli/src/cli.rs similarity index 60% rename from src/cli.rs rename to tockloader-cli/src/cli.rs index 1a903d5..5eccb58 100644 --- a/src/cli.rs +++ b/tockloader-cli/src/cli.rs @@ -1,11 +1,15 @@ -use clap::{arg, crate_version, Command}; +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use clap::{arg, crate_version, value_parser, Command}; /// Create the [command](clap::Command) object which will handle all of the command line arguments. pub fn make_cli() -> Command { Command::new("tockloader") .about("This is a sample description.") .version(crate_version!()) - .arg_required_else_help(true) + .subcommand_required(true) .subcommands(get_subcommands()) .args([ arg!(--debug "Print additional debugging information").action(clap::ArgAction::SetTrue) @@ -16,11 +20,28 @@ pub fn make_cli() -> Command { /// Generate all of the [subcommands](clap::Command) used by the program. fn get_subcommands() -> Vec { - vec![Command::new("listen") - .about("Open a terminal to receive UART data") - .args(get_app_args()) - .args(get_channel_args()) - .arg_required_else_help(true)] + vec![ + Command::new("listen") + .about("Open a terminal to receive UART data") + .args(get_app_args()) + .args(get_channel_args()) + .arg_required_else_help(false), + Command::new("list") + .about("List and inspect probes") + .args(get_app_args()) + .args(get_channel_args()) + .arg_required_else_help(true), + Command::new("info") + .about("Verbose information about the connected board") + .args(get_app_args()) + .args(get_channel_args()) + .arg_required_else_help(true), + Command::new("install") + .about("Install apps") + .args(get_app_args()) + .args(get_channel_args()) + .arg_required_else_help(false), + ] } /// Generate all of the [arguments](clap::Arg) that are required by subcommands which work with apps. @@ -39,24 +60,8 @@ fn get_app_args() -> Vec { /// with channels and computer-board communication. fn get_channel_args() -> Vec { vec![ - arg!(-p --port "The serial port or device name to use"), - arg!(--serial "Use the serial bootloader to flash") - .action(clap::ArgAction::SetTrue), - arg!(--jlink "Use JLinkExe to flash") - .action(clap::ArgAction::SetTrue), - arg!(--openocd "Use OpenOCD to flash") - .action(clap::ArgAction::SetTrue), - arg!(--"jlink-device" "The device type to pass to JLinkExe. Useful for initial commissioning.") - .default_value("cortex-m0"), - arg!(--"jlink-cmd" "The JLinkExe binary to invoke"), - arg!(--"jlink-speed" "The JLink speed to pass to JLinkExe"), - arg!(--"jlink-if" "The interface type to pass to JLinkExe"), - arg!(--"openocd-board" "The cfg file in OpenOCD `board` folder"), - arg!(--"openocd-cmd" "The openocd binary to invoke") - .default_value("openocd"), - // These may not work out of the box - arg!(--"openocd-options" "Tockloader-specific flags to direct how Tockloader uses OpenOCD"), - arg!(--"openocd-commands" "Directly specify which OpenOCD commands to use for \"program\", \"read\", or \"erase\" actions"), + arg!(-p --port "The serial port or device name to use"), + arg!(--serial "Use the serial bootloader to flash").action(clap::ArgAction::SetTrue), // ----- arg!(--"flash-file" "Operate on a binary flash file instead of a proper board") .action(clap::ArgAction::SetTrue), @@ -65,8 +70,14 @@ fn get_channel_args() -> Vec { arg!(--"page-size" "Explicitly specify how many bytes in a flash page") .default_value("0"), arg!(--"baud-rate" "If using serial, set the target baud rate") + .value_parser(value_parser!(u32)) .default_value("115200"), arg!(--"no-bootloader-entry" "Tell Tockloader to assume the bootloader is already active") .action(clap::ArgAction::SetTrue), + arg!(--chip "Explicitly specify the chip"), + arg!(--core "Explicitly specify the core") + .value_parser(clap::value_parser!(usize)) + .default_value("0"), + arg!(--tab "Specify the path of the tab file"), ] } diff --git a/tockloader-cli/src/display.rs b/tockloader-cli/src/display.rs new file mode 100644 index 0000000..5fe1e3e --- /dev/null +++ b/tockloader-cli/src/display.rs @@ -0,0 +1,235 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use tockloader_lib::attributes::{ + app_attributes::AppAttributes, system_attributes::SystemAttributes, +}; + +pub async fn print_list(app_details: &mut [AppAttributes]) { + for (i, details) in app_details.iter().enumerate() { + println!("\n\x1b[0m\x1b[1;35m ┏━━━━━━━━━━━━━━━━┓"); + println!( + "\x1b[0m\x1b[1;31m ┃ \x1b[0m\x1b[1;32m App_{:<9?} \x1b[0m\x1b[1;31m┃", + i + ); + println!("\x1b[0m\x1b[1;33m ┗━━━━━━━━━━━━━━━━┛"); + println!( + "\n \x1b[1;32m Name: {}", + details.tbf_header.get_package_name().unwrap() + ); + + println!( + " \x1b[1;32m Version: {}", + details.tbf_header.get_binary_version() + ); + + println!( + " \x1b[1;32m Enabled: {}", + details.tbf_header.enabled() + ); + + println!( + " \x1b[1;32m Sticky: {}", + details.tbf_header.sticky() + ); + + println!( + " \x1b[1;32m Total_Size: {}\n\n", + details.tbf_header.total_size() + ); + } +} + +pub async fn print_info(app_details: &mut [AppAttributes], system_details: &mut SystemAttributes) { + for (i, details) in app_details.iter().enumerate() { + println!("\n\x1b[0m\x1b[1;35m ┏━━━━━━━━━━━━━━━━┓"); + println!( + "\x1b[0m\x1b[1;31m ┃ \x1b[0m\x1b[1;32m App_{:<9?} \x1b[0m\x1b[1;31m┃", + i + ); + println!("\x1b[0m\x1b[1;33m ┗━━━━━━━━━━━━━━━━┛"); + println!( + "\n \x1b[1;32m Name: {}", + details.tbf_header.get_package_name().unwrap() + ); + + println!( + " \x1b[1;32m Version: {}", + details.tbf_header.get_binary_version() + ); + + println!( + " \x1b[1;32m Enabled: {}", + details.tbf_header.enabled() + ); + + println!( + " \x1b[1;32m Stricky: {}", + details.tbf_header.sticky() + ); + + println!( + " \x1b[1;32m Total_Size: {}", + details.tbf_header.total_size() + ); + + println!( + " \x1b[1;32m Address in Flash: {}", + system_details.appaddr.unwrap() + ); + + println!( + " \x1b[1;32m TBF version: {}", + details.tbf_header.get_binary_version() + ); + + println!( + " \x1b[1;32m header_size: {}", + details.tbf_header.header_size() + ); + + println!( + " \x1b[1;32m total_size: {}", + details.tbf_header.total_size() + ); + + println!( + " \x1b[1;32m checksum: {}", + details.tbf_header.checksum() + ); + + println!(" \x1b[1;32m flags:"); + println!( + " \x1b[1;32m enabled: {}", + details.tbf_header.enabled() + ); + + println!( + " \x1b[1;32m sticky: {}", + details.tbf_header.sticky() + ); + + println!(" \x1b[1;32m TVL: Main (1)",); + + println!( + " \x1b[1;32m init_fn_offset: {}", + details.tbf_header.get_init_function_offset() + ); + + println!( + " \x1b[1;32m protected_size: {}", + details.tbf_header.get_protected_size() + ); + + println!( + " \x1b[1;32m minimum_ram_size: {}", + details.tbf_header.get_minimum_app_ram_size() + ); + + println!(" \x1b[1;32m TVL: Program (9)",); + + println!( + " \x1b[1;32m init_fn_offset: {}", + details.tbf_header.get_init_function_offset() + ); + + println!( + " \x1b[1;32m protected_size: {}", + details.tbf_header.get_protected_size() + ); + + println!( + " \x1b[1;32m minimum_ram_size: {}", + details.tbf_header.get_minimum_app_ram_size() + ); + + println!( + " \x1b[1;32m binary_end_offset: {}", + details.tbf_header.get_binary_end() + ); + + println!( + " \x1b[1;32m app_version: {}", + details.tbf_header.get_binary_version() + ); + + println!(" \x1b[1;32m TVL: Package Name (3)",); + + println!( + " \x1b[1;32m package_name: {}", + details.tbf_header.get_package_name().unwrap() + ); + + println!(" \x1b[1;32m TVL: Kernel Version (8)",); + + println!( + " \x1b[1;32m kernel_major: {}", + details.tbf_header.get_kernel_version().unwrap().0 + ); + + println!( + " \x1b[1;32m kernel_minor: {}", + details.tbf_header.get_kernel_version().unwrap().1, + ); + + println!("\n \x1b[1;32m Footer"); + + let mut total_footer_size: u32 = 0; + + // Usage of +4 is a result of the structure of the Tock Binary Format (https://book.tockos.org/doc/tock_binary_format) + // Because we need the real size of the footer including the type and length. + for footer_details in details.tbf_footers.iter() { + total_footer_size += footer_details.size + 4; + } + + println!( + " \x1b[1;32m footer_size: {}", + total_footer_size + ); + + for (i, footer_details) in details.tbf_footers.iter().enumerate() { + println!(" \x1b[1;32m Footer [{i}] TVL: Credentials"); + + println!( + " \x1b[1;32m Type: {}", + footer_details.credentials.get_type() + ); + + // Usage of -4 is a result of the structure of the Tock Binary Format (https://book.tockos.org/doc/tock_binary_format) + // Because we only need the size of the credentials without the type and length bytes. + println!( + " \x1b[1;32m Length: {}", + footer_details.size - 4 + ); + } + } + + println!("\n\n\x1b[1;32m Kernel Attributes"); + println!( + "\x1b[1;32m Sentinel: {:<10}", + system_details.sentinel.clone().unwrap() + ); + println!( + "\x1b[1;32m Version: {:<10}", + system_details.kernel_version.unwrap() + ); + println!("\x1b[1;32m KATLV: APP Memory"); + println!( + "\x1b[1;32m app_memory_start: {:<10}", + system_details.app_mem_start.unwrap() + ); + println!( + "\x1b[1;32m app_memory_len: {:<10}", + system_details.app_mem_len.unwrap() + ); + println!("\x1b[1;32m KATLV: Kernel Binary"); + println!( + "\x1b[1;32m kernel_binary_start: {:<10}", + system_details.kernel_bin_start.unwrap() + ); + println!( + "\x1b[1;32m kernel_binary_len: {:<10}\n\n", + system_details.kernel_bin_len.unwrap() + ); +} diff --git a/tockloader-cli/src/main.rs b/tockloader-cli/src/main.rs new file mode 100644 index 0000000..311bd61 --- /dev/null +++ b/tockloader-cli/src/main.rs @@ -0,0 +1,147 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod cli; +mod display; + +use anyhow::{Context, Result}; +use cli::make_cli; +use display::{print_info, print_list}; +use inquire::Select; +use tockloader_lib::{ + connection::{Connection, ConnectionInfo}, + info, install_app, list, list_debug_probes, list_serial_ports, + tabs::tab::Tab, +}; + +#[tokio::main] + +async fn main() -> Result<()> { + let matches = make_cli().get_matches(); + + if matches.get_flag("debug") { + println!("Debug mode enabled."); + } + + match matches.subcommand() { + Some(("listen", _sub_matches)) => { + tock_process_console::run() + .await + .context("Failed to run console.")?; + } + Some(("list", sub_matches)) => { + if sub_matches.get_one::("serial").is_some() { + let serial_ports = list_serial_ports().context("Failed to list serial ports.")?; + let port_names: Vec<_> = serial_ports.iter().map(|p| p.port_name.clone()).collect(); + let ans = Select::new("Which serial port do you want to use?", port_names) + .prompt() + .context("No device is connected.")?; + let conn = Connection::open(ConnectionInfo::from(ans), None) + .context("Failed to open serial connection.")?; + let mut apps_details = list(conn, None).await.context("Failed to list apps.")?; + print_list(&mut apps_details).await; + } else { + let ans = Select::new("Which debug probe do you want to use?", list_debug_probes()) + .prompt() + .context("No debug probe is connected.")?; + let conn = Connection::open( + ConnectionInfo::ProbeInfo(ans), + Some( + sub_matches + .get_one::("chip") + .context("No chip has been provided.")? + .to_string(), + ), + ) + .context("Failed to open probe connection.")?; + let mut apps_details = list(conn, sub_matches.get_one::("core")) + .await + .context("Failed to list apps.")?; + print_list(&mut apps_details).await; + } + } + Some(("info", sub_matches)) => { + if sub_matches.get_one::("serial").is_some() { + let serial_ports = list_serial_ports().context("Failed to list serial ports.")?; + // Let the user choose the port that will be used + let port_names: Vec<_> = serial_ports.iter().map(|p| p.port_name.clone()).collect(); + let ans = Select::new("Which serial port do you want to use?", port_names) + .prompt() + .context("No device is connected.")?; + // Open connection + let conn = Connection::open(ConnectionInfo::from(ans), None) + .context("Failed to open serial connection.")?; + let mut attributes = info(conn, None) + .await + .context("Failed to get data from the board.")?; + print_info(&mut attributes.apps, &mut attributes.system).await; + } else { + // TODO(Micu Ana): Add error handling + let ans = Select::new("Which debug probe do you want to use?", list_debug_probes()) + .prompt() + .context("No debug probe is connected.")?; + // Open connection + let conn = Connection::open( + ConnectionInfo::ProbeInfo(ans), + Some( + sub_matches + .get_one::("chip") + .context("No chip has been provided.")? + .to_string(), + ), + ) + .context("Failed to open probe connection.")?; + + let mut attributes = info(conn, sub_matches.get_one::("core")) + .await + .context("Failed to get data from the board.")?; + + print_info(&mut attributes.apps, &mut attributes.system).await; + } + } + Some(("install", sub_matches)) => { + let tab_file = Tab::open(sub_matches.get_one::("tab").unwrap().to_string()) + .context("Failed to use provided tab file.")?; + // If "--serial" flag is used, we choose the serial connection + if sub_matches.get_one::("serial").is_some() { + let serial_ports = list_serial_ports().context("Failed to list serial ports.")?; + // Let the user choose the port that will be used + let port_names: Vec<_> = serial_ports.iter().map(|p| p.port_name.clone()).collect(); + let ans = Select::new("Which serial port do you want to use?", port_names) + .prompt() + .context("No device is connected.")?; + // Open connection + let conn = Connection::open(ConnectionInfo::from(ans), None) + .context("Failed to open serial connection.")?; + // Install app + install_app(conn, None, tab_file) + .await + .context("Failed to install app.")?; + } else { + let ans = Select::new("Which debug probe do you want to use?", list_debug_probes()) + .prompt() + .context("No debug probe is connected.")?; + let conn = Connection::open( + ConnectionInfo::ProbeInfo(ans), + Some( + sub_matches + .get_one::("chip") + .context("No chip has been provided.")? + .to_string(), + ), + ) + .context("Failed to open probe connection.")?; + // Install app + install_app(conn, sub_matches.get_one::("core"), tab_file) + .await + .context("Failed to install app.")?; + } + } + _ => { + println!("Could not run the provided subcommand."); + _ = make_cli().print_help(); + } + } + Ok(()) +} diff --git a/tockloader-lib/Cargo.toml b/tockloader-lib/Cargo.toml new file mode 100644 index 0000000..3f35775 --- /dev/null +++ b/tockloader-lib/Cargo.toml @@ -0,0 +1,21 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright OXIDOS AUTOMOTIVE 2024. + +[package] +name = "tockloader-lib" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.32.0", features = ["full"] } +tokio-serial = {version = "5.4.4", features = ["libudev"]} +probe-rs = "0.24.0" +tbf-parser = { path = "../tbf-parser"} +utf8-decode = "1.0.1" +byteorder = "1.5.0" +tar = "0.4.41" +bytes = "1.7.1" +toml = "0.8.19" +serde = { version = "1.0.210", features = ["derive"] } +thiserror = "1.0.63" diff --git a/tockloader-lib/src/attributes/app_attributes.rs b/tockloader-lib/src/attributes/app_attributes.rs new file mode 100644 index 0000000..9369b53 --- /dev/null +++ b/tockloader-lib/src/attributes/app_attributes.rs @@ -0,0 +1,223 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use probe_rs::{Core, MemoryInterface}; + +use tbf_parser::{ + self, + parse::{parse_tbf_footer, parse_tbf_header, parse_tbf_header_lengths}, + types::{TbfFooterV2Credentials, TbfHeader}, +}; +use tokio_serial::SerialStream; + +use crate::{ + bootloader_serial::{issue_command, Command, Response}, + errors::TockloaderError, +}; + +#[derive(Debug)] +pub struct AppAttributes { + pub tbf_header: TbfHeader, + pub tbf_footers: Vec, +} + +#[derive(Debug)] +pub struct TbfFooter { + pub credentials: TbfFooterV2Credentials, + pub size: u32, +} + +impl TbfFooter { + pub fn new(credentials: TbfFooterV2Credentials, size: u32) -> TbfFooter { + TbfFooter { credentials, size } + } +} + +impl AppAttributes { + pub(crate) fn new(header_data: TbfHeader, footers_data: Vec) -> AppAttributes { + AppAttributes { + tbf_header: header_data, + tbf_footers: footers_data, + } + } + + // TODO: Document this function + pub(crate) fn read_apps_data_probe( + board_core: &mut Core, + addr: u64, + ) -> Result, TockloaderError> { + let mut appaddr: u64 = addr; + let mut apps_counter = 0; + let mut apps_details: Vec = vec![]; + + loop { + let mut appdata = vec![0u8; 8]; + + board_core + .read(appaddr, &mut appdata) + .map_err(TockloaderError::ProbeRsReadError)?; + + let tbf_version: u16; + let header_size: u16; + let total_size: u32; + + match parse_tbf_header_lengths( + &appdata + .try_into() + .expect("Buffer length must be at least 8 bytes long."), + ) { + Ok(data) => { + tbf_version = data.0; + header_size = data.1; + total_size = data.2; + } + _ => return Ok(apps_details), + }; + + let mut header_data = vec![0u8; header_size as usize]; + + board_core + .read(appaddr, &mut header_data) + .map_err(TockloaderError::ProbeRsReadError)?; + let header = parse_tbf_header(&header_data, tbf_version) + .map_err(TockloaderError::ParsingError)?; + + let binary_end_offset = header.get_binary_end(); + + let mut footers: Vec = vec![]; + let total_footers_size = total_size - binary_end_offset; + let mut footer_offset = binary_end_offset; + let mut footer_number = 0; + + loop { + let mut appfooter = + vec![0u8; (total_footers_size - (footer_offset - binary_end_offset)) as usize]; + + board_core + .read(appaddr + footer_offset as u64, &mut appfooter) + .map_err(TockloaderError::ProbeRsReadError)?; + + let footer_info = + parse_tbf_footer(&appfooter).map_err(TockloaderError::ParsingError)?; + + footers.insert(footer_number, TbfFooter::new(footer_info.0, footer_info.1)); + + footer_number += 1; + footer_offset += footer_info.1 + 4; + + if footer_offset == total_size { + break; + } + } + + let details: AppAttributes = AppAttributes::new(header, footers); + + apps_details.insert(apps_counter, details); + apps_counter += 1; + appaddr += total_size as u64; + } + } + + // TODO: Document this function + pub(crate) async fn read_apps_data_serial( + port: &mut SerialStream, + addr: u64, + ) -> Result, TockloaderError> { + let mut appaddr: u64 = addr; + let mut apps_counter = 0; + let mut apps_details: Vec = vec![]; + + loop { + let mut pkt = (appaddr as u32).to_le_bytes().to_vec(); + let length = (8_u16).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, appdata) = + issue_command(port, Command::ReadRange, pkt, true, 8, Response::ReadRange).await?; + + let tbf_version: u16; + let header_size: u16; + let total_size: u32; + + match parse_tbf_header_lengths( + &appdata[0..8] + .try_into() + .expect("Buffer length must be at least 8 bytes long."), + ) { + Ok(data) => { + tbf_version = data.0; + header_size = data.1; + total_size = data.2; + } + _ => break, + }; + + let mut pkt = (appaddr as u32).to_le_bytes().to_vec(); + let length = (header_size).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, header_data) = issue_command( + port, + Command::ReadRange, + pkt, + true, + header_size.into(), + Response::ReadRange, + ) + .await?; + + let header = parse_tbf_header(&header_data, tbf_version) + .map_err(TockloaderError::ParsingError)?; + let binary_end_offset = header.get_binary_end(); + + let mut footers: Vec = vec![]; + let total_footers_size = total_size - binary_end_offset; + let mut footer_offset = binary_end_offset; + let mut footer_number = 0; + + loop { + let mut pkt = (appaddr as u32 + footer_offset).to_le_bytes().to_vec(); + let length = ((total_footers_size - (footer_offset - binary_end_offset)) as u16) + .to_le_bytes() + .to_vec(); + for i in length { + pkt.push(i); + } + + let (_, appfooter) = issue_command( + port, + Command::ReadRange, + pkt, + true, + (total_footers_size - (footer_offset - binary_end_offset)) as usize, + Response::ReadRange, + ) + .await?; + + let footer_info = + parse_tbf_footer(&appfooter).map_err(TockloaderError::ParsingError)?; + + footers.insert(footer_number, TbfFooter::new(footer_info.0, footer_info.1)); + + footer_number += 1; + footer_offset += footer_info.1 + 4; + + if footer_offset == total_size { + break; + } + } + + let details: AppAttributes = AppAttributes::new(header, footers); + + apps_details.insert(apps_counter, details); + apps_counter += 1; + appaddr += total_size as u64; + } + Ok(apps_details) + } +} diff --git a/tockloader-lib/src/attributes/decode.rs b/tockloader-lib/src/attributes/decode.rs new file mode 100644 index 0000000..3579e08 --- /dev/null +++ b/tockloader-lib/src/attributes/decode.rs @@ -0,0 +1,59 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +#[derive(Debug)] +pub struct DecodedAttribute { + pub key: String, + pub value: String, +} + +impl DecodedAttribute { + pub(crate) fn new(decoded_key: String, decoded_value: String) -> DecodedAttribute { + DecodedAttribute { + key: decoded_key, + value: decoded_value, + } + } +} + +// TODO: explain what is happening here +pub(crate) fn decode_attribute(step: &[u8]) -> Option { + let raw_key = &step[0..8]; + + let decoder_key = utf8_decode::Decoder::new(raw_key.iter().cloned()); + + let mut key = String::new(); + for n in decoder_key { + key.push(n.expect("Error getting key for attributes.")); + } + + key = key.trim_end_matches('\0').to_string(); + let vlen = step[8]; + + if vlen > 55 || vlen == 0 { + return None; + } + let raw_value = &step[9..(9 + vlen as usize)]; + let decoder_value = utf8_decode::Decoder::new(raw_value.iter().cloned()); + + let mut value = String::new(); + + for n in decoder_value { + value.push(n.expect("Error getting key for attributes.")); + } + + value = value.trim_end_matches('\0').to_string(); + Some(DecodedAttribute::new(key, value)) +} + +// TODO: explain what is happening here +pub(crate) fn bytes_to_string(raw: &[u8]) -> String { + let decoder = utf8_decode::Decoder::new(raw.iter().cloned()); + + let mut string = String::new(); + for n in decoder { + string.push(n.expect("Error getting key for attributes.")); + } + string +} diff --git a/tockloader-lib/src/attributes/general_attributes.rs b/tockloader-lib/src/attributes/general_attributes.rs new file mode 100644 index 0000000..a072e27 --- /dev/null +++ b/tockloader-lib/src/attributes/general_attributes.rs @@ -0,0 +1,23 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use super::{app_attributes::AppAttributes, system_attributes::SystemAttributes}; + +#[derive(Debug)] +pub struct GeneralAttributes { + pub system: SystemAttributes, + pub apps: Vec, +} + +impl GeneralAttributes { + pub(crate) fn new( + system_attributes: SystemAttributes, + apps_attributes: Vec, + ) -> GeneralAttributes { + GeneralAttributes { + system: system_attributes, + apps: apps_attributes, + } + } +} diff --git a/tockloader-lib/src/attributes/mod.rs b/tockloader-lib/src/attributes/mod.rs new file mode 100644 index 0000000..b35657f --- /dev/null +++ b/tockloader-lib/src/attributes/mod.rs @@ -0,0 +1,8 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod app_attributes; +pub mod decode; +pub mod general_attributes; +pub mod system_attributes; diff --git a/tockloader-lib/src/attributes/system_attributes.rs b/tockloader-lib/src/attributes/system_attributes.rs new file mode 100644 index 0000000..d086123 --- /dev/null +++ b/tockloader-lib/src/attributes/system_attributes.rs @@ -0,0 +1,270 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use byteorder::{ByteOrder, LittleEndian}; +use probe_rs::{Core, MemoryInterface}; +use tokio_serial::SerialStream; + +use crate::{ + bootloader_serial::{issue_command, Command, Response}, + errors::TockloaderError, +}; + +use super::decode::{bytes_to_string, decode_attribute}; + +#[derive(Debug)] +pub struct SystemAttributes { + pub board: Option, + pub arch: Option, + pub appaddr: Option, + pub boothash: Option, + pub bootloader_version: Option, + pub sentinel: Option, + pub kernel_version: Option, + pub app_mem_start: Option, + pub app_mem_len: Option, + pub kernel_bin_start: Option, + pub kernel_bin_len: Option, +} + +impl SystemAttributes { + pub(crate) fn new() -> SystemAttributes { + SystemAttributes { + board: None, + arch: None, + appaddr: None, + boothash: None, + bootloader_version: None, + sentinel: None, + kernel_version: None, + app_mem_start: None, + app_mem_len: None, + kernel_bin_start: None, + kernel_bin_len: None, + } + } + + // TODO: explain what is happening here + pub(crate) fn read_system_attributes_probe( + board_core: &mut Core, + ) -> Result { + let mut result = SystemAttributes::new(); + let address = 0x600; + let mut buf = [0u8; 64 * 16]; + + let _ = board_core.read(address, &mut buf); + + let mut data = buf.chunks(64); + + for index_data in 0..data.len() { + let step = match data.next() { + Some(data) => data, + None => break, + }; + + let step_option = decode_attribute(step); + + if let Some(decoded_attributes) = step_option { + match index_data { + 0 => { + result.board = Some(decoded_attributes.value.to_string()); + } + 1 => { + result.arch = Some(decoded_attributes.value.to_string()); + } + 2 => { + result.appaddr = Some( + u64::from_str_radix( + decoded_attributes + .value + .to_string() + .trim_start_matches("0x"), + 16, + ) + .map_err(|_| { + TockloaderError::MisconfiguredBoard( + "Invalid start address.".to_owned(), + ) + })?, + ); + } + 3 => { + result.boothash = Some(decoded_attributes.value.to_string()); + } + _ => {} + } + } else { + continue; + } + } + + let address = 0x40E; + + let mut buf = [0u8; 8]; + + let _ = board_core.read_8(address, &mut buf); + + let string = String::from_utf8(buf.to_vec()).map_err(|_| { + TockloaderError::MisconfiguredBoard( + "Data may be corrupted. System attribure is not UTF-8.".to_owned(), + ) + })?; + + let string = string.trim_matches(char::from(0)); + + result.bootloader_version = Some(string.to_owned()); + + let mut kernel_attr_binary = [0u8; 100]; + board_core + .read( + result.appaddr.ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))? - 100, + &mut kernel_attr_binary, + ) + .map_err(TockloaderError::ProbeRsReadError)?; + + let sentinel = bytes_to_string(&kernel_attr_binary[96..100]); + let kernel_version = LittleEndian::read_uint(&kernel_attr_binary[95..96], 1); + + let app_memory_len = LittleEndian::read_u32(&kernel_attr_binary[84..92]); + let app_memory_start = LittleEndian::read_u32(&kernel_attr_binary[80..84]); + + let kernel_binary_start = LittleEndian::read_u32(&kernel_attr_binary[68..72]); + let kernel_binary_len = LittleEndian::read_u32(&kernel_attr_binary[72..76]); + + result.sentinel = Some(sentinel); + result.kernel_version = Some(kernel_version); + result.app_mem_start = Some(app_memory_start); + result.app_mem_len = Some(app_memory_len); + result.kernel_bin_start = Some(kernel_binary_start); + result.kernel_bin_len = Some(kernel_binary_len); + + Ok(result) + } + + // TODO: explain what is happening here + pub(crate) async fn read_system_attributes_serial( + port: &mut SerialStream, + ) -> Result { + let mut result = SystemAttributes::new(); + + let mut pkt = (0x600_u32).to_le_bytes().to_vec(); + let length = (1024_u16).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, buf) = issue_command( + port, + Command::ReadRange, + pkt, + true, + 64 * 16, + Response::ReadRange, + ) + .await?; + + let mut data = buf.chunks(64); + + for index_data in 0..data.len() { + let step = match data.next() { + Some(data) => data, + None => break, + }; + + let step_option = decode_attribute(step); + + if let Some(decoded_attributes) = step_option { + match index_data { + 0 => { + result.board = Some(decoded_attributes.value.to_string()); + } + 1 => { + result.arch = Some(decoded_attributes.value.to_string()); + } + 2 => { + result.appaddr = Some( + u64::from_str_radix( + decoded_attributes + .value + .to_string() + .trim_start_matches("0x"), + 16, + ) + .map_err(|_| { + TockloaderError::MisconfiguredBoard( + "Invalid start address.".to_owned(), + ) + })?, + ); + } + 3 => { + result.boothash = Some(decoded_attributes.value.to_string()); + } + _ => {} + } + } else { + continue; + } + } + + let mut pkt = (0x40E_u32).to_le_bytes().to_vec(); + let length = (8_u16).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, buf) = + issue_command(port, Command::ReadRange, pkt, true, 8, Response::ReadRange).await?; + + let string = String::from_utf8(buf).map_err(|_| { + TockloaderError::MisconfiguredBoard( + "Data may be corrupted. System attribure is not UTF-8.".to_owned(), + ) + })?; + + let string = string.trim_matches(char::from(0)); + + result.bootloader_version = Some(string.to_owned()); + + let mut pkt = ((result.appaddr.ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))? - 100) as u32) + .to_le_bytes() + .to_vec(); + let length = (100_u16).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, kernel_attr_binary) = issue_command( + port, + Command::ReadRange, + pkt, + true, + 100, + Response::ReadRange, + ) + .await?; + + let sentinel = bytes_to_string(&kernel_attr_binary[96..100]); + let kernel_version = LittleEndian::read_uint(&kernel_attr_binary[95..96], 1); + + let app_memory_len = LittleEndian::read_u32(&kernel_attr_binary[84..92]); + let app_memory_start = LittleEndian::read_u32(&kernel_attr_binary[80..84]); + + let kernel_binary_start = LittleEndian::read_u32(&kernel_attr_binary[68..72]); + let kernel_binary_len = LittleEndian::read_u32(&kernel_attr_binary[72..76]); + + result.sentinel = Some(sentinel); + result.kernel_version = Some(kernel_version); + result.app_mem_start = Some(app_memory_start); + result.app_mem_len = Some(app_memory_len); + result.kernel_bin_start = Some(kernel_binary_start); + result.kernel_bin_len = Some(kernel_binary_len); + + Ok(result) + } +} diff --git a/tockloader-lib/src/bootloader_serial.rs b/tockloader-lib/src/bootloader_serial.rs new file mode 100644 index 0000000..739bce1 --- /dev/null +++ b/tockloader-lib/src/bootloader_serial.rs @@ -0,0 +1,219 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +// The "X" commands are for external flash + +use crate::errors; +use bytes::BytesMut; +use errors::TockloaderError; +use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_serial::{SerialPort, SerialStream}; + +// Tell the bootloader to reset its buffer to handle a new command +pub const SYNC_MESSAGE: [u8; 3] = [0x00, 0xFC, 0x05]; + +// "This was chosen as it is infrequent in .bin files" - immesys +pub const ESCAPE_CHAR: u8 = 0xFC; + +#[allow(dead_code)] +pub enum Command { + // Commands from this tool to the bootloader + Ping = 0x01, + Info = 0x03, + ID = 0x04, + Reset = 0x05, + ErasePage = 0x06, + WritePage = 0x07, + XEBlock = 0x08, + XWPage = 0x09, + Crcx = 0x10, + ReadRange = 0x11, + XRRange = 0x12, + SetAttribute = 0x13, + GetAttribute = 0x14, + CRCInternalFlash = 0x15, + Crcef = 0x16, + XEPage = 0x17, + XFinit = 0x18, + ClkOut = 0x19, + WUser = 0x20, + ChangeBaudRate = 0x21, + Exit = 0x22, + SetStartAddress = 0x23, +} + +#[derive(Clone, Debug)] +pub enum Response { + // Responses from the bootloader + Overflow = 0x10, + Pong = 0x11, + BadAddr = 0x12, + IntError = 0x13, + BadArgs = 0x14, + OK = 0x15, + Unknown = 0x16, + XFTimeout = 0x17, + Xfepe = 0x18, + Crcrx = 0x19, + ReadRange = 0x20, + XRRange = 0x21, + GetAttribute = 0x22, + CRCInternalFlash = 0x23, + Crcxf = 0x24, + Info = 0x25, + ChangeBaudFail = 0x26, + BadResp, +} + +impl From for Response { + fn from(value: u8) -> Self { + match value { + 0x10 => Response::Overflow, + 0x11 => Response::Pong, + 0x12 => Response::BadAddr, + 0x13 => Response::IntError, + 0x14 => Response::BadArgs, + 0x15 => Response::OK, + 0x16 => Response::Unknown, + 0x17 => Response::XFTimeout, + 0x18 => Response::Xfepe, + 0x19 => Response::Crcrx, + 0x20 => Response::ReadRange, + 0x21 => Response::XRRange, + 0x22 => Response::GetAttribute, + 0x23 => Response::CRCInternalFlash, + 0x24 => Response::Crcxf, + 0x25 => Response::Info, + 0x26 => Response::ChangeBaudFail, + + // This error handling is temmporary + //TODO(Micu Ana): Add error handling + _ => Response::BadResp, + } + } +} + +#[allow(dead_code)] +pub async fn toggle_bootloader_entry_dtr_rts( + port: &mut SerialStream, +) -> Result<(), TockloaderError> { + port.write_data_terminal_ready(true) + .map_err(TockloaderError::SerialInitializationError)?; + port.write_request_to_send(true) + .map_err(TockloaderError::SerialInitializationError)?; + + tokio::time::sleep(Duration::from_millis(100)).await; + + port.write_data_terminal_ready(false) + .map_err(TockloaderError::SerialInitializationError)?; + + tokio::time::sleep(Duration::from_millis(500)).await; + + port.write_request_to_send(false) + .map_err(TockloaderError::SerialInitializationError)?; + + Ok(()) +} + +#[allow(dead_code)] +pub async fn ping_bootloader_and_wait_for_response( + port: &mut SerialStream, +) -> Result { + let ping_pkt = [ESCAPE_CHAR, Command::Ping as u8]; + + let mut ret = BytesMut::with_capacity(200); + + for _ in 0..30 { + let mut bytes_written = 0; + while bytes_written != ping_pkt.len() { + bytes_written += port.write_buf(&mut &ping_pkt[bytes_written..]).await?; + } + let mut read_bytes = 0; + while read_bytes < 2 { + read_bytes += port.read_buf(&mut ret).await?; + } + if ret[1] == Response::Pong as u8 { + return Ok(Response::from(ret[1])); + } + } + Ok(Response::from(ret[1])) +} + +#[allow(dead_code)] +pub async fn issue_command( + port: &mut SerialStream, + command: Command, + mut message: Vec, + sync: bool, + response_len: usize, + response_code: Response, +) -> Result<(Response, Vec), TockloaderError> { + // Setup a command to send to the bootloader and handle the response + // Generate the message to send to the bootloader + let mut i = 0; + while i < message.len() { + if message[i] == ESCAPE_CHAR { + // Escaped by replacing all 0xFC with two consecutive 0xFC - tock bootloader readme + message.insert(i + 1, ESCAPE_CHAR); + // Skip the inserted character + i += 2; + } else { + i += 1; + } + } + message.push(ESCAPE_CHAR); + message.push(command as u8); + + // If there should be a sync/reset message, prepend the outgoing message with it + if sync { + message.insert(0, SYNC_MESSAGE[0]); + message.insert(1, SYNC_MESSAGE[1]); + message.insert(2, SYNC_MESSAGE[2]); + } + + // Write the command message + let mut bytes_written = 0; + while bytes_written != message.len() { + bytes_written += port.write_buf(&mut &message[bytes_written..]).await?; + } + + // Response has a two byte header, then response_len bytes + let bytes_to_read = 2 + response_len; + let mut ret = BytesMut::with_capacity(2); + + // We are waiting for 2 bytes to be read + let mut read_bytes = 0; + while read_bytes < 2 { + read_bytes += port.read_buf(&mut ret).await?; + } + + if ret[0] != ESCAPE_CHAR { + return Err(TockloaderError::BootloaderError(ret[0])); + } + + if ret[1] != response_code.clone() as u8 { + return Err(TockloaderError::BootloaderError(ret[1])); + } + + let mut new_data: Vec = Vec::new(); + let mut value = 2; + + if response_len != 0 { + while bytes_to_read > value { + value += port.read_buf(&mut new_data).await?; + } + + // De-escape and add array of read in the bytes + for i in 0..(new_data.len() - 1) { + if new_data[i] == ESCAPE_CHAR && new_data[i + 1] == ESCAPE_CHAR { + new_data.remove(i + 1); + } + } + + ret.extend_from_slice(&new_data); + } + + Ok((Response::from(ret[1]), ret[2..].to_vec())) +} diff --git a/tockloader-lib/src/connection.rs b/tockloader-lib/src/connection.rs new file mode 100644 index 0000000..ce96b72 --- /dev/null +++ b/tockloader-lib/src/connection.rs @@ -0,0 +1,59 @@ +use std::time::Duration; + +use probe_rs::{probe::DebugProbeInfo, Permissions, Session}; +use tokio_serial::{FlowControl, Parity, SerialPort, SerialStream, StopBits}; + +use crate::errors::TockloaderError; + +pub enum ConnectionInfo { + SerialInfo(String), + ProbeInfo(DebugProbeInfo), +} + +impl From for ConnectionInfo { + fn from(value: String) -> Self { + ConnectionInfo::SerialInfo(value) + } +} + +pub enum Connection { + ProbeRS(Session), + Serial(SerialStream), +} + +impl Connection { + pub fn open(info: ConnectionInfo, chip: Option) -> Result { + match info { + ConnectionInfo::SerialInfo(serial_info) => { + let builder = tokio_serial::new(serial_info, 115200); + match SerialStream::open(&builder) { + Ok(mut port) => { + port.set_parity(Parity::None) + .map_err(TockloaderError::SerialInitializationError)?; + port.set_stop_bits(StopBits::One) + .map_err(TockloaderError::SerialInitializationError)?; + port.set_flow_control(FlowControl::None) + .map_err(TockloaderError::SerialInitializationError)?; + port.set_timeout(Duration::from_millis(500)) + .map_err(TockloaderError::SerialInitializationError)?; + port.write_request_to_send(false) + .map_err(TockloaderError::SerialInitializationError)?; + port.write_data_terminal_ready(false) + .map_err(TockloaderError::SerialInitializationError)?; + Ok(Connection::Serial(port)) + } + Err(e) => Err(TockloaderError::SerialInitializationError(e)), + } + } + ConnectionInfo::ProbeInfo(probe_info) => { + let probe = probe_info + .open() + .map_err(TockloaderError::ProbeRsInitializationError)?; + match probe.attach(chip.unwrap(), Permissions::default()) { + Ok(session) => Ok(Connection::ProbeRS(session)), + Err(e) => Err(TockloaderError::ProbeRsCommunicationError(e)), + } + } + } + } +} diff --git a/tockloader-lib/src/errors.rs b/tockloader-lib/src/errors.rs new file mode 100644 index 0000000..6f8f20c --- /dev/null +++ b/tockloader-lib/src/errors.rs @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use std::io; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum TockloaderError { + #[error("Error occurred while trying to access core: {0}")] + CoreAccessError(usize, probe_rs::Error), + + #[error("Failed to initialize probe_rs connection due to a communication error. Inner: {0}")] + ProbeRsInitializationError(#[from] probe_rs::probe::DebugProbeError), + + #[error("Failed to establish communication with board. Inner: {0}")] + ProbeRsCommunicationError(probe_rs::Error), + + #[error("Failed to read from debug probe. Inner: {0}")] + ProbeRsReadError(probe_rs::Error), + + #[error("Failed to write binary. Inner: {0}")] + ProbeRsWriteError(#[from] probe_rs::flashing::FlashError), + + #[error("Failed to initialize serial connection due to a communication error. Inner: {0}")] + SerialInitializationError(#[from] tokio_serial::Error), + + #[error("Bootloader did not respond properly: {0}")] + BootloaderError(u8), + + #[error("No binary found for {0} architecture.")] + NoBinaryError(String), + + #[error("App data could not be parsed.")] + ParsingError(tbf_parser::types::TbfParseError), + + #[error("Failed to perform read/write operations on serial port. Inner: {0}")] + IOError(#[from] io::Error), + + #[error("Expected board attribute to be present")] + MisconfiguredBoard(String), + + #[error("Failed to use tab from provided path. Inner: {0}")] + UnusableTab(io::Error), + + #[error("Failed to parse metadata. Inner: {0}")] + InvalidMetadata(toml::de::Error), + + #[error("No metadata.toml found.")] + NoMetadata, +} diff --git a/tockloader-lib/src/lib.rs b/tockloader-lib/src/lib.rs new file mode 100644 index 0000000..83a782f --- /dev/null +++ b/tockloader-lib/src/lib.rs @@ -0,0 +1,510 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +pub mod attributes; +pub(crate) mod bootloader_serial; +pub mod connection; +mod errors; +pub mod tabs; + +use std::time::Duration; + +use attributes::app_attributes::AppAttributes; +use attributes::general_attributes::GeneralAttributes; +use attributes::system_attributes::SystemAttributes; + +use bootloader_serial::{issue_command, ping_bootloader_and_wait_for_response, Command, Response}; +use connection::Connection; +use probe_rs::flashing::DownloadOptions; +use probe_rs::probe::DebugProbeInfo; +use probe_rs::MemoryInterface; + +use errors::TockloaderError; +use tabs::tab::Tab; +use tbf_parser::parse::parse_tbf_header_lengths; +use tokio_serial::SerialPortInfo; + +pub fn list_debug_probes() -> Vec { + probe_rs::probe::list::Lister::new().list_all() +} + +pub fn list_serial_ports() -> Result, TockloaderError> { + tokio_serial::available_ports().map_err(TockloaderError::SerialInitializationError) +} + +pub async fn list( + choice: Connection, + core_index: Option<&usize>, +) -> Result, TockloaderError> { + match choice { + Connection::ProbeRS(mut session) => { + let mut core = session + .core(*core_index.unwrap()) + .map_err(|e| TockloaderError::CoreAccessError(*core_index.unwrap(), e))?; + let system_attributes = SystemAttributes::read_system_attributes_probe(&mut core)?; + let appaddr = system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + match AppAttributes::read_apps_data_probe(&mut core, appaddr) { + Ok(app_attributes) => Ok(app_attributes), + Err(e) => Err(e), + } + } + Connection::Serial(mut port) => { + let response = ping_bootloader_and_wait_for_response(&mut port).await?; + + if response as u8 != Response::Pong as u8 { + tokio::time::sleep(Duration::from_millis(100)).await; + let _ = ping_bootloader_and_wait_for_response(&mut port).await?; + } + + let system_attributes = + SystemAttributes::read_system_attributes_serial(&mut port).await?; + + let appaddr = system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + match AppAttributes::read_apps_data_serial(&mut port, appaddr).await { + Ok(app_attributes) => Ok(app_attributes), + Err(e) => Err(e), + } + } + } +} + +pub async fn info( + choice: Connection, + core_index: Option<&usize>, +) -> Result { + match choice { + Connection::ProbeRS(mut session) => { + let mut core = session + .core(*core_index.unwrap()) + .map_err(|e| TockloaderError::CoreAccessError(*core_index.unwrap(), e))?; + let system_attributes = SystemAttributes::read_system_attributes_probe(&mut core)?; + let appaddr = system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + match AppAttributes::read_apps_data_probe(&mut core, appaddr) { + Ok(app_attributes) => Ok(GeneralAttributes::new(system_attributes, app_attributes)), + Err(e) => Err(e), + } + } + Connection::Serial(mut port) => { + let response = ping_bootloader_and_wait_for_response(&mut port).await?; + + if response as u8 != Response::Pong as u8 { + tokio::time::sleep(Duration::from_millis(100)).await; + let _ = ping_bootloader_and_wait_for_response(&mut port).await?; + } + + let system_attributes = + SystemAttributes::read_system_attributes_serial(&mut port).await?; + + let appaddr = system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + match AppAttributes::read_apps_data_serial(&mut port, appaddr).await { + Ok(app_attributes) => Ok(GeneralAttributes::new(system_attributes, app_attributes)), + Err(e) => Err(e), + } + } + } +} + +pub async fn install_app( + choice: Connection, + core_index: Option<&usize>, + tab_file: Tab, +) -> Result<(), TockloaderError> { + match choice { + Connection::ProbeRS(mut session) => { + // Get core - if not specified, by default is 0 + let mut core = session + .core(*core_index.unwrap()) + .map_err(|e| TockloaderError::CoreAccessError(*core_index.unwrap(), e))?; + // Get board data + let system_attributes = SystemAttributes::read_system_attributes_probe(&mut core)?; + + let board = system_attributes + .board + .ok_or(TockloaderError::MisconfiguredBoard( + "No board name found.".to_owned(), + ))?; + let kernel_version = + system_attributes + .kernel_version + .ok_or(TockloaderError::MisconfiguredBoard( + "No kernel version found.".to_owned(), + ))?; + + // Verify if the specified app is compatible with board + // TODO(Micu Ana): Replace the print with log messages + if tab_file.is_compatible_with_board(&board) { + println!("Specified tab is compatible with board."); + } else { + panic!("Specified tab is not compatible with board."); + } + + // Verify if the specified app is compatible with kernel version + // TODO(Micu Ana): Replace the prints with log messages + if tab_file.is_compatible_with_kernel_verison(kernel_version as u32) { + println!("Specified tab is compatible with your kernel version."); + } else { + println!("Specified tab is not compatible with your kernel version."); + } + + // Get the address from which we start writing the new app + // TODO: change appaddr to 32 bit + // TODO for the future: support 64 bit arhitecture + let mut address = + system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + + // Loop to check if there are another apps installed + loop { + // Read a block of 200 8-bit words + let mut buff = vec![0u8; 200]; + core.read(address, &mut buff) + .map_err(TockloaderError::ProbeRsReadError)?; + + let (_ver, _header_len, whole_len) = match parse_tbf_header_lengths( + &buff[0..8] + .try_into() + .expect("Buffer length must be at least 8 bytes long."), + ) { + Ok((ver, header_len, whole_len)) if header_len != 0 => { + (ver, header_len, whole_len) + } + _ => break, // No more apps + }; + address += whole_len as u64; + } + + let arch = system_attributes + .arch + .ok_or(TockloaderError::MisconfiguredBoard( + "No architecture found.".to_owned(), + ))?; + + let mut binary = tab_file.extract_binary(&arch.clone())?; // use the system_attributes arch or the provided one? + + let size = binary.len() as u64; + + // Make sure the app is aligned to a multiple of its size + let multiple = address / size; + + let (new_address, _gap_size) = if multiple * size != address { + let new_address = ((address + size) / size) * size; + let gap_size = new_address - address; + (new_address, gap_size) + } else { + (address, 0) + }; + + // No more need of core + drop(core); + + // Make sure the binary is a multiple of the page size by padding 0xFFs + // TODO(Micu Ana): check if the page-size differs + let page_size = 512; + let needs_padding = binary.len() % page_size != 0; + + if needs_padding { + let remaining = page_size - (binary.len() % page_size); + dbg!(remaining); + for _i in 0..remaining { + binary.push(0xFF); + } + } + + // Get indices of pages that have valid data to write + let mut valid_pages: Vec = Vec::new(); + for i in 0..(size as usize / page_size) { + for b in binary[(i * page_size)..((i + 1) * page_size)] + .iter() + .copied() + { + if b != 0 { + valid_pages.push(i.try_into().unwrap()); + break; + } + } + } + + // If there are no pages valid, all pages would have been removed, so we write them all + if valid_pages.is_empty() { + for i in 0..(size as usize / page_size) { + valid_pages.push(i.try_into().unwrap()); + } + } + + // Include a blank page (if exists) after the end of a valid page. There might be a usable 0 on the next page + let mut ending_pages: Vec = Vec::new(); + for &i in &valid_pages { + let mut iter = valid_pages.iter(); + if !iter.any(|&x| x == (i + 1)) && (i + 1) < (size as usize / page_size) as u8 { + ending_pages.push(i + 1); + } + } + + for i in ending_pages { + valid_pages.push(i); + } + + for i in valid_pages { + println!("Writing page number {}", i); + // Create the packet that we send to the bootloader + // First four bytes are the address of the page + let mut pkt = Vec::new(); + + // Then the bytes that go into the page + for b in binary[(i as usize * page_size)..((i + 1) as usize * page_size)] + .iter() + .copied() + { + pkt.push(b); + } + let mut loader = session.target().flash_loader(); + + loader + .add_data( + (new_address as u32 + (i as usize * page_size) as u32).into(), + &pkt, + ) + .map_err(TockloaderError::ProbeRsWriteError)?; + + let mut options = DownloadOptions::default(); + options.keep_unwritten_bytes = true; + + // Finally, the data can be programmed + loader + .commit(&mut session, options) + .map_err(TockloaderError::ProbeRsWriteError)?; + } + + Ok(()) + } + Connection::Serial(mut port) => { + let response = ping_bootloader_and_wait_for_response(&mut port).await?; + + if response as u8 != Response::Pong as u8 { + tokio::time::sleep(Duration::from_millis(100)).await; + let _ = ping_bootloader_and_wait_for_response(&mut port).await?; + } + + let system_attributes = + SystemAttributes::read_system_attributes_serial(&mut port).await?; + + let board = system_attributes + .board + .ok_or("No board name found.".to_owned()); + let kernel_version = system_attributes + .kernel_version + .ok_or("No kernel version found.".to_owned()); + + match board { + Ok(board) => { + // Verify if the specified app is compatible with board + // TODO(Micu Ana): Replace the print with log messages + if tab_file.is_compatible_with_board(&board) { + println!("Specified tab is compatible with board."); + } else { + panic!("Specified tab is not compatible with board."); + } + } + Err(e) => { + return Err(TockloaderError::MisconfiguredBoard(e)); + } + } + + match kernel_version { + Ok(kernel_version) => { + // Verify if the specified app is compatible with kernel version + // TODO(Micu Ana): Replace the prints with log messages + if tab_file.is_compatible_with_kernel_verison(kernel_version as u32) { + println!("Specified tab is compatible with your kernel version."); + } else { + println!("Specified tab is not compatible with your kernel version."); + } + } + Err(e) => { + return Err(TockloaderError::MisconfiguredBoard(e)); + } + } + + let mut address = + system_attributes + .appaddr + .ok_or(TockloaderError::MisconfiguredBoard( + "No start address found.".to_owned(), + ))?; + loop { + // Read a block of 200 8-bit words + let mut pkt = (address as u32).to_le_bytes().to_vec(); + let length = (200_u16).to_le_bytes().to_vec(); + for i in length { + pkt.push(i); + } + + let (_, message) = issue_command( + &mut port, + Command::ReadRange, + pkt, + true, + 200, + Response::ReadRange, + ) + .await?; + + let (_ver, _header_len, whole_len) = match parse_tbf_header_lengths( + &message[0..8] + .try_into() + .expect("Buffer length must be at least 8 bytes long."), + ) { + Ok((ver, header_len, whole_len)) if header_len != 0 => { + (ver, header_len, whole_len) + } + _ => break, // No more apps + }; + + address += whole_len as u64; + } + + let arch = system_attributes + .arch + .ok_or("No architecture found.".to_owned()); + + match arch { + Ok(arch) => { + let binary = tab_file.extract_binary(&arch.clone()); + + match binary { + Ok(mut binary) => { + let size = binary.len() as u64; + + let multiple = address / size; + + let (mut new_address, _gap_size) = if multiple * size != address { + let new_address = ((address + size) / size) * size; + let gap_size = new_address - address; + (new_address, gap_size) + } else { + (address, 0) + }; + + // Make sure the binary is a multiple of the page size by padding 0xFFs + // TODO(Micu Ana): check if the page-size differs + let page_size = 512; + let needs_padding = binary.len() % page_size != 0; + + if needs_padding { + let remaining = page_size - (binary.len() % page_size); + for _i in 0..remaining { + binary.push(0xFF); + } + } + + let binary_len = binary.len(); + + // Get indices of pages that have valid data to write + let mut valid_pages: Vec = Vec::new(); + for i in 0..(binary_len / page_size) { + for b in binary[(i * page_size)..((i + 1) * page_size)] + .iter() + .copied() + { + if b != 0 { + valid_pages.push(i as u8); + break; + } + } + } + + // If there are no pages valid, all pages would have been removed, so we write them all + if valid_pages.is_empty() { + for i in 0..(binary_len / page_size) { + valid_pages.push(i as u8); + } + } + + // Include a blank page (if exists) after the end of a valid page. There might be a usable 0 on the next page + let mut ending_pages: Vec = Vec::new(); + for &i in &valid_pages { + let mut iter = valid_pages.iter(); + if !iter.any(|&x| x == (i + 1)) + && (i + 1) < (binary_len / page_size) as u8 + { + ending_pages.push(i + 1); + } + } + + for i in ending_pages { + valid_pages.push(i); + } + + for i in valid_pages { + // Create the packet that we send to the bootloader + // First four bytes are the address of the page + let mut pkt = (new_address as u32 + + (i as usize * page_size) as u32) + .to_le_bytes() + .to_vec(); + // Then the bytes that go into the page + for b in binary + [(i as usize * page_size)..((i + 1) as usize * page_size)] + .iter() + .copied() + { + pkt.push(b); + } + + // Write to bootloader + let (_, _) = issue_command( + &mut port, + Command::WritePage, + pkt, + true, + 0, + Response::OK, + ) + .await?; + } + + new_address += binary.len() as u64; + + let pkt = (new_address as u32).to_le_bytes().to_vec(); + + let _ = issue_command( + &mut port, + Command::ErasePage, + pkt, + true, + 0, + Response::OK, + ) + .await?; + } + Err(e) => { + return Err(e); + } + } + Ok(()) + } + Err(e) => Err(TockloaderError::MisconfiguredBoard(e)), + } + } + } +} diff --git a/tockloader-lib/src/tabs/metadata.rs b/tockloader-lib/src/tabs/metadata.rs new file mode 100644 index 0000000..1398ea2 --- /dev/null +++ b/tockloader-lib/src/tabs/metadata.rs @@ -0,0 +1,69 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use crate::errors::TockloaderError; +use serde::Deserialize; + +#[allow(dead_code)] +#[derive(Deserialize)] +pub(super) struct Metadata { + #[serde(rename = "tab-version")] + pub tab_version: i64, + pub name: String, + #[serde(rename = "minimum-tock-kernel-version")] + pub minimum_tock_kernel_version: TockKernelVersion, + #[serde(rename = "build-date")] + pub build_date: toml::value::Datetime, + #[serde( + default, + deserialize_with = "deserialize_boards", + rename = "only-for-boards" + )] + pub only_for_boards: Option>, +} + +impl Metadata { + pub fn new(metadata: String) -> Result { + toml::from_str(&metadata).map_err(TockloaderError::InvalidMetadata) + } +} + +fn deserialize_boards<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + Ok(opt.map(|s| s.split(',').map(|s| s.trim().to_string()).collect())) +} + +#[allow(dead_code)] +#[derive(Debug)] +pub(super) struct TockKernelVersion { + pub major: u32, + pub minor: u32, +} + +impl<'de> Deserialize<'de> for TockKernelVersion { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 2 { + return Err(serde::de::Error::custom( + "Invalid version string. It needs to contain exactly one dot.", + )); + } + + let major = parts[0] + .parse::() + .map_err(|_| serde::de::Error::custom("Invalid Major Version"))?; + let minor = parts[1] + .parse::() + .map_err(|_| serde::de::Error::custom("Invalid Minor Version"))?; + + Ok(TockKernelVersion { major, minor }) + } +} diff --git a/tockloader-lib/src/tabs/mod.rs b/tockloader-lib/src/tabs/mod.rs new file mode 100644 index 0000000..14023da --- /dev/null +++ b/tockloader-lib/src/tabs/mod.rs @@ -0,0 +1,6 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +mod metadata; +pub mod tab; diff --git a/tockloader-lib/src/tabs/tab.rs b/tockloader-lib/src/tabs/tab.rs new file mode 100644 index 0000000..85f955a --- /dev/null +++ b/tockloader-lib/src/tabs/tab.rs @@ -0,0 +1,83 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright OXIDOS AUTOMOTIVE 2024. + +use crate::errors::TockloaderError; +use crate::tabs::metadata::Metadata; +use std::{fs::File, io::Read}; +use tar::Archive; + +struct TbfFile { + pub filename: String, + pub data: Vec, +} + +pub struct Tab { + metadata: Metadata, + tbf_files: Vec, +} + +impl Tab { + pub fn open(path: String) -> Result { + let mut metadata = None; + let mut tbf_files = Vec::new(); + let file = File::open(path).map_err(TockloaderError::UnusableTab)?; + let mut archive = Archive::new(file); + for file in archive.entries().map_err(TockloaderError::UnusableTab)? { + let mut file = file.map_err(TockloaderError::UnusableTab)?; + let path = file.path().map_err(TockloaderError::UnusableTab)?; + let file_name = match path.file_name().and_then(|name| name.to_str()) { + Some(name) => name.to_owned(), + None => continue, + }; + if file_name == "metadata.toml" { + let mut buf = String::new(); + file.read_to_string(&mut buf) + .map_err(TockloaderError::UnusableTab)?; + metadata = Some(Metadata::new(buf)?); + } else if file_name.ends_with(".tbf") { + let mut data = Vec::new(); + + file.read_to_end(&mut data) + .map_err(TockloaderError::UnusableTab)?; + tbf_files.push(TbfFile { + filename: file_name.to_string(), + data, + }); + } + } + + match metadata { + Some(metadata) => Ok(Tab { + metadata, + tbf_files, + }), + None => Err(TockloaderError::NoMetadata), + } + } + + pub fn is_compatible_with_kernel_verison(&self, _kernel_version: u32) -> bool { + // Kernel version seems to not be working properly on the microbit bootloader. It is always + // "1" despite the actual version. + // return self.metadata.minimum_tock_kernel_version.major <= kernel_version; + true + } + + pub fn is_compatible_with_board(&self, board: &String) -> bool { + if let Some(boards) = &self.metadata.only_for_boards { + boards.contains(board) + } else { + true + } + } + + pub fn extract_binary(&self, arch: &str) -> Result, TockloaderError> { + for file in &self.tbf_files { + if file.filename.starts_with(arch) { + return Ok(file.data.clone()); + } + } + + Err(TockloaderError::NoBinaryError(arch.to_owned())) + } +} diff --git a/tools/run_clippy.sh b/tools/run_clippy.sh index ca138b2..6ca4b7a 100755 --- a/tools/run_clippy.sh +++ b/tools/run_clippy.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash -# Script (heavily) inspired by 'tock' -# Reference: https://github.com/tock/tock/blob/master/tools/run_clippy.sh # Check to see if we can execute `cargo clippy`. # We don't want to force an installation onto the user, so for we diff --git a/tools/run_fmt_check.sh b/tools/run_fmt_check.sh index 355220d..577bec6 100755 --- a/tools/run_fmt_check.sh +++ b/tools/run_fmt_check.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash -# Script (heavily) inspired by 'tock' -# Reference: https://github.com/tock/tock/blob/master/tools/run_cargo_fmt.sh let FAIL=0