From c606fe7d0ce730f577427233ed9625abace0f348 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 15 Aug 2023 21:10:12 +0200 Subject: [PATCH 01/68] satrs-book init --- satrs-book/.gitignore | 1 + satrs-book/book.toml | 6 ++++++ satrs-book/src/SUMMARY.md | 3 +++ satrs-book/src/chapter_1.md | 1 + 4 files changed, 11 insertions(+) create mode 100644 satrs-book/.gitignore create mode 100644 satrs-book/book.toml create mode 100644 satrs-book/src/SUMMARY.md create mode 100644 satrs-book/src/chapter_1.md diff --git a/satrs-book/.gitignore b/satrs-book/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/satrs-book/.gitignore @@ -0,0 +1 @@ +book diff --git a/satrs-book/book.toml b/satrs-book/book.toml new file mode 100644 index 0000000..95212c7 --- /dev/null +++ b/satrs-book/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Robin Mueller"] +language = "en" +multilingual = false +src = "src" +title = "satrs-book" diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md new file mode 100644 index 0000000..7390c82 --- /dev/null +++ b/satrs-book/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/satrs-book/src/chapter_1.md b/satrs-book/src/chapter_1.md new file mode 100644 index 0000000..b743fda --- /dev/null +++ b/satrs-book/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 From bcf22f42d471993589600c4053261b1509134a5a Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 16:40:32 +0200 Subject: [PATCH 02/68] front page --- satrs-book/src/satrs-book.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 satrs-book/src/satrs-book.md diff --git a/satrs-book/src/satrs-book.md b/satrs-book/src/satrs-book.md new file mode 100644 index 0000000..571c1ea --- /dev/null +++ b/satrs-book/src/satrs-book.md @@ -0,0 +1,10 @@ +The sat-rs book +====== + +This book is the primary information resource for the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/sat-rs) +in addition to the regular API documentation. It contains the following resources: + +1. Architecture informations and consideration which would exceeds the scope of the regular API. +2. A Getting-Started workshop where a small On-Board Software is built from scratch using + sat-rs components. + From f271ae5689715c13e934870a68bb8c01ee74dd38 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 17:03:39 +0200 Subject: [PATCH 03/68] how does this work --- satrs-book/src/chapter_1.md | 1 - satrs-book/src/{satrs-book.md => title-page.md} | 0 2 files changed, 1 deletion(-) delete mode 100644 satrs-book/src/chapter_1.md rename satrs-book/src/{satrs-book.md => title-page.md} (100%) diff --git a/satrs-book/src/chapter_1.md b/satrs-book/src/chapter_1.md deleted file mode 100644 index b743fda..0000000 --- a/satrs-book/src/chapter_1.md +++ /dev/null @@ -1 +0,0 @@ -# Chapter 1 diff --git a/satrs-book/src/satrs-book.md b/satrs-book/src/title-page.md similarity index 100% rename from satrs-book/src/satrs-book.md rename to satrs-book/src/title-page.md From 37261e512c8e7e09be0fe2eb43df5248a0304e72 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 17:12:45 +0200 Subject: [PATCH 04/68] book description --- satrs-book/book.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-book/book.toml b/satrs-book/book.toml index 95212c7..8d01bb6 100644 --- a/satrs-book/book.toml +++ b/satrs-book/book.toml @@ -3,4 +3,4 @@ authors = ["Robin Mueller"] language = "en" multilingual = false src = "src" -title = "satrs-book" +title = "The sat-rs book" From 73a4955fb38a74c8c1838d1796f01d386be53e3f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 17:30:59 +0200 Subject: [PATCH 05/68] continue book --- satrs-book/src/SUMMARY.md | 4 +++- satrs-book/src/introduction.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 satrs-book/src/introduction.md diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index 7390c82..b082e13 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -1,3 +1,5 @@ # Summary -- [Chapter 1](./chapter_1.md) +- [The sat-rs book](./title-page.md) +- [Introduction](./introduction.md) + diff --git a/satrs-book/src/introduction.md b/satrs-book/src/introduction.md new file mode 100644 index 0000000..0573ce2 --- /dev/null +++ b/satrs-book/src/introduction.md @@ -0,0 +1,11 @@ +# Introduction + +This is the repository of the sat-rs framework. Its primary goal is to provide re-usable components +to write on-board software for remote systems like rovers or satellites. It is specifically written +for the special requirements for these systems. + +A lot of the architecture and general design considerations are based on the +[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage +through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/) +and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/). + From f8a92cef3d1209768b971f600d26748ee3ce2b7e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 18:20:27 +0200 Subject: [PATCH 06/68] this is a good start --- satrs-book/src/SUMMARY.md | 3 +- satrs-book/src/design.md | 57 ++++++++++++++++++++++++++++++++++ satrs-book/src/introduction.md | 12 ++++++- satrs-book/src/title-page.md | 10 ------ 4 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 satrs-book/src/design.md delete mode 100644 satrs-book/src/title-page.md diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index b082e13..31c56f6 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -1,5 +1,6 @@ # Summary -- [The sat-rs book](./title-page.md) - [Introduction](./introduction.md) +- [Design](./design.md) + diff --git a/satrs-book/src/design.md b/satrs-book/src/design.md new file mode 100644 index 0000000..cd30845 --- /dev/null +++ b/satrs-book/src/design.md @@ -0,0 +1,57 @@ +# Framework Design + +Satellites and space systems in general are complex systems with a wide range of requirements for +both the hardware and the software. +Consequently, the general design of the framework is centered around many light-weight components +which try to impose as few restrictions as possible on how to solve certain problems. + +There are still a lot of common patterns and architectures across these systems where guidance +of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls +which were already solved and to avoid boilerplate code. This framework tries to provide this +structure and guidance the following way: + +1. Providing this book which explains the architecture and design patterns in respect to common + issues and requirements of space systems. +2. Providing an example application. Space systems still commonly have large monolithic + primary On-Board Softwares, so the choice was made to provide one example software which + contains the various features provided by sat-rs. +3. Providing a good test suite. This includes both unittests and integration tests. The integration + tests can also serve as smaller usage examples than the large `satrs-example` application. + +This framework has special support for standards used in the space industry. This especially +includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European +Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards, +but it is always recommended to use some sort of standard for interoperability. + +A lot of the modules and design considerations are based on the Flight Software Framework (FSFW). +The FSFW has its own [documentation](https://documentation.irs.uni-stuttgart.de/fsfw/), which +will be referred to when applicable. The FSFW was developed over a period of 10 years for the +Flying Laptop Project by the University of Stuttgart with Airbus Defence and Space GmbH. +It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/) +and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/). +Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs` +framework. +FLP is a medium-size small satellite with a higher budget and longer development time than EIVE, +which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a +shorter development cycle and was built using cheaper COTS components. This framework also tries +to accumulate the knowledge of developing the OBSW and operating the satellite for both these +different systems and provide a solution for a wider range of small satellite systems. + +`sat-rs` can be seen as a modern port of the FSFW which uses common principles of software +engineering to provide a reliable and robust basis for space On-Board Software. The choice +of using the Rust programming language was made for the following reasons: + +1. Rust has safety guarantees which are a perfect fit for space systems which generally have high + robustness and reliablity guarantees. +2. Rust is suitable for embedded systems. It can also be run on smaller embedded systems like the + STM32 which have also become common in the space sector. All space systems are embedded systems, + which makes using large languages like Python challenging even for OBCs with more performance. +3. Rust has support for linking C APIs through its excellent FFI support. This is especially + important because many vendor provided libaries are still C based. +4. Modern tooling like a package managers and various development helper, which can further reduce + development cycles for space systems. `cargo` provides tools like auto-formatters and linters + which can immediately ensure a high software quality throughout each development cycle. +5. A large ecosystem with excellent libraries which also leverages the excellent tooling provided + previously. Integrating these libraries is a lot easier compared to languages like C/C++ where + there is still no standardized way to use packages. + diff --git a/satrs-book/src/introduction.md b/satrs-book/src/introduction.md index 0573ce2..3cf731f 100644 --- a/satrs-book/src/introduction.md +++ b/satrs-book/src/introduction.md @@ -1,6 +1,16 @@ +The sat-rs book +====== + +This book is the primary information resource for the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/sat-rs) +in addition to the regular API documentation. It contains the following resources: + +1. Architecture informations and consideration which would exceeds the scope of the regular API. +2. A Getting-Started workshop where a small On-Board Software is built from scratch using + sat-rs components. + # Introduction -This is the repository of the sat-rs framework. Its primary goal is to provide re-usable components +The primary goal of the sat-rs framework is to provide re-usable components to write on-board software for remote systems like rovers or satellites. It is specifically written for the special requirements for these systems. diff --git a/satrs-book/src/title-page.md b/satrs-book/src/title-page.md deleted file mode 100644 index 571c1ea..0000000 --- a/satrs-book/src/title-page.md +++ /dev/null @@ -1,10 +0,0 @@ -The sat-rs book -====== - -This book is the primary information resource for the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/sat-rs) -in addition to the regular API documentation. It contains the following resources: - -1. Architecture informations and consideration which would exceeds the scope of the regular API. -2. A Getting-Started workshop where a small On-Board Software is built from scratch using - sat-rs components. - From dc6b7f64872e5208d97d61d494c73286e4a0dff3 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 18:54:36 +0200 Subject: [PATCH 07/68] add more chapters --- satrs-book/src/SUMMARY.md | 3 +- satrs-book/src/communication.md | 46 +++++++++++++++++++++++++++ satrs-book/src/constrained-systems.md | 0 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 satrs-book/src/communication.md create mode 100644 satrs-book/src/constrained-systems.md diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index 31c56f6..2f40d33 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -2,5 +2,6 @@ - [Introduction](./introduction.md) - [Design](./design.md) - +- [Communication with Space Systems](./communication.md) +- [Working with Constrained Systems](./constrained-systems.md) diff --git a/satrs-book/src/communication.md b/satrs-book/src/communication.md new file mode 100644 index 0000000..e989130 --- /dev/null +++ b/satrs-book/src/communication.md @@ -0,0 +1,46 @@ +# Communication with sat-rs based software + +Communication is a huge topic for satellites. These systems are usually not (directly) connected +to the internet and only have 1-2 communication links during nominal operation. However, most +satellites have internet access during development cycle. There are various standards provided by +CCSDS and ECSS which can be useful to determine how to communicate with the satellite and the +primary On-Board Software. + +# Application layer + +Current communication with satellite systems is usually packet based. For example, the CCSDS space +packet standard only specifies a 6 byte header with at least 1 byte payload. The PUS packet +standard is a subset of the space packet standard which adds some fields and a 16 bit CRC, but +it is still centered around small packets. `sat-rs` provides support for these ECSS and CCSDS +standards to also attempts to fill the gap to the internet protocol by providing the following +components. + +1. UDP TMTC Server. UDP is already packet based which makes it an excellent fit for exchanging + space packets. +2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol + to always deliver complete packets. + +# Working with telemetry and telecommands (TMTC) + +The commands sent to a space system are commonly called telecommands (TC) while the data received +from it are called telemetry (TM). Keeping in mind the previous section, the concept of a TC source +and a TM sink can be applied to most satellites. The TM sink is the one entity where all generated +telemetry arrives in real-time. The most important task of the TM sink usually is to send all +arriving telemetry to the ground segment of a satellite mission immediately. Another important +task might be to store all arriving telemetry persistently. This is especially important for +space systems which do not have permanent contact like low-earth-orbit (LEO) satellites. + +The most important task of a TC source is to deliver the telecommands to the correct recipients. +For modern component oriented software using message passing, this usually includes staged +demultiplexing components to determine where a command needs to be sent. + +# Low-level protocols and the bridge to the communcation subsystem + +Many satellite systems usually use the lower levels of the OSI layer in addition to the application +layer covered by the PUS standard or the CCSDS space packets standard. This oftentimes requires +special hardware like dedicated FPGAs to handle forward error correction fast enough. `sat-rs` +might provide components to handle standard like the Unified Space Data Link Standard (USLP) in +software but most of the time the handling of communication is performed through custom +software and hardware. Still, connecting this custom software and hardware can mostly be done +by using the concept of TC sources and TM sinks mentioned previously. + diff --git a/satrs-book/src/constrained-systems.md b/satrs-book/src/constrained-systems.md new file mode 100644 index 0000000..e69de29 From 3bffa8ed83280bdc8db634a344631b31170b7f55 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 19:15:38 +0200 Subject: [PATCH 08/68] added chapter for constrained systems --- satrs-book/src/constrained-systems.md | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/satrs-book/src/constrained-systems.md b/satrs-book/src/constrained-systems.md index e69de29..ba18e29 100644 --- a/satrs-book/src/constrained-systems.md +++ b/satrs-book/src/constrained-systems.md @@ -0,0 +1,43 @@ +# Working with Constrained Systems + +Software for space systems oftentimes has different requirements than the software for host +systems or servers. Currently, most space systems are considered embedded systems. + +For these systems, the computation power and the available heap are the most important resources +which are constrained. This might make completeley heap based memory management schemes which +are oftentimes used on host and server based systems unfeasable. Still, completely forbidding +heap allocations might make software development unnecessarilly difficult, especially in a +time where the OBSW might be running on Linux based systems with 500 MB RAM. + +A useful pattern used commonly in space systems is to limit heap allocations to program +initialization time and avoid frequent run-time allocations. This prevents issues like +running out of memory (something event Rust can not protect from) or heap fragmentation. + +# Using pre-allocated pool structures + +A huge candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all +candidates where the data size might vary greatly. The regular solution for host systems +might be to send around this data as a `Vec` until it is dropped. `sat-rs` provides +another solution to avoid run-time allocations by pre-allocated static pools. + +These pools are split into subpools where each subpool can have different page sizes. +For example, a very small TC pool might look like this: + +TODO: Add image + +A TC entry inside this pool has a store address which can then be sent around without having +to dynamically allocate memory. The same principle can also be applied to the TM and IPC data. + +# Using special crates to prevent smaller allocations + +Another common way to use the heap on host systems is using containers like `String` and `Vec` +to work with data where the size is not known beforehand. The most common solution for embedded +systems is to determine the maximum expected size and then use a pre-allocated `u8` buffer and a +size variable. Alternatively, you can use the following crates for more convenience or a smart +behaviour which at least reduced heap allocations: + +1. [`smallvec`](https://docs.rs/smallvec/latest/smallvec/). +2. [`arrayvec`](https://docs.rs/arrayvec/latest/arrayvec/index.html) which also contains an + [`ArrayString`](https://docs.rs/arrayvec/latest/arrayvec/struct.ArrayString.html) helper type. +3. [`tinyvec`](https://docs.rs/tinyvec/latest/tinyvec/). + From c153276454398fbe7c98ff56d1b0e28ba2d0b711 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 30 Aug 2023 19:23:48 +0200 Subject: [PATCH 09/68] add section for threads --- satrs-book/src/constrained-systems.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/satrs-book/src/constrained-systems.md b/satrs-book/src/constrained-systems.md index ba18e29..a10619d 100644 --- a/satrs-book/src/constrained-systems.md +++ b/satrs-book/src/constrained-systems.md @@ -41,3 +41,16 @@ behaviour which at least reduced heap allocations: [`ArrayString`](https://docs.rs/arrayvec/latest/arrayvec/struct.ArrayString.html) helper type. 3. [`tinyvec`](https://docs.rs/tinyvec/latest/tinyvec/). +# Using a fixed amount of threads + +On host systems, it is a common practice to dynamically spawn new threads to handle workloads. +On space systems this is generally considered an anti-pattern as this is considered undeterministic +and might lead to similar issues like when dynamically using the heap. For example, spawning a new +thread might use up the remaining heap of a system, leading to undeterministic errors. + +The most common way to avoid this is to simply spawn all required threads at program initialization +time. If a thread is done with its task, it can go back to sleeping regularly, only occasionally +checking for new jobs. If a system still needs to handle burst concurrent loads, another possible +way commonly used for host systems as well would be to use a threadpool, for example by using the +[`threadpool`](https://crates.io/crates/threadpool) crate. + From df90c22fef539c658ba2d6e114a08520434d9dd3 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:09:49 +0200 Subject: [PATCH 10/68] smaller tweaks --- satrs-book/src/communication.md | 12 ++++++------ satrs-book/src/design.md | 6 +++--- satrs-book/src/introduction.md | 2 ++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/satrs-book/src/communication.md b/satrs-book/src/communication.md index e989130..bcda520 100644 --- a/satrs-book/src/communication.md +++ b/satrs-book/src/communication.md @@ -1,16 +1,16 @@ # Communication with sat-rs based software -Communication is a huge topic for satellites. These systems are usually not (directly) connected +Communication is a huge topic for space systems. They are usually not (directly) connected to the internet and only have 1-2 communication links during nominal operation. However, most -satellites have internet access during development cycle. There are various standards provided by -CCSDS and ECSS which can be useful to determine how to communicate with the satellite and the -primary On-Board Software. +of these systems have internet access during development cycle. There are various standards +provided by CCSDS and ECSS which can be useful to determine how to communicate with the satellite +and the primary On-Board Software. # Application layer -Current communication with satellite systems is usually packet based. For example, the CCSDS space +Most communication with space systems is usually packet based. For example, the CCSDS space packet standard only specifies a 6 byte header with at least 1 byte payload. The PUS packet -standard is a subset of the space packet standard which adds some fields and a 16 bit CRC, but +standard is a subset of the space packet standard, which adds some fields and a 16 bit CRC, but it is still centered around small packets. `sat-rs` provides support for these ECSS and CCSDS standards to also attempts to fill the gap to the internet protocol by providing the following components. diff --git a/satrs-book/src/design.md b/satrs-book/src/design.md index cd30845..9ec7317 100644 --- a/satrs-book/src/design.md +++ b/satrs-book/src/design.md @@ -1,9 +1,9 @@ # Framework Design Satellites and space systems in general are complex systems with a wide range of requirements for -both the hardware and the software. -Consequently, the general design of the framework is centered around many light-weight components -which try to impose as few restrictions as possible on how to solve certain problems. +both the hardware and the software. Consequently, the general design of the framework is centered +around many light-weight components which try to impose as few restrictions as possible on how to +solve certain problems. There are still a lot of common patterns and architectures across these systems where guidance of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls diff --git a/satrs-book/src/introduction.md b/satrs-book/src/introduction.md index 3cf731f..31a0b0c 100644 --- a/satrs-book/src/introduction.md +++ b/satrs-book/src/introduction.md @@ -5,6 +5,8 @@ This book is the primary information resource for the [sat-rs framework](https:/ in addition to the regular API documentation. It contains the following resources: 1. Architecture informations and consideration which would exceeds the scope of the regular API. +2. General information on how to build On-Board Software and how `sat-rs` can help to fulfill + the unique requirements of writing software for remote systems. 2. A Getting-Started workshop where a small On-Board Software is built from scratch using sat-rs components. From 44905fb700302c3bdce107b9b49f5884030aca6f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:15:32 +0200 Subject: [PATCH 11/68] add a lot of chapter stubs --- satrs-book/src/SUMMARY.md | 9 +++++++++ satrs-book/src/actions.md | 0 satrs-book/src/fdir.md | 0 satrs-book/src/ground.md | 0 satrs-book/src/modelling-space-systems.md | 0 satrs-book/src/modes-and-health.md | 0 satrs-book/src/power.md | 0 satrs-book/src/thermal.md | 0 8 files changed, 9 insertions(+) create mode 100644 satrs-book/src/actions.md create mode 100644 satrs-book/src/fdir.md create mode 100644 satrs-book/src/ground.md create mode 100644 satrs-book/src/modelling-space-systems.md create mode 100644 satrs-book/src/modes-and-health.md create mode 100644 satrs-book/src/power.md create mode 100644 satrs-book/src/thermal.md diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index 2f40d33..243449b 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -4,4 +4,13 @@ - [Design](./design.md) - [Communication with Space Systems](./communication.md) - [Working with Constrained Systems](./constrained-systems.md) +- [Actions](./actions.md) +- [Modes and Health](./modes-and-health.md) +- [Housekeeping Data](./housekeeping.md) +- [Events](./events.md) +- [Power Components](./power.md) +- [Thermal Components](./thermal.md) +- [FDIR](./fdir.md) +- [Modelling space systems](./modelling-space-systems.md) +- [Ground Systems](./ground.md) diff --git a/satrs-book/src/actions.md b/satrs-book/src/actions.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/fdir.md b/satrs-book/src/fdir.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/ground.md b/satrs-book/src/ground.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/modelling-space-systems.md b/satrs-book/src/modelling-space-systems.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/modes-and-health.md b/satrs-book/src/modes-and-health.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/power.md b/satrs-book/src/power.md new file mode 100644 index 0000000..e69de29 diff --git a/satrs-book/src/thermal.md b/satrs-book/src/thermal.md new file mode 100644 index 0000000..e69de29 From 4e186541ec64db31ecf64374875bcbff74532c31 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:19:31 +0200 Subject: [PATCH 12/68] some more chapters --- satrs-book/src/SUMMARY.md | 3 +++ satrs-book/src/events.md | 1 + satrs-book/src/housekeeping.md | 1 + satrs-book/src/logging.md | 1 + satrs-book/src/persistent-tm-storage.md | 1 + satrs-book/src/serialization.md | 1 + 6 files changed, 8 insertions(+) create mode 100644 satrs-book/src/events.md create mode 100644 satrs-book/src/housekeeping.md create mode 100644 satrs-book/src/logging.md create mode 100644 satrs-book/src/persistent-tm-storage.md create mode 100644 satrs-book/src/serialization.md diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index 243449b..3fbc9f6 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -10,7 +10,10 @@ - [Events](./events.md) - [Power Components](./power.md) - [Thermal Components](./thermal.md) +- [Persistent TM storage](./persistent-tm-storage.md) - [FDIR](./fdir.md) +- [Serialization of Data](./serialization.md) +- [Logging](./logging.md) - [Modelling space systems](./modelling-space-systems.md) - [Ground Systems](./ground.md) diff --git a/satrs-book/src/events.md b/satrs-book/src/events.md new file mode 100644 index 0000000..1f187df --- /dev/null +++ b/satrs-book/src/events.md @@ -0,0 +1 @@ +# Events diff --git a/satrs-book/src/housekeeping.md b/satrs-book/src/housekeeping.md new file mode 100644 index 0000000..af23140 --- /dev/null +++ b/satrs-book/src/housekeeping.md @@ -0,0 +1 @@ +# Housekeeping Data diff --git a/satrs-book/src/logging.md b/satrs-book/src/logging.md new file mode 100644 index 0000000..b921bbe --- /dev/null +++ b/satrs-book/src/logging.md @@ -0,0 +1 @@ +# Logging diff --git a/satrs-book/src/persistent-tm-storage.md b/satrs-book/src/persistent-tm-storage.md new file mode 100644 index 0000000..08b20d9 --- /dev/null +++ b/satrs-book/src/persistent-tm-storage.md @@ -0,0 +1 @@ +# Persistent TM storage diff --git a/satrs-book/src/serialization.md b/satrs-book/src/serialization.md new file mode 100644 index 0000000..0dfc62d --- /dev/null +++ b/satrs-book/src/serialization.md @@ -0,0 +1 @@ +# Serialization From 696d9fe48dce727ed2a239feb8816bd64937b462 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:37:23 +0200 Subject: [PATCH 13/68] added actions chapter --- satrs-book/src/actions.md | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/satrs-book/src/actions.md b/satrs-book/src/actions.md index e69de29..1f3c921 100644 --- a/satrs-book/src/actions.md +++ b/satrs-book/src/actions.md @@ -0,0 +1,42 @@ +# Working with Actions + +Space systems generally need to be commanded regularly. This can include commands periodically +required to ensure a health system, or commands to reach the mission goals. + +These commands can be modelled using the concept of Actions. the ECSS PUS standard also provides +the PUS service 8 for actions, but provides few concrete subservices and specification on how +action commanding could look like. + +`sat-rs` proposes two recommended ways to perform action commanding: + +1. Target ID and Action ID based. The target ID is a 32-bit unsigned ID for an OBSW object entity + which can also accept Actions. The action ID is a 32-bit unsigned ID for each action that a + target is able to perform. +2. Target ID and Action String based. The target ID is the same as in the first proposal, but + the unique action is identified by a string. + +The framework provides an `ActionRequest` abstraction to model both of these cases. + +## Commanding with ECSS PUS 8 + +`sat-rs` provides a generic ECSS PUS 8 action command handler. This handler can convert PUS 8 +telecommands which use the commanding scheme 1 explained above to an `ActionRequest` which is +then forwarded to the target specified by the Target ID. + +There are 3 requirements for the PUS 8 telecommand: + +1. The subservice 128 must be used +2. Bytes 0 to 4 of application data must contain the target ID in `u32` big endian format. +3. Bytes 4 to 8 of application data must contain the action ID in `u32` big endian format. +4. The rest of the application data are assumed to be command specific additional parameters. They + will be added to an IPC store, the the corresponding store address will be sent as part of the + `ActionRequest`. + +## Sending back telemetry + +There are some cases where the regular verification provided by PUS in response to PUS action +commands is not sufficient and some additional telemetry needs to be sent to ground. In that +case, it is recommended to chose some custom subservice for action TM data and then send the +telemetry using the same scheme as shown above, where the first 8 bytes of the application +data is reserved for the target ID and action ID. + From 2322d3a9b347bcfe1255ddc5993e830deeb2bc43 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 20:18:26 +0200 Subject: [PATCH 14/68] added a basic modes and health chapter --- satrs-book/src/actions.md | 2 +- satrs-book/src/modes-and-health.md | 102 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/satrs-book/src/actions.md b/satrs-book/src/actions.md index 1f3c921..6ad9c39 100644 --- a/satrs-book/src/actions.md +++ b/satrs-book/src/actions.md @@ -29,7 +29,7 @@ There are 3 requirements for the PUS 8 telecommand: 2. Bytes 0 to 4 of application data must contain the target ID in `u32` big endian format. 3. Bytes 4 to 8 of application data must contain the action ID in `u32` big endian format. 4. The rest of the application data are assumed to be command specific additional parameters. They - will be added to an IPC store, the the corresponding store address will be sent as part of the + will be added to an IPC store and the corresponding store address will be sent as part of the `ActionRequest`. ## Sending back telemetry diff --git a/satrs-book/src/modes-and-health.md b/satrs-book/src/modes-and-health.md index e69de29..4cb6878 100644 --- a/satrs-book/src/modes-and-health.md +++ b/satrs-book/src/modes-and-health.md @@ -0,0 +1,102 @@ +# Modes + +Modes are an extremely useful concept for complex system in general. They also allow simplified +system reasoning for both system operators and OBSW developers. They model the behaviour of a +component and also provide observability of a system. A few examples of how to model +different components of a space system with modes will be given. + +## Modelling a pyhsical devices with modes + +The following simple mode scheme with the following three mode + +- `OFF` +- `ON` +- `NORMAL` + +can be applied to a large number of simpler devices of a remote system, for example sensors. + +1. `OFF` means that a device is physically switched off, and the corresponding software component +does not poll the device regularly. +2. `ON` means that a device is pyhsically switched on, but the device is not polled perically. +3. `NORMAL` means that a device is powered on and polled periodically. + +If a devices is `OFF`, the device handler will deny commands which include physical communication +with the connected devices. In `NORMAL` mode, it will autonomously perform periodic polling +of a connected physical device in addition to handling remote commands by the operator. +Using these three basic modes, there are two important transitions which need to be taken care of +for the majority of devices: + +1. `OFF` to `ON` or `NORMAL`: The device first needs to be powered on. After that, the + device initial startup configuration must be performed. +2. `NORMAL` or `ON` to `OFF`: Any important shutdown configuration or handling must be performed + before powering off the device. + +## Modelling a controller with modes + +Controller components are not modelling physical devices, but a mode scheme is still the best +way to model most of these components. + +For example, a hypothetical attitude controller might have the following modes: + +- `SAFE` +- `TARGET IDLE` +- `TARGET POINTING GROUND` +- `TARGET POINTING NADIR` + +We can also introduce the concept of submodes: The `SAFE` mode can for example have a +`DEFAULT` submode and a `DETUMBLE` submode. + +## Achieving system observability with modes + +If a system component has a mode in some shape or form, this mode should be observable. This means +that the operator can also retrieve the mode for a particular component. This is especially +important if these components can change their mode autonomously. + +If a component is able to change its mode autonomously, this is also something which is relevant +information for the operator or for other software components. This means that a component +should also be able to announce its mode. + +This concept becomes especially important when applying the mode concept on the whole +system level. This will also be explained in detail in a dedicated chapter, but the basic idea +is to model the whole system as a tree where each node has a mode. A new capability is added now: +A component can announce its mode recursively. This means that the component will announce its +own mode first before announcing the mode of all its children. Using a scheme like this, the mode +of the whole system can be retrieved using only one command. The same concept can also be used +for commanding the whole system, which will be explained in more detail in the dedicated systems +modelling chapter. + +In summary, a component which has modes has to expose the following 4 capabilities: + +1. Set a mode +2. Read the mode +3. Announce the mode +4. Announce the mode recursively + +## Using ECSS PUS to perform mode commanding + +# Health + +Health is an important concept for systems and components which might fail. +Oftentimes, the health is tied to the mode of a system component in some shape or form, and +determines whether a system component is usable. Health is also an extremely useful concept +to simplify the Fault Detection, Isolation and Recovery (FDIR) concept of a system. + +The following health states are based on the ones used inside the FSFW and are enough to model most +use-cases: + +- `HEALTHY` +- `FAULTY` +- `NEEDS RECOVERY` +- `EXTERNAL CONTROL` + +1. `HEALTHY` means that a component is working nominally, and can perform its task without any issues. +2. `FAULTY` means that a component does not work properly. This might also impact other system +components, so the passivation and isolation of that component is desirable for FDIR purposes. +3. `NEEDS RECOVERY` is used to attempt a recovery of a component. For example, a simple sensor +could be power-cycled if there were multiple communication issues in the last time. +4. `EXTERNAL CONTROL` is used to isolate an individual component from the rest of the system. For + example, on operator might be interested in testing a component in isolation, and the interference + of the system is not desired. In that case, the `EXTERNAL CONTROL` health state might be used + to prevent mode commands from the system while allowing external mode commands. + + From 83c5784b9d88c5de89590359cf8c528292515211 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 13 Sep 2023 15:11:44 +0200 Subject: [PATCH 15/68] improve the book, add first pictures --- satrs-book/src/SUMMARY.md | 2 +- satrs-book/src/actions.md | 2 +- satrs-book/src/communication.md | 12 +- satrs-book/src/constrained-systems.md | 11 +- satrs-book/src/events.md | 15 ++ satrs-book/src/fdir.md | 1 + satrs-book/src/ground-segments.md | 1 + satrs-book/src/ground.md | 0 satrs-book/src/housekeeping.md | 23 ++ satrs-book/src/images/event_man_arch.graphml | 259 +++++++++++++++++++ satrs-book/src/images/event_man_arch.png | Bin 0 -> 71832 bytes satrs-book/src/modelling-space-systems.md | 1 + satrs-book/src/persistent-tm-storage.md | 2 +- satrs-book/src/power.md | 1 + satrs-book/src/thermal.md | 1 + 15 files changed, 317 insertions(+), 14 deletions(-) create mode 100644 satrs-book/src/ground-segments.md delete mode 100644 satrs-book/src/ground.md create mode 100644 satrs-book/src/images/event_man_arch.graphml create mode 100644 satrs-book/src/images/event_man_arch.png diff --git a/satrs-book/src/SUMMARY.md b/satrs-book/src/SUMMARY.md index 3fbc9f6..995e8a9 100644 --- a/satrs-book/src/SUMMARY.md +++ b/satrs-book/src/SUMMARY.md @@ -15,5 +15,5 @@ - [Serialization of Data](./serialization.md) - [Logging](./logging.md) - [Modelling space systems](./modelling-space-systems.md) -- [Ground Systems](./ground.md) +- [Ground Segments](./ground-segments.md) diff --git a/satrs-book/src/actions.md b/satrs-book/src/actions.md index 6ad9c39..0e092d9 100644 --- a/satrs-book/src/actions.md +++ b/satrs-book/src/actions.md @@ -1,7 +1,7 @@ # Working with Actions Space systems generally need to be commanded regularly. This can include commands periodically -required to ensure a health system, or commands to reach the mission goals. +required to ensure a healthy system, or commands to reach the mission goals. These commands can be modelled using the concept of Actions. the ECSS PUS standard also provides the PUS service 8 for actions, but provides few concrete subservices and specification on how diff --git a/satrs-book/src/communication.md b/satrs-book/src/communication.md index bcda520..1bb66a5 100644 --- a/satrs-book/src/communication.md +++ b/satrs-book/src/communication.md @@ -1,8 +1,8 @@ # Communication with sat-rs based software -Communication is a huge topic for space systems. They are usually not (directly) connected -to the internet and only have 1-2 communication links during nominal operation. However, most -of these systems have internet access during development cycle. There are various standards +Communication is a huge topic for space systems. Remote systems are usually not (directly) +connected to the internet and only have 1-2 communication links during nominal operation. However, +most of these systems have internet access during development cycle. There are various standards provided by CCSDS and ECSS which can be useful to determine how to communicate with the satellite and the primary On-Board Software. @@ -12,7 +12,7 @@ Most communication with space systems is usually packet based. For example, the packet standard only specifies a 6 byte header with at least 1 byte payload. The PUS packet standard is a subset of the space packet standard, which adds some fields and a 16 bit CRC, but it is still centered around small packets. `sat-rs` provides support for these ECSS and CCSDS -standards to also attempts to fill the gap to the internet protocol by providing the following +standards and also attempts to fill the gap to the internet protocol by providing the following components. 1. UDP TMTC Server. UDP is already packet based which makes it an excellent fit for exchanging @@ -41,6 +41,6 @@ layer covered by the PUS standard or the CCSDS space packets standard. This ofte special hardware like dedicated FPGAs to handle forward error correction fast enough. `sat-rs` might provide components to handle standard like the Unified Space Data Link Standard (USLP) in software but most of the time the handling of communication is performed through custom -software and hardware. Still, connecting this custom software and hardware can mostly be done -by using the concept of TC sources and TM sinks mentioned previously. +software and hardware. Still, connecting this custom software and hardware to `sat-rs` can mostly +be done by using the concept of TC sources and TM sinks mentioned previously. diff --git a/satrs-book/src/constrained-systems.md b/satrs-book/src/constrained-systems.md index a10619d..4dbdb7a 100644 --- a/satrs-book/src/constrained-systems.md +++ b/satrs-book/src/constrained-systems.md @@ -7,18 +7,19 @@ For these systems, the computation power and the available heap are the most imp which are constrained. This might make completeley heap based memory management schemes which are oftentimes used on host and server based systems unfeasable. Still, completely forbidding heap allocations might make software development unnecessarilly difficult, especially in a -time where the OBSW might be running on Linux based systems with 500 MB RAM. +time where the OBSW might be running on Linux based systems with hundreds of MBs of RAM. A useful pattern used commonly in space systems is to limit heap allocations to program initialization time and avoid frequent run-time allocations. This prevents issues like -running out of memory (something event Rust can not protect from) or heap fragmentation. +running out of memory (something even Rust can not protect from) or heap fragmentation. # Using pre-allocated pool structures A huge candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all candidates where the data size might vary greatly. The regular solution for host systems might be to send around this data as a `Vec` until it is dropped. `sat-rs` provides -another solution to avoid run-time allocations by pre-allocated static pools. +another solution to avoid run-time allocations by offering and recommendng pre-allocated static +pools. These pools are split into subpools where each subpool can have different page sizes. For example, a very small TC pool might look like this: @@ -34,7 +35,7 @@ Another common way to use the heap on host systems is using containers like `Str to work with data where the size is not known beforehand. The most common solution for embedded systems is to determine the maximum expected size and then use a pre-allocated `u8` buffer and a size variable. Alternatively, you can use the following crates for more convenience or a smart -behaviour which at least reduced heap allocations: +behaviour which at the very least reduce heap allocations: 1. [`smallvec`](https://docs.rs/smallvec/latest/smallvec/). 2. [`arrayvec`](https://docs.rs/arrayvec/latest/arrayvec/index.html) which also contains an @@ -50,7 +51,7 @@ thread might use up the remaining heap of a system, leading to undeterministic e The most common way to avoid this is to simply spawn all required threads at program initialization time. If a thread is done with its task, it can go back to sleeping regularly, only occasionally -checking for new jobs. If a system still needs to handle burst concurrent loads, another possible +checking for new jobs. If a system still needs to handle bursty concurrent loads, another possible way commonly used for host systems as well would be to use a threadpool, for example by using the [`threadpool`](https://crates.io/crates/threadpool) crate. diff --git a/satrs-book/src/events.md b/satrs-book/src/events.md index 1f187df..083e76a 100644 --- a/satrs-book/src/events.md +++ b/satrs-book/src/events.md @@ -1 +1,16 @@ # Events + +Events can be an extremely important mechanism used for remote systems to monitor unexpected +or expected anomalies and events occuring on these systems. They are oftentimes tied to +Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously. + +Events can also be used as a convenient Inter-Process Communication (IPC) mechansism, which is +also observable for the Ground segment. The PUS Service 5 standardizes how the ground interface +for events might look like, but does not specify how other software components might react +to those events. There is the PUS Service 19, which might be used for that purpose, but the +event components recommended by this framework do not really need this service. + +The following images shows how the flow of events could look like in a system where components +can generate events, and where other system components might be interested in those events: + +![Event flow](images/event_man_arch.png) diff --git a/satrs-book/src/fdir.md b/satrs-book/src/fdir.md index e69de29..074b2ad 100644 --- a/satrs-book/src/fdir.md +++ b/satrs-book/src/fdir.md @@ -0,0 +1 @@ +# Fault Detecion, Isolation And Recovery (FDIR) diff --git a/satrs-book/src/ground-segments.md b/satrs-book/src/ground-segments.md new file mode 100644 index 0000000..58672b2 --- /dev/null +++ b/satrs-book/src/ground-segments.md @@ -0,0 +1 @@ +# Ground Segments diff --git a/satrs-book/src/ground.md b/satrs-book/src/ground.md deleted file mode 100644 index e69de29..0000000 diff --git a/satrs-book/src/housekeeping.md b/satrs-book/src/housekeeping.md index af23140..5a7d73b 100644 --- a/satrs-book/src/housekeeping.md +++ b/satrs-book/src/housekeeping.md @@ -1 +1,24 @@ # Housekeeping Data + +Remote systems like satellites and rovers oftentimes generate data autonomously and periodically. +The most common example for this is temperature or attitude data. Data like this is commonly +referred to as housekeeping data, and is usually one of the most important and most resource heavy +data sources received from a satellite. Standards like the PUS Service 3 make recommendation how to +expose housekeeping data, but the applicability of the interface offered by PUS 3 has proven to be +partially difficult and clunky for modular systems. + +First, we are going to list some assumption and requirements about Housekeeping (HK) data: + +1. HK data is generated periodically by various system components throughout the + systems. +2. An autonomous and periodic sampling of that HK data to be stored and sent to Ground is generally + required. A minimum interface consists of requesting a one-shot sample of HK, enabling and + disabling the periodic autonomous generation of samples and modifying the collection interval + of the periodic autonomous generation. +3. HK data often needs to be shared to other software components. For example, a thermal controller + wants to read the data samples of all sensor components. + +A commonly required way to model HK data in a clean way is also to group related HK data into sets, +which can then dumped via a similar interface. + +TODO: Write down `sat-rs` recommendations how to expose and work with HK data. diff --git a/satrs-book/src/images/event_man_arch.graphml b/satrs-book/src/images/event_man_arch.graphml new file mode 100644 index 0000000..1336793 --- /dev/null +++ b/satrs-book/src/images/event_man_arch.graphml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + Example Event Flow + + + + + + + + + + + Event Manager + + + + + + + + + + + Event +Creator 0 + + + + + + + + + + + Event +Creator 2 + + + + + + + + + + + Event +Creator 1 + + + + + + + + + + + Event +Creator 3 + + + + + + + + + + + PUS Service 5 +Event Reporting + + + + + + + + + + + + PUS Service 19 +Event Action + + + + + + + + + + + Telemetry +Sink + + + + + + + + + + + Subscriptions + +1. Event Creator 0 subscribes + for event 0 +2. Event Creator 1 subscribes + for event group 2 +3. PUS Service 5 handler + subscribes for all events +4. PUS Service 19 handler + subscribes for all events + + + + + + + + + + + event 1 +(group 1) + + + + + + + + + + + + + event 0 +(group 0) + + + + + + + + + + + event 2 +(group 3) + + + + + + + + + + + + + event 3 (group 2) +event 4 (group 2) + + + + + + + + + + + <<all events>> + + + + + + + + + + + <<all events>> + + + + + + + + + + + + + event 1 +event 2 + + + + + + + + + + + group 2 + + + + + + + + + + + enabled Events +as PUS 5 TM + + + + + + + + + diff --git a/satrs-book/src/images/event_man_arch.png b/satrs-book/src/images/event_man_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..61c8d7255dfe663247b6d21db6f368570fd5105c GIT binary patch literal 71832 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

57b?fjoQm3O83H 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 Y_NLHOi$+i4r^p Date: Wed, 13 Sep 2023 15:18:50 +0200 Subject: [PATCH 16/68] add first links to doc --- satrs-book/src/communication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satrs-book/src/communication.md b/satrs-book/src/communication.md index 1bb66a5..447ebfd 100644 --- a/satrs-book/src/communication.md +++ b/satrs-book/src/communication.md @@ -15,8 +15,8 @@ it is still centered around small packets. `sat-rs` provides support for these E standards and also attempts to fill the gap to the internet protocol by providing the following components. -1. UDP TMTC Server. UDP is already packet based which makes it an excellent fit for exchanging - space packets. +1. [UDP TMTC Server](https://docs.rs/satrs-core/0.1.0-alpha.0/satrs_core/hal/host/udp_server/index.html#). + UDP is already packet based which makes it an excellent fit for exchanging space packets. 2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol to always deliver complete packets. From e2bbcedf3e394175a5aa24f2af07f3413ce0a954 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 13 Sep 2023 15:21:28 +0200 Subject: [PATCH 17/68] link corrections --- satrs-core/src/hal/host/udp_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satrs-core/src/hal/host/udp_server.rs b/satrs-core/src/hal/host/udp_server.rs index de5a3f0..b91a239 100644 --- a/satrs-core/src/hal/host/udp_server.rs +++ b/satrs-core/src/hal/host/udp_server.rs @@ -51,9 +51,9 @@ use std::vec::Vec; /// .expect("Error sending PUS TC via UDP"); /// ``` /// -/// The [fsrc-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example) +/// The [satrs-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/-example) /// server code also includes -/// [example code](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example/src/bin/obsw/tmtc.rs) +/// [example code](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/tmtc.rs#L67) /// on how to use this TC server. It uses the server to receive PUS telecommands on a specific port /// and then forwards them to a generic CCSDS packet receiver. pub struct UdpTcServer { From 1bb4238e9fa8d393ca2643436b2fcaad2586adac Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 13 Sep 2023 15:26:20 +0200 Subject: [PATCH 18/68] README update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 548c038..585d3a9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This project currently contains following crates: on a host computer or on any system with a standard runtime like a Raspberry Pi. * [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-mib): Components to build a mission information base from the on-board software directly. -* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/satrs-example-stm32f3-disco): +* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example-stm32f3-disco): Example of a simple example on-board software using sat-rs components on a bare-metal system with constrained resources. From bbd6cec8ac825911ba0ea4afe369383c16ea5de8 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 14 Sep 2023 23:51:17 +0200 Subject: [PATCH 19/68] tcp server init --- satrs-core/src/hal/host/mod.rs | 1 + satrs-core/src/hal/host/tcp_server.rs | 9 +++++++++ satrs-core/src/hal/host/udp_server.rs | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 satrs-core/src/hal/host/tcp_server.rs diff --git a/satrs-core/src/hal/host/mod.rs b/satrs-core/src/hal/host/mod.rs index 8057db1..8b23ae9 100644 --- a/satrs-core/src/hal/host/mod.rs +++ b/satrs-core/src/hal/host/mod.rs @@ -1,2 +1,3 @@ //! Helper modules intended to be used on hosts with a full [std] runtime pub mod udp_server; +pub mod tcp_server; diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs new file mode 100644 index 0000000..fe92351 --- /dev/null +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -0,0 +1,9 @@ + +#[cfg(test)] +mod tests { + + #[test] + fn basic_test() { + + } +} diff --git a/satrs-core/src/hal/host/udp_server.rs b/satrs-core/src/hal/host/udp_server.rs index b91a239..de5a3f0 100644 --- a/satrs-core/src/hal/host/udp_server.rs +++ b/satrs-core/src/hal/host/udp_server.rs @@ -51,9 +51,9 @@ use std::vec::Vec; /// .expect("Error sending PUS TC via UDP"); /// ``` /// -/// The [satrs-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/-example) +/// The [fsrc-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example) /// server code also includes -/// [example code](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/tmtc.rs#L67) +/// [example code](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example/src/bin/obsw/tmtc.rs) /// on how to use this TC server. It uses the server to receive PUS telecommands on a specific port /// and then forwards them to a generic CCSDS packet receiver. pub struct UdpTcServer { From 3e9a07b73259b71dce78ad2c131772ddb9204faa Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 14 Sep 2023 23:52:14 +0200 Subject: [PATCH 20/68] link correction --- NOTICE | 2 ++ README.md | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index 717a583..c39ecb7 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1,3 @@ This software contains code developed at the University of Stuttgart's Institute of Space Systems. + +The sat-rs logo was designed by Nadine Eunous. diff --git a/README.md b/README.md index 75a6b0d..585d3a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

+ sat-rs ========= @@ -21,7 +23,7 @@ This project currently contains following crates: on a host computer or on any system with a standard runtime like a Raspberry Pi. * [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-mib): Components to build a mission information base from the on-board software directly. -* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/satrs-example-stm32f3-disco): +* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example-stm32f3-disco): Example of a simple example on-board software using sat-rs components on a bare-metal system with constrained resources. From 28801a895214bc561f7ddf8f616d446ca55e69ab Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 15 Sep 2023 15:37:57 +0200 Subject: [PATCH 21/68] this is actually quite tricky.. --- satrs-core/Cargo.toml | 5 + satrs-core/src/hal/host/mod.rs | 2 +- satrs-core/src/hal/host/tcp_server.rs | 289 +++++++++++++++++++++++++- 3 files changed, 294 insertions(+), 2 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 2d25971..7b6697f 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -68,6 +68,11 @@ version = "0.7.0-beta.1" # branch = "" default-features = false +[dependencies.cobs] +git = "https://github.com/robamu/cobs.rs.git" +branch = "all_features" +default-features = false + [dev-dependencies] serde = "1" zerocopy = "0.7" diff --git a/satrs-core/src/hal/host/mod.rs b/satrs-core/src/hal/host/mod.rs index 8b23ae9..051a2f7 100644 --- a/satrs-core/src/hal/host/mod.rs +++ b/satrs-core/src/hal/host/mod.rs @@ -1,3 +1,3 @@ //! Helper modules intended to be used on hosts with a full [std] runtime -pub mod udp_server; pub mod tcp_server; +pub mod udp_server; diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs index fe92351..1b3bd7c 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -1,9 +1,296 @@ +use alloc::boxed::Box; +use alloc::vec; +use cobs::decode_in_place; +use std::net::ToSocketAddrs; +use std::vec::Vec; +use std::{io::Read, net::TcpListener}; + +use crate::tmtc::ReceivesTc; + +pub struct TcpTcServer { + listener: TcpListener, + tc_receiver: Box>, + reader_vec: Vec, +} + +impl TcpTcServer { + pub fn new( + addr: A, + tc_receiver: Box>, + reader_vec_size: usize, + ) -> Result { + Ok(TcpTcServer { + listener: TcpListener::bind(addr)?, + tc_receiver, + reader_vec: vec![0; reader_vec_size], + }) + } + + pub fn handle_connections(&mut self) -> Result<(), std::io::Error> { + let mut current_write_idx; + let mut next_write_idx = 0; + for stream in self.listener.incoming() { + current_write_idx = next_write_idx; + next_write_idx = 0; + let mut stream = stream?; + loop { + let read_len = stream.read(&mut self.reader_vec[current_write_idx..])?; + if read_len > 0 { + current_write_idx += read_len; + if current_write_idx == self.reader_vec.capacity() { + // Reader vec full, need to parse for packets. + let parse_result = parse_buffer_for_cobs_encoded_packets( + &mut self.reader_vec, + self.tc_receiver.as_mut(), + &mut next_write_idx, + ); + } + continue; + } + break; + } + if current_write_idx > 0 { + let parse_result = parse_buffer_for_cobs_encoded_packets( + &mut self.reader_vec[..current_write_idx], + self.tc_receiver.as_mut(), + &mut next_write_idx, + ); + } + } + Ok(()) + } +} + +pub fn parse_buffer_for_cobs_encoded_packets( + buf: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let mut start_index_packet = 0; + let mut start_found = false; + let mut last_byte = false; + let mut packets_found = 0; + for i in 0..buf.len() { + if i == buf.len() - 1 { + last_byte = true; + } + if buf[i] == 0 { + if !start_found && !last_byte && buf[i + 1] == 0 { + // Special case: Consecutive sentinel values or all zeroes. + // Skip. + continue; + } + if start_found { + let decode_result = decode_in_place(&mut buf[start_index_packet..i]); + if let Ok(packet_len) = decode_result { + packets_found += 1; + tc_receiver + .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?; + } + start_found = false; + } else { + start_index_packet = i + 1; + start_found = true; + } + } + } + // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. + if start_index_packet > 0 && start_found && packets_found > 0 { + let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); + first_seg[..last_seg.len()].copy_from_slice(last_seg); + *next_write_idx = last_seg.len(); + } + Ok(packets_found) +} #[cfg(test)] mod tests { + use crate::tmtc::ReceivesTcCore; + use alloc::vec::Vec; + use cobs::encode; + + use super::parse_buffer_for_cobs_encoded_packets; + + const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + + #[derive(Default)] + struct TestSender { + received_tcs: Vec>, + } + + impl ReceivesTcCore for TestSender { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + self.received_tcs.push(tc_raw.to_vec()); + Ok(()) + } + } + + fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; + } #[test] - fn basic_test() { + fn test_parsing_simple_packet() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + let packet = &test_sender.received_tcs[0]; + assert_eq!(packet, &SIMPLE_PACKET); + } + #[test] + fn test_parsing_consecutive_packets() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + + let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; + // Second packet + encoded_buf[current_idx] = 0; + current_idx += 1; + current_idx += encode(&inverted_packet, &mut encoded_buf[current_idx..]); + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 2); + assert_eq!(test_sender.received_tcs.len(), 2); + let packet0 = &test_sender.received_tcs[0]; + assert_eq!(packet0, &SIMPLE_PACKET); + let packet1 = &test_sender.received_tcs[1]; + assert_eq!(packet1, &inverted_packet); + } + + #[test] + fn test_split_tail_packet_only() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - 1], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert_eq!(test_sender.received_tcs.len(), 0); + assert_eq!(next_read_idx, 0); + } + + fn generic_test_split_packet(cut_off: usize) { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; + assert!(cut_off < inverted_packet.len() + 1); + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + // Second packet + encoded_buf[current_idx] = 0; + let packet_start = current_idx; + current_idx += 1; + let encoded_len = encode(&inverted_packet, &mut encoded_buf[current_idx..]); + assert_eq!(encoded_len, 6); + current_idx += encoded_len; + // We cut off the sentinel byte, so we expecte the write index to be the length of the + // packet minus the sentinel byte plus the first sentinel byte. + let next_expected_write_idx = 1 + encoded_len - cut_off + 1; + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_write_idx = 0; + let expected_at_start = encoded_buf[packet_start..current_idx - cut_off].to_vec(); + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - cut_off], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, next_expected_write_idx); + assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); + } + + #[test] + fn test_one_packet_and_split_tail_packet_0() { + generic_test_split_packet(1); + } + + #[test] + fn test_one_packet_and_split_tail_packet_1() { + generic_test_split_packet(2); + } + + #[test] + fn test_one_packet_and_split_tail_packet_2() { + generic_test_split_packet(3); + } + + #[test] + fn test_zero_at_end() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut next_write_idx = 0; + let mut current_idx = 0; + encoded_buf[current_idx] = 5; + current_idx += 1; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + encoded_buf[current_idx] = 0; + current_idx += 1; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, 1); + assert_eq!(encoded_buf[0], 0); + } + + #[test] + fn test_all_zeroes() { + let mut test_sender = TestSender::default(); + let mut all_zeroes: [u8; 5] = [0; 5]; + let mut next_write_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut all_zeroes, + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert!(test_sender.received_tcs.is_empty()); + assert_eq!(next_write_idx, 0); } } From 13cacb0b53eeaf5a2b344cf1b1b4f108d269d771 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 15 Sep 2023 18:34:05 +0200 Subject: [PATCH 22/68] this should do the job --- satrs-core/src/hal/host/tcp_server.rs | 88 +++++++++++++++++++++------ 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs index 1b3bd7c..cfa3963 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -1,32 +1,68 @@ use alloc::boxed::Box; use alloc::vec; use cobs::decode_in_place; +use core::fmt::Display; +use std::io::Write; use std::net::ToSocketAddrs; use std::vec::Vec; use std::{io::Read, net::TcpListener}; +use thiserror::Error; use crate::tmtc::ReceivesTc; -pub struct TcpTcServer { - listener: TcpListener, - tc_receiver: Box>, - reader_vec: Vec, +pub trait TmPacketSource { + type Error; + fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result; } -impl TcpTcServer { +pub struct TcpTmtcServer { + listener: TcpListener, + tm_source: Box>, + tm_buffer: Vec, + tc_receiver: Box>, + tc_buffer: Vec, + num_received_tcs: u32, + num_sent_tms: u32, +} + +#[derive(Error, Debug)] +pub enum TcpTmtcError { + #[error("TM retrieval error: {0}")] + TmError(TmError), + #[error("TC retrieval error: {0}")] + TcError(TcError), + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + +impl TcpTmtcServer { pub fn new( addr: A, - tc_receiver: Box>, - reader_vec_size: usize, + tm_buffer_size: usize, + tm_source: Box>, + tc_buffer_size: usize, + tc_receiver: Box>, ) -> Result { - Ok(TcpTcServer { + Ok(Self { listener: TcpListener::bind(addr)?, + tm_source, + tm_buffer: vec![0; tm_buffer_size], tc_receiver, - reader_vec: vec![0; reader_vec_size], + tc_buffer: vec![0; tc_buffer_size], + num_received_tcs: 0, + num_sent_tms: 0, }) } - pub fn handle_connections(&mut self) -> Result<(), std::io::Error> { + pub fn number_of_received_tcs(&self) -> u32 { + self.num_received_tcs + } + + pub fn number_of_sent_tms(&self) -> u32 { + self.num_sent_tms + } + + pub fn handle_connections(&mut self) -> Result<(), TcpTmtcError> { let mut current_write_idx; let mut next_write_idx = 0; for stream in self.listener.incoming() { @@ -34,27 +70,43 @@ impl TcpTcServer { next_write_idx = 0; let mut stream = stream?; loop { - let read_len = stream.read(&mut self.reader_vec[current_write_idx..])?; + let read_len = stream.read(&mut self.tc_buffer[current_write_idx..])?; if read_len > 0 { current_write_idx += read_len; - if current_write_idx == self.reader_vec.capacity() { + if current_write_idx == self.tc_buffer.capacity() { // Reader vec full, need to parse for packets. - let parse_result = parse_buffer_for_cobs_encoded_packets( - &mut self.reader_vec, + self.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut self.tc_buffer[..current_write_idx], self.tc_receiver.as_mut(), &mut next_write_idx, - ); + ) + .map_err(|e| TcpTmtcError::TcError(e))?; } + current_write_idx = next_write_idx; continue; } break; } if current_write_idx > 0 { - let parse_result = parse_buffer_for_cobs_encoded_packets( - &mut self.reader_vec[..current_write_idx], + self.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut self.tc_buffer[..current_write_idx], self.tc_receiver.as_mut(), &mut next_write_idx, - ); + ) + .map_err(|e| TcpTmtcError::TcError(e))?; + } + loop { + // Write TM until TM source is exhausted. For now, there is no limit for the amount + // of TM written this way. + let read_tm_len = self + .tm_source + .retrieve_packet(&mut self.tm_buffer) + .map_err(|e| TcpTmtcError::TmError(e))?; + if read_tm_len == 0 { + break; + } + self.num_sent_tms += 1; + stream.write_all(&self.tm_buffer[..read_tm_len])?; } } Ok(()) From 3d6e33bc001a117062a97ee141635b575e88aac9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 15 Sep 2023 19:15:26 +0200 Subject: [PATCH 23/68] not sure if this is the best structure --- satrs-core/src/hal/host/mod.rs | 1 + satrs-core/src/hal/host/tcp_server.rs | 344 ++--------------- .../src/hal/host/tcp_spacepackets_server.rs | 0 .../src/hal/host/tcp_with_cobs_server.rs | 347 ++++++++++++++++++ satrs-core/src/tmtc/mod.rs | 7 + 5 files changed, 381 insertions(+), 318 deletions(-) create mode 100644 satrs-core/src/hal/host/tcp_spacepackets_server.rs create mode 100644 satrs-core/src/hal/host/tcp_with_cobs_server.rs diff --git a/satrs-core/src/hal/host/mod.rs b/satrs-core/src/hal/host/mod.rs index 051a2f7..dfd02c1 100644 --- a/satrs-core/src/hal/host/mod.rs +++ b/satrs-core/src/hal/host/mod.rs @@ -1,3 +1,4 @@ //! Helper modules intended to be used on hosts with a full [std] runtime +mod tcp_with_cobs_server; pub mod tcp_server; pub mod udp_server; diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs index cfa3963..16bcce4 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -1,29 +1,14 @@ -use alloc::boxed::Box; use alloc::vec; -use cobs::decode_in_place; +use alloc::{boxed::Box, vec::Vec}; +use std::net::SocketAddr; +use std::net::{TcpListener, ToSocketAddrs}; + +use crate::tmtc::{ReceivesTc, TmPacketSource}; use core::fmt::Display; -use std::io::Write; -use std::net::ToSocketAddrs; -use std::vec::Vec; -use std::{io::Read, net::TcpListener}; use thiserror::Error; -use crate::tmtc::ReceivesTc; - -pub trait TmPacketSource { - type Error; - fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result; -} - -pub struct TcpTmtcServer { - listener: TcpListener, - tm_source: Box>, - tm_buffer: Vec, - tc_receiver: Box>, - tc_buffer: Vec, - num_received_tcs: u32, - num_sent_tms: u32, -} +// Re-export the TMTC in COBS server. +pub use crate::hal::host::tcp_with_cobs_server::TcpTmtcInCobsServer; #[derive(Error, Debug)] pub enum TcpTmtcError { @@ -35,8 +20,25 @@ pub enum TcpTmtcError { Io(#[from] std::io::Error), } -impl TcpTmtcServer { - pub fn new( +/// Result of one connection attempt. Contains the client address if a connection was established, +/// in addition to the number of telecommands and telemetry packets exchanged. +#[derive(Debug, Default)] +pub struct ConnectionResult { + pub addr: Option, + pub num_received_tcs: u32, + pub num_sent_tms: u32, +} + +pub(crate) struct TcpTmtcServerBase { + pub (crate) listener: TcpListener, + pub (crate) tm_source: Box>, + pub (crate) tm_buffer: Vec, + pub (crate) tc_receiver: Box>, + pub (crate) tc_buffer: Vec, +} + +impl TcpTmtcServerBase { + pub(crate) fn new( addr: A, tm_buffer_size: usize, tm_source: Box>, @@ -49,300 +51,6 @@ impl TcpTmtcServer u32 { - self.num_received_tcs - } - - pub fn number_of_sent_tms(&self) -> u32 { - self.num_sent_tms - } - - pub fn handle_connections(&mut self) -> Result<(), TcpTmtcError> { - let mut current_write_idx; - let mut next_write_idx = 0; - for stream in self.listener.incoming() { - current_write_idx = next_write_idx; - next_write_idx = 0; - let mut stream = stream?; - loop { - let read_len = stream.read(&mut self.tc_buffer[current_write_idx..])?; - if read_len > 0 { - current_write_idx += read_len; - if current_write_idx == self.tc_buffer.capacity() { - // Reader vec full, need to parse for packets. - self.num_received_tcs += parse_buffer_for_cobs_encoded_packets( - &mut self.tc_buffer[..current_write_idx], - self.tc_receiver.as_mut(), - &mut next_write_idx, - ) - .map_err(|e| TcpTmtcError::TcError(e))?; - } - current_write_idx = next_write_idx; - continue; - } - break; - } - if current_write_idx > 0 { - self.num_received_tcs += parse_buffer_for_cobs_encoded_packets( - &mut self.tc_buffer[..current_write_idx], - self.tc_receiver.as_mut(), - &mut next_write_idx, - ) - .map_err(|e| TcpTmtcError::TcError(e))?; - } - loop { - // Write TM until TM source is exhausted. For now, there is no limit for the amount - // of TM written this way. - let read_tm_len = self - .tm_source - .retrieve_packet(&mut self.tm_buffer) - .map_err(|e| TcpTmtcError::TmError(e))?; - if read_tm_len == 0 { - break; - } - self.num_sent_tms += 1; - stream.write_all(&self.tm_buffer[..read_tm_len])?; - } - } - Ok(()) - } -} - -pub fn parse_buffer_for_cobs_encoded_packets( - buf: &mut [u8], - tc_receiver: &mut dyn ReceivesTc, - next_write_idx: &mut usize, -) -> Result { - let mut start_index_packet = 0; - let mut start_found = false; - let mut last_byte = false; - let mut packets_found = 0; - for i in 0..buf.len() { - if i == buf.len() - 1 { - last_byte = true; - } - if buf[i] == 0 { - if !start_found && !last_byte && buf[i + 1] == 0 { - // Special case: Consecutive sentinel values or all zeroes. - // Skip. - continue; - } - if start_found { - let decode_result = decode_in_place(&mut buf[start_index_packet..i]); - if let Ok(packet_len) = decode_result { - packets_found += 1; - tc_receiver - .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?; - } - start_found = false; - } else { - start_index_packet = i + 1; - start_found = true; - } - } - } - // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. - if start_index_packet > 0 && start_found && packets_found > 0 { - let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); - first_seg[..last_seg.len()].copy_from_slice(last_seg); - *next_write_idx = last_seg.len(); - } - Ok(packets_found) -} - -#[cfg(test)] -mod tests { - use crate::tmtc::ReceivesTcCore; - use alloc::vec::Vec; - use cobs::encode; - - use super::parse_buffer_for_cobs_encoded_packets; - - const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; - - #[derive(Default)] - struct TestSender { - received_tcs: Vec>, - } - - impl ReceivesTcCore for TestSender { - type Error = (); - - fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { - self.received_tcs.push(tc_raw.to_vec()); - Ok(()) - } - } - - fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encoded_buf[*current_idx] = 0; - *current_idx += 1; - *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); - encoded_buf[*current_idx] = 0; - *current_idx += 1; - } - - #[test] - fn test_parsing_simple_packet() { - let mut test_sender = TestSender::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - let packet = &test_sender.received_tcs[0]; - assert_eq!(packet, &SIMPLE_PACKET); - } - - #[test] - fn test_parsing_consecutive_packets() { - let mut test_sender = TestSender::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - - let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; - // Second packet - encoded_buf[current_idx] = 0; - current_idx += 1; - current_idx += encode(&inverted_packet, &mut encoded_buf[current_idx..]); - encoded_buf[current_idx] = 0; - current_idx += 1; - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 2); - assert_eq!(test_sender.received_tcs.len(), 2); - let packet0 = &test_sender.received_tcs[0]; - assert_eq!(packet0, &SIMPLE_PACKET); - let packet1 = &test_sender.received_tcs[1]; - assert_eq!(packet1, &inverted_packet); - } - - #[test] - fn test_split_tail_packet_only() { - let mut test_sender = TestSender::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx - 1], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 0); - assert_eq!(test_sender.received_tcs.len(), 0); - assert_eq!(next_read_idx, 0); - } - - fn generic_test_split_packet(cut_off: usize) { - let mut test_sender = TestSender::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; - assert!(cut_off < inverted_packet.len() + 1); - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - // Second packet - encoded_buf[current_idx] = 0; - let packet_start = current_idx; - current_idx += 1; - let encoded_len = encode(&inverted_packet, &mut encoded_buf[current_idx..]); - assert_eq!(encoded_len, 6); - current_idx += encoded_len; - // We cut off the sentinel byte, so we expecte the write index to be the length of the - // packet minus the sentinel byte plus the first sentinel byte. - let next_expected_write_idx = 1 + encoded_len - cut_off + 1; - encoded_buf[current_idx] = 0; - current_idx += 1; - let mut next_write_idx = 0; - let expected_at_start = encoded_buf[packet_start..current_idx - cut_off].to_vec(); - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx - cut_off], - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); - assert_eq!(next_write_idx, next_expected_write_idx); - assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); - } - - #[test] - fn test_one_packet_and_split_tail_packet_0() { - generic_test_split_packet(1); - } - - #[test] - fn test_one_packet_and_split_tail_packet_1() { - generic_test_split_packet(2); - } - - #[test] - fn test_one_packet_and_split_tail_packet_2() { - generic_test_split_packet(3); - } - - #[test] - fn test_zero_at_end() { - let mut test_sender = TestSender::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut next_write_idx = 0; - let mut current_idx = 0; - encoded_buf[current_idx] = 5; - current_idx += 1; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - encoded_buf[current_idx] = 0; - current_idx += 1; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); - assert_eq!(next_write_idx, 1); - assert_eq!(encoded_buf[0], 0); - } - - #[test] - fn test_all_zeroes() { - let mut test_sender = TestSender::default(); - let mut all_zeroes: [u8; 5] = [0; 5]; - let mut next_write_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut all_zeroes, - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 0); - assert!(test_sender.received_tcs.is_empty()); - assert_eq!(next_write_idx, 0); - } } diff --git a/satrs-core/src/hal/host/tcp_spacepackets_server.rs b/satrs-core/src/hal/host/tcp_spacepackets_server.rs new file mode 100644 index 0000000..e69de29 diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/host/tcp_with_cobs_server.rs new file mode 100644 index 0000000..6c72254 --- /dev/null +++ b/satrs-core/src/hal/host/tcp_with_cobs_server.rs @@ -0,0 +1,347 @@ +use alloc::boxed::Box; +use alloc::vec; +use cobs::decode_in_place; +use cobs::encode; +use cobs::max_encoding_length; +use core::fmt::Display; +use std::io::Read; +use std::io::Write; +use std::net::ToSocketAddrs; +use std::vec::Vec; + +use crate::tmtc::ReceivesTc; +use crate::tmtc::TmPacketSource; +use crate::hal::host::tcp_server::TcpTmtcServerBase; + +use super::tcp_server::ConnectionResult; +use super::tcp_server::TcpTmtcError; + +/// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the +/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). +/// +/// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data +/// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full +/// packets even from a data stream which is split up. +pub struct TcpTmtcInCobsServer { + base: TcpTmtcServerBase, + tm_encoding_buffer: Vec, +} + +impl TcpTmtcInCobsServer { + pub fn new( + addr: A, + tm_buffer_size: usize, + tm_source: Box>, + tc_buffer_size: usize, + tc_receiver: Box>, + ) -> Result { + Ok(Self { + base: TcpTmtcServerBase::new( + addr, + tm_buffer_size, + tm_source, + tc_buffer_size, + tc_receiver, + )?, + tm_encoding_buffer: vec![0; max_encoding_length(tc_buffer_size)], + }) + } + + pub fn handle_next_connection( + &mut self, + ) -> Result> { + let mut connection_result = ConnectionResult::default(); + let mut current_write_idx; + let mut next_write_idx = 0; + let (mut stream, addr) = self.base.listener.accept()?; + connection_result.addr = Some(addr); + current_write_idx = next_write_idx; + next_write_idx = 0; + loop { + let read_len = stream.read(&mut self.base.tc_buffer[current_write_idx..])?; + if read_len > 0 { + current_write_idx += read_len; + if current_write_idx == self.base.tc_buffer.capacity() { + // Reader vec full, need to parse for packets. + connection_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut self.base.tc_buffer[..current_write_idx], + self.base.tc_receiver.as_mut(), + &mut next_write_idx, + ) + .map_err(|e| TcpTmtcError::TcError(e))?; + } + current_write_idx = next_write_idx; + continue; + } + break; + } + if current_write_idx > 0 { + connection_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut self.base.tc_buffer[..current_write_idx], + self.base.tc_receiver.as_mut(), + &mut next_write_idx, + ) + .map_err(|e| TcpTmtcError::TcError(e))?; + } + loop { + // Write TM until TM source is exhausted. For now, there is no limit for the amount + // of TM written this way. + let read_tm_len = self + .base + .tm_source + .retrieve_packet(&mut self.base.tm_buffer) + .map_err(|e| TcpTmtcError::TmError(e))?; + if read_tm_len == 0 { + break; + } + connection_result.num_sent_tms += 1; + + // Encode into COBS and sent to client. + let mut current_idx = 0; + self.tm_encoding_buffer[current_idx] = 0; + current_idx += 1; + current_idx += encode( + &self.base.tm_buffer[..read_tm_len], + &mut self.tm_encoding_buffer, + ); + self.tm_encoding_buffer[current_idx] = 0; + current_idx += 1; + stream.write_all(&self.tm_encoding_buffer[..current_idx])?; + } + Ok(connection_result) + } +} + +pub fn parse_buffer_for_cobs_encoded_packets( + buf: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let mut start_index_packet = 0; + let mut start_found = false; + let mut last_byte = false; + let mut packets_found = 0; + for i in 0..buf.len() { + if i == buf.len() - 1 { + last_byte = true; + } + if buf[i] == 0 { + if !start_found && !last_byte && buf[i + 1] == 0 { + // Special case: Consecutive sentinel values or all zeroes. + // Skip. + continue; + } + if start_found { + let decode_result = decode_in_place(&mut buf[start_index_packet..i]); + if let Ok(packet_len) = decode_result { + packets_found += 1; + tc_receiver + .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?; + } + start_found = false; + } else { + start_index_packet = i + 1; + start_found = true; + } + } + } + // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. + if start_index_packet > 0 && start_found && packets_found > 0 { + let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); + first_seg[..last_seg.len()].copy_from_slice(last_seg); + *next_write_idx = last_seg.len(); + } + Ok(packets_found) +} + +#[cfg(test)] +mod tests { + use crate::tmtc::ReceivesTcCore; + use alloc::vec::Vec; + use cobs::encode; + + use super::parse_buffer_for_cobs_encoded_packets; + + const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + + #[derive(Default)] + struct TestSender { + received_tcs: Vec>, + } + + impl ReceivesTcCore for TestSender { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + self.received_tcs.push(tc_raw.to_vec()); + Ok(()) + } + } + + fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; + } + + #[test] + fn test_parsing_simple_packet() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + let packet = &test_sender.received_tcs[0]; + assert_eq!(packet, &SIMPLE_PACKET); + } + + #[test] + fn test_parsing_consecutive_packets() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + + let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; + // Second packet + encoded_buf[current_idx] = 0; + current_idx += 1; + current_idx += encode(&inverted_packet, &mut encoded_buf[current_idx..]); + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 2); + assert_eq!(test_sender.received_tcs.len(), 2); + let packet0 = &test_sender.received_tcs[0]; + assert_eq!(packet0, &SIMPLE_PACKET); + let packet1 = &test_sender.received_tcs[1]; + assert_eq!(packet1, &inverted_packet); + } + + #[test] + fn test_split_tail_packet_only() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - 1], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert_eq!(test_sender.received_tcs.len(), 0); + assert_eq!(next_read_idx, 0); + } + + fn generic_test_split_packet(cut_off: usize) { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; + assert!(cut_off < inverted_packet.len() + 1); + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + // Second packet + encoded_buf[current_idx] = 0; + let packet_start = current_idx; + current_idx += 1; + let encoded_len = encode(&inverted_packet, &mut encoded_buf[current_idx..]); + assert_eq!(encoded_len, 6); + current_idx += encoded_len; + // We cut off the sentinel byte, so we expecte the write index to be the length of the + // packet minus the sentinel byte plus the first sentinel byte. + let next_expected_write_idx = 1 + encoded_len - cut_off + 1; + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_write_idx = 0; + let expected_at_start = encoded_buf[packet_start..current_idx - cut_off].to_vec(); + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - cut_off], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, next_expected_write_idx); + assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); + } + + #[test] + fn test_one_packet_and_split_tail_packet_0() { + generic_test_split_packet(1); + } + + #[test] + fn test_one_packet_and_split_tail_packet_1() { + generic_test_split_packet(2); + } + + #[test] + fn test_one_packet_and_split_tail_packet_2() { + generic_test_split_packet(3); + } + + #[test] + fn test_zero_at_end() { + let mut test_sender = TestSender::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut next_write_idx = 0; + let mut current_idx = 0; + encoded_buf[current_idx] = 5; + current_idx += 1; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + encoded_buf[current_idx] = 0; + current_idx += 1; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.received_tcs.len(), 1); + assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, 1); + assert_eq!(encoded_buf[0], 0); + } + + #[test] + fn test_all_zeroes() { + let mut test_sender = TestSender::default(); + let mut all_zeroes: [u8; 5] = [0; 5]; + let mut next_write_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut all_zeroes, + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert!(test_sender.received_tcs.is_empty()); + assert_eq!(next_write_idx, 0); + } +} diff --git a/satrs-core/src/tmtc/mod.rs b/satrs-core/src/tmtc/mod.rs index 7237196..2f8bdf3 100644 --- a/satrs-core/src/tmtc/mod.rs +++ b/satrs-core/src/tmtc/mod.rs @@ -92,3 +92,10 @@ pub trait ReceivesCcsdsTc { type Error; fn pass_ccsds(&mut self, header: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error>; } + +/// Generic trait for a TM packet source, with no restrictions on the type of TM. +/// Implementors write the telemetry into the provided buffer and return the size of the telemetry. +pub trait TmPacketSource { + type Error; + fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result; +} From 1af5601d633cdd37b9eb89d2cacec13502b58416 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 15 Sep 2023 19:22:12 +0200 Subject: [PATCH 24/68] looking good --- satrs-core/src/hal/host/mod.rs | 2 +- satrs-core/src/hal/host/tcp_server.rs | 14 ++++++++------ satrs-core/src/hal/host/tcp_with_cobs_server.rs | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/satrs-core/src/hal/host/mod.rs b/satrs-core/src/hal/host/mod.rs index dfd02c1..5e98061 100644 --- a/satrs-core/src/hal/host/mod.rs +++ b/satrs-core/src/hal/host/mod.rs @@ -1,4 +1,4 @@ //! Helper modules intended to be used on hosts with a full [std] runtime -mod tcp_with_cobs_server; pub mod tcp_server; +mod tcp_with_cobs_server; pub mod udp_server; diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs index 16bcce4..c518bad 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -8,7 +8,9 @@ use core::fmt::Display; use thiserror::Error; // Re-export the TMTC in COBS server. -pub use crate::hal::host::tcp_with_cobs_server::TcpTmtcInCobsServer; +pub use crate::hal::host::tcp_with_cobs_server::{ + parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer, +}; #[derive(Error, Debug)] pub enum TcpTmtcError { @@ -30,11 +32,11 @@ pub struct ConnectionResult { } pub(crate) struct TcpTmtcServerBase { - pub (crate) listener: TcpListener, - pub (crate) tm_source: Box>, - pub (crate) tm_buffer: Vec, - pub (crate) tc_receiver: Box>, - pub (crate) tc_buffer: Vec, + pub(crate) listener: TcpListener, + pub(crate) tm_source: Box>, + pub(crate) tm_buffer: Vec, + pub(crate) tc_receiver: Box>, + pub(crate) tc_buffer: Vec, } impl TcpTmtcServerBase { diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/host/tcp_with_cobs_server.rs index 6c72254..359eec3 100644 --- a/satrs-core/src/hal/host/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/host/tcp_with_cobs_server.rs @@ -9,9 +9,9 @@ use std::io::Write; use std::net::ToSocketAddrs; use std::vec::Vec; +use crate::hal::host::tcp_server::TcpTmtcServerBase; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; -use crate::hal::host::tcp_server::TcpTmtcServerBase; use super::tcp_server::ConnectionResult; use super::tcp_server::TcpTmtcError; @@ -22,6 +22,9 @@ use super::tcp_server::TcpTmtcError; /// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data /// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full /// packets even from a data stream which is split up. +/// +/// The server wil use the [parse_buffer_for_cobs_encoded_packets] function to parse for packets +/// and pass them to a generic TC receiver. pub struct TcpTmtcInCobsServer { base: TcpTmtcServerBase, tm_encoding_buffer: Vec, @@ -112,6 +115,16 @@ impl TcpTmtcInCobsServer } } +/// This function parses a given buffer for COBS encoded packets. The packet structure is +/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter. +/// +/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 +/// +/// This function is also able to deal with broken tail packets at the end. If broken tail +/// packets are detected, they are moved to the front of the buffer, and the write index for +/// future write operations will be written to the `next_write_idx` argument. +/// +/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. pub fn parse_buffer_for_cobs_encoded_packets( buf: &mut [u8], tc_receiver: &mut dyn ReceivesTc, From eb5c755dd322e47802d418e32efdc234d7679f9d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 15 Sep 2023 20:29:05 +0200 Subject: [PATCH 25/68] doc improvements --- satrs-core/src/hal/host/tcp_with_cobs_server.rs | 1 + satrs-core/src/hal/host/udp_server.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/host/tcp_with_cobs_server.rs index 359eec3..539e491 100644 --- a/satrs-core/src/hal/host/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/host/tcp_with_cobs_server.rs @@ -25,6 +25,7 @@ use super::tcp_server::TcpTmtcError; /// /// The server wil use the [parse_buffer_for_cobs_encoded_packets] function to parse for packets /// and pass them to a generic TC receiver. +/// pub struct TcpTmtcInCobsServer { base: TcpTmtcServerBase, tm_encoding_buffer: Vec, diff --git a/satrs-core/src/hal/host/udp_server.rs b/satrs-core/src/hal/host/udp_server.rs index de5a3f0..3459212 100644 --- a/satrs-core/src/hal/host/udp_server.rs +++ b/satrs-core/src/hal/host/udp_server.rs @@ -6,7 +6,8 @@ use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; use std::vec; use std::vec::Vec; -/// This TC server helper can be used to receive raw PUS telecommands thorough a UDP interface. +/// This UDP server can be used to receive CCSDS space packet telecommands or any other telecommand +/// format. /// /// It caches all received telecomands into a vector. The maximum expected telecommand size should /// be declared upfront. This avoids dynamic allocation during run-time. The user can specify a TC From 0e6d903942e65eedde3f453e100acab3c97ea8b3 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 16:23:42 +0200 Subject: [PATCH 26/68] add unittest for whole TCP server --- satrs-core/src/hal/host/tcp_server.rs | 11 +- .../src/hal/host/tcp_with_cobs_server.rs | 109 +++++++++++++++--- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/host/tcp_server.rs index c518bad..11818c5 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/host/tcp_server.rs @@ -4,7 +4,6 @@ use std::net::SocketAddr; use std::net::{TcpListener, ToSocketAddrs}; use crate::tmtc::{ReceivesTc, TmPacketSource}; -use core::fmt::Display; use thiserror::Error; // Re-export the TMTC in COBS server. @@ -13,7 +12,7 @@ pub use crate::hal::host::tcp_with_cobs_server::{ }; #[derive(Error, Debug)] -pub enum TcpTmtcError { +pub enum TcpTmtcError { #[error("TM retrieval error: {0}")] TmError(TmError), #[error("TC retrieval error: {0}")] @@ -33,9 +32,9 @@ pub struct ConnectionResult { pub(crate) struct TcpTmtcServerBase { pub(crate) listener: TcpListener, - pub(crate) tm_source: Box>, + pub(crate) tm_source: Box + Send>, pub(crate) tm_buffer: Vec, - pub(crate) tc_receiver: Box>, + pub(crate) tc_receiver: Box + Send>, pub(crate) tc_buffer: Vec, } @@ -43,9 +42,9 @@ impl TcpTmtcServerBase { pub(crate) fn new( addr: A, tm_buffer_size: usize, - tm_source: Box>, + tm_source: Box + Send>, tc_buffer_size: usize, - tc_receiver: Box>, + tc_receiver: Box + Send>, ) -> Result { Ok(Self { listener: TcpListener::bind(addr)?, diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/host/tcp_with_cobs_server.rs index 539e491..0dcb1b6 100644 --- a/satrs-core/src/hal/host/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/host/tcp_with_cobs_server.rs @@ -3,7 +3,6 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use cobs::max_encoding_length; -use core::fmt::Display; use std::io::Read; use std::io::Write; use std::net::ToSocketAddrs; @@ -25,19 +24,18 @@ use super::tcp_server::TcpTmtcError; /// /// The server wil use the [parse_buffer_for_cobs_encoded_packets] function to parse for packets /// and pass them to a generic TC receiver. -/// pub struct TcpTmtcInCobsServer { base: TcpTmtcServerBase, tm_encoding_buffer: Vec, } -impl TcpTmtcInCobsServer { +impl TcpTmtcInCobsServer { pub fn new( addr: A, tm_buffer_size: usize, - tm_source: Box>, + tm_source: Box + Send>, tc_buffer_size: usize, - tc_receiver: Box>, + tc_receiver: Box + Send>, ) -> Result { Ok(Self { base: TcpTmtcServerBase::new( @@ -170,20 +168,31 @@ pub fn parse_buffer_for_cobs_encoded_packets( #[cfg(test)] mod tests { - use crate::tmtc::ReceivesTcCore; - use alloc::vec::Vec; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + use std::{ + io::Write, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, + sync::Mutex, + thread, + }; + + use crate::tmtc::{ReceivesTcCore, TmPacketSource}; + use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use cobs::encode; - use super::parse_buffer_for_cobs_encoded_packets; + use super::{parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer}; const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; #[derive(Default)] - struct TestSender { + struct TestTcSender { received_tcs: Vec>, } - impl ReceivesTcCore for TestSender { + impl ReceivesTcCore for TestTcSender { type Error = (); fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { @@ -192,6 +201,32 @@ mod tests { } } + #[derive(Default, Clone)] + struct TmSource { + shared_tm_source: Arc>>>, + } + + impl TmSource { + fn new() -> Self { + Self { + shared_tm_source: Default::default(), + } + } + + fn add_tm(&mut self, tm: &[u8]) { + let mut shared_tm_source = self.shared_tm_source.lock().unwrap(); + shared_tm_source.push_back(tm.to_vec()); + } + } + + impl TmPacketSource for TmSource { + type Error = (); + + fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { + Ok(0) + } + } + fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { encoded_buf[*current_idx] = 0; *current_idx += 1; @@ -202,7 +237,7 @@ mod tests { #[test] fn test_parsing_simple_packet() { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); @@ -221,7 +256,7 @@ mod tests { #[test] fn test_parsing_consecutive_packets() { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); @@ -250,7 +285,7 @@ mod tests { #[test] fn test_split_tail_packet_only() { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); @@ -268,7 +303,7 @@ mod tests { } fn generic_test_split_packet(cut_off: usize) { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; assert!(cut_off < inverted_packet.len() + 1); @@ -319,7 +354,7 @@ mod tests { #[test] fn test_zero_at_end() { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut next_write_idx = 0; let mut current_idx = 0; @@ -344,7 +379,7 @@ mod tests { #[test] fn test_all_zeroes() { - let mut test_sender = TestSender::default(); + let mut test_sender = TestTcSender::default(); let mut all_zeroes: [u8; 5] = [0; 5]; let mut next_write_idx = 0; let packets = parse_buffer_for_cobs_encoded_packets( @@ -358,4 +393,46 @@ mod tests { assert!(test_sender.received_tcs.is_empty()); assert_eq!(next_write_idx, 0); } + + #[test] + fn test_server_basic() { + let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); + let tc_receiver = TestTcSender::default(); + let tm_source = TmSource::default(); + let mut tcp_server = TcpTmtcInCobsServer::new( + dest_addr, + 1024, + Box::new(tm_source), + 1024, + Box::new(tc_receiver), + ) + .expect("TCP server generation failed"); + let conn_handled: Arc = Default::default(); + let set_if_done = conn_handled.clone(); + // Call the connection handler in separate thread, does block. + thread::spawn(move || { + let result = tcp_server.handle_next_connection(); + if result.is_err() { + panic!("handling connection failed: {:?}", result.unwrap_err()); + } + set_if_done.store(true, Ordering::Relaxed); + }); + // Send TC to server now. + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); + stream + .write_all(&encoded_buf) + .expect("writing to TCP server failed"); + // A certain amount of time is allowed for the transaction to complete. + for _ in 0..3 { + if !conn_handled.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(1)); + } + } + if !conn_handled.load(Ordering::Relaxed) { + panic!("connection was not handled properly"); + } + } } From e3043ce2d76dcbbde3373b3da4299d354066c723 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 21:24:01 +0200 Subject: [PATCH 27/68] this already looks very promising --- .../src/hal/host/tcp_with_cobs_server.rs | 257 ++++++++++++++---- 1 file changed, 198 insertions(+), 59 deletions(-) diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/host/tcp_with_cobs_server.rs index 0dcb1b6..b2d4720 100644 --- a/satrs-core/src/hal/host/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/host/tcp_with_cobs_server.rs @@ -18,18 +18,45 @@ use super::tcp_server::TcpTmtcError; /// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// +/// TCP is stream oriented, so a client can read available telemetry using [std::io::Read] as well. +/// To allow flexibly specifying the telemetry sent back to clients, a generic TM abstraction +/// in form of the [TmPacketSource] trait is used. Telemetry will be encoded with the COBS +/// protocol using [cobs::encode] in addition to being wrapped with the sentinel value 0 as the +/// packet delimiter as well before being sent back to the client. Please note that the server +/// will send as much data as it can retrieve from the [TmPacketSource] in its current +/// implementation. +/// /// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data /// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full -/// packets even from a data stream which is split up. -/// -/// The server wil use the [parse_buffer_for_cobs_encoded_packets] function to parse for packets -/// and pass them to a generic TC receiver. +/// packets even from a data stream which is split up. The server wil use the +/// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a +/// generic TC receiver. pub struct TcpTmtcInCobsServer { base: TcpTmtcServerBase, tm_encoding_buffer: Vec, } impl TcpTmtcInCobsServer { + /// Create a new TMTC server which exchanges TMTC packets encoded with + /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). + /// + /// ## Parameter + /// + /// * `addr` - Address of the TCP server. + /// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and + /// encoding of that data. This buffer should at large enough to hold the maximum expected + /// TM size in addition to the COBS encoding overhead. You can use + /// [cobs::max_encoding_length] to calculate this size. + /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are + /// then sent back to the client. + /// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from + /// the client. It is recommended to make this buffer larger to allow reading multiple + /// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should + /// at the very least be large enough to hold the maximum expected telecommand size in + /// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to + /// calculate this size. + /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded + /// to this TC receiver. pub fn new( addr: A, tm_buffer_size: usize, @@ -49,6 +76,15 @@ impl TcpTmtcInCobsServer { }) } + /// This call is used to handle the next connection to a client. Right now, it performs + /// the following steps: + /// + /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API + /// until a client connects. + /// 2. It reads all the telecommands from the client, which are expected to be COBS + /// encoded packets. + /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve + /// from the user specified [TmPacketSource] back to the client. pub fn handle_next_connection( &mut self, ) -> Result> { @@ -71,8 +107,8 @@ impl TcpTmtcInCobsServer { &mut next_write_idx, ) .map_err(|e| TcpTmtcError::TcError(e))?; + current_write_idx = next_write_idx; } - current_write_idx = next_write_idx; continue; } break; @@ -173,7 +209,7 @@ mod tests { time::Duration, }; use std::{ - io::Write, + io::{Read, Write}, net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, sync::Mutex, thread, @@ -186,43 +222,64 @@ mod tests { use super::{parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer}; const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; - #[derive(Default)] - struct TestTcSender { - received_tcs: Vec>, + #[derive(Default, Clone)] + struct SyncTcCacher { + tc_queue: Arc>>>, } - - impl ReceivesTcCore for TestTcSender { + impl ReceivesTcCore for SyncTcCacher { type Error = (); fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { - self.received_tcs.push(tc_raw.to_vec()); + let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed"); + tc_queue.push_back(tc_raw.to_vec()); + Ok(()) + } + } + + #[derive(Default)] + struct TcCacher { + tc_queue: VecDeque>, + } + + impl ReceivesTcCore for TcCacher { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + self.tc_queue.push_back(tc_raw.to_vec()); Ok(()) } } #[derive(Default, Clone)] - struct TmSource { - shared_tm_source: Arc>>>, + struct SyncTmSource { + tm_queue: Arc>>>, } - impl TmSource { - fn new() -> Self { - Self { - shared_tm_source: Default::default(), - } - } - - fn add_tm(&mut self, tm: &[u8]) { - let mut shared_tm_source = self.shared_tm_source.lock().unwrap(); - shared_tm_source.push_back(tm.to_vec()); + impl SyncTmSource { + pub(crate) fn add_tm(&mut self, tm: &[u8]) { + let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec"); + tm_queue.push_back(tm.to_vec()); } } - impl TmPacketSource for TmSource { + impl TmPacketSource for SyncTmSource { type Error = (); fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { + let tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); + if !tm_queue.is_empty() { + let next_vec = tm_queue.front().unwrap(); + if buffer.len() < next_vec.len() { + panic!( + "provided buffer too small, must be at least {} bytes", + next_vec.len() + ); + } + buffer[0..next_vec.len()].copy_from_slice(next_vec); + return Ok(next_vec.len()); + } Ok(0) } } @@ -237,7 +294,7 @@ mod tests { #[test] fn test_parsing_simple_packet() { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); @@ -249,23 +306,22 @@ mod tests { ) .unwrap(); assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - let packet = &test_sender.received_tcs[0]; + assert_eq!(test_sender.tc_queue.len(), 1); + let packet = &test_sender.tc_queue[0]; assert_eq!(packet, &SIMPLE_PACKET); } #[test] fn test_parsing_consecutive_packets() { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); - let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; // Second packet encoded_buf[current_idx] = 0; current_idx += 1; - current_idx += encode(&inverted_packet, &mut encoded_buf[current_idx..]); + current_idx += encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); encoded_buf[current_idx] = 0; current_idx += 1; let mut next_read_idx = 0; @@ -276,16 +332,16 @@ mod tests { ) .unwrap(); assert_eq!(packets, 2); - assert_eq!(test_sender.received_tcs.len(), 2); - let packet0 = &test_sender.received_tcs[0]; + assert_eq!(test_sender.tc_queue.len(), 2); + let packet0 = &test_sender.tc_queue[0]; assert_eq!(packet0, &SIMPLE_PACKET); - let packet1 = &test_sender.received_tcs[1]; - assert_eq!(packet1, &inverted_packet); + let packet1 = &test_sender.tc_queue[1]; + assert_eq!(packet1, &INVERTED_PACKET); } #[test] fn test_split_tail_packet_only() { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); @@ -298,22 +354,21 @@ mod tests { ) .unwrap(); assert_eq!(packets, 0); - assert_eq!(test_sender.received_tcs.len(), 0); + assert_eq!(test_sender.tc_queue.len(), 0); assert_eq!(next_read_idx, 0); } fn generic_test_split_packet(cut_off: usize) { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut encoded_buf: [u8; 16] = [0; 16]; - let inverted_packet: [u8; 5] = [5, 4, 3, 2, 1]; - assert!(cut_off < inverted_packet.len() + 1); + assert!(cut_off < INVERTED_PACKET.len() + 1); let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); // Second packet encoded_buf[current_idx] = 0; let packet_start = current_idx; current_idx += 1; - let encoded_len = encode(&inverted_packet, &mut encoded_buf[current_idx..]); + let encoded_len = encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); assert_eq!(encoded_len, 6); current_idx += encoded_len; // We cut off the sentinel byte, so we expecte the write index to be the length of the @@ -331,8 +386,8 @@ mod tests { ) .unwrap(); assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(test_sender.tc_queue.len(), 1); + assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); assert_eq!(next_write_idx, next_expected_write_idx); assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); } @@ -354,7 +409,7 @@ mod tests { #[test] fn test_zero_at_end() { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut encoded_buf: [u8; 16] = [0; 16]; let mut next_write_idx = 0; let mut current_idx = 0; @@ -371,15 +426,15 @@ mod tests { ) .unwrap(); assert_eq!(packets, 1); - assert_eq!(test_sender.received_tcs.len(), 1); - assert_eq!(&test_sender.received_tcs[0], &SIMPLE_PACKET); + assert_eq!(test_sender.tc_queue.len(), 1); + assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); assert_eq!(next_write_idx, 1); assert_eq!(encoded_buf[0], 0); } #[test] fn test_all_zeroes() { - let mut test_sender = TestTcSender::default(); + let mut test_sender = TcCacher::default(); let mut all_zeroes: [u8; 5] = [0; 5]; let mut next_write_idx = 0; let packets = parse_buffer_for_cobs_encoded_packets( @@ -390,23 +445,32 @@ mod tests { ) .unwrap(); assert_eq!(packets, 0); - assert!(test_sender.received_tcs.is_empty()); + assert!(test_sender.tc_queue.is_empty()); assert_eq!(next_write_idx, 0); } - #[test] - fn test_server_basic() { - let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); - let tc_receiver = TestTcSender::default(); - let tm_source = TmSource::default(); - let mut tcp_server = TcpTmtcInCobsServer::new( - dest_addr, + fn generic_tmtc_server( + addr: &SocketAddr, + tc_receiver: SyncTcCacher, + tm_source: SyncTmSource, + ) -> TcpTmtcInCobsServer<(), ()> { + TcpTmtcInCobsServer::new( + addr, 1024, Box::new(tm_source), 1024, - Box::new(tc_receiver), + Box::new(tc_receiver.clone()), ) - .expect("TCP server generation failed"); + .expect("TCP server generation failed") + } + + #[test] + fn test_server_basic_no_tm() { + let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); + let tc_receiver = SyncTcCacher::default(); + let tm_source = SyncTmSource::default(); + let mut tcp_server = + generic_tmtc_server(&dest_addr, tc_receiver.clone(), tm_source.clone()); let conn_handled: Arc = Default::default(); let set_if_done = conn_handled.clone(); // Call the connection handler in separate thread, does block. @@ -415,6 +479,9 @@ mod tests { if result.is_err() { panic!("handling connection failed: {:?}", result.unwrap_err()); } + let conn_result = result.unwrap(); + assert_eq!(conn_result.num_received_tcs, 1); + assert_eq!(conn_result.num_sent_tms, 0); set_if_done.store(true, Ordering::Relaxed); }); // Send TC to server now. @@ -423,16 +490,88 @@ mod tests { encode_simple_packet(&mut encoded_buf, &mut current_idx); let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); stream - .write_all(&encoded_buf) + .write_all(&encoded_buf[..current_idx]) .expect("writing to TCP server failed"); + drop(stream); // A certain amount of time is allowed for the transaction to complete. for _ in 0..3 { if !conn_handled.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(1)); + thread::sleep(Duration::from_millis(5)); } } if !conn_handled.load(Ordering::Relaxed) { panic!("connection was not handled properly"); } + // Check that the packet was received and decoded successfully. + let mut tc_queue = tc_receiver + .tc_queue + .lock() + .expect("locking tc queue failed"); + assert_eq!(tc_queue.len(), 1); + assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); + drop(tc_queue); + } + + #[test] + fn test_server_basic_no_tm_multi_tc() {} + + #[test] + fn test_server_basic_with_tm() { + let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); + let tc_receiver = SyncTcCacher::default(); + let mut tm_source = SyncTmSource::default(); + tm_source.add_tm(&INVERTED_PACKET); + let mut tcp_server = + generic_tmtc_server(&dest_addr, tc_receiver.clone(), tm_source.clone()); + let conn_handled: Arc = Default::default(); + let set_if_done = conn_handled.clone(); + // Call the connection handler in separate thread, does block. + thread::spawn(move || { + let result = tcp_server.handle_next_connection(); + if result.is_err() { + panic!("handling connection failed: {:?}", result.unwrap_err()); + } + let conn_result = result.unwrap(); + assert_eq!(conn_result.num_received_tcs, 1); + assert_eq!(conn_result.num_sent_tms, 1); + set_if_done.store(true, Ordering::Relaxed); + }); + // Send TC to server now. + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); + stream + .write_all(&encoded_buf[..current_idx]) + .expect("writing to TCP server failed"); + let mut read_buf: [u8; 16] = [0; 16]; + let read_len = stream.read(&mut read_buf).expect("read failed"); + // 1 byte encoding overhead, 2 sentinel bytes. + assert_eq!(read_len, 8); + assert_eq!(read_buf[0], 0); + assert_eq!(read_buf[read_len - 1], 0); + let decoded_len = + cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); + assert_eq!(decoded_len, 5); + assert_eq!(&read_buf[..INVERTED_PACKET.len()], &INVERTED_PACKET); + + drop(stream); + // A certain amount of time is allowed for the transaction to complete. + for _ in 0..3 { + if !conn_handled.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(5)); + } + } + if !conn_handled.load(Ordering::Relaxed) { + panic!("connection was not handled properly"); + } + // Check that the packet was received and decoded successfully. + let mut tc_queue = tc_receiver + .tc_queue + .lock() + .expect("locking tc queue failed"); + assert_eq!(tc_queue.len(), 1); + assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); + drop(tc_queue); } } From 51e31f70f77ccc91335dc5bfab3ce04413a4f0aa Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 21:28:22 +0200 Subject: [PATCH 28/68] renamed host module to std module --- satrs-core/src/hal/host/mod.rs | 4 ---- satrs-core/src/hal/mod.rs | 2 +- satrs-core/src/hal/std/mod.rs | 5 +++++ satrs-core/src/hal/{host => std}/tcp_server.rs | 3 ++- satrs-core/src/hal/{host => std}/tcp_spacepackets_server.rs | 0 satrs-core/src/hal/{host => std}/tcp_with_cobs_server.rs | 2 +- satrs-core/src/hal/{host => std}/udp_server.rs | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 satrs-core/src/hal/host/mod.rs create mode 100644 satrs-core/src/hal/std/mod.rs rename satrs-core/src/hal/{host => std}/tcp_server.rs (93%) rename satrs-core/src/hal/{host => std}/tcp_spacepackets_server.rs (100%) rename satrs-core/src/hal/{host => std}/tcp_with_cobs_server.rs (99%) rename satrs-core/src/hal/{host => std}/udp_server.rs (99%) diff --git a/satrs-core/src/hal/host/mod.rs b/satrs-core/src/hal/host/mod.rs deleted file mode 100644 index 5e98061..0000000 --- a/satrs-core/src/hal/host/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Helper modules intended to be used on hosts with a full [std] runtime -pub mod tcp_server; -mod tcp_with_cobs_server; -pub mod udp_server; diff --git a/satrs-core/src/hal/mod.rs b/satrs-core/src/hal/mod.rs index c422a72..b6ab984 100644 --- a/satrs-core/src/hal/mod.rs +++ b/satrs-core/src/hal/mod.rs @@ -1,4 +1,4 @@ //! # Hardware Abstraction Layer module #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -pub mod host; +pub mod std; diff --git a/satrs-core/src/hal/std/mod.rs b/satrs-core/src/hal/std/mod.rs new file mode 100644 index 0000000..7ac107b --- /dev/null +++ b/satrs-core/src/hal/std/mod.rs @@ -0,0 +1,5 @@ +//! Helper modules intended to be used on systems with a full [std] runtime. +pub mod tcp_server; +pub mod udp_server; + +mod tcp_with_cobs_server; diff --git a/satrs-core/src/hal/host/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs similarity index 93% rename from satrs-core/src/hal/host/tcp_server.rs rename to satrs-core/src/hal/std/tcp_server.rs index 11818c5..1195dd1 100644 --- a/satrs-core/src/hal/host/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -1,3 +1,4 @@ +//! Generic TCP TMTC servers with different TMTC format flavours. use alloc::vec; use alloc::{boxed::Box, vec::Vec}; use std::net::SocketAddr; @@ -7,7 +8,7 @@ use crate::tmtc::{ReceivesTc, TmPacketSource}; use thiserror::Error; // Re-export the TMTC in COBS server. -pub use crate::hal::host::tcp_with_cobs_server::{ +pub use crate::hal::std::tcp_with_cobs_server::{ parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer, }; diff --git a/satrs-core/src/hal/host/tcp_spacepackets_server.rs b/satrs-core/src/hal/std/tcp_spacepackets_server.rs similarity index 100% rename from satrs-core/src/hal/host/tcp_spacepackets_server.rs rename to satrs-core/src/hal/std/tcp_spacepackets_server.rs diff --git a/satrs-core/src/hal/host/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs similarity index 99% rename from satrs-core/src/hal/host/tcp_with_cobs_server.rs rename to satrs-core/src/hal/std/tcp_with_cobs_server.rs index b2d4720..c59367f 100644 --- a/satrs-core/src/hal/host/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -8,7 +8,7 @@ use std::io::Write; use std::net::ToSocketAddrs; use std::vec::Vec; -use crate::hal::host::tcp_server::TcpTmtcServerBase; +use crate::hal::std::tcp_server::TcpTmtcServerBase; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; diff --git a/satrs-core/src/hal/host/udp_server.rs b/satrs-core/src/hal/std/udp_server.rs similarity index 99% rename from satrs-core/src/hal/host/udp_server.rs rename to satrs-core/src/hal/std/udp_server.rs index 3459212..83ad6f2 100644 --- a/satrs-core/src/hal/host/udp_server.rs +++ b/satrs-core/src/hal/std/udp_server.rs @@ -1,4 +1,4 @@ -//! UDP server helper components +//! Generic UDP TC server. use crate::tmtc::{ReceivesTc, ReceivesTcCore}; use std::boxed::Box; use std::io::{Error, ErrorKind}; From b5813f9c901cf501a567d3070f9050b8eb85cc1b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 21:36:28 +0200 Subject: [PATCH 29/68] lets see if this fixes issues --- satrs-core/src/hal/std/tcp_with_cobs_server.rs | 4 ++-- satrs-core/src/hal/std/udp_server.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index c59367f..066143a 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -466,7 +466,7 @@ mod tests { #[test] fn test_server_basic_no_tm() { - let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); + let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let tm_source = SyncTmSource::default(); let mut tcp_server = @@ -517,7 +517,7 @@ mod tests { #[test] fn test_server_basic_with_tm() { - let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); + let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let mut tm_source = SyncTmSource::default(); tm_source.add_tm(&INVERTED_PACKET); diff --git a/satrs-core/src/hal/std/udp_server.rs b/satrs-core/src/hal/std/udp_server.rs index 83ad6f2..931da1c 100644 --- a/satrs-core/src/hal/std/udp_server.rs +++ b/satrs-core/src/hal/std/udp_server.rs @@ -52,9 +52,9 @@ use std::vec::Vec; /// .expect("Error sending PUS TC via UDP"); /// ``` /// -/// The [fsrc-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example) +/// The [satrs-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/satrs-example) /// server code also includes -/// [example code](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/fsrc-example/src/bin/obsw/tmtc.rs) +/// [example code](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/tmtc.rs#L67) /// on how to use this TC server. It uses the server to receive PUS telecommands on a specific port /// and then forwards them to a generic CCSDS packet receiver. pub struct UdpTcServer { @@ -141,7 +141,7 @@ impl UdpTcServer { #[cfg(test)] mod tests { - use crate::hal::host::udp_server::{ReceiveResult, UdpTcServer}; + use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use crate::tmtc::ReceivesTcCore; use spacepackets::ecss::tc::PusTcCreator; use spacepackets::ecss::SerializablePusPacket; From 706dde51c4bfff58b10170a9595ca42573d76432 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 21:51:06 +0200 Subject: [PATCH 30/68] okay, some stuff still not working --- satrs-core/src/hal/std/tcp_server.rs | 4 ++++ satrs-core/src/hal/std/tcp_with_cobs_server.rs | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 1195dd1..ada56a9 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -55,4 +55,8 @@ impl TcpTmtcServerBase { tc_buffer: vec![0; tc_buffer_size], }) } + + pub (crate) fn local_addr(&self) -> std::io::Result { + self.listener.local_addr() + } } diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 066143a..bd26401 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -3,6 +3,7 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use cobs::max_encoding_length; +use std::net::SocketAddr; use std::io::Read; use std::io::Write; use std::net::ToSocketAddrs; @@ -76,6 +77,12 @@ impl TcpTmtcInCobsServer { }) } + /// Can be used to retrieve the local assigned address of the TCP server. This is especially + /// useful if using the port number 0 for OS auto-assignment. + pub fn local_addr(&self) -> std::io::Result { + self.base.local_addr() + } + /// This call is used to handle the next connection to a client. Right now, it performs /// the following steps: /// @@ -466,11 +473,12 @@ mod tests { #[test] fn test_server_basic_no_tm() { - let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); + let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let tm_source = SyncTmSource::default(); let mut tcp_server = - generic_tmtc_server(&dest_addr, tc_receiver.clone(), tm_source.clone()); + generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); + let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); let conn_handled: Arc = Default::default(); let set_if_done = conn_handled.clone(); // Call the connection handler in separate thread, does block. @@ -517,12 +525,13 @@ mod tests { #[test] fn test_server_basic_with_tm() { - let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); + let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let mut tm_source = SyncTmSource::default(); tm_source.add_tm(&INVERTED_PACKET); let mut tcp_server = - generic_tmtc_server(&dest_addr, tc_receiver.clone(), tm_source.clone()); + generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); + let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); let conn_handled: Arc = Default::default(); let set_if_done = conn_handled.clone(); // Call the connection handler in separate thread, does block. From d582ce212efb4e8b0fa9b8b76fc4cc251a4ab9df Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 16 Sep 2023 22:19:48 +0200 Subject: [PATCH 31/68] might require some more tweaks.. --- satrs-core/src/hal/std/tcp_server.rs | 4 ++-- .../src/hal/std/tcp_with_cobs_server.rs | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index ada56a9..f8cd16a 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -55,8 +55,8 @@ impl TcpTmtcServerBase { tc_buffer: vec![0; tc_buffer_size], }) } - - pub (crate) fn local_addr(&self) -> std::io::Result { + + pub(crate) fn local_addr(&self) -> std::io::Result { self.listener.local_addr() } } diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index bd26401..0d1248c 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -3,9 +3,9 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use cobs::max_encoding_length; -use std::net::SocketAddr; use std::io::Read; use std::io::Write; +use std::net::SocketAddr; use std::net::ToSocketAddrs; use std::vec::Vec; @@ -147,7 +147,7 @@ impl TcpTmtcInCobsServer { current_idx += 1; current_idx += encode( &self.base.tm_buffer[..read_tm_len], - &mut self.tm_encoding_buffer, + &mut self.tm_encoding_buffer[current_idx..], ); self.tm_encoding_buffer[current_idx] = 0; current_idx += 1; @@ -275,7 +275,7 @@ mod tests { type Error = (); fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { - let tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); + let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); if !tm_queue.is_empty() { let next_vec = tm_queue.front().unwrap(); if buffer.len() < next_vec.len() { @@ -284,7 +284,8 @@ mod tests { next_vec.len() ); } - buffer[0..next_vec.len()].copy_from_slice(next_vec); + let next_vec = tm_queue.pop_front().unwrap(); + buffer[0..next_vec.len()].copy_from_slice(&next_vec); return Ok(next_vec.len()); } Ok(0) @@ -478,7 +479,9 @@ mod tests { let tm_source = SyncTmSource::default(); let mut tcp_server = generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); - let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); + let dest_addr = tcp_server + .local_addr() + .expect("retrieving dest addr failed"); let conn_handled: Arc = Default::default(); let set_if_done = conn_handled.clone(); // Call the connection handler in separate thread, does block. @@ -531,7 +534,9 @@ mod tests { tm_source.add_tm(&INVERTED_PACKET); let mut tcp_server = generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); - let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); + let dest_addr = tcp_server + .local_addr() + .expect("retrieving dest addr failed"); let conn_handled: Arc = Default::default(); let set_if_done = conn_handled.clone(); // Call the connection handler in separate thread, does block. @@ -553,6 +558,10 @@ mod tests { stream .write_all(&encoded_buf[..current_idx]) .expect("writing to TCP server failed"); + // Done with writing. + stream + .shutdown(std::net::Shutdown::Write) + .expect("shutting down write failed"); let mut read_buf: [u8; 16] = [0; 16]; let read_len = stream.read(&mut read_buf).expect("read failed"); // 1 byte encoding overhead, 2 sentinel bytes. @@ -562,7 +571,8 @@ mod tests { let decoded_len = cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); assert_eq!(decoded_len, 5); - assert_eq!(&read_buf[..INVERTED_PACKET.len()], &INVERTED_PACKET); + // Skip first sentinel byte. + assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); drop(stream); // A certain amount of time is allowed for the transaction to complete. From de690b3eede0186a9877825c913f4412eb64416f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 17 Sep 2023 01:32:18 +0200 Subject: [PATCH 32/68] some more improvements --- satrs-core/Cargo.toml | 8 ++++++- satrs-core/src/hal/std/tcp_server.rs | 22 +++++++++++++++---- .../src/hal/std/tcp_with_cobs_server.rs | 21 +++++++++++++++--- satrs-core/src/hal/std/udp_server.rs | 2 +- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 7b6697f..9ee7cc1 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -60,6 +60,11 @@ version = "1" default-features = false optional = true +[dependencies.socket2] +version = "0.5.4" +features = ["all"] +optional = true + [dependencies.spacepackets] version = "0.7.0-beta.1" # path = "../../spacepackets" @@ -93,7 +98,8 @@ std = [ "serde/std", "spacepackets/std", "num_enum/std", - "thiserror" + "thiserror", + "socket2" ] alloc = [ "serde/alloc", diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index f8cd16a..c98ab9d 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -1,8 +1,9 @@ //! Generic TCP TMTC servers with different TMTC format flavours. use alloc::vec; use alloc::{boxed::Box, vec::Vec}; +use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; -use std::net::{TcpListener, ToSocketAddrs}; +use std::net::TcpListener; use crate::tmtc::{ReceivesTc, TmPacketSource}; use thiserror::Error; @@ -40,15 +41,24 @@ pub(crate) struct TcpTmtcServerBase { } impl TcpTmtcServerBase { - pub(crate) fn new( - addr: A, + pub(crate) fn new( + addr: &SocketAddr, + reuse_addr: bool, + reuse_port: bool, tm_buffer_size: usize, tm_source: Box + Send>, tc_buffer_size: usize, tc_receiver: Box + Send>, ) -> Result { + // Create a TCP listener bound to two addresses. + let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?; + socket.set_reuse_address(reuse_addr)?; + socket.set_reuse_port(reuse_port)?; + let addr = (*addr).into(); + socket.bind(&addr)?; + socket.listen(128)?; Ok(Self { - listener: TcpListener::bind(addr)?, + listener: socket.into(), tm_source, tm_buffer: vec![0; tm_buffer_size], tc_receiver, @@ -56,6 +66,10 @@ impl TcpTmtcServerBase { }) } + pub(crate) fn listener(&mut self) -> &mut TcpListener { + &mut self.listener + } + pub(crate) fn local_addr(&self) -> std::io::Result { self.listener.local_addr() } diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 0d1248c..605595f 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -6,7 +6,7 @@ use cobs::max_encoding_length; use std::io::Read; use std::io::Write; use std::net::SocketAddr; -use std::net::ToSocketAddrs; +use std::net::TcpListener; use std::vec::Vec; use crate::hal::std::tcp_server::TcpTmtcServerBase; @@ -44,6 +44,10 @@ impl TcpTmtcInCobsServer { /// ## Parameter /// /// * `addr` - Address of the TCP server. + /// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is + /// especially useful if the address and port are static for the server. + /// * `reuse_port` - Can be used to set the `SO_REUSEPORT` option on the raw socket. This is + /// especially useful if the address and port are static for the server. /// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and /// encoding of that data. This buffer should at large enough to hold the maximum expected /// TM size in addition to the COBS encoding overhead. You can use @@ -58,8 +62,10 @@ impl TcpTmtcInCobsServer { /// calculate this size. /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded /// to this TC receiver. - pub fn new( - addr: A, + pub fn new( + addr: &SocketAddr, + reuse_addr: bool, + reuse_port: bool, tm_buffer_size: usize, tm_source: Box + Send>, tc_buffer_size: usize, @@ -68,6 +74,8 @@ impl TcpTmtcInCobsServer { Ok(Self { base: TcpTmtcServerBase::new( addr, + reuse_addr, + reuse_port, tm_buffer_size, tm_source, tc_buffer_size, @@ -77,6 +85,11 @@ impl TcpTmtcInCobsServer { }) } + /// Retrieve the internal [TcpListener] class. + pub fn listener(&mut self) -> &mut TcpListener { + self.base.listener() + } + /// Can be used to retrieve the local assigned address of the TCP server. This is especially /// useful if using the port number 0 for OS auto-assignment. pub fn local_addr(&self) -> std::io::Result { @@ -464,6 +477,8 @@ mod tests { ) -> TcpTmtcInCobsServer<(), ()> { TcpTmtcInCobsServer::new( addr, + false, + false, 1024, Box::new(tm_source), 1024, diff --git a/satrs-core/src/hal/std/udp_server.rs b/satrs-core/src/hal/std/udp_server.rs index 931da1c..28e0328 100644 --- a/satrs-core/src/hal/std/udp_server.rs +++ b/satrs-core/src/hal/std/udp_server.rs @@ -20,7 +20,7 @@ use std::vec::Vec; /// ``` /// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; /// use spacepackets::ecss::SerializablePusPacket; -/// use satrs_core::hal::host::udp_server::UdpTcServer; +/// use satrs_core::hal::std::udp_server::UdpTcServer; /// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; /// use spacepackets::SpHeader; /// use spacepackets::ecss::tc::PusTcCreator; From 8d8e319aee9746adf04c7900460f232a6e0480ae Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 17 Sep 2023 02:31:02 +0200 Subject: [PATCH 33/68] neat --- satrs-core/src/hal/std/tcp_server.rs | 4 + .../src/hal/std/tcp_with_cobs_server.rs | 201 +++++++++++++----- 2 files changed, 147 insertions(+), 58 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index c98ab9d..11dc9e1 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -1,6 +1,7 @@ //! Generic TCP TMTC servers with different TMTC format flavours. use alloc::vec; use alloc::{boxed::Box, vec::Vec}; +use core::time::Duration; use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; use std::net::TcpListener; @@ -34,6 +35,7 @@ pub struct ConnectionResult { pub(crate) struct TcpTmtcServerBase { pub(crate) listener: TcpListener, + pub(crate) inner_loop_delay: Duration, pub(crate) tm_source: Box + Send>, pub(crate) tm_buffer: Vec, pub(crate) tc_receiver: Box + Send>, @@ -43,6 +45,7 @@ pub(crate) struct TcpTmtcServerBase { impl TcpTmtcServerBase { pub(crate) fn new( addr: &SocketAddr, + inner_loop_delay: Duration, reuse_addr: bool, reuse_port: bool, tm_buffer_size: usize, @@ -59,6 +62,7 @@ impl TcpTmtcServerBase { socket.listen(128)?; Ok(Self { listener: socket.into(), + inner_loop_delay, tm_source, tm_buffer: vec![0; tm_buffer_size], tc_receiver, diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 605595f..983e57c 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -3,10 +3,14 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use cobs::max_encoding_length; +use core::time::Duration; use std::io::Read; use std::io::Write; use std::net::SocketAddr; use std::net::TcpListener; +use std::net::TcpStream; +use std::println; +use std::thread; use std::vec::Vec; use crate::hal::std::tcp_server::TcpTmtcServerBase; @@ -16,6 +20,58 @@ use crate::tmtc::TmPacketSource; use super::tcp_server::ConnectionResult; use super::tcp_server::TcpTmtcError; +/// TCP configuration struct. +/// +/// ## Parameters +/// +/// * `addr` - Address of the TCP server. +/// * `inner_loop_delay` - If a client connects for a longer period, but no TC is received or +/// no TM needs to be sent, the TCP server will delay for the specified amount of time +/// to reduce CPU load. +/// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and +/// encoding of that data. This buffer should at large enough to hold the maximum expected +/// TM size in addition to the COBS encoding overhead. You can use +/// [cobs::max_encoding_length] to calculate this size. +/// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from +/// the client. It is recommended to make this buffer larger to allow reading multiple +/// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should +/// at the very least be large enough to hold the maximum expected telecommand size in +/// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to +/// calculate this size. +/// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is +/// especially useful if the address and port are static for the server. Set to false by +/// default. +/// * `reuse_port` - Can be used to set the `SO_REUSEPORT` option on the raw socket. This is +/// especially useful if the address and port are static for the server. Set to false by +/// default. +#[derive(Debug, Copy, Clone)] +pub struct ServerConfig { + pub addr: SocketAddr, + pub inner_loop_delay: Duration, + pub tm_buffer_size: usize, + pub tc_buffer_size: usize, + pub reuse_addr: bool, + pub reuse_port: bool, +} + +impl ServerConfig { + pub fn new( + addr: SocketAddr, + inner_loop_delay: Duration, + tm_buffer_size: usize, + tc_buffer_size: usize, + ) -> Self { + Self { + addr, + inner_loop_delay, + tm_buffer_size, + tc_buffer_size, + reuse_addr: false, + reuse_port: false, + } + } +} + /// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// @@ -43,45 +99,27 @@ impl TcpTmtcInCobsServer { /// /// ## Parameter /// - /// * `addr` - Address of the TCP server. - /// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is - /// especially useful if the address and port are static for the server. - /// * `reuse_port` - Can be used to set the `SO_REUSEPORT` option on the raw socket. This is - /// especially useful if the address and port are static for the server. - /// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and - /// encoding of that data. This buffer should at large enough to hold the maximum expected - /// TM size in addition to the COBS encoding overhead. You can use - /// [cobs::max_encoding_length] to calculate this size. /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// then sent back to the client. - /// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from - /// the client. It is recommended to make this buffer larger to allow reading multiple - /// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should - /// at the very least be large enough to hold the maximum expected telecommand size in - /// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to - /// calculate this size. /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded /// to this TC receiver. pub fn new( - addr: &SocketAddr, - reuse_addr: bool, - reuse_port: bool, - tm_buffer_size: usize, + cfg: ServerConfig, tm_source: Box + Send>, - tc_buffer_size: usize, tc_receiver: Box + Send>, ) -> Result { Ok(Self { base: TcpTmtcServerBase::new( - addr, - reuse_addr, - reuse_port, - tm_buffer_size, + &cfg.addr, + cfg.inner_loop_delay, + cfg.reuse_addr, + cfg.reuse_port, + cfg.tm_buffer_size, tm_source, - tc_buffer_size, + cfg.tc_buffer_size, tc_receiver, )?, - tm_encoding_buffer: vec![0; max_encoding_length(tc_buffer_size)], + tm_encoding_buffer: vec![0; max_encoding_length(cfg.tc_buffer_size)], }) } @@ -112,35 +150,85 @@ impl TcpTmtcInCobsServer { let mut current_write_idx; let mut next_write_idx = 0; let (mut stream, addr) = self.base.listener.accept()?; + stream.set_nonblocking(true)?; connection_result.addr = Some(addr); current_write_idx = next_write_idx; - next_write_idx = 0; loop { - let read_len = stream.read(&mut self.base.tc_buffer[current_write_idx..])?; - if read_len > 0 { - current_write_idx += read_len; - if current_write_idx == self.base.tc_buffer.capacity() { - // Reader vec full, need to parse for packets. - connection_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( - &mut self.base.tc_buffer[..current_write_idx], - self.base.tc_receiver.as_mut(), - &mut next_write_idx, - ) - .map_err(|e| TcpTmtcError::TcError(e))?; - current_write_idx = next_write_idx; + let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]); + match read_result { + Ok(0) => { + // Connection closed by client. If any TC was read, parse for complete packets. + // After that, break the outer loop. + if current_write_idx > 0 { + self.handle_tc_parsing( + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + } + break; } - continue; + Ok(read_len) => { + current_write_idx += read_len; + // TC buffer is full, we must parse for complete packets now. + if current_write_idx == self.base.tc_buffer.capacity() { + self.handle_tc_parsing( + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + current_write_idx = next_write_idx; + } + } + Err(e) => match e.kind() { + // As per [TcpStream::set_read_timeout] documentation, this should work for + // both UNIX and Windows. + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { + println!("should be here.."); + self.handle_tc_parsing( + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + current_write_idx = next_write_idx; + if !self.handle_tm_sending(&mut connection_result, &mut stream)? { + // No TC read, no TM was sent, but the client has not disconnected. + // Perform an inner delay to avoid burning CPU time. + thread::sleep(self.base.inner_loop_delay); + } + } + _ => { + return Err(TcpTmtcError::Io(e)); + } + }, } - break; - } - if current_write_idx > 0 { - connection_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( - &mut self.base.tc_buffer[..current_write_idx], - self.base.tc_receiver.as_mut(), - &mut next_write_idx, - ) - .map_err(|e| TcpTmtcError::TcError(e))?; } + self.handle_tm_sending(&mut connection_result, &mut stream)?; + Ok(connection_result) + } + + fn handle_tc_parsing( + &mut self, + conn_result: &mut ConnectionResult, + current_write_idx: usize, + next_write_idx: &mut usize, + ) -> Result<(), TcpTmtcError> { + // Reader vec full, need to parse for packets. + conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut self.base.tc_buffer[..current_write_idx], + self.base.tc_receiver.as_mut(), + next_write_idx, + ) + .map_err(|e| TcpTmtcError::TcError(e))?; + Ok(()) + } + + fn handle_tm_sending( + &mut self, + conn_result: &mut ConnectionResult, + stream: &mut TcpStream, + ) -> Result> { + let mut tm_was_sent = false; loop { // Write TM until TM source is exhausted. For now, there is no limit for the amount // of TM written this way. @@ -149,10 +237,12 @@ impl TcpTmtcInCobsServer { .tm_source .retrieve_packet(&mut self.base.tm_buffer) .map_err(|e| TcpTmtcError::TmError(e))?; + if read_tm_len == 0 { - break; + return Ok(tm_was_sent); } - connection_result.num_sent_tms += 1; + tm_was_sent = true; + conn_result.num_sent_tms += 1; // Encode into COBS and sent to client. let mut current_idx = 0; @@ -166,7 +256,6 @@ impl TcpTmtcInCobsServer { current_idx += 1; stream.write_all(&self.tm_encoding_buffer[..current_idx])?; } - Ok(connection_result) } } @@ -239,7 +328,7 @@ mod tests { use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use cobs::encode; - use super::{parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer}; + use super::{parse_buffer_for_cobs_encoded_packets, ServerConfig, TcpTmtcInCobsServer}; const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; @@ -476,12 +565,8 @@ mod tests { tm_source: SyncTmSource, ) -> TcpTmtcInCobsServer<(), ()> { TcpTmtcInCobsServer::new( - addr, - false, - false, - 1024, + ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024), Box::new(tm_source), - 1024, Box::new(tc_receiver.clone()), ) .expect("TCP server generation failed") From 8582d226ec8f4ecf4a20189d1abd81689c477698 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 17 Sep 2023 02:35:08 +0200 Subject: [PATCH 34/68] improve API docs --- satrs-core/src/hal/std/tcp_server.rs | 52 +++++++++++++++++ .../src/hal/std/tcp_with_cobs_server.rs | 56 +------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 11dc9e1..9d6cc81 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -14,6 +14,58 @@ pub use crate::hal::std::tcp_with_cobs_server::{ parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer, }; +/// TCP configuration struct. +/// +/// ## Parameters +/// +/// * `addr` - Address of the TCP server. +/// * `inner_loop_delay` - If a client connects for a longer period, but no TC is received or +/// no TM needs to be sent, the TCP server will delay for the specified amount of time +/// to reduce CPU load. +/// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and +/// encoding of that data. This buffer should at large enough to hold the maximum expected +/// TM size in addition to the COBS encoding overhead. You can use +/// [cobs::max_encoding_length] to calculate this size. +/// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from +/// the client. It is recommended to make this buffer larger to allow reading multiple +/// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should +/// at the very least be large enough to hold the maximum expected telecommand size in +/// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to +/// calculate this size. +/// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is +/// especially useful if the address and port are static for the server. Set to false by +/// default. +/// * `reuse_port` - Can be used to set the `SO_REUSEPORT` option on the raw socket. This is +/// especially useful if the address and port are static for the server. Set to false by +/// default. +#[derive(Debug, Copy, Clone)] +pub struct ServerConfig { + pub addr: SocketAddr, + pub inner_loop_delay: Duration, + pub tm_buffer_size: usize, + pub tc_buffer_size: usize, + pub reuse_addr: bool, + pub reuse_port: bool, +} + +impl ServerConfig { + pub fn new( + addr: SocketAddr, + inner_loop_delay: Duration, + tm_buffer_size: usize, + tc_buffer_size: usize, + ) -> Self { + Self { + addr, + inner_loop_delay, + tm_buffer_size, + tc_buffer_size, + reuse_addr: false, + reuse_port: false, + } + } +} + #[derive(Error, Debug)] pub enum TcpTmtcError { #[error("TM retrieval error: {0}")] diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 983e57c..d7d3c57 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -3,7 +3,6 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use cobs::max_encoding_length; -use core::time::Duration; use std::io::Read; use std::io::Write; use std::net::SocketAddr; @@ -13,65 +12,13 @@ use std::println; use std::thread; use std::vec::Vec; -use crate::hal::std::tcp_server::TcpTmtcServerBase; +use crate::hal::std::tcp_server::{TcpTmtcServerBase, ServerConfig}; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; use super::tcp_server::ConnectionResult; use super::tcp_server::TcpTmtcError; -/// TCP configuration struct. -/// -/// ## Parameters -/// -/// * `addr` - Address of the TCP server. -/// * `inner_loop_delay` - If a client connects for a longer period, but no TC is received or -/// no TM needs to be sent, the TCP server will delay for the specified amount of time -/// to reduce CPU load. -/// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and -/// encoding of that data. This buffer should at large enough to hold the maximum expected -/// TM size in addition to the COBS encoding overhead. You can use -/// [cobs::max_encoding_length] to calculate this size. -/// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from -/// the client. It is recommended to make this buffer larger to allow reading multiple -/// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should -/// at the very least be large enough to hold the maximum expected telecommand size in -/// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to -/// calculate this size. -/// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is -/// especially useful if the address and port are static for the server. Set to false by -/// default. -/// * `reuse_port` - Can be used to set the `SO_REUSEPORT` option on the raw socket. This is -/// especially useful if the address and port are static for the server. Set to false by -/// default. -#[derive(Debug, Copy, Clone)] -pub struct ServerConfig { - pub addr: SocketAddr, - pub inner_loop_delay: Duration, - pub tm_buffer_size: usize, - pub tc_buffer_size: usize, - pub reuse_addr: bool, - pub reuse_port: bool, -} - -impl ServerConfig { - pub fn new( - addr: SocketAddr, - inner_loop_delay: Duration, - tm_buffer_size: usize, - tc_buffer_size: usize, - ) -> Self { - Self { - addr, - inner_loop_delay, - tm_buffer_size, - tc_buffer_size, - reuse_addr: false, - reuse_port: false, - } - } -} - /// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// @@ -99,6 +46,7 @@ impl TcpTmtcInCobsServer { /// /// ## Parameter /// + /// * `cfg` - Configuration of the server. /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// then sent back to the client. /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded From 079da20640dc064ade39146f81229d48962d73c6 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:11:01 +0200 Subject: [PATCH 35/68] holy shit, this actually worked --- satrs-core/src/hal/std/tcp_server.rs | 23 +- .../src/hal/std/tcp_with_cobs_server.rs | 276 ++++++++++++------ 2 files changed, 203 insertions(+), 96 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 9d6cc81..49592c0 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -85,7 +85,7 @@ pub struct ConnectionResult { pub num_sent_tms: u32, } -pub(crate) struct TcpTmtcServerBase { +pub(crate) struct TcpTmtcServerBase { pub(crate) listener: TcpListener, pub(crate) inner_loop_delay: Duration, pub(crate) tm_source: Box + Send>, @@ -94,31 +94,26 @@ pub(crate) struct TcpTmtcServerBase { pub(crate) tc_buffer: Vec, } -impl TcpTmtcServerBase { +impl TcpTmtcServerBase { pub(crate) fn new( - addr: &SocketAddr, - inner_loop_delay: Duration, - reuse_addr: bool, - reuse_port: bool, - tm_buffer_size: usize, + cfg: ServerConfig, tm_source: Box + Send>, - tc_buffer_size: usize, tc_receiver: Box + Send>, ) -> Result { // Create a TCP listener bound to two addresses. let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?; - socket.set_reuse_address(reuse_addr)?; - socket.set_reuse_port(reuse_port)?; - let addr = (*addr).into(); + socket.set_reuse_address(cfg.reuse_addr)?; + socket.set_reuse_port(cfg.reuse_port)?; + let addr = (cfg.addr).into(); socket.bind(&addr)?; socket.listen(128)?; Ok(Self { listener: socket.into(), - inner_loop_delay, + inner_loop_delay: cfg.inner_loop_delay, tm_source, - tm_buffer: vec![0; tm_buffer_size], + tm_buffer: vec![0; cfg.tm_buffer_size], tc_receiver, - tc_buffer: vec![0; tc_buffer_size], + tc_buffer: vec![0; cfg.tc_buffer_size], }) } diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index d7d3c57..3b1edbf 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -2,7 +2,7 @@ use alloc::boxed::Box; use alloc::vec; use cobs::decode_in_place; use cobs::encode; -use cobs::max_encoding_length; +use delegate::delegate; use std::io::Read; use std::io::Write; use std::net::SocketAddr; @@ -12,35 +12,126 @@ use std::println; use std::thread; use std::vec::Vec; -use crate::hal::std::tcp_server::{TcpTmtcServerBase, ServerConfig}; +use crate::hal::std::tcp_server::{ServerConfig, TcpTmtcServerBase}; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; use super::tcp_server::ConnectionResult; use super::tcp_server::TcpTmtcError; -/// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the -/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). -/// -/// TCP is stream oriented, so a client can read available telemetry using [std::io::Read] as well. -/// To allow flexibly specifying the telemetry sent back to clients, a generic TM abstraction -/// in form of the [TmPacketSource] trait is used. Telemetry will be encoded with the COBS -/// protocol using [cobs::encode] in addition to being wrapped with the sentinel value 0 as the -/// packet delimiter as well before being sent back to the client. Please note that the server -/// will send as much data as it can retrieve from the [TmPacketSource] in its current -/// implementation. -/// -/// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data -/// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full -/// packets even from a data stream which is split up. The server wil use the -/// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a -/// generic TC receiver. -pub struct TcpTmtcInCobsServer { - base: TcpTmtcServerBase, +pub trait TcpTcHandler { + fn handle_tc_parsing( + &mut self, + tc_buffer: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + conn_result: &mut ConnectionResult, + current_write_idx: usize, + next_write_idx: &mut usize, + ) -> Result<(), TcpTmtcError>; +} + +#[derive(Default)] +struct CobsTcParser {} + +impl TcpTcHandler for CobsTcParser { + fn handle_tc_parsing( + &mut self, + tc_buffer: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + conn_result: &mut ConnectionResult, + current_write_idx: usize, + next_write_idx: &mut usize, + ) -> Result<(), TcpTmtcError> { + // Reader vec full, need to parse for packets. + conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( + &mut tc_buffer[..current_write_idx], + tc_receiver, + next_write_idx, + ) + .map_err(|e| TcpTmtcError::TcError(e))?; + Ok(()) + } +} + +pub trait TcpTmHandler { + fn handle_tm_sending( + &mut self, + tm_buffer: &mut [u8], + tm_source: &mut dyn TmPacketSource, + conn_result: &mut ConnectionResult, + stream: &mut TcpStream, + ) -> Result>; +} + +struct CobsTmParser { tm_encoding_buffer: Vec, } -impl TcpTmtcInCobsServer { +impl CobsTmParser { + fn new(tm_buffer_size: usize) -> Self { + Self { + // The buffer should be large enough to hold the maximum expected TM size encoded with + // COBS. + tm_encoding_buffer: vec![0; cobs::max_encoding_length(tm_buffer_size)], + } + } +} + +impl TcpTmHandler for CobsTmParser { + fn handle_tm_sending( + &mut self, + tm_buffer: &mut [u8], + tm_source: &mut dyn TmPacketSource, + conn_result: &mut ConnectionResult, + stream: &mut TcpStream, + ) -> Result> { + let mut tm_was_sent = false; + loop { + // Write TM until TM source is exhausted. For now, there is no limit for the amount + // of TM written this way. + let read_tm_len = tm_source + .retrieve_packet(tm_buffer) + .map_err(|e| TcpTmtcError::TmError(e))?; + + if read_tm_len == 0 { + return Ok(tm_was_sent); + } + tm_was_sent = true; + conn_result.num_sent_tms += 1; + + // Encode into COBS and sent to client. + let mut current_idx = 0; + self.tm_encoding_buffer[current_idx] = 0; + current_idx += 1; + current_idx += encode( + &tm_buffer[..read_tm_len], + &mut self.tm_encoding_buffer[current_idx..], + ); + self.tm_encoding_buffer[current_idx] = 0; + current_idx += 1; + stream.write_all(&self.tm_encoding_buffer[..current_idx])?; + } + } +} + +pub struct TcpTmtcGenericServer< + TmError, + TcError, + TmHandler: TcpTmHandler, + TcHandler: TcpTcHandler, +> { + base: TcpTmtcServerBase, + tc_handler: TcHandler, + tm_handler: TmHandler, +} + +impl< + TmError: 'static, + TcError: 'static, + TmHandler: TcpTmHandler, + TcHandler: TcpTcHandler, + > TcpTmtcGenericServer +{ /// Create a new TMTC server which exchanges TMTC packets encoded with /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// @@ -53,21 +144,15 @@ impl TcpTmtcInCobsServer { /// to this TC receiver. pub fn new( cfg: ServerConfig, + tc_handler: TcHandler, + tm_handler: TmHandler, tm_source: Box + Send>, tc_receiver: Box + Send>, - ) -> Result { + ) -> Result, std::io::Error> { Ok(Self { - base: TcpTmtcServerBase::new( - &cfg.addr, - cfg.inner_loop_delay, - cfg.reuse_addr, - cfg.reuse_port, - cfg.tm_buffer_size, - tm_source, - cfg.tc_buffer_size, - tc_receiver, - )?, - tm_encoding_buffer: vec![0; max_encoding_length(cfg.tc_buffer_size)], + base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, + tc_handler, + tm_handler, // tmtc_handler: CobsTmtcParser::new(cfg.tm_buffer_size), }) } @@ -108,7 +193,9 @@ impl TcpTmtcInCobsServer { // Connection closed by client. If any TC was read, parse for complete packets. // After that, break the outer loop. if current_write_idx > 0 { - self.handle_tc_parsing( + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), &mut connection_result, current_write_idx, &mut next_write_idx, @@ -120,7 +207,9 @@ impl TcpTmtcInCobsServer { current_write_idx += read_len; // TC buffer is full, we must parse for complete packets now. if current_write_idx == self.base.tc_buffer.capacity() { - self.handle_tc_parsing( + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), &mut connection_result, current_write_idx, &mut next_write_idx, @@ -133,13 +222,21 @@ impl TcpTmtcInCobsServer { // both UNIX and Windows. std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { println!("should be here.."); - self.handle_tc_parsing( + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), &mut connection_result, current_write_idx, &mut next_write_idx, )?; current_write_idx = next_write_idx; - if !self.handle_tm_sending(&mut connection_result, &mut stream)? { + + if !self.tm_handler.handle_tm_sending( + &mut self.base.tm_buffer, + self.base.tm_source.as_mut(), + &mut connection_result, + &mut stream, + )? { // No TC read, no TM was sent, but the client has not disconnected. // Perform an inner delay to avoid burning CPU time. thread::sleep(self.base.inner_loop_delay); @@ -151,58 +248,73 @@ impl TcpTmtcInCobsServer { }, } } - self.handle_tm_sending(&mut connection_result, &mut stream)?; + self.tm_handler.handle_tm_sending( + &mut self.base.tm_buffer, + self.base.tm_source.as_mut(), + &mut connection_result, + &mut stream, + )?; Ok(connection_result) } +} - fn handle_tc_parsing( - &mut self, - conn_result: &mut ConnectionResult, - current_write_idx: usize, - next_write_idx: &mut usize, - ) -> Result<(), TcpTmtcError> { - // Reader vec full, need to parse for packets. - conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( - &mut self.base.tc_buffer[..current_write_idx], - self.base.tc_receiver.as_mut(), - next_write_idx, - ) - .map_err(|e| TcpTmtcError::TcError(e))?; - Ok(()) +/// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the +/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). +/// +/// TCP is stream oriented, so a client can read available telemetry using [std::io::Read] as well. +/// To allow flexibly specifying the telemetry sent back to clients, a generic TM abstraction +/// in form of the [TmPacketSource] trait is used. Telemetry will be encoded with the COBS +/// protocol using [cobs::encode] in addition to being wrapped with the sentinel value 0 as the +/// packet delimiter as well before being sent back to the client. Please note that the server +/// will send as much data as it can retrieve from the [TmPacketSource] in its current +/// implementation. +/// +/// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data +/// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full +/// packets even from a data stream which is split up. The server wil use the +/// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a +/// generic TC receiver. +pub struct TcpTmtcInCobsServer { + generic_server: TcpTmtcGenericServer, +} + +impl TcpTmtcInCobsServer { + pub fn new( + cfg: ServerConfig, + tm_source: Box + Send>, + tc_receiver: Box + Send>, + ) -> Result> { + Ok(Self { + generic_server: TcpTmtcGenericServer::new( + cfg, + CobsTcParser::default(), + CobsTmParser::new(cfg.tm_buffer_size), + tm_source, + tc_receiver, + )?, + }) } - fn handle_tm_sending( - &mut self, - conn_result: &mut ConnectionResult, - stream: &mut TcpStream, - ) -> Result> { - let mut tm_was_sent = false; - loop { - // Write TM until TM source is exhausted. For now, there is no limit for the amount - // of TM written this way. - let read_tm_len = self - .base - .tm_source - .retrieve_packet(&mut self.base.tm_buffer) - .map_err(|e| TcpTmtcError::TmError(e))?; + delegate! { + to self.generic_server { + pub fn listener(&mut self) -> &mut TcpListener; - if read_tm_len == 0 { - return Ok(tm_was_sent); - } - tm_was_sent = true; - conn_result.num_sent_tms += 1; + /// Can be used to retrieve the local assigned address of the TCP server. This is especially + /// useful if using the port number 0 for OS auto-assignment. + pub fn local_addr(&self) -> std::io::Result; - // Encode into COBS and sent to client. - let mut current_idx = 0; - self.tm_encoding_buffer[current_idx] = 0; - current_idx += 1; - current_idx += encode( - &self.base.tm_buffer[..read_tm_len], - &mut self.tm_encoding_buffer[current_idx..], - ); - self.tm_encoding_buffer[current_idx] = 0; - current_idx += 1; - stream.write_all(&self.tm_encoding_buffer[..current_idx])?; + /// This call is used to handle the next connection to a client. Right now, it performs + /// the following steps: + /// + /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API + /// until a client connects. + /// 2. It reads all the telecommands from the client, which are expected to be COBS + /// encoded packets. + /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve + /// from the user specified [TmPacketSource] back to the client. + pub fn handle_next_connection( + &mut self, + ) -> Result>; } } } From b622c3871a5b57056e0ec7f0206709b9686740fa Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:11:44 +0200 Subject: [PATCH 36/68] remove dump printout --- satrs-core/src/hal/std/tcp_with_cobs_server.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 3b1edbf..c013c4c 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -152,7 +152,7 @@ impl< Ok(Self { base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, tc_handler, - tm_handler, // tmtc_handler: CobsTmtcParser::new(cfg.tm_buffer_size), + tm_handler, }) } @@ -221,7 +221,6 @@ impl< // As per [TcpStream::set_read_timeout] documentation, this should work for // both UNIX and Windows. std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { - println!("should be here.."); self.tc_handler.handle_tc_parsing( &mut self.base.tc_buffer, self.base.tc_receiver.as_mut(), From 5aa339286acc97084dcddf284e38d113a2bf7bfe Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:18:01 +0200 Subject: [PATCH 37/68] move generic server --- satrs-core/src/hal/std/tcp_server.rs | 167 ++++++++++++++++- .../src/hal/std/tcp_with_cobs_server.rs | 173 +----------------- 2 files changed, 169 insertions(+), 171 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 49592c0..436cbfc 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -3,8 +3,10 @@ use alloc::vec; use alloc::{boxed::Box, vec::Vec}; use core::time::Duration; use socket2::{Domain, Socket, Type}; -use std::net::SocketAddr; +use std::io::Read; use std::net::TcpListener; +use std::net::{SocketAddr, TcpStream}; +use std::thread; use crate::tmtc::{ReceivesTc, TmPacketSource}; use thiserror::Error; @@ -85,6 +87,169 @@ pub struct ConnectionResult { pub num_sent_tms: u32, } +pub trait TcpTcHandler { + fn handle_tc_parsing( + &mut self, + tc_buffer: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + conn_result: &mut ConnectionResult, + current_write_idx: usize, + next_write_idx: &mut usize, + ) -> Result<(), TcpTmtcError>; +} +pub trait TcpTmHandler { + fn handle_tm_sending( + &mut self, + tm_buffer: &mut [u8], + tm_source: &mut dyn TmPacketSource, + conn_result: &mut ConnectionResult, + stream: &mut TcpStream, + ) -> Result>; +} + +pub struct TcpTmtcGenericServer< + TmError, + TcError, + TmHandler: TcpTmHandler, + TcHandler: TcpTcHandler, +> { + base: TcpTmtcServerBase, + tc_handler: TcHandler, + tm_handler: TmHandler, +} + +impl< + TmError: 'static, + TcError: 'static, + TmHandler: TcpTmHandler, + TcHandler: TcpTcHandler, + > TcpTmtcGenericServer +{ + /// Create a new TMTC server which exchanges TMTC packets encoded with + /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). + /// + /// ## Parameter + /// + /// * `cfg` - Configuration of the server. + /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are + /// then sent back to the client. + /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded + /// to this TC receiver. + pub fn new( + cfg: ServerConfig, + tc_handler: TcHandler, + tm_handler: TmHandler, + tm_source: Box + Send>, + tc_receiver: Box + Send>, + ) -> Result, std::io::Error> { + Ok(Self { + base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, + tc_handler, + tm_handler, + }) + } + + /// Retrieve the internal [TcpListener] class. + pub fn listener(&mut self) -> &mut TcpListener { + self.base.listener() + } + + /// Can be used to retrieve the local assigned address of the TCP server. This is especially + /// useful if using the port number 0 for OS auto-assignment. + pub fn local_addr(&self) -> std::io::Result { + self.base.local_addr() + } + + /// This call is used to handle the next connection to a client. Right now, it performs + /// the following steps: + /// + /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API + /// until a client connects. + /// 2. It reads all the telecommands from the client, which are expected to be COBS + /// encoded packets. + /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve + /// from the user specified [TmPacketSource] back to the client. + pub fn handle_next_connection( + &mut self, + ) -> Result> { + let mut connection_result = ConnectionResult::default(); + let mut current_write_idx; + let mut next_write_idx = 0; + let (mut stream, addr) = self.base.listener.accept()?; + stream.set_nonblocking(true)?; + connection_result.addr = Some(addr); + current_write_idx = next_write_idx; + loop { + let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]); + match read_result { + Ok(0) => { + // Connection closed by client. If any TC was read, parse for complete packets. + // After that, break the outer loop. + if current_write_idx > 0 { + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + } + break; + } + Ok(read_len) => { + current_write_idx += read_len; + // TC buffer is full, we must parse for complete packets now. + if current_write_idx == self.base.tc_buffer.capacity() { + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + current_write_idx = next_write_idx; + } + } + Err(e) => match e.kind() { + // As per [TcpStream::set_read_timeout] documentation, this should work for + // both UNIX and Windows. + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { + self.tc_handler.handle_tc_parsing( + &mut self.base.tc_buffer, + self.base.tc_receiver.as_mut(), + &mut connection_result, + current_write_idx, + &mut next_write_idx, + )?; + current_write_idx = next_write_idx; + + if !self.tm_handler.handle_tm_sending( + &mut self.base.tm_buffer, + self.base.tm_source.as_mut(), + &mut connection_result, + &mut stream, + )? { + // No TC read, no TM was sent, but the client has not disconnected. + // Perform an inner delay to avoid burning CPU time. + thread::sleep(self.base.inner_loop_delay); + } + } + _ => { + return Err(TcpTmtcError::Io(e)); + } + }, + } + } + self.tm_handler.handle_tm_sending( + &mut self.base.tm_buffer, + self.base.tm_source.as_mut(), + &mut connection_result, + &mut stream, + )?; + Ok(connection_result) + } +} + pub(crate) struct TcpTmtcServerBase { pub(crate) listener: TcpListener, pub(crate) inner_loop_delay: Duration, diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index c013c4c..ad1913a 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -3,32 +3,18 @@ use alloc::vec; use cobs::decode_in_place; use cobs::encode; use delegate::delegate; -use std::io::Read; use std::io::Write; use std::net::SocketAddr; use std::net::TcpListener; use std::net::TcpStream; -use std::println; -use std::thread; use std::vec::Vec; -use crate::hal::std::tcp_server::{ServerConfig, TcpTmtcServerBase}; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; -use super::tcp_server::ConnectionResult; -use super::tcp_server::TcpTmtcError; - -pub trait TcpTcHandler { - fn handle_tc_parsing( - &mut self, - tc_buffer: &mut [u8], - tc_receiver: &mut dyn ReceivesTc, - conn_result: &mut ConnectionResult, - current_write_idx: usize, - next_write_idx: &mut usize, - ) -> Result<(), TcpTmtcError>; -} +use crate::hal::std::tcp_server::{ + ConnectionResult, ServerConfig, TcpTcHandler, TcpTmHandler, TcpTmtcError, TcpTmtcGenericServer, +}; #[derive(Default)] struct CobsTcParser {} @@ -53,16 +39,6 @@ impl TcpTcHandler for CobsTcParser { } } -pub trait TcpTmHandler { - fn handle_tm_sending( - &mut self, - tm_buffer: &mut [u8], - tm_source: &mut dyn TmPacketSource, - conn_result: &mut ConnectionResult, - stream: &mut TcpStream, - ) -> Result>; -} - struct CobsTmParser { tm_encoding_buffer: Vec, } @@ -114,149 +90,6 @@ impl TcpTmHandler for CobsTmParser { } } -pub struct TcpTmtcGenericServer< - TmError, - TcError, - TmHandler: TcpTmHandler, - TcHandler: TcpTcHandler, -> { - base: TcpTmtcServerBase, - tc_handler: TcHandler, - tm_handler: TmHandler, -} - -impl< - TmError: 'static, - TcError: 'static, - TmHandler: TcpTmHandler, - TcHandler: TcpTcHandler, - > TcpTmtcGenericServer -{ - /// Create a new TMTC server which exchanges TMTC packets encoded with - /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). - /// - /// ## Parameter - /// - /// * `cfg` - Configuration of the server. - /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are - /// then sent back to the client. - /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded - /// to this TC receiver. - pub fn new( - cfg: ServerConfig, - tc_handler: TcHandler, - tm_handler: TmHandler, - tm_source: Box + Send>, - tc_receiver: Box + Send>, - ) -> Result, std::io::Error> { - Ok(Self { - base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, - tc_handler, - tm_handler, - }) - } - - /// Retrieve the internal [TcpListener] class. - pub fn listener(&mut self) -> &mut TcpListener { - self.base.listener() - } - - /// Can be used to retrieve the local assigned address of the TCP server. This is especially - /// useful if using the port number 0 for OS auto-assignment. - pub fn local_addr(&self) -> std::io::Result { - self.base.local_addr() - } - - /// This call is used to handle the next connection to a client. Right now, it performs - /// the following steps: - /// - /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API - /// until a client connects. - /// 2. It reads all the telecommands from the client, which are expected to be COBS - /// encoded packets. - /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve - /// from the user specified [TmPacketSource] back to the client. - pub fn handle_next_connection( - &mut self, - ) -> Result> { - let mut connection_result = ConnectionResult::default(); - let mut current_write_idx; - let mut next_write_idx = 0; - let (mut stream, addr) = self.base.listener.accept()?; - stream.set_nonblocking(true)?; - connection_result.addr = Some(addr); - current_write_idx = next_write_idx; - loop { - let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]); - match read_result { - Ok(0) => { - // Connection closed by client. If any TC was read, parse for complete packets. - // After that, break the outer loop. - if current_write_idx > 0 { - self.tc_handler.handle_tc_parsing( - &mut self.base.tc_buffer, - self.base.tc_receiver.as_mut(), - &mut connection_result, - current_write_idx, - &mut next_write_idx, - )?; - } - break; - } - Ok(read_len) => { - current_write_idx += read_len; - // TC buffer is full, we must parse for complete packets now. - if current_write_idx == self.base.tc_buffer.capacity() { - self.tc_handler.handle_tc_parsing( - &mut self.base.tc_buffer, - self.base.tc_receiver.as_mut(), - &mut connection_result, - current_write_idx, - &mut next_write_idx, - )?; - current_write_idx = next_write_idx; - } - } - Err(e) => match e.kind() { - // As per [TcpStream::set_read_timeout] documentation, this should work for - // both UNIX and Windows. - std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { - self.tc_handler.handle_tc_parsing( - &mut self.base.tc_buffer, - self.base.tc_receiver.as_mut(), - &mut connection_result, - current_write_idx, - &mut next_write_idx, - )?; - current_write_idx = next_write_idx; - - if !self.tm_handler.handle_tm_sending( - &mut self.base.tm_buffer, - self.base.tm_source.as_mut(), - &mut connection_result, - &mut stream, - )? { - // No TC read, no TM was sent, but the client has not disconnected. - // Perform an inner delay to avoid burning CPU time. - thread::sleep(self.base.inner_loop_delay); - } - } - _ => { - return Err(TcpTmtcError::Io(e)); - } - }, - } - } - self.tm_handler.handle_tm_sending( - &mut self.base.tm_buffer, - self.base.tm_source.as_mut(), - &mut connection_result, - &mut stream, - )?; - Ok(connection_result) - } -} - /// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// From 047256f2f896bcb20af0874ca60f1db0244210d9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:45:13 +0200 Subject: [PATCH 38/68] finishing up --- satrs-core/src/hal/std/tcp_server.rs | 69 ++++++++++++++----- .../src/hal/std/tcp_with_cobs_server.rs | 47 +++++++------ 2 files changed, 73 insertions(+), 43 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 436cbfc..3e75784 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -13,7 +13,7 @@ use thiserror::Error; // Re-export the TMTC in COBS server. pub use crate::hal::std::tcp_with_cobs_server::{ - parse_buffer_for_cobs_encoded_packets, TcpTmtcInCobsServer, + parse_buffer_for_cobs_encoded_packets, CobsTcParser, CobsTmSender, TcpTmtcInCobsServer, }; /// TCP configuration struct. @@ -87,7 +87,10 @@ pub struct ConnectionResult { pub num_sent_tms: u32, } -pub trait TcpTcHandler { +/// Generic parser abstraction for an object which can parse for telecommands given a raw +/// bytestream received from a TCP socket and send them to a generic [ReceivesTc] telecommand +/// receiver. This allows different encoding schemes for telecommands. +pub trait TcpTcParser { fn handle_tc_parsing( &mut self, tc_buffer: &mut [u8], @@ -97,7 +100,12 @@ pub trait TcpTcHandler { next_write_idx: &mut usize, ) -> Result<(), TcpTmtcError>; } -pub trait TcpTmHandler { + +/// Generic sender abstraction for an object which can pull telemetry from a given TM source +/// using a [TmPacketSource] and then send them back to a client using a given [TcpStream]. +/// The concrete implementation can also perform any encoding steps which are necessary before +/// sending back the data to a client. +pub trait TcpTmSender { fn handle_tm_sending( &mut self, tm_buffer: &mut [u8], @@ -107,11 +115,28 @@ pub trait TcpTmHandler { ) -> Result>; } +/// TCP TMTC server implementation for exchange of generic TMTC packets in a generic way which +/// stays agnostic to the encoding scheme and format used for both telecommands and telemetry. +/// +/// This server implements a generic TMTC handling logic and allows modifying its behaviour +/// through the following 4 core abstractions: +/// +/// 1. [TcpTcParser] to parse for telecommands from the raw bytestream received from a client. +/// 2. Parsed telecommands will be sent to the [ReceivesTc] telecommand receiver. +/// 3. [TcpTmSender] to send telemetry pulled from a TM source back to the client. +/// 4. [TmPacketSource] as a generic TM source used by the [TcpTmSender]. +/// +/// It is possible to specify custom abstractions to build a dedicated TCP TMTC server without +/// having to re-implement common logic. +/// +/// Currently, this framework offers the following concrete implementations: +/// +/// 1. [TcpTmtcInCobsServer] to exchange TMTC wrapped inside the COBS framing protocol. pub struct TcpTmtcGenericServer< TmError, TcError, - TmHandler: TcpTmHandler, - TcHandler: TcpTcHandler, + TmHandler: TcpTmSender, + TcHandler: TcpTcParser, > { base: TcpTmtcServerBase, tc_handler: TcHandler, @@ -121,31 +146,33 @@ pub struct TcpTmtcGenericServer< impl< TmError: 'static, TcError: 'static, - TmHandler: TcpTmHandler, - TcHandler: TcpTcHandler, - > TcpTmtcGenericServer + TmSender: TcpTmSender, + TcParser: TcpTcParser, + > TcpTmtcGenericServer { - /// Create a new TMTC server which exchanges TMTC packets encoded with - /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). + /// Create a new generic TMTC server instance. /// /// ## Parameter /// /// * `cfg` - Configuration of the server. + /// * `tc_parser` - Parser which extracts telecommands from the raw bytestream received from + /// the client. + /// * `tm_sender` - Sends back telemetry to the client using the specified TM source. /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// then sent back to the client. /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded /// to this TC receiver. pub fn new( cfg: ServerConfig, - tc_handler: TcHandler, - tm_handler: TmHandler, + tc_parser: TcParser, + tm_sender: TmSender, tm_source: Box + Send>, tc_receiver: Box + Send>, - ) -> Result, std::io::Error> { + ) -> Result, std::io::Error> { Ok(Self { base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, - tc_handler, - tm_handler, + tc_handler: tc_parser, + tm_handler: tm_sender, }) } @@ -165,10 +192,14 @@ impl< /// /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API /// until a client connects. - /// 2. It reads all the telecommands from the client, which are expected to be COBS - /// encoded packets. - /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve - /// from the user specified [TmPacketSource] back to the client. + /// 2. It reads all the telecommands from the client and parses all received data using the + /// user specified [TcpTcParser]. + /// 3. After reading and parsing all telecommands, it sends back all telemetry using the + /// user specified [TcpTmSender]. + /// + /// The server will delay for a user-specified period if the client connects to the server + /// for prolonged periods and there is no traffic for the server. This is the case if the + /// client does not send any telecommands and no telemetry needs to be sent back to the client. pub fn handle_next_connection( &mut self, ) -> Result> { diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index ad1913a..ec39615 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -13,13 +13,13 @@ use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; use crate::hal::std::tcp_server::{ - ConnectionResult, ServerConfig, TcpTcHandler, TcpTmHandler, TcpTmtcError, TcpTmtcGenericServer, + ConnectionResult, ServerConfig, TcpTcParser, TcpTmSender, TcpTmtcError, TcpTmtcGenericServer, }; #[derive(Default)] -struct CobsTcParser {} +pub struct CobsTcParser {} -impl TcpTcHandler for CobsTcParser { +impl TcpTcParser for CobsTcParser { fn handle_tc_parsing( &mut self, tc_buffer: &mut [u8], @@ -39,11 +39,11 @@ impl TcpTcHandler for CobsTcParser { } } -struct CobsTmParser { +pub struct CobsTmSender { tm_encoding_buffer: Vec, } -impl CobsTmParser { +impl CobsTmSender { fn new(tm_buffer_size: usize) -> Self { Self { // The buffer should be large enough to hold the maximum expected TM size encoded with @@ -53,7 +53,7 @@ impl CobsTmParser { } } -impl TcpTmHandler for CobsTmParser { +impl TcpTmSender for CobsTmSender { fn handle_tm_sending( &mut self, tm_buffer: &mut [u8], @@ -93,13 +93,10 @@ impl TcpTmHandler for CobsTmParser { /// TCP TMTC server implementation for exchange of generic TMTC packets which are framed with the /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// -/// TCP is stream oriented, so a client can read available telemetry using [std::io::Read] as well. -/// To allow flexibly specifying the telemetry sent back to clients, a generic TM abstraction -/// in form of the [TmPacketSource] trait is used. Telemetry will be encoded with the COBS -/// protocol using [cobs::encode] in addition to being wrapped with the sentinel value 0 as the -/// packet delimiter as well before being sent back to the client. Please note that the server -/// will send as much data as it can retrieve from the [TmPacketSource] in its current -/// implementation. +/// Telemetry will be encoded with the COBS protocol using [cobs::encode] in addition to being +/// wrapped with the sentinel value 0 as the packet delimiter as well before being sent back to +/// the client. Please note that the server will send as much data as it can retrieve from the +/// [TmPacketSource] in its current implementation. /// /// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data /// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full @@ -107,10 +104,20 @@ impl TcpTmHandler for CobsTmParser { /// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a /// generic TC receiver. pub struct TcpTmtcInCobsServer { - generic_server: TcpTmtcGenericServer, + generic_server: TcpTmtcGenericServer, } impl TcpTmtcInCobsServer { + /// Create a new TCP TMTC server which exchanges TMTC packets encoded with + /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). + /// + /// ## Parameter + /// + /// * `cfg` - Configuration of the server. + /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are + /// then sent back to the client. + /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded + /// to this TC receiver. pub fn new( cfg: ServerConfig, tm_source: Box + Send>, @@ -120,7 +127,7 @@ impl TcpTmtcInCobsServer { generic_server: TcpTmtcGenericServer::new( cfg, CobsTcParser::default(), - CobsTmParser::new(cfg.tm_buffer_size), + CobsTmSender::new(cfg.tm_buffer_size), tm_source, tc_receiver, )?, @@ -135,15 +142,7 @@ impl TcpTmtcInCobsServer { /// useful if using the port number 0 for OS auto-assignment. pub fn local_addr(&self) -> std::io::Result; - /// This call is used to handle the next connection to a client. Right now, it performs - /// the following steps: - /// - /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API - /// until a client connects. - /// 2. It reads all the telecommands from the client, which are expected to be COBS - /// encoded packets. - /// 3. After reading and parsing all telecommands, it sends back all telemetry it can retrieve - /// from the user specified [TmPacketSource] back to the client. + /// Delegation to the [TcpTmtcGenericServer::handle_next_connection] call. pub fn handle_next_connection( &mut self, ) -> Result>; From 3207be7ffe1388f47f200e02b999accf7e3df2e9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:51:38 +0200 Subject: [PATCH 39/68] great, work on the CCSDS TCP server can start.. --- satrs-core/src/hal/std/tcp_server.rs | 12 +++++------- satrs-core/src/hal/std/tcp_with_cobs_server.rs | 8 +++++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index 3e75784..befcfc0 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -16,7 +16,7 @@ pub use crate::hal::std::tcp_with_cobs_server::{ parse_buffer_for_cobs_encoded_packets, CobsTcParser, CobsTmSender, TcpTmtcInCobsServer, }; -/// TCP configuration struct. +/// Configuration struct for the generic TCP TMTC server /// /// ## Parameters /// @@ -26,14 +26,12 @@ pub use crate::hal::std::tcp_with_cobs_server::{ /// to reduce CPU load. /// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and /// encoding of that data. This buffer should at large enough to hold the maximum expected -/// TM size in addition to the COBS encoding overhead. You can use -/// [cobs::max_encoding_length] to calculate this size. +/// TM size read from the packet source. /// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from /// the client. It is recommended to make this buffer larger to allow reading multiple -/// consecutive packets as well, for example by using 4096 or 8192 byte. The buffer should -/// at the very least be large enough to hold the maximum expected telecommand size in -/// addition to its COBS encoding overhead. You can use [cobs::max_encoding_length] to -/// calculate this size. +/// consecutive packets as well, for example by using common buffer sizes like 4096 or 8192 +/// byte. The buffer should at the very least be large enough to hold the maximum expected +/// telecommand size. /// * `reuse_addr` - Can be used to set the `SO_REUSEADDR` option on the raw socket. This is /// especially useful if the address and port are static for the server. Set to false by /// default. diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index ec39615..5eabd69 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -16,6 +16,7 @@ use crate::hal::std::tcp_server::{ ConnectionResult, ServerConfig, TcpTcParser, TcpTmSender, TcpTmtcError, TcpTmtcGenericServer, }; +/// Concrete [TcpTcParser] implementation for the [TcpTmtcInCobsServer]. #[derive(Default)] pub struct CobsTcParser {} @@ -39,6 +40,7 @@ impl TcpTcParser for CobsTcParser { } } +/// Concrete [TcpTmSender] implementation for the [TcpTmtcInCobsServer]. pub struct CobsTmSender { tm_encoding_buffer: Vec, } @@ -116,8 +118,8 @@ impl TcpTmtcInCobsServer { /// * `cfg` - Configuration of the server. /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// then sent back to the client. - /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded - /// to this TC receiver. + /// * `tc_receiver` - Any received telecommands which were decoded successfully will be + /// forwarded to this TC receiver. pub fn new( cfg: ServerConfig, tm_source: Box + Send>, @@ -151,7 +153,7 @@ impl TcpTmtcInCobsServer { } /// This function parses a given buffer for COBS encoded packets. The packet structure is -/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter. +/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: /// /// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 /// From d42999d2adce5dffb864dc082c6db43c16d83f6b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 00:57:25 +0200 Subject: [PATCH 40/68] thats enough for today --- satrs-core/src/hal/std/mod.rs | 1 + satrs-core/src/hal/std/tcp_server.rs | 1 + .../src/hal/std/tcp_spacepackets_server.rs | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/satrs-core/src/hal/std/mod.rs b/satrs-core/src/hal/std/mod.rs index 7ac107b..17ec012 100644 --- a/satrs-core/src/hal/std/mod.rs +++ b/satrs-core/src/hal/std/mod.rs @@ -2,4 +2,5 @@ pub mod tcp_server; pub mod udp_server; +mod tcp_spacepackets_server; mod tcp_with_cobs_server; diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index befcfc0..c2d4869 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -12,6 +12,7 @@ use crate::tmtc::{ReceivesTc, TmPacketSource}; use thiserror::Error; // Re-export the TMTC in COBS server. +pub use crate::hal::std::tcp_spacepackets_server::parse_buffer_for_ccsds_space_packets; pub use crate::hal::std::tcp_with_cobs_server::{ parse_buffer_for_cobs_encoded_packets, CobsTcParser, CobsTmSender, TcpTmtcInCobsServer, }; diff --git a/satrs-core/src/hal/std/tcp_spacepackets_server.rs b/satrs-core/src/hal/std/tcp_spacepackets_server.rs index e69de29..e541602 100644 --- a/satrs-core/src/hal/std/tcp_spacepackets_server.rs +++ b/satrs-core/src/hal/std/tcp_spacepackets_server.rs @@ -0,0 +1,32 @@ +use crate::tmtc::ReceivesTc; + +pub trait ApidLookup { + fn validate(&self, apid: u16) -> bool; +} +/// This function parses a given buffer for COBS encoded packets. The packet structure is +/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: +/// +/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 +/// +/// This function is also able to deal with broken tail packets at the end. If broken tail +/// packets are detected, they are moved to the front of the buffer, and the write index for +/// future write operations will be written to the `next_write_idx` argument. +/// +/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. +pub fn parse_buffer_for_ccsds_space_packets( + buf: &mut [u8], + apid_lookup: &dyn ApidLookup, + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let mut start_index_packet = 0; + let mut start_found = false; + let mut last_byte = false; + let mut packets_found = 0; + for i in 0..buf.len() { + todo!(); + } + // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. + if start_index_packet > 0 && start_found && packets_found > 0 {} + Ok(packets_found) +} From 54bc37b0861252e1262cc27b4eb70a1f5a5b2753 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 14:57:27 +0200 Subject: [PATCH 41/68] fix clippy --- satrs-core/src/hal/std/tcp_spacepackets_server.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_spacepackets_server.rs b/satrs-core/src/hal/std/tcp_spacepackets_server.rs index e541602..2509db1 100644 --- a/satrs-core/src/hal/std/tcp_spacepackets_server.rs +++ b/satrs-core/src/hal/std/tcp_spacepackets_server.rs @@ -15,18 +15,14 @@ pub trait ApidLookup { /// The parser will write all packets which were decoded successfully to the given `tc_receiver`. pub fn parse_buffer_for_ccsds_space_packets( buf: &mut [u8], - apid_lookup: &dyn ApidLookup, - tc_receiver: &mut dyn ReceivesTc, - next_write_idx: &mut usize, + _apid_lookup: &dyn ApidLookup, + _tc_receiver: &mut dyn ReceivesTc, + _next_write_idx: &mut usize, ) -> Result { - let mut start_index_packet = 0; - let mut start_found = false; - let mut last_byte = false; - let mut packets_found = 0; - for i in 0..buf.len() { + let packets_found = 0; + for _ in 0..buf.len() { todo!(); } // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. - if start_index_packet > 0 && start_found && packets_found > 0 {} Ok(packets_found) } From 7536e107da5306c5bc0c7edb1e99ff95413c5a61 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 15:01:40 +0200 Subject: [PATCH 42/68] clippy fix --- satrs-example/src/tmtc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-example/src/tmtc.rs b/satrs-example/src/tmtc.rs index 188a59d..5d2ea5e 100644 --- a/satrs-example/src/tmtc.rs +++ b/satrs-example/src/tmtc.rs @@ -1,5 +1,5 @@ use log::{info, warn}; -use satrs_core::hal::host::udp_server::{ReceiveResult, UdpTcServer}; +use satrs_core::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use std::net::SocketAddr; use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError}; use std::thread; From 88a5a390d93c0a3588776839808f60a6f773d4b8 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 15:59:51 +0200 Subject: [PATCH 43/68] new parser module --- satrs-core/src/hal/std/tcp_server.rs | 5 +- .../src/hal/std/tcp_spacepackets_server.rs | 27 ---- .../src/hal/std/tcp_with_cobs_server.rs | 54 +------- satrs-core/src/lib.rs | 1 + satrs-core/src/parsers.rs | 131 ++++++++++++++++++ 5 files changed, 134 insertions(+), 84 deletions(-) create mode 100644 satrs-core/src/parsers.rs diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index c2d4869..f1f5c47 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -12,10 +12,7 @@ use crate::tmtc::{ReceivesTc, TmPacketSource}; use thiserror::Error; // Re-export the TMTC in COBS server. -pub use crate::hal::std::tcp_spacepackets_server::parse_buffer_for_ccsds_space_packets; -pub use crate::hal::std::tcp_with_cobs_server::{ - parse_buffer_for_cobs_encoded_packets, CobsTcParser, CobsTmSender, TcpTmtcInCobsServer, -}; +pub use crate::hal::std::tcp_with_cobs_server::{CobsTcParser, CobsTmSender, TcpTmtcInCobsServer}; /// Configuration struct for the generic TCP TMTC server /// diff --git a/satrs-core/src/hal/std/tcp_spacepackets_server.rs b/satrs-core/src/hal/std/tcp_spacepackets_server.rs index 2509db1..8b13789 100644 --- a/satrs-core/src/hal/std/tcp_spacepackets_server.rs +++ b/satrs-core/src/hal/std/tcp_spacepackets_server.rs @@ -1,28 +1 @@ -use crate::tmtc::ReceivesTc; -pub trait ApidLookup { - fn validate(&self, apid: u16) -> bool; -} -/// This function parses a given buffer for COBS encoded packets. The packet structure is -/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: -/// -/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 -/// -/// This function is also able to deal with broken tail packets at the end. If broken tail -/// packets are detected, they are moved to the front of the buffer, and the write index for -/// future write operations will be written to the `next_write_idx` argument. -/// -/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. -pub fn parse_buffer_for_ccsds_space_packets( - buf: &mut [u8], - _apid_lookup: &dyn ApidLookup, - _tc_receiver: &mut dyn ReceivesTc, - _next_write_idx: &mut usize, -) -> Result { - let packets_found = 0; - for _ in 0..buf.len() { - todo!(); - } - // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. - Ok(packets_found) -} diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 5eabd69..a540a71 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -1,6 +1,5 @@ use alloc::boxed::Box; use alloc::vec; -use cobs::decode_in_place; use cobs::encode; use delegate::delegate; use std::io::Write; @@ -9,6 +8,7 @@ use std::net::TcpListener; use std::net::TcpStream; use std::vec::Vec; +use crate::parsers::parse_buffer_for_cobs_encoded_packets; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; @@ -152,58 +152,6 @@ impl TcpTmtcInCobsServer { } } -/// This function parses a given buffer for COBS encoded packets. The packet structure is -/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: -/// -/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 -/// -/// This function is also able to deal with broken tail packets at the end. If broken tail -/// packets are detected, they are moved to the front of the buffer, and the write index for -/// future write operations will be written to the `next_write_idx` argument. -/// -/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. -pub fn parse_buffer_for_cobs_encoded_packets( - buf: &mut [u8], - tc_receiver: &mut dyn ReceivesTc, - next_write_idx: &mut usize, -) -> Result { - let mut start_index_packet = 0; - let mut start_found = false; - let mut last_byte = false; - let mut packets_found = 0; - for i in 0..buf.len() { - if i == buf.len() - 1 { - last_byte = true; - } - if buf[i] == 0 { - if !start_found && !last_byte && buf[i + 1] == 0 { - // Special case: Consecutive sentinel values or all zeroes. - // Skip. - continue; - } - if start_found { - let decode_result = decode_in_place(&mut buf[start_index_packet..i]); - if let Ok(packet_len) = decode_result { - packets_found += 1; - tc_receiver - .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?; - } - start_found = false; - } else { - start_index_packet = i + 1; - start_found = true; - } - } - } - // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. - if start_index_packet > 0 && start_found && packets_found > 0 { - let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); - first_seg[..last_seg.len()].copy_from_slice(last_seg); - *next_write_idx = last_seg.len(); - } - Ok(packets_found) -} - #[cfg(test)] mod tests { use core::{ diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 43c8cfb..50e9d42 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -33,6 +33,7 @@ pub mod hk; pub mod mode; pub mod objects; pub mod params; +pub mod parsers; pub mod pool; pub mod power; pub mod pus; diff --git a/satrs-core/src/parsers.rs b/satrs-core/src/parsers.rs new file mode 100644 index 0000000..f2ee248 --- /dev/null +++ b/satrs-core/src/parsers.rs @@ -0,0 +1,131 @@ +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +use cobs::decode_in_place; +#[cfg(feature = "alloc")] +use hashbrown::HashSet; + +use crate::tmtc::ReceivesTc; + +/// This function parses a given buffer for COBS encoded packets. The packet structure is +/// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: +/// +/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 +/// +/// This function is also able to deal with broken tail packets at the end. If broken tail +/// packets are detected, they are moved to the front of the buffer, and the write index for +/// future write operations will be written to the `next_write_idx` argument. +/// +/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. +pub fn parse_buffer_for_cobs_encoded_packets( + buf: &mut [u8], + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let mut start_index_packet = 0; + let mut start_found = false; + let mut last_byte = false; + let mut packets_found = 0; + for i in 0..buf.len() { + if i == buf.len() - 1 { + last_byte = true; + } + if buf[i] == 0 { + if !start_found && !last_byte && buf[i + 1] == 0 { + // Special case: Consecutive sentinel values or all zeroes. + // Skip. + continue; + } + if start_found { + let decode_result = decode_in_place(&mut buf[start_index_packet..i]); + if let Ok(packet_len) = decode_result { + packets_found += 1; + tc_receiver + .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?; + } + start_found = false; + } else { + start_index_packet = i + 1; + start_found = true; + } + } + } + // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. + if start_index_packet > 0 && start_found && packets_found > 0 { + let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); + first_seg[..last_seg.len()].copy_from_slice(last_seg); + *next_write_idx = last_seg.len(); + } + Ok(packets_found) +} + +pub trait PacketIdLookup { + fn validate(&self, apid: u16) -> bool; +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for Vec { + fn validate(&self, apid: u16) -> bool { + self.contains(&apid) + } +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for HashSet { + fn validate(&self, apid: u16) -> bool { + self.contains(&apid) + } +} + +impl PacketIdLookup for &[u16] { + fn validate(&self, apid: u16) -> bool { + if self.binary_search(&apid).is_ok() { + return true; + } + false + } +} + +/// This function parses a given buffer for tightly packed CCSDS space packets. It uses the +/// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then +/// uses the length field of the packet to extract CCSDS packets. +/// +/// This function is also able to deal with broken tail packets at the end as long a the parser +/// can read the full 6 bytes which constitue a space packet header. If broken tail packets are +/// detected, they are moved to the front of the buffer, and the write index for future write +/// operations will be written to the `next_write_idx` argument. +/// +/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. +pub fn parse_buffer_for_ccsds_space_packets( + buf: &mut [u8], + packet_id_lookup: &dyn PacketIdLookup, + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let packets_found = 0; + let mut current_idx = 0; + let buf_len = buf.len(); + loop { + if current_idx + 7 >= buf.len() { + break; + } + let packet_id = u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()); + if packet_id_lookup.validate(packet_id) { + let length_field = + u16::from_be_bytes(buf[current_idx + 4..current_idx + 6].try_into().unwrap()); + let packet_size = length_field + 7; + if (current_idx + packet_size as usize) < buf_len { + tc_receiver.pass_tc(&buf[current_idx..current_idx + packet_size as usize])?; + } else { + // Move packet to start of buffer if applicable. + if current_idx > 0 { + buf.copy_within(current_idx.., 0); + *next_write_idx = current_idx; + } + } + current_idx += packet_size as usize; + continue; + } + current_idx += 1; + } + Ok(packets_found) +} From 2f08365247a2dcaa5f608b48c45c6bc4a3020d39 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 16:08:09 +0200 Subject: [PATCH 44/68] move some tests --- .../src/hal/std/tcp_with_cobs_server.rs | 179 +--------------- satrs-core/src/parsers.rs | 192 ++++++++++++++++++ 2 files changed, 195 insertions(+), 176 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index a540a71..559c7a2 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -165,14 +165,12 @@ mod tests { thread, }; - use crate::tmtc::{ReceivesTcCore, TmPacketSource}; - use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; + use crate::{tmtc::{ReceivesTcCore, TmPacketSource}, hal::std::tcp_server::ServerConfig, parsers::tests::{SIMPLE_PACKET, INVERTED_PACKET}}; + use alloc::{collections::VecDeque, sync::Arc, vec::Vec, boxed::Box}; use cobs::encode; - use super::{parse_buffer_for_cobs_encoded_packets, ServerConfig, TcpTmtcInCobsServer}; + use super::TcpTmtcInCobsServer; - const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; - const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; #[derive(Default, Clone)] struct SyncTcCacher { @@ -188,20 +186,6 @@ mod tests { } } - #[derive(Default)] - struct TcCacher { - tc_queue: VecDeque>, - } - - impl ReceivesTcCore for TcCacher { - type Error = (); - - fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { - self.tc_queue.push_back(tc_raw.to_vec()); - Ok(()) - } - } - #[derive(Default, Clone)] struct SyncTmSource { tm_queue: Arc>>>, @@ -243,163 +227,6 @@ mod tests { *current_idx += 1; } - #[test] - fn test_parsing_simple_packet() { - let mut test_sender = TcCacher::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.tc_queue.len(), 1); - let packet = &test_sender.tc_queue[0]; - assert_eq!(packet, &SIMPLE_PACKET); - } - - #[test] - fn test_parsing_consecutive_packets() { - let mut test_sender = TcCacher::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - - // Second packet - encoded_buf[current_idx] = 0; - current_idx += 1; - current_idx += encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); - encoded_buf[current_idx] = 0; - current_idx += 1; - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 2); - assert_eq!(test_sender.tc_queue.len(), 2); - let packet0 = &test_sender.tc_queue[0]; - assert_eq!(packet0, &SIMPLE_PACKET); - let packet1 = &test_sender.tc_queue[1]; - assert_eq!(packet1, &INVERTED_PACKET); - } - - #[test] - fn test_split_tail_packet_only() { - let mut test_sender = TcCacher::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - let mut next_read_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx - 1], - &mut test_sender, - &mut next_read_idx, - ) - .unwrap(); - assert_eq!(packets, 0); - assert_eq!(test_sender.tc_queue.len(), 0); - assert_eq!(next_read_idx, 0); - } - - fn generic_test_split_packet(cut_off: usize) { - let mut test_sender = TcCacher::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - assert!(cut_off < INVERTED_PACKET.len() + 1); - let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - // Second packet - encoded_buf[current_idx] = 0; - let packet_start = current_idx; - current_idx += 1; - let encoded_len = encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); - assert_eq!(encoded_len, 6); - current_idx += encoded_len; - // We cut off the sentinel byte, so we expecte the write index to be the length of the - // packet minus the sentinel byte plus the first sentinel byte. - let next_expected_write_idx = 1 + encoded_len - cut_off + 1; - encoded_buf[current_idx] = 0; - current_idx += 1; - let mut next_write_idx = 0; - let expected_at_start = encoded_buf[packet_start..current_idx - cut_off].to_vec(); - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx - cut_off], - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.tc_queue.len(), 1); - assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); - assert_eq!(next_write_idx, next_expected_write_idx); - assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); - } - - #[test] - fn test_one_packet_and_split_tail_packet_0() { - generic_test_split_packet(1); - } - - #[test] - fn test_one_packet_and_split_tail_packet_1() { - generic_test_split_packet(2); - } - - #[test] - fn test_one_packet_and_split_tail_packet_2() { - generic_test_split_packet(3); - } - - #[test] - fn test_zero_at_end() { - let mut test_sender = TcCacher::default(); - let mut encoded_buf: [u8; 16] = [0; 16]; - let mut next_write_idx = 0; - let mut current_idx = 0; - encoded_buf[current_idx] = 5; - current_idx += 1; - encode_simple_packet(&mut encoded_buf, &mut current_idx); - encoded_buf[current_idx] = 0; - current_idx += 1; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut encoded_buf[0..current_idx], - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 1); - assert_eq!(test_sender.tc_queue.len(), 1); - assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); - assert_eq!(next_write_idx, 1); - assert_eq!(encoded_buf[0], 0); - } - - #[test] - fn test_all_zeroes() { - let mut test_sender = TcCacher::default(); - let mut all_zeroes: [u8; 5] = [0; 5]; - let mut next_write_idx = 0; - let packets = parse_buffer_for_cobs_encoded_packets( - // Cut off the sentinel byte at the end. - &mut all_zeroes, - &mut test_sender, - &mut next_write_idx, - ) - .unwrap(); - assert_eq!(packets, 0); - assert!(test_sender.tc_queue.is_empty()); - assert_eq!(next_write_idx, 0); - } - fn generic_tmtc_server( addr: &SocketAddr, tc_receiver: SyncTcCacher, diff --git a/satrs-core/src/parsers.rs b/satrs-core/src/parsers.rs index f2ee248..41aeaa1 100644 --- a/satrs-core/src/parsers.rs +++ b/satrs-core/src/parsers.rs @@ -129,3 +129,195 @@ pub fn parse_buffer_for_ccsds_space_packets( } Ok(packets_found) } + +#[cfg(test)] +pub(crate) mod tests { + use alloc::{collections::VecDeque, vec::Vec}; + use cobs::encode; + + use crate::tmtc::ReceivesTcCore; + + use super::parse_buffer_for_cobs_encoded_packets; + + pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; + + #[derive(Default)] + struct TcCacher { + tc_queue: VecDeque>, + } + + impl ReceivesTcCore for TcCacher { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + self.tc_queue.push_back(tc_raw.to_vec()); + Ok(()) + } + } + + pub (crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; + } + + #[test] + fn test_parsing_simple_packet() { + let mut test_sender = TcCacher::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.tc_queue.len(), 1); + let packet = &test_sender.tc_queue[0]; + assert_eq!(packet, &SIMPLE_PACKET); + } + + #[test] + fn test_parsing_consecutive_packets() { + let mut test_sender = TcCacher::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + + // Second packet + encoded_buf[current_idx] = 0; + current_idx += 1; + current_idx += encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 2); + assert_eq!(test_sender.tc_queue.len(), 2); + let packet0 = &test_sender.tc_queue[0]; + assert_eq!(packet0, &SIMPLE_PACKET); + let packet1 = &test_sender.tc_queue[1]; + assert_eq!(packet1, &INVERTED_PACKET); + } + + #[test] + fn test_split_tail_packet_only() { + let mut test_sender = TcCacher::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut next_read_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - 1], + &mut test_sender, + &mut next_read_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert_eq!(test_sender.tc_queue.len(), 0); + assert_eq!(next_read_idx, 0); + } + + fn generic_test_split_packet(cut_off: usize) { + let mut test_sender = TcCacher::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + assert!(cut_off < INVERTED_PACKET.len() + 1); + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + // Second packet + encoded_buf[current_idx] = 0; + let packet_start = current_idx; + current_idx += 1; + let encoded_len = encode(&INVERTED_PACKET, &mut encoded_buf[current_idx..]); + assert_eq!(encoded_len, 6); + current_idx += encoded_len; + // We cut off the sentinel byte, so we expecte the write index to be the length of the + // packet minus the sentinel byte plus the first sentinel byte. + let next_expected_write_idx = 1 + encoded_len - cut_off + 1; + encoded_buf[current_idx] = 0; + current_idx += 1; + let mut next_write_idx = 0; + let expected_at_start = encoded_buf[packet_start..current_idx - cut_off].to_vec(); + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx - cut_off], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.tc_queue.len(), 1); + assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, next_expected_write_idx); + assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); + } + + #[test] + fn test_one_packet_and_split_tail_packet_0() { + generic_test_split_packet(1); + } + + #[test] + fn test_one_packet_and_split_tail_packet_1() { + generic_test_split_packet(2); + } + + #[test] + fn test_one_packet_and_split_tail_packet_2() { + generic_test_split_packet(3); + } + + #[test] + fn test_zero_at_end() { + let mut test_sender = TcCacher::default(); + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut next_write_idx = 0; + let mut current_idx = 0; + encoded_buf[current_idx] = 5; + current_idx += 1; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + encoded_buf[current_idx] = 0; + current_idx += 1; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut encoded_buf[0..current_idx], + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 1); + assert_eq!(test_sender.tc_queue.len(), 1); + assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET); + assert_eq!(next_write_idx, 1); + assert_eq!(encoded_buf[0], 0); + } + + #[test] + fn test_all_zeroes() { + let mut test_sender = TcCacher::default(); + let mut all_zeroes: [u8; 5] = [0; 5]; + let mut next_write_idx = 0; + let packets = parse_buffer_for_cobs_encoded_packets( + // Cut off the sentinel byte at the end. + &mut all_zeroes, + &mut test_sender, + &mut next_write_idx, + ) + .unwrap(); + assert_eq!(packets, 0); + assert!(test_sender.tc_queue.is_empty()); + assert_eq!(next_write_idx, 0); + } +} From e4d8c0c9a790865103f3113d3316dfd161d6f1fe Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 16:08:35 +0200 Subject: [PATCH 45/68] cargo fmt --- satrs-core/src/hal/std/tcp_with_cobs_server.rs | 9 ++++++--- satrs-core/src/parsers.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 559c7a2..31bf4d0 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -165,13 +165,16 @@ mod tests { thread, }; - use crate::{tmtc::{ReceivesTcCore, TmPacketSource}, hal::std::tcp_server::ServerConfig, parsers::tests::{SIMPLE_PACKET, INVERTED_PACKET}}; - use alloc::{collections::VecDeque, sync::Arc, vec::Vec, boxed::Box}; + use crate::{ + hal::std::tcp_server::ServerConfig, + parsers::tests::{INVERTED_PACKET, SIMPLE_PACKET}, + tmtc::{ReceivesTcCore, TmPacketSource}, + }; + use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use cobs::encode; use super::TcpTmtcInCobsServer; - #[derive(Default, Clone)] struct SyncTcCacher { tc_queue: Arc>>>, diff --git a/satrs-core/src/parsers.rs b/satrs-core/src/parsers.rs index 41aeaa1..cfcd9b8 100644 --- a/satrs-core/src/parsers.rs +++ b/satrs-core/src/parsers.rs @@ -156,7 +156,7 @@ pub(crate) mod tests { } } - pub (crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { encoded_buf[*current_idx] = 0; *current_idx += 1; *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); From 86ec0f50b80ad143cd28e949e6336ba57a7944b1 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 16:17:24 +0200 Subject: [PATCH 46/68] proper modularisation --- satrs-core/src/parsers/ccsds.rs | 78 +++++++++++++++ .../src/{parsers.rs => parsers/cobs.rs} | 95 +------------------ satrs-core/src/parsers/mod.rs | 21 ++++ 3 files changed, 104 insertions(+), 90 deletions(-) create mode 100644 satrs-core/src/parsers/ccsds.rs rename satrs-core/src/{parsers.rs => parsers/cobs.rs} (72%) create mode 100644 satrs-core/src/parsers/mod.rs diff --git a/satrs-core/src/parsers/ccsds.rs b/satrs-core/src/parsers/ccsds.rs new file mode 100644 index 0000000..949a1cf --- /dev/null +++ b/satrs-core/src/parsers/ccsds.rs @@ -0,0 +1,78 @@ +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +#[cfg(feature = "alloc")] +use hashbrown::HashSet; + +use crate::tmtc::ReceivesTc; + +pub trait PacketIdLookup { + fn validate(&self, apid: u16) -> bool; +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for Vec { + fn validate(&self, apid: u16) -> bool { + self.contains(&apid) + } +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for HashSet { + fn validate(&self, apid: u16) -> bool { + self.contains(&apid) + } +} + +impl PacketIdLookup for &[u16] { + fn validate(&self, apid: u16) -> bool { + if self.binary_search(&apid).is_ok() { + return true; + } + false + } +} + +/// This function parses a given buffer for tightly packed CCSDS space packets. It uses the +/// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then +/// uses the length field of the packet to extract CCSDS packets. +/// +/// This function is also able to deal with broken tail packets at the end as long a the parser +/// can read the full 6 bytes which constitue a space packet header. If broken tail packets are +/// detected, they are moved to the front of the buffer, and the write index for future write +/// operations will be written to the `next_write_idx` argument. +/// +/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. +pub fn parse_buffer_for_ccsds_space_packets( + buf: &mut [u8], + packet_id_lookup: &dyn PacketIdLookup, + tc_receiver: &mut dyn ReceivesTc, + next_write_idx: &mut usize, +) -> Result { + let packets_found = 0; + let mut current_idx = 0; + let buf_len = buf.len(); + loop { + if current_idx + 7 >= buf.len() { + break; + } + let packet_id = u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()); + if packet_id_lookup.validate(packet_id) { + let length_field = + u16::from_be_bytes(buf[current_idx + 4..current_idx + 6].try_into().unwrap()); + let packet_size = length_field + 7; + if (current_idx + packet_size as usize) < buf_len { + tc_receiver.pass_tc(&buf[current_idx..current_idx + packet_size as usize])?; + } else { + // Move packet to start of buffer if applicable. + if current_idx > 0 { + buf.copy_within(current_idx.., 0); + *next_write_idx = current_idx; + } + } + current_idx += packet_size as usize; + continue; + } + current_idx += 1; + } + Ok(packets_found) +} diff --git a/satrs-core/src/parsers.rs b/satrs-core/src/parsers/cobs.rs similarity index 72% rename from satrs-core/src/parsers.rs rename to satrs-core/src/parsers/cobs.rs index cfcd9b8..bf99ce6 100644 --- a/satrs-core/src/parsers.rs +++ b/satrs-core/src/parsers/cobs.rs @@ -1,10 +1,5 @@ -#[cfg(feature = "alloc")] -use alloc::vec::Vec; -use cobs::decode_in_place; -#[cfg(feature = "alloc")] -use hashbrown::HashSet; - use crate::tmtc::ReceivesTc; +use cobs::decode_in_place; /// This function parses a given buffer for COBS encoded packets. The packet structure is /// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: @@ -58,90 +53,18 @@ pub fn parse_buffer_for_cobs_encoded_packets( Ok(packets_found) } -pub trait PacketIdLookup { - fn validate(&self, apid: u16) -> bool; -} - -#[cfg(feature = "alloc")] -impl PacketIdLookup for Vec { - fn validate(&self, apid: u16) -> bool { - self.contains(&apid) - } -} - -#[cfg(feature = "alloc")] -impl PacketIdLookup for HashSet { - fn validate(&self, apid: u16) -> bool { - self.contains(&apid) - } -} - -impl PacketIdLookup for &[u16] { - fn validate(&self, apid: u16) -> bool { - if self.binary_search(&apid).is_ok() { - return true; - } - false - } -} - -/// This function parses a given buffer for tightly packed CCSDS space packets. It uses the -/// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then -/// uses the length field of the packet to extract CCSDS packets. -/// -/// This function is also able to deal with broken tail packets at the end as long a the parser -/// can read the full 6 bytes which constitue a space packet header. If broken tail packets are -/// detected, they are moved to the front of the buffer, and the write index for future write -/// operations will be written to the `next_write_idx` argument. -/// -/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. -pub fn parse_buffer_for_ccsds_space_packets( - buf: &mut [u8], - packet_id_lookup: &dyn PacketIdLookup, - tc_receiver: &mut dyn ReceivesTc, - next_write_idx: &mut usize, -) -> Result { - let packets_found = 0; - let mut current_idx = 0; - let buf_len = buf.len(); - loop { - if current_idx + 7 >= buf.len() { - break; - } - let packet_id = u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()); - if packet_id_lookup.validate(packet_id) { - let length_field = - u16::from_be_bytes(buf[current_idx + 4..current_idx + 6].try_into().unwrap()); - let packet_size = length_field + 7; - if (current_idx + packet_size as usize) < buf_len { - tc_receiver.pass_tc(&buf[current_idx..current_idx + packet_size as usize])?; - } else { - // Move packet to start of buffer if applicable. - if current_idx > 0 { - buf.copy_within(current_idx.., 0); - *next_write_idx = current_idx; - } - } - current_idx += packet_size as usize; - continue; - } - current_idx += 1; - } - Ok(packets_found) -} - #[cfg(test)] pub(crate) mod tests { use alloc::{collections::VecDeque, vec::Vec}; use cobs::encode; - use crate::tmtc::ReceivesTcCore; + use crate::{ + parsers::tests::{encode_simple_packet, INVERTED_PACKET, SIMPLE_PACKET}, + tmtc::ReceivesTcCore, + }; use super::parse_buffer_for_cobs_encoded_packets; - pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; - pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; - #[derive(Default)] struct TcCacher { tc_queue: VecDeque>, @@ -156,14 +79,6 @@ pub(crate) mod tests { } } - pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encoded_buf[*current_idx] = 0; - *current_idx += 1; - *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); - encoded_buf[*current_idx] = 0; - *current_idx += 1; - } - #[test] fn test_parsing_simple_packet() { let mut test_sender = TcCacher::default(); diff --git a/satrs-core/src/parsers/mod.rs b/satrs-core/src/parsers/mod.rs new file mode 100644 index 0000000..c25060a --- /dev/null +++ b/satrs-core/src/parsers/mod.rs @@ -0,0 +1,21 @@ +pub mod ccsds; +pub mod cobs; + +pub use crate::parsers::ccsds::parse_buffer_for_ccsds_space_packets; +pub use crate::parsers::cobs::parse_buffer_for_cobs_encoded_packets; + +#[cfg(test)] +pub(crate) mod tests { + use cobs::encode; + + pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; + + pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; + } +} From d0e6ccdaa30818de3144596db898bed4189b7bb1 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 16:18:14 +0200 Subject: [PATCH 47/68] this is better --- satrs-core/Cargo.toml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 9ee7cc1..a26cf03 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -90,23 +90,23 @@ version = "1" [features] default = ["std"] std = [ - "downcast-rs/std", - "alloc", - "bus", - "postcard/use-std", - "crossbeam-channel/std", - "serde/std", - "spacepackets/std", - "num_enum/std", - "thiserror", - "socket2" + "downcast-rs/std", + "alloc", + "bus", + "postcard/use-std", + "crossbeam-channel/std", + "serde/std", + "spacepackets/std", + "num_enum/std", + "thiserror", + "socket2" ] alloc = [ - "serde/alloc", - "spacepackets/alloc", - "hashbrown", - "dyn-clone", - "downcast-rs" + "serde/alloc", + "spacepackets/alloc", + "hashbrown", + "dyn-clone", + "downcast-rs" ] serde = ["dep:serde", "spacepackets/serde"] crossbeam = ["crossbeam-channel"] From d5722b7f39e68237e3562edb88516cc131bb2957 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:27:08 +0200 Subject: [PATCH 48/68] appears to work now --- satrs-core/Cargo.toml | 4 ++-- satrs-core/src/parsers/ccsds.rs | 37 ++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index a26cf03..543039e 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -68,8 +68,8 @@ optional = true [dependencies.spacepackets] version = "0.7.0-beta.1" # path = "../../spacepackets" -# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" -# rev = "" +git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" +rev = "79d26e1a6" # branch = "" default-features = false diff --git a/satrs-core/src/parsers/ccsds.rs b/satrs-core/src/parsers/ccsds.rs index 949a1cf..1a90a7b 100644 --- a/satrs-core/src/parsers/ccsds.rs +++ b/satrs-core/src/parsers/ccsds.rs @@ -2,36 +2,59 @@ use alloc::vec::Vec; #[cfg(feature = "alloc")] use hashbrown::HashSet; +use spacepackets::PacketId; use crate::tmtc::ReceivesTc; pub trait PacketIdLookup { - fn validate(&self, apid: u16) -> bool; + fn validate(&self, packet_id: u16) -> bool; } #[cfg(feature = "alloc")] impl PacketIdLookup for Vec { - fn validate(&self, apid: u16) -> bool { - self.contains(&apid) + fn validate(&self, packet_id: u16) -> bool { + self.contains(&packet_id) + } +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for Vec { + fn validate(&self, packet_id: u16) -> bool { + self.contains(&PacketId::from(packet_id)) } } #[cfg(feature = "alloc")] impl PacketIdLookup for HashSet { - fn validate(&self, apid: u16) -> bool { - self.contains(&apid) + fn validate(&self, packet_id: u16) -> bool { + self.contains(&packet_id) + } +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for HashSet { + fn validate(&self, packet_id: u16) -> bool { + self.contains(&PacketId::from(packet_id)) } } impl PacketIdLookup for &[u16] { - fn validate(&self, apid: u16) -> bool { - if self.binary_search(&apid).is_ok() { + fn validate(&self, packet_id: u16) -> bool { + if self.binary_search(&packet_id).is_ok() { return true; } false } } +impl PacketIdLookup for &[PacketId] { + fn validate(&self, packet_id: u16) -> bool { + if self.binary_search(&PacketId::from(packet_id)).is_ok() { + return true; + } + false + } +} /// This function parses a given buffer for tightly packed CCSDS space packets. It uses the /// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then /// uses the length field of the packet to extract CCSDS packets. From aa556ad746f603581b90b4fe9c5fab9d797ce999 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:35:24 +0200 Subject: [PATCH 49/68] maybe like this? --- satrs-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 543039e..5eb0421 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -66,7 +66,7 @@ features = ["all"] optional = true [dependencies.spacepackets] -version = "0.7.0-beta.1" +# version = "0.7.0-beta.1" # path = "../../spacepackets" git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" rev = "79d26e1a6" From 4dd85f294cfe97247ed0020825661e8ed34554b4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:36:10 +0200 Subject: [PATCH 50/68] this in confusing --- satrs-mib/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satrs-mib/Cargo.toml b/satrs-mib/Cargo.toml index f6dd23a..db9b85e 100644 --- a/satrs-mib/Cargo.toml +++ b/satrs-mib/Cargo.toml @@ -23,8 +23,8 @@ version = "1" optional = true [dependencies.satrs-core] -version = "0.1.0-alpha.0" -# path = "../satrs-core" +# version = "0.1.0-alpha.0" +path = "../satrs-core" [dependencies.satrs-mib-codegen] path = "codegen" From 35e1f7a983f6535c5571186e361fe101d4306b89 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:40:50 +0200 Subject: [PATCH 51/68] jenkinsfile improvements --- automation/Jenkinsfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/automation/Jenkinsfile b/automation/Jenkinsfile index 44946f7..0770614 100644 --- a/automation/Jenkinsfile +++ b/automation/Jenkinsfile @@ -8,6 +8,11 @@ pipeline { } stages { + stage('Rust Toolchain Info') { + steps { + sh 'rustc --version' + } + } stage('Clippy') { steps { sh 'cargo clippy' @@ -15,7 +20,9 @@ pipeline { } stage('Docs') { steps { - sh 'cargo +nightly doc --all-features' + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh 'cargo +nightly doc --all-features' + } } } stage('Rustfmt') { From b62d60f5790bb7aa9fa7bbe0dfa7d1496bdb5c08 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:48:56 +0200 Subject: [PATCH 52/68] lets try this --- satrs-mib/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/satrs-mib/Cargo.toml b/satrs-mib/Cargo.toml index db9b85e..5f304cd 100644 --- a/satrs-mib/Cargo.toml +++ b/satrs-mib/Cargo.toml @@ -24,7 +24,8 @@ optional = true [dependencies.satrs-core] # version = "0.1.0-alpha.0" -path = "../satrs-core" +git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" +rev = "35e1f7a983f6535c5571186e361fe101d4306b89" [dependencies.satrs-mib-codegen] path = "codegen" From e1998a8bcc4e2863f38a083b3e4527012389b335 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:51:39 +0200 Subject: [PATCH 53/68] wrong repo --- satrs-mib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-mib/Cargo.toml b/satrs-mib/Cargo.toml index 5f304cd..cdfa4f5 100644 --- a/satrs-mib/Cargo.toml +++ b/satrs-mib/Cargo.toml @@ -24,7 +24,7 @@ optional = true [dependencies.satrs-core] # version = "0.1.0-alpha.0" -git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" +git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" rev = "35e1f7a983f6535c5571186e361fe101d4306b89" [dependencies.satrs-mib-codegen] From 9ccb6bb0005853ab340ac9a5bfd5949c00a915c9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Sep 2023 18:55:30 +0200 Subject: [PATCH 54/68] stupid circ deps --- satrs-mib/codegen/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/satrs-mib/codegen/Cargo.toml b/satrs-mib/codegen/Cargo.toml index ccc4d1a..db6a671 100644 --- a/satrs-mib/codegen/Cargo.toml +++ b/satrs-mib/codegen/Cargo.toml @@ -20,8 +20,9 @@ quote = "1" proc-macro2 = "1" [dependencies.satrs-core] -version = "0.1.0-alpha.0" -# path = "../../satrs-core" +# version = "0.1.0-alpha.0" +git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" +rev = "35e1f7a983f6535c5571186e361fe101d4306b89" [dev-dependencies] trybuild = { version = "1", features = ["diff"] } From 22254e4bbe468701cbc53a453d8f56c8fb4c105b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 19 Sep 2023 00:13:55 +0200 Subject: [PATCH 55/68] this works, just not sure whether its the best solution.. --- satrs-core/src/hal/std/tcp_server.rs | 12 ++-- .../src/hal/std/tcp_with_cobs_server.rs | 14 ++--- satrs-core/src/parsers/ccsds.rs | 4 +- satrs-core/src/parsers/cobs.rs | 4 +- satrs-core/src/tmtc/mod.rs | 58 ++++++++++++++++++- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_server.rs b/satrs-core/src/hal/std/tcp_server.rs index f1f5c47..dcccf66 100644 --- a/satrs-core/src/hal/std/tcp_server.rs +++ b/satrs-core/src/hal/std/tcp_server.rs @@ -162,8 +162,8 @@ impl< cfg: ServerConfig, tc_parser: TcParser, tm_sender: TmSender, - tm_source: Box + Send>, - tc_receiver: Box + Send>, + tm_source: Box>, + tc_receiver: Box>, ) -> Result, std::io::Error> { Ok(Self { base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?, @@ -280,17 +280,17 @@ impl< pub(crate) struct TcpTmtcServerBase { pub(crate) listener: TcpListener, pub(crate) inner_loop_delay: Duration, - pub(crate) tm_source: Box + Send>, + pub(crate) tm_source: Box>, pub(crate) tm_buffer: Vec, - pub(crate) tc_receiver: Box + Send>, + pub(crate) tc_receiver: Box>, pub(crate) tc_buffer: Vec, } impl TcpTmtcServerBase { pub(crate) fn new( cfg: ServerConfig, - tm_source: Box + Send>, - tc_receiver: Box + Send>, + tm_source: Box>, + tc_receiver: Box>, ) -> Result { // Create a TCP listener bound to two addresses. let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?; diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 31bf4d0..52f277b 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -20,7 +20,7 @@ use crate::hal::std::tcp_server::{ #[derive(Default)] pub struct CobsTcParser {} -impl TcpTcParser for CobsTcParser { +impl TcpTcParser for CobsTcParser { fn handle_tc_parsing( &mut self, tc_buffer: &mut [u8], @@ -32,7 +32,7 @@ impl TcpTcParser for CobsTcParser { // Reader vec full, need to parse for packets. conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( &mut tc_buffer[..current_write_idx], - tc_receiver, + tc_receiver.upcast_mut(), next_write_idx, ) .map_err(|e| TcpTmtcError::TcError(e))?; @@ -105,7 +105,7 @@ impl TcpTmSender for CobsTmSender { /// packets even from a data stream which is split up. The server wil use the /// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a /// generic TC receiver. -pub struct TcpTmtcInCobsServer { +pub struct TcpTmtcInCobsServer { generic_server: TcpTmtcGenericServer, } @@ -122,8 +122,8 @@ impl TcpTmtcInCobsServer { /// forwarded to this TC receiver. pub fn new( cfg: ServerConfig, - tm_source: Box + Send>, - tc_receiver: Box + Send>, + tm_source: Box>, + tc_receiver: Box>, ) -> Result> { Ok(Self { generic_server: TcpTmtcGenericServer::new( @@ -168,7 +168,7 @@ mod tests { use crate::{ hal::std::tcp_server::ServerConfig, parsers::tests::{INVERTED_PACKET, SIMPLE_PACKET}, - tmtc::{ReceivesTcCore, TmPacketSource}, + tmtc::{ReceivesTcCore, TmPacketSourceCore}, }; use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use cobs::encode; @@ -201,7 +201,7 @@ mod tests { } } - impl TmPacketSource for SyncTmSource { + impl TmPacketSourceCore for SyncTmSource { type Error = (); fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { diff --git a/satrs-core/src/parsers/ccsds.rs b/satrs-core/src/parsers/ccsds.rs index 1a90a7b..71e6aec 100644 --- a/satrs-core/src/parsers/ccsds.rs +++ b/satrs-core/src/parsers/ccsds.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use hashbrown::HashSet; use spacepackets::PacketId; -use crate::tmtc::ReceivesTc; +use crate::tmtc::ReceivesTcCore; pub trait PacketIdLookup { fn validate(&self, packet_id: u16) -> bool; @@ -68,7 +68,7 @@ impl PacketIdLookup for &[PacketId] { pub fn parse_buffer_for_ccsds_space_packets( buf: &mut [u8], packet_id_lookup: &dyn PacketIdLookup, - tc_receiver: &mut dyn ReceivesTc, + tc_receiver: &mut dyn ReceivesTcCore, next_write_idx: &mut usize, ) -> Result { let packets_found = 0; diff --git a/satrs-core/src/parsers/cobs.rs b/satrs-core/src/parsers/cobs.rs index bf99ce6..e57a719 100644 --- a/satrs-core/src/parsers/cobs.rs +++ b/satrs-core/src/parsers/cobs.rs @@ -1,4 +1,4 @@ -use crate::tmtc::ReceivesTc; +use crate::tmtc::ReceivesTcCore; use cobs::decode_in_place; /// This function parses a given buffer for COBS encoded packets. The packet structure is @@ -13,7 +13,7 @@ use cobs::decode_in_place; /// The parser will write all packets which were decoded successfully to the given `tc_receiver`. pub fn parse_buffer_for_cobs_encoded_packets( buf: &mut [u8], - tc_receiver: &mut dyn ReceivesTc, + tc_receiver: &mut dyn ReceivesTcCore, next_write_idx: &mut usize, ) -> Result { let mut start_index_packet = 0; diff --git a/satrs-core/src/tmtc/mod.rs b/satrs-core/src/tmtc/mod.rs index 2f8bdf3..04f4299 100644 --- a/satrs-core/src/tmtc/mod.rs +++ b/satrs-core/src/tmtc/mod.rs @@ -72,12 +72,33 @@ pub trait ReceivesTcCore { /// Extension trait of [ReceivesTcCore] which allows downcasting by implementing [Downcast] and /// is also sendable. #[cfg(feature = "alloc")] -pub trait ReceivesTc: ReceivesTcCore + Downcast + Send {} +pub trait ReceivesTc: ReceivesTcCore + Downcast + Send { + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast(&self) -> &dyn ReceivesTcCore; + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast_mut(&mut self) -> &mut dyn ReceivesTcCore; +} /// Blanket implementation to automatically implement [ReceivesTc] when the [alloc] feature /// is enabled. #[cfg(feature = "alloc")] -impl ReceivesTc for T where T: ReceivesTcCore + Send + 'static {} +impl ReceivesTc for T +where + T: ReceivesTcCore + Send + 'static, +{ + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast(&self) -> &dyn ReceivesTcCore { + self + } + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast_mut(&mut self) -> &mut dyn ReceivesTcCore { + self + } +} #[cfg(feature = "alloc")] impl_downcast!(ReceivesTc assoc Error); @@ -95,7 +116,38 @@ pub trait ReceivesCcsdsTc { /// Generic trait for a TM packet source, with no restrictions on the type of TM. /// Implementors write the telemetry into the provided buffer and return the size of the telemetry. -pub trait TmPacketSource { +pub trait TmPacketSourceCore { type Error; fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result; } + +/// Extension trait of [TmPacketSourceCore] which allows downcasting by implementing [Downcast] and +/// is also sendable. +#[cfg(feature = "alloc")] +pub trait TmPacketSource: TmPacketSourceCore + Downcast + Send { + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast(&self) -> &dyn TmPacketSourceCore; + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast_mut(&mut self) -> &mut dyn TmPacketSourceCore; +} + +/// Blanket implementation to automatically implement [ReceivesTc] when the [alloc] feature +/// is enabled. +#[cfg(feature = "alloc")] +impl TmPacketSource for T +where + T: TmPacketSourceCore + Send + 'static, +{ + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast(&self) -> &dyn TmPacketSourceCore { + self + } + // Remove this once trait upcasting coercion has been implemented. + // Tracking issue: https://github.com/rust-lang/rust/issues/65991 + fn upcast_mut(&mut self) -> &mut dyn TmPacketSourceCore { + self + } +} From 3aba6b42761b49efd02848a9bcbcba1a07e72732 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 11:45:27 +0200 Subject: [PATCH 56/68] stupid doctests --- .../src/hal/std/tcp_with_cobs_server.rs | 150 +++++++++++++++++- satrs-core/src/parsers/ccsds.rs | 3 + 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 52f277b..de15884 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -105,6 +105,153 @@ impl TcpTmSender for CobsTmSender { /// packets even from a data stream which is split up. The server wil use the /// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a /// generic TC receiver. +/// +/// ## Example +/// +/// ``` +/// use core::{ +/// sync::atomic::{AtomicBool, Ordering}, +/// time::Duration, +/// }; +/// use std::{ +/// io::{Read, Write}, +/// net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, +/// sync::Mutex, +/// thread, +/// }; +/// +/// use satrs_core::{ +/// hal::std::tcp_server::{ServerConfig, TcpTmtcInCobsServer}, +/// tmtc::{ReceivesTcCore, TmPacketSourceCore}, +/// }; +/// use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; +/// use cobs::encode; +/// +/// #[derive(Default, Clone)] +/// struct SyncTcCacher { +/// tc_queue: Arc>>>, +/// } +/// impl ReceivesTcCore for SyncTcCacher { +/// type Error = (); +/// +/// fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { +/// let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed"); +/// tc_queue.push_back(tc_raw.to_vec()); +/// Ok(()) +/// } +/// } +/// +/// #[derive(Default, Clone)] +/// struct SyncTmSource { +/// tm_queue: Arc>>>, +/// } +/// +/// impl SyncTmSource { +/// pub(crate) fn add_tm(&mut self, tm: &[u8]) { +/// let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec"); +/// tm_queue.push_back(tm.to_vec()); +/// } +/// } +/// +/// impl TmPacketSourceCore for SyncTmSource { +/// type Error = (); +/// +/// fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { +/// let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); +/// if !tm_queue.is_empty() { +/// let next_vec = tm_queue.front().unwrap(); +/// if buffer.len() < next_vec.len() { +/// panic!( +/// "provided buffer too small, must be at least {} bytes", +/// next_vec.len() +/// ); +/// } +/// let next_vec = tm_queue.pop_front().unwrap(); +/// buffer[0..next_vec.len()].copy_from_slice(&next_vec); +/// return Ok(next_vec.len()); +/// } +/// Ok(0) +/// } +/// } +/// +/// fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { +/// encoded_buf[*current_idx] = 0; +/// *current_idx += 1; +/// *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); +/// encoded_buf[*current_idx] = 0; +/// *current_idx += 1; +/// } +/// +/// const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; +/// const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 4, 1]; +/// +/// let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); +/// let tc_receiver = SyncTcCacher::default(); +/// let tm_source = SyncTmSource::default(); +/// let mut tcp_server = TcpTmtcInCobsServer::new( +/// ServerConfig::new(auto_port_addr, Duration::from_millis(2), 1024, 1024), +/// Box::new(tm_source), +/// Box::new(tc_receiver.clone()), +/// ).expect("TCP server generation failed"); +/// let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); +/// let conn_handled: Arc = Default::default(); +/// let set_if_done = conn_handled.clone(); +/// +/// // Call the connection handler in separate thread, does block. +/// thread::spawn(move || { +/// let result = tcp_server.handle_next_connection(); +/// if result.is_err() { +/// panic!("handling connection failed: {:?}", result.unwrap_err()); +/// } +/// let conn_result = result.unwrap(); +/// assert_eq!(conn_result.num_received_tcs, 1); +/// assert_eq!(conn_result.num_sent_tms, 1); +/// set_if_done.store(true, Ordering::Relaxed); +/// }); +/// +/// // Send TC to server now. +/// let mut encoded_buf: [u8; 16] = [0; 16]; +/// let mut current_idx = 0; +/// encode_simple_packet(&mut encoded_buf, &mut current_idx); +/// let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); +/// stream +/// .write_all(&encoded_buf[..current_idx]) +/// .expect("writing to TCP server failed"); +/// // Done with writing. +/// stream +/// .shutdown(std::net::Shutdown::Write) +/// .expect("shutting down write failed"); +/// let mut read_buf: [u8; 16] = [0; 16]; +/// let read_len = stream.read(&mut read_buf).expect("read failed"); +/// drop(stream); +/// +/// // 1 byte encoding overhead, 2 sentinel bytes. +/// assert_eq!(read_len, 8); +/// assert_eq!(read_buf[0], 0); +/// assert_eq!(read_buf[read_len - 1], 0); +/// let decoded_len = +/// cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); +/// assert_eq!(decoded_len, 5); +/// // Skip first sentinel byte. +/// assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); +/// // A certain amount of time is allowed for the transaction to complete. +/// for _ in 0..3 { +/// if !conn_handled.load(Ordering::Relaxed) { +/// thread::sleep(Duration::from_millis(5)); +/// } +/// } +/// if !conn_handled.load(Ordering::Relaxed) { +/// panic!("connection was not handled properly"); +/// } +/// // Check that the packet was received and decoded successfully. +/// let mut tc_queue = tc_receiver +/// .tc_queue +/// .lock() +/// .expect("locking tc queue failed"); +/// assert_eq!(tc_queue.len(), 1); +/// assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); +/// drop(tc_queue); +/// ``` pub struct TcpTmtcInCobsServer { generic_server: TcpTmtcGenericServer, } @@ -335,6 +482,8 @@ mod tests { .expect("shutting down write failed"); let mut read_buf: [u8; 16] = [0; 16]; let read_len = stream.read(&mut read_buf).expect("read failed"); + drop(stream); + // 1 byte encoding overhead, 2 sentinel bytes. assert_eq!(read_len, 8); assert_eq!(read_buf[0], 0); @@ -345,7 +494,6 @@ mod tests { // Skip first sentinel byte. assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); - drop(stream); // A certain amount of time is allowed for the transaction to complete. for _ in 0..3 { if !conn_handled.load(Ordering::Relaxed) { diff --git a/satrs-core/src/parsers/ccsds.rs b/satrs-core/src/parsers/ccsds.rs index 71e6aec..fe84f3f 100644 --- a/satrs-core/src/parsers/ccsds.rs +++ b/satrs-core/src/parsers/ccsds.rs @@ -99,3 +99,6 @@ pub fn parse_buffer_for_ccsds_space_packets( } Ok(packets_found) } + +#[cfg(test)] +mod tests {} From 567a0a1cf5863961cc8fe78499029d24206c7a2d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 14:05:25 +0200 Subject: [PATCH 57/68] doc example works --- .../src/hal/std/tcp_with_cobs_server.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index de15884..3888c53 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -108,6 +108,18 @@ impl TcpTmSender for CobsTmSender { /// /// ## Example /// +/// This is a more elaborate example. It showcases all major features of the TCP server by +/// performing following steps: +/// +/// 1. It defines both a TC receiver and a TM source which are [Sync]. +/// 2. A telemetry packet is inserted into the TM source. The packet will be handled by the +/// TCP server after handling all TCs. +/// 3. It instantiates the TCP server on localhost with automatic port assignment and assigns +/// the TC receiver and TM source created previously. +/// 4. It moves the TCP server to a different thread and calls the +/// [TcpTmtcInCobsServer::handle_next_connection] call inside that thread +/// 5. The main threads connects to the server, sends a test telecommand and then reads back +/// the test telemetry insertd in to the TM source previously. /// ``` /// use core::{ /// sync::atomic::{AtomicBool, Ordering}, @@ -174,6 +186,7 @@ impl TcpTmSender for CobsTmSender { /// } /// } /// +/// // Simple COBS encoder which also inserts the sentinel bytes. /// fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { /// encoded_buf[*current_idx] = 0; /// *current_idx += 1; @@ -187,7 +200,9 @@ impl TcpTmSender for CobsTmSender { /// /// let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); /// let tc_receiver = SyncTcCacher::default(); -/// let tm_source = SyncTmSource::default(); +/// let mut tm_source = SyncTmSource::default(); +/// // Insert a telemetry packet which will be read back by the client at a later stage. +/// tm_source.add_tm(&INVERTED_PACKET); /// let mut tcp_server = TcpTmtcInCobsServer::new( /// ServerConfig::new(auto_port_addr, Duration::from_millis(2), 1024, 1024), /// Box::new(tm_source), @@ -204,8 +219,9 @@ impl TcpTmSender for CobsTmSender { /// panic!("handling connection failed: {:?}", result.unwrap_err()); /// } /// let conn_result = result.unwrap(); -/// assert_eq!(conn_result.num_received_tcs, 1); -/// assert_eq!(conn_result.num_sent_tms, 1); +/// assert_eq!(conn_result.num_received_tcs, 1, "No TC received"); +/// assert_eq!(conn_result.num_sent_tms, 1, "No TM received"); +/// // Signal the main thread we are done. /// set_if_done.store(true, Ordering::Relaxed); /// }); /// From afd7999d5cb51f37b39b9971cbd6623285860f71 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 14:57:11 +0200 Subject: [PATCH 58/68] added missing tests --- .../src/hal/std/tcp_with_cobs_server.rs | 89 ++++++++++++++----- satrs-core/src/parsers/cobs.rs | 7 +- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 3888c53..494ebad 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -148,6 +148,7 @@ impl TcpTmSender for CobsTmSender { /// /// fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { /// let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed"); +/// println!("Received TC: {:x?}", tc_raw); /// tc_queue.push_back(tc_raw.to_vec()); /// Ok(()) /// } @@ -178,6 +179,7 @@ impl TcpTmSender for CobsTmSender { /// next_vec.len() /// ); /// } +/// println!("Sending and encoding TM: {:x?}", next_vec); /// let next_vec = tm_queue.pop_front().unwrap(); /// buffer[0..next_vec.len()].copy_from_slice(&next_vec); /// return Ok(next_vec.len()); @@ -386,9 +388,17 @@ mod tests { } fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encode_packet(&SIMPLE_PACKET, encoded_buf, current_idx) + } + + fn encode_inverted_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encode_packet(&INVERTED_PACKET, encoded_buf, current_idx) + } + + fn encode_packet(packet: &[u8], encoded_buf: &mut [u8], current_idx: &mut usize) { encoded_buf[*current_idx] = 0; *current_idx += 1; - *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + *current_idx += encode(packet, &mut encoded_buf[*current_idx..]); encoded_buf[*current_idx] = 0; *current_idx += 1; } @@ -401,7 +411,7 @@ mod tests { TcpTmtcInCobsServer::new( ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024), Box::new(tm_source), - Box::new(tc_receiver.clone()), + Box::new(tc_receiver), ) .expect("TCP server generation failed") } @@ -411,8 +421,7 @@ mod tests { let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let tm_source = SyncTmSource::default(); - let mut tcp_server = - generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); + let mut tcp_server = generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source); let dest_addr = tcp_server .local_addr() .expect("retrieving dest addr failed"); @@ -458,14 +467,12 @@ mod tests { } #[test] - fn test_server_basic_no_tm_multi_tc() {} - - #[test] - fn test_server_basic_with_tm() { + fn test_server_basic_multi_tm_multi_tc() { let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let tc_receiver = SyncTcCacher::default(); let mut tm_source = SyncTmSource::default(); tm_source.add_tm(&INVERTED_PACKET); + tm_source.add_tm(&SIMPLE_PACKET); let mut tcp_server = generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone()); let dest_addr = tcp_server @@ -480,15 +487,19 @@ mod tests { panic!("handling connection failed: {:?}", result.unwrap_err()); } let conn_result = result.unwrap(); - assert_eq!(conn_result.num_received_tcs, 1); - assert_eq!(conn_result.num_sent_tms, 1); + assert_eq!(conn_result.num_received_tcs, 2, "Not enough TCs received"); + assert_eq!(conn_result.num_sent_tms, 2, "Not enough TMs received"); set_if_done.store(true, Ordering::Relaxed); }); // Send TC to server now. - let mut encoded_buf: [u8; 16] = [0; 16]; + let mut encoded_buf: [u8; 32] = [0; 32]; let mut current_idx = 0; encode_simple_packet(&mut encoded_buf, &mut current_idx); + encode_inverted_packet(&mut encoded_buf, &mut current_idx); let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); + stream + .set_read_timeout(Some(Duration::from_millis(10))) + .expect("setting reas timeout failed"); stream .write_all(&encoded_buf[..current_idx]) .expect("writing to TCP server failed"); @@ -497,18 +508,49 @@ mod tests { .shutdown(std::net::Shutdown::Write) .expect("shutting down write failed"); let mut read_buf: [u8; 16] = [0; 16]; - let read_len = stream.read(&mut read_buf).expect("read failed"); - drop(stream); + let mut read_len_total = 0; + // Timeout ensures this does not block forever. + while read_len_total < 16 { + let read_len = stream.read(&mut read_buf).expect("read failed"); + read_len_total += read_len; + // Read until full expected size is available. + if read_len == 16 { + // Read first TM packet. + current_idx = 0; + assert_eq!(read_len, 16); + assert_eq!(read_buf[0], 0); + current_idx += 1; + let mut dec_report = cobs::decode_in_place_report(&mut read_buf[current_idx..]) + .expect("COBS decoding failed"); + assert_eq!(dec_report.dst_used, 5); + // Skip first sentinel byte. + assert_eq!( + &read_buf[current_idx..current_idx + INVERTED_PACKET.len()], + &INVERTED_PACKET + ); + current_idx += dec_report.src_used; + // End sentinel. + assert_eq!(read_buf[current_idx], 0, "invalid sentinel end byte"); + current_idx += 1; - // 1 byte encoding overhead, 2 sentinel bytes. - assert_eq!(read_len, 8); - assert_eq!(read_buf[0], 0); - assert_eq!(read_buf[read_len - 1], 0); - let decoded_len = - cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); - assert_eq!(decoded_len, 5); - // Skip first sentinel byte. - assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); + // Read second TM packet. + assert_eq!(read_buf[current_idx], 0); + current_idx += 1; + dec_report = cobs::decode_in_place_report(&mut read_buf[current_idx..]) + .expect("COBS decoding failed"); + assert_eq!(dec_report.dst_used, 5); + // Skip first sentinel byte. + assert_eq!( + &read_buf[current_idx..current_idx + SIMPLE_PACKET.len()], + &SIMPLE_PACKET + ); + current_idx += dec_report.src_used; + // End sentinel. + assert_eq!(read_buf[current_idx], 0); + break; + } + } + drop(stream); // A certain amount of time is allowed for the transaction to complete. for _ in 0..3 { @@ -524,8 +566,9 @@ mod tests { .tc_queue .lock() .expect("locking tc queue failed"); - assert_eq!(tc_queue.len(), 1); + assert_eq!(tc_queue.len(), 2); assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); + assert_eq!(tc_queue.pop_front().unwrap(), &INVERTED_PACKET); drop(tc_queue); } } diff --git a/satrs-core/src/parsers/cobs.rs b/satrs-core/src/parsers/cobs.rs index e57a719..5d906a8 100644 --- a/satrs-core/src/parsers/cobs.rs +++ b/satrs-core/src/parsers/cobs.rs @@ -44,11 +44,10 @@ pub fn parse_buffer_for_cobs_encoded_packets( } } } - // Split frame at the end for a multi-packet frame. Move it to the front of the buffer. + // Move split frame at the end to the front of the buffer. if start_index_packet > 0 && start_found && packets_found > 0 { - let (first_seg, last_seg) = buf.split_at_mut(start_index_packet - 1); - first_seg[..last_seg.len()].copy_from_slice(last_seg); - *next_write_idx = last_seg.len(); + buf.copy_within(start_index_packet - 1.., 0); + *next_write_idx = buf.len() - start_index_packet + 1; } Ok(packets_found) } From f314e69ed84706aea412b7de5a343d133c12826f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 15:04:46 +0200 Subject: [PATCH 59/68] lets see --- .../src/hal/std/tcp_with_cobs_server.rs | 164 +---------------- satrs-core/tests/tcp_server_cobs.rs | 165 ++++++++++++++++++ 2 files changed, 167 insertions(+), 162 deletions(-) create mode 100644 satrs-core/tests/tcp_server_cobs.rs diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 494ebad..ec080e2 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -108,168 +108,8 @@ impl TcpTmSender for CobsTmSender { /// /// ## Example /// -/// This is a more elaborate example. It showcases all major features of the TCP server by -/// performing following steps: -/// -/// 1. It defines both a TC receiver and a TM source which are [Sync]. -/// 2. A telemetry packet is inserted into the TM source. The packet will be handled by the -/// TCP server after handling all TCs. -/// 3. It instantiates the TCP server on localhost with automatic port assignment and assigns -/// the TC receiver and TM source created previously. -/// 4. It moves the TCP server to a different thread and calls the -/// [TcpTmtcInCobsServer::handle_next_connection] call inside that thread -/// 5. The main threads connects to the server, sends a test telecommand and then reads back -/// the test telemetry insertd in to the TM source previously. -/// ``` -/// use core::{ -/// sync::atomic::{AtomicBool, Ordering}, -/// time::Duration, -/// }; -/// use std::{ -/// io::{Read, Write}, -/// net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, -/// sync::Mutex, -/// thread, -/// }; -/// -/// use satrs_core::{ -/// hal::std::tcp_server::{ServerConfig, TcpTmtcInCobsServer}, -/// tmtc::{ReceivesTcCore, TmPacketSourceCore}, -/// }; -/// use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; -/// use cobs::encode; -/// -/// #[derive(Default, Clone)] -/// struct SyncTcCacher { -/// tc_queue: Arc>>>, -/// } -/// impl ReceivesTcCore for SyncTcCacher { -/// type Error = (); -/// -/// fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { -/// let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed"); -/// println!("Received TC: {:x?}", tc_raw); -/// tc_queue.push_back(tc_raw.to_vec()); -/// Ok(()) -/// } -/// } -/// -/// #[derive(Default, Clone)] -/// struct SyncTmSource { -/// tm_queue: Arc>>>, -/// } -/// -/// impl SyncTmSource { -/// pub(crate) fn add_tm(&mut self, tm: &[u8]) { -/// let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec"); -/// tm_queue.push_back(tm.to_vec()); -/// } -/// } -/// -/// impl TmPacketSourceCore for SyncTmSource { -/// type Error = (); -/// -/// fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { -/// let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); -/// if !tm_queue.is_empty() { -/// let next_vec = tm_queue.front().unwrap(); -/// if buffer.len() < next_vec.len() { -/// panic!( -/// "provided buffer too small, must be at least {} bytes", -/// next_vec.len() -/// ); -/// } -/// println!("Sending and encoding TM: {:x?}", next_vec); -/// let next_vec = tm_queue.pop_front().unwrap(); -/// buffer[0..next_vec.len()].copy_from_slice(&next_vec); -/// return Ok(next_vec.len()); -/// } -/// Ok(0) -/// } -/// } -/// -/// // Simple COBS encoder which also inserts the sentinel bytes. -/// fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { -/// encoded_buf[*current_idx] = 0; -/// *current_idx += 1; -/// *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); -/// encoded_buf[*current_idx] = 0; -/// *current_idx += 1; -/// } -/// -/// const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; -/// const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 4, 1]; -/// -/// let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); -/// let tc_receiver = SyncTcCacher::default(); -/// let mut tm_source = SyncTmSource::default(); -/// // Insert a telemetry packet which will be read back by the client at a later stage. -/// tm_source.add_tm(&INVERTED_PACKET); -/// let mut tcp_server = TcpTmtcInCobsServer::new( -/// ServerConfig::new(auto_port_addr, Duration::from_millis(2), 1024, 1024), -/// Box::new(tm_source), -/// Box::new(tc_receiver.clone()), -/// ).expect("TCP server generation failed"); -/// let dest_addr = tcp_server.local_addr().expect("retrieving dest addr failed"); -/// let conn_handled: Arc = Default::default(); -/// let set_if_done = conn_handled.clone(); -/// -/// // Call the connection handler in separate thread, does block. -/// thread::spawn(move || { -/// let result = tcp_server.handle_next_connection(); -/// if result.is_err() { -/// panic!("handling connection failed: {:?}", result.unwrap_err()); -/// } -/// let conn_result = result.unwrap(); -/// assert_eq!(conn_result.num_received_tcs, 1, "No TC received"); -/// assert_eq!(conn_result.num_sent_tms, 1, "No TM received"); -/// // Signal the main thread we are done. -/// set_if_done.store(true, Ordering::Relaxed); -/// }); -/// -/// // Send TC to server now. -/// let mut encoded_buf: [u8; 16] = [0; 16]; -/// let mut current_idx = 0; -/// encode_simple_packet(&mut encoded_buf, &mut current_idx); -/// let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); -/// stream -/// .write_all(&encoded_buf[..current_idx]) -/// .expect("writing to TCP server failed"); -/// // Done with writing. -/// stream -/// .shutdown(std::net::Shutdown::Write) -/// .expect("shutting down write failed"); -/// let mut read_buf: [u8; 16] = [0; 16]; -/// let read_len = stream.read(&mut read_buf).expect("read failed"); -/// drop(stream); -/// -/// // 1 byte encoding overhead, 2 sentinel bytes. -/// assert_eq!(read_len, 8); -/// assert_eq!(read_buf[0], 0); -/// assert_eq!(read_buf[read_len - 1], 0); -/// let decoded_len = -/// cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); -/// assert_eq!(decoded_len, 5); -/// // Skip first sentinel byte. -/// assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); -/// // A certain amount of time is allowed for the transaction to complete. -/// for _ in 0..3 { -/// if !conn_handled.load(Ordering::Relaxed) { -/// thread::sleep(Duration::from_millis(5)); -/// } -/// } -/// if !conn_handled.load(Ordering::Relaxed) { -/// panic!("connection was not handled properly"); -/// } -/// // Check that the packet was received and decoded successfully. -/// let mut tc_queue = tc_receiver -/// .tc_queue -/// .lock() -/// .expect("locking tc queue failed"); -/// assert_eq!(tc_queue.len(), 1); -/// assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); -/// drop(tc_queue); -/// ``` +/// The [TCP COBS integration](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_server_cobs.rs) +/// test also serves as the example application for this module. pub struct TcpTmtcInCobsServer { generic_server: TcpTmtcGenericServer, } diff --git a/satrs-core/tests/tcp_server_cobs.rs b/satrs-core/tests/tcp_server_cobs.rs new file mode 100644 index 0000000..bac9437 --- /dev/null +++ b/satrs-core/tests/tcp_server_cobs.rs @@ -0,0 +1,165 @@ +//! This serves as both an integration test and an example application showcasing all major +//! features of the TCP COBS server by performing following steps: +//! +//! 1. It defines both a TC receiver and a TM source which are [Sync]. +//! 2. A telemetry packet is inserted into the TM source. The packet will be handled by the +//! TCP server after handling all TCs. +//! 3. It instantiates the TCP server on localhost with automatic port assignment and assigns +//! the TC receiver and TM source created previously. +//! 4. It moves the TCP server to a different thread and calls the +//! [TcpTmtcInCobsServer::handle_next_connection] call inside that thread +//! 5. The main threads connects to the server, sends a test telecommand and then reads back +//! the test telemetry insertd in to the TM source previously. +use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; +use std::{ + io::{Read, Write}, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, + sync::Mutex, + thread, +}; + +use cobs::encode; +use satrs_core::{ + hal::std::tcp_server::{ServerConfig, TcpTmtcInCobsServer}, + tmtc::{ReceivesTcCore, TmPacketSourceCore}, +}; +use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; + +#[derive(Default, Clone)] +struct SyncTcCacher { + tc_queue: Arc>>>, +} +impl ReceivesTcCore for SyncTcCacher { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed"); + println!("Received TC: {:x?}", tc_raw); + tc_queue.push_back(tc_raw.to_vec()); + Ok(()) + } +} + +#[derive(Default, Clone)] +struct SyncTmSource { + tm_queue: Arc>>>, +} + +impl SyncTmSource { + pub(crate) fn add_tm(&mut self, tm: &[u8]) { + let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec"); + tm_queue.push_back(tm.to_vec()); + } +} + +impl TmPacketSourceCore for SyncTmSource { + type Error = (); + + fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result { + let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed"); + if !tm_queue.is_empty() { + let next_vec = tm_queue.front().unwrap(); + if buffer.len() < next_vec.len() { + panic!( + "provided buffer too small, must be at least {} bytes", + next_vec.len() + ); + } + println!("Sending and encoding TM: {:x?}", next_vec); + let next_vec = tm_queue.pop_front().unwrap(); + buffer[0..next_vec.len()].copy_from_slice(&next_vec); + return Ok(next_vec.len()); + } + Ok(0) + } +} + +// Simple COBS encoder which also inserts the sentinel bytes. +fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; +} + +const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; +const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 4, 1]; + +fn main() { + let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); + let tc_receiver = SyncTcCacher::default(); + let mut tm_source = SyncTmSource::default(); + // Insert a telemetry packet which will be read back by the client at a later stage. + tm_source.add_tm(&INVERTED_PACKET); + let mut tcp_server = TcpTmtcInCobsServer::new( + ServerConfig::new(auto_port_addr, Duration::from_millis(2), 1024, 1024), + Box::new(tm_source), + Box::new(tc_receiver.clone()), + ) + .expect("TCP server generation failed"); + let dest_addr = tcp_server + .local_addr() + .expect("retrieving dest addr failed"); + let conn_handled: Arc = Default::default(); + let set_if_done = conn_handled.clone(); + + // Call the connection handler in separate thread, does block. + thread::spawn(move || { + let result = tcp_server.handle_next_connection(); + if result.is_err() { + panic!("handling connection failed: {:?}", result.unwrap_err()); + } + let conn_result = result.unwrap(); + assert_eq!(conn_result.num_received_tcs, 1, "No TC received"); + assert_eq!(conn_result.num_sent_tms, 1, "No TM received"); + // Signal the main thread we are done. + set_if_done.store(true, Ordering::Relaxed); + }); + + // Send TC to server now. + let mut encoded_buf: [u8; 16] = [0; 16]; + let mut current_idx = 0; + encode_simple_packet(&mut encoded_buf, &mut current_idx); + let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); + stream + .write_all(&encoded_buf[..current_idx]) + .expect("writing to TCP server failed"); + // Done with writing. + stream + .shutdown(std::net::Shutdown::Write) + .expect("shutting down write failed"); + let mut read_buf: [u8; 16] = [0; 16]; + let read_len = stream.read(&mut read_buf).expect("read failed"); + drop(stream); + + // 1 byte encoding overhead, 2 sentinel bytes. + assert_eq!(read_len, 8); + assert_eq!(read_buf[0], 0); + assert_eq!(read_buf[read_len - 1], 0); + let decoded_len = + cobs::decode_in_place(&mut read_buf[1..read_len]).expect("COBS decoding failed"); + assert_eq!(decoded_len, 5); + // Skip first sentinel byte. + assert_eq!(&read_buf[1..1 + INVERTED_PACKET.len()], &INVERTED_PACKET); + // A certain amount of time is allowed for the transaction to complete. + for _ in 0..3 { + if !conn_handled.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(5)); + } + } + if !conn_handled.load(Ordering::Relaxed) { + panic!("connection was not handled properly"); + } + // Check that the packet was received and decoded successfully. + let mut tc_queue = tc_receiver + .tc_queue + .lock() + .expect("locking tc queue failed"); + assert_eq!(tc_queue.len(), 1); + assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET); + drop(tc_queue); +} From 4017b5afc24cc807f92cd1e81c61df591b713cee Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 15:18:20 +0200 Subject: [PATCH 60/68] better module name --- satrs-core/src/{parsers => encoding}/ccsds.rs | 0 satrs-core/src/{parsers => encoding}/cobs.rs | 16 ++++++++++++-- satrs-core/src/encoding/mod.rs | 22 +++++++++++++++++++ .../src/hal/std/tcp_with_cobs_server.rs | 4 ++-- satrs-core/src/lib.rs | 2 +- satrs-core/src/parsers/mod.rs | 21 ------------------ 6 files changed, 39 insertions(+), 26 deletions(-) rename satrs-core/src/{parsers => encoding}/ccsds.rs (100%) rename satrs-core/src/{parsers => encoding}/cobs.rs (92%) create mode 100644 satrs-core/src/encoding/mod.rs delete mode 100644 satrs-core/src/parsers/mod.rs diff --git a/satrs-core/src/parsers/ccsds.rs b/satrs-core/src/encoding/ccsds.rs similarity index 100% rename from satrs-core/src/parsers/ccsds.rs rename to satrs-core/src/encoding/ccsds.rs diff --git a/satrs-core/src/parsers/cobs.rs b/satrs-core/src/encoding/cobs.rs similarity index 92% rename from satrs-core/src/parsers/cobs.rs rename to satrs-core/src/encoding/cobs.rs index 5d906a8..0b0e04a 100644 --- a/satrs-core/src/parsers/cobs.rs +++ b/satrs-core/src/encoding/cobs.rs @@ -1,5 +1,17 @@ use crate::tmtc::ReceivesTcCore; -use cobs::decode_in_place; +use cobs::{decode_in_place, encode}; + +/// This function encodes the given packet with COBS and also wraps the encoded packet with +/// the sentinel value 0. It can be used repeatedly on the same encoded buffer by expecting +/// and incrementing the mutable reference of the current packet index. This is also used +/// to retrieve the total encoded size. +pub fn encode_packet_with_cobs(packet: &[u8], encoded_buf: &mut [u8], current_idx: &mut usize) { + encoded_buf[*current_idx] = 0; + *current_idx += 1; + *current_idx += encode(packet, &mut encoded_buf[*current_idx..]); + encoded_buf[*current_idx] = 0; + *current_idx += 1; +} /// This function parses a given buffer for COBS encoded packets. The packet structure is /// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: @@ -58,7 +70,7 @@ pub(crate) mod tests { use cobs::encode; use crate::{ - parsers::tests::{encode_simple_packet, INVERTED_PACKET, SIMPLE_PACKET}, + encoding::tests::{encode_simple_packet, INVERTED_PACKET, SIMPLE_PACKET}, tmtc::ReceivesTcCore, }; diff --git a/satrs-core/src/encoding/mod.rs b/satrs-core/src/encoding/mod.rs new file mode 100644 index 0000000..33d2727 --- /dev/null +++ b/satrs-core/src/encoding/mod.rs @@ -0,0 +1,22 @@ +pub mod ccsds; +pub mod cobs; + +pub use crate::encoding::ccsds::parse_buffer_for_ccsds_space_packets; +pub use crate::encoding::cobs::parse_buffer_for_cobs_encoded_packets; + +#[cfg(test)] +pub(crate) mod tests { + use super::cobs::encode_packet_with_cobs; + + pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; + pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; + + pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encode_packet_with_cobs(&SIMPLE_PACKET, encoded_buf, current_idx) + } + + #[allow(dead_code)] + pub(crate) fn encode_inverted_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { + encode_packet_with_cobs(&INVERTED_PACKET, encoded_buf, current_idx) + } +} diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index ec080e2..357b07d 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -8,7 +8,7 @@ use std::net::TcpListener; use std::net::TcpStream; use std::vec::Vec; -use crate::parsers::parse_buffer_for_cobs_encoded_packets; +use crate::encoding::parse_buffer_for_cobs_encoded_packets; use crate::tmtc::ReceivesTc; use crate::tmtc::TmPacketSource; @@ -171,8 +171,8 @@ mod tests { }; use crate::{ + encoding::tests::{INVERTED_PACKET, SIMPLE_PACKET}, hal::std::tcp_server::ServerConfig, - parsers::tests::{INVERTED_PACKET, SIMPLE_PACKET}, tmtc::{ReceivesTcCore, TmPacketSourceCore}, }; use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 50e9d42..8ae56e5 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -20,6 +20,7 @@ extern crate downcast_rs; #[cfg(any(feature = "std", test))] extern crate std; +pub mod encoding; pub mod error; #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] @@ -33,7 +34,6 @@ pub mod hk; pub mod mode; pub mod objects; pub mod params; -pub mod parsers; pub mod pool; pub mod power; pub mod pus; diff --git a/satrs-core/src/parsers/mod.rs b/satrs-core/src/parsers/mod.rs deleted file mode 100644 index c25060a..0000000 --- a/satrs-core/src/parsers/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod ccsds; -pub mod cobs; - -pub use crate::parsers::ccsds::parse_buffer_for_ccsds_space_packets; -pub use crate::parsers::cobs::parse_buffer_for_cobs_encoded_packets; - -#[cfg(test)] -pub(crate) mod tests { - use cobs::encode; - - pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; - pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; - - pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encoded_buf[*current_idx] = 0; - *current_idx += 1; - *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); - encoded_buf[*current_idx] = 0; - *current_idx += 1; - } -} From 1851b7427972723c8b0d9ce68d5b8b9b9c59c738 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 15:20:14 +0200 Subject: [PATCH 61/68] use new public function --- satrs-core/src/encoding/mod.rs | 2 +- satrs-core/tests/tcp_server_cobs.rs | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/satrs-core/src/encoding/mod.rs b/satrs-core/src/encoding/mod.rs index 33d2727..20973f2 100644 --- a/satrs-core/src/encoding/mod.rs +++ b/satrs-core/src/encoding/mod.rs @@ -2,7 +2,7 @@ pub mod ccsds; pub mod cobs; pub use crate::encoding::ccsds::parse_buffer_for_ccsds_space_packets; -pub use crate::encoding::cobs::parse_buffer_for_cobs_encoded_packets; +pub use crate::encoding::cobs::{encode_packet_with_cobs, parse_buffer_for_cobs_encoded_packets}; #[cfg(test)] pub(crate) mod tests { diff --git a/satrs-core/tests/tcp_server_cobs.rs b/satrs-core/tests/tcp_server_cobs.rs index bac9437..5956fb3 100644 --- a/satrs-core/tests/tcp_server_cobs.rs +++ b/satrs-core/tests/tcp_server_cobs.rs @@ -21,8 +21,8 @@ use std::{ thread, }; -use cobs::encode; use satrs_core::{ + encoding::cobs::encode_packet_with_cobs, hal::std::tcp_server::{ServerConfig, TcpTmtcInCobsServer}, tmtc::{ReceivesTcCore, TmPacketSourceCore}, }; @@ -77,15 +77,6 @@ impl TmPacketSourceCore for SyncTmSource { } } -// Simple COBS encoder which also inserts the sentinel bytes. -fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encoded_buf[*current_idx] = 0; - *current_idx += 1; - *current_idx += encode(&SIMPLE_PACKET, &mut encoded_buf[*current_idx..]); - encoded_buf[*current_idx] = 0; - *current_idx += 1; -} - const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 4, 1]; @@ -123,7 +114,7 @@ fn main() { // Send TC to server now. let mut encoded_buf: [u8; 16] = [0; 16]; let mut current_idx = 0; - encode_simple_packet(&mut encoded_buf, &mut current_idx); + encode_packet_with_cobs(&SIMPLE_PACKET, &mut encoded_buf, &mut current_idx); let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); stream .write_all(&encoded_buf[..current_idx]) From 1517811d1352c6b7278631a589fe866608d9fc3b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 15:22:37 +0200 Subject: [PATCH 62/68] better docs --- satrs-core/src/hal/std/tcp_with_cobs_server.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/hal/std/tcp_with_cobs_server.rs b/satrs-core/src/hal/std/tcp_with_cobs_server.rs index 357b07d..fc44ebf 100644 --- a/satrs-core/src/hal/std/tcp_with_cobs_server.rs +++ b/satrs-core/src/hal/std/tcp_with_cobs_server.rs @@ -104,7 +104,8 @@ impl TcpTmSender for CobsTmSender { /// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full /// packets even from a data stream which is split up. The server wil use the /// [parse_buffer_for_cobs_encoded_packets] function to parse for packets and pass them to a -/// generic TC receiver. +/// generic TC receiver. The user can use [crate::encoding::encode_packet_with_cobs] to encode +/// telecommands sent to the server. /// /// ## Example /// From 3f73b73ded52c392b21ea646070023b71f96b6c3 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 15:45:06 +0200 Subject: [PATCH 63/68] add doctest for encoder function --- satrs-core/src/encoding/cobs.rs | 36 +++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/satrs-core/src/encoding/cobs.rs b/satrs-core/src/encoding/cobs.rs index 0b0e04a..6fa4901 100644 --- a/satrs-core/src/encoding/cobs.rs +++ b/satrs-core/src/encoding/cobs.rs @@ -1,16 +1,48 @@ use crate::tmtc::ReceivesTcCore; -use cobs::{decode_in_place, encode}; +use cobs::{decode_in_place, encode, max_encoding_length}; /// This function encodes the given packet with COBS and also wraps the encoded packet with /// the sentinel value 0. It can be used repeatedly on the same encoded buffer by expecting /// and incrementing the mutable reference of the current packet index. This is also used /// to retrieve the total encoded size. -pub fn encode_packet_with_cobs(packet: &[u8], encoded_buf: &mut [u8], current_idx: &mut usize) { +/// +/// This function will return [false] if the given encoding buffer is not large enough to hold +/// the encoded buffer and the two sentinel bytes and [true] if the encoding was successfull. +/// +/// ## Example +/// +/// ``` +/// use cobs::decode_in_place_report; +/// use satrs_core::encoding::{encode_packet_with_cobs}; +// +/// const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; +/// const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; +/// +/// let mut encoding_buf: [u8; 32] = [0; 32]; +/// let mut current_idx = 0; +/// assert!(encode_packet_with_cobs(&SIMPLE_PACKET, &mut encoding_buf, &mut current_idx)); +/// assert!(encode_packet_with_cobs(&INVERTED_PACKET, &mut encoding_buf, &mut current_idx)); +/// assert_eq!(encoding_buf[0], 0); +/// let dec_report = decode_in_place_report(&mut encoding_buf[1..]).expect("decoding failed"); +/// assert_eq!(encoding_buf[1 + dec_report.src_used], 0); +/// assert_eq!(dec_report.dst_used, 5); +/// assert_eq!(current_idx, 16); +/// ``` +pub fn encode_packet_with_cobs( + packet: &[u8], + encoded_buf: &mut [u8], + current_idx: &mut usize, +) -> bool { + let max_encoding_len = max_encoding_length(packet.len()); + if *current_idx + max_encoding_len + 2 > encoded_buf.len() { + return false; + } encoded_buf[*current_idx] = 0; *current_idx += 1; *current_idx += encode(packet, &mut encoded_buf[*current_idx..]); encoded_buf[*current_idx] = 0; *current_idx += 1; + true } /// This function parses a given buffer for COBS encoded packets. The packet structure is From e717999cb0c5b4db4e4f98ab7e9968c4938f0758 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 20 Sep 2023 17:14:07 +0200 Subject: [PATCH 64/68] cobs --- satrs-core/src/encoding/cobs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-core/src/encoding/cobs.rs b/satrs-core/src/encoding/cobs.rs index 6fa4901..28ed3a1 100644 --- a/satrs-core/src/encoding/cobs.rs +++ b/satrs-core/src/encoding/cobs.rs @@ -48,7 +48,7 @@ pub fn encode_packet_with_cobs( /// This function parses a given buffer for COBS encoded packets. The packet structure is /// expected to be like this, assuming a sentinel value of 0 as the packet delimiter: /// -/// 0 | ... Packet Data ... | 0 | 0 | ... Packet Data ... | 0 +/// 0 | ... Encoded Packet Data ... | 0 | 0 | ... Encoded Packet Data ... | 0 /// /// This function is also able to deal with broken tail packets at the end. If broken tail /// packets are detected, they are moved to the front of the buffer, and the write index for From c3bce2774757212afb7db629d1be92b936947fd0 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 15:11:00 +0200 Subject: [PATCH 65/68] push some progress --- satrs-core/src/encoding/ccsds.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/encoding/ccsds.rs b/satrs-core/src/encoding/ccsds.rs index fe84f3f..141e81a 100644 --- a/satrs-core/src/encoding/ccsds.rs +++ b/satrs-core/src/encoding/ccsds.rs @@ -55,6 +55,7 @@ impl PacketIdLookup for &[PacketId] { false } } + /// This function parses a given buffer for tightly packed CCSDS space packets. It uses the /// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then /// uses the length field of the packet to extract CCSDS packets. @@ -101,4 +102,12 @@ pub fn parse_buffer_for_ccsds_space_packets( } #[cfg(test)] -mod tests {} +mod tests { + use spacepackets::{ecss::tc::PusTcCreator, SpHeader}; + + #[test] + fn test_basic() { + let sph = SpHeader::tc_unseg(0x02, 0, 0); + let ping_tc = PusTcCreator::new_simple(sph, service, subservice, app_data, set_ccsds_len) + } +} From 39621cf855af75d8de251d404aa571f27c5895fc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 16:34:18 +0200 Subject: [PATCH 66/68] added more tests --- Cargo.toml | 3 +- satrs-core/src/encoding/ccsds.rs | 117 +++++++++++++++++++++++-------- satrs-core/src/encoding/cobs.rs | 20 +----- satrs-core/src/encoding/mod.rs | 22 +++++- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c9d35a8..eaeb356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] - +resolver = "2" members = [ "satrs-core", "satrs-mib", @@ -9,3 +9,4 @@ members = [ exclude = [ "satrs-example-stm32f3-disco", ] + diff --git a/satrs-core/src/encoding/ccsds.rs b/satrs-core/src/encoding/ccsds.rs index 141e81a..b6ccef6 100644 --- a/satrs-core/src/encoding/ccsds.rs +++ b/satrs-core/src/encoding/ccsds.rs @@ -17,13 +17,6 @@ impl PacketIdLookup for Vec { } } -#[cfg(feature = "alloc")] -impl PacketIdLookup for Vec { - fn validate(&self, packet_id: u16) -> bool { - self.contains(&PacketId::from(packet_id)) - } -} - #[cfg(feature = "alloc")] impl PacketIdLookup for HashSet { fn validate(&self, packet_id: u16) -> bool { @@ -31,6 +24,18 @@ impl PacketIdLookup for HashSet { } } +impl PacketIdLookup for [u16] { + fn validate(&self, packet_id: u16) -> bool { + self.binary_search(&packet_id).is_ok() + } +} + +#[cfg(feature = "alloc")] +impl PacketIdLookup for Vec { + fn validate(&self, packet_id: u16) -> bool { + self.contains(&PacketId::from(packet_id)) + } +} #[cfg(feature = "alloc")] impl PacketIdLookup for HashSet { fn validate(&self, packet_id: u16) -> bool { @@ -38,21 +43,9 @@ impl PacketIdLookup for HashSet { } } -impl PacketIdLookup for &[u16] { +impl PacketIdLookup for [PacketId] { fn validate(&self, packet_id: u16) -> bool { - if self.binary_search(&packet_id).is_ok() { - return true; - } - false - } -} - -impl PacketIdLookup for &[PacketId] { - fn validate(&self, packet_id: u16) -> bool { - if self.binary_search(&PacketId::from(packet_id)).is_ok() { - return true; - } - false + self.binary_search(&PacketId::from(packet_id)).is_ok() } } @@ -65,14 +58,16 @@ impl PacketIdLookup for &[PacketId] { /// detected, they are moved to the front of the buffer, and the write index for future write /// operations will be written to the `next_write_idx` argument. /// -/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. +/// The parser will write all packets which were decoded successfully to the given `tc_receiver` +/// and return the number of packets found. If the [ReceivesTcCore::pass_tc] calls fails, the +/// error will be returned. pub fn parse_buffer_for_ccsds_space_packets( buf: &mut [u8], - packet_id_lookup: &dyn PacketIdLookup, - tc_receiver: &mut dyn ReceivesTcCore, + packet_id_lookup: &(impl PacketIdLookup + ?Sized), + tc_receiver: &mut impl ReceivesTcCore, next_write_idx: &mut usize, ) -> Result { - let packets_found = 0; + let mut packets_found = 0; let mut current_idx = 0; let buf_len = buf.len(); loop { @@ -86,6 +81,7 @@ pub fn parse_buffer_for_ccsds_space_packets( let packet_size = length_field + 7; if (current_idx + packet_size as usize) < buf_len { tc_receiver.pass_tc(&buf[current_idx..current_idx + packet_size as usize])?; + packets_found += 1; } else { // Move packet to start of buffer if applicable. if current_idx > 0 { @@ -103,11 +99,76 @@ pub fn parse_buffer_for_ccsds_space_packets( #[cfg(test)] mod tests { - use spacepackets::{ecss::tc::PusTcCreator, SpHeader}; + use spacepackets::{ + ecss::{tc::PusTcCreator, SerializablePusPacket}, + PacketId, SpHeader, + }; + + use crate::encoding::tests::TcCacher; + + use super::parse_buffer_for_ccsds_space_packets; + + const TEST_APID: u16 = 0x02; #[test] fn test_basic() { - let sph = SpHeader::tc_unseg(0x02, 0, 0); - let ping_tc = PusTcCreator::new_simple(sph, service, subservice, app_data, set_ccsds_len) + let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); + let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); + let mut buffer: [u8; 32] = [0; 32]; + let packet_len = ping_tc + .write_to_bytes(&mut buffer) + .expect("writing packet failed"); + let valid_packet_ids = [PacketId::const_tc(true, TEST_APID)]; + let mut tc_cacher = TcCacher::default(); + let mut next_write_idx = 0; + let parse_result = parse_buffer_for_ccsds_space_packets( + &mut buffer, + valid_packet_ids.as_slice(), + &mut tc_cacher, + &mut next_write_idx, + ); + assert!(parse_result.is_ok()); + let parsed_packets = parse_result.unwrap(); + assert_eq!(parsed_packets, 1); + assert_eq!(tc_cacher.tc_queue.len(), 1); + assert_eq!( + tc_cacher.tc_queue.pop_front().unwrap(), + buffer[..packet_len] + ); + } + + #[test] + fn test_multi_pakcet() { + let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); + let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); + let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true); + let mut buffer: [u8; 32] = [0; 32]; + let packet_len_ping = ping_tc + .write_to_bytes(&mut buffer) + .expect("writing packet failed"); + let _packet_len_action = action_tc + .write_to_bytes(&mut buffer[packet_len_ping..]) + .expect("writing packet failed"); + let valid_packet_ids = [PacketId::const_tc(true, TEST_APID)]; + let mut tc_cacher = TcCacher::default(); + let mut next_write_idx = 0; + let parse_result = parse_buffer_for_ccsds_space_packets( + &mut buffer, + valid_packet_ids.as_slice(), + &mut tc_cacher, + &mut next_write_idx, + ); + assert!(parse_result.is_ok()); + let parsed_packets = parse_result.unwrap(); + assert_eq!(parsed_packets, 1); + assert_eq!(tc_cacher.tc_queue.len(), 2); + assert_eq!( + tc_cacher.tc_queue.pop_front().unwrap(), + buffer[..packet_len_ping] + ); + assert_eq!( + tc_cacher.tc_queue.pop_front().unwrap(), + buffer[packet_len_ping..] + ); } } diff --git a/satrs-core/src/encoding/cobs.rs b/satrs-core/src/encoding/cobs.rs index 28ed3a1..2645745 100644 --- a/satrs-core/src/encoding/cobs.rs +++ b/satrs-core/src/encoding/cobs.rs @@ -98,30 +98,12 @@ pub fn parse_buffer_for_cobs_encoded_packets( #[cfg(test)] pub(crate) mod tests { - use alloc::{collections::VecDeque, vec::Vec}; use cobs::encode; - use crate::{ - encoding::tests::{encode_simple_packet, INVERTED_PACKET, SIMPLE_PACKET}, - tmtc::ReceivesTcCore, - }; + use crate::encoding::tests::{encode_simple_packet, TcCacher, INVERTED_PACKET, SIMPLE_PACKET}; use super::parse_buffer_for_cobs_encoded_packets; - #[derive(Default)] - struct TcCacher { - tc_queue: VecDeque>, - } - - impl ReceivesTcCore for TcCacher { - type Error = (); - - fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { - self.tc_queue.push_back(tc_raw.to_vec()); - Ok(()) - } - } - #[test] fn test_parsing_simple_packet() { let mut test_sender = TcCacher::default(); diff --git a/satrs-core/src/encoding/mod.rs b/satrs-core/src/encoding/mod.rs index 20973f2..94e3dee 100644 --- a/satrs-core/src/encoding/mod.rs +++ b/satrs-core/src/encoding/mod.rs @@ -6,17 +6,35 @@ pub use crate::encoding::cobs::{encode_packet_with_cobs, parse_buffer_for_cobs_e #[cfg(test)] pub(crate) mod tests { + use alloc::{collections::VecDeque, vec::Vec}; + + use crate::tmtc::ReceivesTcCore; + use super::cobs::encode_packet_with_cobs; pub(crate) const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; pub(crate) const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; + #[derive(Default)] + pub(crate) struct TcCacher { + pub(crate) tc_queue: VecDeque>, + } + + impl ReceivesTcCore for TcCacher { + type Error = (); + + fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> { + self.tc_queue.push_back(tc_raw.to_vec()); + Ok(()) + } + } + pub(crate) fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encode_packet_with_cobs(&SIMPLE_PACKET, encoded_buf, current_idx) + encode_packet_with_cobs(&SIMPLE_PACKET, encoded_buf, current_idx); } #[allow(dead_code)] pub(crate) fn encode_inverted_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { - encode_packet_with_cobs(&INVERTED_PACKET, encoded_buf, current_idx) + encode_packet_with_cobs(&INVERTED_PACKET, encoded_buf, current_idx); } } From 0d49dbcc2a556127ecc13b15170b13ade28cf5ad Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 16:44:52 +0200 Subject: [PATCH 67/68] Another test --- satrs-core/src/encoding/ccsds.rs | 61 +++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/satrs-core/src/encoding/ccsds.rs b/satrs-core/src/encoding/ccsds.rs index b6ccef6..7012e1f 100644 --- a/satrs-core/src/encoding/ccsds.rs +++ b/satrs-core/src/encoding/ccsds.rs @@ -108,17 +108,20 @@ mod tests { use super::parse_buffer_for_ccsds_space_packets; - const TEST_APID: u16 = 0x02; + const TEST_APID_0: u16 = 0x02; + const TEST_APID_1: u16 = 0x10; + const TEST_PACKET_ID_0: PacketId = PacketId::const_tc(true, TEST_APID_0); + const TEST_PACKET_ID_1: PacketId = PacketId::const_tc(true, TEST_APID_1); #[test] fn test_basic() { - let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); + let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap(); let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); let mut buffer: [u8; 32] = [0; 32]; let packet_len = ping_tc .write_to_bytes(&mut buffer) .expect("writing packet failed"); - let valid_packet_ids = [PacketId::const_tc(true, TEST_APID)]; + let valid_packet_ids = [TEST_PACKET_ID_0]; let mut tc_cacher = TcCacher::default(); let mut next_write_idx = 0; let parse_result = parse_buffer_for_ccsds_space_packets( @@ -138,18 +141,18 @@ mod tests { } #[test] - fn test_multi_pakcet() { - let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); + fn test_multi_packet() { + let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap(); let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true); let mut buffer: [u8; 32] = [0; 32]; let packet_len_ping = ping_tc .write_to_bytes(&mut buffer) .expect("writing packet failed"); - let _packet_len_action = action_tc + let packet_len_action = action_tc .write_to_bytes(&mut buffer[packet_len_ping..]) .expect("writing packet failed"); - let valid_packet_ids = [PacketId::const_tc(true, TEST_APID)]; + let valid_packet_ids = [TEST_PACKET_ID_0]; let mut tc_cacher = TcCacher::default(); let mut next_write_idx = 0; let parse_result = parse_buffer_for_ccsds_space_packets( @@ -160,7 +163,7 @@ mod tests { ); assert!(parse_result.is_ok()); let parsed_packets = parse_result.unwrap(); - assert_eq!(parsed_packets, 1); + assert_eq!(parsed_packets, 2); assert_eq!(tc_cacher.tc_queue.len(), 2); assert_eq!( tc_cacher.tc_queue.pop_front().unwrap(), @@ -168,7 +171,47 @@ mod tests { ); assert_eq!( tc_cacher.tc_queue.pop_front().unwrap(), - buffer[packet_len_ping..] + buffer[packet_len_ping..packet_len_ping + packet_len_action] ); } + + #[test] + fn test_multi_apid() { + let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap(); + let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); + sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap(); + let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true); + let mut buffer: [u8; 32] = [0; 32]; + let packet_len_ping = ping_tc + .write_to_bytes(&mut buffer) + .expect("writing packet failed"); + let packet_len_action = action_tc + .write_to_bytes(&mut buffer[packet_len_ping..]) + .expect("writing packet failed"); + let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1]; + let mut tc_cacher = TcCacher::default(); + let mut next_write_idx = 0; + let parse_result = parse_buffer_for_ccsds_space_packets( + &mut buffer, + valid_packet_ids.as_slice(), + &mut tc_cacher, + &mut next_write_idx, + ); + assert!(parse_result.is_ok()); + let parsed_packets = parse_result.unwrap(); + assert_eq!(parsed_packets, 2); + assert_eq!(tc_cacher.tc_queue.len(), 2); + assert_eq!( + tc_cacher.tc_queue.pop_front().unwrap(), + buffer[..packet_len_ping] + ); + assert_eq!( + tc_cacher.tc_queue.pop_front().unwrap(), + buffer[packet_len_ping..packet_len_ping + packet_len_action] + ); + } + + #[test] + fn test_split_packet() { + } } From 216874d32913d18b0635408c393be62916104760 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 18:08:40 +0200 Subject: [PATCH 68/68] CCSDS parser working well --- satrs-core/src/encoding/ccsds.rs | 62 +++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/satrs-core/src/encoding/ccsds.rs b/satrs-core/src/encoding/ccsds.rs index 7012e1f..f8f775f 100644 --- a/satrs-core/src/encoding/ccsds.rs +++ b/satrs-core/src/encoding/ccsds.rs @@ -54,9 +54,9 @@ impl PacketIdLookup for [PacketId] { /// uses the length field of the packet to extract CCSDS packets. /// /// This function is also able to deal with broken tail packets at the end as long a the parser -/// can read the full 6 bytes which constitue a space packet header. If broken tail packets are -/// detected, they are moved to the front of the buffer, and the write index for future write -/// operations will be written to the `next_write_idx` argument. +/// can read the full 7 bytes which constitue a space packet header plus one byte minimal size. +/// If broken tail packets are detected, they are moved to the front of the buffer, and the write +/// index for future write operations will be written to the `next_write_idx` argument. /// /// The parser will write all packets which were decoded successfully to the given `tc_receiver` /// and return the number of packets found. If the [ReceivesTcCore::pass_tc] calls fails, the @@ -67,6 +67,7 @@ pub fn parse_buffer_for_ccsds_space_packets( tc_receiver: &mut impl ReceivesTcCore, next_write_idx: &mut usize, ) -> Result { + *next_write_idx = 0; let mut packets_found = 0; let mut current_idx = 0; let buf_len = buf.len(); @@ -86,7 +87,7 @@ pub fn parse_buffer_for_ccsds_space_packets( // Move packet to start of buffer if applicable. if current_idx > 0 { buf.copy_within(current_idx.., 0); - *next_write_idx = current_idx; + *next_write_idx = buf.len() - current_idx; } } current_idx += packet_size as usize; @@ -212,6 +213,57 @@ mod tests { } #[test] - fn test_split_packet() { + fn test_split_packet_multi() { + let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap(); + let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); + sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap(); + let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true); + let mut buffer: [u8; 32] = [0; 32]; + let packet_len_ping = ping_tc + .write_to_bytes(&mut buffer) + .expect("writing packet failed"); + let packet_len_action = action_tc + .write_to_bytes(&mut buffer[packet_len_ping..]) + .expect("writing packet failed"); + let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1]; + let mut tc_cacher = TcCacher::default(); + let mut next_write_idx = 0; + let parse_result = parse_buffer_for_ccsds_space_packets( + &mut buffer[..packet_len_ping + packet_len_action - 4], + valid_packet_ids.as_slice(), + &mut tc_cacher, + &mut next_write_idx, + ); + assert!(parse_result.is_ok()); + let parsed_packets = parse_result.unwrap(); + assert_eq!(parsed_packets, 1); + assert_eq!(tc_cacher.tc_queue.len(), 1); + // The broken packet was moved to the start, so the next write index should be after the + // last segment missing 4 bytes. + assert_eq!(next_write_idx, packet_len_action - 4); + } + + #[test] + fn test_one_split_packet() { + let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap(); + let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true); + let mut buffer: [u8; 32] = [0; 32]; + let packet_len_ping = ping_tc + .write_to_bytes(&mut buffer) + .expect("writing packet failed"); + let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1]; + let mut tc_cacher = TcCacher::default(); + let mut next_write_idx = 0; + let parse_result = parse_buffer_for_ccsds_space_packets( + &mut buffer[..packet_len_ping - 4], + valid_packet_ids.as_slice(), + &mut tc_cacher, + &mut next_write_idx, + ); + assert_eq!(next_write_idx, 0); + assert!(parse_result.is_ok()); + let parsed_packets = parse_result.unwrap(); + assert_eq!(parsed_packets, 0); + assert_eq!(tc_cacher.tc_queue.len(), 0); } }