Compare commits

..
Author SHA1 Message Date
muellerr 2eaa78dfbc this actually works!
Rust/sat-rs/pipeline/head This commit looks good
2024-05-25 13:46:14 +02:00
muellerr a6d9bee5df Merge branch 'sim-mgm-update' into serialization-prototyping
Rust/sat-rs/pipeline/head This commit looks good
2024-05-25 13:09:25 +02:00
muellerr a77bbfa953 Merge remote-tracking branch 'origin/main' into serialization-prototyping 2024-05-25 13:08:54 +02:00
muellerr 4c67bcdde1 clean up serializatio ntest code 2024-05-25 13:08:32 +02:00
muellerr a710b30013 Merge remote-tracking branch 'origin/main' into serialization-prototyping
Rust/sat-rs/pipeline/head This commit looks good
2024-05-25 12:31:51 +02:00
muellerr 29783b2b07 introduce new HK helper
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-25 12:29:44 +02:00
muellerr 2a2a3a3eab PCDU switch set TM handling
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-22 18:48:46 +02:00
muellerr 2507469e68 continue PCDU integration
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-22 18:34:37 +02:00
muellerr b4febefa33 introduce switch handling for MGM
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-22 16:48:51 +02:00
muellerr fe60cb9ccf continue integrating power subsystem
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-19 17:33:37 +02:00
muellerr 27e88ed7f7 fix tests
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-18 18:45:42 +02:00
muellerr 295fed9a72 continue PCDU handler
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-18 18:39:25 +02:00
muellerr 8e89c8dd66 compiles again
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-18 17:58:54 +02:00
muellerr cb0a65c4d4 continue PCDU
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-18 14:08:42 +02:00
muellerr 3db54da3df Merge remote-tracking branch 'origin/main' into sim-mgm-update
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-18 12:49:20 +02:00
muellerr 15fcb17363 continue PCDU handler
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-16 16:28:22 +02:00
muellerr 8728c7ebea continued sample PCDU handler
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-12 14:23:42 +02:00
muellerr 7606767f63 the PCDU handler is already required
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-11 19:11:41 +02:00
muellerr 37b32a9008 try to make MGM set HK data work
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-10 17:55:11 +02:00
muellerr 9e096193dd clean up python commander a bit 2024-05-10 17:21:59 +02:00
muellerr 43bd77eef0 check that MGM data conversion works
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-10 15:33:43 +02:00
muellerr a4888bce01 add first MGM device unittests 2024-05-09 21:38:56 +02:00
muellerr 6e5b70af34 basic tests for SIM client
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-09 13:23:40 +02:00
muellerr d1476eb770 added basic tests for pytmtc app
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-09 11:41:11 +02:00
muellerr 783388aa6f pytmtc as regular package now
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-09 11:07:08 +02:00
muellerr 4a8db6b26a fix tests
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-08 21:08:41 +02:00
muellerr b86c2eb1d1 added some test stubs
Rust/sat-rs/pipeline/head Build started...
2024-05-08 21:02:16 +02:00
muellerr fe4126f7e2 first connection success
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-05-08 20:55:56 +02:00
muellerr c20163b10a start integrating sim in example
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-05-08 20:38:45 +02:00
muellerr b970154488 add serialization prototyping
Rust/sat-rs/pipeline/head This commit looks good
2024-04-26 10:01:29 +02:00
320 changed files with 166884 additions and 32998 deletions

No files matched your search

+3 -14
View File
@@ -11,12 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install libudev-dev on Ubuntu
if: ${{ matrix.os == 'ubuntu-latest' }}
run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo check
# Check example with static pool configuration
- run: cargo check -p example-std --no-default-features
- run: cargo check --release
test:
name: Run Tests
@@ -26,7 +21,6 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo nextest run --all-features
- run: cargo test --doc --all-features
@@ -43,7 +37,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
- run: cargo check -p satrs --target=${{matrix.target}} --no-default-features
- run: cargo check -p satrs --release --target=${{matrix.target}} --no-default-features
fmt:
name: Check formatting
@@ -51,8 +45,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check
docs:
@@ -61,7 +53,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
- run: cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docs_rs"]'
clippy:
name: Clippy
@@ -69,7 +61,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo clippy -- -D warnings
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Check" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="check" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+18
View File
@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy Fix" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy --fix" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docs" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="doc --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Doctest" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test --doc" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+18
View File
@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Examples" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --example test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="fmt" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+18
View File
@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw example" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run -p satrs-example --bin satrs-example" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw simple client" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package fsrc-example --bin client" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
<option name="command" value="test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+18
View File
@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test All" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -- --include-ignored" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+19
View File
@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test satrs-core" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -p satrs-core --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+6 -8
View File
@@ -3,16 +3,14 @@ resolver = "2"
members = [
"satrs",
"satrs-mib",
"examples/example-std",
"examples/types",
"examples/client",
"examples/minisim",
"examples/minisim-types",
"satrs-example",
"satrs-minisim",
"satrs-shared",
"tmtc-utils",
]
exclude = [
"examples/stm32h7-nucleo-rtic",
"examples/stm32h7-nucleo-embassy",
"embedded-examples/stm32f3-disco-rtic",
"embedded-examples/stm32h7-rtic",
"serialization-prototyping",
]
+18 -35
View File
@@ -1,9 +1,9 @@
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p>
[![sat-rs book](https://img.shields.io/badge/sat--rs-book-darkgreen?style=flat)](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
[![sat-rs website](https://img.shields.io/badge/sat--rs-website-darkgreen?style=flat)](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
[![sat-rs book](https://img.shields.io/badge/sat--rs-book-darkgreen?style=flat)](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/)
[![Crates.io](https://img.shields.io/crates/v/satrs)](https://crates.io/crates/satrs)
[![docs.rs](https://img.shields.io/docsrs/satrs)](https://docs.rs/satrs)
[![matrix chat](https://img.shields.io/matrix/sat-rs%3Amatrix.org)](https://matrix.to/#/#sat-rs:matrix.org)
sat-rs
=========
@@ -11,8 +11,8 @@ sat-rs
This is the repository of the sat-rs library. 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. You can find an overview of the project and the
link to the [more high-level sat-rs book](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
at the [IRS software projects website](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/).
link to the [more high-level sat-rs book](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS software projects website](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
This is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
@@ -26,40 +26,29 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
# Overview
This project currently contains the following crates:
This project currently contains following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
Primary information resource in addition to the API documentation, hosted
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/). It can be useful to read
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read
this first before delving into the example application and the API documentation.
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
Primary crate.
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
Example of a simple example on-board software using various sat-rs components which can be run
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/sat-rs/src/branch/main/satrs-mib):
Components to build a mission information base from the on-board software directly.
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/satrs-stm32f3-disco-rtic):
Example of a simple example using low-level sat-rs components on a bare-metal system
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
framework on the STM32F3-Discovery device.
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/satrs-stm32h7-nucleo-rtic):
Example of a simple example using sat-rs components on a bare-metal system
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
framework on the STM32H743ZIT device.
## Examples
All examples and their helper crates are located inside the
[`examples`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples) folder:
* [`example-std`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std):
Example on-board software using various sat-rs components which can be run on a host computer
or on any system with a standard runtime like a Raspberry Pi.
* [`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim):
Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which
simulates some physical devices for the `example-std` application device handlers.
* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application and the STM32H7 examples.
* [`types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/types): Telecommand and telemetry definitions shared by the
`example-std` application, the STM32H7 examples and the `client`.
* [`stm32h7-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-rtic):
Simple example using sat-rs components on a bare-metal system with constrained resources.
This example uses the [RTIC](https://github.com/rtic-rs/rtic) framework on the NUCLEO-H753ZI
board.
* [`stm32h7-nucleo-embassy`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-embassy):
Same as `stm32h7-nucleo-rtic`, but using the [embassy](https://embassy.dev/) executor instead
of RTIC.
The library crates and the `example-std` application have their own `CHANGELOG.md`.
Each project has its own `CHANGELOG.md`.
# Related projects
@@ -69,8 +58,6 @@ The library crates and the `example-std` application have their own `CHANGELOG.m
packet protocol implementations. This repository is re-exported in the
[`satrs`](https://egit.irs.uni-stuttgart.de/rust/satrs/src/branch/main/satrs)
crate.
* [`cfdp`](https://egit.irs.uni-stuttgart.de/rust/cfdp): CCSDS File Delivery Protocol
(CFDP) high-level library components.
# Flight Heritage
@@ -82,10 +69,6 @@ Currently this library has the following flight heritage:
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
The application is strongly based on the sat-rs example application. You can find the repository
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
alongside a Flight System Simulator in the context of a
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
# Coverage
+1 -1
View File
@@ -47,7 +47,7 @@ def main():
parser.add_argument(
"-p",
"--package",
choices=["satrs", "minisim", "example-std"],
choices=["satrs", "satrs-minisim", "satrs-example"],
default="satrs",
help="Choose project to generate coverage for",
)
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
cargo +nightly doc --all-features --open
@@ -0,0 +1,37 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
# You can also replace openocd.gdb by jlink.gdb when using a J-Link.
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
# runner = "gdb-multiarch -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
runner = "probe-rs run --chip STM32F303VCTx"
rustflags = [
"-C", "linker=flip-link",
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# if you run into problems with LLD switch to the GNU linker by commenting out
# this line
# "-C", "linker=arm-none-eabi-ld",
# if you need to link to pre-compiled C libraries provided by a C toolchain
# use GCC as the linker by commenting out both lines above and then
# uncommenting the three lines below
# "-C", "linker=arm-none-eabi-gcc",
# "-C", "link-arg=-Wl,-Tlink.x",
# "-C", "link-arg=-nostartfiles",
]
[build]
# comment out the following line if you intend to run unit tests on host machine
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[env]
DEFMT_LOG = "info"
@@ -0,0 +1,4 @@
/target
/itm.txt
/.cargo/config*
/.vscode
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,84 @@
[package]
name = "satrs-stm32f3-disco-rtic"
version = "0.1.0"
edition = "2021"
default-run = "satrs-stm32f3-disco-rtic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
panic-probe = { version = "0.3", features = ["print-defmt"] }
embedded-hal = "0.2.7"
cortex-m-semihosting = "0.5.0"
enumset = "1"
heapless = "0.8"
[dependencies.rtic]
version = "2"
features = ["thumbv7-backend"]
[dependencies.rtic-monotonics]
version = "1"
features = ["cortex-m-systick"]
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
branch = "all_features"
default-features = false
[dependencies.stm32f3xx-hal]
git = "https://github.com/robamu/stm32f3xx-hal"
version = "0.11.0-alpha.0"
features = ["stm32f303xc", "rt", "enumset"]
branch = "complete-dma-update"
# Can be used in workspace to develop and update HAL
# path = "../stm32f3xx-hal"
[dependencies.stm32f3-discovery]
git = "https://github.com/robamu/stm32f3-discovery"
version = "0.8.0-alpha.0"
branch = "complete-dma-update-hal"
# Can be used in workspace to develop and update BSP
# path = "../stm32f3-discovery"
[dependencies.satrs]
# path = "satrs"
version = "0.2"
default-features = false
features = ["defmt"]
[dev-dependencies]
defmt-test = "0.3"
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = "s" # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-
File renamed without changes.
@@ -0,0 +1,114 @@
sat-rs example for the STM32F3-Discovery board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
to embedded Rust.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32F303VCTx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
target extended-remote localhost:2331
monitor reset
# *try* to stop at the user entry point (it might be gone due to inlining)
break main
load
continue
@@ -0,0 +1,12 @@
# Sample OpenOCD configuration for the STM32F3DISCOVERY development board
# Depending on the hardware revision you got you'll have to pick ONE of these
# interfaces. At any time only one interface should be commented out.
# Revision C (newer revision)
source [find interface/stlink.cfg]
# Revision A and B (older revisions)
# source [find interface/stlink-v2.cfg]
source [find target/stm32f3x.cfg]
@@ -0,0 +1,42 @@
target extended-remote :3333
# print demangled symbols
set print asm-demangle on
# set backtrace limit to not have infinite backtrace loops
set backtrace limit 32
# detect unhandled exceptions, hard faults and panics
break DefaultHandler
break HardFault
break rust_begin_unwind
# # run the next few lines so the panic message is printed immediately
# # the number needs to be adjusted for your panic handler
# commands $bpnum
# next 4
# end
# *try* to stop at the user entry point (it might be gone due to inlining)
break main
# monitor arm semihosting enable
# # send captured ITM to the file itm.fifo
# # (the microcontroller SWO pin must be connected to the programmer SWO pin)
# # 8000000 must match the core clock frequency
# # 2000000 is the frequency of the SWO pin. This was added for newer
# openocd versions like v0.12.0.
# monitor tpiu config internal itm.txt uart off 8000000 2000000
# # OR: make the microcontroller SWO pin output compatible with UART (8N1)
# # 8000000 must match the core clock frequency
# # 2000000 is the frequency of the SWO pin
# monitor tpiu config external uart off 8000000 2000000
# # enable ITM port 0
# monitor itm port 0 on
load
# start the process but immediately halt the processor
stepi
@@ -0,0 +1,33 @@
/* Linker script for the STM32F303VCT6 */
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
FLASH : ORIGIN = 0x08000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 40K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* You may want to use this variable to locate the call stack and static
variables in different memory regions. Below is shown the default value */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* You can use this symbol to customize the location of the .text section */
/* If omitted the .text section will be placed right after the .vector_table
section */
/* This is required only on microcontrollers that store some configuration right
after the vector table */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* Example of putting non-initialized variables into custom RAM locations. */
/* This assumes you have defined a region RAM2 above, and in the Rust
sources added the attribute `#[link_section = ".ram2bss"]` to the data
you want to place there. */
/* Note that the section will not be zero-initialized by the runtime! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
@@ -0,0 +1,8 @@
/venv
/.tmtc-history.txt
/log
/.idea/*
!/.idea/runConfigurations
/seqcnt.txt
/tmtc_conf.json
@@ -0,0 +1,4 @@
{
"com_if": "serial_cobs",
"serial_baudrate": 115200
}
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""Example client for the sat-rs example application"""
import struct
import logging
import sys
import time
from typing import Any, Optional, cast
from prompt_toolkit.history import FileHistory, History
from spacepackets.ecss.tm import CdsShortTimestamp
import tmtccmd
from spacepackets.ecss import PusTelemetry, PusTelecommand, PusTm, PusVerificator
from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest
from tmtccmd.core.ccsds_backend import QueueWrapper
from tmtccmd.logging import add_colorlog_console_logger
from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
from tmtccmd.com import ComInterface
from tmtccmd.config import (
CmdTreeNode,
default_json_path,
SetupParams,
HookBase,
params_to_procedure_conversion,
)
from tmtccmd.config.com import SerialCfgWrapper
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
from tmtccmd.logging.pus import (
RegularTmtcLogWrapper,
RawTmtcTimedLogWrapper,
TimedLogWhen,
)
from tmtccmd.tmtc import (
TcQueueEntryType,
ProcedureWrapper,
TcProcedureType,
FeedWrapper,
SendCbParams,
DefaultPusQueueHelper,
)
from tmtccmd.pus.s5_fsfw_event import Service5Tm
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
from tmtccmd.util.obj_id import ObjectIdDictT
_LOGGER = logging.getLogger()
EXAMPLE_PUS_APID = 0x02
class SatRsConfigHook(HookBase):
def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path)
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import (
create_com_interface_default,
create_com_interface_cfg_default,
)
assert self.cfg_path is not None
cfg = create_com_interface_cfg_default(
com_if_key=com_if_key,
json_cfg_path=self.cfg_path,
space_packet_ids=None,
)
if cfg is None:
raise ValueError(
f"No valid configuration could be retrieved for the COM IF with key {com_if_key}"
)
if cfg.com_if_key == "serial_cobs":
cfg = cast(SerialCfgWrapper, cfg)
cfg.serial_cfg.serial_timeout = 0.5
return create_com_interface_default(cfg)
def get_command_definitions(self) -> CmdTreeNode:
"""This function should return the root node of the command definition tree."""
return create_cmd_definition_tree()
def get_cmd_history(self) -> Optional[History]:
"""Optionlly return a history class for the past command paths which will be used
when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids
return get_core_object_ids()
def create_cmd_definition_tree() -> CmdTreeNode:
root_node = CmdTreeNode.root_node()
root_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
root_node.add_child(CmdTreeNode("change_blink_freq", "Change blink frequency"))
return root_node
class PusHandler(SpecificApidHandlerBase):
def __init__(
self,
file_logger: logging.Logger,
verif_wrapper: VerificationWrapper,
raw_logger: RawTmtcTimedLogWrapper,
):
super().__init__(EXAMPLE_PUS_APID, None)
self.file_logger = file_logger
self.raw_logger = raw_logger
self.verif_wrapper = verif_wrapper
def handle_tm(self, packet: bytes, _user_args: Any):
try:
pus_tm = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e
service = pus_tm.service
tm_packet = None
if service == 1:
tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
)
res = self.verif_wrapper.add_tm(tm_packet)
if res is None:
_LOGGER.info(
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
)
_LOGGER.warning(
f"No matching telecommand found for {tm_packet.tc_req_id}"
)
else:
self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res)
if service == 3:
_LOGGER.info("No handling for HK packets implemented")
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
pus_tm = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if pus_tm.subservice == 25:
if len(pus_tm.source_data) < 8:
raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:]
_LOGGER.info("received JSON string: " + json_str.decode("utf-8"))
if service == 5:
tm_packet = Service5Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if service == 17:
tm_packet = Service17Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if tm_packet.subservice == 2:
_LOGGER.info("Received Ping Reply TM[17,2]")
else:
_LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
if tm_packet is None:
_LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory"
)
tm_packet = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
self.raw_logger.log_tm(pus_tm)
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
byte_string = bytearray(struct.pack("!I", target_id))
byte_string.extend(struct.pack("!I", unique_id))
return byte_string
class TcHandler(TcHandlerBase):
def __init__(
self,
seq_count_provider: FileSeqCountProvider,
verif_wrapper: VerificationWrapper,
):
super(TcHandler, self).__init__()
self.seq_count_provider = seq_count_provider
self.verif_wrapper = verif_wrapper
self.queue_helper = DefaultPusQueueHelper(
queue_wrapper=QueueWrapper.empty(),
tc_sched_timestamp_len=7,
seq_cnt_provider=seq_count_provider,
pus_verificator=verif_wrapper.pus_verificator,
default_pus_apid=EXAMPLE_PUS_APID,
)
def send_cb(self, send_params: SendCbParams):
entry_helper = send_params.entry
if entry_helper.is_tc:
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
pus_tc_wrapper.pus_tc.seq_count = (
self.seq_count_provider.get_and_increment()
)
self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc)
raw_tc = pus_tc_wrapper.pus_tc.pack()
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
send_params.com_if.send(raw_tc)
elif entry_helper.entry_type == TcQueueEntryType.LOG:
log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, info: ProcedureWrapper):
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
cmd_path = def_proc.cmd_path
if cmd_path == "/ping":
q.add_log_cmd("Sending PUS ping telecommand")
q.add_pus_tc(PusTelecommand(service=17, subservice=1))
if cmd_path == "/change_blink_freq":
self.create_change_blink_freq_command(q)
def create_change_blink_freq_command(self, q: DefaultPusQueueHelper):
q.add_log_cmd("Changing blink frequency")
while True:
blink_freq = int(
input(
"Please specify new blink frequency in ms. Valid Range [2..10000]: "
)
)
if blink_freq < 2 or blink_freq > 10000:
print(
"Invalid blink frequency. Please specify a value between 2 and 10000."
)
continue
break
app_data = struct.pack("!I", blink_freq)
q.add_pus_tc(PusTelecommand(service=8, subservice=1, app_data=app_data))
def main():
add_colorlog_console_logger(_LOGGER)
tmtccmd.init_printout(False)
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path())
parser_wrapper = PreArgsParsingWrapper()
parser_wrapper.create_default_parent_parser()
parser_wrapper.create_default_parser()
parser_wrapper.add_def_proc_args()
params = SetupParams()
post_args_wrapper = parser_wrapper.parse(hook_obj, params)
proc_wrapper = ProcedureParamsWrapper()
if post_args_wrapper.use_gui:
post_args_wrapper.set_params_without_prompts(proc_wrapper)
else:
post_args_wrapper.set_params_with_prompts(proc_wrapper)
params.apid = EXAMPLE_PUS_APID
setup_args = SetupWrapper(
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
)
# Create console logger helper and file loggers
tmtc_logger = RegularTmtcLogWrapper()
file_logger = tmtc_logger.logger
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
verificator = PusVerificator()
verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger)
# Create primary TM handler and add it to the CCSDS Packet Handler
tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger)
ccsds_handler = CcsdsTmHandler(generic_handler=None)
ccsds_handler.add_apid_handler(tm_handler)
# Create TC handler
seq_count_provider = PusFileSeqCountProvider()
tc_handler = TcHandler(seq_count_provider, verification_wrapper)
tmtccmd.setup(setup_args=setup_args)
init_proc = params_to_procedure_conversion(setup_args.proc_param_wrapper)
tmtc_backend = tmtccmd.create_default_tmtc_backend(
setup_wrapper=setup_args,
tm_handler=ccsds_handler,
tc_handler=tc_handler,
init_procedure=init_proc,
)
tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj)
try:
while True:
state = tmtc_backend.periodic_op(None)
if state.request == BackendRequest.TERMINATION_NO_ERROR:
sys.exit(0)
elif state.request == BackendRequest.DELAY_IDLE:
_LOGGER.info("TMTC Client in IDLE mode")
time.sleep(3.0)
elif state.request == BackendRequest.DELAY_LISTENER:
time.sleep(0.8)
elif state.request == BackendRequest.DELAY_CUSTOM:
if state.next_delay.total_seconds() <= 0.4:
time.sleep(state.next_delay.total_seconds())
else:
time.sleep(0.4)
elif state.request == BackendRequest.CALL_NEXT:
pass
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
tmtccmd == 8.0.1
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
@@ -0,0 +1,76 @@
#![no_std]
#![no_main]
use satrs_stm32f3_disco_rtic as _;
use stm32f3_discovery::leds::Leds;
use stm32f3_discovery::stm32f3xx_hal::delay::Delay;
use stm32f3_discovery::stm32f3xx_hal::{pac, prelude::*};
use stm32f3_discovery::switch_hal::{OutputSwitch, ToggleableOutputSwitch};
#[cortex_m_rt::entry]
fn main() -> ! {
defmt::println!("STM32F3 Discovery Blinky");
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let cp = cortex_m::Peripherals::take().unwrap();
let mut flash = dp.FLASH.constrain();
let clocks = rcc.cfgr.freeze(&mut flash.acr);
let mut delay = Delay::new(cp.SYST, clocks);
let mut gpioe = dp.GPIOE.split(&mut rcc.ahb);
let mut leds = Leds::new(
gpioe.pe8,
gpioe.pe9,
gpioe.pe10,
gpioe.pe11,
gpioe.pe12,
gpioe.pe13,
gpioe.pe14,
gpioe.pe15,
&mut gpioe.moder,
&mut gpioe.otyper,
);
let delay_ms = 200u16;
loop {
leds.ld3_n.toggle().ok();
delay.delay_ms(delay_ms);
leds.ld3_n.toggle().ok();
delay.delay_ms(delay_ms);
//explicit on/off
leds.ld4_nw.on().ok();
delay.delay_ms(delay_ms);
leds.ld4_nw.off().ok();
delay.delay_ms(delay_ms);
leds.ld5_ne.on().ok();
delay.delay_ms(delay_ms);
leds.ld5_ne.off().ok();
delay.delay_ms(delay_ms);
leds.ld6_w.on().ok();
delay.delay_ms(delay_ms);
leds.ld6_w.off().ok();
delay.delay_ms(delay_ms);
leds.ld7_e.on().ok();
delay.delay_ms(delay_ms);
leds.ld7_e.off().ok();
delay.delay_ms(delay_ms);
leds.ld8_sw.on().ok();
delay.delay_ms(delay_ms);
leds.ld8_sw.off().ok();
delay.delay_ms(delay_ms);
leds.ld9_se.on().ok();
delay.delay_ms(delay_ms);
leds.ld9_se.off().ok();
delay.delay_ms(delay_ms);
leds.ld10_s.on().ok();
delay.delay_ms(delay_ms);
leds.ld10_s.off().ok();
delay.delay_ms(delay_ms);
}
}
@@ -1,27 +1,14 @@
#![no_main]
#![no_std]
use defmt_rtt as _;
use embassy_stm32 as _;
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
use stm32f3xx_hal as _; // memory layout
use panic_probe as _;
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
const HEAP_SIZE: usize = 131_072;
// Part of the library, because all binaries depend on crates which require an allocator.
#[global_allocator]
static HEAP: Heap = Heap::empty();
/// # Safety
///
/// Must be called exactly once, before the first allocation.
pub unsafe fn init_heap() {
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
}
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
@@ -29,6 +16,14 @@ fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
@@ -36,7 +31,9 @@ fn panic() -> ! {
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
panic!("unexpected hard fault");
loop {
debug::exit(debug::EXIT_FAILURE);
}
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
@@ -0,0 +1,684 @@
#![no_std]
#![no_main]
use satrs::pus::verification::{
FailParams, TcStateAccepted, VerificationReportCreator, VerificationToken,
};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use satrs::spacepackets::ecss::EcssEnumU16;
use satrs::spacepackets::CcsdsPacket;
use satrs::spacepackets::{ByteConversionError, SpHeader};
// global logger + panicking-behavior + memory layout
use satrs_stm32f3_disco_rtic as _;
use rtic::app;
use heapless::{mpmc::Q8, Vec};
#[allow(unused_imports)]
use rtic_monotonics::systick::fugit::{MillisDurationU32, TimerInstantU32};
use rtic_monotonics::systick::ExtU32;
use satrs::seq_count::SequenceCountProviderCore;
use satrs::spacepackets::{ecss::PusPacket, ecss::WritablePusPacket};
use stm32f3xx_hal::dma::dma1;
use stm32f3xx_hal::gpio::{PushPull, AF7, PA2, PA3};
use stm32f3xx_hal::pac::USART2;
use stm32f3xx_hal::serial::{Rx, RxEvent, Serial, SerialDmaRx, SerialDmaTx, Tx, TxEvent};
const UART_BAUD: u32 = 115200;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const TX_HANDLER_FREQ_MS: u32 = 20;
const MIN_DELAY_BETWEEN_TX_PACKETS_MS: u32 = 5;
const MAX_TC_LEN: usize = 128;
const MAX_TM_LEN: usize = 128;
pub const PUS_APID: u16 = 0x02;
type TxType = Tx<USART2, PA2<AF7<PushPull>>>;
type RxType = Rx<USART2, PA3<AF7<PushPull>>>;
type InstantFugit = TimerInstantU32<1000>;
type TxDmaTransferType = SerialDmaTx<&'static [u8], dma1::C7, TxType>;
type RxDmaTransferType = SerialDmaRx<&'static mut [u8], dma1::C6, RxType>;
// This is the predictable maximum overhead of the COBS encoding scheme.
// It is simply the maximum packet lenght dividied by 254 rounded up.
const COBS_TC_OVERHEAD: usize = (MAX_TC_LEN + 254 - 1) / 254;
const COBS_TM_OVERHEAD: usize = (MAX_TM_LEN + 254 - 1) / 254;
const TC_BUF_LEN: usize = MAX_TC_LEN + COBS_TC_OVERHEAD;
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
// This is a static buffer which should ONLY (!) be used as the TX DMA
// transfer buffer.
static mut DMA_TX_BUF: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN];
// This is a static buffer which should ONLY (!) be used as the RX DMA
// transfer buffer.
static mut DMA_RX_BUF: [u8; TC_BUF_LEN] = [0; TC_BUF_LEN];
type TmPacket = Vec<u8, MAX_TM_LEN>;
type TcPacket = Vec<u8, MAX_TC_LEN>;
static TM_REQUESTS: Q8<TmPacket> = Q8::new();
use core::sync::atomic::{AtomicU16, Ordering};
pub struct SeqCountProviderAtomicRef {
atomic: AtomicU16,
ordering: Ordering,
}
impl SeqCountProviderAtomicRef {
pub const fn new(ordering: Ordering) -> Self {
Self {
atomic: AtomicU16::new(0),
ordering,
}
}
}
impl SequenceCountProviderCore<u16> for SeqCountProviderAtomicRef {
fn get(&self) -> u16 {
self.atomic.load(self.ordering)
}
fn increment(&self) {
self.atomic.fetch_add(1, self.ordering);
}
fn get_and_increment(&self) -> u16 {
self.atomic.fetch_add(1, self.ordering)
}
}
static SEQ_COUNT_PROVIDER: SeqCountProviderAtomicRef =
SeqCountProviderAtomicRef::new(Ordering::Relaxed);
pub struct TxIdle {
tx: TxType,
dma_channel: dma1::C7,
}
#[derive(Debug, defmt::Format)]
pub enum TmSendError {
ByteConversion(ByteConversionError),
Queue,
}
impl From<ByteConversionError> for TmSendError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversion(value)
}
}
fn send_tm(tm_creator: PusTmCreator) -> Result<(), TmSendError> {
if tm_creator.len_written() > MAX_TM_LEN {
return Err(ByteConversionError::ToSliceTooSmall {
expected: tm_creator.len_written(),
found: MAX_TM_LEN,
}
.into());
}
let mut tm_vec = TmPacket::new();
tm_vec
.resize(tm_creator.len_written(), 0)
.expect("vec resize failed");
tm_creator.write_to_bytes(tm_vec.as_mut_slice())?;
defmt::info!(
"Sending TM[{},{}] with size {}",
tm_creator.service(),
tm_creator.subservice(),
tm_creator.len_written()
);
TM_REQUESTS
.enqueue(tm_vec)
.map_err(|_| TmSendError::Queue)?;
Ok(())
}
fn handle_tm_send_error(error: TmSendError) {
defmt::warn!("sending tm failed with error {}", error);
}
pub enum UartTxState {
// Wrapped in an option because we need an owned type later.
Idle(Option<TxIdle>),
// Same as above
Transmitting(Option<TxDmaTransferType>),
}
pub struct UartTxShared {
last_completed: Option<InstantFugit>,
state: UartTxState,
}
pub struct RequestWithToken {
token: VerificationToken<TcStateAccepted>,
request: Request,
}
#[derive(Debug, defmt::Format)]
pub enum Request {
Ping,
ChangeBlinkFrequency(u32),
}
#[derive(Debug, defmt::Format)]
pub enum RequestError {
InvalidApid = 1,
InvalidService = 2,
InvalidSubservice = 3,
NotEnoughAppData = 4,
}
pub fn convert_pus_tc_to_request(
tc: &PusTcReader,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) -> Result<RequestWithToken, RequestError> {
defmt::info!(
"Found PUS TC [{},{}] with length {}",
tc.service(),
tc.subservice(),
tc.len_packed()
);
let token = verif_reporter.add_tc(tc);
if tc.apid() != PUS_APID {
defmt::warn!("Received tc with unknown APID {}", tc.apid());
let result = send_tm(
verif_reporter
.acceptance_failure(
src_data_buf,
token,
SEQ_COUNT_PROVIDER.get_and_increment(),
0,
FailParams::new(timestamp, &EcssEnumU16::new(0), &[]),
)
.unwrap(),
);
if let Err(e) = result {
handle_tm_send_error(e);
}
return Err(RequestError::InvalidApid);
}
let (tm_creator, accepted_token) = verif_reporter
.acceptance_success(
src_data_buf,
token,
SEQ_COUNT_PROVIDER.get_and_increment(),
0,
timestamp,
)
.unwrap();
if let Err(e) = send_tm(tm_creator) {
handle_tm_send_error(e);
}
if tc.service() == 17 && tc.subservice() == 1 {
if tc.subservice() == 1 {
return Ok(RequestWithToken {
request: Request::Ping,
token: accepted_token,
});
} else {
return Err(RequestError::InvalidSubservice);
}
} else if tc.service() == 8 {
if tc.subservice() == 1 {
if tc.user_data().len() < 4 {
return Err(RequestError::NotEnoughAppData);
}
let new_freq_ms = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap());
return Ok(RequestWithToken {
request: Request::ChangeBlinkFrequency(new_freq_ms),
token: accepted_token,
});
} else {
return Err(RequestError::InvalidSubservice);
}
} else {
return Err(RequestError::InvalidService);
}
}
#[app(device = stm32f3xx_hal::pac, peripherals = true)]
mod app {
use super::*;
use core::slice::Iter;
use rtic_monotonics::systick::Systick;
use rtic_monotonics::Monotonic;
use satrs::pus::verification::{TcStateStarted, VerificationReportCreator};
use satrs::spacepackets::{ecss::tc::PusTcReader, time::cds::P_FIELD_BASE};
#[allow(unused_imports)]
use stm32f3_discovery::leds::Direction;
use stm32f3_discovery::leds::Leds;
use stm32f3xx_hal::prelude::*;
use stm32f3_discovery::switch_hal::OutputSwitch;
use stm32f3xx_hal::Switch;
#[allow(dead_code)]
type SerialType = Serial<USART2, (PA2<AF7<PushPull>>, PA3<AF7<PushPull>>)>;
#[shared]
struct Shared {
blink_freq: MillisDurationU32,
tx_shared: UartTxShared,
rx_transfer: Option<RxDmaTransferType>,
}
#[local]
struct Local {
verif_reporter: VerificationReportCreator,
leds: Leds,
last_dir: Direction,
curr_dir: Iter<'static, Direction>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let mut rcc = cx.device.RCC.constrain();
// Initialize the systick interrupt & obtain the token to prove that we did
let systick_mono_token = rtic_monotonics::create_systick_token!();
Systick::start(cx.core.SYST, 8_000_000, systick_mono_token);
let mut flash = cx.device.FLASH.constrain();
let clocks = rcc
.cfgr
.use_hse(8.MHz())
.sysclk(8.MHz())
.pclk1(8.MHz())
.freeze(&mut flash.acr);
// Set up monotonic timer.
//let mono_timer = MonoTimer::new(cx.core.DWT, clocks, &mut cx.core.DCB);
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery");
let mut gpioe = cx.device.GPIOE.split(&mut rcc.ahb);
let leds = Leds::new(
gpioe.pe8,
gpioe.pe9,
gpioe.pe10,
gpioe.pe11,
gpioe.pe12,
gpioe.pe13,
gpioe.pe14,
gpioe.pe15,
&mut gpioe.moder,
&mut gpioe.otyper,
);
let mut gpioa = cx.device.GPIOA.split(&mut rcc.ahb);
// USART2 pins
let mut pins = (
// TX pin: PA2
gpioa
.pa2
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
// RX pin: PA3
gpioa
.pa3
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
);
pins.1.internal_pull_up(&mut gpioa.pupdr, true);
let mut usart2 = Serial::new(
cx.device.USART2,
pins,
UART_BAUD.Bd(),
clocks,
&mut rcc.apb1,
);
usart2.configure_rx_interrupt(RxEvent::Idle, Switch::On);
// This interrupt is enabled to re-schedule new transfers in the interrupt handler immediately.
usart2.configure_tx_interrupt(TxEvent::TransmissionComplete, Switch::On);
let dma1 = cx.device.DMA1.split(&mut rcc.ahb);
let (mut tx_serial, mut rx_serial) = usart2.split();
// This interrupt is immediately triggered, clear it. It will only be reset
// by the hardware when data is received on RX (RXNE event)
rx_serial.clear_event(RxEvent::Idle);
// For some reason, this is also immediately triggered..
tx_serial.clear_event(TxEvent::TransmissionComplete);
let rx_transfer = rx_serial.read_exact(unsafe { DMA_RX_BUF.as_mut_slice() }, dma1.ch6);
defmt::info!("Spawning tasks");
blink::spawn().unwrap();
serial_tx_handler::spawn().unwrap();
let verif_reporter = VerificationReportCreator::new(PUS_APID).unwrap();
(
Shared {
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
tx_shared: UartTxShared {
last_completed: None,
state: UartTxState::Idle(Some(TxIdle {
tx: tx_serial,
dma_channel: dma1.ch7,
})),
},
rx_transfer: Some(rx_transfer),
},
Local {
verif_reporter,
leds,
last_dir: Direction::North,
curr_dir: Direction::iter(),
},
)
}
#[task(local = [leds, curr_dir, last_dir], shared=[blink_freq])]
async fn blink(mut cx: blink::Context) {
let blink::LocalResources {
leds,
curr_dir,
last_dir,
..
} = cx.local;
let mut toggle_leds = |dir: &Direction| {
let last_led = leds.for_direction(*last_dir);
last_led.off().ok();
let led = leds.for_direction(*dir);
led.on().ok();
*last_dir = *dir;
};
loop {
match curr_dir.next() {
Some(dir) => {
toggle_leds(dir);
}
None => {
*curr_dir = Direction::iter();
toggle_leds(curr_dir.next().unwrap());
}
}
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Systick::delay(current_blink_freq).await;
}
}
#[task(
shared = [tx_shared],
)]
async fn serial_tx_handler(mut cx: serial_tx_handler::Context) {
loop {
let is_idle = cx.shared.tx_shared.lock(|tx_shared| {
if let UartTxState::Idle(_) = tx_shared.state {
return true;
}
false
});
if is_idle {
let last_completed = cx.shared.tx_shared.lock(|shared| shared.last_completed);
if let Some(last_completed) = last_completed {
let elapsed_ms = (Systick::now() - last_completed).to_millis();
if elapsed_ms < MIN_DELAY_BETWEEN_TX_PACKETS_MS {
Systick::delay((MIN_DELAY_BETWEEN_TX_PACKETS_MS - elapsed_ms).millis())
.await;
}
}
} else {
// Check for completion after 1 ms
Systick::delay(1.millis()).await;
continue;
}
if let Some(vec) = TM_REQUESTS.dequeue() {
cx.shared
.tx_shared
.lock(|tx_shared| match &mut tx_shared.state {
UartTxState::Idle(tx) => {
let encoded_len;
//debug!(target: "serial_tx_handler", "bytes: {:x?}", &buf[0..len]);
// Safety: We only copy the data into the TX DMA buffer in this task.
// If the DMA is active, another branch will be taken.
unsafe {
// 0 sentinel value as start marker
DMA_TX_BUF[0] = 0;
encoded_len =
cobs::encode(&vec[0..vec.len()], &mut DMA_TX_BUF[1..]);
// Should never panic, we accounted for the overhead.
// Write into transfer buffer directly, no need for intermediate
// encoding buffer.
// 0 end marker
DMA_TX_BUF[encoded_len + 1] = 0;
}
//debug!(target: "serial_tx_handler", "Sending {} bytes", encoded_len + 2);
//debug!("sent: {:x?}", &mut_tx_dma_buf[0..encoded_len + 2]);
let tx_idle = tx.take().unwrap();
// Transfer completion and re-scheduling of new TX transfers will be done
// by the IRQ handler.
// SAFETY: The DMA is the exclusive writer to the DMA buffer now.
let transfer = tx_idle.tx.write_all(
unsafe { &DMA_TX_BUF[0..encoded_len + 2] },
tx_idle.dma_channel,
);
tx_shared.state = UartTxState::Transmitting(Some(transfer));
// The memory block is automatically returned to the pool when it is dropped.
}
UartTxState::Transmitting(_) => (),
});
// Check for completion after 1 ms
Systick::delay(1.millis()).await;
continue;
}
// Nothing to do, and we are idle.
Systick::delay(TX_HANDLER_FREQ_MS.millis()).await;
}
}
#[task(
local = [
verif_reporter,
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
src_data_buf: [u8; MAX_TM_LEN] = [0; MAX_TM_LEN],
timestamp: [u8; 7] = [0; 7],
],
shared = [blink_freq]
)]
async fn serial_rx_handler(
mut cx: serial_rx_handler::Context,
received_packet: Vec<u8, MAX_TC_LEN>,
) {
cx.local.timestamp[0] = P_FIELD_BASE;
defmt::info!("Received packet with {} bytes", received_packet.len());
let decode_buf = cx.local.decode_buf;
let packet = received_packet.as_slice();
let mut start_idx = None;
for (idx, byte) in packet.iter().enumerate() {
if *byte != 0 {
start_idx = Some(idx);
break;
}
}
if start_idx.is_none() {
defmt::warn!("decoding error, can only process cobs encoded frames, data is all 0");
return;
}
let start_idx = start_idx.unwrap();
match cobs::decode(&received_packet.as_slice()[start_idx..], decode_buf) {
Ok(len) => {
defmt::info!("Decoded packet length: {}", len);
let pus_tc = PusTcReader::new(decode_buf);
match pus_tc {
Ok((tc, _tc_len)) => {
match convert_pus_tc_to_request(
&tc,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
) {
Ok(request_with_token) => {
let started_token = handle_start_verification(
request_with_token.token,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
);
match request_with_token.request {
Request::Ping => {
handle_ping_request(cx.local.timestamp);
}
Request::ChangeBlinkFrequency(new_freq_ms) => {
defmt::info!("Received blink frequency change request with new frequncy {}", new_freq_ms);
cx.shared.blink_freq.lock(|blink_freq| {
*blink_freq =
MillisDurationU32::from_ticks(new_freq_ms);
});
}
}
handle_completion_verification(
started_token,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
);
}
Err(e) => {
// TODO: Error handling: Send verification failure based on request error.
defmt::warn!("request error {}", e);
}
}
}
Err(e) => {
defmt::warn!("Error unpacking PUS TC: {}", e);
}
}
}
Err(_) => {
defmt::warn!("decoding error, can only process cobs encoded frames")
}
}
}
fn handle_ping_request(timestamp: &[u8]) {
defmt::info!("Received PUS ping telecommand, sending ping reply TM[17,2]");
let sp_header =
SpHeader::new_for_unseg_tc(PUS_APID, SEQ_COUNT_PROVIDER.get_and_increment(), 0);
let sec_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp);
let ping_reply = PusTmCreator::new(sp_header, sec_header, &[], true);
let mut tm_packet = TmPacket::new();
tm_packet
.resize(ping_reply.len_written(), 0)
.expect("vec resize failed");
ping_reply.write_to_bytes(&mut tm_packet).unwrap();
if TM_REQUESTS.enqueue(tm_packet).is_err() {
defmt::warn!("TC queue full");
return;
}
}
fn handle_start_verification(
accepted_token: VerificationToken<TcStateAccepted>,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) -> VerificationToken<TcStateStarted> {
let (tm_creator, started_token) = verif_reporter
.start_success(
src_data_buf,
accepted_token,
SEQ_COUNT_PROVIDER.get(),
0,
&timestamp,
)
.unwrap();
let result = send_tm(tm_creator);
if let Err(e) = result {
handle_tm_send_error(e);
}
started_token
}
fn handle_completion_verification(
started_token: VerificationToken<TcStateStarted>,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) {
let result = send_tm(
verif_reporter
.completion_success(
src_data_buf,
started_token,
SEQ_COUNT_PROVIDER.get(),
0,
timestamp,
)
.unwrap(),
);
if let Err(e) = result {
handle_tm_send_error(e);
}
}
#[task(binds = DMA1_CH6, shared = [rx_transfer])]
fn rx_dma_isr(mut cx: rx_dma_isr::Context) {
let mut tc_packet = TcPacket::new();
cx.shared.rx_transfer.lock(|rx_transfer| {
let rx_ref = rx_transfer.as_ref().unwrap();
if rx_ref.is_complete() {
let uart_rx_owned = rx_transfer.take().unwrap();
let (buf, c, rx) = uart_rx_owned.stop();
// The received data is transferred to another task now to avoid any processing overhead
// during the interrupt. There are multiple ways to do this, we use a stack allocaed vector here
// to do this.
tc_packet.resize(buf.len(), 0).expect("vec resize failed");
tc_packet.copy_from_slice(buf);
// Start the next transfer as soon as possible.
*rx_transfer = Some(rx.read_exact(buf, c));
// Send the vector to a regular task.
serial_rx_handler::spawn(tc_packet).expect("spawning rx handler task failed");
// If this happens, there is a high chance that the maximum packet length was
// exceeded. Circular mode is not used here, so data might be missed.
defmt::warn!(
"rx transfer with maximum length {}, might miss data",
TC_BUF_LEN
);
}
});
}
#[task(binds = USART2_EXTI26, shared = [rx_transfer, tx_shared])]
fn serial_isr(mut cx: serial_isr::Context) {
cx.shared
.tx_shared
.lock(|tx_shared| match &mut tx_shared.state {
UartTxState::Idle(_) => (),
UartTxState::Transmitting(transfer) => {
let transfer_ref = transfer.as_ref().unwrap();
if transfer_ref.is_complete() {
let transfer = transfer.take().unwrap();
let (_, dma_channel, mut tx) = transfer.stop();
tx.clear_event(TxEvent::TransmissionComplete);
tx_shared.state = UartTxState::Idle(Some(TxIdle { tx, dma_channel }));
// We cache the last completed time to ensure that there is a minimum delay between consecutive
// transferred packets.
tx_shared.last_completed = Some(Systick::now());
}
}
});
let mut tc_packet = TcPacket::new();
cx.shared.rx_transfer.lock(|rx_transfer| {
let rx_transfer_ref = rx_transfer.as_ref().unwrap();
// Received a partial packet.
if rx_transfer_ref.is_event_triggered(RxEvent::Idle) {
let rx_transfer_owned = rx_transfer.take().unwrap();
let (buf, ch, mut rx, rx_len) = rx_transfer_owned.stop_and_return_received_bytes();
// The received data is transferred to another task now to avoid any processing overhead
// during the interrupt. There are multiple ways to do this, we use a stack
// allocated vector to do this.
tc_packet
.resize(rx_len as usize, 0)
.expect("vec resize failed");
tc_packet[0..rx_len as usize].copy_from_slice(&buf[0..rx_len as usize]);
rx.clear_event(RxEvent::Idle);
serial_rx_handler::spawn(tc_packet).expect("spawning rx handler failed");
*rx_transfer = Some(rx.read_exact(buf, ch));
}
});
}
}
@@ -0,0 +1,2 @@
/settings.json
/.cortex-debug.*
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"rust-lang.rust",
"probe-rs.probe-rs-debugger"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
@@ -0,0 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32F303VCTx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32f3-disco-rtic",
"rttEnabled": true,
"svdFile": "STM32F303.svd"
}
]
}
]
}
@@ -0,0 +1,18 @@
#
# Cortex-Debug extension calls this function during initialization. You can copy this
# file, modify it and specifyy it as one of the config files supplied in launch.json
# preferably at the beginning.
#
# Note that this file simply defines a function for use later when it is time to configure
# for SWO.
#
set USE_SWO 0
proc CDSWOConfigure { CDCPUFreqHz CDSWOFreqHz CDSWOOutput } {
# Alternative option: Pipe ITM output into itm.txt file
# tpiu config internal itm.txt uart off $CDCPUFreqHz
# Default option so SWO display of VS code works. Please note that this might not be required
# anymore starting at openocd v0.12.0
tpiu config internal $CDSWOOutput uart off $CDCPUFreqHz $CDSWOFreqHz
itm port 0 on
}
@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}
@@ -1,5 +1,5 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H753ZITx"
runner = "probe-rs run --chip STM32H743ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
@@ -1,4 +1,4 @@
/target
/.cargo/config.toml
/.cargo/config*
/.vscode
/app.map
+881
View File
@@ -0,0 +1,881 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bare-metal"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
dependencies = [
"rustc_version 0.2.3",
]
[[package]]
name = "bare-metal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "bitfield"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cobs"
version = "0.2.3"
source = "git+https://github.com/robamu/cobs.rs.git?branch=all_features#c70a7f30fd00a7cbdb7666dec12b437977385d40"
[[package]]
name = "cortex-m"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [
"bare-metal 0.2.5",
"bitfield",
"critical-section",
"embedded-hal 0.2.7",
"volatile-register",
]
[[package]]
name = "cortex-m-rt"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2722f5b7d6ea8583cffa4d247044e280ccbb9fe501bed56552e2ba48b02d5f3d"
dependencies = [
"cortex-m-rt-macros",
]
[[package]]
name = "cortex-m-rt-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "cortex-m-semihosting"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c23234600452033cc77e4b761e740e02d2c4168e11dbf36ab14a0f58973592b0"
dependencies = [
"cortex-m",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]]
name = "defmt"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0"
dependencies = [
"bitflags",
"defmt-macros",
]
[[package]]
name = "defmt-brtt"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f0ac3635d0c89d12b8101fcb44a7625f5f030a1c0491124b74467eb5a58a78"
dependencies = [
"critical-section",
"defmt",
]
[[package]]
name = "defmt-macros"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb"
dependencies = [
"defmt-parser",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "defmt-parser"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f"
dependencies = [
"thiserror",
]
[[package]]
name = "defmt-test"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290966e8c38f94b11884877242de876280d0eab934900e9642d58868e77c5df1"
dependencies = [
"cortex-m-rt",
"cortex-m-semihosting",
"defmt",
"defmt-test-macros",
]
[[package]]
name = "defmt-test-macros"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "984bc6eca246389726ac2826acc2488ca0fe5fcd6b8d9b48797021951d76a125"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "delegate"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee5df75c70b95bd3aacc8e2fd098797692fb1d54121019c4de481e42f04c8a1"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive-new"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "embedded-alloc"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddae17915accbac2cfbc64ea0ae6e3b330e6ea124ba108dada63646fd3c6f815"
dependencies = [
"critical-section",
"linked_list_allocator",
]
[[package]]
name = "embedded-dma"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
dependencies = [
"nb 0.1.3",
"void",
]
[[package]]
name = "embedded-hal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
dependencies = [
"defmt",
]
[[package]]
name = "embedded-hal-async"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
dependencies = [
"defmt",
"embedded-hal 1.0.0",
]
[[package]]
name = "embedded-hal-bus"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886"
dependencies = [
"critical-section",
"defmt",
"embedded-hal 1.0.0",
"embedded-hal-async",
]
[[package]]
name = "embedded-storage"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fugit"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7"
dependencies = [
"gcd",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "gcd"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a"
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32 0.2.1",
"rustc_version 0.4.0",
"spin",
"stable_deref_trait",
]
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"defmt",
"hash32 0.3.1",
"stable_deref_trait",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "linked_list_allocator"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "managed"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
[[package]]
name = "nb"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
dependencies = [
"nb 1.1.0",
]
[[package]]
name = "nb"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "panic-probe"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0"
dependencies = [
"cortex-m",
"defmt",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rtic"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c443db16326376bdd64377da268f6616d5f804aba8ce799bac7d1f7f244e9d51"
dependencies = [
"atomic-polyfill",
"bare-metal 1.0.0",
"cortex-m",
"critical-section",
"rtic-core",
"rtic-macros",
]
[[package]]
name = "rtic-common"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0786b50b81ef9d2a944a000f60405bb28bf30cd45da2d182f3fe636b2321f35c"
dependencies = [
"critical-section",
]
[[package]]
name = "rtic-core"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9369355b04d06a3780ec0f51ea2d225624db777acbc60abd8ca4832da5c1a42"
[[package]]
name = "rtic-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54053598ea24b1b74937724e366558412a1777eb2680b91ef646db540982789a"
dependencies = [
"indexmap",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "rtic-monotonics"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058c2397dbd5bb4c5650a0e368c3920953e458805ff5097a0511b8147b3619d7"
dependencies = [
"atomic-polyfill",
"cfg-if",
"cortex-m",
"embedded-hal 1.0.0",
"fugit",
"rtic-time",
]
[[package]]
name = "rtic-sync"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b1200137ccb2bf272a1801fa6e27264535facd356cb2c1d5bc8e12aa211bad"
dependencies = [
"critical-section",
"defmt",
"embedded-hal 1.0.0",
"embedded-hal-async",
"embedded-hal-bus",
"heapless 0.8.0",
"portable-atomic",
"rtic-common",
]
[[package]]
name = "rtic-time"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b232e7aebc045cfea81cdd164bc2727a10aca9a4568d406d0a5661cdfd0f19"
dependencies = [
"critical-section",
"futures-util",
"rtic-common",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.23",
]
[[package]]
name = "satrs"
version = "0.2.1"
dependencies = [
"cobs",
"crc",
"defmt",
"delegate",
"derive-new",
"heapless 0.7.17",
"num-traits",
"num_enum",
"paste",
"satrs-shared",
"smallvec",
"spacepackets",
]
[[package]]
name = "satrs-shared"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6042477018c2d43fffccaaa5099bc299a58485139b4d31c5b276889311e474f1"
dependencies = [
"spacepackets",
]
[[package]]
name = "satrs-stm32h7-nucleo-rtic"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"cortex-m-semihosting",
"defmt",
"defmt-brtt",
"defmt-test",
"embedded-alloc",
"panic-probe",
"rtic",
"rtic-monotonics",
"rtic-sync",
"satrs",
"smoltcp",
"stm32h7xx-hal",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smoltcp"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a1a996951e50b5971a2c8c0fa05a381480d70a933064245c4a223ddc87ccc97"
dependencies = [
"bitflags",
"byteorder",
"cfg-if",
"defmt",
"heapless 0.8.0",
"managed",
]
[[package]]
name = "spacepackets"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85574d113a06312010c0ba51aadccd4ba2806231ebe9a49fc6473d0534d8696"
dependencies = [
"crc",
"defmt",
"delegate",
"num-traits",
"num_enum",
"zerocopy",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stm32h7"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "362f288cd8341e9209587b889c385f323e82fc237b60c272868965bb879bb9b1"
dependencies = [
"bare-metal 1.0.0",
"cortex-m",
"cortex-m-rt",
"vcell",
]
[[package]]
name = "stm32h7xx-hal"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd869329be25440b24e2b3583a1c016151b4a54bc36d96d82af7fcd9d010b98"
dependencies = [
"bare-metal 1.0.0",
"cast",
"cortex-m",
"embedded-dma",
"embedded-hal 0.2.7",
"embedded-storage",
"fugit",
"nb 1.1.0",
"paste",
"smoltcp",
"stm32h7",
"void",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "vcell"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "volatile-register"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc"
dependencies = [
"vcell",
]
[[package]]
name = "zerocopy"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
@@ -1,8 +1,9 @@
[package]
name = "stm32h7-nucleo-embassy"
edition = "2024"
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
name = "satrs-stm32h7-nucleo-rtic"
edition = "2021"
version = "0.1.0"
default-run = "stm32h7-nucleo-embassy"
default-run = "satrs-stm32h7-nucleo-rtic"
[lib]
harness = false
@@ -13,29 +14,37 @@ name = "integration"
harness = false
[dependencies]
types = { path = "../types" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
arbitrary-int = "2"
cortex-m-rt = "0.7"
defmt = "1"
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }
embedded-alloc = "0.7"
static_cell = "2"
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
postcard = "1"
serde = { version = "1", default-features = false }
defmt = "0.3"
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
panic-probe = { version = "0.3", features = ["print-defmt"] }
cortex-m-semihosting = "0.5.0"
stm32h7xx-hal = { version="0.16", features= ["stm32h743v", "ethernet"] }
embedded-alloc = "0.5"
rtic-sync = { version = "1", features = ["defmt-03"] }
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] }
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms"] }
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
embassy-sync = "0.8"
embassy-futures = "0.1"
[dependencies.smoltcp]
version = "0.11.0"
default-features = false
features = ["medium-ethernet", "proto-ipv4", "socket-raw", "socket-dhcpv4", "socket-udp", "defmt"]
[dependencies.rtic]
version = "2"
features = ["thumbv7-backend"]
[dependencies.rtic-monotonics]
version = "1"
features = ["cortex-m-systick"]
[dependencies.satrs]
path = "../../satrs"
version = "0.2"
default-features = false
features = ["defmt", "heapless"]
[dev-dependencies]
defmt-test = "0.5"
defmt-test = "0.3"
# cargo build/run
[profile.dev]
@@ -0,0 +1,118 @@
sat-rs example for the STM32F3-Discovery board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32H743ZIT device was picked because it is one of the more powerful Cortex-M based devices
available for STM with which also has a little bit more RAM available and also allows commanding
via TCP/IP.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32H743ZITx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,119 @@
/* Taken from https://github.com/stm32-rs/stm32h7xx-hal/pull/299, adapted slightly to work with */
/* flip-link */
MEMORY
{
/* This file is intended for parts in the STM32H743/743v/753/753v families (RM0433), */
/* with the exception of the STM32H742/742v parts which have a different RAM layout. */
/* - FLASH and RAM are mandatory memory sections. */
/* - The sum of all non-FLASH sections must add to 1060K total device RAM. */
/* - The FLASH section size must match your device, see table below. */
/* FLASH */
/* Flash is divided in two independent banks (except 750xB). */
/* Select the appropriate FLASH size for your device. */
/* - STM32H750xB 128K (only FLASH1) */
/* - STM32H750xB 1M (512K + 512K) */
/* - STM32H743xI/753xI 2M ( 1M + 1M) */
FLASH1 : ORIGIN = 0x08000000, LENGTH = 1M
FLASH2 : ORIGIN = 0x08100000, LENGTH = 1M
/* Data TCM */
/* - Two contiguous 64KB RAMs. */
/* - Used for interrupt handlers, stacks and general RAM. */
/* - Zero wait-states. */
/* - The DTCM is taken as the origin of the base ram. (See below.) */
/* This is also where the interrupt table and such will live, */
/* which is required for deterministic performance. */
/* Need a region called RAM */
/* DTCM : ORIGIN = 0x20000000, LENGTH = 128K */
RAM : ORIGIN = 0x20000000, LENGTH = 128K
/* Instruction TCM */
/* - Used for latency-critical interrupt handlers etc. */
/* - Zero wait-states. */
ITCM : ORIGIN = 0x00000000, LENGTH = 64K
/* AXI SRAM */
/* - AXISRAM is in D1 and accessible by all system masters except BDMA. */
/* - Suitable for application data not stored in DTCM. */
/* - Zero wait-states. */
AXISRAM : ORIGIN = 0x24000000, LENGTH = 512K
/* AHB SRAM */
/* - SRAM1-3 are in D2 and accessible by all system masters except BDMA. */
/* Suitable for use as DMA buffers. */
/* - SRAM4 is in D3 and additionally accessible by the BDMA. Used for BDMA */
/* buffers, for storing application data in lower-power modes. */
/* - Zero wait-states. */
SRAM1 : ORIGIN = 0x30000000, LENGTH = 128K
SRAM2 : ORIGIN = 0x30020000, LENGTH = 128K
SRAM3 : ORIGIN = 0x30040000, LENGTH = 32K
SRAM4 : ORIGIN = 0x38000000, LENGTH = 64K
/* Backup SRAM */
BSRAM : ORIGIN = 0x38800000, LENGTH = 4K
}
/*
/* Assign the memory regions defined above for use. */
/*
/* Provide the mandatory FLASH and RAM definitions for cortex-m-rt's linker script. */
/* These do not work with flip-link */
REGION_ALIAS(FLASH, FLASH1);
/* REGION_ALIAS(RAM, DTCM); */
/* The location of the stack can be overridden using the `_stack_start` symbol. */
/* - Set the stack location at the end of RAM, using all remaining space. */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
/* The location of the .text section can be overridden using the */
/* `_stext` symbol. By default it will place after .vector_table. */
/* _stext = ORIGIN(FLASH) + 0x40c; */
/* Define sections for placing symbols into the extra memory regions above. */
/* This makes them accessible from code. */
/* - ITCM, DTCM and AXISRAM connect to a 64-bit wide bus -> align to 8 bytes. */
/* - All other memories connect to a 32-bit wide bus -> align to 4 bytes. */
SECTIONS {
.flash2 (NOLOAD) : ALIGN(4) {
*(.flash2 .flash2.*);
. = ALIGN(4);
} > FLASH2
.itcm (NOLOAD) : ALIGN(8) {
*(.itcm .itcm.*);
. = ALIGN(8);
} > ITCM
.axisram (NOLOAD) : ALIGN(8) {
*(.axisram .axisram.*);
. = ALIGN(8);
} > AXISRAM
.sram1 (NOLOAD) : ALIGN(8) {
*(.sram1 .sram1.*);
. = ALIGN(4);
} > SRAM1
.sram2 (NOLOAD) : ALIGN(8) {
*(.sram2 .sram2.*);
. = ALIGN(4);
} > SRAM2
.sram3 (NOLOAD) : ALIGN(4) {
*(.sram3 .sram3.*);
. = ALIGN(4);
} > SRAM3
.sram4 (NOLOAD) : ALIGN(4) {
*(.sram4 .sram4.*);
. = ALIGN(4);
} > SRAM4
.bsram (NOLOAD) : ALIGN(4) {
*(.bsram .bsram.*);
. = ALIGN(4);
} > BSRAM
};
@@ -0,0 +1,8 @@
/venv
/.tmtc-history.txt
/log
/.idea/*
!/.idea/runConfigurations
/seqcnt.txt
/tmtc_conf.json
@@ -0,0 +1,4 @@
{
"com_if": "udp",
"tcpip_udp_port": 7301
}
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""Example client for the sat-rs example application"""
import struct
import logging
import sys
import time
from typing import Any, Optional, cast
from prompt_toolkit.history import FileHistory, History
from spacepackets.ecss.tm import CdsShortTimestamp
import tmtccmd
from spacepackets.ecss import PusTelemetry, PusTelecommand, PusTm, PusVerificator
from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest
from tmtccmd.core.ccsds_backend import QueueWrapper
from tmtccmd.logging import add_colorlog_console_logger
from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
from tmtccmd.com import ComInterface
from tmtccmd.config import (
CmdTreeNode,
default_json_path,
SetupParams,
HookBase,
params_to_procedure_conversion,
)
from tmtccmd.config.com import SerialCfgWrapper
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
from tmtccmd.logging.pus import (
RegularTmtcLogWrapper,
RawTmtcTimedLogWrapper,
TimedLogWhen,
)
from tmtccmd.tmtc import (
TcQueueEntryType,
ProcedureWrapper,
TcProcedureType,
FeedWrapper,
SendCbParams,
DefaultPusQueueHelper,
)
from tmtccmd.pus.s5_fsfw_event import Service5Tm
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
from tmtccmd.util.obj_id import ObjectIdDictT
_LOGGER = logging.getLogger()
EXAMPLE_PUS_APID = 0x02
class SatRsConfigHook(HookBase):
def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path)
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import (
create_com_interface_default,
create_com_interface_cfg_default,
)
assert self.cfg_path is not None
cfg = create_com_interface_cfg_default(
com_if_key=com_if_key,
json_cfg_path=self.cfg_path,
space_packet_ids=None,
)
if cfg is None:
raise ValueError(
f"No valid configuration could be retrieved for the COM IF with key {com_if_key}"
)
if cfg.com_if_key == "serial_cobs":
cfg = cast(SerialCfgWrapper, cfg)
cfg.serial_cfg.serial_timeout = 0.5
return create_com_interface_default(cfg)
def get_command_definitions(self) -> CmdTreeNode:
"""This function should return the root node of the command definition tree."""
return create_cmd_definition_tree()
def get_cmd_history(self) -> Optional[History]:
"""Optionlly return a history class for the past command paths which will be used
when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids
return get_core_object_ids()
def create_cmd_definition_tree() -> CmdTreeNode:
root_node = CmdTreeNode.root_node()
root_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
root_node.add_child(CmdTreeNode("change_blink_freq", "Change blink frequency"))
return root_node
class PusHandler(SpecificApidHandlerBase):
def __init__(
self,
file_logger: logging.Logger,
verif_wrapper: VerificationWrapper,
raw_logger: RawTmtcTimedLogWrapper,
):
super().__init__(EXAMPLE_PUS_APID, None)
self.file_logger = file_logger
self.raw_logger = raw_logger
self.verif_wrapper = verif_wrapper
def handle_tm(self, packet: bytes, _user_args: Any):
try:
pus_tm = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e
service = pus_tm.service
tm_packet = None
if service == 1:
tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
)
res = self.verif_wrapper.add_tm(tm_packet)
if res is None:
_LOGGER.info(
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
)
_LOGGER.warning(
f"No matching telecommand found for {tm_packet.tc_req_id}"
)
else:
self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res)
if service == 3:
_LOGGER.info("No handling for HK packets implemented")
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
pus_tm = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if pus_tm.subservice == 25:
if len(pus_tm.source_data) < 8:
raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:]
_LOGGER.info("received JSON string: " + json_str.decode("utf-8"))
if service == 5:
tm_packet = Service5Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if service == 17:
tm_packet = Service17Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if tm_packet.subservice == 2:
_LOGGER.info("Received Ping Reply TM[17,2]")
else:
_LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
if tm_packet is None:
_LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory"
)
tm_packet = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
self.raw_logger.log_tm(pus_tm)
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
byte_string = bytearray(struct.pack("!I", target_id))
byte_string.extend(struct.pack("!I", unique_id))
return byte_string
class TcHandler(TcHandlerBase):
def __init__(
self,
seq_count_provider: FileSeqCountProvider,
verif_wrapper: VerificationWrapper,
):
super(TcHandler, self).__init__()
self.seq_count_provider = seq_count_provider
self.verif_wrapper = verif_wrapper
self.queue_helper = DefaultPusQueueHelper(
queue_wrapper=QueueWrapper.empty(),
tc_sched_timestamp_len=7,
seq_cnt_provider=seq_count_provider,
pus_verificator=verif_wrapper.pus_verificator,
default_pus_apid=EXAMPLE_PUS_APID,
)
def send_cb(self, send_params: SendCbParams):
entry_helper = send_params.entry
if entry_helper.is_tc:
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
pus_tc_wrapper.pus_tc.seq_count = (
self.seq_count_provider.get_and_increment()
)
self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc)
raw_tc = pus_tc_wrapper.pus_tc.pack()
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
send_params.com_if.send(raw_tc)
elif entry_helper.entry_type == TcQueueEntryType.LOG:
log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, info: ProcedureWrapper):
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
cmd_path = def_proc.cmd_path
if cmd_path == "/ping":
q.add_log_cmd("Sending PUS ping telecommand")
q.add_pus_tc(PusTelecommand(service=17, subservice=1))
if cmd_path == "/change_blink_freq":
self.create_change_blink_freq_command(q)
def create_change_blink_freq_command(self, q: DefaultPusQueueHelper):
q.add_log_cmd("Changing blink frequency")
while True:
blink_freq = int(
input(
"Please specify new blink frequency in ms. Valid Range [2..10000]: "
)
)
if blink_freq < 2 or blink_freq > 10000:
print(
"Invalid blink frequency. Please specify a value between 2 and 10000."
)
continue
break
app_data = struct.pack("!I", blink_freq)
q.add_pus_tc(PusTelecommand(service=8, subservice=1, app_data=app_data))
def main():
add_colorlog_console_logger(_LOGGER)
tmtccmd.init_printout(False)
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path())
parser_wrapper = PreArgsParsingWrapper()
parser_wrapper.create_default_parent_parser()
parser_wrapper.create_default_parser()
parser_wrapper.add_def_proc_args()
params = SetupParams()
post_args_wrapper = parser_wrapper.parse(hook_obj, params)
proc_wrapper = ProcedureParamsWrapper()
if post_args_wrapper.use_gui:
post_args_wrapper.set_params_without_prompts(proc_wrapper)
else:
post_args_wrapper.set_params_with_prompts(proc_wrapper)
params.apid = EXAMPLE_PUS_APID
setup_args = SetupWrapper(
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
)
# Create console logger helper and file loggers
tmtc_logger = RegularTmtcLogWrapper()
file_logger = tmtc_logger.logger
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
verificator = PusVerificator()
verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger)
# Create primary TM handler and add it to the CCSDS Packet Handler
tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger)
ccsds_handler = CcsdsTmHandler(generic_handler=None)
ccsds_handler.add_apid_handler(tm_handler)
# Create TC handler
seq_count_provider = PusFileSeqCountProvider()
tc_handler = TcHandler(seq_count_provider, verification_wrapper)
tmtccmd.setup(setup_args=setup_args)
init_proc = params_to_procedure_conversion(setup_args.proc_param_wrapper)
tmtc_backend = tmtccmd.create_default_tmtc_backend(
setup_wrapper=setup_args,
tm_handler=ccsds_handler,
tc_handler=tc_handler,
init_procedure=init_proc,
)
tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj)
try:
while True:
state = tmtc_backend.periodic_op(None)
if state.request == BackendRequest.TERMINATION_NO_ERROR:
sys.exit(0)
elif state.request == BackendRequest.DELAY_IDLE:
_LOGGER.info("TMTC Client in IDLE mode")
time.sleep(3.0)
elif state.request == BackendRequest.DELAY_LISTENER:
time.sleep(0.8)
elif state.request == BackendRequest.DELAY_CUSTOM:
if state.next_delay.total_seconds() <= 0.4:
time.sleep(state.next_delay.total_seconds())
else:
time.sleep(0.4)
elif state.request == BackendRequest.CALL_NEXT:
pass
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
tmtccmd == 8.0.1
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
@@ -0,0 +1,55 @@
//! Blinks an LED
//!
//! This assumes that LD2 (blue) is connected to pb7 and LD3 (red) is connected
//! to pb14. This assumption is true for the nucleo-h743zi board.
#![no_std]
#![no_main]
use satrs_stm32h7_nucleo_rtic as _;
use stm32h7xx_hal::{block, prelude::*, timer::Timer};
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
defmt::println!("starting stm32h7 blinky example");
// Get access to the device specific peripherals from the peripheral access crate
let dp = stm32h7xx_hal::stm32::Peripherals::take().unwrap();
// Take ownership over the RCC devices and convert them into the corresponding HAL structs
let rcc = dp.RCC.constrain();
let pwr = dp.PWR.constrain();
let pwrcfg = pwr.freeze();
// Freeze the configuration of all the clocks in the system and
// retrieve the Core Clock Distribution and Reset (CCDR) object
let rcc = rcc.use_hse(8.MHz()).bypass_hse();
let ccdr = rcc.freeze(pwrcfg, &dp.SYSCFG);
// Acquire the GPIOB peripheral
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
// Configure gpio B pin 0 as a push-pull output.
let mut ld1 = gpiob.pb0.into_push_pull_output();
// Configure gpio B pin 7 as a push-pull output.
let mut ld2 = gpiob.pb7.into_push_pull_output();
// Configure gpio B pin 14 as a push-pull output.
let mut ld3 = gpiob.pb14.into_push_pull_output();
// Configure the timer to trigger an update every second
let mut timer = Timer::tim1(dp.TIM1, ccdr.peripheral.TIM1, &ccdr.clocks);
timer.start(1.Hz());
// Wait for the timer to trigger an update and change the state of the LED
loop {
ld1.toggle();
ld2.toggle();
ld3.toggle();
block!(timer.wait()).unwrap();
}
}
@@ -0,0 +1,11 @@
#![no_main]
#![no_std]
use satrs_stm32h7_nucleo_rtic as _; // global logger + panicking-behavior + memory layout
#[cortex_m_rt::entry]
fn main() -> ! {
defmt::println!("Hello, world!");
satrs_stm32h7_nucleo_rtic::exit()
}
@@ -1,27 +1,15 @@
#![no_main]
#![no_std]
use defmt_rtt as _;
use embassy_stm32 as _;
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
// TODO(5) adjust HAL import
use stm32h7xx_hal as _; // memory layout
use panic_probe as _;
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
const HEAP_SIZE: usize = 131_072;
// Part of the library, because all binaries depend on crates which require an allocator.
#[global_allocator]
static HEAP: Heap = Heap::empty();
/// # Safety
///
/// Must be called exactly once, before the first allocation.
pub unsafe fn init_heap() {
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
}
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
@@ -29,6 +17,14 @@ fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
@@ -36,9 +32,14 @@ fn panic() -> ! {
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
panic!("unexpected hard fault");
loop {
debug::exit(debug::EXIT_FAILURE);
}
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
// once within a crate. the module can be in any file but there can only be at most
// one `#[tests]` module in this library crate
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
@@ -0,0 +1,528 @@
#![no_main]
#![no_std]
extern crate alloc;
use rtic::app;
use rtic_monotonics::systick::Systick;
use rtic_monotonics::Monotonic;
use satrs::pool::{PoolAddr, PoolProvider, StaticHeaplessMemoryPool};
use satrs::static_subpool;
// global logger + panicking-behavior + memory layout
use satrs_stm32h7_nucleo_rtic as _;
use smoltcp::socket::udp::UdpMetadata;
use smoltcp::socket::{dhcpv4, udp};
use core::mem::MaybeUninit;
use embedded_alloc::Heap;
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage};
use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr};
use stm32h7xx_hal::ethernet;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const PORT: u16 = 7301;
const HEAP_SIZE: usize = 131_072;
const TC_SOURCE_CHANNEL_DEPTH: usize = 16;
pub type SharedPool = StaticHeaplessMemoryPool<3>;
pub type TcSourceChannel = rtic_sync::channel::Channel<PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceTx = rtic_sync::channel::Sender<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceRx = rtic_sync::channel::Receiver<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
#[global_allocator]
static HEAP: Heap = Heap::empty();
// We place the memory pool buffers inside the larger AXISRAM.
pub const SUBPOOL_SMALL_NUM_BLOCKS: u16 = 32;
pub const SUBPOOL_SMALL_BLOCK_SIZE: usize = 32;
pub const SUBPOOL_MEDIUM_NUM_BLOCKS: u16 = 16;
pub const SUBPOOL_MEDIUM_BLOCK_SIZE: usize = 128;
pub const SUBPOOL_LARGE_NUM_BLOCKS: u16 = 8;
pub const SUBPOOL_LARGE_BLOCK_SIZE: usize = 2048;
// This data will be held by Net through a mutable reference
pub struct NetStorageStatic<'a> {
socket_storage: [SocketStorage<'a>; 8],
}
// MaybeUninit allows us write code that is correct even if STORE is not
// initialised by the runtime
static mut STORE: MaybeUninit<NetStorageStatic> = MaybeUninit::uninit();
static mut UDP_RX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_RX: [u8; 2048] = [0; 2048];
static mut UDP_TX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_TX: [u8; 2048] = [0; 2048];
/// Locally administered MAC address
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
pub struct Net {
iface: Interface,
ethdev: ethernet::EthernetDMA<4, 4>,
dhcp_handle: SocketHandle,
}
impl Net {
pub fn new(
sockets: &mut SocketSet<'static>,
mut ethdev: ethernet::EthernetDMA<4, 4>,
ethernet_addr: HardwareAddress,
) -> Self {
let config = Config::new(ethernet_addr);
let mut iface = Interface::new(
config,
&mut ethdev,
smoltcp::time::Instant::from_millis((Systick::now() - Systick::ZERO).to_millis()),
);
// Create sockets
let dhcp_socket = dhcpv4::Socket::new();
iface.update_ip_addrs(|addrs| {
let _ = addrs.push(IpCidr::new(IpAddress::v4(192, 168, 1, 99), 0));
});
let dhcp_handle = sockets.add(dhcp_socket);
Net {
iface,
ethdev,
dhcp_handle,
}
}
/// Polls on the ethernet interface. You should refer to the smoltcp
/// documentation for poll() to understand how to call poll efficiently
pub fn poll<'a>(&mut self, sockets: &'a mut SocketSet) -> bool {
let uptime = Systick::now() - Systick::ZERO;
let timestamp = smoltcp::time::Instant::from_millis(uptime.to_millis());
self.iface.poll(timestamp, &mut self.ethdev, sockets)
}
pub fn poll_dhcp<'a>(&mut self, sockets: &'a mut SocketSet) -> Option<dhcpv4::Event<'a>> {
let opt_event = sockets.get_mut::<dhcpv4::Socket>(self.dhcp_handle).poll();
if let Some(event) = &opt_event {
match event {
dhcpv4::Event::Deconfigured => {
defmt::info!("DHCP lost configuration");
self.iface.update_ip_addrs(|addrs| addrs.clear());
self.iface.routes_mut().remove_default_ipv4_route();
}
dhcpv4::Event::Configured(config) => {
defmt::info!("DHCP configuration acquired");
defmt::info!("IP address: {}", config.address);
self.iface.update_ip_addrs(|addrs| {
addrs.clear();
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
});
if let Some(router) = config.router {
defmt::debug!("Default gateway: {}", router);
self.iface
.routes_mut()
.add_default_ipv4_route(router)
.unwrap();
} else {
defmt::debug!("Default gateway: None");
self.iface.routes_mut().remove_default_ipv4_route();
}
}
}
}
opt_event
}
}
pub struct UdpNet {
udp_handle: SocketHandle,
last_client: Option<UdpMetadata>,
tc_source_tx: TcSourceTx,
}
impl UdpNet {
pub fn new<'sockets>(sockets: &mut SocketSet<'sockets>, tc_source_tx: TcSourceTx) -> Self {
// SAFETY: The RX and TX buffers are passed here and not used anywhere else.
let udp_rx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_RX_META[..] }, unsafe {
&mut UDP_RX[..]
});
let udp_tx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_TX_META[..] }, unsafe {
&mut UDP_TX[..]
});
let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
let udp_handle = sockets.add(udp_socket);
Self {
udp_handle,
last_client: None,
tc_source_tx,
}
}
pub fn poll<'sockets>(
&mut self,
sockets: &'sockets mut SocketSet,
shared_pool: &mut SharedPool,
) {
let socket = sockets.get_mut::<udp::Socket>(self.udp_handle);
if !socket.is_open() {
if let Err(e) = socket.bind(PORT) {
defmt::warn!("binding UDP socket failed: {}", e);
}
}
loop {
match socket.recv() {
Ok((data, client)) => {
match shared_pool.add(data) {
Ok(store_addr) => {
if let Err(e) = self.tc_source_tx.try_send(store_addr) {
defmt::warn!("TC source channel is full: {}", e);
}
}
Err(e) => {
defmt::warn!("could not add UDP packet to shared pool: {}", e);
}
}
self.last_client = Some(client);
// TODO: Implement packet wiretapping.
}
Err(e) => match e {
udp::RecvError::Exhausted => {
break;
}
udp::RecvError::Truncated => {
defmt::warn!("UDP packet was truncacted");
}
},
};
}
}
}
#[app(device = stm32h7xx_hal::stm32, peripherals = true)]
mod app {
use core::ptr::addr_of_mut;
use super::*;
use rtic_monotonics::systick::fugit::MillisDurationU32;
use rtic_monotonics::systick::Systick;
use satrs::spacepackets::ecss::tc::PusTcReader;
use stm32h7xx_hal::ethernet::{EthernetMAC, PHY};
use stm32h7xx_hal::gpio::{Output, Pin};
use stm32h7xx_hal::prelude::*;
use stm32h7xx_hal::stm32::Interrupt;
struct BlinkyLeds {
led1: Pin<'B', 7, Output>,
led2: Pin<'B', 14, Output>,
}
#[local]
struct Local {
leds: BlinkyLeds,
link_led: Pin<'B', 0, Output>,
net: Net,
udp: UdpNet,
tc_source_rx: TcSourceRx,
phy: ethernet::phy::LAN8742A<EthernetMAC>,
}
#[shared]
struct Shared {
blink_freq: MillisDurationU32,
eth_link_up: bool,
sockets: SocketSet<'static>,
shared_pool: SharedPool,
}
#[init]
fn init(mut cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
let pwr = cx.device.PWR.constrain();
let pwrcfg = pwr.freeze();
let rcc = cx.device.RCC.constrain();
// Try to keep the clock configuration similar to one used in STM examples:
// https://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/NUCLEO-H743ZI/Examples/GPIO/GPIO_EXTI/Src/main.c
let ccdr = rcc
.sys_ck(400.MHz())
.hclk(200.MHz())
.use_hse(8.MHz())
.bypass_hse()
.pclk1(100.MHz())
.pclk2(100.MHz())
.pclk3(100.MHz())
.pclk4(100.MHz())
.freeze(pwrcfg, &cx.device.SYSCFG);
// Initialize the systick interrupt & obtain the token to prove that we did
let systick_mono_token = rtic_monotonics::create_systick_token!();
Systick::start(
cx.core.SYST,
ccdr.clocks.sys_ck().to_Hz(),
systick_mono_token,
);
// Those are used in the smoltcp of the stm32h7xx-hal , I am not fully sure what they are
// good for.
cx.core.SCB.enable_icache();
cx.core.DWT.enable_cycle_counter();
let gpioa = cx.device.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = cx.device.GPIOB.split(ccdr.peripheral.GPIOB);
let gpioc = cx.device.GPIOC.split(ccdr.peripheral.GPIOC);
let gpiog = cx.device.GPIOG.split(ccdr.peripheral.GPIOG);
let link_led = gpiob.pb0.into_push_pull_output();
let mut led1 = gpiob.pb7.into_push_pull_output();
let mut led2 = gpiob.pb14.into_push_pull_output();
// Criss-cross pattern looks cooler.
led1.set_high();
led2.set_low();
let leds = BlinkyLeds { led1, led2 };
let rmii_ref_clk = gpioa.pa1.into_alternate::<11>();
let rmii_mdio = gpioa.pa2.into_alternate::<11>();
let rmii_mdc = gpioc.pc1.into_alternate::<11>();
let rmii_crs_dv = gpioa.pa7.into_alternate::<11>();
let rmii_rxd0 = gpioc.pc4.into_alternate::<11>();
let rmii_rxd1 = gpioc.pc5.into_alternate::<11>();
let rmii_tx_en = gpiog.pg11.into_alternate::<11>();
let rmii_txd0 = gpiog.pg13.into_alternate::<11>();
let rmii_txd1 = gpiob.pb13.into_alternate::<11>();
let mac_addr = smoltcp::wire::EthernetAddress::from_bytes(&MAC_ADDRESS);
/// Ethernet descriptor rings are a global singleton
#[link_section = ".sram3.eth"]
static mut DES_RING: MaybeUninit<ethernet::DesRing<4, 4>> = MaybeUninit::uninit();
let (eth_dma, eth_mac) = ethernet::new(
cx.device.ETHERNET_MAC,
cx.device.ETHERNET_MTL,
cx.device.ETHERNET_DMA,
(
rmii_ref_clk,
rmii_mdio,
rmii_mdc,
rmii_crs_dv,
rmii_rxd0,
rmii_rxd1,
rmii_tx_en,
rmii_txd0,
rmii_txd1,
),
// SAFETY: We do not move the returned DMA struct across thread boundaries, so this
// should be safe according to the docs.
unsafe { DES_RING.assume_init_mut() },
mac_addr,
ccdr.peripheral.ETH1MAC,
&ccdr.clocks,
);
// Initialise ethernet PHY...
let mut lan8742a = ethernet::phy::LAN8742A::new(eth_mac.set_phy_addr(0));
lan8742a.phy_reset();
lan8742a.phy_init();
unsafe {
ethernet::enable_interrupt();
cx.core.NVIC.set_priority(Interrupt::ETH, 196); // Mid prio
cortex_m::peripheral::NVIC::unmask(Interrupt::ETH);
}
// unsafe: mutable reference to static storage, we only do this once
let store = unsafe {
let store_ptr = STORE.as_mut_ptr();
// Initialise the socket_storage field. Using `write` instead of
// assignment via `=` to not call `drop` on the old, uninitialised
// value
addr_of_mut!((*store_ptr).socket_storage).write([SocketStorage::EMPTY; 8]);
// Now that all fields are initialised we can safely use
// assume_init_mut to return a mutable reference to STORE
STORE.assume_init_mut()
};
let (tc_source_tx, tc_source_rx) =
rtic_sync::make_channel!(PoolAddr, TC_SOURCE_CHANNEL_DEPTH);
let mut sockets = SocketSet::new(&mut store.socket_storage[..]);
let net = Net::new(&mut sockets, eth_dma, mac_addr.into());
let udp = UdpNet::new(&mut sockets, tc_source_tx);
let mut shared_pool: SharedPool = StaticHeaplessMemoryPool::new(true);
static_subpool!(
SUBPOOL_SMALL,
SUBPOOL_SMALL_SIZES,
SUBPOOL_SMALL_NUM_BLOCKS as usize,
SUBPOOL_SMALL_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_MEDIUM,
SUBPOOL_MEDIUM_SIZES,
SUBPOOL_MEDIUM_NUM_BLOCKS as usize,
SUBPOOL_MEDIUM_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_LARGE,
SUBPOOL_LARGE_SIZES,
SUBPOOL_LARGE_NUM_BLOCKS as usize,
SUBPOOL_LARGE_BLOCK_SIZE,
link_section = ".axisram"
);
shared_pool
.grow(
unsafe { SUBPOOL_SMALL.assume_init_mut() },
unsafe { SUBPOOL_SMALL_SIZES.assume_init_mut() },
SUBPOOL_SMALL_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
unsafe { SUBPOOL_MEDIUM.assume_init_mut() },
unsafe { SUBPOOL_MEDIUM_SIZES.assume_init_mut() },
SUBPOOL_MEDIUM_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
unsafe { SUBPOOL_LARGE.assume_init_mut() },
unsafe { SUBPOOL_LARGE_SIZES.assume_init_mut() },
SUBPOOL_LARGE_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
// Set up global allocator. Use AXISRAM for the heap.
#[link_section = ".axisram"]
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
eth_link_check::spawn().expect("eth link check failed");
blinky::spawn().expect("spawning blink task failed");
udp_task::spawn().expect("spawning UDP task failed");
tc_source_task::spawn().expect("spawning TC source task failed");
(
Shared {
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
eth_link_up: false,
sockets,
shared_pool,
},
Local {
link_led,
leds,
net,
udp,
tc_source_rx,
phy: lan8742a,
},
)
}
#[task(local = [leds], shared=[blink_freq])]
async fn blinky(mut cx: blinky::Context) {
let leds = cx.local.leds;
loop {
leds.led1.toggle();
leds.led2.toggle();
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Systick::delay(current_blink_freq).await;
}
}
/// This task checks for the network link.
#[task(local=[link_led, phy], shared=[eth_link_up])]
async fn eth_link_check(mut cx: eth_link_check::Context) {
let phy = cx.local.phy;
let link_led = cx.local.link_led;
loop {
let link_was_up = cx.shared.eth_link_up.lock(|link_up| *link_up);
if phy.poll_link() {
if !link_was_up {
link_led.set_high();
cx.shared.eth_link_up.lock(|link_up| *link_up = true);
defmt::info!("Ethernet link up");
}
} else if link_was_up {
link_led.set_low();
cx.shared.eth_link_up.lock(|link_up| *link_up = false);
defmt::info!("Ethernet link down");
}
Systick::delay(100.millis()).await;
}
}
#[task(binds=ETH, local=[net], shared=[sockets])]
fn eth_isr(mut cx: eth_isr::Context) {
// SAFETY: We do not write the register mentioned inside the docs anywhere else.
unsafe {
ethernet::interrupt_handler();
}
// Check and process ETH frames and DHCP. UDP is checked in a different task.
cx.shared.sockets.lock(|sockets| {
cx.local.net.poll(sockets);
cx.local.net.poll_dhcp(sockets);
});
}
/// This task routes UDP packets.
#[task(local=[udp], shared=[sockets, shared_pool])]
async fn udp_task(mut cx: udp_task::Context) {
loop {
cx.shared.sockets.lock(|sockets| {
cx.shared.shared_pool.lock(|pool| {
cx.local.udp.poll(sockets, pool);
})
});
Systick::delay(40.millis()).await;
}
}
/// This task handles all the incoming telecommands.
#[task(local=[read_buf: [u8; 1024] = [0; 1024], tc_source_rx], shared=[shared_pool])]
async fn tc_source_task(mut cx: tc_source_task::Context) {
loop {
let recv_result = cx.local.tc_source_rx.recv().await;
match recv_result {
Ok(pool_addr) => {
cx.shared.shared_pool.lock(|pool| {
match pool.read(&pool_addr, cx.local.read_buf.as_mut()) {
Ok(packet_len) => {
defmt::info!("received {} bytes in the TC source task", packet_len);
match PusTcReader::new(&cx.local.read_buf[0..packet_len]) {
Ok((packet, _tc_len)) => {
// TODO: Handle packet here or dispatch to dedicated PUS
// handler? Dispatching could simplify some things and make
// the software more scalable..
defmt::info!("received PUS packet: {}", packet);
}
Err(e) => {
defmt::info!("invalid TC format, not a PUS packet: {}", e);
}
}
if let Err(e) = pool.delete(pool_addr) {
defmt::warn!("deleting TC data failed: {}", e);
}
}
Err(e) => {
defmt::warn!("TC packet read failed: {}", e);
}
}
});
}
Err(e) => {
defmt::warn!("TC source reception error: {}", e);
}
};
}
}
}
@@ -1,7 +1,7 @@
#![no_std]
#![no_main]
use stm32h7_nucleo_rtic as _; // memory layout + panic handler
use stm32h7_testapp as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
@@ -0,0 +1,2 @@
/settings.json
/.cortex-debug.*
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"rust-lang.rust",
"probe-rs.probe-rs-debugger"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
@@ -0,0 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32H743ZITx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32h7-nucleo-rtic",
"rttEnabled": true,
"svdFile": "STM32H743.svd"
}
]
}
]
}
@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}
-1
View File
@@ -1 +0,0 @@
/config.toml
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "client"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4", features = ["derive"] }
log = "0.4"
fern = "0.7"
humantime = "2"
serde = { version = "1", features = ["derive"] }
toml = "1"
satrs = { path = "../../satrs" }
example-std = { path = "../example-std" }
minisim-types = { path = "../minisim-types" }
types = { path = "../types" }
spacepackets = { version = "0.18", default-features = false }
bitbybit = "2"
arbitrary-int = "2"
ctrlc = { version = "3.5" }
postcard = { version = "1", features = ["alloc"] }
anyhow = "1"
-13
View File
@@ -1,13 +0,0 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let config = manifest_dir.join("config.toml");
let config_template = manifest_dir.join("config.toml.template");
if !config.exists() {
fs::copy(&config_template, &config).unwrap();
}
println!("cargo::rerun-if-changed=config.toml.template");
}
-7
View File
@@ -1,7 +0,0 @@
# Copied to config.toml by the build script if config.toml does not exist yet. config.toml is not
# tracked by git, so it can hold local settings like the address of a development board.
[interface]
# Address of the commanded application. Defaults to the example-std OBSW on the local host.
# Can be overridden with the --udp-addr argument.
# udp_addr = "192.168.1.50:7301"
-909
View File
@@ -1,909 +0,0 @@
use anyhow::{Context as _, bail};
use arbitrary_int::u11;
use clap::Parser as _;
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
udp::SIM_CTRL_PORT,
};
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, SystemTime},
};
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
#[derive(clap::Parser)]
pub struct Cli {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
test_event: bool,
/// Address of the commanded application. Overrides the address inside `config.toml`.
#[arg(long, global = true)]
udp_addr: Option<SocketAddr>,
#[command(subcommand)]
commands: Option<Commands>,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Config {
#[serde(default)]
interface: InterfaceConfig,
}
#[derive(Debug, Default, serde::Deserialize)]
struct InterfaceConfig {
/// Defaults to the example-std OBSW on the local host.
udp_addr: Option<SocketAddr>,
}
impl Config {
/// The build script creates `config.toml` from the template. A missing file is still accepted,
/// because all parameters have defaults.
fn load() -> anyhow::Result<Self> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml");
match std::fs::read_to_string(&path) {
Ok(content) => toml::from_str(&content)
.with_context(|| format!("parsing {} failed", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(e) => Err(e).with_context(|| format!("reading {} failed", path.display())),
}
}
}
#[derive(clap::Subcommand)]
enum Commands {
Mgm0(MgmArgs),
Mgm1(MgmArgs),
MgmAssy(MgmAssemblyArgs),
Mgt(MgtArgs),
AcsSubsystem(SubsystemArgs),
EventManager(EventManagerArgs),
/// Blinking LEDs of the embedded examples.
Led(LedArgs),
}
#[derive(clap::Parser)]
struct EventManagerArgs {
#[command(subcommand)]
action: EventFilterAction,
}
#[derive(clap::Subcommand)]
enum EventFilterAction {
/// Enable event TM generation.
Enable(EventFilterArgs),
/// Disable event TM generation.
Disable(EventFilterArgs),
}
#[derive(clap::Args)]
struct EventFilterArgs {
#[arg(value_enum)]
component: EventSenderSelect,
/// Raw event ID. Without it, the filter applies to all events of the component.
#[arg(short, long)]
event_id: Option<u16>,
}
/// Components which emit events.
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum EventSenderSelect {
Controller,
Mgm0,
Mgm1,
MgmAssy,
Mgt,
Pcdu,
UdpServer,
TcpServer,
Ground,
}
impl From<EventSenderSelect> for types::ComponentId {
fn from(sender: EventSenderSelect) -> Self {
match sender {
EventSenderSelect::Controller => types::ComponentId::Controller,
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
EventSenderSelect::Ground => types::ComponentId::Ground,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultMode {
None,
/// SPI communication is all zeroes, modelling an unconnected sensor.
AllZeros,
/// SPI communication is all ones, modelling a broken sensor.
AllOnes,
}
impl From<FaultMode> for mgm::SpiFaultMode {
fn from(mode: FaultMode) -> Self {
match mode {
FaultMode::None => mgm::SpiFaultMode::None,
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultKind {
/// Cleared when the device is switched off, so a power cycle recovers from it.
Transient,
/// Survives power cycles.
#[default]
Permanent,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HkSelect {
OneShot,
EnablePeriodic,
DisablePeriodic,
ModifyInterval,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HealthStateSelect {
Healthy,
Faulty,
PermanentFaulty,
ExternalControl,
NeedsRecovery,
}
impl From<HealthStateSelect> for satrs::health::HealthState {
fn from(state: HealthStateSelect) -> Self {
match state {
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the sensor data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
#[arg(long, value_enum)]
fault: Option<FaultMode>,
/// Whether a power cycle clears the injected SPI fault.
#[arg(long, value_enum, default_value_t)]
fault_kind: FaultKind,
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
/// the handler after the underlying issue has been fixed or worked around.
#[arg(long, value_enum)]
health: Option<HealthStateSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgtArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the status data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
torque: Option<types::acs::mgt::Dipole>,
#[arg(long, default_value_t = 1000)]
torque_duration_ms: u64,
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
#[arg(long, value_enum)]
health: Option<HealthStateSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct LedArgs {
#[arg(short, long)]
ping: bool,
/// Mode of the red and the orange LED.
#[arg(short, long, value_enum)]
mode: Option<LedModeSelect>,
/// Toggle period of the toggle modes.
#[arg(long, default_value_t = 500)]
toggle_period_ms: u64,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum LedModeSelect {
AllOff,
RedOn,
OrangeOn,
AlternatingToggle,
UnifiedToggle,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmAssemblyArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<AssemblyModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct SubsystemArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<SubsystemModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum DeviceModeSelect {
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum AssemblyModeSelect {
NoModeKeeping,
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum SubsystemModeSelect {
Off,
Safe,
}
fn hk_request_type(
hk: HkSelect,
hk_interval_ms: Option<u64>,
) -> anyhow::Result<types::HkRequestType> {
let opt_interval = hk_interval_ms.map(Duration::from_millis);
Ok(match hk {
HkSelect::OneShot => types::HkRequestType::OneShot,
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
),
})
}
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
let axes: Vec<i16> = value
.split(',')
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
.collect::<Result<_, _>>()?;
let [x, y, z] = axes[..] else {
return Err(format!("expected 3 values, got {}", axes.len()));
};
Ok(types::acs::mgt::Dipole { x, y, z })
}
fn send_mgt_request(
client: &UdpSocket,
addr: SocketAddr,
request: types::acs::mgt::request::Request,
) {
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending MGT request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::request::Request) {
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Led, request.message_type()),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending LED request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) {
use types::led::request::Request;
if args.ping {
send_led_request(client, addr, Request::Ping);
}
if let Some(mode) = args.mode {
let toggle_period = Duration::from_millis(args.toggle_period_ms);
let mode = match mode {
LedModeSelect::AllOff => types::led::Mode::AllOff,
LedModeSelect::RedOn => types::led::Mode::RedOn,
LedModeSelect::OrangeOn => types::led::Mode::OrangeOn,
LedModeSelect::AlternatingToggle => types::led::Mode::AlternatingToggle(toggle_period),
LedModeSelect::UnifiedToggle => types::led::Mode::UnifiedToggle(toggle_period),
};
send_led_request(client, addr, Request::SetMode(mode));
}
}
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
use types::acs::mgt::request::{ModeRequest, Request};
if args.ping {
send_mgt_request(client, addr, Request::Ping);
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
send_mgt_request(client, addr, Request::Hk(req_type));
}
if let Some(mode) = args.mode {
let mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
}
if let Some(dipole) = args.torque {
let request = Request::ApplyTorque {
dipole,
duration: Duration::from_millis(args.torque_duration_ms),
};
send_mgt_request(client, addr, request);
}
if let Some(health) = args.health {
send_mgt_request(
client,
addr,
Request::Health(types::HealthRequest::SetHealth(health.into())),
);
}
Ok(())
}
fn handle_mgm_command(
client: &UdpSocket,
addr: SocketAddr,
target_id: types::ComponentId,
args: MgmArgs,
) -> anyhow::Result<()> {
if let Some(mode) = args.fault {
inject_mgm_failure(
target_id,
mgm::SpiFault {
mode: mode.into(),
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
},
)?;
}
if args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Hk),
types::acs::mgm::request::Request::Hk(HkRequest {
id: types::acs::mgm::request::HkId::Sensor,
req_type,
}),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = args.mode {
let dev_mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm::request::Request::Mode(
types::acs::mgm::request::ModeRequest::SetMode(dev_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(health) = args.health {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Health),
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
health.into(),
)),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} set-health request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
Ok(())
}
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
use types::event_manager::request::Request;
let request = match args.action {
EventFilterAction::Enable(filter) => match filter.event_id {
Some(event_id) => Request::EnableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::EnableComponent(filter.component.into()),
},
EventFilterAction::Disable(filter) => match filter.event_id {
Some(event_id) => Request::DisableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::DisableComponent(filter.component.into()),
},
};
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
log::info!(
"sending event manager request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&request_packet.to_vec(), addr).unwrap();
}
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(level)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
fn main() -> anyhow::Result<()> {
setup_logger(log::LevelFilter::Debug).unwrap();
let kill_signal = Arc::new(AtomicBool::new(false));
let ctrl_kill_signal = kill_signal.clone();
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
let cli = Cli::parse();
let config = Config::load()?;
let addr = cli
.udp_addr
.or(config.interface.udp_addr)
.unwrap_or(SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT));
// Bind to all interfaces, so embedded targets inside the local network can be reached too.
let client = UdpSocket::bind("0.0.0.0:7302").expect("Connecting to UDP server failed");
client.set_nonblocking(true)?;
client.set_read_timeout(Some(Duration::from_millis(200)))?;
if cli.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
types::control::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if cli.test_event {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
types::control::request::Request::TestEvent,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending event request with TC ID {:#010x}",
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(cmd) = cli.commands {
match cmd {
Commands::Mgm0(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
}
Commands::Mgm1(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
}
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
Commands::MgmAssy(mgm_assembly_args) => {
let target_id = types::ComponentId::AcsMgmAssembly;
if mgm_assembly_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = mgm_assembly_args.mode {
let assembly_mode = match mode {
AssemblyModeSelect::NoModeKeeping => {
types::acs::mgm_assembly::Mode::NoModeKeeping
}
AssemblyModeSelect::Off => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
}
AssemblyModeSelect::Normal => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
}
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm_assembly::request::Request::Mode(
types::acs::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::AcsSubsystem(subsystem_args) => {
let target_id = types::ComponentId::AcsSubsystem;
if subsystem_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::subsystem::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = subsystem_args.mode {
let subsystem_mode = match mode {
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::subsystem::request::Request::Mode(
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} mode request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
Commands::Led(args) => handle_led_command(&client, addr, args),
}
}
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
log::info!("entering listening loop");
loop {
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
log::info!("received kill signal, exiting");
break;
}
match client.recv(recv_buf.as_mut_slice()) {
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
continue;
}
log::warn!("UDP reception error: {}", e)
}
}
}
Ok(())
}
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
///
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
/// UDP send would otherwise silently do nothing if minisim is not running.
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
let mut reply_buf = [0u8; 4096];
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
match sim_socket.recv(&mut reply_buf) {
Ok(len) => {
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
}
}
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
}
Err(e) => return Err(e.into()),
}
let id = match target_id {
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
};
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
id,
request: mgm::Request::SetSpiFault(fault),
});
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
Ok(())
}
/// Each component has its own event type, so the sender ID determines how to decode the event.
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
sender_id: types::ComponentId,
data: &[u8],
) {
match postcard::from_bytes::<E>(data) {
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
}
}
match sender_id {
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
log_event::<types::acs::mgm::Event>(sender_id, data)
}
types::ComponentId::AcsMgmAssembly => {
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
}
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
// TC source events are sent with the ID of the packet source.
types::ComponentId::UdpServer
| types::ComponentId::TcpServer
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
_ => log::warn!(
"Received event from {:?} with unknown event type",
sender_id
),
}
}
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
Ok(packet) => {
let tm_header_result = postcard::take_from_bytes::<types::TmHeader>(packet.user_data());
if let Err(e) = tm_header_result {
bail!("Failed to deserialize TM header: {}", e);
}
let (tm_header, remainder) = tm_header_result.unwrap();
if let Some(tc_id) = tm_header.tc_id {
log::info!(
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
packet.apid(),
tm_header.sender_id,
tc_id.raw()
);
}
if tm_header.message_type == MessageType::Event {
handle_event(tm_header.sender_id, remainder);
return Ok(());
}
match tm_header.sender_id {
types::ComponentId::EpsPcdu => {
let response =
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
log::info!("Received response from PCDU: {:?}", response.unwrap());
}
types::ComponentId::Controller => {
let response =
postcard::from_bytes::<types::control::response::Response>(remainder);
log::info!("Received response from controller: {:?}", response.unwrap());
}
types::ComponentId::AcsMgmAssembly => {
let response = postcard::from_bytes::<
types::acs::mgm_assembly::response::Response,
>(remainder);
log::info!(
"Received response from MGM Assembly: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsMgm0 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM0: {:?}", response.unwrap());
}
types::ComponentId::AcsMgm1 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM1: {:?}", response.unwrap());
}
types::ComponentId::AcsSubsystem => {
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
remainder,
);
log::info!(
"Received response from ACS subsystem: {:?}",
response.unwrap()
);
}
types::ComponentId::EpsSubsystem => todo!(),
types::ComponentId::UdpServer => todo!(),
types::ComponentId::TcpServer => todo!(),
types::ComponentId::Ground => todo!(),
types::ComponentId::EventManager => {
let response =
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
log::info!(
"Received response from event manager: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsController => todo!(),
types::ComponentId::AcsMgt => {
let response =
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
log::info!("Received response from MGT: {:?}", response.unwrap());
}
types::ComponentId::Led => {
let response =
postcard::from_bytes::<types::led::response::Response>(remainder);
log::info!("Received response from LED: {:?}", response.unwrap());
}
}
}
Err(_) => todo!(),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dipole() {
assert_eq!(
parse_dipole("-200, 200,1000"),
Ok(types::acs::mgt::Dipole {
x: -200,
y: 200,
z: 1000
})
);
assert!(parse_dipole("1,2").is_err());
assert!(parse_dipole("1,2,3,4").is_err());
}
#[test]
fn test_config_template_is_valid() {
let template = include_str!("../config.toml.template");
let config: Config = toml::from_str(template).unwrap();
assert_eq!(config.interface.udp_addr, None);
let config: Config =
toml::from_str("[interface]\nudp_addr = \"192.168.1.50:7301\"").unwrap();
assert_eq!(
config.interface.udp_addr,
Some("192.168.1.50:7301".parse().unwrap())
);
}
#[test]
fn test_negative_torque_argument() {
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
let Some(Commands::Mgt(args)) = cli.commands else {
panic!("expected mgt subcommand");
};
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
}
}
-38
View File
@@ -1,38 +0,0 @@
[package]
name = "example-std"
version = "0.1.1"
edition = "2024"
default-run = "example-std"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
[dependencies]
fern = "0.7"
chrono = "0.4"
log = "0.4"
crossbeam-channel = "0.5"
delegate = "0.13"
zerocopy = "0.8"
csv = "1"
num_enum = "0.7"
thiserror = "2"
lazy_static = "1"
strum = { version = "0.28", features = ["derive"] }
derive-new = "0.7"
cfg-if = "1"
arbitrary-int = "2"
bitbybit = "2"
postcard = { version = "1", features = ["alloc"] }
ctrlc = "3"
serde = { version = "1", features = ["derive"] }
satrs = { path = "../../satrs", features = ["test_util"] }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
satrs-mib = { path = "../../satrs-mib" }
[features]
# default = ["heap_tmtc"]
# heap_tmtc = []
[dev-dependencies]
env_logger = "0.11"
-57
View File
@@ -1,57 +0,0 @@
sat-rs example
======
This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
You can run the application using `cargo run`.
# Features
The example has the `heap_tmtc` feature which is enabled by default. With this feature enabled,
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
stores.
You can run the application without this feature using
```sh
cargo run --no-default-features
```
# Interacting with the sat-rs example
The `client` crate is a command line client which sends telecommands to the example application
and prints the received telemetry. For example, you can ping the application or switch MGM 0
to normal mode like this:
```sh
cargo run -p client -- --ping
cargo run -p client -- mgm0 -m normal
```
Use `cargo run -p client -- --help` to list all available commands.
## Adding the mini simulator application
This example application features a few device handlers. The
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
can be used to simulate the physical devices managed by these device handlers.
The example application will attempt communication with the mini simulator on UDP port 7303.
If this works, the device handlers will use communication interfaces dedicated to the communication
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
return constant values or behave like ideal devices.
In summary, you can use the following command command to run the mini-simulator first:
```sh
cargo run -p minisim
```
and then start the example using `cargo run -p example-std`.
-118
View File
@@ -1,118 +0,0 @@
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use types::acs::ctrl::{Mode, request::ModeRequest, response::ModeReport};
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: Receiver<ModeRequest>,
pub report_tx: SyncSender<ModeReport>,
}
/// Dummy ACS controller. It has no actual control law and reaches any commanded mode
/// immediately, which is sufficient to exercise the mode commanding of its parent subsystem.
pub struct Controller {
mode: Mode,
mode_leaf_helper: ModeLeafHelper,
}
impl Controller {
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
Self {
mode: Mode::Passive,
mode_leaf_helper,
}
}
#[allow(dead_code)]
#[inline]
pub fn mode(&self) -> Mode {
self.mode
}
pub fn periodic_operation(&mut self) {
self.handle_mode_leaf_handling();
}
fn handle_mode_leaf_handling(&mut self) {
loop {
match self.mode_leaf_helper.request_rx.try_recv() {
Ok(request) => match request {
ModeRequest::SetMode(mode) => {
log::info!("ACS controller: transitioning to mode {:?}", mode);
self.mode = mode;
self.report_mode();
}
ModeRequest::ReadMode => self.report_mode(),
},
Err(e) => match e {
TryRecvError::Empty => break,
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
fn report_mode(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeReport::Mode(self.mode))
.expect("failed to send mode report to parent");
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use super::*;
struct ControllerTestbench {
request_tx: SyncSender<ModeRequest>,
report_rx: Receiver<ModeReport>,
controller: Controller,
}
impl ControllerTestbench {
fn new() -> Self {
let (request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, report_rx) = mpsc::sync_channel(5);
Self {
request_tx,
report_rx,
controller: Controller::new(ModeLeafHelper {
request_rx,
report_tx,
}),
}
}
}
#[test]
fn test_initial_mode() {
let testbench = ControllerTestbench::new();
assert_eq!(testbench.controller.mode(), Mode::Passive);
}
#[test]
fn test_set_mode() {
let mut testbench = ControllerTestbench::new();
testbench
.request_tx
.send(ModeRequest::SetMode(Mode::Safe))
.unwrap();
testbench.controller.periodic_operation();
assert_eq!(testbench.controller.mode(), Mode::Safe);
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Safe));
}
#[test]
fn test_read_mode() {
let mut testbench = ControllerTestbench::new();
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
testbench.controller.periodic_operation();
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Passive));
}
}
File diff suppressed because it is too large. Load diff
@@ -1,662 +0,0 @@
use std::{sync::mpsc, time::Duration};
use example_std::{ModeHelper, TmtcQueues};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, DeviceMode,
acs::mgm_assembly::{Mode, request, response},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct ParentQueueHelper {
pub request_rx: mpsc::Receiver<request::ModeRequest>,
pub report_tx: mpsc::SyncSender<response::ModeResponse>,
}
/// Helper component for communication with a parent component, which is usually as assembly.
pub struct ChildrenQueueHelper {
pub request_tx_queues: [mpsc::SyncSender<types::acs::mgm::request::ModeRequest>; 2],
pub report_rx_queues: [mpsc::Receiver<types::acs::mgm::response::ModeResponse>; 2],
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
#[derive(Debug, Default, Copy, Clone)]
pub struct MgmInfo {
reply_received: bool,
mode: Option<DeviceMode>,
}
/// MGM assembly component.
pub struct Assembly {
mode_helper: ModeHelper<Mode, TransitionState>,
/// This boolean is used for the distinction between transitions commanded by the parent
/// or by ground, and transitions which were commanded autonomously as part of children
/// mode keeping.
mode_keeping_transition: bool,
tmtc_queues: TmtcQueues,
mgm_modes: [MgmInfo; 2],
parent_queues: ParentQueueHelper,
pub(crate) children_queues: ChildrenQueueHelper,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
}
impl Assembly {
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
pub fn new(
parent_queues: ParentQueueHelper,
children_queues: ChildrenQueueHelper,
tmtc_queues: TmtcQueues,
mode_timeout: Duration,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
) -> Self {
Self {
mode_helper: ModeHelper::new(Mode::NoModeKeeping, mode_timeout),
mode_keeping_transition: false,
tmtc_queues,
mgm_modes: [MgmInfo::default(); 2],
parent_queues,
children_queues,
event_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_parent_mode_queue();
self.handle_children_mode_queues();
if self.mode_helper.transition_active() {
self.handle_mode_transition();
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::mgm_assembly::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::mgm_assembly::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::mgm_assembly::request::Request::Mode(request) => {
match request {
request::ModeRequest::SetMode(assembly_mode) => {
self.start_transition(false, assembly_mode, Some(tc_id))
}
request::ModeRequest::ReadMode => self.send_telemetry(
Some(tc_id),
response::Response::Mode(response::ModeResponse::Mode(
self.mode(),
)),
),
}
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::mgm_assembly::response::Response,
) {
match pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_parent_mode_queue(&mut self) {
loop {
match self.parent_queues.request_rx.try_recv() {
Ok(request) => match request {
request::ModeRequest::SetMode(assembly_mode) => match assembly_mode {
Mode::Device(_device_mode) => {
self.start_transition(false, assembly_mode, None);
}
Mode::NoModeKeeping => {
self.mode_helper.current = Mode::NoModeKeeping;
}
},
request::ModeRequest::ReadMode => self
.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap(),
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_children_mode_queues(&mut self) {
let mut mode_report_received = false;
for (idx, rx) in self.children_queues.report_rx_queues.iter_mut().enumerate() {
loop {
match rx.try_recv() {
Ok(report) => match report {
types::acs::mgm::response::ModeResponse::Mode(device_mode) => {
self.mgm_modes[idx].mode = Some(device_mode);
self.mgm_modes[idx].reply_received = true;
mode_report_received = true;
}
types::acs::mgm::response::ModeResponse::SetModeTimeout => {
// Ignore, handle this with our own timeout.
log::warn!("MGM {} mode timeout", idx);
}
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
if !mode_report_received {
return;
}
// Transition is active, check for completion.
if self.mode_helper.transition_active()
&& self.mgm_modes.iter().all(|i| i.reply_received)
&& let Mode::Device(device_mode) = self.mode_helper.target.unwrap()
{
// If at least one child reached the correct mode, we are done.
if self.mgm_modes.iter().any(|i| i.mode == Some(device_mode)) {
self.handle_mode_reached(true);
} else {
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::WrongMode(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
// Mode keeping active: Check children modes.
if let Mode::Device(device_mode) = self.mode_helper.current
&& self
.mgm_modes
.iter()
.all(|info| info.mode != Some(device_mode))
{
// Children lost mode. Try to command them back to the correct
// mode.
self.start_transition(true, self.mode_helper.current, None);
}
}
pub fn handle_mode_transition(&mut self) {
if self.mode_helper.target.is_none() {
self.handle_mode_reached(true);
return;
}
let target = self.mode_helper.target.unwrap();
let device_mode = match target {
Mode::Device(device_mode) => device_mode,
Mode::NoModeKeeping => {
self.handle_mode_reached(true);
return;
}
};
if self.mode_helper.transition_state == TransitionState::Idle {
self.command_children(device_mode);
self.mode_helper.transition_state = TransitionState::AwaitingReplies;
}
if self.mode_helper.transition_state == TransitionState::AwaitingReplies
&& self.mode_helper.timed_out()
{
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::SetModeTimeout(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
pub fn handle_mode_reached(&mut self, success: bool) {
let tc_commander = self.mode_helper.finish(success);
self.announce_mode();
if tc_commander.is_some() {
self.send_telemetry(tc_commander, response::Response::Ok);
}
self.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap();
}
pub fn handle_mode_transition_failure(&mut self, report: response::ModeResponse) {
if self.mode_helper.tc_commander.is_some() {
self.send_telemetry(
self.mode_helper.tc_commander,
response::Response::Mode(response::ModeResponse::SetModeTimeout(
self.mgm_modes.map(|info| info.mode),
)),
);
}
self.parent_queues.report_tx.send(report).unwrap();
self.mode_helper.finish(false);
}
pub fn command_children(&self, mode: DeviceMode) {
for tx in &self.children_queues.request_tx_queues {
tx.send(types::acs::mgm::request::ModeRequest::SetMode(mode))
.unwrap();
}
}
pub fn start_transition(
&mut self,
mode_keeping: bool,
target: Mode,
tc_id: Option<CcsdsPacketIdAndPsc>,
) {
self.mode_keeping_transition = mode_keeping;
self.mode_helper.tc_commander = tc_id;
self.mgm_modes
.iter_mut()
.for_each(|m| m.reply_received = false);
self.mode_helper.start(target);
}
fn announce_mode(&self) {
log::info!(
"{:?} announcing mode: {:?}",
Self::ID,
self.mode_helper.current
);
if let Err(e) = self
.event_tx
.send(types::acs::mgm_assembly::Event::ModeChanged(
self.mode_helper.current,
))
{
log::warn!("{:?}: failed to send mode changed event: {}", Self::ID, e);
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
#[cfg(test)]
fn mode_transition_active(&self) -> bool {
self.mode_helper.transition_active()
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message, MessageType, TcHeader,
acs::mgm_assembly,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
};
use super::*;
pub struct Testbench {
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
subsystem_report_rx: mpsc::Receiver<response::ModeResponse>,
mgm_request_rx: [mpsc::Receiver<types::acs::mgm::request::ModeRequest>; 2],
mgm_report_tx: [mpsc::SyncSender<types::acs::mgm::response::ModeResponse>; 2],
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgm_assembly::Event>,
assembly: Assembly,
}
impl Testbench {
pub fn new() -> Self {
let (subsystem_req_tx, subsystem_req_rx) = mpsc::sync_channel(5);
let (subsystem_report_tx, subsystem_report_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
Self {
subsystem_req_tx,
subsystem_report_rx,
mgm_request_rx: [mgm_0_mode_request_rx, mgm_1_mode_request_rx],
mgm_report_tx: [mgm_0_mode_report_tx, mgm_1_mode_report_tx],
tc_tx,
tm_rx,
event_rx,
assembly: Assembly::new(
ParentQueueHelper {
request_rx: subsystem_req_rx,
report_tx: subsystem_report_tx,
},
ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues { tc_rx, tm_tx },
Duration::from_millis(20),
event_tx,
),
}
}
pub fn assert_all_queues_empty(&self) {
assert!(
matches!(self.tm_rx.try_recv().unwrap_err(), TryRecvError::Empty),
"TM queue not empty"
);
assert!(
matches!(
self.subsystem_report_rx.try_recv().unwrap_err(),
TryRecvError::Empty
),
"subsystem report queue not empty"
);
for rx in self.mgm_request_rx.iter() {
assert!(
matches!(rx.try_recv().unwrap_err(), TryRecvError::Empty),
"mgm request queue not empty"
)
}
}
}
pub fn create_request_tc(
request: types::acs::mgm_assembly::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(Assembly::ID, request.message_type()),
request,
)
}
#[test]
fn basic_test() {
let mut tb = Testbench::new();
tb.assert_all_queues_empty();
tb.assembly.periodic_operation();
tb.assert_all_queues_empty();
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
}
#[test]
fn test_tc_commanded_transition() {
let mut tb = Testbench::new();
tb.tc_tx
.send(create_request_tc(mgm_assembly::request::Request::Mode(
request::ModeRequest::SetMode(Mode::Device(DeviceMode::Normal)),
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let response = tb.tm_rx.try_recv().unwrap();
assert_eq!(response.tm_header.sender_id, Assembly::ID);
assert_eq!(response.tm_header.message_type, MessageType::Verification);
let response: response::Response = postcard::from_bytes(&response.payload).unwrap();
assert_eq!(response, response::Response::Ok);
let event = tb.event_rx.try_recv().expect("expected mode changed event");
assert!(matches!(
event,
mgm_assembly::Event::ModeChanged(Mode::Device(DeviceMode::Normal))
));
}
#[test]
fn test_parent_commanded_transition() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_one_mgm_is_sufficient() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// One device is sufficient.
tb.mgm_report_tx[0]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
tb.mgm_report_tx[1]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_mode_commanding_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::WrongMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
#[test]
fn test_mode_keeping_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
// This should start mode keeping.
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Let the mode keeping fail.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
}
-876
View File
@@ -1,876 +0,0 @@
use std::collections::VecDeque;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use example_std::{HkHelperSingleSet, TmtcQueues};
use minisim_types::acs::mgt as sim_mgt;
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::acs::mgt::{
self, HkSet,
request::{ModeRequest, Request},
response::{ModeResponse, Response},
};
use types::pcdu::SwitchId;
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
use crate::device_fdir::{DeviceFdir, FdirEvent};
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
use crate::eps::PowerSwitchHelper;
// The handler blocks while waiting for a reply, so this must be well below the cycle time
// of the ACS thread.
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
/// Interface for ideal device which never fails.
#[derive(Default)]
pub struct DummyInterface {
dipole: mgt::Dipole,
torque_end: Option<Instant>,
}
impl DummyInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
sim_mgt::Request::ApplyTorque { duration, dipole } => {
self.dipole = dipole;
self.torque_end = Some(Instant::now() + duration);
sim_mgt::Reply::Ack
}
sim_mgt::Request::RequestHk => {
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: if torquing {
self.dipole
} else {
mgt::Dipole::default()
},
torquing,
})
}
};
Some(reply.to_frame())
}
}
/// Records all sent frames and returns injected reply frames.
#[derive(Default)]
pub struct TestInterface {
pub sent_frames: Vec<Vec<u8>>,
pub replies: VecDeque<Vec<u8>>,
}
pub struct SimInterface {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
impl SimInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
// Replies which arrived after a previous timeout must not be mistaken for this reply.
while self.sim_reply_rx.try_recv().is_ok() {}
if let Err(e) = self
.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
frame.to_vec(),
)))
{
log::error!("failed to send MGT SIM request: {e}");
return None;
}
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
SimReply::Mgt(frame) => Some(frame),
sim_reply => {
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
None
}
}
}
}
/// Frame based transport to the device. The handler implements the protocol on top of it.
pub enum MgtCommunication {
Dummy(DummyInterface),
Sim(SimInterface),
#[allow(dead_code)]
Test(TestInterface),
}
impl MgtCommunication {
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
/// was no reply in time.
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
match self {
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
MgtCommunication::Sim(sim) => sim.transfer(frame),
MgtCommunication::Test(test) => {
test.sent_frames.push(frame.to_vec());
test.replies.pop_front()
}
}
}
}
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: mpsc::Receiver<ModeRequest>,
pub report_tx: mpsc::SyncSender<ModeResponse>,
}
/// Magnetorquer (MGT) device handler.
///
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
/// a fault which is handled by the FDIR.
///
/// This device handler includes several components beyond the scope of commanding the device:
///
/// - The [MgtCommunication] structure models different communication interfaces to the
/// physical device.
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
/// helper.
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
pub struct MgtHandler {
tmtc_queues: TmtcQueues,
pub com: MgtCommunication,
hk_set: HkSet,
hk_helper: HkHelperSingleSet,
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
mode_leaf_helper: ModeLeafHelper,
fdir: DeviceFdir,
event_tx: mpsc::SyncSender<mgt::Event>,
}
impl MgtHandler {
pub fn new(
tmtc_queues: TmtcQueues,
switch_helper: PowerSwitchHelper,
com: MgtCommunication,
mode_leaf_helper: ModeLeafHelper,
mode_timeout: Duration,
health_table: HealthTableMapSync,
event_tx: mpsc::SyncSender<mgt::Event>,
) -> Self {
Self {
tmtc_queues,
com,
hk_set: HkSet::default(),
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
switch_and_mode_helper: SwitchAndModeHelper::new(
DeviceMode::Off,
mode_timeout,
switch_helper,
SwitchId::Mgt,
),
mode_leaf_helper,
fdir: DeviceFdir::new(
"MGT",
ComponentId::AcsMgt,
health_table,
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
),
event_tx,
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.switch_and_mode_helper.mode()
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_mode_leaf_handling();
self.fdir
.periodic_operation(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
match event {
ModeTransitionEvent::Reached(tc_commander) => {
self.handle_mode_reached(tc_commander)
}
ModeTransitionEvent::Failed(tc_commander) => {
self.handle_mode_transition_failure(tc_commander)
}
ModeTransitionEvent::PowerCycleDone => {
self.fdir.handle_power_cycle_done();
self.handle_fdir_events();
}
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
self.fdir
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
self.handle_fdir_events();
}
}
}
if self.ready_for_commanding() {
self.poll_hk();
}
if self.hk_helper.needs_generation() {
self.send_telemetry(None, Response::Hk(self.hk_set));
}
}
fn poll_hk(&mut self) {
match self.transfer(sim_mgt::Request::RequestHk) {
Some(sim_mgt::Reply::Hk(hk)) => {
self.fdir.register_success();
self.hk_set = HkSet {
valid: true,
dipole: hk.dipole,
torquing: hk.torquing,
};
}
reply => self.register_reply_fault(reply),
}
}
/// Returns [None] if there was no reply in time or the reply frame was invalid.
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
let frame = self.com.transfer(&request.to_frame())?;
sim_mgt::Reply::from_frame(&frame)
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
.ok()
}
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
log::warn!("MGT: missing or unexpected reply {reply:?}");
self.hk_set.valid = false;
self.fdir.register_fault(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
}
fn handle_fdir_events(&mut self) {
while let Some(event) = self.fdir.next_event() {
let event = match event {
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
FdirEvent::Recovery(recovery_event) => {
// The device is power cycled or switched off.
if matches!(
recovery_event,
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
) {
self.hk_set = HkSet::default();
}
mgt::Event::Recovery(recovery_event)
}
};
self.send_event(event);
}
}
fn ready_for_commanding(&self) -> bool {
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("MGT: failed to deserialize request: {}", e);
continue;
}
};
log::info!(
"MGT: received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
Request::Mode(ModeRequest::SetMode(mode)) => {
self.start_transition(mode, Some(tc_id))
}
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
Some(tc_id),
Response::Mode(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
)),
),
Request::ApplyTorque { dipole, duration } => {
self.handle_torque_command(tc_id, dipole, duration)
}
Request::Health(HealthRequest::SetHealth(health)) => {
log::info!("MGT: setting health to {health:?} via ground command");
self.fdir.set_health(health);
self.send_telemetry(Some(tc_id), Response::Ok);
}
}
}
}
fn handle_mode_leaf_handling(&mut self) {
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
match request {
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
ModeRequest::ReadMode => self.report_mode_to_parent(),
}
}
}
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
match hk_request {
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
HkRequestType::EnablePeriodic(opt_interval) => {
self.hk_helper.enabled = true;
if let Some(interval) = opt_interval {
self.hk_helper.frequency = interval;
}
}
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
_ => log::warn!("MGT: unhandled HK request"),
}
}
fn handle_torque_command(
&mut self,
tc_id: CcsdsPacketIdAndPsc,
dipole: mgt::Dipole,
duration: Duration,
) {
if !self.ready_for_commanding() {
log::warn!("MGT: rejecting torque command, device not in normal mode");
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
return;
}
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
Some(sim_mgt::Reply::Ack) => {
self.fdir.register_success();
self.send_telemetry(Some(tc_id), Response::Ok);
}
reply => {
self.register_reply_fault(reply);
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
}
}
}
fn start_transition(
&mut self,
target_mode: DeviceMode,
tc_commander: Option<CcsdsPacketIdAndPsc>,
) {
log::info!("MGT: transitioning to mode {:?}", target_mode);
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
if target_mode == DeviceMode::Off {
self.hk_set = HkSet::default();
}
self.switch_and_mode_helper
.start_transition(target_mode, tc_commander);
}
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
log::info!("MGT: mode {:?} reached", self.mode());
self.send_event(mgt::Event::ModeChanged(self.mode()));
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Ok);
}
self.report_mode_to_parent();
}
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
}
self.mode_leaf_helper
.report_tx
.send(ModeResponse::SetModeTimeout)
.unwrap();
}
fn report_mode_to_parent(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
))
.unwrap();
}
fn send_event(&self, event: mgt::Event) {
if let Err(e) = self.event_tx.send(event) {
log::warn!("MGT: failed to send event {:?}: {}", event, e);
}
}
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("MGT: failed to send TM packet: {}", e);
}
}
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use arbitrary_int::u11;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message as _, TcHeader,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::device_fdir::RECOVERY_THRESHOLD;
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
impl TestInterface {
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
self.sent_frames
.iter()
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
.collect()
}
fn push_reply(&mut self, reply: sim_mgt::Reply) {
self.replies.push_back(reply.to_frame());
}
}
struct MgtTestbench {
parent_request_tx: mpsc::SyncSender<ModeRequest>,
parent_report_rx: mpsc::Receiver<ModeResponse>,
shared_switch_set: SharedSwitchSet,
switch_rx: mpsc::Receiver<SwitchRequest>,
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgt::Event>,
health_table: HealthTableMapSync,
handler: MgtHandler,
}
impl MgtTestbench {
fn new() -> Self {
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let (event_tx, event_rx) = mpsc::sync_channel(20);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
let health_table = HealthTableMapSync::default();
let mut handler = MgtHandler::new(
TmtcQueues { tc_rx, tm_tx },
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
MgtCommunication::Test(TestInterface::default()),
ModeLeafHelper {
request_rx,
report_tx,
},
Duration::from_millis(100),
health_table.clone(),
event_tx,
);
handler.fdir.recovery_off_duration = Duration::ZERO;
Self {
parent_request_tx,
parent_report_rx,
shared_switch_set,
switch_rx,
tc_tx,
tm_rx,
event_rx,
health_table,
handler,
}
}
fn send_tc(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
request,
))
.unwrap();
}
fn next_response(&self) -> Response {
let tm = self.tm_rx.try_recv().expect("no TM generated");
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
}
/// Completes the power switch handshake for a commanded switch-on.
fn switch_to_normal(&mut self) {
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
assert_eq!(self.next_response(), Response::Ok);
}
fn test_interface(&mut self) -> &mut TestInterface {
match &mut self.handler.com {
MgtCommunication::Test(test) => test,
_ => panic!("unexpected MGT interface"),
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, state);
}
fn health(&self) -> Option<HealthState> {
self.health_table.health(ComponentId::AcsMgt.into())
}
fn drain_events(&self) -> Vec<mgt::Event> {
self.event_rx.try_iter().collect()
}
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
/// reached normal mode already registered the first fault.
fn exceed_reply_fault_threshold(&mut self) {
for _ in 0..REPLY_FAULT_THRESHOLD {
self.handler.periodic_operation();
}
}
/// Drives a started power cycle recovery to completion, completing both power switch
/// handshakes.
fn complete_power_cycle(&mut self) {
self.handler.periodic_operation();
self.set_switch_state(SwitchState::Off);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Off);
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
}
}
#[test]
fn test_initial_state_no_polling() {
let mut testbench = MgtTestbench::new();
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_switch_to_normal() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
testbench.handler.periodic_operation();
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.event_rx.try_recv(),
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
}
#[test]
fn test_mode_command_from_parent() {
let mut testbench = MgtTestbench::new();
testbench
.parent_request_tx
.send(ModeRequest::SetMode(DeviceMode::Normal))
.unwrap();
testbench.handler.periodic_operation();
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
// Commanded by the parent, so no TC response is expected.
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_torque_command_rejected_when_off() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_torque_command_forwarded_in_normal_mode() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.test_interface().sent_frames.clear();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(
testbench.test_interface().sent_requests().first(),
Some(&sim_mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
})
);
}
#[test]
fn test_torque_command_without_ack() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
}
#[test]
fn test_hk_polling_updates_hk_set() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert!(
testbench
.test_interface()
.sent_requests()
.contains(&sim_mgt::Request::RequestHk)
);
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(
testbench.next_response(),
Response::Hk(HkSet {
valid: true,
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
})
);
}
#[test]
fn test_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.handler.periodic_operation();
let switch_request = testbench
.switch_rx
.try_iter()
.last()
.expect("no switch request");
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
testbench.set_switch_state(SwitchState::Off);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Off)]
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Off))
);
}
#[test]
fn test_hk_set_invalid_after_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
}
#[test]
fn test_periodic_hk() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
Duration::ZERO,
))));
testbench.handler.periodic_operation();
assert!(matches!(testbench.next_response(), Response::Hk(_)));
let tm = testbench.tm_rx.try_recv();
assert!(tm.is_err(), "only one HK packet per cycle expected");
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
testbench.handler.periodic_operation();
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_valid_reply_no_fault() {
let mut testbench = MgtTestbench::new();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 0);
assert!(testbench.handler.hk_set.valid);
}
#[test]
fn test_missing_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(!testbench.handler.hk_set.valid);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_unexpected_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_invalid_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().replies.push_back(vec![0xff]);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_reply_fault_threshold_starts_recovery() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::Started)
]
));
testbench.complete_power_cycle();
assert_eq!(testbench.health(), Some(HealthState::Healthy));
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::Recovery(RecoveryEvent::Done)]
));
// The power cycle is hidden from the parent.
assert!(testbench.parent_report_rx.try_recv().is_err());
}
#[test]
fn test_unresponsive_device_marked_faulty() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
for _ in 0..RECOVERY_THRESHOLD {
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
testbench.complete_power_cycle();
}
testbench.drain_events();
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::Faulty));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
]
));
assert_eq!(
testbench.handler.switch_and_mode_helper.target(),
Some(DeviceMode::Off)
);
}
#[test]
fn test_set_health() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
HealthState::Faulty,
)));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(testbench.health(), Some(HealthState::Faulty));
}
}
-5
View File
@@ -1,5 +0,0 @@
pub mod ctrl;
pub mod mgm;
pub mod mgm_assembly;
pub mod mgt;
pub mod subsystem;
-406
View File
@@ -1,406 +0,0 @@
#![allow(dead_code)]
use std::{
sync::mpsc::{self, Receiver, SyncSender},
time::Duration,
};
use example_std::{ModeHelper, TmtcQueues};
use satrs::{
mode_tree::{
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
SequenceTableMapTable, SequenceTablesMapValue, TargetModeTables,
},
spacepackets::CcsdsPacketIdAndPsc,
subsystem::{
ModeCommandingResult, ModeRaw, ModeResponse, ModeSetRequest, ModeTreeHelperError,
SubsystemCommandingHelper, SubsystemHelperResult,
},
};
use types::{
ComponentId,
acs::subsystem::{Mode, response},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
fn build_sequence_tables() -> SequenceModeTables {
let mut seq_tables = SequenceModeTables::default();
let mut off_table = SequenceTablesMapValue::new("OFF");
let mut off_step_0 = SequenceTableMapTable::new("OFF_STEP_0");
off_step_0.add_entry(SequenceTableEntry::new(
"OFF_CTRL_PASSIVE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
true,
));
off_table.add_sequence_table(off_step_0);
let mut off_step_1 = SequenceTableMapTable::new("OFF_STEP_1");
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGM_ASSY_OFF",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off).into(),
false,
));
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGT_OFF",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
false,
));
off_table.add_sequence_table(off_step_1);
seq_tables.0.insert(Mode::Off as ModeRaw, off_table);
let mut safe_table = SequenceTablesMapValue::new("SAFE");
let mut safe_step_0 = SequenceTableMapTable::new("SAFE_STEP_0");
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGM_ASSY_NORMAL",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal).into(),
false,
));
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGT_NORMAL",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Normal.into(),
false,
));
safe_table.add_sequence_table(safe_step_0);
let mut safe_step_1 = SequenceTableMapTable::new("SAFE_STEP_1");
safe_step_1.add_entry(SequenceTableEntry::new(
"SAFE_CTRL_SAFE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Safe.into(),
false,
));
safe_table.add_sequence_table(safe_step_1);
seq_tables.0.insert(Mode::Safe as ModeRaw, safe_table);
seq_tables
}
fn ctrl_response_to_mode_response(
response: types::acs::ctrl::response::ModeReport,
) -> ModeResponse {
let sender_id = ComponentId::AcsController as satrs::ComponentId;
match response {
types::acs::ctrl::response::ModeReport::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::ctrl::response::ModeReport::WrongMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgm_assy_response_to_mode_response(
response: types::acs::mgm_assembly::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgmAssembly as satrs::ComponentId;
match response {
types::acs::mgm_assembly::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgm_assembly::response::ModeResponse::SetModeTimeout(_)
| types::acs::mgm_assembly::response::ModeResponse::WrongMode(_)
| types::acs::mgm_assembly::response::ModeResponse::CanNotKeepMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgt_response_to_mode_response(
response: types::acs::mgt::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
match response {
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
#[derive(Debug)]
pub struct ModeRequestSenders {
pub mode_request_ctrl: SyncSender<types::acs::ctrl::request::ModeRequest>,
pub mode_request_mgm_assy: SyncSender<types::acs::mgm_assembly::request::ModeRequest>,
pub mode_request_mgt: SyncSender<types::acs::mgt::request::ModeRequest>,
}
#[derive(Debug)]
pub struct ModeReportReceivers {
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
}
#[derive(Debug)]
pub struct Subsystem {
mode_helper: ModeHelper<types::acs::subsystem::Mode, TransitionState>,
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
subsystem_helper: SubsystemCommandingHelper,
}
impl Subsystem {
pub const ID: ComponentId = ComponentId::AcsSubsystem;
pub fn new(
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
) -> Self {
let mut mode_store_vec = ModeStoreVec::default();
mode_store_vec
.add_component(
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::NoModeKeeping.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
)
.unwrap();
let target_tables = TargetModeTables::default();
let sequence_tables = build_sequence_tables();
Self {
mode_helper: ModeHelper::new(
types::acs::subsystem::Mode::Off,
Duration::from_millis(2000),
),
mode_request_senders,
mode_report_receivers,
tmtc_queues,
subsystem_helper: SubsystemCommandingHelper::new(
mode_store_vec,
target_tables,
sequence_tables,
),
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
let mut mode_requests = Vec::new();
while let Ok(response) = self.mode_report_receivers.mode_response_ctrl.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(ctrl_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgm_assy.try_recv() {
let result = self.subsystem_helper.state_machine(
Some(mgm_assy_response_to_mode_response(response)),
|request| mode_requests.push(request),
);
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgt.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(mgt_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
let result = self
.subsystem_helper
.state_machine(None, |request| mode_requests.push(request));
self.handle_state_machine_result(result);
for request in mode_requests {
self.handle_mode_set_request(request);
}
}
fn handle_state_machine_result(
&mut self,
result: Result<SubsystemHelperResult, ModeTreeHelperError>,
) {
match result {
Ok(result) => match result {
SubsystemHelperResult::Idle => (),
SubsystemHelperResult::TargetKeeping => (),
SubsystemHelperResult::ModeCommanding(mode_commanding_result) => {
match mode_commanding_result {
ModeCommandingResult::Done => {
let target_mode_typed =
self.subsystem_helper.seq_exec_helper.target_mode();
let target_mode = target_mode_typed.map(Mode::try_from);
log::info!(
"ACS SS: mode commanding for target mode {:?} done",
target_mode
);
self.finish_mode_transition();
}
ModeCommandingResult::StepDone => log::info!("mode commanding step done"),
ModeCommandingResult::AwaitingSuccessCheck => {
log::info!("ACS SS: mode commanding awaiting success check")
}
}
}
},
Err(e) => {
log::error!("mode tree helper error: {}", e);
self.mode_helper.finish(false);
}
}
}
fn finish_mode_transition(&mut self) {
let tc_commander = self.mode_helper.finish(true);
if let Some(requestor) = tc_commander {
self.send_telemetry(
Some(requestor),
response::Response::Mode(response::ModeResponse::Mode(self.mode_helper.current)),
);
}
}
fn handle_mode_set_request(&mut self, request: ModeSetRequest) {
let target_id = ComponentId::try_from(request.target_id);
if let Ok(target_id) = target_id {
match target_id {
ComponentId::AcsMgmAssembly => self
.mode_request_senders
.mode_request_mgm_assy
.send(types::acs::mgm_assembly::request::ModeRequest::SetMode(
types::acs::mgm_assembly::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsController => self
.mode_request_senders
.mode_request_ctrl
.send(types::acs::ctrl::request::ModeRequest::SetMode(
types::acs::ctrl::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsMgt => self
.mode_request_senders
.mode_request_mgt
.send(types::acs::mgt::request::ModeRequest::SetMode(
types::DeviceMode::try_from(request.mode).unwrap(),
))
.unwrap(),
_ => {
log::error!("invalid target ID {:?} for mode command", target_id);
}
}
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::subsystem::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::subsystem::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::subsystem::request::Request::Mode(mode_request) => {
self.handle_mode_request(Some(tc_id), mode_request);
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn handle_mode_request(
&mut self,
tc_id: Option<CcsdsPacketIdAndPsc>,
mode_request: types::acs::subsystem::request::ModeRequest,
) {
match mode_request {
types::acs::subsystem::request::ModeRequest::SetMode(target_mode) => {
self.mode_helper.start(target_mode);
self.mode_helper.tc_commander = tc_id;
if let Err(e) = self
.subsystem_helper
.start_command_sequence(target_mode as ModeRaw)
{
log::error!("error starting command sequence: {}", e);
}
}
types::acs::subsystem::request::ModeRequest::ReadMode => {
self.send_telemetry(
None,
response::Response::Mode(response::ModeResponse::Mode(
self.mode_helper.current,
)),
);
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::subsystem::response::Response,
) {
match crate::ccsds::pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
-34
View File
@@ -1,34 +0,0 @@
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketIdAndPsc, SpHeader,
time::{StdTimestampError, cds::CdsTime},
};
use serde::Serialize;
use types::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
#[derive(Debug, thiserror::Error)]
pub enum CcsdsTmCreationError {
#[error("postcard error: {0}")]
Postcard(#[from] postcard::Error),
#[error("timestamp error: {0}")]
Time(#[from] StdTimestampError),
}
pub fn pack_ccsds_tm_packet_for_now(
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl Serialize + Message),
) -> Result<CcsdsTmPacketOwned, CcsdsTmCreationError> {
let now = CdsTime::now_with_u16_days()?;
let sp_header = SpHeader::new_from_apid(u11::new(Apid::Tmtc as u16));
let tm_header = TmHeader::new(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
&now,
);
Ok(CcsdsTmPacketOwned::new_with_serde_payload(
sp_header, &tm_header, payload,
)?)
}
-88
View File
@@ -1,88 +0,0 @@
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct Controller {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
}
impl Controller {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
) -> Self {
Self {
tc_rx,
tm_tx,
event_ctrl_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
control::request::Request::Ping => self
.send_telemetry(Some(tc_id), control::response::Response::Ok),
control::request::Request::TestEvent => {
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
self.send_telemetry(
Some(tc_id),
control::response::Response::Ok,
);
}
}
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: control::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
-200
View File
@@ -1,200 +0,0 @@
use std::collections::VecDeque;
use std::time::Duration;
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use satrs::health::{HealthState, HealthTableMapSync};
use types::{ComponentId, DeviceMode};
use crate::device_mode::SwitchAndModeHelper;
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
// before the counter is decremented again.
pub const RECOVERY_THRESHOLD: u32 = 2;
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
/// Generic FDIR events. The device handler maps them to its own event type.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FdirEvent {
FaultThresholdExceeded,
Recovery(RecoveryEvent),
}
/// Fault counting and power cycle recovery for device handlers which own the power switch of
/// their device.
///
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
/// events with [Self::next_event].
pub struct DeviceFdir {
name: &'static str,
fault_counter: FaultCounterStd,
recovery: RecoveryFdir<HealthTableMapSync>,
pub recovery_off_duration: Duration,
events: VecDeque<FdirEvent>,
}
impl DeviceFdir {
pub fn new(
name: &'static str,
component_id: ComponentId,
health_table: HealthTableMapSync,
fault_counter: FaultCounterStd,
) -> Self {
Self {
name,
fault_counter,
recovery: RecoveryFdir::new(
component_id.into(),
health_table,
RECOVERY_THRESHOLD,
RECOVERY_DECREMENT_AFTER,
),
recovery_off_duration: RECOVERY_OFF_DURATION,
events: VecDeque::new(),
}
}
#[cfg(test)]
pub fn fault_count(&self) -> u32 {
self.fault_counter.fault_count()
}
pub fn set_health(&mut self, health: HealthState) {
self.recovery.set_health(health);
}
pub fn next_event(&mut self) -> Option<FdirEvent> {
self.events.pop_front()
}
/// Should be called once per cycle, before the mode transition is handled. Starts a power
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
self.recovery.periodic_operation();
self.check_needs_recovery(modes);
}
pub fn register_success(&mut self) {
self.fault_counter.try_decrement();
}
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
/// too often, the component is marked faulty and commanded off instead.
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if !self.fault_counter.increment_and_check() {
return;
}
match self.recovery.handle_fault() {
FaultResponse::Ignored => {
log::info!(
"{}: fault threshold exceeded, but component is already faulty, \
recovering or externally controlled",
self.name
);
}
FaultResponse::Recover => {
log::warn!(
"{}: fault threshold exceeded, power cycling device",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.check_needs_recovery(modes);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: fault threshold exceeded after too many recoveries, marking \
component faulty",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
}
}
/// Mode commands from ground or the parent abort a running recovery. Must be called before
/// the commanded transition is started.
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() {
log::warn!("{}: mode command aborts power cycle recovery", self.name);
// Otherwise, the recovery would restart right away.
self.recovery.recovery_done();
}
}
pub fn handle_power_cycle_done(&mut self) {
log::info!("{}: power cycle recovery done", self.name);
// Faults registered while the device was switched off do not count anymore.
self.fault_counter.clear();
self.recovery.recovery_done();
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
}
/// A failed power cycle costs a recovery attempt like any other fault.
pub fn handle_power_cycle_failed(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
match self.recovery.recovery_failed() {
FaultResponse::Recover => {
log::warn!("{}: power cycle recovery failed, retrying", self.name);
self.start_recovery(modes, restore_mode);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: power cycle recovery failed too often, marking component faulty",
self.name
);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
// Ground changed the health during the recovery and is in charge now.
FaultResponse::Ignored => (),
}
}
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
{
return;
}
if modes.mode() == DeviceMode::Off {
// Nothing to power cycle, the next switch-on is a fresh start anyway.
log::info!("{}: device is off, no recovery required", self.name);
self.recovery.recovery_done();
return;
}
let restore_mode = modes.mode();
self.start_recovery(modes, restore_mode);
}
fn start_recovery(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
log::warn!("{}: starting power cycle recovery", self.name);
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
}
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
// Do not restart an already pending Off transition, which would reset the transition
// state machine before it can finish.
if modes.target() != Some(DeviceMode::Off) {
log::warn!("{}: commanding device off due to fault", self.name);
modes.start_transition(DeviceMode::Off, None);
}
}
}
-504
View File
@@ -1,504 +0,0 @@
use std::time::{Duration, Instant};
use types::pcdu::SwitchId;
use crate::eps::PowerSwitchHelper;
/// This is a helper trait required to make [SwitchAndModeHelper] generic.
///
/// It allows distinguish a powered-off state from one or more powered-on states, so
/// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode.
pub trait PowerSwitchedMode: Copy + PartialEq {
const OFF: Self;
fn requires_power(&self) -> bool;
}
impl PowerSwitchedMode for types::DeviceMode {
const OFF: Self = types::DeviceMode::Off;
fn requires_power(&self) -> bool {
*self != types::DeviceMode::Off
}
}
#[derive(Default, Debug, PartialEq, Eq)]
enum SwitchTransitionState {
#[default]
Idle,
PowerSwitching,
Done,
}
/// Outcome of a single power switch transition.
enum SwitchOutcome {
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
}
/// Dedicated states for power cycling a device.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PowerCycleState<Mode> {
Idle,
SwitchingOff { restore_mode: Mode },
WaitingOff { restore_mode: Mode, since: Instant },
SwitchingOn { restore_mode: Mode },
}
/// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`]
/// has driven it to completion. Carries back whichever TC commanded the transition, if any, so
/// the caller can reply to it -- what that reply looks like is handler-specific, so this stays
/// out of the helper.
pub enum ModeTransitionEvent<Mode> {
/// The target mode was reached.
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The target mode could not be reached.
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The power cycle completed and the mode before the power cycle was restored.
PowerCycleDone,
/// Power switching failed during the power cycle. The power cycle is not hidden anymore,
/// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. `restore_mode` is
/// the mode the power cycle should have restored, which can be used to retry it.
PowerCycleFailed { restore_mode: Mode },
}
/// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done)
/// shared by every device handler that owns a single power switch of its own.
///
/// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
/// transition finishes (or fails) and leaves what to do about it to the caller.
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
power_cycle_state: PowerCycleState<Mode>,
power_cycle_off_duration: Duration,
}
impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
pub fn new(
init_mode: Mode,
timeout: Duration,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
) -> Self {
Self {
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
switch_helper,
switch_id,
power_cycle_state: PowerCycleState::Idle,
power_cycle_off_duration: Duration::ZERO,
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
pub fn target(&self) -> Option<Mode> {
self.mode_helper.target
}
/// Mode which should be reported to other components. A power cycle is hidden from them,
/// so this is the mode which is restored after the power cycle while one is active.
pub fn reported_mode(&self) -> Mode {
match self.power_cycle_state {
PowerCycleState::SwitchingOff { restore_mode }
| PowerCycleState::WaitingOff { restore_mode, .. }
| PowerCycleState::SwitchingOn { restore_mode } => restore_mode,
PowerCycleState::Idle => self.mode(),
}
}
#[inline]
pub fn power_cycle_active(&self) -> bool {
self.power_cycle_state != PowerCycleState::Idle
}
/// Starts a new transition, aborting a running power cycle.
pub fn start_transition(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.power_cycle_state = PowerCycleState::Idle;
self.start_transition_internal(target_mode, tc_commander);
}
/// Switches the device off, keeps it off for `off_duration` and then switches it to
/// `restore_mode`. Reaching the intermediate off mode does not generate an event.
pub fn start_power_cycle(&mut self, restore_mode: Mode, off_duration: Duration) {
self.power_cycle_state = PowerCycleState::SwitchingOff { restore_mode };
self.power_cycle_off_duration = off_duration;
self.start_transition_internal(Mode::OFF, None);
}
fn start_transition_internal(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.mode_helper.tc_commander = tc_commander;
self.mode_helper.start(target_mode);
}
/// This is the main API that the periodic handler of a device handler should call.
///
/// It handles the switch commanding and returns relevant events.
pub fn handle_mode_transition(&mut self) -> Option<ModeTransitionEvent<Mode>> {
// The most probable case: Nothing to do.
if self.target().is_none() && !self.power_cycle_active() {
return None;
}
// Handle this as an extra step so the switch transition after this can proceed.
self.handle_waiting_for_off_when_power_cycling();
// Core logic: Command the switches, check whether target switch state was reached.
// Note the ?: if a switch transition is on-going, we might do an early return.
let outcome = self.handle_switch_transition()?;
// Regular mode transition without power cycling.
if self.power_cycle_state == PowerCycleState::Idle {
return Some(match outcome {
SwitchOutcome::Reached(tc_commander) => ModeTransitionEvent::Reached(tc_commander),
SwitchOutcome::Failed(tc_commander) => ModeTransitionEvent::Failed(tc_commander),
});
}
// Power cycling, where a bit more logic is required.
// Handle the error case first.
if let SwitchOutcome::Failed(_) = outcome {
let restore_mode = self.reported_mode();
self.power_cycle_state = PowerCycleState::Idle;
return Some(ModeTransitionEvent::PowerCycleFailed { restore_mode });
}
// At this point: The switching was succesfull, so we only match on the
// power cycle state.
match self.power_cycle_state {
// No switching going on for thse cases.
PowerCycleState::Idle | PowerCycleState::WaitingOff { .. } => None,
PowerCycleState::SwitchingOff { restore_mode } => {
self.power_cycle_state = PowerCycleState::WaitingOff {
restore_mode,
since: Instant::now(),
};
None
}
PowerCycleState::SwitchingOn { .. } => {
// Power is back and we are done.
self.power_cycle_state = PowerCycleState::Idle;
Some(ModeTransitionEvent::PowerCycleDone)
}
}
}
fn handle_waiting_for_off_when_power_cycling(&mut self) {
if let PowerCycleState::WaitingOff {
restore_mode,
since,
} = self.power_cycle_state
&& since.elapsed() >= self.power_cycle_off_duration
{
self.power_cycle_state = PowerCycleState::SwitchingOn { restore_mode };
self.start_transition_internal(restore_mode, None);
}
}
fn handle_switch_transition(&mut self) -> Option<SwitchOutcome> {
let target_mode = self.mode_helper.target?;
let switch_target_on = target_mode.requires_power();
if self.mode_helper.transition_state == SwitchTransitionState::Idle {
let result = if switch_target_on {
self.switch_helper.send_switch_on_cmd(self.switch_id)
} else {
self.switch_helper.send_switch_off_cmd(self.switch_id)
};
if result.is_err() {
// Could not send switch command.. still continue with transition.
log::error!(
"failed to send switch {} command",
if switch_target_on { "on" } else { "off" }
);
}
self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching;
}
if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching {
if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on {
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
self.mode_helper.transition_state = SwitchTransitionState::Done;
} else if self.mode_helper.timed_out() {
return Some(SwitchOutcome::Failed(self.mode_helper.finish(false)));
}
}
if self.mode_helper.transition_state == SwitchTransitionState::Done {
return Some(SwitchOutcome::Reached(self.mode_helper.finish(true)));
}
None
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex, mpsc};
use arbitrary_int::u11;
use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use types::{
DeviceMode,
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
const TIMEOUT: Duration = Duration::from_millis(50);
struct Testbench {
helper: SwitchAndModeHelper<DeviceMode>,
switch_rx: mpsc::Receiver<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
impl Testbench {
fn new() -> Self {
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgm0, SwitchState::Off);
let shared_switch_set: SharedSwitchSet =
Arc::new(Mutex::new(SwitchSet::new(switch_map)));
Self {
helper: SwitchAndModeHelper::new(
DeviceMode::Off,
TIMEOUT,
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
SwitchId::Mgm0,
),
switch_rx,
shared_switch_set,
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgm0, state);
}
fn switch_requests(&self) -> Vec<SwitchStateBinary> {
self.switch_rx
.try_iter()
.map(|req| req.target_state)
.collect()
}
/// Drives a transition to `Normal` to completion.
fn switch_to_normal(&mut self) {
self.helper.start_transition(DeviceMode::Normal, None);
self.set_switch_state(SwitchState::On);
assert!(matches!(
self.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
self.switch_requests();
}
/// Starts a power cycle from `Normal` and drives it until the device is off.
fn power_cycle_until_off(&mut self, off_duration: Duration) {
self.switch_to_normal();
self.helper
.start_power_cycle(DeviceMode::Normal, off_duration);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]);
self.set_switch_state(SwitchState::Off);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.helper.mode(), DeviceMode::Off);
}
}
fn tc_id() -> CcsdsPacketIdAndPsc {
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1)))
}
#[test]
fn test_no_transition() {
let mut tb = Testbench::new();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.handle_mode_transition().is_none());
assert!(tb.switch_requests().is_empty());
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_switch_on() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode reached event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert_eq!(tb.helper.target(), None);
assert!(tb.switch_requests().is_empty());
}
#[test]
fn test_switch_off() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper.start_transition(DeviceMode::Off, None);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]);
tb.set_switch_state(SwitchState::Off);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
}
#[test]
fn test_switch_already_in_target_state() {
let mut tb = Testbench::new();
tb.set_switch_state(SwitchState::On);
tb.helper.start_transition(DeviceMode::On, None);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
// The switch command is still sent.
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
#[test]
fn test_switch_timeout() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode failed event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
}
#[test]
fn test_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.power_cycle_active());
// The off duration elapsed, so switching on starts right away.
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleDone)
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_reports_restored_mode() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::from_secs(60));
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
tb.helper.handle_mode_transition();
tb.set_switch_state(SwitchState::Off);
tb.helper.handle_mode_transition();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_reports_restored_mode_while_switching_on() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_waits_off_duration() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
for _ in 0..3 {
assert!(tb.helper.handle_mode_transition().is_none());
}
assert!(tb.switch_requests().is_empty());
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_off_timeout() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_on_timeout() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert!(!tb.helper.power_cycle_active());
// The failed power cycle is not hidden anymore.
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_transition_aborts_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
tb.helper.start_transition(DeviceMode::On, Some(tc_id()));
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
tb.set_switch_state(SwitchState::On);
// A regular transition event instead of a power cycle event.
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(Some(_)))
));
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
}
-119
View File
@@ -1,119 +0,0 @@
use derive_new::new;
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
use types::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
use satrs::{queue::GenericSendError, request::MessageMetadata};
use thiserror::Error;
use crate::eps::pcdu::SwitchMapWrapper;
use self::pcdu::SharedSwitchSet;
pub mod pcdu;
#[derive(new, Clone)]
pub struct PowerSwitchHelper {
switcher_tx: mpsc::SyncSender<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum SwitchCommandingError {
#[error("send error: {0}")]
Send(#[from] GenericSendError),
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
pub enum SwitchInfoError {
/// This is a configuration error which should not occur.
#[error("switch ID not in map")]
SwitchIdNotInMap(SwitchId),
#[error("switch set invalid")]
SwitchSetInvalid,
}
impl PowerSwitchHelper {
pub fn send_switch_on_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::On))?;
Ok(())
}
pub fn send_switch_off_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::Off))?;
Ok(())
}
pub fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, SwitchInfoError> {
let switch_set = self
.shared_switch_set
.lock()
.expect("failed to lock switch set");
if !switch_set.valid {
return Err(SwitchInfoError::SwitchSetInvalid);
}
if let Some(state) = switch_set.switch_map.get(&switch_id) {
return Ok(*state);
}
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
}
#[allow(dead_code)]
fn switch_delay_ms(&self) -> Duration {
// Here, we could set device specific switch delays theoretically. Set it to this value
// for now.
Duration::from_millis(1000)
}
pub fn is_switch_on(&self, switch_id: SwitchId) -> bool {
if let Ok(state) = self.switch_state(switch_id) {
state == SwitchState::On
} else {
false
}
}
}
#[allow(dead_code)]
#[derive(new)]
pub struct SwitchRequestInfo {
pub requestor_info: MessageMetadata,
pub switch_id: SwitchId,
pub target_state: SwitchStateBinary,
}
// Test switch helper which can be used for unittests.
#[allow(dead_code)]
pub struct TestSwitchHelper {
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
#[allow(dead_code)]
pub switch_delay_request_count: u32,
pub next_switch_delay: Duration,
pub switch_map: RefCell<SwitchMapWrapper>,
pub switch_map_valid: bool,
}
impl Default for TestSwitchHelper {
fn default() -> Self {
Self {
switch_requests: Default::default(),
switch_info_requests: Default::default(),
switch_delay_request_count: Default::default(),
next_switch_delay: Duration::from_millis(1000),
switch_map: Default::default(),
switch_map_valid: true,
}
}
}
#[allow(dead_code)]
impl TestSwitchHelper {
// Helper function which can be used to force a switch to another state for test purposes.
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
self.switch_map.get_mut().0.insert(switch, state);
}
}
-773
View File
@@ -1,773 +0,0 @@
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
sync::{Arc, Mutex, mpsc},
};
use derive_new::new;
use example_std::TimestampHelper;
use minisim_types::{
SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator as _;
use types::{
ComponentId, DeviceMode,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
SwitchStateBinary, SwitchesBitfield,
},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwitchSet {
pub valid: bool,
pub switch_map: SwitchMap,
}
impl SwitchSet {
pub fn new(switch_map: SwitchMap) -> Self {
Self {
valid: true,
switch_map,
}
}
pub fn new_with_init_switches_unknown() -> Self {
let wrapper = SwitchMapWrapper::default();
Self::new(wrapper.0)
}
pub fn as_bitfield(&self) -> Option<SwitchesBitfield> {
for entry in SwitchId::iter() {
if !self.switch_map.contains_key(&entry) {
return None;
}
}
Some(
SwitchesBitfield::builder()
.with_magnetorquer(*self.switch_map.get(&SwitchId::Mgt).unwrap() == SwitchState::On)
.with_mgm1(*self.switch_map.get(&SwitchId::Mgm1).unwrap() == SwitchState::On)
.with_mgm0(*self.switch_map.get(&SwitchId::Mgm0).unwrap() == SwitchState::On)
.build(),
)
}
#[allow(dead_code)]
pub fn set_switch_state(&mut self, switch_id: SwitchId, state: SwitchState) -> bool {
if !self.switch_map.contains_key(&switch_id) {
return false;
}
*self.switch_map.get_mut(&switch_id).unwrap() = state;
true
}
}
pub type SwitchMap = HashMap<SwitchId, SwitchState>;
pub struct SwitchMapWrapper(pub SwitchMap);
impl Default for SwitchMapWrapper {
fn default() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Unknown);
}
Self(switch_map)
}
}
impl SwitchMapWrapper {
#[allow(dead_code)]
pub fn new_with_init_switches_off() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Off);
}
Self(switch_map)
}
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
Self(
switch_map
.iter()
.map(|(key, value)| (*key, SwitchState::from(*value)))
.collect(),
)
}
}
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
pub trait SerialInterface {
type Error: core::fmt::Debug;
/// Send some data via the serial interface.
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
/// Receive all replies received on the serial interface so far. This function takes a closure
/// and call its for each received packet, passing the received packet into it.
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error>;
}
#[derive(new)]
pub struct SerialInterfaceToSim {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum SetId {
SwitcherSet = 0,
}
impl SerialInterface for SerialInterfaceToSim {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
self.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(request))
.expect("failed to send request to simulation");
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
loop {
match self.sim_reply_rx.try_recv() {
Ok(reply) => {
let reply = postcard::to_allocvec(&reply).unwrap();
f(&reply);
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("sim reply sender has disconnected");
break;
}
},
}
}
Ok(())
}
}
#[derive(Default)]
pub struct SerialInterfaceDummy {
// Need interior mutability here for both fields.
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
pub reply_deque: RefCell<VecDeque<SimReply>>,
}
impl SerialInterface for SerialInterfaceDummy {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
match pcdu_req {
PcduRequest::SwitchDevice { switch, state } => {
switch_map_mut
.insert(switch, state)
.expect("switch map capacity exceeded");
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
switch_map_mut.clone(),
)));
}
};
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.next_reply_serialized();
f(&reply);
if self.reply_queue_empty() {
break;
}
}
Ok(())
}
}
impl SerialInterfaceDummy {
fn next_reply_serialized(&self) -> Vec<u8> {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
let next_reply = reply_deque_mut.pop_front().unwrap();
postcard::to_allocvec(&next_reply).unwrap()
}
fn reply_queue_empty(&self) -> bool {
self.reply_deque.borrow().is_empty()
}
}
pub enum SerialSimInterfaceWrapper {
Dummy(SerialInterfaceDummy),
Sim(SerialInterfaceToSim),
}
impl SerialInterface for SerialSimInterfaceWrapper {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
}
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpCode {
RegularOp = 0,
PollAndRecvReplies = 1,
}
/// Example PCDU device handler.
#[allow(clippy::too_many_arguments)]
pub struct PcduHandler<ComInterface: SerialInterface> {
dev_str: &'static str,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
pub com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
mode: DeviceMode,
stamp_helper: TimestampHelper,
event_tx: mpsc::SyncSender<pcdu::Event>,
}
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
init_mode: DeviceMode,
event_tx: mpsc::SyncSender<pcdu::Event>,
) -> Self {
Self {
dev_str: "PCDU",
tc_rx,
switch_request_rx,
tm_tx,
com_interface,
shared_switch_map,
stamp_helper: TimestampHelper::default(),
// Start in normal mode by default. Assume that the PCDU itself is on by default.
mode: init_mode,
event_tx,
}
}
pub fn periodic_operation(&mut self, op_code: OpCode) {
match op_code {
OpCode::RegularOp => {
self.stamp_helper.update_from_now();
// Handle requests.
self.handle_telecommands();
self.handle_switch_requests();
// Poll the switch states and/or telemetry regularly here.
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
self.handle_periodic_commands();
}
}
OpCode::PollAndRecvReplies => {
self.poll_and_handle_replies();
}
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.mode
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<pcdu::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
pcdu::request::Request::Ping => {
self.send_tm(Some(tc_id), pcdu::response::Response::Ok)
}
pcdu::request::Request::GetSwitches => self.send_tm(
Some(tc_id),
pcdu::response::Response::Switches(
self.shared_switch_map
.lock()
.unwrap()
.as_bitfield()
.expect("could not build switches response"),
),
),
pcdu::request::Request::EnableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::On,
);
}
pcdu::request::Request::DisableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::Off,
);
}
pcdu::request::Request::Mode(device_mode) => {
self.switch_mode(tc_id, device_mode)
}
}
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_switches_bitfield_request(
&mut self,
switches: SwitchesBitfield,
state: SwitchStateBinary,
) {
if switches.mgm0() {
self.handle_device_switching(SwitchId::Mgm0, state);
}
if switches.mgm1() {
self.handle_device_switching(SwitchId::Mgm1, state);
}
if switches.magnetorquer() {
self.handle_device_switching(SwitchId::Mgt, state);
}
}
pub fn send_tm(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: pcdu::response::Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
fn switch_mode(&mut self, requestor: CcsdsPacketIdAndPsc, mode: DeviceMode) {
log::info!("{}: transitioning to mode {:?}", self.dev_str, mode);
self.mode = mode;
if self.mode() == DeviceMode::Off {
self.shared_switch_map.lock().unwrap().valid = false;
}
log::info!("{} announcing mode: {:?}", self.dev_str, self.mode);
self.send_telemetry(Some(requestor), pcdu::response::Response::Ok);
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: pcdu::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_periodic_commands(&self) {
let pcdu_req = PcduRequest::RequestSwitchInfo;
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
log::warn!("polling PCDU switch info failed");
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
log::warn!("failed to send comm error event: {}", e);
}
}
}
/*
pub fn handle_mode_requests(&mut self) {
loop {
// TODO: Only allow one set mode request per cycle?
match self.mode_node.try_recv_mode_request() {
Ok(opt_msg) => {
if let Some(msg) = opt_msg {
let result = self.handle_mode_request(msg);
// TODO: Trigger event?
if result.is_err() {
log::warn!(
"{}: mode request failed with error {:?}",
self.dev_str,
result.err().unwrap()
);
}
} else {
break;
}
}
Err(e) => match e {
satrs::queue::GenericReceiveError::Empty => {
break;
}
satrs::queue::GenericReceiveError::TxDisconnected(_) => {
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
}
},
}
}
}
*/
pub fn handle_device_switching(&mut self, switch_id: SwitchId, state: SwitchStateBinary) {
let pcdu_req = PcduRequest::SwitchDevice {
switch: switch_id,
state,
};
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
self.com_interface
.send(&pcdu_req_ser)
.expect("failed to send switch request to PCDU");
}
pub fn handle_switch_requests(&mut self) {
loop {
match self.switch_request_rx.try_recv() {
Ok(switch_req) => {
self.handle_device_switching(switch_req.switch_id(), switch_req.target_state());
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("switch request receiver has disconnected");
break;
}
},
};
}
}
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
return;
};
match pcdu_reply {
PcduReply::SwitchInfo(switch_info) => {
let switch_map_wrapper =
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
let mut shared_switch_map = self
.shared_switch_map
.lock()
.expect("failed to lock switch map");
shared_switch_map.switch_map = switch_map_wrapper.0;
shared_switch_map.valid = true;
}
}
}) {
log::warn!("receiving PCDU replies failed: {e:?}");
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, TcHeader,
pcdu::{SwitchMapBinary, SwitchStateBinary},
};
use super::*;
pub fn create_request_tc(
request: types::pcdu::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Eps as u16)),
TcHeader::new(ComponentId::EpsPcdu, request.message_type()),
request,
)
}
#[derive(Default)]
pub struct SerialInterfaceTest {
pub inner: SerialInterfaceDummy,
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
/// Makes the next `send` call fail, to exercise comm-error handling.
pub fail_next_send: RefCell<bool>,
}
impl SerialInterface for SerialInterfaceTest {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
if self.fail_next_send.replace(false) {
return Err(());
}
let mut send_queue_mut = self.send_queue.borrow_mut();
send_queue_mut.push_back(data.to_vec());
self.inner.send(data)
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.inner.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.inner.next_reply_serialized();
self.reply_queue.borrow_mut().push_back(reply.clone());
f(&reply);
if self.inner.reply_queue_empty() {
break;
}
}
Ok(())
}
}
#[allow(dead_code)]
pub struct PcduTestbench {
pub mode_request_tx: mpsc::SyncSender<types::pcdu::request::Request>,
pub mode_reply_rx_to_parent: mpsc::Receiver<types::pcdu::response::Response>,
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
pub switch_request_tx: mpsc::Sender<SwitchRequest>,
pub event_rx: mpsc::Receiver<pcdu::Event>,
pub handler: PcduHandler<SerialInterfaceTest>,
}
impl PcduTestbench {
pub fn new() -> Self {
let (mode_request_tx, _mode_request_rx) = mpsc::sync_channel(5);
let (_mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
let (event_tx, event_rx) = mpsc::sync_channel(5);
let shared_switch_map =
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let handler = PcduHandler::new(
tc_rx,
tm_tx.clone(),
switch_reqest_rx,
SerialInterfaceTest::default(),
shared_switch_map,
DeviceMode::Off,
event_tx,
);
Self {
mode_request_tx,
mode_reply_rx_to_parent,
tc_tx,
tm_rx,
switch_request_tx,
event_rx,
handler,
}
}
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
}
pub fn verify_switch_req_was_sent(
&self,
expected_queue_len: usize,
switch_id: SwitchId,
target_state: SwitchStateBinary,
) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(
pcdu_req,
PcduRequest::SwitchDevice {
switch: switch_id,
state: target_state
}
)
}
pub fn verify_switch_reply_received(
&self,
expected_queue_len: usize,
expected_map: SwitchMapBinary,
) {
// Check that a switch reply was read back.
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
assert_eq!(reply_received_mut.len(), expected_queue_len);
let reply_received = reply_received_mut.pop_front().unwrap();
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
assert_eq!(
sim_reply,
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
);
}
}
#[test]
fn test_periodic_command_send_failure_sends_event() {
let testbench = PcduTestbench::new();
*testbench.handler.com_interface.fail_next_send.borrow_mut() = true;
testbench.handler.handle_periodic_commands();
let event = testbench
.event_rx
.try_recv()
.expect("expected comm error event");
assert!(matches!(event, pcdu::Event::SerialCommError));
}
#[test]
fn test_basic_handler() {
let mut testbench = PcduTestbench::new();
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Handler is OFF, no changes expected.
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
}
#[test]
fn test_normal_mode() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Check correctness of mode.
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
testbench.verify_switch_info_req_was_sent(1);
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
#[test]
fn test_switch_request_handling() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
testbench
.switch_request_tx
.send(SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On))
.expect("failed to send switch request");
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
testbench.verify_switch_info_req_was_sent(1);
let mut switch_map = SwitchMapBinaryWrapper::default().0;
*switch_map
.get_mut(&SwitchId::Mgm0)
.expect("switch state setting failed") = SwitchStateBinary::On;
testbench.verify_switch_reply_received(1, switch_map);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
}
-340
View File
@@ -1,340 +0,0 @@
use std::collections::HashSet;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, Event, EventId, Message,
acs::{mgm, mgm_assembly, mgt},
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
event_manager::{request::Request, response::Response},
pcdu, tmtc,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct EventManager {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
/// Shared by all MGM instances, which is why the sender ID is part of the message.
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
/// Shared by all TC sources, which is why the sender ID is part of the message.
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
/// Senders with all their event TM generation disabled.
disabled_components: HashSet<ComponentId>,
/// Individual (sender, event ID) pairs with their TM generation disabled.
disabled_events: HashSet<(ComponentId, u16)>,
}
impl EventManager {
#[allow(clippy::too_many_arguments)]
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
disabled_components: HashSet::new(),
disabled_events: HashSet::new(),
}
}
/// Silences all event TM generation for `sender_id`, regardless of the specific event.
fn disable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.insert(sender_id);
}
fn enable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.remove(&sender_id);
}
/// Silences TM generation for one specific event of `sender_id`, leaving its other events
/// unaffected.
fn disable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.insert((sender_id, event_id));
}
fn enable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.remove(&(sender_id, event_id));
}
fn tm_generation_enabled(&self, sender_id: ComponentId, event_id: u16) -> bool {
!self.disabled_components.contains(&sender_id)
&& !self.disabled_events.contains(&(sender_id, event_id))
}
pub fn periodic_operation(&mut self) {
// Telecommands first, so filter changes already apply to the events of this cycle.
self.handle_telecommands();
while let Ok(event) = self.ctrl_rx.try_recv() {
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
}
while let Ok((sender_id, event)) = self.mgm_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
}
while let Ok(event) = self.mgt_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgt, &event);
}
while let Ok(event) = self.pcdu_rx.try_recv() {
self.event_to_tm(ComponentId::EpsPcdu, &event);
}
while let Ok((sender_id, event)) = self.tc_source_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("failed to deserialize event manager request: {}", e);
continue;
}
};
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::EnableComponent(sender_id) => self.enable_component(sender_id),
Request::DisableComponent(sender_id) => self.disable_component(sender_id),
Request::EnableEvent {
sender_id,
event_id,
} => self.enable_event(sender_id, event_id),
Request::DisableEvent {
sender_id,
event_id,
} => self.disable_event(sender_id, event_id),
}
self.send_tm(ComponentId::EventManager, Some(tc_id), &Response::Ok);
}
}
pub fn event_to_tm(
&mut self,
sender_id: ComponentId,
event: &(impl serde::Serialize + Message + EventId + core::fmt::Debug),
) {
let tm_enabled = self.tm_generation_enabled(sender_id, event.event_id());
log::info!(
"event {:?} (ID {}) from {:?}{}",
event,
event.event_id(),
sender_id,
if tm_enabled { "" } else { ", TM disabled" }
);
if !tm_enabled {
return;
}
self.send_tm(sender_id, None, event);
}
fn send_tm(
&self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
match pack_ccsds_tm_packet_for_now(sender_id, tc_id, payload) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("error sending TM packet: {:?}", e);
}
}
Err(e) => {
log::warn!("error packing TM packet: {:?}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::{self, TryRecvError};
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{Apid, MessageType, TcHeader, acs::mgm};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
mgm_event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_manager: EventManager,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
Self {
tc_tx,
mgm_event_tx,
tm_rx,
event_manager: EventManager::new(
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
),
}
}
fn send_request(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(ComponentId::EventManager, MessageType::Event),
request,
))
.unwrap();
}
}
#[test]
fn test_events_enabled_by_default() {
let mut tb = Testbench::new();
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_component_silences_all_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disabled_component_does_not_affect_other_components() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm1);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_event_only_silences_that_specific_event() {
let mut tb = Testbench::new();
tb.event_manager.disable_event(
ComponentId::AcsMgm0,
mgm::Event::SpiFaultThresholdExceeded.event_id(),
);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected event TM");
let event: mgm::Event = postcard::from_bytes(&tm.payload).unwrap();
assert!(matches!(event, mgm::Event::ModeChanged(_)));
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_re_enabling_a_component_restores_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.event_manager.enable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disable_component_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableComponent(ComponentId::AcsMgm0));
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected request response TM");
assert_eq!(tm.tm_header.sender_id, ComponentId::EventManager);
assert!(tm.tm_header.tc_id.is_some());
let response: Response = postcard::from_bytes(&tm.payload).unwrap();
assert_eq!(response, Response::Ok);
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disable_event_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.send_request(Request::EnableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.tm_rx.try_recv().expect("expected event TM");
}
}
-158
View File
@@ -1,158 +0,0 @@
extern crate alloc;
use std::{
sync::mpsc,
time::{Duration, Instant},
};
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
pub use types::ComponentId;
use types::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
pub mod config;
/// Simple type modelling packet stored in the heap. This structure is intended to
/// be used when sending a packet via a message queue, so it also contains the sender ID.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PacketAsVec {
pub sender_id: ComponentId,
pub packet: Vec<u8>,
}
impl PacketAsVec {
pub fn new(sender_id: ComponentId, packet: Vec<u8>) -> Self {
Self { sender_id, packet }
}
}
pub struct TimestampHelper {
stamper: CdsTime,
time_stamp: [u8; 7],
}
impl TimestampHelper {
pub fn stamp(&self) -> &[u8] {
&self.time_stamp
}
pub fn update_from_now(&mut self) {
self.stamper
.update_from_now()
.expect("Updating timestamp failed");
self.stamper
.write_to_bytes(&mut self.time_stamp)
.expect("Writing timestamp failed");
}
}
impl Default for TimestampHelper {
fn default() -> Self {
Self {
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),
time_stamp: Default::default(),
}
}
}
/// Helper structure for periodic HK generation of a single set.
#[derive(Debug)]
pub struct HkHelperSingleSet {
pub enabled: bool,
pub frequency: Duration,
pub last_generated: Option<Instant>,
}
impl HkHelperSingleSet {
#[inline]
pub const fn new(enabled: bool, init_frequency: Duration) -> Self {
Self {
enabled,
frequency: init_frequency,
last_generated: None,
}
}
#[inline]
pub const fn enabled(&self) -> bool {
self.enabled
}
/// Check whether a new HK packet needs to be generated.
pub fn needs_generation(&mut self) -> bool {
if !self.enabled {
return false;
}
if self.last_generated.is_none() {
self.last_generated = Some(Instant::now());
return true;
}
let last_generated = self.last_generated.unwrap();
if Instant::now() - last_generated >= self.frequency {
self.last_generated = Some(Instant::now());
return true;
}
false
}
}
#[derive(Debug)]
pub struct TmtcQueues {
pub tc_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
#[derive(Debug)]
pub struct ModeHelper<Mode, TransitionState> {
pub current: Mode,
pub target: Option<Mode>,
pub tc_commander: Option<CcsdsPacketIdAndPsc>,
pub transition_start: Option<Instant>,
pub timeout: Duration,
pub transition_state: TransitionState,
}
impl<Mode: Copy + Clone, TransitionState: Default> ModeHelper<Mode, TransitionState> {
pub fn new(init_mode: Mode, timeout: Duration) -> Self {
Self {
current: init_mode,
target: Default::default(),
tc_commander: Default::default(),
transition_start: None,
timeout,
transition_state: Default::default(),
}
}
pub fn start(&mut self, target: Mode) {
self.target = Some(target);
self.transition_start = Some(Instant::now());
self.transition_state = TransitionState::default();
}
#[inline]
pub fn transition_active(&self) -> bool {
self.target.is_some()
}
pub fn timed_out(&self) -> bool {
if self.target.is_none() {
return false;
}
if let Some(transition_start) = self.transition_start {
return Instant::now() - transition_start >= self.timeout;
}
false
}
pub fn finish(&mut self, success: bool) -> Option<CcsdsPacketIdAndPsc> {
self.target?;
if success {
self.current = self.target.take().unwrap();
} else {
self.target = None;
}
self.transition_state = Default::default();
self.transition_start = None;
self.tc_commander.take()
}
}
-475
View File
@@ -1,475 +0,0 @@
use std::{
net::{IpAddr, SocketAddr},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
mpsc,
},
thread,
time::Duration,
};
use eps::{
PowerSwitchHelper,
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
};
use example_std::{
TmtcQueues,
config::{
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
},
};
use interface::{
sim_client_udp::create_sim_client,
tcp::{SyncTcpTmSource, TcpTask},
udp::UdpTmtcServer,
};
use log::info;
use logger::setup_logger;
use satrs::{
HandlingStatus,
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
spacepackets::time::cds::CdsTime,
};
use tmtc::sender::TmTcSender;
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
use types::{ComponentId, DeviceMode};
use crate::{
acs::{ctrl, mgm, mgm_assembly, mgt, subsystem},
controller::Controller,
eps::pcdu::SwitchSet,
event_manager::EventManager,
interface::udp::UdpTmHandlerWithChannel,
tmtc::tc_source::CcsdsDistributor,
};
mod acs;
mod ccsds;
mod controller;
mod device_fdir;
mod device_mode;
mod eps;
mod event_manager;
mod interface;
mod logger;
mod tmtc;
fn main() {
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
setup_logger().expect("setting up logging with fern failed");
println!("Runng OBSW example");
ctrlc::set_handler(move || {
log::info!("Received Ctrl-C, shutting down");
KILL_SIGNAL.store(true, Ordering::Relaxed);
})
.expect("Error setting Ctrl-C handler");
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
let (sim_request_tx, sim_request_rx) = mpsc::channel();
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
let (mgt_sim_reply_tx, mgt_sim_reply_rx) = mpsc::channel();
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
let mut opt_sim_client = create_sim_client(sim_request_rx);
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
let (event_manager_tc_tx, event_manager_tc_rx) = mpsc::sync_channel(10);
let (mgt_request_tx, mgt_request_rx) = mpsc::sync_channel(5);
let (mgt_report_tx, mgt_report_rx) = mpsc::sync_channel(5);
let (acs_ctrl_request_tx, acs_ctrl_request_rx) = mpsc::sync_channel(5);
let (acs_ctrl_response_tx, acs_ctrl_response_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and ACS subsystem.
let (mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
let (mgm_assembly_report_tx, mgm_assembly_report_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and MGM devices.
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
let mut event_manager = EventManager::new(
event_manager_tc_rx,
event_ctrl_rx,
mgm_event_rx,
mgm_assembly_event_rx,
mgt_event_rx,
pcdu_event_rx,
tc_source_event_rx,
tm_sink_tx.clone(),
);
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
let ccsds_distributor = CcsdsDistributor::default();
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor, tc_source_event_tx);
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
let udp_tm_handler = UdpTmHandlerWithChannel {
tm_rx: tm_server_rx,
};
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
let udp_tc_server = UdpTcServer::new(
ComponentId::UdpServer as u32,
sock_addr,
2048,
tc_sender.clone(),
)
.expect("creating UDP TMTC server failed");
let mut udp_tmtc_server = UdpTmtcServer {
udp_tc_server,
tm_handler: udp_tm_handler.into(),
};
let tcp_server_cfg = ServerConfig::new(
ComponentId::TcpServer as u32,
sock_addr,
Duration::from_millis(400),
4096,
8192,
);
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
let mut tcp_server = TcpTask::new(
tcp_server_cfg,
sync_tm_tcp_source.clone(),
tc_sender,
PACKET_ID_VALIDATOR.clone(),
)
.expect("tcp server creation failed");
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
let shared_switch_set = Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
// Global FDIR health table, shared by all software objects.
let health_table = satrs::health::HealthTableMapSync::default();
let shared_mgm_0_set = Arc::default();
let shared_mgm_1_set = Arc::default();
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
opt_sim_client.as_mut()
{
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_0_sim_reply_rx,
}),
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_1,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_1_sim_reply_rx,
}),
)
} else {
(
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
)
};
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_0,
TmtcQueues {
tc_rx: mgm_0_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_0_spi_interface,
shared_mgm_0_set,
mgm::ModeLeafHelper {
request_rx: mgm_0_mode_request_rx,
report_tx: mgm_0_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx.clone(),
);
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_1,
TmtcQueues {
tc_rx: mgm_1_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_1_spi_interface,
shared_mgm_1_set,
mgm::ModeLeafHelper {
request_rx: mgm_1_mode_request_rx,
report_tx: mgm_1_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx,
);
let mut mgm_assembly = mgm_assembly::Assembly::new(
mgm_assembly::ParentQueueHelper {
request_rx: mgm_assembly_request_rx,
report_tx: mgm_assembly_report_tx,
},
mgm_assembly::ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues {
tc_rx: mgm_assembly_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
Duration::from_millis(2000),
mgm_assembly_event_tx,
);
let mut acs_controller = ctrl::Controller::new(ctrl::ModeLeafHelper {
request_rx: acs_ctrl_request_rx,
report_tx: acs_ctrl_response_tx,
});
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
mgt::MgtCommunication::Sim(mgt::SimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgt_sim_reply_rx,
})
} else {
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
};
let mut mgt_handler = mgt::MgtHandler::new(
TmtcQueues {
tc_rx: mgt_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgt_com,
mgt::ModeLeafHelper {
request_rx: mgt_request_rx,
report_tx: mgt_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgt_event_tx,
);
let mut acs_subsystem = subsystem::Subsystem::new(
subsystem::ModeRequestSenders {
mode_request_ctrl: acs_ctrl_request_tx,
mode_request_mgm_assy: mgm_assembly_request_tx,
mode_request_mgt: mgt_request_tx,
},
subsystem::ModeReportReceivers {
mode_response_ctrl: acs_ctrl_response_rx,
mode_response_mgm_assy: mgm_assembly_report_rx,
mode_response_mgt: mgt_report_rx,
},
TmtcQueues {
tc_rx: acs_subsystem_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
);
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
))
} else {
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
};
let mut pcdu_handler = PcduHandler::new(
pcdu_handler_tc_rx,
tm_sink_tx.clone(),
switch_request_rx,
pcdu_serial_interface,
shared_switch_set,
DeviceMode::Normal,
pcdu_event_tx,
);
// The PCDU is a critical component which should be in normal mode immediately.
pcdu_handler_mode_tx
.send(types::pcdu::request::Request::Mode(DeviceMode::Normal))
.expect("sending initial mode request failed");
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC & UDP".to_string())
.spawn(move || {
info!("Running UDP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
udp_tmtc_server.periodic_operation();
tc_source.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
.spawn(move || {
info!("Running TCP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tcp_server.periodic_operation();
}
})
.unwrap();
info!("Starting TM funnel task");
let jh_tm_funnel = thread::Builder::new()
.name("TM SINK".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tm_sink.operation();
}
})
.unwrap();
let mut opt_jh_sim_client = None;
if let Some(mut sim_client) = opt_sim_client {
info!("Starting UDP sim client task");
opt_jh_sim_client = Some(
thread::Builder::new()
.name("SIM ADAPTER".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
if sim_client.operation() == HandlingStatus::Empty {
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
}
}
})
.unwrap(),
);
}
info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new()
.name("AOCS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
mgm_0_handler.periodic_operation();
mgm_1_handler.periodic_operation();
mgm_assembly.periodic_operation();
acs_controller.periodic_operation();
mgt_handler.periodic_operation();
acs_subsystem.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
}
})
.unwrap();
info!("Starting EPS thread");
let jh_eps = thread::Builder::new()
.name("EPS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
// TODO: We should introduce something like a fixed timeslot helper to allow a more
// declarative API. It would also be very useful for the AOCS task.
//
// TODO: The fixed timeslot handler exists.. use it.
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(300));
}
})
.unwrap();
info!("Starting controller thread");
let jh_controller_thread = thread::Builder::new()
.name("CTRL".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
controller.periodic_operation();
event_manager.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
}
})
.unwrap();
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh_tm_funnel
.join()
.expect("Joining TM Funnel thread failed");
if let Some(jh_sim_client) = opt_jh_sim_client {
jh_sim_client
.join()
.expect("Joining SIM client thread failed");
}
jh_aocs.join().expect("Joining AOCS thread failed");
jh_eps.join().expect("Joining EPS thread failed");
jh_controller_thread
.join()
.expect("Joining PUS handler thread failed");
}
pub fn update_time(time_provider: &mut CdsTime, timestamp: &mut [u8]) {
time_provider
.update_from_now()
.expect("Could not get current time");
time_provider
.write_to_bytes(timestamp)
.expect("Writing timestamp failed");
}
-55
View File
@@ -1,55 +0,0 @@
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
use satrs::{
ComponentId,
queue::GenericSendError,
tmtc::{PacketAsVec, PacketHandler},
};
#[derive(Default, Debug, Clone)]
pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum TmTcSender {
Normal(mpsc::SyncSender<PacketAsVec>),
Mock(MockSender),
}
impl TmTcSender {
#[allow(dead_code)]
pub fn get_mock_sender(&mut self) -> Option<&mut MockSender> {
match self {
TmTcSender::Mock(sender) => Some(sender),
_ => None,
}
}
}
impl PacketHandler for TmTcSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
match self {
TmTcSender::Normal(sync_sender) => {
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
}
}
TmTcSender::Mock(sender) => {
sender.handle_packet(sender_id, packet).unwrap();
}
}
Ok(())
}
}
impl PacketHandler for MockSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut mut_queue = self.0.borrow_mut();
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
Ok(())
}
}
-240
View File
@@ -1,240 +0,0 @@
use satrs::{
ComponentId as RawComponentId, HandlingStatus,
spacepackets::{CcsdsPacketReader, ChecksumType},
tmtc::PacketAsVec,
};
use std::{
collections::HashMap,
sync::mpsc::{self, TryRecvError},
};
use types::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned, tmtc};
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
// TC source components where the heap is the backing memory of the received telecommands.
pub struct TcSourceTask {
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
}
impl TcSourceTask {
pub fn new(
tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
) -> Self {
Self {
tc_receiver,
ccsds_distributor,
event_tx,
}
}
pub fn add_target(
&mut self,
target_id: ComponentId,
sender: mpsc::SyncSender<CcsdsTcPacketOwned>,
) {
self.ccsds_distributor.insert(target_id, sender);
}
pub fn periodic_operation(&mut self) {
loop {
if self.poll_tc() == HandlingStatus::Empty {
break;
}
}
}
pub fn poll_tc(&mut self) -> HandlingStatus {
match self.tc_receiver.try_recv() {
Ok(packet) => {
log::debug!("received raw packet: {:?}", packet);
let ccsds_tc_reader_result =
CcsdsPacketReader::new(&packet.packet, Some(ChecksumType::WithCrc16));
if ccsds_tc_reader_result.is_err() {
log::warn!(
"received invalid CCSDS TC packet: {:?}",
ccsds_tc_reader_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcPacket);
return HandlingStatus::HandledOne;
}
let ccsds_tc_reader = ccsds_tc_reader_result.unwrap();
let tc_header_result =
postcard::take_from_bytes::<TcHeader>(ccsds_tc_reader.user_data());
if tc_header_result.is_err() {
log::warn!(
"received CCSDS TC packet with invalid TC header: {:?}",
tc_header_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcHeader);
return HandlingStatus::HandledOne;
}
let (tc_header, payload) = tc_header_result.unwrap();
if let Some(sender) = self.ccsds_distributor.get(&tc_header.target_id) {
log::debug!("sending TC packet to target ID: {:?}", tc_header.target_id);
sender
.send(CcsdsTcPacketOwned {
sp_header: *ccsds_tc_reader.sp_header(),
tc_header,
payload: payload.to_vec(),
})
.ok();
} else {
log::warn!("no TC handler for target ID {:?}", tc_header.target_id);
self.send_event(
packet.sender_id,
tmtc::Event::UnknownTargetId(tc_header.target_id),
);
}
HandlingStatus::HandledOne
}
Err(e) => match e {
TryRecvError::Empty => HandlingStatus::Empty,
TryRecvError::Disconnected => {
log::warn!("tmtc thread: sender disconnected");
HandlingStatus::Empty
}
},
}
}
/// `sender_id` is the raw ID tagged on the received packet, which is not necessarily a
/// known [ComponentId] (e.g. a spoofed or garbled packet). Falls back to [ComponentId::Ground]
/// as the event sender in that case.
fn send_event(&self, sender_id: RawComponentId, event: tmtc::Event) {
let sender_id = ComponentId::try_from(sender_id).unwrap_or_else(|_| {
log::warn!("TC source event for unknown raw sender ID {}", sender_id);
ComponentId::Ground
});
if let Err(e) = self.event_tx.send((sender_id, event)) {
log::warn!("failed to send TC source event: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketCreatorOwned, ChecksumType, PacketType, SpacePacketHeader,
};
use types::{Apid, MessageType, ccsds::CcsdsTcPacketOwned};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<PacketAsVec>,
target_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
event_rx: mpsc::Receiver<(ComponentId, tmtc::Event)>,
tc_source: TcSourceTask,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_source_rx) = mpsc::sync_channel(5);
let (target_tx, target_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
let mut tc_source =
TcSourceTask::new(tc_source_rx, CcsdsDistributor::default(), event_tx);
tc_source.add_target(ComponentId::EpsPcdu, target_tx);
Self {
tc_tx,
target_rx,
event_rx,
tc_source,
}
}
}
fn valid_tc_raw(target_id: ComponentId) -> Vec<u8> {
CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, MessageType::Ping),
(),
)
.to_vec()
}
/// A structurally valid CCSDS packet (correct length and CRC), but with arbitrary user data
/// instead of a postcard-encoded [TcHeader].
fn raw_ccsds_tc_with_user_data(user_data: &[u8]) -> Vec<u8> {
CcsdsPacketCreatorOwned::new(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
PacketType::Tc,
user_data,
Some(ChecksumType::WithCrc16),
)
.unwrap()
.to_vec()
}
#[test]
fn test_valid_tc_is_routed_without_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::EpsPcdu),
))
.unwrap();
tb.tc_source.periodic_operation();
tb.target_rx.try_recv().expect("TC was not routed");
assert!(matches!(tb.event_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_invalid_ccsds_packet_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
vec![1, 2, 3],
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcPacket));
}
#[test]
fn test_invalid_tc_header_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
// A single byte is enough to decode the `ComponentId` discriminant, but not
// enough for the trailing `MessageType`, so `TcHeader` deserialization fails
// while the CCSDS packet itself stays valid.
raw_ccsds_tc_with_user_data(&[0]),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcHeader));
}
#[test]
fn test_unknown_target_id_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::Ground),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(
event,
tmtc::Event::UnknownTargetId(ComponentId::Ground)
));
}
}
-58
View File
@@ -1,58 +0,0 @@
use std::{
collections::HashMap,
sync::mpsc::{self},
};
use arbitrary_int::{u11, u14};
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
use types::ccsds::CcsdsTmPacketOwned;
use crate::interface::tcp::SyncTcpTmSource;
#[derive(Default)]
pub struct CcsdsSeqCounterMap {
apid_seq_counter_map: HashMap<u11, SequenceCounterCcsdsSimple>,
}
impl CcsdsSeqCounterMap {
pub fn get_and_increment(&mut self, apid: u11) -> u14 {
u14::new(
self.apid_seq_counter_map
.entry(apid)
.or_default()
.get_and_increment(),
)
}
}
pub struct TmSink {
seq_counter_map: CcsdsSeqCounterMap,
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
impl TmSink {
pub fn new(
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
seq_counter_map: Default::default(),
sync_tm_tcp_source,
tm_funnel_rx,
tm_server_tx,
}
}
pub fn operation(&mut self) {
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
tm.sp_header
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
self.tm_server_tx
.send(tm)
.expect("Sending TM to server failed");
}
}
}
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "minisim-types"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { version = "0.7", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
tai-time = { version = "1", default-features = false, features = ["serde"] }
thiserror = { version = "2", default-features = false }
types = { path = "../types" }
[dev-dependencies]
postcard = { version = "1", features = ["alloc"] }
-472
View File
@@ -1,472 +0,0 @@
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use tai_time::MonotonicTime;
use crate::{
acs::{mgm, mgt},
eps::{PcduReply, PcduRequest},
};
/// Used by clients to route replies to the component handling them.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum ComponentId {
SimCtrl,
Mgm0Lis3Mdl,
Mgm1Lis3Mdl,
Mgt,
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimRequest {
SimCtrl(SimCtrlRequest),
Mgm {
id: mgm::Id,
request: mgm::Request,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduRequest),
}
impl From<SimCtrlRequest> for SimRequest {
fn from(request: SimCtrlRequest) -> Self {
Self::SimCtrl(request)
}
}
impl From<mgt::Request> for SimRequest {
fn from(request: mgt::Request) -> Self {
Self::Mgt(request.to_frame())
}
}
impl From<PcduRequest> for SimRequest {
fn from(request: PcduRequest) -> Self {
Self::Pcdu(request)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimRequestWithTime {
pub request: SimRequest,
pub timestamp: MonotonicTime,
}
impl SimRequestWithTime {
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
Self {
request: request.into(),
timestamp,
}
}
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
Self::new(request, MonotonicTime::EPOCH)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimReply {
SimCtrl(SimCtrlReply),
Mgm {
id: mgm::Id,
reply: mgm::Reply,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduReply),
}
impl SimReply {
pub fn component(&self) -> ComponentId {
match self {
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
SimReply::Mgm { id, .. } => id.sim_component(),
SimReply::Mgt(_) => ComponentId::Mgt,
SimReply::Pcdu(_) => ComponentId::Pcdu,
}
}
}
impl From<SimCtrlReply> for SimReply {
fn from(reply: SimCtrlReply) -> Self {
Self::SimCtrl(reply)
}
}
impl From<mgt::Reply> for SimReply {
fn from(reply: mgt::Reply) -> Self {
Self::Mgt(reply.to_frame())
}
}
impl From<PcduReply> for SimReply {
fn from(reply: PcduReply) -> Self {
Self::Pcdu(reply)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlRequest {
Ping,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlReply {
Pong,
}
pub mod eps {
use super::*;
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
switch: SwitchId,
state: SwitchStateBinary,
},
RequestSwitchInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
SwitchInfo(SwitchMapBinary),
}
}
pub mod acs {
/// MGM module strongly based on the LIS3MDL device.
pub mod mgm {
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::ComponentId;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
// similar which needs to be converted to a signed float value with physical units. We will
// simplify this now and generate the signed float values directly. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reply {
pub switch_state: SwitchStateBinary,
pub sensor_values: SensorValuesMicroTesla,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: RawValues,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum Id {
Mgm0,
Mgm1,
}
impl Id {
pub const fn sim_component(&self) -> ComponentId {
match self {
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
}
}
}
impl RawValues {
pub const fn splat(value: i16) -> Self {
Self {
x: value,
y: value,
z: value,
}
}
}
}
/// Simple serial protocol of the magnetorquer.
///
/// The first byte of each frame is the packet ID. The high bit of the ID is set for replies.
/// All fields are big endian. Every command is answered with exactly one reply, but only
/// if the device is powered. The device drops invalid frames.
///
/// A data link layer is deliberately skipped for simplicity. A real serial link would need
/// framing and error detection, for example COBS encoding and a CRC. Here, the transport
/// always delivers complete and intact frames.
pub mod mgt {
use alloc::{vec, vec::Vec};
use core::time::Duration;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use types::acs::mgt::Dipole;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum RequestId {
RequestHk = 0x01,
/// Payload: dipole (3 x i16), duration in milliseconds (u32).
ApplyTorque = 0x02,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum ReplyId {
/// Payload: dipole (3 x i16), torquing flag (u8).
Hk = 0x81,
/// Reply to [RequestId::ApplyTorque].
Ack = 0x82,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FrameError {
#[error("empty frame")]
Empty,
#[error("unknown packet ID {0:#04x}")]
UnknownPacketId(u8),
#[error("invalid length {len} for packet ID {id:#04x}")]
InvalidLength { id: u8, len: usize },
}
fn split_packet_id(frame: &[u8]) -> Result<(u8, &[u8]), FrameError> {
let (&id, payload) = frame.split_first().ok_or(FrameError::Empty)?;
Ok((id, payload))
}
fn payload_array<const N: usize>(id: u8, payload: &[u8]) -> Result<&[u8; N], FrameError> {
payload.try_into().map_err(|_| FrameError::InvalidLength {
id,
len: payload.len() + 1,
})
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Request {
/// The duration has millisecond resolution on the wire.
ApplyTorque {
duration: Duration,
dipole: Dipole,
},
RequestHk,
}
impl Request {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Request::RequestHk => vec![RequestId::RequestHk.into()],
Request::ApplyTorque { duration, dipole } => {
let mut frame = vec![RequestId::ApplyTorque.into()];
frame.extend_from_slice(&dipole.to_be_bytes());
let duration_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX);
frame.extend_from_slice(&duration_ms.to_be_bytes());
frame
}
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match RequestId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
RequestId::RequestHk => {
payload_array::<0>(id, payload)?;
Ok(Request::RequestHk)
}
RequestId::ApplyTorque => {
let payload: &[u8; Dipole::LEN + 4] = payload_array(id, payload)?;
let [dipole @ .., d0, d1, d2, d3] = *payload;
Ok(Request::ApplyTorque {
duration: Duration::from_millis(
u32::from_be_bytes([d0, d1, d2, d3]).into(),
),
dipole: Dipole::from_be_bytes(&dipole),
})
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HkSet {
pub dipole: Dipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Reply {
Hk(HkSet),
Ack,
}
impl Reply {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Reply::Hk(hk) => {
let mut frame = vec![ReplyId::Hk.into()];
frame.extend_from_slice(&hk.dipole.to_be_bytes());
frame.push(hk.torquing as u8);
frame
}
Reply::Ack => vec![ReplyId::Ack.into()],
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match ReplyId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
ReplyId::Hk => {
let payload: &[u8; Dipole::LEN + 1] = payload_array(id, payload)?;
let [dipole @ .., torquing] = *payload;
Ok(Reply::Hk(HkSet {
dipole: Dipole::from_be_bytes(&dipole),
torquing: torquing != 0,
}))
}
ReplyId::Ack => {
payload_array::<0>(id, payload)?;
Ok(Reply::Ack)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_torque_frame() {
let request = Request::ApplyTorque {
duration: Duration::from_millis(0x0102_0304),
dipole: Dipole {
x: -2,
y: 0x0506,
z: 0x0708,
},
};
let frame = request.to_frame();
assert_eq!(
frame,
[
0x02, 0xff, 0xfe, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04
]
);
assert_eq!(Request::from_frame(&frame), Ok(request));
}
#[test]
fn test_request_hk_frame() {
assert_eq!(Request::RequestHk.to_frame(), [0x01]);
assert_eq!(Request::from_frame(&[0x01]), Ok(Request::RequestHk));
}
#[test]
fn test_reply_frames() {
let hk = Reply::Hk(HkSet {
dipole: Dipole { x: 1, y: 2, z: 3 },
torquing: true,
});
let frame = hk.to_frame();
assert_eq!(frame, [0x81, 0, 1, 0, 2, 0, 3, 1]);
assert_eq!(Reply::from_frame(&frame), Ok(hk));
assert_eq!(Reply::Ack.to_frame(), [0x82]);
assert_eq!(Reply::from_frame(&[0x82]), Ok(Reply::Ack));
}
#[test]
fn test_invalid_frames() {
assert_eq!(Request::from_frame(&[]), Err(FrameError::Empty));
assert_eq!(
Request::from_frame(&[0x81]),
Err(FrameError::UnknownPacketId(0x81))
);
assert_eq!(
Request::from_frame(&[0x01, 0x00]),
Err(FrameError::InvalidLength { id: 0x01, len: 2 })
);
assert_eq!(
Reply::from_frame(&[0x81, 0, 1]),
Err(FrameError::InvalidLength { id: 0x81, len: 3 })
);
}
}
}
}
pub mod udp {
pub const SIM_CTRL_PORT: u16 = 7303;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_serde_roundtrip() {
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let bytes = postcard::to_allocvec(&sim_request).unwrap();
let deserialized: SimRequestWithTime = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_request);
}
#[test]
fn test_reply_serde_roundtrip() {
let sim_reply = SimReply::from(SimCtrlReply::Pong);
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
let bytes = postcard::to_allocvec(&sim_reply).unwrap();
let deserialized: SimReply = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_reply);
}
}
-24
View File
@@ -1,24 +0,0 @@
[package]
name = "minisim"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }
postcard = { version = "1", features = ["alloc"] }
log = "0.4"
thiserror = "2"
fern = "0.7"
strum = { version = "0.28", features = ["derive"] }
num_enum = "0.7"
humantime = "2"
nexosim = "1"
satrs = { path = "../../satrs" }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
[dev-dependencies]
delegate = "0.13"
-32
View File
@@ -1,32 +0,0 @@
sat-rs minisim
======
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
[nexosim](https://github.com/asynchronics/nexosim).
Right now, this crate is primarily used together with the
[`example-std` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std)
to simulate the devices connected to the example application.
You can simply run this application using
```sh
cargo run
```
or
```sh
cargo run -p minisim
```
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
simulation replies with any other application.
The simulator was designed in a modular way to be scalable and adaptable to other communication
schemes. This might allow it to serve a mini-simulator for other example applications which
still have similar device handlers.
The following graph shows the high-level architecture of the mini-simulator.
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
-262
View File
@@ -1,262 +0,0 @@
use std::f32::consts::PI;
use minisim_types::{SimReply, acs::mgm};
use nexosim::{
model::{Context, Model},
ports::Output,
};
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::time::current_millis;
// Earth magnetic field varies between roughly -30 uT and 30 uT
const AMPLITUDE_MGM_UT: f32 = 30.0;
// Lets start with a simple frequency here.
const FREQUENCY_MGM: f32 = 1.0;
const PHASE_X: f32 = 0.0;
// Different phases to have different values on the other axes.
const PHASE_Y: f32 = 0.1;
const PHASE_Z: f32 = 0.2;
/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves.
///
/// An ideal sensor would sample the magnetic field at a high fixed rate. This might not be
/// possible for a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms)
/// might still be possible and is probably sufficient for many OBSW needs.
#[derive(Serialize, Deserialize)]
pub struct MgmModel {
id: mgm::Id,
switch_state: SwitchStateBinary,
external_mag_field: Option<mgm::SensorValuesMicroTesla>,
spi_fault: mgm::SpiFault,
pub reply: Output<SimReply>,
}
#[Model]
impl MgmModel {
pub fn new(mgm_id: mgm::Id) -> Self {
Self {
id: mgm_id,
switch_state: SwitchStateBinary::Off,
external_mag_field: None,
spi_fault: mgm::SpiFault::default(),
reply: Output::new(),
}
}
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state;
if switch_state == SwitchStateBinary::Off && self.spi_fault.cleared_by_power_cycle {
self.spi_fault = mgm::SpiFault::default();
}
}
/// Force (or clear) a stuck-bus SPI fault, for FDIR testing purposes.
pub async fn set_spi_fault(&mut self, fault: mgm::SpiFault) {
self.spi_fault = fault;
}
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
let reply = SimReply::Mgm {
id: self.id,
reply: create_reply(
self.switch_state,
self.calculate_current_mgm_tuple(current_millis(cx.time())),
self.spi_fault.mode,
),
};
self.reply.send(reply).await;
}
// Devices like magnetorquers generate a strong magnetic field which overrides the default
// model for the measured magnetic field.
pub async fn apply_external_magnetic_field(&mut self, field: mgm::SensorValuesMicroTesla) {
self.external_mag_field = Some(field);
}
pub async fn clear_external_magnetic_field(&mut self, _: ()) {
self.external_mag_field = None;
}
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> mgm::SensorValuesMicroTesla {
if SwitchStateBinary::On == self.switch_state {
if let Some(ext_field) = self.external_mag_field {
return ext_field;
}
let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0);
return mgm::SensorValuesMicroTesla {
x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
};
}
mgm::SensorValuesMicroTesla {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
/// Builds the reply of the simulated LIS3MDL, including the raw register values.
fn create_reply(
switch_state: SwitchStateBinary,
sensor_values: mgm::SensorValuesMicroTesla,
fault_mode: mgm::SpiFaultMode,
) -> mgm::Reply {
// An injected fault always wins. A switched off device reads back like an undriven bus.
let raw = match (fault_mode, switch_state) {
(mgm::SpiFaultMode::AllZeros, _) => mgm::RawValues::splat(mgm::ALL_ZEROS_SENSOR_VAL),
(mgm::SpiFaultMode::AllOnes, _) | (mgm::SpiFaultMode::None, SwitchStateBinary::Off) => {
mgm::RawValues::splat(mgm::ALL_ONES_SENSOR_VAL)
}
(mgm::SpiFaultMode::None, SwitchStateBinary::On) => {
raw_values_from_microtesla(sensor_values)
}
};
mgm::Reply {
switch_state,
sensor_values,
raw,
}
}
fn raw_values_from_microtesla(values: mgm::SensorValuesMicroTesla) -> mgm::RawValues {
let to_raw = |microtesla: f32| {
(microtesla / (mgm::GAUSS_TO_MICROTESLA_FACTOR as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16
};
mgm::RawValues {
x: to_raw(values.x),
y: to_raw(values.y),
z: to_raw(values.z),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use minisim_types::{SimReply, SimRequest, acs::mgm};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
eps::tests::{switch_device_off, switch_device_on},
test_helpers::SimTestbench,
};
fn request_sensor_data(sim_testbench: &mut SimTestbench, id: mgm::Id) -> mgm::Reply {
let sim_reply = sim_testbench
.request_reply(SimRequest::Mgm {
id,
request: mgm::Request::RequestSensorData,
})
.expect("no MGM reply received");
let SimReply::Mgm {
id: reply_id,
reply,
} = sim_reply
else {
panic!("unexpected reply {sim_reply:?}");
};
assert_eq!(reply_id, id);
reply
}
fn inject_spi_fault(sim_testbench: &mut SimTestbench, cleared_by_power_cycle: bool) {
sim_testbench.send_and_step(SimRequest::Mgm {
id: mgm::Id::Mgm0,
request: mgm::Request::SetSpiFault(mgm::SpiFault {
mode: mgm::SpiFaultMode::AllOnes,
cleared_by_power_cycle,
}),
});
}
fn is_stuck_bus_reply(reply: &mgm::Reply) -> bool {
reply.raw.x == -1 && reply.raw.y == -1 && reply.raw.z == -1
}
#[test]
fn test_basic_mgm_request() {
let mut sim_testbench = SimTestbench::new();
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(reply.switch_state, SwitchStateBinary::Off);
assert_eq!(reply.sensor_values.x, 0.0);
assert_eq!(reply.sensor_values.y, 0.0);
assert_eq!(reply.sensor_values.z, 0.0);
}
#[test]
fn test_mgm_spi_fault_injection_all_ones() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, false);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
// Even though the device is switched on, the injected fault forces a stuck-bus reply.
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
}
#[test]
fn test_mgm_spi_fault_cleared_by_power_cycle() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, true);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(is_stuck_bus_reply(&reply));
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(!is_stuck_bus_reply(&reply));
}
#[test]
fn test_mgm_spi_fault_persists_after_power_cycle() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, false);
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
}
#[test]
fn test_basic_mgm_request_switched_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let first_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
let second_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
let to_microtesla = |raw: i16| {
raw as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS * mgm::GAUSS_TO_MICROTESLA_FACTOR as f32
};
let values = second_reply.sensor_values;
let raw = second_reply.raw;
for (value, raw) in [(values.x, raw.x), (values.y, raw.y), (values.z, raw.z)] {
let diff = (value - to_microtesla(raw)).abs();
assert!(diff < 0.01, "raw value conversion diff too large: {diff}");
}
// Check that the values are changing.
assert_ne!(first_reply, second_reply);
}
#[test]
fn test_mgm_1_request_switched_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm1);
let mgm_0_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(mgm_0_reply.switch_state, SwitchStateBinary::Off);
let mgm_1_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm1);
assert_eq!(mgm_1_reply.switch_state, SwitchStateBinary::On);
}
}
-295
View File
@@ -1,295 +0,0 @@
use minisim_types::{
SimReply,
acs::{mgm, mgt},
};
use nexosim::{
model::{Context, Model, schedulable},
ports::Output,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use types::pcdu::SwitchStateBinary;
/// Time the device needs to answer a command.
const REPLY_DELAY: Duration = Duration::from_millis(15);
/// Simple magnetorquer simulation model.
#[derive(Serialize, Deserialize)]
pub struct MgtModel {
switch_state: SwitchStateBinary,
torquing: bool,
torque_dipole: mgt::Dipole,
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
pub clear_magnetic_field: Output<()>,
pub reply: Output<SimReply>,
}
#[Model]
impl MgtModel {
pub fn new() -> Self {
Self {
switch_state: SwitchStateBinary::Off,
torquing: false,
torque_dipole: mgt::Dipole::default(),
gen_magnetic_field: Output::new(),
clear_magnetic_field: Output::new(),
reply: Output::new(),
}
}
pub async fn apply_torque(
&mut self,
duration_and_dipole: (Duration, mgt::Dipole),
cx: &Context<Self>,
) {
if self.switch_state != SwitchStateBinary::On {
return;
}
self.torque_dipole = duration_and_dipole.1;
self.torquing = true;
if cx
.schedule_event(duration_and_dipole.0, schedulable!(Self::clear_torque), ())
.is_err()
{
log::warn!("torque clearing can only be set for a future time.");
}
self.generate_magnetic_field(()).await;
self.schedule_reply(mgt::Reply::Ack, cx);
}
#[nexosim(schedulable)]
async fn clear_torque(&mut self) {
self.torque_dipole = mgt::Dipole::default();
self.torquing = false;
self.clear_magnetic_field.send(()).await;
}
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state;
match switch_state {
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
SwitchStateBinary::Off => self.clear_torque().await,
}
}
pub async fn request_housekeeping_data(&mut self, _: (), cx: &Context<Self>) {
if self.switch_state != SwitchStateBinary::On {
return;
}
// The HK is sampled when the command is processed, not when the reply is sent.
let hk = mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
};
self.schedule_reply(mgt::Reply::Hk(hk), cx);
}
fn schedule_reply(&self, reply: mgt::Reply, cx: &Context<Self>) {
cx.schedule_event(REPLY_DELAY, schedulable!(Self::send_reply), reply)
.expect("scheduling MGT reply failed")
}
#[nexosim(schedulable)]
async fn send_reply(&mut self, reply: mgt::Reply) {
self.reply.send(SimReply::from(reply)).await;
}
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
// Simplified model: Just returns some fixed magnetic field for now.
// Later, we could make this more fancy by incorporating the commanded dipole.
mgm::MGT_GEN_MAGNETIC_FIELD
}
/// A torquing magnetorquer generates a magnetic field. This function can be used to apply
/// the magnetic field.
async fn generate_magnetic_field(&mut self, _: ()) {
if self.switch_state != SwitchStateBinary::On || !self.torquing {
return;
}
self.gen_magnetic_field
.send(self.calc_magnetic_field(self.torque_dipole))
.await;
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use minisim_types::{
SimReply, SimRequest, SimRequestWithTime,
acs::{mgm, mgt},
eps::PcduRequest,
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
fn decode_reply(sim_reply: SimReply) -> mgt::Reply {
let SimReply::Mgt(frame) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
mgt::Reply::from_frame(&frame).expect("invalid MGT reply frame")
}
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
let mgt::Reply::Hk(hk) = decode_reply(sim_reply) else {
panic!("unexpected MGT reply");
};
Some(hk)
}
#[test]
fn test_basic_mgt_request_is_off() {
let mut sim_testbench = SimTestbench::new();
assert!(request_hk(&mut sim_testbench).is_none());
}
#[test]
fn test_basic_mgt_request_is_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
})
);
}
#[test]
fn test_basic_mgt_request_is_on_and_torquing() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
let commanded_dipole = mgt::Dipole {
x: -200,
y: 200,
z: 1000,
};
let request = SimRequestWithTime::new_with_epoch_time(mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: commanded_dipole,
});
sim_testbench
.send_request(request)
.expect("sending MGT request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(20)).unwrap();
let ack = sim_testbench
.try_receive_next_reply()
.expect("no torque command ack");
assert_eq!(decode_reply(ack), mgt::Reply::Ack);
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: commanded_dipole,
torquing: true,
})
);
sim_testbench
.step_until(Duration::from_millis(100))
.unwrap();
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
})
);
}
#[test]
fn test_torque_command_not_acked_when_off() {
let mut sim_testbench = SimTestbench::new();
let reply = sim_testbench.request_reply(mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
});
assert!(reply.is_none());
}
#[test]
fn test_invalid_frame_is_dropped() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
assert!(
sim_testbench
.request_reply(SimRequest::Mgt(vec![0x01, 0x00]))
.is_none()
);
}
/// Processes the request without stepping, so scheduled events like the torque clearing do
/// not fire.
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
sim_testbench
.send_request(SimRequestWithTime::new_with_epoch_time(request))
.expect("sending request failed");
sim_testbench.handle_sim_requests_time_agnostic();
}
fn read_mgm_0_field(sim_testbench: &mut SimTestbench) -> mgm::SensorValuesMicroTesla {
process_without_step(
sim_testbench,
SimRequest::Mgm {
id: mgm::Id::Mgm0,
request: mgm::Request::RequestSensorData,
},
);
// Skips pending MGT replies, for example torque command acks.
loop {
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM reply received");
if let SimReply::Mgm { reply, .. } = sim_reply {
return reply.sensor_values;
}
}
}
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
switch_device_on(sim_testbench, SwitchId::Mgm0);
switch_device_on(sim_testbench, SwitchId::Mgt);
process_without_step(
sim_testbench,
mgt::Request::ApplyTorque {
duration,
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
},
);
assert_eq!(read_mgm_0_field(sim_testbench), mgm::MGT_GEN_MAGNETIC_FIELD);
}
#[test]
fn test_mgm_field_cleared_after_torquing() {
let mut sim_testbench = SimTestbench::new();
start_torquing(&mut sim_testbench, Duration::from_millis(100));
sim_testbench
.step_until(Duration::from_millis(100))
.unwrap();
assert_ne!(
read_mgm_0_field(&mut sim_testbench),
mgm::MGT_GEN_MAGNETIC_FIELD
);
}
#[test]
fn test_mgm_field_cleared_by_switching_mgt_off() {
let mut sim_testbench = SimTestbench::new();
start_torquing(&mut sim_testbench, Duration::from_millis(100));
process_without_step(
&mut sim_testbench,
PcduRequest::SwitchDevice {
switch: SwitchId::Mgt,
state: SwitchStateBinary::Off,
},
);
assert_ne!(
read_mgm_0_field(&mut sim_testbench),
mgm::MGT_GEN_MAGNETIC_FIELD
);
}
}
-309
View File
@@ -1,309 +0,0 @@
use std::{
sync::mpsc,
time::{Duration, SystemTime},
};
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
acs::{mgm, mgt},
eps::PcduRequest,
};
use nexosim::{
ports::{EventQueueReader, EventSinkReader, EventSource, SinkState, event_queue},
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
time::{Clock, Deadline, MonotonicTime, SystemClock},
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
acs::{mgm::MgmModel, mgt::MgtModel},
eps::PcduModel,
};
const WARNING_FOR_STALE_DATA: bool = false;
const SIM_CTRL_REQ_WIRETAPPING: bool = false;
const MGM_REQ_WIRETAPPING: bool = false;
const PCDU_REQ_WIRETAPPING: bool = false;
const MGT_REQ_WIRETAPPING: bool = false;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ThreadingModel {
Default = 0,
Single = 1,
}
struct MgmInputs {
send_sensor_values: EventId<()>,
set_spi_fault: EventId<mgm::SpiFault>,
}
impl MgmInputs {
fn register(sim_init: &mut SimInit, mailbox: &Mailbox<MgmModel>) -> Self {
Self {
send_sensor_values: EventSource::new()
.connect(MgmModel::send_sensor_values, mailbox)
.register(sim_init),
set_spi_fault: EventSource::new()
.connect(MgmModel::set_spi_fault, mailbox)
.register(sim_init),
}
}
}
/// Model inputs which are driven by simulation requests.
struct ModelInputs {
mgm_0: MgmInputs,
mgm_1: MgmInputs,
pcdu_request_switch_info: EventId<()>,
pcdu_switch_device: EventId<(SwitchId, SwitchStateBinary)>,
mgt_apply_torque: EventId<(Duration, mgt::Dipole)>,
mgt_request_hk: EventId<()>,
}
// The simulation controller processes requests and drives the simulation.
pub struct SimController {
sys_clock: SystemClock,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
simulation: Simulation,
inputs: ModelInputs,
model_replies: EventQueueReader<SimReply>,
}
impl SimController {
pub fn new(
threading_model: ThreadingModel,
start_time: MonotonicTime,
reply_sender: mpsc::Sender<SimReply>,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
) -> Self {
let mut mgm_0_model = MgmModel::new(mgm::Id::Mgm0);
let mut mgm_1_model = MgmModel::new(mgm::Id::Mgm1);
let mut pcdu_model = PcduModel::new();
let mut mgt_model = MgtModel::new();
let mgm_0_mailbox = Mailbox::new();
let mgm_1_mailbox = Mailbox::new();
let pcdu_mailbox = Mailbox::new();
let mgt_mailbox = Mailbox::new();
pcdu_model
.mgm_0_switch
.connect(MgmModel::switch_device, &mgm_0_mailbox);
pcdu_model
.mgm_1_switch
.connect(MgmModel::switch_device, &mgm_1_mailbox);
pcdu_model
.mgt_switch
.connect(MgtModel::switch_device, &mgt_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &mgm_1_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &mgm_1_mailbox);
let (reply_sink, model_replies) = event_queue(SinkState::Enabled);
mgm_0_model.reply.connect_sink(reply_sink.clone());
mgm_1_model.reply.connect_sink(reply_sink.clone());
pcdu_model.reply.connect_sink(reply_sink.clone());
mgt_model.reply.connect_sink(reply_sink);
let mut sim_init = if threading_model == ThreadingModel::Single {
SimInit::with_num_threads(1)
} else {
SimInit::new()
};
let inputs = ModelInputs {
mgm_0: MgmInputs::register(&mut sim_init, &mgm_0_mailbox),
mgm_1: MgmInputs::register(&mut sim_init, &mgm_1_mailbox),
pcdu_request_switch_info: EventSource::new()
.connect(PcduModel::request_switch_info, &pcdu_mailbox)
.register(&mut sim_init),
pcdu_switch_device: EventSource::new()
.connect(PcduModel::switch_device, &pcdu_mailbox)
.register(&mut sim_init),
mgt_apply_torque: EventSource::new()
.connect(MgtModel::apply_torque, &mgt_mailbox)
.register(&mut sim_init),
mgt_request_hk: EventSource::new()
.connect(MgtModel::request_housekeeping_data, &mgt_mailbox)
.register(&mut sim_init),
};
let simulation = sim_init
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
.add_model(mgt_model, mgt_mailbox, "MGT model")
.init(start_time)
.unwrap();
Self {
sys_clock: SystemClock::from_system_time(start_time, SystemTime::now()),
request_receiver,
reply_sender,
simulation,
inputs,
model_replies,
}
}
#[cfg(test)]
pub fn step(&mut self) -> Result<(), ExecutionError> {
self.simulation.step()?;
self.forward_model_replies();
Ok(())
}
pub fn step_until(&mut self, deadline: impl Deadline) -> Result<(), ExecutionError> {
self.simulation.step_until(deadline)?;
self.forward_model_replies();
Ok(())
}
fn forward_model_replies(&mut self) {
while let Some(reply) = self.model_replies.try_read() {
self.reply_sender
.send(reply)
.expect("sending model reply failed");
}
}
pub fn run(&mut self, start_time: MonotonicTime, udp_polling_interval_ms: u64) {
let mut t = start_time;
loop {
let t_old = t;
// Check for UDP requests every millisecond. Shift the simulator ahead here to prevent
// replies lying in the past.
t += Duration::from_millis(udp_polling_interval_ms);
let _synch_status = self.sys_clock.synchronize(t);
self.handle_sim_requests(t_old);
self.step_until(t).expect("simulation step failed");
}
}
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime) {
loop {
match self.request_receiver.try_recv() {
Ok(request) => {
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
log::warn!("stale data with timestamp {:?} received", request.timestamp);
}
match request.request {
SimRequest::SimCtrl(request) => self.handle_ctrl_request(request),
SimRequest::Mgm { id, request } => self.handle_mgm_request(id, request),
SimRequest::Mgt(request) => self.handle_mgt_request(request),
SimRequest::Pcdu(request) => self.handle_pcdu_request(request),
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
panic!("all request sender disconnected")
}
},
}
}
self.forward_model_replies();
}
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
if SIM_CTRL_REQ_WIRETAPPING {
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
}
match sim_ctrl_request {
SimCtrlRequest::Ping => {
log::info!("received ping request, a client is connecting");
self.reply_sender
.send(SimReply::from(SimCtrlReply::Pong))
.expect("sending reply from sim controller failed");
}
}
}
fn handle_mgm_request(&mut self, mgm_id: mgm::Id, mgm_request: mgm::Request) {
let inputs = match mgm_id {
mgm::Id::Mgm0 => &self.inputs.mgm_0,
mgm::Id::Mgm1 => &self.inputs.mgm_1,
};
if MGM_REQ_WIRETAPPING {
log::info!("received {mgm_id:?} request: {mgm_request:?}");
}
match mgm_request {
mgm::Request::RequestSensorData => {
self.simulation
.process_event(&inputs.send_sensor_values, ())
.expect("event execution error for mgm");
}
mgm::Request::SetSpiFault(fault_mode) => {
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
self.simulation
.process_event(&inputs.set_spi_fault, fault_mode)
.expect("event execution error for mgm");
}
}
}
fn handle_pcdu_request(&mut self, pcdu_request: PcduRequest) {
if PCDU_REQ_WIRETAPPING {
log::info!("received PCDU request: {pcdu_request:?}");
}
match pcdu_request {
PcduRequest::RequestSwitchInfo => {
self.simulation
.process_event(&self.inputs.pcdu_request_switch_info, ())
.unwrap();
}
PcduRequest::SwitchDevice { switch, state } => {
self.simulation
.process_event(&self.inputs.pcdu_switch_device, (switch, state))
.unwrap();
}
}
}
fn handle_mgt_request(&mut self, frame: Vec<u8>) {
let mgt_request = match mgt::Request::from_frame(&frame) {
Ok(request) => request,
Err(e) => {
log::warn!("dropping invalid MGT frame {frame:02x?}: {e}");
return;
}
};
if MGT_REQ_WIRETAPPING {
log::info!("received MGT request: {mgt_request:?}");
}
match mgt_request {
mgt::Request::ApplyTorque { duration, dipole } => self
.simulation
.process_event(&self.inputs.mgt_apply_torque, (duration, dipole))
.unwrap(),
mgt::Request::RequestHk => self
.simulation
.process_event(&self.inputs.mgt_request_hk, ())
.unwrap(),
};
}
}
#[cfg(test)]
mod tests {
use crate::test_helpers::SimTestbench;
use super::*;
#[test]
fn test_basic_ping() {
let mut sim_testbench = SimTestbench::new();
assert_eq!(
sim_testbench.request_reply(SimCtrlRequest::Ping),
Some(SimReply::SimCtrl(SimCtrlReply::Pong))
);
}
}
-162
View File
@@ -1,162 +0,0 @@
use std::time::Duration;
use minisim_types::{SimReply, eps::PcduReply};
use nexosim::{
model::{Context, Model, schedulable},
ports::Output,
};
use serde::{Deserialize, Serialize};
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
#[derive(Serialize, Deserialize)]
pub struct PcduModel {
switcher_map: SwitchMapBinary,
pub mgm_0_switch: Output<SwitchStateBinary>,
pub mgm_1_switch: Output<SwitchStateBinary>,
pub mgt_switch: Output<SwitchStateBinary>,
pub reply: Output<SimReply>,
}
#[Model]
impl PcduModel {
pub fn new() -> Self {
Self {
switcher_map: SwitchMapBinaryWrapper::default().0,
mgm_0_switch: Output::new(),
mgm_1_switch: Output::new(),
mgt_switch: Output::new(),
reply: Output::new(),
}
}
pub async fn request_switch_info(&mut self, _: (), cx: &Context<Self>) {
cx.schedule_event(
Duration::from_millis(SWITCH_INFO_DELAY_MS),
schedulable!(Self::send_switch_info),
(),
)
.expect("requesting switch info failed");
}
#[nexosim(schedulable)]
async fn send_switch_info(&mut self) {
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.clone()));
self.reply.send(reply).await;
}
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
log::info!(
"switching {:?} to {:?}",
switch_and_target_state.0,
switch_and_target_state.1
);
let val = self
.switcher_map
.get_mut(&switch_and_target_state.0)
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
*val = switch_and_target_state.1;
match switch_and_target_state.0 {
SwitchId::Mgm0 => {
self.mgm_0_switch.send(switch_and_target_state.1).await;
}
SwitchId::Mgm1 => {
self.mgm_1_switch.send(switch_and_target_state.1).await;
}
SwitchId::Mgt => {
self.mgt_switch.send(switch_and_target_state.1).await;
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::time::Duration;
use minisim_types::{SimRequestWithTime, eps::PcduRequest};
use types::pcdu::SwitchMapBinary;
use crate::test_helpers::SimTestbench;
fn switch_device(
sim_testbench: &mut SimTestbench,
switch: SwitchId,
target: SwitchStateBinary,
) {
sim_testbench.send_and_step(PcduRequest::SwitchDevice {
switch,
state: target,
});
}
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
}
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: SwitchId) {
switch_device(sim_testbench, switch, SwitchStateBinary::On);
}
pub(crate) fn get_all_off_switch_map() -> SwitchMapBinary {
SwitchMapBinaryWrapper::default().0
}
fn unwrap_switch_map(sim_reply: SimReply) -> SwitchMapBinary {
let SimReply::Pcdu(PcduReply::SwitchInfo(switch_map)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
switch_map
}
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
let sim_reply = sim_testbench
.request_reply(PcduRequest::RequestSwitchInfo)
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), *expected_switch_map);
}
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
let mut sim_testbench = SimTestbench::new();
switch_device(&mut sim_testbench, switch, target);
let mut switcher_map = get_all_off_switch_map();
*switcher_map.get_mut(&switch).unwrap() = target;
check_switch_state(&mut sim_testbench, &switcher_map);
}
#[test]
fn test_pcdu_switcher_request() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
sim_testbench
.send_request(request)
.expect("sending PCDU request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
assert!(sim_testbench.try_receive_next_reply().is_none());
// The reply is delayed by SWITCH_INFO_DELAY_MS.
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), get_all_off_switch_map());
}
#[test]
fn test_pcdu_switching_mgm_on() {
test_pcdu_switching_single_switch(SwitchId::Mgm0, SwitchStateBinary::On);
}
#[test]
fn test_pcdu_switching_mgt_on() {
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
}
#[test]
fn test_pcdu_switching_mgt_off() {
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::Off);
}
}
-63
View File
@@ -1,63 +0,0 @@
use controller::{SimController, ThreadingModel};
use minisim_types::udp::SIM_CTRL_PORT;
use nexosim::time::MonotonicTime;
use std::sync::mpsc;
use std::thread;
use udp::SimUdpServer;
mod acs;
mod controller;
mod eps;
#[cfg(test)]
mod test_helpers;
mod time;
mod udp;
fn main() {
let (request_sender, request_receiver) = mpsc::channel();
let (reply_sender, reply_receiver) = mpsc::channel();
let t0 = MonotonicTime::EPOCH;
let mut sim_ctrl =
SimController::new(ThreadingModel::Default, t0, reply_sender, request_receiver);
// Configure logger at runtime
fern::Dispatch::new()
// Perform allocation-free log formatting
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339(std::time::SystemTime::now()),
record.level(),
record.target(),
message
))
})
// Add blanket level filter -
.level(log::LevelFilter::Debug)
// - and per-module overrides
// Output to stdout, files, and other Dispatch configurations
.chain(std::io::stdout())
.chain(fern::log_file("output.log").expect("could not open log output file"))
// Apply globally
.apply()
.expect("could not apply logger configuration");
log::info!("starting simulation thread");
// This thread schedules the simulator.
let sim_thread = thread::spawn(move || {
sim_ctrl.run(t0, 1);
});
let mut udp_server =
SimUdpServer::new(SIM_CTRL_PORT, request_sender, reply_receiver, 200, None)
.expect("could not create UDP request server");
log::info!("starting UDP server on port {SIM_CTRL_PORT}");
// This thread manages the simulator UDP server.
let udp_tc_thread = thread::spawn(move || {
udp_server.run();
});
sim_thread.join().expect("joining simulation thread failed");
udp_tc_thread
.join()
.expect("joining UDP server thread failed");
}
@@ -1,29 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H753ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
Loaded 100 of 320 files, more files were not shown because too many files have changed in this diff. Show more