Compare commits
127
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830df80291 | ||
|
|
13325056f6 | ||
|
|
d22ee524ff | ||
|
|
03fa49341b | ||
|
|
93fcd44f13 | ||
|
|
7ae61a81e6 | ||
|
|
9ff6600920 | ||
|
|
32834920d1 | ||
|
|
b4869e0db4 | ||
|
|
db906565c8 | ||
|
|
ba9ea3a12c | ||
|
|
782f6c3936 | ||
|
|
3fa655da59 | ||
|
|
1aee1b8513 | ||
|
|
d791ca2a5f | ||
|
|
7a2a317d58 | ||
|
|
d9ead9516e | ||
|
|
7b74bee581 | ||
|
|
1c6e777d24 | ||
|
|
919f1aa65d | ||
|
|
883998e31d | ||
|
|
8d36fd3761 | ||
|
|
2a7b3796ec | ||
|
|
1c98f3772f | ||
|
|
76074d69e4 | ||
|
|
86d8528d21 | ||
|
|
7d6e4ca2f8 | ||
|
|
eeb6fe17cc | ||
|
|
9de9ce3131 | ||
|
|
87fc65fdd4 | ||
|
|
877312d3c3 | ||
|
|
1649306802 | ||
|
|
12824b2906 | ||
|
|
cbce7916c2 | ||
|
|
54971683bf | ||
|
|
4ba4d3e6a3 | ||
|
|
2d9b14e69e | ||
|
|
d2b8bc6afb | ||
|
|
2cf39a0e1c | ||
|
|
1e44b2d59f | ||
|
|
c40b8f32c9 | ||
|
|
f6a87cff28 | ||
|
|
8195dac587 | ||
|
|
5fffcc011f | ||
|
|
3e4c3c4e64 | ||
|
|
3f0937de83 | ||
|
|
d2db28468f | ||
|
|
3550026c00 | ||
|
|
ad4908feef | ||
|
|
20bc6c4fb6 | ||
|
|
e97e6f2a54 | ||
|
|
1c9ebe8a75 | ||
|
|
7a464843ed | ||
|
|
d58b1db55b | ||
|
|
b1253eaad4 | ||
|
|
ae4d26b8bd | ||
|
|
cfcfabb5e3 | ||
|
|
70f747ad86 | ||
|
|
f44aac6ea2 | ||
|
|
df517af85b | ||
|
|
ae9edf5888 | ||
|
|
512384026c | ||
|
|
42c7a3b9ee | ||
|
|
1e15e3d501 | ||
|
|
d7e6732888 | ||
|
|
c27569a526 | ||
|
|
b2bc87641c | ||
|
|
c8245772bb | ||
|
|
56d9cafa59 | ||
|
|
7de057aaf2
|
||
|
|
2f4a11a0bc | ||
|
|
0cd929a19c | ||
|
|
d54167837e | ||
|
|
ecce07a471 | ||
|
|
526254851a | ||
|
|
440e757141
|
||
|
|
02aa825783 | ||
|
|
bc9e0e4a94 | ||
|
|
06e713a557 | ||
|
|
778512d50e | ||
|
|
5d40638964 | ||
|
|
1b07b845f2 | ||
|
|
2e58a311c8 | ||
|
|
62d64e692a
|
||
|
|
3784f47b66 | ||
|
|
e1911f1b6e | ||
|
|
2e53ce1871 | ||
|
|
a6c460129b
|
||
|
|
d5ecefd683 | ||
|
|
2f9f3b8183 | ||
|
|
b28cff85de | ||
|
|
da3de9f22c | ||
|
|
aefdf0987d | ||
|
|
7ae9e62d66 | ||
|
|
fea1a9028b | ||
|
|
abc145fb2d
|
||
|
|
490635dfcf | ||
|
|
de3d22a022 | ||
|
|
cbe211fe8b | ||
|
|
e379bc3fd7 | ||
|
|
a61ee85796 | ||
|
|
81473b30f9
|
||
|
|
22675a73f9 | ||
|
|
c68e3d4f75
|
||
|
|
3deedfba17 | ||
|
|
533caea0fe
|
||
|
|
848fe6f207 | ||
|
|
cf64fea7d9 | ||
|
|
4948db3fa5 | ||
|
|
1920e4878c
|
||
|
|
f46bc94b04 | ||
|
|
fb45da1890
|
||
|
|
f600ee499a | ||
|
|
3f28f60c59 | ||
|
|
44b1f2b037 | ||
|
|
4b22958b34 | ||
|
|
a711c6acd9 | ||
|
|
99a954a1f5
|
||
|
|
4a4fd7ac2c
|
||
|
|
9bf08849a2
|
||
|
|
b8f7fefe26 | ||
|
|
a501832698
|
||
|
|
c7284d3f1c | ||
|
|
18263d4568 | ||
|
|
95519c1363
|
||
|
|
2ec32717d0 | ||
|
|
bfdd777685
|
No files matched your search
@@ -11,6 +11,9 @@ 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 satrs-example --no-default-features
|
||||
@@ -23,6 +26,7 @@ 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
|
||||
|
||||
@@ -47,6 +51,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
docs:
|
||||
@@ -55,7 +61,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- run: cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docs_rs"]'
|
||||
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
@@ -63,4 +69,7 @@ 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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,18 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,18 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,18 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
@@ -1,18 +0,0 @@
|
||||
<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
@@ -1,19 +0,0 @@
|
||||
<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
-1
@@ -4,8 +4,13 @@ members = [
|
||||
"satrs",
|
||||
"satrs-mib",
|
||||
"satrs-example",
|
||||
"satrs-minisim",
|
||||
"satrs-example/types",
|
||||
"satrs-example/client",
|
||||
"satrs-example/minisim",
|
||||
"satrs-shared",
|
||||
"tmtc-utils",
|
||||
"embedded-examples/embedded-client",
|
||||
"embedded-examples/types",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p>
|
||||
|
||||
[](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
||||
[](https://robamu.github.io/sat-rs/book/)
|
||||
[](https://documentation.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://robamu.github.io/sat-rs/book/)
|
||||
at the [IRS software projects website](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||
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/).
|
||||
|
||||
This is early-stage software. Important features are missing. New releases
|
||||
with breaking changes are released regularly, with all changes documented inside respective
|
||||
@@ -30,7 +30,7 @@ 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/). It can be useful to read
|
||||
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/). 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.
|
||||
@@ -61,6 +61,8 @@ Each project has its own `CHANGELOG.md`.
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "embedded-client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serialport = "4"
|
||||
toml = "0.9"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
spacepackets = { version = "0.18" }
|
||||
embedded-types = { path = "../types" }
|
||||
tmtc-utils = { path = "../../tmtc-utils" }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
anyhow = "1"
|
||||
cobs = "0.5"
|
||||
fern = "0.7"
|
||||
humantime = "2"
|
||||
log = "0.4"
|
||||
@@ -0,0 +1,3 @@
|
||||
[interface]
|
||||
serial_port = "/dev/ttyUSB0"
|
||||
# udp_addr = "192.168.XXX.XX:7301"
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use cobs::CobsDecoderOwned;
|
||||
use embedded_client::setup_logger;
|
||||
use embedded_types::stm32f3;
|
||||
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
|
||||
/// Set frequency in milliseconds.
|
||||
#[arg(short, long)]
|
||||
set_led_frequency: Option<u32>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_logger().expect("failed to initialize logger");
|
||||
println!("-- STM32F3 TMTC client --");
|
||||
let cli = Cli::parse();
|
||||
let config = embedded_client::Config::new_from_file();
|
||||
|
||||
if config.interface.serial_port.is_none() {
|
||||
bail!("Serial port not specified in configuration file.");
|
||||
}
|
||||
let serial_port = config.interface.serial_port.as_ref().unwrap();
|
||||
let serial = serialport::new(serial_port, 115200)
|
||||
.open()
|
||||
.expect("opening serial port failed");
|
||||
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
|
||||
|
||||
if cli.ping {
|
||||
let tc = create_stm32f3_tc(&embedded_types::stm32f3::Request::Ping);
|
||||
log::info!(
|
||||
"Sending ping request with TC ID: {:#010x}",
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(freq_ms) = cli.set_led_frequency {
|
||||
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||
let tc = create_stm32f3_tc(&request);
|
||||
log::info!(
|
||||
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||
request,
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
log::info!("Waiting for response...");
|
||||
loop {
|
||||
transport
|
||||
.receive(|packet: &[u8]| {
|
||||
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||
log::info!("Received packet: {:?}", reader);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
|
||||
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||
let sp_header = SpHeader::new_from_apid(embedded_types::stm32f3::PUS_APID);
|
||||
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::{net::UdpSocket, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
use clap::Parser;
|
||||
use embedded_client::setup_logger;
|
||||
use embedded_types::{TmHeader, stm32h7};
|
||||
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||
use tmtc_utils::transport::udp::PacketTransportUdp;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
|
||||
/// Set frequency in milliseconds.
|
||||
#[arg(short, long)]
|
||||
set_led_frequency: Option<u32>,
|
||||
|
||||
/// UDP address to bind to.
|
||||
#[arg(short, long)]
|
||||
udp_addr: Option<std::net::SocketAddr>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_logger().expect("failed to initialize logger");
|
||||
println!("-- STM32H7 TMTC client --");
|
||||
let cli = Cli::parse();
|
||||
let config = embedded_client::Config::new_from_file();
|
||||
let mut udp_addr = cli.udp_addr;
|
||||
if udp_addr.is_none() {
|
||||
udp_addr = config.interface.udp_addr;
|
||||
}
|
||||
if udp_addr.is_none() {
|
||||
bail!("UDP address not specified in config.toml or via command line");
|
||||
}
|
||||
let udp_addr = udp_addr.unwrap();
|
||||
log::info!("binding to UDP address: {}", udp_addr);
|
||||
let local_socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket");
|
||||
let mut transport = PacketTransportUdp::new(local_socket, udp_addr)
|
||||
.with_context(|| "crateing UDP transport failed")?;
|
||||
|
||||
if cli.ping {
|
||||
let tc = create_stm32h7_tc(&embedded_types::stm32h7::Request::Ping);
|
||||
log::info!(
|
||||
"Sending ping request with TC ID: {:#010x}",
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(freq_ms) = cli.set_led_frequency {
|
||||
let request = stm32h7::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||
let tc = create_stm32h7_tc(&request);
|
||||
log::info!(
|
||||
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||
request,
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
log::info!("Waiting for response...");
|
||||
loop {
|
||||
transport
|
||||
.receive(|packet: &[u8]| {
|
||||
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||
log::debug!("Received packet: {:?}", reader);
|
||||
if let Ok(reader) = reader {
|
||||
let packet_data = reader.packet_data();
|
||||
let tm_header = postcard::take_from_bytes::<TmHeader>(packet_data);
|
||||
if let Ok((tm_header, remainder)) = tm_header {
|
||||
let response = postcard::from_bytes::<stm32h7::Response>(remainder);
|
||||
if let Ok(response) = response {
|
||||
log::info!(
|
||||
"Received TM with header: {:?} and response: {:?}",
|
||||
tm_header,
|
||||
response
|
||||
);
|
||||
} else {
|
||||
log::error!("Failed to deserialize response: {:?}", response.err());
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to deserialize TM header: {:?}", tm_header.err());
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned {
|
||||
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||
let sp_header = SpHeader::new_from_apid(embedded_types::stm32h7::PUS_APID);
|
||||
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::{fs::File, io::Read as _, net::SocketAddr, path::Path, time::SystemTime};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Config {
|
||||
pub interface: Interface,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Interface {
|
||||
pub serial_port: Option<String>,
|
||||
pub udp_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new_from_file() -> Self {
|
||||
let mut config_file =
|
||||
File::open(Path::new("config.toml")).expect("opening config.toml file failed");
|
||||
let mut toml_str = String::new();
|
||||
config_file
|
||||
.read_to_string(&mut toml_str)
|
||||
.expect("reading config.toml file failed");
|
||||
let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed");
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_logger() -> 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(log::LevelFilter::Info)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
||||
+1
-1
@@ -34,4 +34,4 @@ rustflags = [
|
||||
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "info"
|
||||
DEFMT_LOG = "info"
|
||||
@@ -1,4 +1,4 @@
|
||||
/target
|
||||
/itm.txt
|
||||
/.cargo/config*
|
||||
/.cargo/config.toml
|
||||
/.vscode
|
||||
+864
-360
File diff suppressed because it is too large.
Load diff
@@ -7,51 +7,32 @@ default-run = "satrs-stm32f3-disco-rtic"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
embedded-types = { path = "../types", features = ["defmt"] }
|
||||
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"
|
||||
defmt = "1"
|
||||
defmt-rtt = { version = "1" }
|
||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||
embedded-hal = "1"
|
||||
cortex-m-semihosting = "0.5.0"
|
||||
embassy-stm32 = { version = "0.6", features = ["defmt", "stm32f303vc", "memory-x", "unstable-pac", "time-driver-any"] }
|
||||
embassy-time = { version = "0.5", features = ["defmt", "generic-queue-16", "defmt-timestamp-uptime-ms"]}
|
||||
enumset = "1"
|
||||
heapless = "0.8"
|
||||
heapless = "0.9"
|
||||
embassy-sync = "0.8"
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
|
||||
static_cell = "2"
|
||||
cobs = { version = "0.5", default-features = false, features = ["defmt"] }
|
||||
postcard = { version = "1" }
|
||||
arbitrary-int = "2"
|
||||
thiserror = { version = "2", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
|
||||
[dependencies.rtic]
|
||||
version = "2"
|
||||
features = ["thumbv7-backend"]
|
||||
|
||||
[dependencies.rtic-monotonics]
|
||||
version = "2"
|
||||
features = ["cortex-m-systick"]
|
||||
|
||||
[dependencies.cobs]
|
||||
version = "0.3"
|
||||
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"]
|
||||
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||
rtic-sync = { version = "1" }
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test = "0.3"
|
||||
defmt-test = "0.5"
|
||||
|
||||
# cargo test
|
||||
[profile.test]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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]
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
@@ -1,33 +0,0 @@
|
||||
/* 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;
|
||||
*/
|
||||
@@ -1,8 +0,0 @@
|
||||
/venv
|
||||
/.tmtc-history.txt
|
||||
/log
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
/seqcnt.txt
|
||||
/tmtc_conf.json
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"com_if": "serial_cobs",
|
||||
"serial_baudrate": 115200
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,2 +0,0 @@
|
||||
tmtccmd == 8.0.1
|
||||
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
|
||||
@@ -1,76 +1,56 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use satrs_stm32f3_disco_rtic as _;
|
||||
#![no_std]
|
||||
|
||||
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};
|
||||
use panic_probe as _;
|
||||
use rtic::app;
|
||||
|
||||
#[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);
|
||||
#[app(device = embassy_stm32)]
|
||||
mod app {
|
||||
use embassy_time::Timer;
|
||||
use satrs_stm32f3_disco_rtic::{Direction, LedPinSet, Leds};
|
||||
|
||||
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);
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
//explicit on/off
|
||||
leds.ld4_nw.on().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
leds.ld4_nw.off().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
#[local]
|
||||
struct Local {
|
||||
leds: Leds,
|
||||
current_dir: Direction,
|
||||
}
|
||||
|
||||
leds.ld5_ne.on().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
leds.ld5_ne.off().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
|
||||
leds.ld6_w.on().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
leds.ld6_w.off().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery using RTICv2");
|
||||
|
||||
leds.ld7_e.on().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
leds.ld7_e.off().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
let led_pin_set = LedPinSet {
|
||||
pin_n: p.PE8,
|
||||
pin_ne: p.PE9,
|
||||
pin_e: p.PE10,
|
||||
pin_se: p.PE11,
|
||||
pin_s: p.PE12,
|
||||
pin_sw: p.PE13,
|
||||
pin_w: p.PE14,
|
||||
pin_nw: p.PE15,
|
||||
};
|
||||
let leds = Leds::new(led_pin_set);
|
||||
|
||||
leds.ld8_sw.on().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
leds.ld8_sw.off().ok();
|
||||
delay.delay_ms(delay_ms);
|
||||
blinky::spawn().expect("failed to spawn blinky task");
|
||||
(
|
||||
Shared {},
|
||||
Local {
|
||||
leds,
|
||||
current_dir: Direction::North,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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);
|
||||
#[task(local = [leds, current_dir])]
|
||||
async fn blinky(cx: blinky::Context) {
|
||||
loop {
|
||||
cx.local.leds.blink_next(cx.local.current_dir);
|
||||
Timer::after_millis(200).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,145 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use cortex_m_semihosting::debug;
|
||||
|
||||
use defmt_brtt as _; // global logger
|
||||
|
||||
use stm32f3xx_hal as _; // memory layout
|
||||
|
||||
use defmt_rtt as _;
|
||||
use panic_probe as _;
|
||||
|
||||
// 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]
|
||||
fn panic() -> ! {
|
||||
cortex_m::asm::udf()
|
||||
use arbitrary_int::u11;
|
||||
use embassy_stm32::gpio::Output;
|
||||
|
||||
pub const APID: u11 = u11::new(0x02);
|
||||
|
||||
#[derive(defmt::Format, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Direction {
|
||||
North,
|
||||
NorthEast,
|
||||
East,
|
||||
SouthEast,
|
||||
South,
|
||||
SouthWest,
|
||||
West,
|
||||
NorthWest,
|
||||
}
|
||||
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with status code 0.
|
||||
pub fn exit() -> ! {
|
||||
loop {
|
||||
debug::exit(debug::EXIT_SUCCESS);
|
||||
impl Direction {
|
||||
pub fn switch_to_next(&mut self) -> (Self, Self) {
|
||||
let curr = *self;
|
||||
*self = match self {
|
||||
Direction::North => Direction::NorthEast,
|
||||
Direction::NorthEast => Direction::East,
|
||||
Direction::East => Direction::SouthEast,
|
||||
Direction::SouthEast => Direction::South,
|
||||
Direction::South => Direction::SouthWest,
|
||||
Direction::SouthWest => Direction::West,
|
||||
Direction::West => Direction::NorthWest,
|
||||
Direction::NorthWest => Direction::North,
|
||||
};
|
||||
(curr, *self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hardfault handler.
|
||||
///
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with an error. This seems better than the default, which is to spin in a
|
||||
/// loop.
|
||||
#[cortex_m_rt::exception]
|
||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||
loop {
|
||||
debug::exit(debug::EXIT_FAILURE);
|
||||
pub struct Leds {
|
||||
pub north: Output<'static>,
|
||||
pub north_east: Output<'static>,
|
||||
pub east: Output<'static>,
|
||||
pub south_east: Output<'static>,
|
||||
pub south: Output<'static>,
|
||||
pub south_west: Output<'static>,
|
||||
pub west: Output<'static>,
|
||||
pub north_west: Output<'static>,
|
||||
}
|
||||
|
||||
impl Leds {
|
||||
pub fn blink_next(&mut self, current_dir: &mut Direction) {
|
||||
let (prev, curr) = current_dir.switch_to_next();
|
||||
self.set_dir_low(prev);
|
||||
self.set_dir_high(curr);
|
||||
}
|
||||
|
||||
pub fn set_dir(&mut self, dir: Direction, level: embassy_stm32::gpio::Level) {
|
||||
match dir {
|
||||
Direction::North => self.north.set_level(level),
|
||||
Direction::NorthEast => self.north_east.set_level(level),
|
||||
Direction::East => self.east.set_level(level),
|
||||
Direction::SouthEast => self.south_east.set_level(level),
|
||||
Direction::South => self.south.set_level(level),
|
||||
Direction::SouthWest => self.south_west.set_level(level),
|
||||
Direction::West => self.west.set_level(level),
|
||||
Direction::NorthWest => self.north_west.set_level(level),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dir_low(&mut self, dir: Direction) {
|
||||
self.set_dir(dir, embassy_stm32::gpio::Level::Low);
|
||||
}
|
||||
|
||||
pub fn set_dir_high(&mut self, dir: Direction) {
|
||||
self.set_dir(dir, embassy_stm32::gpio::Level::High);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
use defmt::assert;
|
||||
pub struct LedPinSet {
|
||||
pub pin_n: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE8>,
|
||||
pub pin_ne: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE9>,
|
||||
pub pin_e: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE10>,
|
||||
pub pin_se: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE11>,
|
||||
pub pin_s: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE12>,
|
||||
pub pin_sw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE13>,
|
||||
pub pin_w: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE14>,
|
||||
pub pin_nw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE15>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
impl Leds {
|
||||
pub fn new(pin_set: LedPinSet) -> Self {
|
||||
let led_n = Output::new(
|
||||
pin_set.pin_n,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_ne = Output::new(
|
||||
pin_set.pin_ne,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_e = Output::new(
|
||||
pin_set.pin_e,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_se = Output::new(
|
||||
pin_set.pin_se,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_s = Output::new(
|
||||
pin_set.pin_s,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_sw = Output::new(
|
||||
pin_set.pin_sw,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_w = Output::new(
|
||||
pin_set.pin_w,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_nw = Output::new(
|
||||
pin_set.pin_nw,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
Self {
|
||||
north: led_n,
|
||||
north_east: led_ne,
|
||||
east: led_e,
|
||||
south_east: led_se,
|
||||
south: led_s,
|
||||
south_west: led_sw,
|
||||
west: led_w,
|
||||
north_west: led_nw,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,682 +1,336 @@
|
||||
#![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 arbitrary_int::u14;
|
||||
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embedded_types::{create_tm_packet, stm32f3, tm_size, TmHeader};
|
||||
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
|
||||
|
||||
use defmt_rtt as _; // global logger
|
||||
use panic_probe as _;
|
||||
|
||||
use rtic::app;
|
||||
|
||||
use heapless::{mpmc::Q8, Vec};
|
||||
#[allow(unused_imports)]
|
||||
use rtic_monotonics::fugit::{MillisDurationU32, TimerInstantU32};
|
||||
use rtic_monotonics::systick::prelude::*;
|
||||
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 COBS_TM_OVERHEAD: usize = cobs::max_encoding_overhead(MAX_TM_LEN);
|
||||
|
||||
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];
|
||||
const TC_DMA_BUF_LEN: usize = 512;
|
||||
|
||||
type TmPacket = Vec<u8, MAX_TM_LEN>;
|
||||
type TcPacket = Vec<u8, MAX_TC_LEN>;
|
||||
type TmPacket = heapless::Vec<u8, MAX_TM_LEN>;
|
||||
|
||||
static TM_REQUESTS: Q8<TmPacket> = Q8::new();
|
||||
static TM_QUEUE: embassy_sync::channel::Channel<CriticalSectionRawMutex, TmPacket, 16> =
|
||||
embassy_sync::channel::Channel::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)]
|
||||
#[derive(Debug, defmt::Format, thiserror::Error)]
|
||||
pub enum TmSendError {
|
||||
ByteConversion(ByteConversionError),
|
||||
#[error("packet creation error: {0}")]
|
||||
PacketCreation(#[from] CcsdsPacketCreationError),
|
||||
#[error("queue error")]
|
||||
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),
|
||||
pub struct RequestWithTcId {
|
||||
pub request: stm32f3::Request,
|
||||
pub tc_id: CcsdsPacketIdAndPsc,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
#[app(device = embassy_stm32)]
|
||||
mod app {
|
||||
use core::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use core::slice::Iter;
|
||||
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 arbitrary_int::u14;
|
||||
use embassy_time::Timer;
|
||||
use embedded_types::stm32f3::{Request, Response};
|
||||
use rtic::Mutex;
|
||||
use rtic_sync::{
|
||||
channel::{Receiver, Sender},
|
||||
make_channel,
|
||||
};
|
||||
use satrs_stm32f3_disco_rtic::LedPinSet;
|
||||
use spacepackets::CcsdsPacketReader;
|
||||
|
||||
use stm32f3_discovery::switch_hal::OutputSwitch;
|
||||
use stm32f3xx_hal::Switch;
|
||||
#[allow(dead_code)]
|
||||
type SerialType = Serial<USART2, (PA2<AF7<PushPull>>, PA3<AF7<PushPull>>)>;
|
||||
|
||||
systick_monotonic!(Mono, 1000);
|
||||
embassy_stm32::bind_interrupts!(struct Irqs {
|
||||
USART2 => embassy_stm32::usart::InterruptHandler<embassy_stm32::peripherals::USART2>;
|
||||
DMA1_CHANNEL6 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH6>;
|
||||
DMA1_CHANNEL7 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH7>;
|
||||
});
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
blink_freq: MillisDurationU32,
|
||||
tx_shared: UartTxShared,
|
||||
rx_transfer: Option<RxDmaTransferType>,
|
||||
blink_freq: Duration,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
verif_reporter: VerificationReportCreator,
|
||||
leds: Leds,
|
||||
last_dir: Direction,
|
||||
curr_dir: Iter<'static, Direction>,
|
||||
leds: satrs_stm32f3_disco_rtic::Leds,
|
||||
current_dir: satrs_stm32f3_disco_rtic::Direction,
|
||||
seq_count: u14,
|
||||
tx: embassy_stm32::usart::UartTx<'static, embassy_stm32::mode::Async>,
|
||||
rx: embassy_stm32::usart::RingBufferedUartRx<'static>,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(cx: init::Context) -> (Shared, Local) {
|
||||
let mut rcc = cx.device.RCC.constrain();
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
static DMA_BUF: static_cell::ConstStaticCell<[u8; TC_DMA_BUF_LEN]> =
|
||||
static_cell::ConstStaticCell::new([0; TC_DMA_BUF_LEN]);
|
||||
|
||||
// Initialize the systick interrupt & obtain the token to prove that we did
|
||||
Mono::start(cx.core.SYST, 8_000_000);
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
|
||||
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);
|
||||
let (req_sender, req_receiver) = make_channel!(RequestWithTcId, 16);
|
||||
|
||||
// Set up monotonic timer.
|
||||
//let mono_timer = MonoTimer::new(cx.core.DWT, clocks, &mut cx.core.DCB);
|
||||
defmt::info!("sat-rs demo application for the STM32F3-Discovery with RTICv2");
|
||||
let led_pin_set = LedPinSet {
|
||||
pin_n: p.PE8,
|
||||
pin_ne: p.PE9,
|
||||
pin_e: p.PE10,
|
||||
pin_se: p.PE11,
|
||||
pin_s: p.PE12,
|
||||
pin_sw: p.PE13,
|
||||
pin_w: p.PE14,
|
||||
pin_nw: p.PE15,
|
||||
};
|
||||
let leds = satrs_stm32f3_disco_rtic::Leds::new(led_pin_set);
|
||||
|
||||
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery");
|
||||
let mut gpioe = cx.device.GPIOE.split(&mut rcc.ahb);
|
||||
let mut config = embassy_stm32::usart::Config::default();
|
||||
config.baudrate = UART_BAUD;
|
||||
let uart = embassy_stm32::usart::Uart::new(
|
||||
p.USART2, p.PA3, p.PA2, p.DMA1_CH7, p.DMA1_CH6, Irqs, config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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);
|
||||
let (tx, rx) = uart.split();
|
||||
defmt::info!("Spawning tasks");
|
||||
blink::spawn().unwrap();
|
||||
blinky::spawn().unwrap();
|
||||
serial_tx_handler::spawn().unwrap();
|
||||
|
||||
let verif_reporter = VerificationReportCreator::new(PUS_APID).unwrap();
|
||||
serial_rx_handler::spawn(req_sender).unwrap();
|
||||
req_handler::spawn(req_receiver).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),
|
||||
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||
},
|
||||
Local {
|
||||
verif_reporter,
|
||||
leds,
|
||||
last_dir: Direction::North,
|
||||
curr_dir: Direction::iter(),
|
||||
tx,
|
||||
seq_count: u14::new(0),
|
||||
rx: rx.into_ring_buffered(DMA_BUF.take()),
|
||||
current_dir: satrs_stm32f3_disco_rtic::Direction::North,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[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;
|
||||
};
|
||||
#[task(local = [leds, current_dir], shared=[blink_freq])]
|
||||
async fn blinky(mut cx: blinky::Context) {
|
||||
loop {
|
||||
match curr_dir.next() {
|
||||
Some(dir) => {
|
||||
toggle_leds(dir);
|
||||
}
|
||||
None => {
|
||||
*curr_dir = Direction::iter();
|
||||
toggle_leds(curr_dir.next().unwrap());
|
||||
}
|
||||
}
|
||||
cx.local.leds.blink_next(cx.local.current_dir);
|
||||
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||
Mono::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 = (Mono::now() - last_completed).to_millis();
|
||||
if elapsed_ms < MIN_DELAY_BETWEEN_TX_PACKETS_MS {
|
||||
Mono::delay((MIN_DELAY_BETWEEN_TX_PACKETS_MS - elapsed_ms).millis()).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for completion after 1 ms
|
||||
Mono::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
|
||||
Mono::delay(1.millis()).await;
|
||||
continue;
|
||||
}
|
||||
// Nothing to do, and we are idle.
|
||||
Mono::delay(TX_HANDLER_FREQ_MS.millis()).await;
|
||||
Timer::after_millis(current_blink_freq.as_millis() as u64).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[task(
|
||||
local = [
|
||||
verif_reporter,
|
||||
tx,
|
||||
encoded_buf: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN]
|
||||
],
|
||||
shared = [],
|
||||
)]
|
||||
async fn serial_tx_handler(cx: serial_tx_handler::Context) {
|
||||
loop {
|
||||
loop {
|
||||
let vec = TM_QUEUE.receive().await;
|
||||
let encoded_len =
|
||||
cobs::encode_including_sentinels(&vec[0..vec.len()], cx.local.encoded_buf);
|
||||
defmt::debug!("sending {} bytes over UART", encoded_len);
|
||||
cx.local
|
||||
.tx
|
||||
.write(&cx.local.encoded_buf[0..encoded_len])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(
|
||||
local = [
|
||||
rx,
|
||||
read_buf: [u8; 128] = [0; 128],
|
||||
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: serial_rx_handler::Context,
|
||||
mut sender: Sender<'static, RequestWithTcId, 16>,
|
||||
) {
|
||||
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);
|
||||
let mut decoder = cobs::CobsDecoder::new(cx.local.decode_buf);
|
||||
loop {
|
||||
match cx.local.rx.read(cx.local.read_buf).await {
|
||||
Ok(bytes) => {
|
||||
defmt::debug!("received {} bytes over UART", bytes);
|
||||
for byte in cx.local.read_buf[0..bytes].iter() {
|
||||
match decoder.feed(*byte) {
|
||||
Ok(None) => (),
|
||||
Ok(Some(packet_size)) => {
|
||||
match CcsdsPacketReader::new_with_checksum(
|
||||
&decoder.dest()[0..packet_size],
|
||||
) {
|
||||
Ok(packet) => {
|
||||
let tc_packet_id =
|
||||
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet);
|
||||
if let Ok(request) =
|
||||
postcard::from_bytes::<Request>(packet.packet_data())
|
||||
{
|
||||
sender
|
||||
.send(RequestWithTcId {
|
||||
request,
|
||||
tc_id: tc_packet_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
Err(e) => {
|
||||
defmt::error!("error unpacking ccsds packet: {}", e);
|
||||
}
|
||||
}
|
||||
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);
|
||||
defmt::error!("cobs decoding error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::warn!("Error unpacking PUS TC: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::error!("uart read error: {}", 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],
|
||||
#[task(shared = [blink_freq], local = [seq_count])]
|
||||
async fn req_handler(
|
||||
mut cx: req_handler::Context,
|
||||
mut receiver: Receiver<'static, RequestWithTcId, 16>,
|
||||
) {
|
||||
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);
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Ok(request_with_tc_id) => {
|
||||
let tm_send_result = match request_with_tc_id.request {
|
||||
Request::Ping => {
|
||||
handle_ping_request(&mut cx, request_with_tc_id.tc_id).await
|
||||
}
|
||||
Request::ChangeBlinkFrequency(duration) => {
|
||||
handle_change_blink_frequency_request(
|
||||
&mut cx,
|
||||
request_with_tc_id.tc_id,
|
||||
duration,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
if let Err(e) = tm_send_result {
|
||||
defmt::error!("error sending TM response: {}", e);
|
||||
}
|
||||
}
|
||||
Err(_e) => defmt::error!("request receive error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
});
|
||||
async fn handle_ping_request(
|
||||
cx: &mut req_handler::Context<'_>,
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
) -> Result<(), TmSendError> {
|
||||
defmt::info!("Received PUS ping telecommand, sending ping reply");
|
||||
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
|
||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[task(binds = USART2_EXTI26, shared = [rx_transfer, tx_shared])]
|
||||
fn serial_isr(mut cx: serial_isr::Context) {
|
||||
async fn handle_change_blink_frequency_request(
|
||||
cx: &mut req_handler::Context<'_>,
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
duration: Duration,
|
||||
) -> Result<(), TmSendError> {
|
||||
defmt::info!(
|
||||
"Received ChangeBlinkFrequency request, new frequency: {} ms",
|
||||
duration.as_millis()
|
||||
);
|
||||
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(Mono::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));
|
||||
}
|
||||
});
|
||||
.blink_freq
|
||||
.lock(|blink_freq| *blink_freq = duration);
|
||||
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
|
||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_tm(
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
response: stm32f3::Response,
|
||||
current_seq_count: u14,
|
||||
) -> Result<(), TmSendError> {
|
||||
let sp_header = SpHeader::new_for_unseg_tc(stm32f3::PUS_APID, current_seq_count, 0);
|
||||
let tm_header = TmHeader {
|
||||
tc_packet_id: Some(tc_packet_id),
|
||||
uptime_millis: embassy_time::Instant::now().as_millis(),
|
||||
};
|
||||
let mut tm_packet = TmPacket::new();
|
||||
let tm_size = tm_size(&tm_header, &response);
|
||||
tm_packet.resize(tm_size, 0).expect("vec resize failed");
|
||||
create_tm_packet(&mut tm_packet, sp_header, tm_header, response)?;
|
||||
TM_QUEUE.send(tm_packet).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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]
|
||||
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(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hardfault handler.
|
||||
///
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with an error. This seems better than the default, which is to spin in a
|
||||
/// loop.
|
||||
#[cortex_m_rt::exception]
|
||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||
loop {
|
||||
debug::exit(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 {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||
runner = "probe-rs run --chip STM32H743ZITx"
|
||||
# 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"
|
||||
+725
-264
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,4 @@
|
||||
[package]
|
||||
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
|
||||
name = "satrs-stm32h7-nucleo-rtic"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
@@ -14,37 +13,28 @@ name = "integration"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
embedded-types = { path = "../types", features = ["defmt"] }
|
||||
|
||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||
arbitrary-int = "2"
|
||||
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"] }
|
||||
cortex-m-semihosting = "0.5.0"
|
||||
stm32h7xx-hal = { version="0.16", features= ["stm32h743v", "ethernet"] }
|
||||
embedded-alloc = "0.6"
|
||||
rtic-sync = { version = "1", features = ["defmt-03"] }
|
||||
defmt = "1"
|
||||
defmt-rtt = "1"
|
||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||
embedded-alloc = "0.7"
|
||||
static_cell = "2"
|
||||
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
|
||||
postcard = "1"
|
||||
|
||||
[dependencies.smoltcp]
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
features = ["medium-ethernet", "proto-ipv4", "socket-raw", "socket-dhcpv4", "socket-udp", "defmt"]
|
||||
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.6", features = ["stm32h743zi", "memory-x", "defmt", "time-driver-any"]}
|
||||
|
||||
[dependencies.rtic]
|
||||
version = "2"
|
||||
features = ["thumbv7-backend"]
|
||||
|
||||
[dependencies.rtic-monotonics]
|
||||
version = "2"
|
||||
features = ["cortex-m-systick"]
|
||||
|
||||
[dependencies.satrs]
|
||||
path = "../../satrs"
|
||||
version = "0.2"
|
||||
default-features = false
|
||||
features = ["defmt", "heapless"]
|
||||
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||
embassy-net = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c" }
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test = "0.3"
|
||||
defmt-test = "0.5"
|
||||
|
||||
# cargo build/run
|
||||
[profile.dev]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
|
||||
let config = cargo_dir.join("config.toml");
|
||||
let config_template = cargo_dir.join("config.toml.template");
|
||||
|
||||
if !config.exists() && config_template.exists() {
|
||||
fs::create_dir_all(&cargo_dir).unwrap();
|
||||
fs::copy(&config_template, &config).unwrap();
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,8 +0,0 @@
|
||||
/venv
|
||||
/.tmtc-history.txt
|
||||
/log
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
/seqcnt.txt
|
||||
/tmtc_conf.json
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"com_if": "udp",
|
||||
"tcpip_udp_port": 7301
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,2 +0,0 @@
|
||||
tmtccmd == 8.0.1
|
||||
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["thumbv7em-none-eabihf"]
|
||||
@@ -5,51 +5,53 @@
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use rtic::app;
|
||||
use satrs_stm32h7_nucleo_rtic as _;
|
||||
|
||||
use stm32h7xx_hal::{block, prelude::*, timer::Timer};
|
||||
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
|
||||
mod app {
|
||||
use embassy_stm32::gpio;
|
||||
|
||||
use cortex_m_rt::entry;
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
defmt::println!("starting stm32h7 blinky example");
|
||||
#[local]
|
||||
struct Local {}
|
||||
|
||||
// Get access to the device specific peripherals from the peripheral access crate
|
||||
let dp = stm32h7xx_hal::stm32::Peripherals::take().unwrap();
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
defmt::info!("Hello World!");
|
||||
// Configure gpio B pin 0 as a push-pull output.
|
||||
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld2 = gpio::Output::new(p.PB7, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||
|
||||
// Take ownership over the RCC devices and convert them into the corresponding HAL structs
|
||||
let rcc = dp.RCC.constrain();
|
||||
// Schedule the blinking task
|
||||
blink::spawn(ld1, ld2, ld3).ok();
|
||||
|
||||
let pwr = dp.PWR.constrain();
|
||||
let pwrcfg = pwr.freeze();
|
||||
(Shared {}, Local {})
|
||||
}
|
||||
|
||||
// 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);
|
||||
#[task()]
|
||||
async fn blink(
|
||||
_cx: blink::Context,
|
||||
mut ld1: gpio::Output<'static>,
|
||||
mut ld2: gpio::Output<'static>,
|
||||
mut ld3: gpio::Output<'static>,
|
||||
) {
|
||||
loop {
|
||||
defmt::info!("high");
|
||||
ld1.set_high();
|
||||
ld2.set_high();
|
||||
ld3.set_high();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
|
||||
// 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();
|
||||
defmt::info!("low");
|
||||
ld1.set_low();
|
||||
ld2.set_low();
|
||||
ld3.set_low();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use satrs_stm32h7_nucleo_rtic as _; // global logger + panicking-behavior + memory layout
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use satrs_stm32h7_nucleo_rtic as _;
|
||||
|
||||
#[cortex_m_rt::entry]
|
||||
fn main() -> ! {
|
||||
defmt::println!("Hello, world!");
|
||||
|
||||
satrs_stm32h7_nucleo_rtic::exit()
|
||||
loop {
|
||||
defmt::println!("Hello, world!");
|
||||
cortex_m::asm::delay(100_000_000);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use cortex_m_semihosting::debug;
|
||||
|
||||
use defmt_brtt as _; // global logger
|
||||
|
||||
// TODO(5) adjust HAL import
|
||||
use stm32h7xx_hal as _; // memory layout
|
||||
|
||||
use defmt_rtt as _;
|
||||
use embassy_stm32 as _;
|
||||
use panic_probe as _;
|
||||
|
||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||
@@ -17,14 +12,6 @@ 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
|
||||
@@ -32,9 +19,7 @@ pub fn exit() -> ! {
|
||||
/// loop.
|
||||
#[cortex_m_rt::exception]
|
||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||
loop {
|
||||
debug::exit(debug::EXIT_FAILURE);
|
||||
}
|
||||
panic!("unexpected hard fault");
|
||||
}
|
||||
|
||||
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||
|
||||
@@ -3,422 +3,215 @@
|
||||
extern crate alloc;
|
||||
|
||||
use rtic::app;
|
||||
use rtic_monotonics::systick::prelude::*;
|
||||
use satrs::pool::{PoolAddr, PoolProvider, StaticHeaplessMemoryPool};
|
||||
use satrs::static_subpool;
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use embassy_stm32::bind_interrupts;
|
||||
use satrs_stm32h7_nucleo_rtic as _;
|
||||
use smoltcp::socket::udp::UdpMetadata;
|
||||
use smoltcp::socket::{dhcpv4, udp};
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
use embedded_alloc::LlffHeap as 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();
|
||||
|
||||
systick_monotonic!(Mono, 1000);
|
||||
|
||||
// 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,
|
||||
}
|
||||
const TC_QUEUE_DEPTH: usize = 32;
|
||||
const TM_QUEUE_DEPTH: usize = 32;
|
||||
|
||||
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(Mono::now().duration_since_epoch().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 = Mono::now().duration_since_epoch();
|
||||
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)]
|
||||
#[app(device = embassy_stm32, peripherals = false)]
|
||||
mod app {
|
||||
use core::ptr::addr_of_mut;
|
||||
|
||||
use super::*;
|
||||
use rtic_monotonics::fugit::MillisDurationU32;
|
||||
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;
|
||||
use arbitrary_int::u14;
|
||||
use embassy_net::udp::UdpSocket;
|
||||
use embassy_net::StackResources;
|
||||
use embassy_stm32::eth;
|
||||
use embassy_stm32::gpio;
|
||||
use embassy_stm32::peripherals;
|
||||
use embassy_stm32::rng;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Duration;
|
||||
use embassy_time::Timer;
|
||||
use embassy_time::WithTimeout as _;
|
||||
use embedded_types::create_tm_packet;
|
||||
use embedded_types::stm32h7;
|
||||
use embedded_types::tm_size;
|
||||
use embedded_types::TmHeader;
|
||||
use spacepackets::CcsdsPacketCreationError;
|
||||
use spacepackets::CcsdsPacketIdAndPsc;
|
||||
use spacepackets::CcsdsPacketReader;
|
||||
use spacepackets::SpHeader;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
ETH => eth::InterruptHandler;
|
||||
RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||
});
|
||||
|
||||
type Device = eth::Ethernet<
|
||||
'static,
|
||||
peripherals::ETH,
|
||||
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
|
||||
>;
|
||||
|
||||
struct BlinkyLeds {
|
||||
led1: Pin<'B', 7, Output>,
|
||||
led2: Pin<'B', 14, Output>,
|
||||
led1: gpio::Output<'static>,
|
||||
led2: gpio::Output<'static>,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
net_runner: embassy_net::Runner<'static, Device>,
|
||||
net_stack: embassy_net::Stack<'static>,
|
||||
leds: BlinkyLeds,
|
||||
link_led: Pin<'B', 0, Output>,
|
||||
net: Net,
|
||||
udp: UdpNet,
|
||||
tc_source_rx: TcSourceRx,
|
||||
phy: ethernet::phy::LAN8742A<EthernetMAC>,
|
||||
link_led: gpio::Output<'static>,
|
||||
tc_rx: embassy_sync::channel::Receiver<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TC_QUEUE_DEPTH,
|
||||
>,
|
||||
tc_tx: embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TC_QUEUE_DEPTH,
|
||||
>,
|
||||
tm_rx: embassy_sync::channel::Receiver<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
tm_tx: embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
}
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
blink_freq: MillisDurationU32,
|
||||
eth_link_up: bool,
|
||||
sockets: SocketSet<'static>,
|
||||
shared_pool: SharedPool,
|
||||
sequence_count: u14,
|
||||
blink_freq: embassy_time::Duration,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(mut cx: init::Context) -> (Shared, Local) {
|
||||
fn init(_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 mut config = embassy_stm32::Config::default();
|
||||
{
|
||||
use embassy_stm32::rcc::*;
|
||||
config.rcc.hsi = Some(HSIPrescaler::Div1);
|
||||
config.rcc.csi = true;
|
||||
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
|
||||
config.rcc.pll1 = Some(Pll {
|
||||
source: PllSource::Hsi,
|
||||
prediv: PllPreDiv::Div4,
|
||||
mul: PllMul::Mul50,
|
||||
fracn: None,
|
||||
divp: Some(PllDiv::Div2),
|
||||
divq: None,
|
||||
divr: None,
|
||||
});
|
||||
config.rcc.sys = Sysclk::Pll1P; // 400 Mhz
|
||||
config.rcc.ahb_pre = AHBPrescaler::Div2; // 200 Mhz
|
||||
config.rcc.apb1_pre = APBPrescaler::Div2; // 100 Mhz
|
||||
config.rcc.apb2_pre = APBPrescaler::Div2; // 100 Mhz
|
||||
config.rcc.apb3_pre = APBPrescaler::Div2; // 100 Mhz
|
||||
config.rcc.apb4_pre = APBPrescaler::Div2; // 100 Mhz
|
||||
config.rcc.voltage_scale = VoltageScale::Scale1;
|
||||
}
|
||||
let periphs = embassy_stm32::init(config);
|
||||
|
||||
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
|
||||
Mono::start(cx.core.SYST, ccdr.clocks.sys_ck().to_Hz());
|
||||
|
||||
// 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();
|
||||
let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let mut led1 = gpio::Output::new(periphs.PB7, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium);
|
||||
|
||||
// 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"
|
||||
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
|
||||
// warning: Not all STM32H7 devices have the exact same pins here
|
||||
// for STM32H747XIH, replace p.PB13 for PG12
|
||||
let device = eth::Ethernet::new(
|
||||
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
|
||||
periphs.ETH,
|
||||
Irqs,
|
||||
periphs.PA1, // ref_clk
|
||||
periphs.PA7, // CRS_DV: Carrier Sense
|
||||
periphs.PC4, // RX_D0: Received Bit 0
|
||||
periphs.PC5, // RX_D1: Received Bit 1
|
||||
periphs.PG13, // TX_D0: Transmit Bit 0
|
||||
periphs.PB13, // TX_D1: Transmit Bit 1
|
||||
periphs.PG11, // TX_EN: Transmit Enable
|
||||
MAC_ADDRESS,
|
||||
periphs.ETH_SMA,
|
||||
periphs.PA2, // mdio
|
||||
periphs.PC1, // mdc
|
||||
);
|
||||
|
||||
shared_pool
|
||||
.grow(
|
||||
SUBPOOL_SMALL.get_mut().unwrap(),
|
||||
SUBPOOL_SMALL_SIZES.get_mut().unwrap(),
|
||||
SUBPOOL_SMALL_NUM_BLOCKS,
|
||||
true,
|
||||
)
|
||||
.expect("growing heapless memory pool failed");
|
||||
shared_pool
|
||||
.grow(
|
||||
SUBPOOL_MEDIUM.get_mut().unwrap(),
|
||||
SUBPOOL_MEDIUM_SIZES.get_mut().unwrap(),
|
||||
SUBPOOL_MEDIUM_NUM_BLOCKS,
|
||||
true,
|
||||
)
|
||||
.expect("growing heapless memory pool failed");
|
||||
shared_pool
|
||||
.grow(
|
||||
SUBPOOL_LARGE.get_mut().unwrap(),
|
||||
SUBPOOL_LARGE_SIZES.get_mut().unwrap(),
|
||||
SUBPOOL_LARGE_NUM_BLOCKS,
|
||||
true,
|
||||
)
|
||||
.expect("growing heapless memory pool failed");
|
||||
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
|
||||
|
||||
// Generate random seed.
|
||||
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
|
||||
let mut seed = [0; 8];
|
||||
rng.fill_bytes(&mut seed);
|
||||
let seed = u64::from_le_bytes(seed);
|
||||
|
||||
// Init network stack
|
||||
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||
let (stack, runner) =
|
||||
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
|
||||
|
||||
// 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(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||
|
||||
eth_link_check::spawn().expect("eth link check failed");
|
||||
static TC_CHANNEL: static_cell::ConstStaticCell<
|
||||
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TC_QUEUE_DEPTH>,
|
||||
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||
let tc_channel = TC_CHANNEL.take();
|
||||
let tc_sender = tc_channel.sender();
|
||||
let tc_receiver = tc_channel.receiver();
|
||||
|
||||
static TM_CHANNEL: static_cell::ConstStaticCell<
|
||||
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
|
||||
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||
let tm_channel = TM_CHANNEL.take();
|
||||
let tm_sender = tm_channel.sender();
|
||||
let tm_receiver = tm_channel.receiver();
|
||||
|
||||
net_lib_task::spawn().expect("spawning net library task failed");
|
||||
net_app_task::spawn().expect("spawning net application task 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");
|
||||
tc_handler::spawn().expect("spawning TC handler task failed");
|
||||
|
||||
(
|
||||
Shared {
|
||||
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
|
||||
eth_link_up: false,
|
||||
sockets,
|
||||
shared_pool,
|
||||
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||
sequence_count: u14::new(0),
|
||||
},
|
||||
Local {
|
||||
link_led,
|
||||
leds,
|
||||
net,
|
||||
udp,
|
||||
tc_source_rx,
|
||||
phy: lan8742a,
|
||||
net_runner: runner,
|
||||
net_stack: stack,
|
||||
tc_tx: tc_sender,
|
||||
tc_rx: tc_receiver,
|
||||
tm_tx: tm_sender,
|
||||
tm_rx: tm_receiver,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -430,94 +223,164 @@ mod app {
|
||||
leds.led1.toggle();
|
||||
leds.led2.toggle();
|
||||
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||
Mono::delay(current_blink_freq).await;
|
||||
Timer::after_millis(current_blink_freq.as_millis()).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;
|
||||
#[task(local=[net_runner])]
|
||||
async fn net_lib_task(cx: net_lib_task::Context) {
|
||||
cx.local.net_runner.run().await;
|
||||
}
|
||||
|
||||
#[task(local = [net_stack, link_led, tc_tx, tm_rx])]
|
||||
async fn net_app_task(cx: net_app_task::Context) {
|
||||
pub const MTU: usize = 1500;
|
||||
|
||||
// Ensure those are in the data section by making them static.
|
||||
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||
static_cell::ConstStaticCell::new([0; MTU]);
|
||||
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||
static_cell::ConstStaticCell::new([0; MTU]);
|
||||
|
||||
let rx_udp_meta = RX_UDP_META.take();
|
||||
let rx_udp_bufs = RX_UDP_BUFS.take();
|
||||
let tx_udp_meta = TX_UDP_META.take();
|
||||
let tx_udp_bufs = TX_UDP_BUFS.take();
|
||||
|
||||
let mut rx_buffer = [0; MTU];
|
||||
|
||||
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");
|
||||
cx.local.net_stack.wait_link_up().await;
|
||||
cx.local.link_led.set_high();
|
||||
defmt::info!("Network link is up");
|
||||
|
||||
// Ensure DHCP configuration is up before trying connect
|
||||
cx.local.net_stack.wait_config_up().await;
|
||||
|
||||
let config = cx.local.net_stack.config_v4();
|
||||
defmt::info!("Network task initialized, config: {}", config);
|
||||
|
||||
let mut udp = UdpSocket::new(
|
||||
cx.local.net_stack.clone(),
|
||||
rx_udp_meta,
|
||||
rx_udp_bufs,
|
||||
tx_udp_meta,
|
||||
tx_udp_bufs,
|
||||
);
|
||||
defmt::info!("UDP socket bound to port {}", PORT);
|
||||
udp.bind(PORT).expect("failed to bind UDP socket");
|
||||
let mut remote_endpoint = None;
|
||||
loop {
|
||||
if !cx.local.net_stack.is_link_up() {
|
||||
defmt::warn!("Network link is down");
|
||||
cx.local.link_led.set_low();
|
||||
break;
|
||||
}
|
||||
} 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");
|
||||
}
|
||||
Mono::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);
|
||||
})
|
||||
});
|
||||
Mono::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);
|
||||
}
|
||||
match udp
|
||||
.recv_from(&mut rx_buffer)
|
||||
.with_timeout(Duration::from_millis(200))
|
||||
.await
|
||||
{
|
||||
Ok(result) => match result {
|
||||
Ok((data, meta)) => {
|
||||
remote_endpoint = Some(meta.endpoint);
|
||||
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
|
||||
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
|
||||
}
|
||||
});
|
||||
Err(e) => {
|
||||
defmt::warn!("udp receive error: {}", e);
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
},
|
||||
Err(_e) => (),
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::warn!("TC source reception error: {}", e);
|
||||
if let Some(endpoint) = remote_endpoint {
|
||||
while let Ok(packet) = cx.local.tm_rx.try_receive() {
|
||||
match udp.send_to(&packet, endpoint).await {
|
||||
Ok(_) => {
|
||||
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
|
||||
}
|
||||
Err(e) => defmt::warn!("udp send error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])]
|
||||
async fn tc_handler(mut cx: tc_handler::Context) {
|
||||
loop {
|
||||
let tc = cx.local.tc_rx.receive().await;
|
||||
|
||||
match CcsdsPacketReader::new_with_checksum(&tc) {
|
||||
Ok(packet) => {
|
||||
let packet_id = packet.packet_id();
|
||||
let psc = packet.psc();
|
||||
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
|
||||
if let Ok(request) =
|
||||
postcard::from_bytes::<stm32h7::Request>(packet.packet_data())
|
||||
{
|
||||
let response = match request {
|
||||
stm32h7::Request::Ping => {
|
||||
defmt::info!("Received Ping request");
|
||||
stm32h7::Response::Ok
|
||||
}
|
||||
stm32h7::Request::ChangeBlinkFrequency(duration) => {
|
||||
defmt::info!(
|
||||
"Received blinky frequency change request: {} ms",
|
||||
duration.as_millis()
|
||||
);
|
||||
cx.shared.blink_freq.lock(|current| {
|
||||
*current = Duration::from_millis(duration.as_millis() as u64)
|
||||
});
|
||||
stm32h7::Response::Ok
|
||||
}
|
||||
};
|
||||
let sequence_count = cx.shared.sequence_count.lock(|v| {
|
||||
let current = *v;
|
||||
*v = v.wrapping_add(u14::new(1));
|
||||
current
|
||||
});
|
||||
|
||||
// Send Pong/OK response immediately.
|
||||
if let Err(e) =
|
||||
send_tm(tc_packet_id, response, sequence_count, cx.local.tm_tx).await
|
||||
{
|
||||
defmt::warn!("Failed to send TM response: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => defmt::warn!("Failed to parse received TC packet: {}", e,),
|
||||
}
|
||||
defmt::info!("Received from UDP client: {}", tc.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_tm(
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
response: stm32h7::Response,
|
||||
current_seq_count: u14,
|
||||
sender: &embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
) -> Result<(), CcsdsPacketCreationError> {
|
||||
let sp_header = SpHeader::new_for_unseg_tc(stm32h7::PUS_APID, current_seq_count, 0);
|
||||
let tm_header = TmHeader {
|
||||
tc_packet_id: Some(tc_packet_id),
|
||||
uptime_millis: embassy_time::Instant::now().as_millis(),
|
||||
};
|
||||
let tm_size = tm_size(&tm_header, &response);
|
||||
let mut packet = alloc::vec![0; tm_size];
|
||||
create_tm_packet(&mut packet, sp_header, tm_header, response)?;
|
||||
sender.send(packet).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use stm32h7_testapp as _; // memory layout + panic handler
|
||||
use satrs_stm32h7_nucleo_rtic as _; // memory layout + panic handler
|
||||
|
||||
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
|
||||
// feature)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/settings.json
|
||||
/.cortex-debug.*
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
// 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": []
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
// 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
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "embedded-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", default-features = false }
|
||||
defmt = { version = "1", optional = true }
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
|
||||
postcard = { version = "1", features = ["defmt"] }
|
||||
arbitrary-int = "2"
|
||||
@@ -0,0 +1,84 @@
|
||||
#![no_std]
|
||||
|
||||
use spacepackets::{
|
||||
CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, CcsdsPacketIdAndPsc,
|
||||
SpacePacketHeader, ccsds_packet_len_for_user_data_len_with_checksum,
|
||||
};
|
||||
|
||||
pub mod stm32f3 {
|
||||
use arbitrary_int::u11;
|
||||
use core::time::Duration;
|
||||
|
||||
pub const PUS_APID: u11 = u11::new(0x02);
|
||||
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
ChangeBlinkFrequency(Duration),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
}
|
||||
}
|
||||
|
||||
/// This might look like a duplication, but we intentionally keep those separate so they can
|
||||
/// change independently.
|
||||
pub mod stm32h7 {
|
||||
use arbitrary_int::u11;
|
||||
use core::time::Duration;
|
||||
|
||||
pub const PUS_APID: u11 = u11::new(0x03);
|
||||
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
ChangeBlinkFrequency(Duration),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct TmHeader {
|
||||
pub tc_packet_id: Option<CcsdsPacketIdAndPsc>,
|
||||
pub uptime_millis: u64,
|
||||
}
|
||||
|
||||
pub fn tm_size<Response: serde::Serialize>(tm_header: &TmHeader, response: &Response) -> usize {
|
||||
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||
postcard::experimental::serialized_size(tm_header).unwrap()
|
||||
+ postcard::experimental::serialized_size(response).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn create_tm_packet<Response: serde::Serialize>(
|
||||
buf: &mut [u8],
|
||||
sp_header: SpacePacketHeader,
|
||||
tm_header: TmHeader,
|
||||
response: Response,
|
||||
) -> Result<usize, CcsdsPacketCreationError> {
|
||||
let packet_data_size = postcard::experimental::serialized_size(&tm_header).unwrap()
|
||||
+ postcard::experimental::serialized_size(&response).unwrap();
|
||||
let mut creator =
|
||||
CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(sp_header, packet_data_size, buf)?;
|
||||
|
||||
let current_index = postcard::to_slice(&tm_header, creator.packet_data_mut())
|
||||
.unwrap()
|
||||
.len();
|
||||
postcard::to_slice(&response, &mut creator.packet_data_mut()[current_index..]).unwrap();
|
||||
Ok(creator.finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "satrs-gen"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
toml = "0.8"
|
||||
heck = "0.5"
|
||||
@@ -0,0 +1,34 @@
|
||||
[apid]
|
||||
Sched = 1
|
||||
GenericPus = 2
|
||||
Acs = 3
|
||||
Cfdp = 4
|
||||
Tmtc = 5
|
||||
Eps = 6
|
||||
|
||||
|
||||
[ids]
|
||||
[ids.Eps]
|
||||
Pcdu = 0
|
||||
Subsystem = 1
|
||||
|
||||
[ids.Tmtc]
|
||||
UdpServer = 0
|
||||
TcpServer = 1
|
||||
|
||||
[ids.GenericPus]
|
||||
PusEventManagement = 0
|
||||
PusRouting = 1
|
||||
PusTest = 2
|
||||
PusAction = 3
|
||||
PusMode = 4
|
||||
PusHk = 5
|
||||
|
||||
[ids.Sched]
|
||||
PusSched = 0
|
||||
|
||||
[ids.Acs]
|
||||
Subsystem = 1
|
||||
Assembly = 2
|
||||
Mgm0 = 3
|
||||
Mgm1 = 4
|
||||
@@ -0,0 +1,91 @@
|
||||
use heck::{ToShoutySnakeCase, ToSnakeCase};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use toml::{Value, map::Map};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
// Read the configuration file
|
||||
let config_str = fs::read_to_string("components.toml").expect("Unable to read file");
|
||||
let config: Value = toml::from_str(&config_str).expect("Unable to parse TOML");
|
||||
|
||||
let mut output = File::create("../satrs-example/src/ids.rs")?;
|
||||
|
||||
generate_rust_code(&config, &mut output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_enum_table(table_map: &Map<String, Value>) -> BTreeMap<u64, &str> {
|
||||
// Collect entries into a BTreeMap to sort them by key
|
||||
let mut sorted_entries: BTreeMap<u64, &str> = BTreeMap::new();
|
||||
|
||||
for (key, value) in table_map {
|
||||
if let Some(value) = value.as_integer() {
|
||||
if !(0..=0x7FF).contains(&value) {
|
||||
panic!("Invalid APID value: {}", value);
|
||||
}
|
||||
sorted_entries.insert(value as u64, key);
|
||||
}
|
||||
}
|
||||
sorted_entries
|
||||
}
|
||||
|
||||
fn generate_rust_code(config: &Value, writer: &mut impl Write) {
|
||||
writeln!(
|
||||
writer,
|
||||
"//! This is an auto-generated configuration module."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(writer, "use satrs::request::UniqueApidTargetId;").unwrap();
|
||||
writeln!(writer).unwrap();
|
||||
|
||||
// Generate the main module
|
||||
writeln!(
|
||||
writer,
|
||||
"#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumIter)]"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(writer, "pub enum Apid {{").unwrap();
|
||||
|
||||
// Generate constants for the main module
|
||||
if let Some(apid_table) = config.get("apid").and_then(Value::as_table) {
|
||||
let sorted_entries = sort_enum_table(apid_table);
|
||||
// Write the sorted entries to the writer
|
||||
for (value, key) in sorted_entries {
|
||||
writeln!(writer, " {} = {},", key, value).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(writer, "}}").unwrap();
|
||||
|
||||
// Generate ID tables.
|
||||
if let Some(id_tables) = config.get("ids").and_then(Value::as_table) {
|
||||
for (mod_name, table) in id_tables {
|
||||
let mod_name_as_snake = mod_name.to_snake_case();
|
||||
writeln!(writer).unwrap();
|
||||
writeln!(writer, "pub mod {} {{", mod_name_as_snake).unwrap();
|
||||
let sorted_entries = sort_enum_table(table.as_table().unwrap());
|
||||
writeln!(writer, " #[derive(Debug, Copy, Clone, PartialEq, Eq)]").unwrap();
|
||||
writeln!(writer, " pub enum Id {{").unwrap();
|
||||
// Write the sorted entries to the writer
|
||||
for (value, key) in &sorted_entries {
|
||||
writeln!(writer, " {} = {},", key, value).unwrap();
|
||||
}
|
||||
writeln!(writer, " }}").unwrap();
|
||||
writeln!(writer).unwrap();
|
||||
|
||||
for (_value, key) in sorted_entries {
|
||||
let key_shouting = key.to_shouty_snake_case();
|
||||
writeln!(
|
||||
writer,
|
||||
" pub const {}: super::UniqueApidTargetId = super::UniqueApidTargetId::new(super::Apid::{} as u16, Id::{} as u32);",
|
||||
key_shouting, mod_name, key
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(writer, "}}").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.23.2-->
|
||||
<!--Created by yEd 3.25.1-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
@@ -15,7 +15,6 @@
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0" xml:space="preserve"/>
|
||||
<node id="n0">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="360.0" width="479.0" x="771.3047672479152" y="458.0"/>
|
||||
@@ -39,7 +38,7 @@
|
||||
<y:Geometry height="177.64799999999997" width="200.75199999999973" x="1037.5527672479152" y="470.15200000000027"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.919921875" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.25241088867188" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -50,7 +49,7 @@
|
||||
<y:Geometry height="34.0" width="84.39999999999986" x="1068.8351781652768" y="508.2800000000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.638671875" x="23.380664062499818" xml:space="preserve" y="8.015625">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.37623596191406" x="24.011882019042787" xml:space="preserve" y="6.827942848205566">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -61,7 +60,7 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="550.4800000000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.453125" x="13.973437499999818" xml:space="preserve" y="8.015625">Magnetometer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.56465148925781" x="12.917674255370912" xml:space="preserve" y="6.827942848205566">Magnetometers<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -72,7 +71,7 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="594.9000000000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.83203125" x="15.783984374999818" xml:space="preserve" y="8.015625">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.24858093261719" x="17.075709533691224" xml:space="preserve" y="6.827942848205566">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -83,7 +82,7 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="783.4063563305535" y="545.2800000000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.931640625" x="17.234179687499932" xml:space="preserve" y="8.015625">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="81.62857055664062" x="19.38571472167962" xml:space="preserve" y="6.827942848205566">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -94,7 +93,7 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="840.5407126611072" y="677.8000000000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="105.05078125" x="7.674609374999932" xml:space="preserve" y="8.015625">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="96.96467590332031" x="11.717662048339776" xml:space="preserve" y="6.827942848205566">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -105,7 +104,7 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="1005.2814253222144" y="677.8000000000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.111328125" x="11.644335937499932" xml:space="preserve" y="8.015625">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.46463012695312" x="13.96768493652337" xml:space="preserve" y="6.827942848205566">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -116,7 +115,18 @@
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="931.6174253222144" y="775.5920000000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="38.740234375" x="40.82988281249993" xml:space="preserve" y="8.015625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.292236328125" x="42.05388183593743" xml:space="preserve" y="6.827942848205566">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n9">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="34.0" width="120.39999999999986" x="1074.8237583924094" y="555.3663917525774"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.56465148925781" x="12.917674255370912" xml:space="preserve" y="6.827942848205566">Magnetometers<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -153,7 +163,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.955078125" x="12.686124396959713" xml:space="preserve" y="-22.50440429687478">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.06060791015625" x="13.086737058292329" xml:space="preserve" y="-23.692086448669215">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -167,7 +177,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.923828125" x="-87.89792405764274" xml:space="preserve" y="-40.606550292968564">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.78047180175781" x="-85.32624589602165" xml:space="preserve" y="-42.388073520660214">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -185,10 +195,10 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n3" target="n7">
|
||||
<edge id="e5" source="n9" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Path sx="57.449660948066594" sy="-4.88639175257731" tx="0.0" ty="0.0">
|
||||
<y:Point x="1223.8814253222142" y="567.4800000000001"/>
|
||||
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||
</y:Path>
|
||||
@@ -207,7 +217,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="60.4140625" x="-2.4087265765670054" xml:space="preserve" y="145.1356018470808">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.73236083984375" x="-1.658628279667937" xml:space="preserve" y="143.94791969528637">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -221,7 +231,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="119.751953125" x="-132.27600022951788" xml:space="preserve" y="-32.03587548828091">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.32476806640625" x="-128.562407700221" xml:space="preserve" y="-33.22355764007534">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -235,7 +245,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.2421875" x="-62.15307370122309" xml:space="preserve" y="34.80927001953137">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.27665710449219" x="-58.67030850346919" xml:space="preserve" y="33.62158786773705">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -248,7 +258,7 @@
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.95703125" x="73.38950633588263" xml:space="preserve" y="-62.699758016200235">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.228179931640625" x="74.7630257963599" xml:space="preserve" y="-63.88744016799478">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,46 @@
|
||||
all: check build embedded test check-fmt clippy docs
|
||||
|
||||
check:
|
||||
cargo check
|
||||
cargo check -p satrs-example --no-default-features
|
||||
|
||||
build:
|
||||
cargo build
|
||||
|
||||
test:
|
||||
cargo nextest run --all-features
|
||||
cargo test --doc --all-features
|
||||
|
||||
embedded: embedded-stm32h7 embedded-stm32f3
|
||||
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
|
||||
|
||||
[working-directory:"embedded-examples/stm32h7-nucleo-rtic"]
|
||||
embedded-stm32h7:
|
||||
cargo build --target=thumbv7em-none-eabihf --release
|
||||
|
||||
[working-directory:"embedded-examples/stm32f3-disco-rtic"]
|
||||
embedded-stm32f3:
|
||||
cargo build --target=thumbv7em-none-eabihf --release
|
||||
|
||||
check-fmt:
|
||||
cargo fmt --all -- --check
|
||||
|
||||
fmt:
|
||||
cargo fmt --all
|
||||
|
||||
clippy:
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
docs-satrs:
|
||||
RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc -p satrs --all-features
|
||||
|
||||
docs: docs-satrs
|
||||
|
||||
[working-directory:"satrs-book"]
|
||||
book *args:
|
||||
mdbook build {{args}}
|
||||
|
||||
# Pass --dry-run to preview the changes first.
|
||||
[working-directory:"satrs-book"]
|
||||
deploy-book *args: book
|
||||
rsync -avz --delete {{args}} book/html/ numalfix@documentation.irs.uni-stuttgart.de:/home/numalfix/www/projects/sat-rs/book/
|
||||
@@ -1,7 +1,7 @@
|
||||
sat-rs book
|
||||
=========
|
||||
|
||||
High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||
High-level documentation of the [sat-rs project](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
[book]
|
||||
authors = ["Robin Mueller"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The sat-rs book"
|
||||
|
||||
[output.html]
|
||||
additional-js = ["mermaid.min.js", "mermaid-init.js"]
|
||||
|
||||
[output.linkcheck]
|
||||
command = "mdbook-linkcheck2"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.mermaid]
|
||||
command = "mdbook-mermaid"
|
||||
@@ -0,0 +1,39 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
(() => {
|
||||
const darkThemes = ['ayu', 'navy', 'coal'];
|
||||
const lightThemes = ['light', 'rust'];
|
||||
|
||||
const classList = document.getElementsByTagName('html')[0].classList;
|
||||
|
||||
let lastThemeWasLight = true;
|
||||
for (const cssClass of classList) {
|
||||
if (darkThemes.includes(cssClass)) {
|
||||
lastThemeWasLight = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const theme = lastThemeWasLight ? 'default' : 'dark';
|
||||
mermaid.initialize({ startOnLoad: true, theme });
|
||||
|
||||
// Simplest way to make mermaid re-render the diagrams in the new theme is via refreshing the page
|
||||
|
||||
for (const darkTheme of darkThemes) {
|
||||
document.getElementById('mdbook-theme-' + darkTheme).addEventListener('click', () => {
|
||||
if (lastThemeWasLight) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const lightTheme of lightThemes) {
|
||||
document.getElementById('mdbook-theme-' + lightTheme).addEventListener('click', () => {
|
||||
if (!lastThemeWasLight) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
Vendored
+2609
File diff suppressed because it is too large.
Load diff
@@ -1,17 +1,22 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Design](./design.md)
|
||||
|
||||
# Basic concepts and components
|
||||
|
||||
- [Communication with Space Systems](./communication.md)
|
||||
- [TMTC modelling](./tmtc-modelling.md)
|
||||
- [Working with Constrained Systems](./constrained-systems.md)
|
||||
- [Actions](./actions.md)
|
||||
- [Modes and Health](./modes-and-health.md)
|
||||
- [Housekeeping Data](./housekeeping.md)
|
||||
- [Events](./events.md)
|
||||
|
||||
# Architecture
|
||||
|
||||
- [System View](./system-view.md)
|
||||
- [Design](./design.md)
|
||||
|
||||
# Example project
|
||||
|
||||
- [The satrs-example application](./example.md)
|
||||
@@ -7,3 +7,7 @@
|
||||
- [Modelling space systems](./modelling-space-systems.md)
|
||||
- [Ground Segments](./ground-segments.md)
|
||||
|
||||
Refer to new sections in the system view page:
|
||||
|
||||
- [Fault Detection, Isolation and Recovery (FDIR)](./fdir.md)
|
||||
- and the [mode tree](./mode-tree.md)
|
||||
@@ -3,40 +3,9 @@
|
||||
Space systems generally need to be commanded regularly. This can include commands periodically
|
||||
required to ensure a healthy system, or commands to reach the mission goals.
|
||||
|
||||
These commands can be modelled using the concept of Actions. the ECSS PUS standard also provides
|
||||
the PUS service 8 for actions, but provides few concrete subservices and specification on how
|
||||
action commanding could look like.
|
||||
|
||||
`sat-rs` proposes two recommended ways to perform action commanding:
|
||||
|
||||
1. Target ID and Action ID based. The target ID is a 32-bit unsigned ID for an OBSW object entity
|
||||
which can also accept Actions. The action ID is a 32-bit unsigned ID for each action that a
|
||||
target is able to perform.
|
||||
2. Target ID and Action String based. The target ID is the same as in the first proposal, but
|
||||
the unique action is identified by a string.
|
||||
|
||||
The library provides an `ActionRequest` abstraction to model both of these cases.
|
||||
|
||||
## Commanding with ECSS PUS 8
|
||||
|
||||
`sat-rs` provides a generic ECSS PUS 8 action command handler. This handler can convert PUS 8
|
||||
telecommands which use the commanding scheme 1 explained above to an `ActionRequest` which is
|
||||
then forwarded to the target specified by the Target ID.
|
||||
|
||||
There are 3 requirements for the PUS 8 telecommand:
|
||||
|
||||
1. The subservice 128 must be used
|
||||
2. Bytes 0 to 4 of application data must contain the target ID in `u32` big endian format.
|
||||
3. Bytes 4 to 8 of application data must contain the action ID in `u32` big endian format.
|
||||
4. The rest of the application data are assumed to be command specific additional parameters. They
|
||||
will be added to an IPC store and the corresponding store address will be sent as part of the
|
||||
`ActionRequest`.
|
||||
|
||||
## Sending back telemetry
|
||||
|
||||
There are some cases where the regular verification provided by PUS in response to PUS action
|
||||
commands is not sufficient and some additional telemetry needs to be sent to ground. In that
|
||||
case, it is recommended to chose some custom subservice for action TM data and then send the
|
||||
telemetry using the same scheme as shown above, where the first 8 bytes of the application
|
||||
data is reserved for the target ID and action ID.
|
||||
These commands can be modelled using the concept of Actions. If you have not read the
|
||||
[TMTC modelling](./tmtc-modelling.md) chapter yet, it is recommended to read it first.
|
||||
|
||||
For a low number of actions, it is recommended to add the actions as `enum` variants of your
|
||||
`Request` type. For a higher number of actions, you can create a dedicated `ActionRequest`
|
||||
structure.
|
||||
@@ -2,25 +2,20 @@
|
||||
|
||||
# Communication with sat-rs based software
|
||||
|
||||
Communication is a vital topic for remote system which are usually not (directly)
|
||||
Communication is a vital topic for remote systems which are usually not (directly)
|
||||
connected to the internet and only have 1-2 communication links during nominal operation. However,
|
||||
most of these systems have internet access during development cycle. There are various standards
|
||||
provided by CCSDS and ECSS which can be useful to determine how to communicate with the satellite
|
||||
most of these systems have internet access during the development cycle. There are various standards
|
||||
provided by CCSDS which can be useful to determine how to communicate with the satellite
|
||||
and the primary On-Board Software.
|
||||
|
||||
# Application layer
|
||||
|
||||
Most communication with space systems is usually packet based. For example, the CCSDS space
|
||||
packet standard only specifies a 6 byte header with at least 1 byte payload. The PUS packet
|
||||
standard is a subset of the space packet standard, which adds some fields and a 16 bit CRC, but
|
||||
it is still centered around small packets. `sat-rs` provides support for these ECSS and CCSDS
|
||||
standards and also attempts to fill the gap to the internet protocol by providing the following
|
||||
components.
|
||||
packet standard only specifies a 6 byte header with at least 1 byte payload. The `sat-rs` library
|
||||
provides some support for the [CCSDS space packet protocol](https://ccsds.org/Pubs/133x0b2e2.pdf).
|
||||
|
||||
1. [UDP TMTC Server](https://docs.rs/satrs/latest/satrs/hal/std/udp_server/index.html).
|
||||
UDP is already packet based which makes it an excellent fit for exchanging space packets.
|
||||
2. [TCP TMTC Server Components](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/index.html).
|
||||
TCP is a stream based protocol, so the library provides building blocks to parse telemetry
|
||||
TCP is a stream based protocol, so the library provides building blocks to parse telecommands
|
||||
from an arbitrary bytestream. Two concrete implementations are provided:
|
||||
- [TCP spacepackets server](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
|
||||
to parse tightly packed CCSDS Spacepackets.
|
||||
@@ -31,28 +26,70 @@ components.
|
||||
# Working with telemetry and telecommands (TMTC)
|
||||
|
||||
The commands sent to a space system are commonly called telecommands (TC) while the data received
|
||||
from it are called telemetry (TM). Keeping in mind the previous section, the concept of a TC source
|
||||
and a TM sink can be applied to most satellites. The TM sink is the one entity where all generated
|
||||
telemetry arrives in real-time. The most important task of the TM sink usually is to send all
|
||||
arriving telemetry to the ground segment of a satellite mission immediately. Another important
|
||||
task might be to store all arriving telemetry persistently. This is especially important for
|
||||
space systems which do not have permanent contact like low-earth-orbit (LEO) satellites.
|
||||
from it are called telemetry (TM). One way to model the packet handling, which can be applied to most
|
||||
satellites, is to introduce the concept of a TC source and a TM sink. The TM sink is the one entity where
|
||||
all generated telemetry arrives in real-time. The most important task of the TM sink usually is to
|
||||
send all arriving telemetry to the ground segment of a satellite mission immediately.
|
||||
|
||||
Another important task might be to store all arriving telemetry persistently. This is especially
|
||||
important for space systems which do not have permanent contact like low-earth-orbit (LEO)
|
||||
satellites.
|
||||
|
||||
The diagram below shows one concrete example of what this could look like.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Dev[Device Handlers] --> Sink[TM Sink]
|
||||
Sub[Subsystem Handlers] --> Sink
|
||||
Sink --> Ground[Ground Link]
|
||||
Sink --> Store[Persistent Storage]
|
||||
Sink --> Udp[UDP Server]
|
||||
Sink --> Tcp[TCP Server]
|
||||
```
|
||||
|
||||
The most important task of a TC source is to deliver the telecommands to the correct recipients.
|
||||
For component oriented software using message passing, this usually includes staged demultiplexing
|
||||
components to determine where a command needs to be sent.
|
||||
For component oriented software using message passing, this usually includes demultiplexing
|
||||
to determine where a command needs to be sent.
|
||||
|
||||
The diagram below shows one concrete example of what this could look like.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Udp[UDP Server] --> Source[TC Source]
|
||||
Tcp[TCP Server] --> Source
|
||||
Radio[Radio Handler] --> Source
|
||||
Source --> Dev[Device Handlers]
|
||||
Source --> Sub[Subsystem Handlers]
|
||||
Source --> File[File Service Handler]
|
||||
```
|
||||
|
||||
Using a generic concept of a TC source and a TM sink as part of the software design simplifies
|
||||
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receiver only have to
|
||||
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receivers only have to
|
||||
forward their generated or received packets to those handler objects.
|
||||
|
||||
# Low-level protocols and the bridge to the communcation subsystem
|
||||
# Packet format
|
||||
|
||||
We talked about some basic support for the CCSDS space packet protocol. This is a really simple
|
||||
protocol which just specifies a header that every exchanged TMTC packet has:
|
||||
|
||||

|
||||
|
||||
This is a protocol which already provides us with some useful fields:
|
||||
|
||||
- ID field provided by the Application Process Identifier (APID). This can also be useful for packet
|
||||
multiplexing
|
||||
- Basic sequence counter which can be used to determine missed packets
|
||||
|
||||
However, what does the actual payload that we want to send to or from the satellite actually look
|
||||
like? We recommend a payload format which is created with the excellent [`serde`](https://serde.rs/)
|
||||
library. The [TMTC modelling](./tmtc-modelling.md) chapter provides more information.
|
||||
|
||||
# Low-level protocols and the bridge to the communication subsystem
|
||||
|
||||
Many satellite systems usually use the lower levels of the OSI layer in addition to the application
|
||||
layer covered by the PUS standard or the CCSDS space packets standard. This oftentimes requires
|
||||
special hardware like dedicated FPGAs to handle forward error correction fast enough. `sat-rs`
|
||||
might provide components to handle standard like the Unified Space Data Link Standard (USLP) in
|
||||
layer. This oftentimes requires special hardware like dedicated FPGAs to handle forward error
|
||||
correction fast enough. `sat-rs`
|
||||
might provide components to handle standards like the Unified Space Data Link Protocol (USLP) in
|
||||
software but most of the time the handling of communication is performed through custom
|
||||
software and hardware. Still, connecting this custom software and hardware to `sat-rs` can mostly
|
||||
be done by using the concept of TC sources and TM sinks mentioned previously.
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
Software for space systems oftentimes has different requirements than the software for host
|
||||
systems or servers. Currently, most space systems are considered embedded systems.
|
||||
|
||||
For these systems, the computation power and the available heap are important resources
|
||||
which are also constrained. This might make completeley heap based memory management schemes which
|
||||
are oftentimes used on host and server based systems unfeasable. Still, completely forbidding
|
||||
heap allocations might make software development unnecessarilly difficult, especially in a
|
||||
For these systems, the computation power and the available memory are important resources
|
||||
which are also constrained. This might make completely heap based memory management schemes which
|
||||
are oftentimes used on host and server based systems infeasible. Still, completely forbidding
|
||||
heap allocations might make software development unnecessarily difficult, especially in a
|
||||
time where the OBSW might be running on Linux based systems with hundreds of MBs of RAM.
|
||||
|
||||
A useful pattern commonly used in space systems is to limit heap allocations to program
|
||||
initialization time and avoid frequent run-time allocations. This prevents issues like
|
||||
running out of memory (something even Rust can not protect from) or heap fragmentation on systems
|
||||
without a MMU.
|
||||
without an MMU.
|
||||
|
||||
# Using an embedded allocator
|
||||
|
||||
The [`embedded-alloc`](https://github.com/rust-embedded/embedded-alloc) library provides
|
||||
a global allocator based on statically sized memory blocks. It also exposes an API
|
||||
which allows run-time tracking of the memory usage.
|
||||
|
||||
# Using pre-allocated pool structures
|
||||
|
||||
A candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
|
||||
A candidate for heap allocations is the TMTC handling. TC, TMs and IPC data are all
|
||||
candidates where the data size might vary greatly. The regular solution for host systems
|
||||
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
|
||||
another solution to avoid run-time allocations by offering pre-allocated static
|
||||
@@ -27,8 +33,8 @@ For example, a very small telecommand (TC) pool might look like this:
|
||||
|
||||
The core of the pool abstractions is the
|
||||
[PoolProvider trait](https://docs.rs/satrs/latest/satrs/pool/trait.PoolProvider.html).
|
||||
This trait specifies the general API a pool structure should have without making assumption
|
||||
of how the data is stored.
|
||||
This trait specifies the general API a pool structure should have without making assumptions
|
||||
about how the data is stored.
|
||||
|
||||
This trait is implemented by a static memory pool implementation.
|
||||
The code to generate this static pool would look like this:
|
||||
|
||||
+20
-16
@@ -2,38 +2,43 @@
|
||||
|
||||
Satellites and space systems in general are complex systems with a wide range of requirements for
|
||||
both the hardware and the software. Consequently, the general design of the library is centered
|
||||
around many light-weight components which try to impose as few restrictions as possible on how to
|
||||
solve certain problems. This is also the reason why sat-rs is explicitely called a library
|
||||
instead of a framework.
|
||||
around many light-weight components and a toolbox principle where you assemble everything
|
||||
you need instead of plugging something into a larger framework. This approach allows the largest
|
||||
amount of flexibility, including the operating system and platform choice. For example, `sat-rs`
|
||||
can be used both in `async` and regular synchronous platforms.
|
||||
|
||||
There are still a lot of common patterns and architectures across these systems where guidance
|
||||
of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls
|
||||
which were already solved and to avoid boilerplate code. This library tries to provide this
|
||||
structure and guidance the following way:
|
||||
|
||||
1. Providing this book which explains the architecture and design patterns in respect to common
|
||||
1. Providing this book which explains the architecture and design patterns with respect to common
|
||||
issues and requirements of space systems.
|
||||
2. Providing an example application. Space systems still commonly have large monolithic
|
||||
primary On-Board Softwares, so the choice was made to provide one example software which
|
||||
primary On-Board Software, so the choice was made to provide one example software which
|
||||
contains the various features provided by sat-rs.
|
||||
3. Providing a good test suite. This includes both unittests and integration tests. The integration
|
||||
3. Providing a good test suite. This includes both unit tests and integration tests. The integration
|
||||
tests can also serve as smaller usage examples than the large `satrs-example` application.
|
||||
|
||||
This library has special support for standards used in the space industry. This especially
|
||||
includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European
|
||||
Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards,
|
||||
but it is always recommended to use some sort of standard for interoperability.
|
||||
This library has special support for standards used in the space industry. The recommended
|
||||
standards are provided by the Consultative Committee for Space Data Systems (CCSDS):
|
||||
|
||||
- The CCSDS Space Packet Protocol as the basic packet format for telecommands and telemetry.
|
||||
- The CCSDS File Delivery Protocol (CFDP) for file transfers.
|
||||
|
||||
The library does not enforce using any of those standards, but it is always recommended to use
|
||||
some sort of standard for interoperability.
|
||||
|
||||
A lot of the modules and design considerations are based on the Flight Software Framework (FSFW).
|
||||
The FSFW has its own [documentation](https://documentation.irs.uni-stuttgart.de/fsfw/), which
|
||||
will be referred to when applicable. The FSFW was developed over a period of 10 years for the
|
||||
Flying Laptop Project by the University of Stuttgart with Airbus Defence and Space GmbH.
|
||||
It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||
It has flight heritage through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
|
||||
Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs`
|
||||
library.
|
||||
FLP is a medium-size small satellite with a higher budget and longer development time than EIVE,
|
||||
which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
|
||||
which allowed building a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
|
||||
shorter development cycle and was built using cheaper COTS components. This library also tries
|
||||
to accumulate the knowledge of developing the OBSW and operating the satellite for both these
|
||||
different systems and provide a solution for a wider range of small satellite systems.
|
||||
@@ -43,16 +48,15 @@ engineering to provide a reliable and robust basis for space On-Board Software.
|
||||
of using the Rust programming language was made for the following reasons:
|
||||
|
||||
1. Rust has safety guarantees which are a perfect fit for space systems which generally have high
|
||||
robustness and reliablity guarantees.
|
||||
robustness and reliability guarantees.
|
||||
2. Rust is suitable for embedded systems. It can also be run on smaller embedded systems like the
|
||||
STM32 which have also become common in the space sector. All space systems are embedded systems,
|
||||
which makes using large languages like Python challenging even for OBCs with more performance.
|
||||
3. Rust has support for linking C APIs through its excellent FFI support. This is especially
|
||||
important because many vendor provided libaries are still C based.
|
||||
4. Modern tooling like a package managers and various development helper, which can further reduce
|
||||
important because many vendor provided libraries are still C based.
|
||||
4. Modern tooling like a package manager and various development helpers, which can further reduce
|
||||
development cycles for space systems. `cargo` provides tools like auto-formatters and linters
|
||||
which can immediately ensure a high software quality throughout each development cycle.
|
||||
5. A large ecosystem with excellent libraries which also leverages the excellent tooling provided
|
||||
previously. Integrating these libraries is a lot easier compared to languages like C/C++ where
|
||||
there is still no standardized way to use packages.
|
||||
|
||||
+88
-16
@@ -1,24 +1,96 @@
|
||||
# Events
|
||||
|
||||
Events are an important mechanism used for remote systems to monitor unexpected
|
||||
or expected anomalies and events occuring on these systems.
|
||||
or expected anomalies and events occurring on these systems.
|
||||
They can improve the observability of a system significantly and provide a
|
||||
"paper trail" of what is happening or has happened on a satellite where regular
|
||||
housekeeping packets might not be sufficient. They can also be used for fault
|
||||
detection, isolation and recovery (FDIR) purposes. For example, higher level
|
||||
system objects can listen on certain high criticality events to initiate
|
||||
custom system responses.
|
||||
|
||||
One common use case for events on remote systems is to offer a light-weight publish-subscribe
|
||||
mechanism and IPC mechanism for software and hardware events which are also packaged as telemetry
|
||||
(TM) or can trigger a system response. They can also be tied to
|
||||
Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously.
|
||||
|
||||
The PUS Service 5 standardizes how the ground interface for events might look like, but does not
|
||||
specify how other software components might react to those events. There is the PUS Service 19,
|
||||
which might be used for that purpose, but the event components recommended by this framework do not
|
||||
rely on the present of this service.
|
||||
## Event Severity
|
||||
|
||||
The following images shows how the flow of events could look like in a system where components
|
||||
can generate events, and where other system components might be interested in those events:
|
||||
Generally, it also makes sense to classify events according to a severity system
|
||||
so operators can quickly judge the importance of an event. `sat-rs` does not
|
||||
constrain the severity classes or enforce their usage, but a severity
|
||||
classification like this can make sense:
|
||||
|
||||

|
||||
- INFO
|
||||
- LOW ERROR
|
||||
- MEDIUM ERROR
|
||||
- HIGH ERROR
|
||||
|
||||
For the concrete implementation of your own event management and/or event routing system, you
|
||||
can have a look at the event management documentation inside the
|
||||
[API documentation](https://docs.rs/satrs/latest/satrs/event_man/index.html) where you can also
|
||||
find references to all examples.
|
||||
## Modelling Events with Rust
|
||||
|
||||
Usually, events will be associated with certain software objects or handlers.
|
||||
Oftentimes, developers and operators want to supply parameters or metadata
|
||||
associated with an event. This can all be done using the Rust `enum` type.
|
||||
|
||||
Let's start with an example: a camera device
|
||||
handler might have the following events:
|
||||
|
||||
- Image taken event
|
||||
- Communication error event including an error classifier
|
||||
- Communication timeout event with the configured timeout
|
||||
- Overheating event
|
||||
|
||||
You can model these events using the following data structure, also including
|
||||
a `severity` method.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub enum Event {
|
||||
ImageTaken,
|
||||
CommunicationError(ErrorType),
|
||||
CommunicationTimeout(core::time::Duration),
|
||||
Overheating
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn severity(&self) -> Severity {
|
||||
match self {
|
||||
Event::ImageTaken => Severity::Info,
|
||||
Event::CommunicationError(_) => Severity::Low,
|
||||
Event::CommunicationTimeout(_) => Severity::Low,
|
||||
Event::Overheating => Severity::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Depending on the requirements of your system, you might want to filter which
|
||||
events are packaged and sent as telemetry. This requires an identification
|
||||
system. A simple scheme would be to add something like this:
|
||||
|
||||
```rust
|
||||
impl Event {
|
||||
pub fn id(&self) -> u32 {
|
||||
match self {
|
||||
Event::ImageTaken => 0,
|
||||
Event::CommunicationError(_) => 1,
|
||||
Event::CommunicationTimeout(_) => 2,
|
||||
Event::Overheating => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Handling events
|
||||
|
||||
When an event occurs in the system, you want to trigger the event.
|
||||
This usually includes sending the event to a centralized event funnel. The funnel
|
||||
takes care of packing the event into a telemetry packet as well as forwarding
|
||||
the event to any other objects which are interested in the event. A message
|
||||
queue system is the best solution for this. For example, on an embedded Linux
|
||||
system, you might have an event sender handle like this inside your camera
|
||||
device handler:
|
||||
|
||||
```rust
|
||||
pub struct CameraHandler {
|
||||
// (...)
|
||||
event_sender: std::sync::mpsc::SyncSender<Event>
|
||||
}
|
||||
```
|
||||
+69
-101
@@ -12,10 +12,59 @@ The example project contains components which could also be expected to be part
|
||||
On-Board Software. A structural diagram of the example application is given to provide
|
||||
a brief high-level view of the components used inside the example application:
|
||||
|
||||

|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph TMTC[TMTC Infrastructure]
|
||||
subgraph TMTCRow1[ ]
|
||||
direction LR
|
||||
Udp[UDP Server]
|
||||
Tcp[TCP Server]
|
||||
end
|
||||
subgraph TMTCRow2[ ]
|
||||
direction LR
|
||||
Source[TC Source]
|
||||
Sink[TM Sink]
|
||||
end
|
||||
end
|
||||
|
||||
The dotted lines are used to denote optional components. In this case, the static pool components
|
||||
are optional because the heap can be used as a simpler mechanism to store TMTC packets as well.
|
||||
subgraph AOCS[AOCS Stack]
|
||||
subgraph AOCSRow1[ ]
|
||||
direction LR
|
||||
Mgm0[MGM 0 Handler]
|
||||
Mgm1[MGM 1 Handler]
|
||||
Assy[MGM Assembly]
|
||||
end
|
||||
subgraph AOCSRow2[ ]
|
||||
direction LR
|
||||
AcsCtrl[ACS Controller]
|
||||
Mgt[MGT Handler]
|
||||
AcsSub[ACS Subsystem]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph EPS[EPS Stack]
|
||||
Pcdu[PCDU Handler]
|
||||
end
|
||||
|
||||
subgraph Core[Core]
|
||||
direction LR
|
||||
Ctrl[Core Controller]
|
||||
Evt[Event Manager]
|
||||
end
|
||||
|
||||
Sim[Sim Client]:::optional
|
||||
|
||||
TMTC ~~~ EPS
|
||||
AOCS ~~~ Core
|
||||
Core ~~~ Sim
|
||||
|
||||
classDef optional stroke-dasharray: 5 5;
|
||||
classDef invisible fill:none,stroke:none;
|
||||
class TMTCRow1,TMTCRow2,AOCSRow1,AOCSRow2 invisible;
|
||||
```
|
||||
|
||||
The dotted lines are used to denote optional components. In this case, the simulation client is
|
||||
optional because a dummy interface can be used instead to run the example without the simulator.
|
||||
Some additional explanation is provided for the various components.
|
||||
|
||||
### TCP/IP server components
|
||||
@@ -37,116 +86,35 @@ telecommands from the client.
|
||||
The most important components of the TMTC infrastructure include the following components:
|
||||
|
||||
- A TC source component which demultiplexes and routes telecommands based on parameters like
|
||||
packet APID or PUS service and subservice type.
|
||||
- A TM sink sink component which is the target of all sent telemetry and sends it to downlink
|
||||
packet APID and a target ID which is part of the packet payload.
|
||||
- A TM sink component which is the target of all sent telemetry and sends it to downlink
|
||||
handlers like the UDP and TCP server.
|
||||
|
||||
You can read the [Communications chapter](./communication.md) for more
|
||||
background information on the chosen TMTC infrastructure approach.
|
||||
|
||||
### PUS Service Components
|
||||
|
||||
A PUS service stack is provided which exposes some functionality conformant with the ECSS PUS
|
||||
services. This currently includes the following services:
|
||||
|
||||
- Service 1 for telecommand verification. The verification handling is handled locally: Each
|
||||
component which generates verification telemetry in some shape or form receives a
|
||||
[reporter](https://docs.rs/satrs/latest/satrs/pus/verification/struct.VerificationReporterWithSender.html)
|
||||
object which can be used to send PUS 1 verification telemetry to the TM funnel.
|
||||
- Service 3 for housekeeping telemetry handling.
|
||||
- Service 5 for management and downlink of on-board events.
|
||||
- Service 8 for handling on-board actions.
|
||||
- Service 11 for scheduling telecommands to be released at a specific time. This component
|
||||
uses the [PUS scheduler class](https://docs.rs/satrs/latest/satrs/pus/scheduler/alloc_mod/struct.PusScheduler.html)
|
||||
which performs the core logic of scheduling telecommands. All telecommands released by the
|
||||
scheduler are sent to the central TC source using a message.
|
||||
- Service 17 for test purposes like pings.
|
||||
|
||||
### Event Management Component
|
||||
|
||||
An event manager based on the sat-rs
|
||||
[event manager component](https://docs.rs/satrs/latest/satrs/event_man/index.html)
|
||||
is provided to handle the event IPC and FDIR mechanism. The event message are converted to PUS 5
|
||||
telemetry by the
|
||||
[PUS event dispatcher](https://docs.rs/satrs/latest/satrs/pus/event_man/alloc_mod/struct.PusEventDispatcher.html).
|
||||
|
||||
You can read the [events](./events.md) chapter for more in-depth information about event management.
|
||||
|
||||
### Sample Application Components
|
||||
|
||||
These components are example mission specific. They provide an idea how mission specific modules
|
||||
would look like the sat-rs context. It currently includes the following components:
|
||||
|
||||
- An Attitute and Orbit Control (AOCS) example task which can also process some PUS commands.
|
||||
|
||||
## Dataflow
|
||||
|
||||
The interaction of the various components is provided in the following diagram:
|
||||
### TMTC component group
|
||||
|
||||

|
||||
|
||||
It should be noted that an arrow coming out of a component group refers to multiple components
|
||||
in that group. An explanation for important component groups will be given.
|
||||
|
||||
#### TMTC component group
|
||||
|
||||
This groups is the primary interface for clients to communicate with the on-board software
|
||||
using a standardized TMTC protocol. The example uses the
|
||||
[ECSS PUS protocol](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
|
||||
This group is the primary interface for clients to communicate with the on-board software
|
||||
using the combination of CCSDS space packets and `serde` serialized payloads.
|
||||
In the future, this might be extended with the
|
||||
[CCSDS File Delivery Protocol](https://public.ccsds.org/Pubs/727x0b5.pdf).
|
||||
|
||||
A client can connect to the UDP or TCP server to send these PUS packets to the on-board software.
|
||||
These servers then forward the telecommads to a centralized TC source component using a dedicated
|
||||
message abstraction.
|
||||
A client can connect to the UDP or TCP server to send telecommands to the on-board software.
|
||||
These servers forward all telecommands to a centralized TC source component, which demultiplexes
|
||||
them and routes each one to its target component.
|
||||
|
||||
This TC source component then demultiplexes the message and forwards it to the relevant components.
|
||||
Right now, it forwards all PUS requests to the respective PUS service handlers using the PUS
|
||||
receiver component. The individual PUS services are running in a separate thread. In the future,
|
||||
additional forwarding to components like a CFDP handler might be added as well. It should be noted
|
||||
that PUS11 commands might contain other PUS commands which should be scheduled in the future.
|
||||
These wrapped commands are forwarded to the PUS11 handler. When the schedule releases those
|
||||
commands, it forwards the released commands to the TC source again. This allows the scheduler
|
||||
and the TC source to run in separate threads and keeps them cleanly separated.
|
||||
All telemetry generated by the on-board software is sent to a centralized TM sink. The core
|
||||
controller also forwards events to the event manager, which converts them into telemetry and
|
||||
sends it to the TM sink as well. The TM sink performs a demultiplexing step to forward all
|
||||
telemetry to the relevant recipients, which in the example case are the last connected UDP
|
||||
client and any connected TCP client.
|
||||
|
||||
All telemetry generated by the on-board software is sent to a centralized TM funnel. This component
|
||||
also performs a demultiplexing step to forward all telemetry to the relevant TM recipients.
|
||||
In the example case, this is the last UDP client, or a connected TCP client. In the future,
|
||||
forwarding to a persistent telemetry store and a simulated communication component might be
|
||||
added here as well. The centralized TM funnel also takes care of some packet processing steps which
|
||||
need to be applied for each ECSS PUS packet, for example CCSDS specific APID incrementation and
|
||||
PUS specific message counter incrementation.
|
||||
### Application Group
|
||||
|
||||
#### Application Group
|
||||
The application group contains some components you might also find in a real satellite software.
|
||||
This includes an AOCS stack with various device handlers and system level objects.
|
||||
|
||||
The application components generally do not receive raw PUS packets directly, even though
|
||||
this is certainly possible. Instead, they receive internalized messages from the PUS service
|
||||
handlers. For example, instead of receiving a PUS 8 Action Telecommand directly, an application
|
||||
component will receive a special `ActionRequest` message type reduced to the basic important
|
||||
information required to execute a request. These special requests are denoted by the blue arrow
|
||||
in the diagram.
|
||||
|
||||
It should be noted that the arrow pointing towards the event manager points in both directions.
|
||||
This is because the application components might be interested in events generated by other
|
||||
components as well. This mechanism is oftentimes used to implement the FDIR functionality on system
|
||||
and component level.
|
||||
|
||||
#### Shared components and functional interfaces
|
||||
|
||||
It should be noted that sometimes, a functional interface is used instead of a message. This
|
||||
is used for the generation of verification telemetry. The verification reporter is a clonable
|
||||
component which generates and sends PUS1 verification telemetry directly to the TM funnel. This
|
||||
introduces a loose coupling to the PUS standard but was considered the easiest solution for
|
||||
a project which utilizes PUS as the main communication protocol. In the future, a generic
|
||||
verification abstraction might be introduced to completely decouple the application layer from
|
||||
PUS.
|
||||
|
||||
The same concept is applied if the backing store of TMTC packets are shared pools. Every
|
||||
component which needs to read telecommands inside that shared pool or generate new telemetry
|
||||
into that shared pool will received a clonable shared handle to that pool.
|
||||
|
||||
The same concept could be extended to power or thermal handling. For example, a shared power helper
|
||||
component might be used to retrieve power state information and send power switch commands through
|
||||
a functional interface. The actual implementation of the functional interface might still use
|
||||
shared memory and/or messages, but the functional interface makes using and testing the interaction
|
||||
with these components easier.
|
||||
### Shared components and functional interfaces
|
||||
@@ -1 +1 @@
|
||||
# Fault Detecion, Isolation And Recovery (FDIR)
|
||||
# Fault Detection, Isolation And Recovery (FDIR)
|
||||
@@ -1,24 +1,107 @@
|
||||
# Housekeeping Data
|
||||
|
||||
Remote systems like satellites and rovers oftentimes generate data autonomously and periodically.
|
||||
The most common example for this is temperature or attitude data. Data like this is commonly
|
||||
referred to as housekeeping data, and is usually one of the most important and most resource heavy
|
||||
data sources received from a satellite. Standards like the PUS Service 3 make recommendation how to
|
||||
expose housekeeping data, but the applicability of the interface offered by PUS 3 has proven to be
|
||||
partially difficult and clunky for modular systems.
|
||||
If you have not read [the TMTC modelling chapter](./tmtc-modelling.md) yet, it is recommended to
|
||||
do that first.
|
||||
|
||||
First, we are going to list some assumption and requirements about Housekeeping (HK) data:
|
||||
Remote systems like satellites and rovers oftentimes generate data autonomously and periodically.
|
||||
An example for this could be temperature or attitude data. Data like this is commonly
|
||||
referred to as housekeeping data, and is usually one of the most important and most resource heavy
|
||||
data sources received from a satellite.
|
||||
|
||||
First, we are going to list some assumptions and requirements about Housekeeping (HK) data:
|
||||
|
||||
1. HK data is generated periodically by various system components throughout the
|
||||
systems.
|
||||
system.
|
||||
2. An autonomous and periodic sampling of that HK data to be stored and sent to Ground is generally
|
||||
required. A minimum interface consists of requesting a one-shot sample of HK, enabling and
|
||||
disabling the periodic autonomous generation of samples and modifying the collection interval
|
||||
of the periodic autonomous generation.
|
||||
3. HK data often needs to be shared to other software components. For example, a thermal controller
|
||||
3. HK data often needs to be shared with other software components. For example, a thermal controller
|
||||
wants to read the data samples of all sensor components.
|
||||
|
||||
A commonly required way to model HK data in a clean way is also to group related HK data into sets,
|
||||
which can then dumped via a similar interface.
|
||||
## Modelling our data
|
||||
|
||||
TODO: Write down `sat-rs` recommendations how to expose and work with HK data.
|
||||
Generally, it makes sense to model the data with Rust data structures for various reasons. For
|
||||
example, the sensor data received from a 3-axis magnetometer might be modelled like this:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MgmData {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub z: i16,
|
||||
}
|
||||
```
|
||||
|
||||
You can then re-use this data structure for various purposes. Also note the `serde` implementations,
|
||||
which are useful for generating the housekeeping data sent to ground.
|
||||
|
||||
We can model the housekeeping requests for a handler with a single data set like this:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum HkRequest {
|
||||
OneShot,
|
||||
EnablePeriodic,
|
||||
DisablePeriodic,
|
||||
ModifyInterval(core::time::Duration)
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
which might then be a part of a top level request type, e.g.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Hk(HkRequest)
|
||||
}
|
||||
```
|
||||
|
||||
A corresponding `Response` type might just include a HK data variant:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
Hk(MgmData)
|
||||
}
|
||||
```
|
||||
|
||||
If the software object managed multiple data sets, you could model it like this:
|
||||
|
||||
```rust
|
||||
/// Example set ID.
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SetId {
|
||||
Data,
|
||||
Config
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Hk {
|
||||
set_id: SetId,
|
||||
request: HkRequest
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sometimes, you need to share the generated data as well. Furthermore, it might make sense to
|
||||
decouple the HK generation from the data acquisition and only return the latest snapshot
|
||||
of the data. In this case, you can put the `MgmData` inside an appropriate lock structure for your
|
||||
platform/runtime to share it safely with other software components. For example, in a `std` system,
|
||||
you might simply use an `Arc<Mutex<MgmData>>` or an `Arc<RwLock<MgmData>>` for this.
|
||||
|
||||
Now, you can update that shared data structure when acquiring new data, and other software objects
|
||||
or the HK generation routine can safely read from it.
|
||||
|
||||
## Helper components
|
||||
|
||||
You need some application logic to track whether periodic data generation is enabled, what
|
||||
the current generation interval is and whether a HK set needs to be generated if the interval
|
||||
period has elapsed.
|
||||
|
||||
`sat-rs` provides some simple helper components for this inside the [`hk`](https://docs.rs/satrs/latest/satrs/hk/index.html) module. The module documentation contains more information.
|
||||
@@ -0,0 +1 @@
|
||||
*.bkp
|
||||
@@ -0,0 +1,99 @@
|
||||
<mxfile host="Electron">
|
||||
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
|
||||
<mxGraphModel dx="808" dy="1600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="Et7Is9MkKvAT9IauE9_I-4" connectable="0" parent="1" style="group" value="" vertex="1">
|
||||
<mxGeometry height="114" width="443" x="482" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-27" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
|
||||
<mxGeometry height="117" width="430" y="-3" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-46" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mission Logic</span>" vertex="1">
|
||||
<mxGeometry height="22" width="113.16" x="13" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-47" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">sat-rs</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="238" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="eFmRNKLwCo7I2zrsRzJH-1" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">spacepackets</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="113.16" x="13" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-1" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Events</span>" vertex="1">
|
||||
<mxGeometry height="22" width="86.24" x="136" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Et7Is9MkKvAT9IauE9_I-3" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">cfdp</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="231" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-7" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Device Drivers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="115" x="12.999999999999968" y="85" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-5" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mode Tree</span>" vertex="1">
|
||||
<mxGeometry height="22" width="105.48" x="135.99999999999997" y="85" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-6" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Controllers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="86.24" x="333" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-8" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">TMTC</span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="133.67" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wZbC1GPg-bVreQhl9PIS-20" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">FDIR</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="79" x="342" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
|
||||
<mxGeometry height="40" width="430" x="482" y="385" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-31" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;endArrow=classic;endFill=1;edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.506;entryY=0.034;entryDx=0;entryDy=0;entryPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="700" y="359" />
|
||||
<mxPoint x="700" y="370" />
|
||||
<mxPoint x="699.6" y="370" />
|
||||
</Array>
|
||||
<mxPoint x="700" y="370" as="sourcePoint" />
|
||||
<mxPoint x="700.16" y="420" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-32" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;fontColor=default;exitX=0.502;exitY=0.992;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;strokeColor=#969696;" target="mcy6J03i3kvHfejOpHHr-16">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="680" y="104.48999999999998" as="sourcePoint" />
|
||||
<mxPoint x="680.4399999999998" y="140.00000000000003" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;verticalAlign=middle;" value="" vertex="1">
|
||||
<mxGeometry height="219" width="432" x="482" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="mcy6J03i3kvHfejOpHHr-43" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform
async-centric, embassy or RTICv2" vertex="1">
|
||||
<mxGeometry height="219" width="432" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Logging<br></span><div><span style="font-weight: normal;"><font face="Courier New" style="font-size: 12px;">defmt + defmt-rtt</font></span></div>" vertex="1">
|
||||
<mxGeometry height="40" width="133.52063580591187" x="9.000332775608848" y="56.73391304347825" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Serialization<br><font face="Courier New" style="font-size: 12px;">serde + postcard</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="128" x="155" y="57.34" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Containers<br><font face="Courier New" style="font-size: 12px;">heapless</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="122" x="295" y="57" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Time<br><font style="font-size: 12px;" face="Courier New">embassy-time</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="131.43389604559735" x="9.004190097896517" y="104.99833992094861" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">IPC / Sync<br><font face="Courier New" style="font-size: 12px;">embassy-sync</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="130.29" x="153" y="105" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Memory<br><font face="Courier New" style="font-size: 12px;">embedded-alloc</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="123.45" x="296" y="105" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Tasks<br><font style="font-size: 12px;" face="Courier New">embassy-executor </font><font style="font-size: 12px;" face="Helvetica">or&nbsp;</font><font style="font-size: 12px;" face="Courier New">rtic</font></span>" vertex="1">
|
||||
<mxGeometry height="55" width="130.29303773935672" x="8.99793084216397" y="153" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Hardware API</span><div><span style="font-weight: normal;"><font style="font-size: 12px;">BSP + HAL + PAC</font></span></div>" vertex="1">
|
||||
<mxGeometry height="40" width="130.5" x="152" y="153" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Et7Is9MkKvAT9IauE9_I-2" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Filesystem</span><div><span style="font-weight: normal;"><font style="font-size: 12px;"><font face="Courier New">embedded-sdmmc</font> or<br><font face="Courier New">embedded-fatfs</font></font></span></div>" vertex="1">
|
||||
<mxGeometry height="55" width="123.45" x="296" y="153" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
@@ -0,0 +1,85 @@
|
||||
<mxfile host="Electron">
|
||||
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
|
||||
<mxGraphModel dx="975" dy="1703" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-27" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
|
||||
<mxGeometry height="122" width="426.02" x="490.98" y="-10" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
|
||||
<mxGeometry height="40" width="420" x="490.5" y="290" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-32" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;exitX=0.496;exitY=0.984;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;" target="mcy6J03i3kvHfejOpHHr-16">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="680" y="104.48999999999998" as="sourcePoint" />
|
||||
<mxPoint x="680.4399999999998" y="140.00000000000003" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;" value="" vertex="1">
|
||||
<mxGeometry height="124" width="423" x="490.5" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="mcy6J03i3kvHfejOpHHr-43" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform" vertex="1">
|
||||
<mxGeometry height="124" width="423" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Logging<br></span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="15" y="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Serialization</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="146.5" y="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Containers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="278.5" y="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">IPC / Sync</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="14.5" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Memory</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="278" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Tasks</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="146.5" y="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Hardware API</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="15" y="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="QQywTSC8lEcqjhdOS4oo-1" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Filesystem</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="146.5" y="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="<span style="font-weight: normal;">Time</span>" vertex="1">
|
||||
<mxGeometry height="22" width="117.5" x="278.5" y="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_ne4E4OgUf3ctT-MU7S6-3" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.506;entryY=0.031;entryDx=0;entryDy=0;fontColor=#000000;strokeColor=#969696;strokeWidth=2;exitX=0.503;exitY=1.009;exitDx=0;exitDy=0;entryPerimeter=0;exitPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="702.54" y="281.12" as="sourcePoint" />
|
||||
<mxPoint x="701.46" y="303" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="_ne4E4OgUf3ctT-MU7S6-4" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Command Scheduling</span>" vertex="1">
|
||||
<mxGeometry height="22" width="171" x="499" y="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-46" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mission Logic</span>" vertex="1">
|
||||
<mxGeometry height="22" width="105.48496240601504" x="498.9978345864661" y="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-47" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Events</span>" vertex="1">
|
||||
<mxGeometry height="22" width="94.67142857142858" x="719.0028571428571" y="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="nZ8_NfAIOoxuCbflcZYr-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">FDIR</span>" vertex="1">
|
||||
<mxGeometry height="22" width="80.57142857142857" x="824.0028571428571" y="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="nZ8_NfAIOoxuCbflcZYr-4" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">TMTC</span>" vertex="1">
|
||||
<mxGeometry height="22" width="94.67142857142858" x="614.0028571428571" y="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="932u3wCXrdYco77gi_Ax-1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mode Tree</span>" vertex="1">
|
||||
<mxGeometry height="22" width="105.48" x="680" y="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="932u3wCXrdYco77gi_Ax-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Controllers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="104.57" x="800" y="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="932u3wCXrdYco77gi_Ax-5" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Device Drivers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="115" x="499" y="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,100 @@
|
||||
<mxfile host="Electron">
|
||||
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
|
||||
<mxGraphModel dx="571" dy="1434" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;container=0;" value="" vertex="1">
|
||||
<mxGeometry height="190" width="428" x="482" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
|
||||
<mxGeometry height="40" width="430" x="482" y="352" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-31" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;entryX=0.52;entryY=-0.019;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=classic;endFill=1;edgeStyle=orthogonalEdgeStyle;exitX=0.518;exitY=1.002;exitDx=0;exitDy=0;exitPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="703.6" y="330.4" />
|
||||
</Array>
|
||||
<mxPoint x="700" y="370" as="sourcePoint" />
|
||||
<mxPoint x="680.36" y="340.24" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-27" connectable="0" parent="1" style="group" value=" " vertex="1">
|
||||
<mxGeometry height="139" width="443" x="481" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-14" connectable="0" parent="T22fa2qrIJ5I1ijH7CBO-27" style="group" value="" vertex="1">
|
||||
<mxGeometry height="114" width="443" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-15" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
|
||||
<mxGeometry height="117" width="430" y="-3" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-16" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mission Logic</span>" vertex="1">
|
||||
<mxGeometry height="22" width="113.16" x="13" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-17" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">sat-rs</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="238" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-18" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">spacepackets</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="113.16" x="14" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-19" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Events</span>" vertex="1">
|
||||
<mxGeometry height="22" width="86.24" x="136" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-20" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">cfdp</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="231" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-21" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Device Drivers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="114.16" x="13" y="85" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-22" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Mode Tree</span>" vertex="1">
|
||||
<mxGeometry height="22" width="105.48" x="135.99999999999997" y="85" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-23" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">Controllers</span>" vertex="1">
|
||||
<mxGeometry height="22" width="86.24" x="333" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-24" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;">TMTC</span>" vertex="1">
|
||||
<mxGeometry height="22" width="93" x="133.67" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-25" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="<span style="font-weight: normal;"><font face="Courier New">FDIR</font></span>" vertex="1">
|
||||
<mxGeometry height="22" width="79" x="342" y="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="T22fa2qrIJ5I1ijH7CBO-26" edge="1" parent="T22fa2qrIJ5I1ijH7CBO-27" source="T22fa2qrIJ5I1ijH7CBO-15" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;fontColor=default;exitX=0.502;exitY=0.992;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;strokeColor=#969696;">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="198" y="104.49000000000001" as="sourcePoint" />
|
||||
<mxPoint x="216" y="139" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="1" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform
Embedded Linux" vertex="1">
|
||||
<mxGeometry height="190" width="428" x="482" y="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Logging<br></span><div><span style="font-weight: normal;"><font face="Courier New" style="font-size: 12px;">log + fern</font></span></div>" vertex="1">
|
||||
<mxGeometry height="40" width="125" x="495" y="189.49" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Serialization<br><font face="Courier New" style="font-size: 12px;">serde + postcard</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="134.66" x="634" y="189.18367088607596" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Containers<br><font face="Courier New" style="font-size: 12px;">heapless + std</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="115" x="782.79" y="189.18" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Time<br><font style="font-size: 12px;" face="Courier New">std</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="125" x="494" y="236" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">IPC / Sync<br></span><div><span style="font-weight: normal;"><font style="font-size: 12px;" face="Courier New">std</font></span></div>" vertex="1">
|
||||
<mxGeometry height="40" width="135" x="633" y="236" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Memory<br><font face="Courier New" style="font-size: 12px;">alloc</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="113.79" x="782" y="235.99628691983122" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Tasks<br><font face="Courier New"><span style="font-size: 12px;">std</span></font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="125" x="495" y="283" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Hardware API</span><div><span style="font-weight: normal;"><font style="font-size: 12px;">Linux + <font face="Courier New">uio</font></font></span></div>" vertex="1">
|
||||
<mxGeometry height="40" width="137.7" x="633" y="282.9989029535865" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6sRxE-k6t311zxd993k4-5" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="<span style="font-weight: normal;">Filesystem<br><font face="Courier New" style="font-size: 12px;">std</font></span>" vertex="1">
|
||||
<mxGeometry height="40" width="113.79" x="782" y="283" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -4,8 +4,8 @@ The sat-rs book
|
||||
This book is the primary information resource for the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||
in addition to the regular API documentation. It contains the following resources:
|
||||
|
||||
1. Architecture informations and consideration which would exceeds the scope of the regular API.
|
||||
2. General information on how to build on-board Software and how `sat-rs` can help to fulfill
|
||||
1. Architecture information and considerations which would exceed the scope of the regular API.
|
||||
2. General information on how to build on-board software and how `sat-rs` can help to fulfill
|
||||
the unique requirements of writing software for remote systems.
|
||||
|
||||
# Introduction
|
||||
@@ -14,23 +14,22 @@ The primary goal of the sat-rs library 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.
|
||||
|
||||
It should be noted that sat-rs is early-stage software. Important features are missing. New releases
|
||||
with breaking changes are released regularly, with all changes documented inside respective
|
||||
changelog files. You should only use this library if your are willing to work in this
|
||||
environment.
|
||||
|
||||
A lot of the architecture and general design considerations are based on the
|
||||
Some architecture and general design considerations are based on the
|
||||
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
|
||||
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
|
||||
|
||||
However, `sat-rs` has a significantly reduced scope compared to those frameworks. Rust provides
|
||||
a great ecosystem and a powerful standard library which reduces the need for large and complex
|
||||
frameworks.
|
||||
|
||||
# Getting started with the example
|
||||
|
||||
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
||||
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
||||
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||
applicatin complements the example application and can be used to simulate some physical devices
|
||||
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/minisim)
|
||||
application complements the example application and can be used to simulate some physical devices
|
||||
for the `satrs-example` device handlers.
|
||||
|
||||
# Flight Heritage
|
||||
@@ -45,5 +44,5 @@ Currently this library has the following flight heritage:
|
||||
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)
|
||||
[Bachelor's 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/).
|
||||
@@ -5,9 +5,9 @@ system reasoning for both system operators and OBSW developers. They also provid
|
||||
the behaviour of a component and also provide observability of a system. A few examples of how to
|
||||
model the mode of different components within a space system with modes will be given.
|
||||
|
||||
## Pyhsical device component with modes
|
||||
## Physical device component with modes
|
||||
|
||||
The following simple mode scheme with the following three mode
|
||||
The following simple mode scheme with the following three modes
|
||||
|
||||
- `OFF`
|
||||
- `ON`
|
||||
@@ -18,10 +18,10 @@ sensors.
|
||||
|
||||
1. `OFF` means that a device is physically switched off, and the corresponding software component
|
||||
does not poll the device regularly.
|
||||
2. `ON` means that a device is pyhsically switched on, but the device is not polled perically.
|
||||
2. `ON` means that a device is physically switched on, but the device is not polled periodically.
|
||||
3. `NORMAL` means that a device is powered on and polled periodically.
|
||||
|
||||
If a devices is `OFF`, the device handler will deny commands which include physical communication
|
||||
If a device is `OFF`, the device handler will deny commands which include physical communication
|
||||
with the connected devices. In `NORMAL` mode, it will autonomously perform periodic polling
|
||||
of a connected physical device in addition to handling remote commands by the operator.
|
||||
Using these three basic modes, there are two important transitions which need to be taken care of
|
||||
@@ -73,8 +73,6 @@ In summary, a component which has modes has to expose the following 4 capabiliti
|
||||
3. Announce the mode
|
||||
4. Announce the mode recursively
|
||||
|
||||
## Using ECSS PUS to perform mode commanding
|
||||
|
||||
# Health
|
||||
|
||||
Health is an important concept for systems and components which might fail.
|
||||
@@ -94,10 +92,8 @@ use-cases:
|
||||
2. `FAULTY` means that a component does not work properly. This might also impact other system
|
||||
components, so the passivation and isolation of that component is desirable for FDIR purposes.
|
||||
3. `NEEDS RECOVERY` is used to attempt a recovery of a component. For example, a simple sensor
|
||||
could be power-cycled if there were multiple communication issues in the last time.
|
||||
could be power-cycled if there were multiple communication issues recently.
|
||||
4. `EXTERNAL CONTROL` is used to isolate an individual component from the rest of the system. For
|
||||
example, on operator might be interested in testing a component in isolation, and the interference
|
||||
example, an operator might be interested in testing a component in isolation, and the interference
|
||||
of the system is not desired. In that case, the `EXTERNAL CONTROL` health state might be used
|
||||
to prevent mode commands from the system while allowing external mode commands.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Serialization
|
||||
@@ -0,0 +1,55 @@
|
||||
# System View
|
||||
|
||||
This chapter gives a system level view of how a typical flight software built with `sat-rs`,
|
||||
[`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets) and
|
||||
[`cfdp`](https://egit.irs.uni-stuttgart.de/rust/cfdp) is layered. It complements the previous
|
||||
chapters, which focus on individual components, by showing how those components fit together
|
||||
and where the line between application and platform is usually drawn.
|
||||
|
||||
## Generic layering
|
||||
|
||||
Flight software built with `sat-rs` is generally structured into three layers.
|
||||
|
||||

|
||||
|
||||
- **Application**: The mission specific logic. This is the code a developer writes for a
|
||||
particular mission.
|
||||
- **System / platform**: The set of services the application is built on. This covers
|
||||
concepts like logging, serialization, IPC, task and memory management, hardware
|
||||
access, filesystem access and time. Most of these components are provided by external libraries
|
||||
and APIs.
|
||||
- **Hardware**: The physical target the software runs on.
|
||||
|
||||
The application layer stays largely the same across missions and targets. The system / platform
|
||||
layer is where the target environment determines which concrete crates and mechanisms are used.
|
||||
|
||||
The book has dedicated chapters for some of the topics:
|
||||
|
||||
- [TMTC handling and Serialization](./tmtc-modelling.md)
|
||||
- [Events](./events.md)
|
||||
- [Modes](./modes-and-health.md)
|
||||
|
||||
## Embedded Linux
|
||||
|
||||
On an embedded Linux target, the platform layer is provided by the Rust standard library and a
|
||||
small set of additional crates.
|
||||
|
||||

|
||||
|
||||
The application layer uses `sat-rs` together with `spacepackets` for CCSDS packet handling
|
||||
and `cfdp` for file transfer. The platform layer relies on `std` for tasks, IPC, memory, time and
|
||||
filesystem access, `serde` and `postcard` for serialization and `log`/`fern` for logging. Hardware
|
||||
access typically goes through Linux mechanisms like `uio`.
|
||||
|
||||
## Embedded async targets (Embassy / RTIC)
|
||||
|
||||
On smaller microcontrollers without an operating system, the platform layer looks quite
|
||||
different, even though the application layer stays the same.
|
||||
|
||||

|
||||
|
||||
Here the platform layer is built around an async-centric executor, either
|
||||
[Embassy](https://embassy.dev/) or [RTICv2](https://rtic.rs/). `no_std` crates like
|
||||
`heapless` and `embedded-alloc` replace `std` collections and allocation, `defmt` replaces `log`
|
||||
for logging and hardware access goes through a board support package (BSP), a hardware
|
||||
abstraction layer (HAL) and a peripheral access crate (PAC) instead of the OS.
|
||||
@@ -0,0 +1,86 @@
|
||||
# TMTC modelling using Rust
|
||||
|
||||
Before we talk about how to model telecommand and telemetry data using Rust, we are going
|
||||
to present some basic concepts and useful libraries first.
|
||||
|
||||
## Serialization
|
||||
|
||||
Serialization and deserialization is the process of converting (Rust) data structures into
|
||||
some format which can be stored or transmitted. We can use this system for generating the payload
|
||||
of our telecommand and telemetry packets. This allows us to model our payloads with Rust data
|
||||
structures, fits perfectly into the data-driven approach that Rust programs tend to favor and
|
||||
allows us to use the excellent type system.
|
||||
|
||||
The Rust ecosystem provides the [`serde`](https://serde.rs/) library for this task. The library
|
||||
makes it trivial to add serialization support to custom data structures by providing a
|
||||
[`derive`](https://serde.rs/derive.html) macro. In almost all cases, you can just add this derive
|
||||
macro to a data structure to make it serializable with any `serde` compatible serializer.
|
||||
|
||||
There are various serializers available which are well suited to the requirements of space systems.
|
||||
|
||||
- Generally, we try to minimize the payload size to save data bandwidth.
|
||||
- The data does not necessarily have to be human-readable
|
||||
|
||||
We recommend the [`postcard`](https://github.com/jamesmunns/postcard) serializer, which fulfills
|
||||
these requirements and also works well for embedded systems.
|
||||
|
||||
## Modelling telecommands and telemetry
|
||||
|
||||
Using a serializer library like `serde` allows us to do some interesting things. For example,
|
||||
let's assume you have a `Camera` object in software that you want to send some commands to.
|
||||
This object should have the following capabilities:
|
||||
|
||||
- Process a ping command
|
||||
- Capture an image
|
||||
- Send back configuration data
|
||||
|
||||
You can now model a request to your `Camera` object using the following data structure
|
||||
|
||||
```rust
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CameraRequest {
|
||||
Ping,
|
||||
CaptureImage,
|
||||
RequestConfig,
|
||||
}
|
||||
```
|
||||
|
||||
This data structure models all the requests that the `Camera` provides.
|
||||
On the telemetry side, you would have a similar object
|
||||
|
||||
```rust
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CameraResponse {
|
||||
Ok,
|
||||
Config(ConfigStructure)
|
||||
}
|
||||
```
|
||||
|
||||
where `ConfigStructure` would be some other wrapped configuration structure, and the `Ok` response
|
||||
would be the reply for successful execution for all other commands which do not have additional
|
||||
telemetry information.
|
||||
|
||||
Rust makes it trivial to move components into a new shared library. You can now put these data
|
||||
structures in a shared `types` or `data` library which can be re-used by both a ground system
|
||||
library and by the on-board software.
|
||||
|
||||
On the ground system, you could use a function like [`postcard::to_allocvec`](https://docs.rs/postcard/latest/postcard/fn.to_allocvec.html)
|
||||
to generate the byte representation of a `CameraRequest`, which is then sent as the payload
|
||||
inside a CCSDS space packet. On the on-board software side, you can use
|
||||
[`postcard::from_bytes`](https://docs.rs/postcard/latest/postcard/fn.from_bytes.html) to deserialize
|
||||
the `CameraRequest` from the raw payload bytes. In both cases, you do not need to hand-write
|
||||
the serialization and deserialization code anymore. The only trade-off is that you need a Rust
|
||||
conversion layer if you want to create your telecommands in another language like Python.
|
||||
|
||||
Using Rust structures like this also has other advantages. Once you have the `CameraRequest`
|
||||
structure, you can `match` on it to cover **all** commands that the device handler needs to cover.
|
||||
If you add a new variant, you have to handle it as well and you can not forget to handle a
|
||||
variant.
|
||||
|
||||
One trade-off to keep in mind is that a Rust `enum` will always have the size of its largest variant
|
||||
in memory. If you need to send large payloads to and from the on-board software, you can also
|
||||
add this data as a secondary data blob behind the primary `serde` payload, and still send something
|
||||
like small metadata as part of the payload. `postcard` can tell you the size of the deserialized
|
||||
payload which helps with determining the size of any additional payload data.
|
||||
|
||||
We recommend this approach for all TMTC definitions where you control all sides of the communication.
|
||||
+12
-15
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "satrs-example"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||
edition = "2024"
|
||||
default-run = "satrs-example"
|
||||
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||
@@ -18,26 +17,24 @@ csv = "1"
|
||||
num_enum = "0.7"
|
||||
thiserror = "2"
|
||||
lazy_static = "1"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
derive-new = "0.7"
|
||||
cfg-if = "1"
|
||||
arbitrary-int = "2"
|
||||
bitbybit = "2"
|
||||
postcard = "1"
|
||||
ctrlc = "3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[dependencies.satrs]
|
||||
path = "../satrs"
|
||||
features = ["test_util"]
|
||||
|
||||
[dependencies.satrs-minisim]
|
||||
path = "../satrs-minisim"
|
||||
|
||||
[dependencies.satrs-mib]
|
||||
version = "0.1.1"
|
||||
path = "../satrs-mib"
|
||||
satrs = { path = "../satrs", features = ["test_util"] }
|
||||
types = { path = "./types" }
|
||||
satrs-minisim = { path = "./minisim" }
|
||||
satrs-mib = { path = "../satrs-mib" }
|
||||
|
||||
[features]
|
||||
heap_tmtc = []
|
||||
default = ["heap_tmtc"]
|
||||
# default = ["heap_tmtc"]
|
||||
# heap_tmtc = []
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
+8
-45
@@ -5,7 +5,7 @@ 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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
|
||||
[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.
|
||||
@@ -26,58 +26,21 @@ cargo run --no-default-features
|
||||
|
||||
# Interacting with the sat-rs example
|
||||
|
||||
## Simple Client
|
||||
|
||||
The `simpleclient` binary target sends a
|
||||
ping telecommand and then verifies the telemetry generated by the example application.
|
||||
It can be run like this:
|
||||
|
||||
```rs
|
||||
cargo run --bin simpleclient
|
||||
```
|
||||
|
||||
This repository also contains a more complex client using the
|
||||
[Python tmtccmd](https://github.com/robamu-org/tmtccmd) module.
|
||||
|
||||
## <a id="tmtccmd"></a> Using the tmtccmd Python client
|
||||
|
||||
The python client requires a valid installation of the
|
||||
[tmtccmd package](https://github.com/robamu-org/tmtccmd).
|
||||
|
||||
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 interactively:
|
||||
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
|
||||
pip install -e .
|
||||
cargo run -p client -- --ping
|
||||
cargo run -p client -- mgm0 -m normal
|
||||
```
|
||||
|
||||
Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also
|
||||
install it manually with
|
||||
|
||||
```sh
|
||||
pip install -e .
|
||||
pip install tmtccmd[gui]
|
||||
```
|
||||
|
||||
After setting up the dependencies, you can simply run the `main.py` script to send commands
|
||||
to the OBSW example and to view and handle incoming telemetry. The script and the `tmtccmd`
|
||||
framework it uses allow to easily add and expose additional telecommand and telemetry handling
|
||||
as Python code. For example, you can use the following command to send a ping like done with
|
||||
the `simpleclient`:
|
||||
|
||||
```sh
|
||||
./main.py -p /test/ping
|
||||
```
|
||||
|
||||
You can also simply call the script without any arguments to view the command tree.
|
||||
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
|
||||
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/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.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[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" }
|
||||
serde_json = "1"
|
||||
satrs = { path = "../../satrs" }
|
||||
satrs-example = { path = ".." }
|
||||
satrs-minisim = { path = "../minisim" }
|
||||
types = { path = "../types" }
|
||||
spacepackets = { version = "0.18", default-features = false }
|
||||
bitbybit = "2"
|
||||
arbitrary-int = "2"
|
||||
ctrlc = { version = "3.5" }
|
||||
postcard = { version = "1" }
|
||||
anyhow = "1"
|
||||
@@ -0,0 +1,789 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use arbitrary_int::u11;
|
||||
use clap::Parser as _;
|
||||
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||
use satrs_minisim::{
|
||||
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,
|
||||
|
||||
#[command(subcommand)]
|
||||
commands: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Commands {
|
||||
Mgm0(MgmArgs),
|
||||
Mgm1(MgmArgs),
|
||||
MgmAssy(MgmAssemblyArgs),
|
||||
Mgt(MgtArgs),
|
||||
AcsSubsystem(SubsystemArgs),
|
||||
EventManager(EventManagerArgs),
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[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 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);
|
||||
}
|
||||
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::acs::mgm::request::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 addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let client = UdpSocket::bind("127.0.0.1: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),
|
||||
}
|
||||
}
|
||||
|
||||
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(&serde_json::to_vec(&ping)?, sim_addr)?;
|
||||
match sim_socket.recv(&mut reply_buf) {
|
||||
Ok(len) => {
|
||||
let reply: SimReply = serde_json::from_slice(&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(&serde_json::to_vec(&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());
|
||||
}
|
||||
}
|
||||
}
|
||||
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_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));
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,14 @@ serde_json = "1"
|
||||
log = "0.4"
|
||||
thiserror = "2"
|
||||
fern = "0.7"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
num_enum = "0.7"
|
||||
humantime = "2"
|
||||
tai-time = { version = "0.3", features = ["serde"] }
|
||||
nexosim = "1"
|
||||
|
||||
[dependencies.nexosim]
|
||||
version = "0.3.1"
|
||||
|
||||
[dependencies.satrs]
|
||||
path = "../satrs"
|
||||
satrs = { path = "../../satrs" }
|
||||
types = { path = "../types" }
|
||||
|
||||
[dev-dependencies]
|
||||
delegate = "0.13"
|
||||
@@ -2,7 +2,7 @@ sat-rs minisim
|
||||
======
|
||||
|
||||
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
|
||||
[asynchronix](https://github.com/asynchronics/asynchronix).
|
||||
[nexosim](https://github.com/asynchronics/nexosim).
|
||||
|
||||
Right now, this crate is primarily used together with the
|
||||
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||
@@ -29,4 +29,4 @@ 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"/>
|
||||
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
||||
@@ -0,0 +1,227 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use nexosim::{
|
||||
model::{Context, Model},
|
||||
ports::Output,
|
||||
};
|
||||
use satrs_minisim::{acs::mgm, SimReply};
|
||||
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: mgm::Reply::new(
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use satrs_minisim::{acs::mgm, SimReply, SimRequest};
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
use nexosim::{
|
||||
model::{schedulable, Context, Model},
|
||||
ports::Output,
|
||||
};
|
||||
use satrs_minisim::{
|
||||
acs::{mgm, mgt},
|
||||
SimReply,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use types::pcdu::SwitchStateBinary;
|
||||
|
||||
/// 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>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
cx.schedule_event(
|
||||
Duration::from_millis(15),
|
||||
schedulable!(Self::send_housekeeping_data),
|
||||
(),
|
||||
)
|
||||
.expect("requesting housekeeping data failed")
|
||||
}
|
||||
|
||||
#[nexosim(schedulable)]
|
||||
async fn send_housekeeping_data(&mut self) {
|
||||
self.reply
|
||||
.send(SimReply::from(mgt::Reply::Hk(mgt::HkSet {
|
||||
dipole: self.torque_dipole,
|
||||
torquing: self.torquing,
|
||||
})))
|
||||
.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 satrs_minisim::{
|
||||
acs::{mgm, mgt},
|
||||
eps::PcduRequest,
|
||||
SimReply, SimRequest, SimRequestWithTime,
|
||||
};
|
||||
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||
|
||||
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
|
||||
|
||||
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
|
||||
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
|
||||
let SimReply::Mgt(mgt::Reply::Hk(hk)) = sim_reply else {
|
||||
panic!("unexpected reply {sim_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(5)).unwrap();
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
},
|
||||
);
|
||||
let sim_reply = sim_testbench
|
||||
.try_receive_next_reply()
|
||||
.expect("no MGM reply received");
|
||||
let SimReply::Mgm { reply, .. } = sim_reply else {
|
||||
panic!("unexpected reply {sim_reply:?}");
|
||||
};
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 257 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user