Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eaa78dfbc | ||
|
|
a6d9bee5df | ||
|
|
a77bbfa953 | ||
|
|
4c67bcdde1 | ||
|
|
a710b30013 | ||
|
|
29783b2b07 | ||
|
|
2a2a3a3eab
|
||
|
|
2507469e68
|
||
|
|
b4febefa33 | ||
|
|
fe60cb9ccf | ||
|
|
27e88ed7f7
|
||
|
|
295fed9a72
|
||
|
|
8e89c8dd66
|
||
|
|
cb0a65c4d4 | ||
|
|
3db54da3df | ||
|
|
15fcb17363 | ||
|
|
8728c7ebea | ||
|
|
7606767f63 | ||
|
|
37b32a9008 | ||
|
|
9e096193dd | ||
|
|
43bd77eef0
|
||
|
|
a4888bce01
|
||
|
|
6e5b70af34
|
||
|
|
d1476eb770
|
||
|
|
783388aa6f
|
||
|
|
4a8db6b26a
|
||
|
|
b86c2eb1d1
|
||
|
|
fe4126f7e2
|
||
|
|
c20163b10a
|
||
|
|
b970154488 |
No files matched your search
@@ -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
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+18
@@ -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>
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+18
@@ -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>
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+18
@@ -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
@@ -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
@@ -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>
|
||||
Generated
+19
@@ -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>
|
||||
Generated
+18
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p>
|
||||
|
||||
[](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
|
||||
[](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
||||
[](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/)
|
||||
[](https://crates.io/crates/satrs)
|
||||
[](https://docs.rs/satrs)
|
||||
[](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
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
+1018
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.
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
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
-20
@@ -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,
|
||||
×tamp,
|
||||
)
|
||||
.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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
/target
|
||||
/.cargo/config.toml
|
||||
/.cargo/config*
|
||||
/.vscode
|
||||
/app.map
|
||||
+881
@@ -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",
|
||||
]
|
||||
+30
-21
@@ -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]
|
||||
File renamed without changes.
@@ -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
Binary file not shown.
BIN
Binary file not shown.
@@ -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
@@ -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()
|
||||
}
|
||||
+21
-20
@@ -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
-1
@@ -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 +0,0 @@
|
||||
/config.toml
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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`.
|
||||
@@ -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)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod ctrl;
|
||||
pub mod mgm;
|
||||
pub mod mgm_assembly;
|
||||
pub mod mgt;
|
||||
pub mod subsystem;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)?)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"/>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user