Compare commits
294
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce016b1e3f | ||
|
|
1db716b166 | ||
|
|
2a5b8f95d3 | ||
|
|
26c0b042c9 | ||
|
|
e103702184 | ||
|
|
233e5abb23 | ||
|
|
f3e5e06937 | ||
|
|
7b33855ba1 | ||
|
|
8660dbae90 | ||
|
|
394ffec6c9 | ||
|
|
7cc2ff3ff9 | ||
|
|
bb8decfc74 | ||
|
|
23ac7cf0ad | ||
|
|
cb6c8b84bb | ||
|
|
390d7aaff5 | ||
|
|
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
|
||
|
|
b54c2b7863 | ||
|
|
19f3355283 | ||
|
|
4aeb28d2f1
|
||
|
|
ddc4544456 | ||
|
|
9ab36c0362 | ||
|
|
b95769c177 | ||
|
|
bb20533ae1 | ||
|
|
27cacd0f43
|
||
|
|
bd6488e87b | ||
|
|
52ec0d44aa | ||
|
|
4fa9f8d685 | ||
|
|
767cf6b1a5 | ||
|
|
5bf5518591
|
||
|
|
f3e0609910 | ||
|
|
05106354e3 | ||
|
|
719b70d834 | ||
|
|
d4082fa098 | ||
|
|
c9c9e4f735 | ||
|
|
2c92a95184
|
||
|
|
1a1d330814 | ||
|
|
ef7b5d66fe
|
||
|
|
b3497e0592 | ||
|
|
73286e36c2
|
||
|
|
ed266a11f6 | ||
|
|
af972e174f
|
||
|
|
3d12083c16
|
||
|
|
e8d2c020fa | ||
|
|
69e172b633 | ||
|
|
e1dda751bc
|
||
|
|
b01628d8ef | ||
|
|
31844e4fe2 | ||
|
|
738872f421 | ||
|
|
309e39999f
|
||
|
|
1c43c3adf9 | ||
|
|
d9e0abffa7 | ||
|
|
abec9dd448
|
||
|
|
c18fbb59ad | ||
|
|
c91ddcd658
|
||
|
|
c5fa1955d7 | ||
|
|
c9708810e6
|
||
|
|
79d0c2e222 | ||
|
|
8e87875c0e | ||
|
|
1ac6c02c06
|
||
|
|
6e7907522e | ||
|
|
f747a5efdc
|
||
|
|
6ffd55cec2 | ||
|
|
5e51b3de42
|
||
|
|
bd059a2541 | ||
|
|
1a4d764f25 | ||
|
|
b38c617fae | ||
|
|
edcd5491f1 | ||
|
|
b4cb034b73 | ||
|
|
47c86aea5c | ||
|
|
c8bba48e76 | ||
|
|
176da4838a
|
||
|
|
8114195bc6 | ||
|
|
1726e34fa7 | ||
|
|
1f2d6c9474 | ||
|
|
9960930339
|
||
|
|
cb270964a1 | ||
|
|
a45e634214
|
||
|
|
9e4132706c | ||
|
|
36d889a504
|
||
|
|
97a6510af7 | ||
|
|
2e5d6a5c41
|
||
|
|
29167736db
|
||
|
|
a4c433a7be | ||
|
|
472a8ce0f9 | ||
|
|
e753319dac | ||
|
|
99dddf36f3 | ||
|
|
a819feeaa2 | ||
|
|
46ce3fc772 | ||
|
|
8d27bdf3bf | ||
|
|
3d2a46f044 | ||
|
|
1f192af262
|
||
|
|
3f78c200ad
|
||
|
|
d73dfcdd67
|
||
|
|
5cae0f7036 | ||
|
|
832250d211
|
||
|
|
3c3b4349e8
|
||
|
|
acf73e93b1
|
||
|
|
0b2d4f6187 | ||
|
|
f7016b940a
|
||
|
|
397ecd0c40
|
||
|
|
422f2c11ab | ||
|
|
37e945fd91 | ||
|
|
45379858f0 | ||
|
|
7c194ab543 | ||
|
|
bca1d7292a
|
||
|
|
cdcb9cae1c | ||
|
|
9dcbd42862
|
||
|
|
da05efc16d
|
||
|
|
e38e25a998 | ||
|
|
14b381cf4a | ||
|
|
3746e9ebb0 | ||
|
|
d2fc783562 | ||
|
|
282f799203 | ||
|
|
46dbb4309b
|
||
|
|
42d1257e83
|
||
|
|
583f6ce4d2 | ||
|
|
408803fe86
|
||
|
|
9ffe4d0ae0 | ||
|
|
e37061dcf0
|
||
|
|
3a2ac11407 | ||
|
|
23327a7786
|
||
|
|
89d5a1022f | ||
|
|
a00c843698
|
||
|
|
c586fd7fef | ||
|
|
7e78e70a17
|
||
|
|
424dfc439c | ||
|
|
45eb2f1343
|
||
|
|
736eb74e66
|
||
|
|
29f71c2a57 | ||
|
|
f0d08b65a4 | ||
|
|
c7a74a844c | ||
|
|
9c60427f89 | ||
|
|
958ab9bab6
|
||
|
|
312849bddb | ||
|
|
b0159a3ba7
|
||
|
|
c477739f6d
|
||
|
|
b7ce039406
|
||
|
|
4736d40997 | ||
|
|
5ec5124ea3
|
||
|
|
5e43259d4f | ||
|
|
bfaddd0ebb | ||
|
|
423a068736 | ||
|
|
8022af1bf2 | ||
|
|
acd2260dfd | ||
|
|
e5ee698dc4 | ||
|
|
e8907c74d4
|
||
|
|
536051e05b | ||
|
|
701db659e9 | ||
|
|
4b8e54b91b
|
||
|
|
870d60cfd6 | ||
|
|
9e62e4292c
|
||
|
|
b2e77fbc09 | ||
|
|
5371928496
|
||
|
|
31cddbd99b | ||
|
|
7c00e13e70 | ||
|
|
aa72063454 | ||
|
|
7b37b76695 | ||
|
|
ea5d95c12d | ||
|
|
c62adbb300 | ||
|
|
9242b8a607 | ||
|
|
4a27d2605d
|
||
|
|
8195245481
|
||
|
|
975cd927f4 | ||
|
|
9039c1b59a | ||
|
|
972bf19188
|
||
|
|
9d711d2b73
|
||
|
|
d0005cdd63
|
||
|
|
f00e6cf50c
|
No files matched your search
@@ -0,0 +1,75 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check build
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install libudev-dev on Ubuntu
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: sudo apt update && sudo apt install -y libudev-dev
|
||||
- run: cargo check
|
||||
# Check example with static pool configuration
|
||||
- run: cargo check -p example-std --no-default-features
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
|
||||
cross-check:
|
||||
name: Check Cross-Compilation
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- armv7-unknown-linux-gnueabihf
|
||||
- thumbv7em-none-eabihf
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
|
||||
- run: cargo check -p satrs --target=${{matrix.target}} --no-default-features
|
||||
|
||||
fmt:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
docs:
|
||||
name: Check Documentation Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@@ -2,6 +2,7 @@ target/
|
||||
|
||||
output.log
|
||||
/Cargo.lock
|
||||
output.log
|
||||
|
||||
output.log
|
||||
|
||||
|
||||
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>
|
||||
+7
-4
@@ -3,12 +3,15 @@ resolver = "2"
|
||||
members = [
|
||||
"satrs",
|
||||
"satrs-mib",
|
||||
"satrs-example",
|
||||
"satrs-minisim",
|
||||
"examples/example-std",
|
||||
"examples/types",
|
||||
"examples/client",
|
||||
"examples/minisim",
|
||||
"examples/minisim-types",
|
||||
"satrs-shared",
|
||||
"tmtc-utils",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"satrs-example-stm32f3-disco",
|
||||
"examples/embedded",
|
||||
]
|
||||
|
||||
@@ -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://absatsw.irs.uni-stuttgart.de/projects/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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
||||
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
|
||||
@@ -26,33 +26,64 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
|
||||
|
||||
# Overview
|
||||
|
||||
This project currently contains following crates:
|
||||
This project currently contains the 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.
|
||||
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
|
||||
Example of a simple example on-board software using various sat-rs components which can be run
|
||||
on a host computer or on any system with a standard runtime like a Raspberry Pi.
|
||||
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
|
||||
Components to build a mission information base from the on-board software directly.
|
||||
* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example-stm32f3-disco):
|
||||
Example of a simple example using low-level sat-rs components on a bare-metal system
|
||||
with constrained resources.
|
||||
|
||||
Each project has its own `CHANGELOG.md`.
|
||||
## Examples
|
||||
|
||||
All examples and their helper crates are located inside the
|
||||
[`examples`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples) folder:
|
||||
|
||||
* [`example-std`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std):
|
||||
Example on-board software using various sat-rs components which can be run on a host computer
|
||||
or on any system with a standard runtime like a Raspberry Pi.
|
||||
* [`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim):
|
||||
Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which
|
||||
simulates some physical devices for the `example-std` application device handlers.
|
||||
* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application and the STM32H7 examples.
|
||||
* [`types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/types): Telecommand and telemetry definitions shared by the
|
||||
`example-std` application, the STM32H7 examples and the `client`.
|
||||
* [`embedded`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded):
|
||||
Simple examples using sat-rs components on a bare-metal system with constrained resources,
|
||||
running on the NUCLEO-H753ZI board. `stm32h7-nucleo-rtic` uses the
|
||||
[RTIC](https://github.com/rtic-rs/rtic) framework and `stm32h7-nucleo-embassy` uses the
|
||||
[embassy](https://embassy.dev/) executor.
|
||||
|
||||
The library crates and the `example-std` application have their own `CHANGELOG.md`.
|
||||
|
||||
# Related projects
|
||||
|
||||
|
||||
In addition to the crates in this repository, the sat-rs project also maintains other libraries.
|
||||
|
||||
* [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS
|
||||
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
|
||||
|
||||
There is an active and continuous effort to get early flight heritage for the sat-rs library.
|
||||
Currently this library has the following flight heritage:
|
||||
|
||||
- Submission as an [OPS-SAT experiment](https://www.esa.int/Enabling_Support/Operations/OPS-SAT)
|
||||
which has also
|
||||
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
|
||||
The application is strongly based on the sat-rs example application. You can find the repository
|
||||
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
|
||||
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
|
||||
alongside a Flight System Simulator in the context of a
|
||||
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
|
||||
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
|
||||
|
||||
# Coverage
|
||||
|
||||
@@ -64,5 +95,5 @@ rustup component add llvm-tools-preview
|
||||
cargo install grcov --locked
|
||||
```
|
||||
|
||||
After that, you can simply run `coverage.py` to test the `satrs-core` crate with coverage. You can
|
||||
After that, you can simply run `coverage.py` to test the `satrs` crate with coverage. You can
|
||||
optionally supply the `--open` flag to open the coverage report in your webbrowser.
|
||||
Vendored
+1
@@ -33,6 +33,7 @@ pipeline {
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh 'cargo nextest r --all-features'
|
||||
sh 'cargo test --doc --all-features'
|
||||
}
|
||||
}
|
||||
stage('Check with all features') {
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ def main():
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--package",
|
||||
choices=["satrs", "satrs-minisim", "satrs-example"],
|
||||
choices=["satrs", "minisim", "example-std"],
|
||||
default="satrs",
|
||||
help="Choose project to generate coverage for",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
|
||||
cargo +nightly doc --all-features --open
|
||||
@@ -0,0 +1 @@
|
||||
/config.toml
|
||||
@@ -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", features = ["derive"] }
|
||||
toml = "1"
|
||||
satrs = { path = "../../satrs" }
|
||||
example-std = { path = "../example-std" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
types = { path = "../types" }
|
||||
spacepackets = { version = "0.18", default-features = false }
|
||||
bitbybit = "2"
|
||||
arbitrary-int = "2"
|
||||
ctrlc = { version = "3.5" }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
anyhow = "1"
|
||||
@@ -0,0 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let config = manifest_dir.join("config.toml");
|
||||
let config_template = manifest_dir.join("config.toml.template");
|
||||
|
||||
if !config.exists() {
|
||||
fs::copy(&config_template, &config).unwrap();
|
||||
}
|
||||
println!("cargo::rerun-if-changed=config.toml.template");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copied to config.toml by the build script if config.toml does not exist yet. config.toml is not
|
||||
# tracked by git, so it can hold local settings like the address of a development board.
|
||||
|
||||
[interface]
|
||||
# Address of the commanded application. Defaults to the example-std OBSW on the local host.
|
||||
# Can be overridden with the --udp-addr argument.
|
||||
# udp_addr = "192.168.1.50:7301"
|
||||
@@ -0,0 +1,945 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use arbitrary_int::u11;
|
||||
use clap::Parser as _;
|
||||
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||
use minisim_types::{
|
||||
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
|
||||
udp::SIM_CTRL_PORT,
|
||||
};
|
||||
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub struct Cli {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
#[arg(short, long)]
|
||||
test_event: bool,
|
||||
/// Address of the commanded application. Overrides the address inside `config.toml`.
|
||||
#[arg(long, global = true)]
|
||||
udp_addr: Option<SocketAddr>,
|
||||
|
||||
#[command(subcommand)]
|
||||
commands: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct Config {
|
||||
#[serde(default)]
|
||||
interface: InterfaceConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct InterfaceConfig {
|
||||
/// Defaults to the example-std OBSW on the local host.
|
||||
udp_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// The build script creates `config.toml` from the template. A missing file is still accepted,
|
||||
/// because all parameters have defaults.
|
||||
fn load() -> anyhow::Result<Self> {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => toml::from_str(&content)
|
||||
.with_context(|| format!("parsing {} failed", path.display())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
||||
Err(e) => Err(e).with_context(|| format!("reading {} failed", path.display())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Commands {
|
||||
Mgm0(MgmArgs),
|
||||
Mgm1(MgmArgs),
|
||||
MgmAssy(MgmAssemblyArgs),
|
||||
Mgt(MgtArgs),
|
||||
AcsSubsystem(SubsystemArgs),
|
||||
EventManager(EventManagerArgs),
|
||||
/// Blinking LEDs of the embedded examples.
|
||||
Led(LedArgs),
|
||||
Sim(SimArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct EventManagerArgs {
|
||||
#[command(subcommand)]
|
||||
action: EventFilterAction,
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct SimArgs {
|
||||
#[command(subcommand)]
|
||||
action: SimAction,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum SimAction {
|
||||
/// Let the OBSW connect to the minisim.
|
||||
Connect {
|
||||
/// IP address of the minisim host. Defaults to the address of this client if nothing
|
||||
/// is specified.
|
||||
ip: Option<Ipv4Addr>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum EventFilterAction {
|
||||
/// Enable event TM generation.
|
||||
Enable(EventFilterArgs),
|
||||
/// Disable event TM generation.
|
||||
Disable(EventFilterArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
struct EventFilterArgs {
|
||||
#[arg(value_enum)]
|
||||
component: EventSenderSelect,
|
||||
/// Raw event ID. Without it, the filter applies to all events of the component.
|
||||
#[arg(short, long)]
|
||||
event_id: Option<u16>,
|
||||
}
|
||||
|
||||
/// Components which emit events.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum EventSenderSelect {
|
||||
Controller,
|
||||
Mgm0,
|
||||
Mgm1,
|
||||
MgmAssy,
|
||||
Mgt,
|
||||
Pcdu,
|
||||
UdpServer,
|
||||
TcpServer,
|
||||
Ground,
|
||||
}
|
||||
|
||||
impl From<EventSenderSelect> for types::ComponentId {
|
||||
fn from(sender: EventSenderSelect) -> Self {
|
||||
match sender {
|
||||
EventSenderSelect::Controller => types::ComponentId::Controller,
|
||||
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
|
||||
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
|
||||
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
|
||||
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
|
||||
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
|
||||
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
|
||||
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
|
||||
EventSenderSelect::Ground => types::ComponentId::Ground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum FaultMode {
|
||||
None,
|
||||
/// SPI communication is all zeroes, modelling an unconnected sensor.
|
||||
AllZeros,
|
||||
/// SPI communication is all ones, modelling a broken sensor.
|
||||
AllOnes,
|
||||
}
|
||||
|
||||
impl From<FaultMode> for mgm::SpiFaultMode {
|
||||
fn from(mode: FaultMode) -> Self {
|
||||
match mode {
|
||||
FaultMode::None => mgm::SpiFaultMode::None,
|
||||
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
|
||||
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum FaultKind {
|
||||
/// Cleared when the device is switched off, so a power cycle recovers from it.
|
||||
Transient,
|
||||
/// Survives power cycles.
|
||||
#[default]
|
||||
Permanent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum HkSelect {
|
||||
OneShot,
|
||||
EnablePeriodic,
|
||||
DisablePeriodic,
|
||||
ModifyInterval,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum HealthStateSelect {
|
||||
Healthy,
|
||||
Faulty,
|
||||
PermanentFaulty,
|
||||
ExternalControl,
|
||||
NeedsRecovery,
|
||||
}
|
||||
|
||||
impl From<HealthStateSelect> for satrs::health::HealthState {
|
||||
fn from(state: HealthStateSelect) -> Self {
|
||||
match state {
|
||||
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
|
||||
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
|
||||
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
|
||||
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
|
||||
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgmArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Housekeeping request for the sensor data set.
|
||||
#[arg(long, value_enum)]
|
||||
hk: Option<HkSelect>,
|
||||
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||
#[arg(long)]
|
||||
hk_interval_ms: Option<u64>,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
|
||||
#[arg(long, value_enum)]
|
||||
fault: Option<FaultMode>,
|
||||
/// Whether a power cycle clears the injected SPI fault.
|
||||
#[arg(long, value_enum, default_value_t)]
|
||||
fault_kind: FaultKind,
|
||||
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
|
||||
/// the handler after the underlying issue has been fixed or worked around.
|
||||
#[arg(long, value_enum)]
|
||||
health: Option<HealthStateSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgtArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Housekeeping request for the status data set.
|
||||
#[arg(long, value_enum)]
|
||||
hk: Option<HkSelect>,
|
||||
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||
#[arg(long)]
|
||||
hk_interval_ms: Option<u64>,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
|
||||
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
|
||||
torque: Option<types::acs::mgt::Dipole>,
|
||||
#[arg(long, default_value_t = 1000)]
|
||||
torque_duration_ms: u64,
|
||||
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
|
||||
#[arg(long, value_enum)]
|
||||
health: Option<HealthStateSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct LedArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Mode of the red and the orange LED.
|
||||
#[arg(short, long, value_enum)]
|
||||
mode: Option<LedModeSelect>,
|
||||
/// Toggle period of the toggle modes.
|
||||
#[arg(long, default_value_t = 500)]
|
||||
toggle_period_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum LedModeSelect {
|
||||
AllOff,
|
||||
RedOn,
|
||||
OrangeOn,
|
||||
AlternatingToggle,
|
||||
UnifiedToggle,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgmAssemblyArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
#[arg(short, long)]
|
||||
mode: Option<AssemblyModeSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct SubsystemArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
#[arg(short, long)]
|
||||
mode: Option<SubsystemModeSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum DeviceModeSelect {
|
||||
Off,
|
||||
Normal,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum AssemblyModeSelect {
|
||||
NoModeKeeping,
|
||||
Off,
|
||||
Normal,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum SubsystemModeSelect {
|
||||
Off,
|
||||
Safe,
|
||||
}
|
||||
|
||||
fn hk_request_type(
|
||||
hk: HkSelect,
|
||||
hk_interval_ms: Option<u64>,
|
||||
) -> anyhow::Result<types::HkRequestType> {
|
||||
let opt_interval = hk_interval_ms.map(Duration::from_millis);
|
||||
Ok(match hk {
|
||||
HkSelect::OneShot => types::HkRequestType::OneShot,
|
||||
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
|
||||
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
|
||||
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
|
||||
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
|
||||
let axes: Vec<i16> = value
|
||||
.split(',')
|
||||
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let [x, y, z] = axes[..] else {
|
||||
return Err(format!("expected 3 values, got {}", axes.len()));
|
||||
};
|
||||
Ok(types::acs::mgt::Dipole { x, y, z })
|
||||
}
|
||||
|
||||
fn send_mgt_request(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
request: types::acs::mgt::request::Request,
|
||||
) {
|
||||
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
log::info!(
|
||||
"sending MGT request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::request::Request) {
|
||||
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Led, request.message_type()),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
log::info!(
|
||||
"sending LED request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn handle_sim_command(client: &UdpSocket, addr: SocketAddr, args: SimArgs) {
|
||||
let request = match args.action {
|
||||
SimAction::Connect { ip } => types::control::request::Request::SimConnect(ip),
|
||||
};
|
||||
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Controller, MessageType::Action),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
log::info!(
|
||||
"sending SIM request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) {
|
||||
use types::led::request::Request;
|
||||
|
||||
if args.ping {
|
||||
send_led_request(client, addr, Request::Ping);
|
||||
}
|
||||
if let Some(mode) = args.mode {
|
||||
let toggle_period = Duration::from_millis(args.toggle_period_ms);
|
||||
let mode = match mode {
|
||||
LedModeSelect::AllOff => types::led::Mode::AllOff,
|
||||
LedModeSelect::RedOn => types::led::Mode::RedOn,
|
||||
LedModeSelect::OrangeOn => types::led::Mode::OrangeOn,
|
||||
LedModeSelect::AlternatingToggle => types::led::Mode::AlternatingToggle(toggle_period),
|
||||
LedModeSelect::UnifiedToggle => types::led::Mode::UnifiedToggle(toggle_period),
|
||||
};
|
||||
send_led_request(client, addr, Request::SetMode(mode));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
|
||||
use types::acs::mgt::request::{ModeRequest, Request};
|
||||
|
||||
if args.ping {
|
||||
send_mgt_request(client, addr, Request::Ping);
|
||||
}
|
||||
if let Some(hk) = args.hk {
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
send_mgt_request(client, addr, Request::Hk(req_type));
|
||||
}
|
||||
if let Some(mode) = args.mode {
|
||||
let mode = match mode {
|
||||
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||
};
|
||||
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
|
||||
}
|
||||
if let Some(dipole) = args.torque {
|
||||
let request = Request::ApplyTorque {
|
||||
dipole,
|
||||
duration: Duration::from_millis(args.torque_duration_ms),
|
||||
};
|
||||
send_mgt_request(client, addr, request);
|
||||
}
|
||||
if let Some(health) = args.health {
|
||||
send_mgt_request(
|
||||
client,
|
||||
addr,
|
||||
Request::Health(types::HealthRequest::SetHealth(health.into())),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mgm_command(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
target_id: types::ComponentId,
|
||||
args: MgmArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(mode) = args.fault {
|
||||
inject_mgm_failure(
|
||||
target_id,
|
||||
mgm::SpiFault {
|
||||
mode: mode.into(),
|
||||
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
if args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::mgm::request::Request::Ping,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} ping request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(hk) = args.hk {
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Hk),
|
||||
types::acs::mgm::request::Request::Hk(HkRequest {
|
||||
id: types::acs::mgm::request::HkId::Sensor,
|
||||
req_type,
|
||||
}),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} HK request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(mode) = args.mode {
|
||||
let dev_mode = match mode {
|
||||
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::mgm::request::Request::Mode(
|
||||
types::acs::mgm::request::ModeRequest::SetMode(dev_mode),
|
||||
),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} HK request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(health) = args.health {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Health),
|
||||
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
|
||||
health.into(),
|
||||
)),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} set-health request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
|
||||
use types::event_manager::request::Request;
|
||||
|
||||
let request = match args.action {
|
||||
EventFilterAction::Enable(filter) => match filter.event_id {
|
||||
Some(event_id) => Request::EnableEvent {
|
||||
sender_id: filter.component.into(),
|
||||
event_id,
|
||||
},
|
||||
None => Request::EnableComponent(filter.component.into()),
|
||||
},
|
||||
EventFilterAction::Disable(filter) => match filter.event_id {
|
||||
Some(event_id) => Request::DisableEvent {
|
||||
sender_id: filter.component.into(),
|
||||
event_id,
|
||||
},
|
||||
None => Request::DisableComponent(filter.component.into()),
|
||||
},
|
||||
};
|
||||
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
|
||||
log::info!(
|
||||
"sending event manager request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&request_packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{} {} {}] {}",
|
||||
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||
record.level(),
|
||||
record.target(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(level)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_logger(log::LevelFilter::Debug).unwrap();
|
||||
let kill_signal = Arc::new(AtomicBool::new(false));
|
||||
let ctrl_kill_signal = kill_signal.clone();
|
||||
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
|
||||
let cli = Cli::parse();
|
||||
let config = Config::load()?;
|
||||
|
||||
let addr = cli
|
||||
.udp_addr
|
||||
.or(config.interface.udp_addr)
|
||||
.unwrap_or(SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT));
|
||||
// Bind to all interfaces, so embedded targets inside the local network can be reached too.
|
||||
let client = UdpSocket::bind("0.0.0.0:7302").expect("Connecting to UDP server failed");
|
||||
client.set_nonblocking(true)?;
|
||||
client.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||
|
||||
if cli.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
|
||||
types::control::request::Request::Ping,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if cli.test_event {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
|
||||
types::control::request::Request::TestEvent,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending event request with TC ID {:#010x}",
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(cmd) = cli.commands {
|
||||
match cmd {
|
||||
Commands::Mgm0(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
|
||||
}
|
||||
Commands::Mgm1(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
|
||||
}
|
||||
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
|
||||
Commands::MgmAssy(mgm_assembly_args) => {
|
||||
let target_id = types::ComponentId::AcsMgmAssembly;
|
||||
if mgm_assembly_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::mgm::request::Request::Ping,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} ping request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(mode) = mgm_assembly_args.mode {
|
||||
let assembly_mode = match mode {
|
||||
AssemblyModeSelect::NoModeKeeping => {
|
||||
types::acs::mgm_assembly::Mode::NoModeKeeping
|
||||
}
|
||||
AssemblyModeSelect::Off => {
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
|
||||
}
|
||||
AssemblyModeSelect::Normal => {
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
|
||||
}
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::mgm_assembly::request::Request::Mode(
|
||||
types::acs::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
|
||||
),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} HK request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
}
|
||||
Commands::AcsSubsystem(subsystem_args) => {
|
||||
let target_id = types::ComponentId::AcsSubsystem;
|
||||
if subsystem_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::subsystem::request::Request::Ping,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} ping request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(mode) = subsystem_args.mode {
|
||||
let subsystem_mode = match mode {
|
||||
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
|
||||
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::subsystem::request::Request::Mode(
|
||||
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
|
||||
),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} mode request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
}
|
||||
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
|
||||
Commands::Led(args) => handle_led_command(&client, addr, args),
|
||||
Commands::Sim(args) => handle_sim_command(&client, addr, args),
|
||||
}
|
||||
}
|
||||
|
||||
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
|
||||
log::info!("entering listening loop");
|
||||
loop {
|
||||
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
log::info!("received kill signal, exiting");
|
||||
break;
|
||||
}
|
||||
match client.recv(recv_buf.as_mut_slice()) {
|
||||
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut
|
||||
{
|
||||
continue;
|
||||
}
|
||||
log::warn!("UDP reception error: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
|
||||
///
|
||||
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
|
||||
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
|
||||
/// UDP send would otherwise silently do nothing if minisim is not running.
|
||||
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
|
||||
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
|
||||
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||
|
||||
let mut reply_buf = [0u8; 4096];
|
||||
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
|
||||
match sim_socket.recv(&mut reply_buf) {
|
||||
Ok(len) => {
|
||||
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
|
||||
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
|
||||
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
|
||||
) =>
|
||||
{
|
||||
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let id = match target_id {
|
||||
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
|
||||
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
|
||||
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
|
||||
};
|
||||
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
|
||||
id,
|
||||
request: mgm::Request::SetSpiFault(fault),
|
||||
});
|
||||
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
|
||||
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Each component has its own event type, so the sender ID determines how to decode the event.
|
||||
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
|
||||
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
|
||||
sender_id: types::ComponentId,
|
||||
data: &[u8],
|
||||
) {
|
||||
match postcard::from_bytes::<E>(data) {
|
||||
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
|
||||
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
|
||||
}
|
||||
}
|
||||
match sender_id {
|
||||
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
|
||||
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
|
||||
log_event::<types::acs::mgm::Event>(sender_id, data)
|
||||
}
|
||||
types::ComponentId::AcsMgmAssembly => {
|
||||
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
|
||||
}
|
||||
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
|
||||
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
|
||||
// TC source events are sent with the ID of the packet source.
|
||||
types::ComponentId::UdpServer
|
||||
| types::ComponentId::TcpServer
|
||||
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
|
||||
_ => log::warn!(
|
||||
"Received event from {:?} with unknown event type",
|
||||
sender_id
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
|
||||
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
|
||||
Ok(packet) => {
|
||||
let tm_header_result = postcard::take_from_bytes::<types::TmHeader>(packet.user_data());
|
||||
if let Err(e) = tm_header_result {
|
||||
bail!("Failed to deserialize TM header: {}", e);
|
||||
}
|
||||
let (tm_header, remainder) = tm_header_result.unwrap();
|
||||
if let Some(tc_id) = tm_header.tc_id {
|
||||
log::info!(
|
||||
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
|
||||
packet.apid(),
|
||||
tm_header.sender_id,
|
||||
tc_id.raw()
|
||||
);
|
||||
}
|
||||
if tm_header.message_type == MessageType::Event {
|
||||
handle_event(tm_header.sender_id, remainder);
|
||||
return Ok(());
|
||||
}
|
||||
match tm_header.sender_id {
|
||||
types::ComponentId::EpsPcdu => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
|
||||
log::info!("Received response from PCDU: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::Controller => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::control::response::Response>(remainder);
|
||||
log::info!("Received response from controller: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsMgmAssembly => {
|
||||
let response = postcard::from_bytes::<
|
||||
types::acs::mgm_assembly::response::Response,
|
||||
>(remainder);
|
||||
log::info!(
|
||||
"Received response from MGM Assembly: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::AcsMgm0 => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||
log::info!("Received response from MGM0: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsMgm1 => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||
log::info!("Received response from MGM1: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsSubsystem => {
|
||||
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
|
||||
remainder,
|
||||
);
|
||||
log::info!(
|
||||
"Received response from ACS subsystem: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::EpsSubsystem => todo!(),
|
||||
types::ComponentId::UdpServer => todo!(),
|
||||
types::ComponentId::TcpServer => todo!(),
|
||||
types::ComponentId::Ground => todo!(),
|
||||
types::ComponentId::EventManager => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
|
||||
log::info!(
|
||||
"Received response from event manager: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::AcsController => todo!(),
|
||||
types::ComponentId::AcsMgt => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
|
||||
log::info!("Received response from MGT: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::Led => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::led::response::Response>(remainder);
|
||||
log::info!("Received response from LED: {:?}", response.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_dipole() {
|
||||
assert_eq!(
|
||||
parse_dipole("-200, 200,1000"),
|
||||
Ok(types::acs::mgt::Dipole {
|
||||
x: -200,
|
||||
y: 200,
|
||||
z: 1000
|
||||
})
|
||||
);
|
||||
assert!(parse_dipole("1,2").is_err());
|
||||
assert!(parse_dipole("1,2,3,4").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_template_is_valid() {
|
||||
let template = include_str!("../config.toml.template");
|
||||
let config: Config = toml::from_str(template).unwrap();
|
||||
assert_eq!(config.interface.udp_addr, None);
|
||||
let config: Config =
|
||||
toml::from_str("[interface]\nudp_addr = \"192.168.1.50:7301\"").unwrap();
|
||||
assert_eq!(
|
||||
config.interface.udp_addr,
|
||||
Some("192.168.1.50:7301".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_torque_argument() {
|
||||
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
|
||||
let Some(Commands::Mgt(args)) = cli.commands else {
|
||||
panic!("expected mgt subcommand");
|
||||
};
|
||||
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||
runner = "probe-rs run --chip STM32H753ZITx"
|
||||
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
|
||||
|
||||
rustflags = [
|
||||
"-C", "linker=flip-link",
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "link-arg=-Tdefmt.x",
|
||||
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
|
||||
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
|
||||
"-C", "link-arg=--nmagic",
|
||||
# Can be useful for debugging.
|
||||
# "-Clink-args=-Map=app.map"
|
||||
]
|
||||
|
||||
[build]
|
||||
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
|
||||
# target improves performance)
|
||||
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
||||
# target = "thumbv7m-none-eabi" # Cortex-M3
|
||||
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
|
||||
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
|
||||
|
||||
[alias]
|
||||
rb = "run --bin"
|
||||
rrb = "run --release --bin"
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "info"
|
||||
# IP address of the host running the minisim. Without it, the firmware does not try to connect
|
||||
# to a simulator.
|
||||
# SIM_IP_ADDR = "192.168.1.10"
|
||||
@@ -0,0 +1,3 @@
|
||||
/.cargo/config.toml
|
||||
.vscode/
|
||||
app.map
|
||||
Generated
+1740
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,71 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"shared-embedded",
|
||||
"stm32h7-nucleo-embassy",
|
||||
"stm32h7-nucleo-rtic",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
types = { path = "../types" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
shared-embedded = { path = "shared-embedded" }
|
||||
|
||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7"
|
||||
arbitrary-int = "2"
|
||||
defmt = "1"
|
||||
defmt-rtt = "1"
|
||||
defmt-test = "0.5"
|
||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||
embedded-alloc = "0.7"
|
||||
static_cell = "2"
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
|
||||
postcard = "1"
|
||||
serde = { version = "1", default-features = false }
|
||||
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||
|
||||
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "defmt"] }
|
||||
embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] }
|
||||
embassy-time = "0.5"
|
||||
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||
embassy-sync = "0.8"
|
||||
embassy-futures = "0.1"
|
||||
|
||||
# cargo build/run
|
||||
[profile.dev]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = true # <-
|
||||
incremental = false
|
||||
opt-level = 's' # <-
|
||||
overflow-checks = true # <-
|
||||
|
||||
# cargo test
|
||||
[profile.test]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = true # <-
|
||||
incremental = false
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = true # <-
|
||||
|
||||
# cargo build/run --release
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = false # <-
|
||||
|
||||
# cargo test --release
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = false # <-
|
||||
@@ -0,0 +1,153 @@
|
||||
sat-rs examples for the STM32H753ZI-Nucleo board
|
||||
=======
|
||||
|
||||
These example applications show how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||
can be used on an embedded target.
|
||||
They also show how a relatively simple OBSW could be built when no standard runtime is available.
|
||||
Both use the [defmt](https://defmt.ferrous-systems.com/) framework for logging and provide the
|
||||
same functionality with a different concurrency framework:
|
||||
|
||||
- [`stm32h7-nucleo-embassy`](./stm32h7-nucleo-embassy) uses the [embassy](https://embassy.dev/)
|
||||
executor.
|
||||
- [`stm32h7-nucleo-rtic`](./stm32h7-nucleo-rtic) uses [RTIC](https://rtic.rs/2/book/en/).
|
||||
|
||||
The application code is shared inside the [`shared-embedded`](./shared-embedded) crate.
|
||||
Each application only initializes the hardware and wraps the shared code inside its own tasks.
|
||||
|
||||
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
|
||||
devices. It has more RAM available and allows commanding via Ethernet. The examples are written
|
||||
for the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
|
||||
|
||||
## Pre-Requisites
|
||||
|
||||
Make sure the following tools are installed:
|
||||
|
||||
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
|
||||
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
|
||||
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
|
||||
for debugging.
|
||||
|
||||
## Preparing Rust and the repository
|
||||
|
||||
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
||||
If you have not installed it yet, you can do so with
|
||||
|
||||
```sh
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
All crates in this directory form a separate workspace, because they are built for a different
|
||||
target than the rest of the repository. They share one `.cargo` config file. A default is
|
||||
provided as `.cargo/config.toml.template`. The build scripts copy it to `.cargo/config.toml`
|
||||
if that file does not exist yet. The copy is not tracked by git, so you can change settings like
|
||||
the runner for your setup.
|
||||
|
||||
Cargo reads the configuration before the build script runs, so the very first build on a fresh
|
||||
checkout does not use it yet and might fail. Simply run the build again, or copy the file
|
||||
manually beforehand:
|
||||
|
||||
```sh
|
||||
cp .cargo/config.toml.template .cargo/config.toml
|
||||
```
|
||||
|
||||
The configuration file also sets the target so it does not always have to be specified with
|
||||
the `--target` argument.
|
||||
|
||||
## Building
|
||||
|
||||
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
||||
you can build all applications from this directory with
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
```
|
||||
|
||||
or a single application with `cargo build -p stm32h7-nucleo-embassy`, for example.
|
||||
|
||||
## Flashing from the command line
|
||||
|
||||
The configuration file sets `probe-rs` as the runner, so you can flash and run an application
|
||||
with
|
||||
|
||||
```sh
|
||||
cargo run -p stm32h7-nucleo-embassy
|
||||
```
|
||||
|
||||
## Debugging with VS Code
|
||||
|
||||
The Nucleo board comes with an on-board ST-Link so all that is required to flash and debug
|
||||
the board is a USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
|
||||
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
|
||||
Make sure to install this plugin first.
|
||||
|
||||
## Commanding the board
|
||||
|
||||
The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be
|
||||
connected to a network with a DHCP server. The network configuration including the IP address is
|
||||
logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72
|
||||
must be ON when using Ethernet. The board uses the same TMTC protocol as the
|
||||
[`example-std`](../example-std) application, which is defined inside the [`types`](../types)
|
||||
crate.
|
||||
|
||||
The [`client`](../client) application is used to command the board. Set the address of the board
|
||||
inside `client/config.toml`, which is created from `client/config.toml.template` on the first
|
||||
build:
|
||||
|
||||
```toml
|
||||
[interface]
|
||||
udp_addr = "192.168.1.50:7301"
|
||||
```
|
||||
|
||||
For example, you can then send a ping to the MCU using
|
||||
|
||||
```sh
|
||||
cargo run -p client -- --ping
|
||||
```
|
||||
|
||||
Like the `example-std` application, the board has a controller component which handles pings and
|
||||
test events. A test event can be triggered with `--test-event`.
|
||||
|
||||
The green LED blinks every 0.5 seconds as a heartbeat. The red and the orange LED are controlled
|
||||
with a mode. For example, you can let both toggle together every 200 ms using
|
||||
|
||||
```sh
|
||||
cargo run -p client -- led --mode unified-toggle --toggle-period-ms 200
|
||||
```
|
||||
|
||||
Use `cargo run -p client -- led --help` to list all modes.
|
||||
|
||||
You can also pass the board address with `--udp-addr` instead of setting it inside the
|
||||
configuration file.
|
||||
|
||||
## Connecting to the mini simulator
|
||||
|
||||
The firmware can connect to the [`minisim`](../minisim), which simulates the devices of the OBSW.
|
||||
The simulator address can be passed with a sim connect request:
|
||||
|
||||
```sh
|
||||
cargo run -p client -- sim connect
|
||||
```
|
||||
|
||||
Without an IP address, the firmware uses the sender address of the request, which fits the
|
||||
common setup where the client and the simulator run on the same host. Otherwise, pass the IP
|
||||
address of the simulator host, for example `sim connect 192.168.1.10`.
|
||||
|
||||
The simulator address can also be set at build time inside `.cargo/config.toml`, so the firmware
|
||||
connects on its own after startup:
|
||||
|
||||
```toml
|
||||
[env]
|
||||
SIM_IP_ADDR = "192.168.1.10"
|
||||
```
|
||||
|
||||
The firmware pings the simulator on UDP port 7303 and logs the result. It reconnects after a
|
||||
network link loss. A new sim connect request, for example after restarting the simulator,
|
||||
triggers a new connection attempt.
|
||||
Like the `example-std` application, the device handlers use dummy interfaces if no simulator
|
||||
address is known or the simulator does not reply. The simulator sends its replies to the
|
||||
last client which contacted it, so only one application can use it at a time.
|
||||
|
||||
## Resources
|
||||
|
||||
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
|
||||
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["thumbv7em-none-eabihf"]
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "shared-embedded"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
types.workspace = true
|
||||
minisim-types.workspace = true
|
||||
|
||||
arbitrary-int.workspace = true
|
||||
defmt.workspace = true
|
||||
spacepackets.workspace = true
|
||||
postcard.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
embassy-stm32.workspace = true
|
||||
embassy-time.workspace = true
|
||||
embassy-net.workspace = true
|
||||
embassy-sync.workspace = true
|
||||
embassy-futures.workspace = true
|
||||
@@ -0,0 +1,68 @@
|
||||
use embassy_futures::select::{Either, select};
|
||||
use embassy_stm32::gpio;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::signal::Signal;
|
||||
use embassy_time::{Duration, Timer};
|
||||
use types::led;
|
||||
|
||||
pub static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
|
||||
|
||||
const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_LED_MODE: led::Mode =
|
||||
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
|
||||
|
||||
pub struct Leds {
|
||||
pub red: gpio::Output<'static>,
|
||||
pub orange: gpio::Output<'static>,
|
||||
}
|
||||
|
||||
pub async fn heartbeat(led: &mut gpio::Output<'static>) {
|
||||
loop {
|
||||
led.toggle();
|
||||
Timer::after(HEARTBEAT_PERIOD).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the current mode to the red and orange LED. A new mode is applied immediately.
|
||||
pub async fn led_task(leds: &mut Leds) {
|
||||
let mut mode = DEFAULT_LED_MODE;
|
||||
loop {
|
||||
let toggle_period = match mode {
|
||||
led::Mode::AllOff => {
|
||||
leds.red.set_low();
|
||||
leds.orange.set_low();
|
||||
None
|
||||
}
|
||||
led::Mode::RedOn => {
|
||||
leds.red.set_high();
|
||||
leds.orange.set_low();
|
||||
None
|
||||
}
|
||||
led::Mode::OrangeOn => {
|
||||
leds.red.set_low();
|
||||
leds.orange.set_high();
|
||||
None
|
||||
}
|
||||
led::Mode::AlternatingToggle(period) => {
|
||||
leds.red.toggle();
|
||||
leds.orange.set_level((!leds.red.is_set_high()).into());
|
||||
Some(period)
|
||||
}
|
||||
led::Mode::UnifiedToggle(period) => {
|
||||
leds.red.toggle();
|
||||
leds.orange.set_level(leds.red.is_set_high().into());
|
||||
Some(period)
|
||||
}
|
||||
};
|
||||
mode = match toggle_period {
|
||||
Some(period) => {
|
||||
let period = Duration::try_from(period).unwrap_or(Duration::MAX);
|
||||
match select(Timer::after(period), LED_MODE.wait()).await {
|
||||
Either::First(()) => mode,
|
||||
Either::Second(new_mode) => new_mode,
|
||||
}
|
||||
}
|
||||
None => LED_MODE.wait().await,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Application code shared by the STM32H7 Nucleo applications. Each application wraps the
|
||||
//! async functions inside tasks of its own executor.
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
pub mod leds;
|
||||
pub mod net;
|
||||
pub mod sim_client;
|
||||
pub mod tmtc;
|
||||
@@ -0,0 +1,118 @@
|
||||
use core::cell::Cell;
|
||||
|
||||
use embassy_futures::select::{Either3, select3};
|
||||
use embassy_net::{
|
||||
IpEndpoint,
|
||||
udp::{PacketMetadata, UdpSocket},
|
||||
};
|
||||
use embassy_stm32::{eth, peripherals};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::Timer;
|
||||
|
||||
use crate::tmtc::{TcSender, TmReceiver};
|
||||
|
||||
const PORT: u16 = 7301;
|
||||
const MTU: usize = 1500;
|
||||
|
||||
/// Locally administered MAC address
|
||||
pub const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
|
||||
|
||||
pub static LAST_SENDER: Mutex<CriticalSectionRawMutex, Cell<Option<core::net::Ipv4Addr>>> =
|
||||
Mutex::new(Cell::new(None));
|
||||
|
||||
pub type Device = eth::Ethernet<
|
||||
'static,
|
||||
peripherals::ETH,
|
||||
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
|
||||
>;
|
||||
|
||||
pub async fn net_stack_task(runner: &mut embassy_net::Runner<'static, Device>) -> ! {
|
||||
runner.run().await
|
||||
}
|
||||
|
||||
pub async fn udp_task(stack: embassy_net::Stack<'static>, tc_tx: TcSender, tm_rx: TmReceiver) {
|
||||
// Task futures are allocated statically, so these buffers do not live on the stack.
|
||||
let mut rx_udp_meta = [PacketMetadata::EMPTY; 8];
|
||||
let mut tx_udp_meta = [PacketMetadata::EMPTY; 8];
|
||||
let mut rx_udp_buf = [0; MTU];
|
||||
let mut tx_udp_buf = [0; MTU];
|
||||
let mut rx_buffer = [0; MTU];
|
||||
|
||||
loop {
|
||||
stack.wait_link_up().await;
|
||||
defmt::info!("Network link is up");
|
||||
|
||||
// Ensure DHCP configuration is up before trying connect
|
||||
stack.wait_config_up().await;
|
||||
defmt::info!("Network task initialized, config: {}", stack.config_v4());
|
||||
|
||||
let mut udp = UdpSocket::new(
|
||||
stack,
|
||||
&mut rx_udp_meta,
|
||||
&mut rx_udp_buf,
|
||||
&mut tx_udp_meta,
|
||||
&mut tx_udp_buf,
|
||||
);
|
||||
if let Err(e) = udp.bind(PORT) {
|
||||
defmt::error!("Failed to bind UDP socket: {}", e);
|
||||
Timer::after_secs(1).await;
|
||||
continue;
|
||||
}
|
||||
defmt::info!("UDP socket bound to port {}", PORT);
|
||||
let mut remote_endpoint = None;
|
||||
loop {
|
||||
match select3(
|
||||
udp.recv_from(&mut rx_buffer),
|
||||
tm_rx.receive(),
|
||||
stack.wait_link_down(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either3::First(Ok((len, meta))) => {
|
||||
remote_endpoint = Some(meta.endpoint);
|
||||
let embassy_net::IpAddress::Ipv4(sender_ip) = meta.endpoint.addr;
|
||||
|
||||
LAST_SENDER.lock(|val| {
|
||||
val.set(Some(sender_ip));
|
||||
});
|
||||
|
||||
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
|
||||
tc_tx.send(rx_buffer[0..len].to_vec()).await;
|
||||
// Incoming TCs take priority in the select. Draining TM here prevents a burst
|
||||
// of TCs from overflowing the TM queue. This could lead to a task deadlock
|
||||
// where each task is waiting on each other.
|
||||
while let Ok(packet) = tm_rx.try_receive() {
|
||||
handle_tm(&packet, &mut udp, &remote_endpoint).await;
|
||||
}
|
||||
}
|
||||
Either3::First(Err(e)) => {
|
||||
defmt::warn!("udp receive error: {}", e);
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
// TM is only generated as a response to a TC, so the endpoint is usually known.
|
||||
Either3::Second(packet) => handle_tm(&packet, &mut udp, &remote_endpoint).await,
|
||||
Either3::Third(()) => {
|
||||
defmt::warn!("Network link is down");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tm(
|
||||
packet: &[u8],
|
||||
udp_socket: &mut UdpSocket<'_>,
|
||||
remote_endpoint: &Option<IpEndpoint>,
|
||||
) {
|
||||
match remote_endpoint {
|
||||
Some(endpoint) => match udp_socket.send_to(packet, *endpoint).await {
|
||||
Ok(_) => {
|
||||
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
|
||||
}
|
||||
Err(e) => defmt::warn!("udp send error: {}", e),
|
||||
},
|
||||
None => defmt::warn!("dropping TM, no remote endpoint known"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! UDP client for the minisim, which simulates the devices of the OBSW.
|
||||
|
||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||
|
||||
use defmt::Debug2Format;
|
||||
use embassy_futures::select::{Either, select};
|
||||
use embassy_net::udp::{PacketMetadata, UdpSocket};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::signal::Signal;
|
||||
use embassy_sync::watch::Watch;
|
||||
use embassy_time::{Duration, WithTimeout as _};
|
||||
use minisim_types::udp::SIM_CTRL_PORT;
|
||||
use minisim_types::{SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime};
|
||||
|
||||
/// Set by a sim connect request. Each new address triggers a connection attempt.
|
||||
pub static SIM_HOST: Signal<CriticalSectionRawMutex, Ipv4Addr> = Signal::new();
|
||||
|
||||
/// IP address of the host running the minisim, set at build time. Without it, no connection
|
||||
/// to a simulator is attempted until a sim connect request is received.
|
||||
const SIM_FROM_ENV: Option<&str> = option_env!("SIM_IP_ADDR");
|
||||
|
||||
const PING_ATTEMPTS: usize = 3;
|
||||
const PING_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
const MAX_PACKET_LEN: usize = 1024;
|
||||
|
||||
/// Result of the connection check. Device handlers use it to pick either the simulator or a
|
||||
/// dummy interface.
|
||||
pub static SIM_AVAILABLE: Watch<CriticalSectionRawMutex, bool, 4> = Watch::new();
|
||||
|
||||
pub async fn sim_client_task(stack: embassy_net::Stack<'static>) {
|
||||
let mut rx_meta = [PacketMetadata::EMPTY; 4];
|
||||
let mut tx_meta = [PacketMetadata::EMPTY; 4];
|
||||
let mut rx_buf = [0; MAX_PACKET_LEN];
|
||||
let mut tx_buf = [0; MAX_PACKET_LEN];
|
||||
|
||||
let sim_available = SIM_AVAILABLE.sender();
|
||||
sim_available.send(false);
|
||||
|
||||
// Bound to an ephemeral port without a fixed local address, so the socket survives a new
|
||||
// DHCP lease and does not need to be recreated after a link loss.
|
||||
let mut udp = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf);
|
||||
if let Err(e) = udp.bind(0) {
|
||||
defmt::error!("Failed to bind simulator UDP socket: {}", e);
|
||||
return;
|
||||
}
|
||||
let mut sim_addr = sim_addr_from_env();
|
||||
|
||||
loop {
|
||||
stack.wait_link_up().await;
|
||||
stack.wait_config_up().await;
|
||||
loop {
|
||||
if let Some(addr) = sim_addr {
|
||||
let connected = check_connection(&udp, addr).await;
|
||||
if connected {
|
||||
defmt::info!("Connected to simulator at {}", Debug2Format(&addr));
|
||||
} else {
|
||||
defmt::warn!(
|
||||
"Simulator at {} not reachable, using dummy interfaces",
|
||||
Debug2Format(&addr)
|
||||
);
|
||||
}
|
||||
sim_available.send(connected);
|
||||
}
|
||||
match select(SIM_HOST.wait(), stack.wait_link_down()).await {
|
||||
Either::First(ip_addr) => {
|
||||
sim_addr = Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT));
|
||||
}
|
||||
Either::Second(()) => {
|
||||
sim_available.send(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sim_addr_from_env() -> Option<SocketAddrV4> {
|
||||
let ip_addr = SIM_FROM_ENV?;
|
||||
match ip_addr.parse::<Ipv4Addr>() {
|
||||
Ok(ip_addr) => Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT)),
|
||||
Err(_) => {
|
||||
defmt::error!("Invalid simulator IP address {}", ip_addr);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses several attempts, because the first packet can get lost while the MAC address of the
|
||||
/// simulator host is resolved.
|
||||
async fn check_connection(udp: &UdpSocket<'_>, sim_addr: SocketAddrV4) -> bool {
|
||||
let mut tx_buf = [0; MAX_PACKET_LEN];
|
||||
let mut rx_buf = [0; MAX_PACKET_LEN];
|
||||
let request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
let ping = match postcard::to_slice(&request, &mut tx_buf) {
|
||||
Ok(ping) => ping,
|
||||
Err(e) => {
|
||||
defmt::error!("Failed to serialize simulator ping: {}", Debug2Format(&e));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
for _ in 0..PING_ATTEMPTS {
|
||||
if let Err(e) = udp.send_to(ping, sim_addr).await {
|
||||
defmt::warn!("Failed to send simulator ping: {}", e);
|
||||
continue;
|
||||
}
|
||||
if let Ok(Ok((len, _))) = udp.recv_from(&mut rx_buf).with_timeout(PING_TIMEOUT).await
|
||||
&& matches!(
|
||||
postcard::from_bytes::<SimReply>(&rx_buf[..len]),
|
||||
Ok(SimReply::SimCtrl(SimCtrlReply::Pong))
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
use alloc::vec::Vec;
|
||||
use arbitrary_int::u14;
|
||||
use defmt::Debug2Format;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_sync::channel::{Channel, Receiver, Sender};
|
||||
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
|
||||
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
|
||||
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
|
||||
|
||||
use crate::leds::LED_MODE;
|
||||
use crate::net::LAST_SENDER;
|
||||
use crate::sim_client;
|
||||
|
||||
pub const TC_QUEUE_DEPTH: usize = 32;
|
||||
pub const TM_QUEUE_DEPTH: usize = 32;
|
||||
|
||||
pub type TcChannel = Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
|
||||
pub type TmChannel = Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
|
||||
|
||||
pub type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
|
||||
pub type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
|
||||
pub type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
|
||||
pub type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
|
||||
|
||||
pub async fn tc_handler(tc_rx: TcReceiver, telemetry: &mut Telemetry) {
|
||||
loop {
|
||||
let tc = tc_rx.receive().await;
|
||||
let packet = match CcsdsPacketReader::new_with_checksum(&tc) {
|
||||
Ok(packet) => packet,
|
||||
Err(e) => {
|
||||
defmt::warn!("Failed to parse received TC packet: {}", e);
|
||||
send_tmtc_event(telemetry, tmtc::Event::InvalidTcPacket).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let tc_id = CcsdsPacketIdAndPsc {
|
||||
packet_id: packet.packet_id(),
|
||||
psc: packet.psc(),
|
||||
};
|
||||
let Ok((tc_header, payload)) = postcard::take_from_bytes::<TcHeader>(packet.user_data())
|
||||
else {
|
||||
defmt::warn!("Failed to deserialize TC header");
|
||||
send_tmtc_event(telemetry, tmtc::Event::InvalidTcHeader).await;
|
||||
continue;
|
||||
};
|
||||
match tc_header.target_id {
|
||||
ComponentId::Controller => handle_controller_tc(payload, tc_id, telemetry).await,
|
||||
ComponentId::Led => handle_led_tc(payload, tc_id, telemetry).await,
|
||||
target_id => {
|
||||
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
|
||||
send_tmtc_event(telemetry, tmtc::Event::UnknownTargetId(target_id)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All TCs are received via UDP, so the UDP server is the sender of TMTC events.
|
||||
async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) {
|
||||
telemetry.send(ComponentId::UdpServer, None, &event).await;
|
||||
}
|
||||
|
||||
/// The controller does not control anything yet, but handles generic requests like pings.
|
||||
async fn handle_controller_tc(
|
||||
payload: &[u8],
|
||||
tc_id: CcsdsPacketIdAndPsc,
|
||||
telemetry: &mut Telemetry,
|
||||
) {
|
||||
let Ok(request) = postcard::from_bytes::<control::request::Request>(payload) else {
|
||||
defmt::warn!("Failed to deserialize controller request");
|
||||
return;
|
||||
};
|
||||
match request {
|
||||
control::request::Request::Ping => defmt::info!("Received controller ping request"),
|
||||
control::request::Request::TestEvent => {
|
||||
defmt::info!("Received test event request");
|
||||
let event = types::Event::ControllerEvent(control::Event::TestEvent);
|
||||
telemetry.send(ComponentId::Controller, None, &event).await;
|
||||
}
|
||||
control::request::Request::SimConnect(opt_ipv4_addr) => match opt_ipv4_addr {
|
||||
Some(ipv4_addr) => sim_client::SIM_HOST.signal(ipv4_addr),
|
||||
None => {
|
||||
// If the UDP socket has received anything, the last sender should be stored and
|
||||
// we use that IP address.
|
||||
let opt_last_sender = LAST_SENDER.lock(|val| val.clone());
|
||||
match opt_last_sender.get() {
|
||||
Some(last_sender) => sim_client::SIM_HOST.signal(last_sender),
|
||||
None => defmt::warn!("SimConnect without IP and no known sender"),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
telemetry
|
||||
.send(
|
||||
ComponentId::Controller,
|
||||
Some(tc_id),
|
||||
&control::response::Response::Ok,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) {
|
||||
let Ok(request) = postcard::from_bytes::<led::request::Request>(payload) else {
|
||||
defmt::warn!("Failed to deserialize LED request");
|
||||
return;
|
||||
};
|
||||
match request {
|
||||
led::request::Request::Ping => defmt::info!("Received LED ping request"),
|
||||
led::request::Request::SetMode(mode) => {
|
||||
defmt::info!("Received LED mode request: {}", Debug2Format(&mode));
|
||||
LED_MODE.signal(mode);
|
||||
}
|
||||
}
|
||||
telemetry
|
||||
.send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Packs TM and passes it to the UDP task.
|
||||
pub struct Telemetry {
|
||||
tx: TmSender,
|
||||
sequence_count: u14,
|
||||
}
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(tx: TmSender) -> Self {
|
||||
Self {
|
||||
tx,
|
||||
sequence_count: u14::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// TM without a TC ID is sent unsolicited, for example events.
|
||||
async fn send(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
payload: &(impl serde::Serialize + Message),
|
||||
) {
|
||||
let sp_header = SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0);
|
||||
let tm_header = TmHeader::new_without_timestamp(
|
||||
sender_id,
|
||||
ComponentId::Ground,
|
||||
payload.message_type(),
|
||||
tc_id,
|
||||
);
|
||||
match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload)
|
||||
.map_err(CcsdsCreationError::from)
|
||||
.and_then(|packet| packet.try_to_vec())
|
||||
{
|
||||
Ok(raw_packet) => {
|
||||
self.tx.send(raw_packet).await;
|
||||
self.sequence_count = self.sequence_count.wrapping_add(u14::new(1));
|
||||
}
|
||||
Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "stm32h7-nucleo-embassy"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
default-run = "stm32h7-nucleo-embassy"
|
||||
|
||||
[lib]
|
||||
harness = false
|
||||
|
||||
# needed for each integration test
|
||||
[[test]]
|
||||
name = "integration"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
shared-embedded.workspace = true
|
||||
|
||||
cortex-m.workspace = true
|
||||
cortex-m-rt.workspace = true
|
||||
defmt.workspace = true
|
||||
defmt-rtt.workspace = true
|
||||
panic-probe.workspace = true
|
||||
embedded-alloc.workspace = true
|
||||
static_cell.workspace = true
|
||||
|
||||
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
|
||||
embassy-executor.workspace = true
|
||||
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms"] }
|
||||
embassy-net.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test.workspace = true
|
||||
File renamed without changes.
@@ -0,0 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
// The configuration is shared by all applications of the embedded workspace.
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Blinks an LED
|
||||
//!
|
||||
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
|
||||
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
|
||||
//! NUCLEO-H753ZI.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::gpio;
|
||||
use stm32h7_nucleo_embassy as _;
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
defmt::info!("Hello World!");
|
||||
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld2 = gpio::Output::new(p.PE1, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||
|
||||
spawner.spawn(blink(ld1, ld2, ld3).expect("spawning blink task failed"));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn blink(
|
||||
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;
|
||||
|
||||
defmt::info!("low");
|
||||
ld1.set_low();
|
||||
ld2.set_low();
|
||||
ld3.set_low();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use stm32h7_nucleo_embassy as _;
|
||||
|
||||
#[cortex_m_rt::entry]
|
||||
fn main() -> ! {
|
||||
loop {
|
||||
defmt::println!("Hello, world!");
|
||||
cortex_m::asm::delay(100_000_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use defmt_rtt as _;
|
||||
use embassy_stm32 as _;
|
||||
use panic_probe as _;
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
use embedded_alloc::LlffHeap as Heap;
|
||||
|
||||
const HEAP_SIZE: usize = 131_072;
|
||||
|
||||
// Part of the library, because all binaries depend on crates which require an allocator.
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Must be called exactly once, before the first allocation.
|
||||
pub unsafe fn init_heap() {
|
||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||
}
|
||||
|
||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
|
||||
#[defmt::panic_handler]
|
||||
fn panic() -> ! {
|
||||
cortex_m::asm::udf()
|
||||
}
|
||||
|
||||
/// 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) -> ! {
|
||||
panic!("unexpected hard fault");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[defmt_test::tests]
|
||||
mod unit_tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::StackResources;
|
||||
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
|
||||
use static_cell::{ConstStaticCell, StaticCell};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
ETH => eth::InterruptHandler;
|
||||
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||
});
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
|
||||
|
||||
// Safety: Called once, before the first allocation.
|
||||
unsafe { stm32h7_nucleo_embassy::init_heap() };
|
||||
|
||||
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,
|
||||
divp: Some(PllDiv::DIV2),
|
||||
divq: None,
|
||||
divr: None,
|
||||
});
|
||||
config.rcc.sys = Sysclk::PLL1_P; // 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 green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let leds = shared_embedded::leds::Leds {
|
||||
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
|
||||
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
|
||||
};
|
||||
|
||||
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
|
||||
shared_embedded::net::MAC_ADDRESS,
|
||||
periphs.ETH_SMA,
|
||||
periphs.PA2, // mdio
|
||||
periphs.PC1, // mdc
|
||||
);
|
||||
|
||||
let net_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);
|
||||
|
||||
// DHCP, TMTC and simulator socket.
|
||||
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||
let (stack, runner) = embassy_net::new(
|
||||
device,
|
||||
net_config,
|
||||
RESOURCES.init(StackResources::new()),
|
||||
seed,
|
||||
);
|
||||
|
||||
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
|
||||
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
|
||||
let tc_channel = TC_CHANNEL.take();
|
||||
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
|
||||
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
|
||||
let tm_channel = TM_CHANNEL.take();
|
||||
|
||||
spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed"));
|
||||
spawner.spawn(sim_client_task(stack).expect("spawning sim client task failed"));
|
||||
spawner.spawn(
|
||||
udp_task(stack, tc_channel.sender(), tm_channel.receiver())
|
||||
.expect("spawning UDP task failed"),
|
||||
);
|
||||
spawner.spawn(heartbeat(green_led).expect("spawning heartbeat task failed"));
|
||||
spawner.spawn(led_task(leds).expect("spawning LED task failed"));
|
||||
spawner.spawn(
|
||||
tc_handler(
|
||||
tc_channel.receiver(),
|
||||
shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
|
||||
)
|
||||
.expect("spawning TC handler task failed"),
|
||||
);
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn net_stack_task(
|
||||
mut runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
|
||||
) -> ! {
|
||||
shared_embedded::net::net_stack_task(&mut runner).await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn udp_task(
|
||||
stack: embassy_net::Stack<'static>,
|
||||
tc_tx: shared_embedded::tmtc::TcSender,
|
||||
tm_rx: shared_embedded::tmtc::TmReceiver,
|
||||
) {
|
||||
shared_embedded::net::udp_task(stack, tc_tx, tm_rx).await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn sim_client_task(stack: embassy_net::Stack<'static>) {
|
||||
shared_embedded::sim_client::sim_client_task(stack).await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn heartbeat(mut led: gpio::Output<'static>) {
|
||||
shared_embedded::leds::heartbeat(&mut led).await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn led_task(mut leds: shared_embedded::leds::Leds) {
|
||||
shared_embedded::leds::led_task(&mut leds).await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn tc_handler(
|
||||
tc_rx: shared_embedded::tmtc::TcReceiver,
|
||||
mut telemetry: shared_embedded::tmtc::Telemetry,
|
||||
) {
|
||||
shared_embedded::tmtc::tc_handler(tc_rx, &mut telemetry).await
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use stm32h7_nucleo_embassy as _; // memory layout + panic handler
|
||||
|
||||
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
|
||||
// feature)
|
||||
#[defmt_test::tests]
|
||||
mod tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "stm32h7-nucleo-rtic"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
default-run = "stm32h7-nucleo-rtic"
|
||||
|
||||
[lib]
|
||||
harness = false
|
||||
|
||||
# needed for each integration test
|
||||
[[test]]
|
||||
name = "integration"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
shared-embedded.workspace = true
|
||||
|
||||
cortex-m.workspace = true
|
||||
cortex-m-rt.workspace = true
|
||||
defmt.workspace = true
|
||||
defmt-rtt.workspace = true
|
||||
panic-probe.workspace = true
|
||||
embedded-alloc.workspace = true
|
||||
static_cell.workspace = true
|
||||
rtic.workspace = true
|
||||
|
||||
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
|
||||
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||
embassy-net.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test.workspace = true
|
||||
File renamed without changes.
@@ -0,0 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
// The configuration is shared by all applications of the embedded workspace.
|
||||
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();
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,58 @@
|
||||
//! Blinks an LED
|
||||
//!
|
||||
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
|
||||
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
|
||||
//! NUCLEO-H753ZI.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use rtic::app;
|
||||
use stm32h7_nucleo_rtic as _;
|
||||
|
||||
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
|
||||
mod app {
|
||||
use embassy_stm32::gpio;
|
||||
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
#[local]
|
||||
struct Local {}
|
||||
|
||||
#[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.PE1, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||
|
||||
// Schedule the blinking task
|
||||
blink::spawn(ld1, ld2, ld3).ok();
|
||||
|
||||
(Shared {}, Local {})
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
defmt::info!("low");
|
||||
ld1.set_low();
|
||||
ld2.set_low();
|
||||
ld3.set_low();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use stm32h7_nucleo_rtic as _;
|
||||
|
||||
#[cortex_m_rt::entry]
|
||||
fn main() -> ! {
|
||||
loop {
|
||||
defmt::println!("Hello, world!");
|
||||
cortex_m::asm::delay(100_000_000);
|
||||
}
|
||||
}
|
||||
+20
-17
@@ -1,14 +1,27 @@
|
||||
#![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 embassy_stm32 as _;
|
||||
use panic_probe as _;
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
use embedded_alloc::LlffHeap as Heap;
|
||||
|
||||
const HEAP_SIZE: usize = 131_072;
|
||||
|
||||
// Part of the library, because all binaries depend on crates which require an allocator.
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Must be called exactly once, before the first allocation.
|
||||
pub unsafe fn init_heap() {
|
||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||
}
|
||||
|
||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
|
||||
#[defmt::panic_handler]
|
||||
@@ -16,14 +29,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
|
||||
@@ -31,9 +36,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
|
||||
@@ -0,0 +1,163 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
use rtic::app;
|
||||
|
||||
#[app(device = embassy_stm32, peripherals = false)]
|
||||
mod app {
|
||||
use embassy_net::StackResources;
|
||||
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
|
||||
use static_cell::{ConstStaticCell, StaticCell};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
ETH => eth::InterruptHandler;
|
||||
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||
});
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
net_runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
|
||||
net_stack: embassy_net::Stack<'static>,
|
||||
sim_net_stack: embassy_net::Stack<'static>,
|
||||
leds: shared_embedded::leds::Leds,
|
||||
green_led: gpio::Output<'static>,
|
||||
tc_rx: shared_embedded::tmtc::TcReceiver,
|
||||
tc_tx: shared_embedded::tmtc::TcSender,
|
||||
tm_rx: shared_embedded::tmtc::TmReceiver,
|
||||
telemetry: shared_embedded::tmtc::Telemetry,
|
||||
}
|
||||
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
|
||||
|
||||
// Safety: Called once, before the first allocation.
|
||||
unsafe { stm32h7_nucleo_rtic::init_heap() };
|
||||
|
||||
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,
|
||||
divp: Some(PllDiv::DIV2),
|
||||
divq: None,
|
||||
divr: None,
|
||||
});
|
||||
config.rcc.sys = Sysclk::PLL1_P; // 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 green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let leds = shared_embedded::leds::Leds {
|
||||
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
|
||||
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
|
||||
};
|
||||
|
||||
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
|
||||
shared_embedded::net::MAC_ADDRESS,
|
||||
periphs.ETH_SMA,
|
||||
periphs.PA2, // mdio
|
||||
periphs.PC1, // mdc
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
// DHCP, TMTC and simulator socket.
|
||||
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||
let (stack, runner) =
|
||||
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
|
||||
|
||||
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
|
||||
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
|
||||
let tc_channel = TC_CHANNEL.take();
|
||||
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
|
||||
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
|
||||
let tm_channel = TM_CHANNEL.take();
|
||||
|
||||
net_lib_task::spawn().expect("spawning net library task failed");
|
||||
udp_task::spawn().expect("spawning UDP task failed");
|
||||
sim_client_task::spawn().expect("spawning sim client task failed");
|
||||
heartbeat::spawn().expect("spawning heartbeat task failed");
|
||||
led_task::spawn().expect("spawning LED task failed");
|
||||
tc_handler::spawn().expect("spawning TC handler task failed");
|
||||
|
||||
(
|
||||
Shared {},
|
||||
Local {
|
||||
green_led,
|
||||
leds,
|
||||
net_runner: runner,
|
||||
net_stack: stack,
|
||||
sim_net_stack: stack,
|
||||
tc_tx: tc_channel.sender(),
|
||||
tc_rx: tc_channel.receiver(),
|
||||
telemetry: shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
|
||||
tm_rx: tm_channel.receiver(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[task(local = [green_led])]
|
||||
async fn heartbeat(cx: heartbeat::Context) {
|
||||
shared_embedded::leds::heartbeat(cx.local.green_led).await;
|
||||
}
|
||||
|
||||
#[task(local = [leds])]
|
||||
async fn led_task(cx: led_task::Context) {
|
||||
shared_embedded::leds::led_task(cx.local.leds).await;
|
||||
}
|
||||
|
||||
#[task(local = [net_runner])]
|
||||
async fn net_lib_task(cx: net_lib_task::Context) {
|
||||
shared_embedded::net::net_stack_task(cx.local.net_runner).await;
|
||||
}
|
||||
|
||||
#[task(local = [net_stack, tc_tx, tm_rx])]
|
||||
async fn udp_task(cx: udp_task::Context) {
|
||||
shared_embedded::net::udp_task(*cx.local.net_stack, *cx.local.tc_tx, *cx.local.tm_rx).await;
|
||||
}
|
||||
|
||||
#[task(local = [sim_net_stack])]
|
||||
async fn sim_client_task(cx: sim_client_task::Context) {
|
||||
shared_embedded::sim_client::sim_client_task(*cx.local.sim_net_stack).await;
|
||||
}
|
||||
|
||||
#[task(local = [tc_rx, telemetry])]
|
||||
async fn tc_handler(cx: tc_handler::Context) {
|
||||
shared_embedded::tmtc::tc_handler(*cx.local.tc_rx, cx.local.telemetry).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use 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)
|
||||
#[defmt_test::tests]
|
||||
mod tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -7,3 +7,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# [unreleased]
|
||||
|
||||
# [v0.1.1] 2024-02-21
|
||||
|
||||
satrs v0.2.0-rc.0
|
||||
satrs-mib v0.1.1
|
||||
|
||||
# [v0.1.0] 2024-02-13
|
||||
|
||||
satrs v0.1.1
|
||||
satrs-mib v0.1.0
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "example-std"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
default-run = "example-std"
|
||||
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||
|
||||
[dependencies]
|
||||
fern = "0.7"
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
delegate = "0.13"
|
||||
zerocopy = "0.8"
|
||||
csv = "1"
|
||||
num_enum = "0.7"
|
||||
thiserror = "2"
|
||||
lazy_static = "1"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
derive-new = "0.7"
|
||||
cfg-if = "1"
|
||||
arbitrary-int = "2"
|
||||
bitbybit = "2"
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
ctrlc = "3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
satrs = { path = "../../satrs", features = ["test_util"] }
|
||||
types = { path = "../types" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
satrs-mib = { path = "../../satrs-mib" }
|
||||
|
||||
[features]
|
||||
# default = ["heap_tmtc"]
|
||||
# heap_tmtc = []
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
File renamed without changes.
@@ -0,0 +1,57 @@
|
||||
sat-rs example
|
||||
======
|
||||
|
||||
This crate contains an example application which simulates an on-board software.
|
||||
It uses various components provided by the sat-rs framework to do this. As such, it shows how
|
||||
a more complex real on-board software could be built from these components. It is recommended to
|
||||
read the dedicated
|
||||
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
|
||||
the sat-rs book.
|
||||
|
||||
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
|
||||
|
||||
You can run the application using `cargo run`.
|
||||
|
||||
# Features
|
||||
|
||||
The example has the `heap_tmtc` feature which is enabled by default. With this feature enabled,
|
||||
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
|
||||
stores.
|
||||
|
||||
You can run the application without this feature using
|
||||
|
||||
```sh
|
||||
cargo run --no-default-features
|
||||
```
|
||||
|
||||
# Interacting with the sat-rs example
|
||||
|
||||
The `client` crate is a command line client which sends telecommands to the example application
|
||||
and prints the received telemetry. For example, you can ping the application or switch MGM 0
|
||||
to normal mode like this:
|
||||
|
||||
```sh
|
||||
cargo run -p client -- --ping
|
||||
cargo run -p client -- mgm0 -m normal
|
||||
```
|
||||
|
||||
Use `cargo run -p client -- --help` to list all available commands.
|
||||
|
||||
## Adding the mini simulator application
|
||||
|
||||
This example application features a few device handlers. The
|
||||
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
|
||||
can be used to simulate the physical devices managed by these device handlers.
|
||||
|
||||
The example application will attempt communication with the mini simulator on UDP port 7303.
|
||||
If this works, the device handlers will use communication interfaces dedicated to the communication
|
||||
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
|
||||
return constant values or behave like ideal devices.
|
||||
|
||||
In summary, you can use the following command command to run the mini-simulator first:
|
||||
|
||||
```sh
|
||||
cargo run -p minisim
|
||||
```
|
||||
|
||||
and then start the example using `cargo run -p example-std`.
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
|
||||
|
||||
use types::acs::ctrl::{Mode, request::ModeRequest, response::ModeReport};
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually an assembly
|
||||
/// or a subsystem.
|
||||
pub struct ModeLeafHelper {
|
||||
pub request_rx: Receiver<ModeRequest>,
|
||||
pub report_tx: SyncSender<ModeReport>,
|
||||
}
|
||||
|
||||
/// Dummy ACS controller. It has no actual control law and reaches any commanded mode
|
||||
/// immediately, which is sufficient to exercise the mode commanding of its parent subsystem.
|
||||
pub struct Controller {
|
||||
mode: Mode,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
|
||||
Self {
|
||||
mode: Mode::Passive,
|
||||
mode_leaf_helper,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_mode_leaf_handling();
|
||||
}
|
||||
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
loop {
|
||||
match self.mode_leaf_helper.request_rx.try_recv() {
|
||||
Ok(request) => match request {
|
||||
ModeRequest::SetMode(mode) => {
|
||||
log::info!("ACS controller: transitioning to mode {:?}", mode);
|
||||
self.mode = mode;
|
||||
self.report_mode();
|
||||
}
|
||||
ModeRequest::ReadMode => self.report_mode(),
|
||||
},
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => break,
|
||||
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_mode(&self) {
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeReport::Mode(self.mode))
|
||||
.expect("failed to send mode report to parent");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct ControllerTestbench {
|
||||
request_tx: SyncSender<ModeRequest>,
|
||||
report_rx: Receiver<ModeReport>,
|
||||
controller: Controller,
|
||||
}
|
||||
|
||||
impl ControllerTestbench {
|
||||
fn new() -> Self {
|
||||
let (request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, report_rx) = mpsc::sync_channel(5);
|
||||
Self {
|
||||
request_tx,
|
||||
report_rx,
|
||||
controller: Controller::new(ModeLeafHelper {
|
||||
request_rx,
|
||||
report_tx,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_mode() {
|
||||
let testbench = ControllerTestbench::new();
|
||||
assert_eq!(testbench.controller.mode(), Mode::Passive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_mode() {
|
||||
let mut testbench = ControllerTestbench::new();
|
||||
testbench
|
||||
.request_tx
|
||||
.send(ModeRequest::SetMode(Mode::Safe))
|
||||
.unwrap();
|
||||
testbench.controller.periodic_operation();
|
||||
assert_eq!(testbench.controller.mode(), Mode::Safe);
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Safe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_mode() {
|
||||
let mut testbench = ControllerTestbench::new();
|
||||
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
|
||||
testbench.controller.periodic_operation();
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Passive));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,662 @@
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
use example_std::{ModeHelper, TmtcQueues};
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId, DeviceMode,
|
||||
acs::mgm_assembly::{Mode, request, response},
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
pub struct ParentQueueHelper {
|
||||
pub request_rx: mpsc::Receiver<request::ModeRequest>,
|
||||
pub report_tx: mpsc::SyncSender<response::ModeResponse>,
|
||||
}
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually as assembly.
|
||||
pub struct ChildrenQueueHelper {
|
||||
pub request_tx_queues: [mpsc::SyncSender<types::acs::mgm::request::ModeRequest>; 2],
|
||||
pub report_rx_queues: [mpsc::Receiver<types::acs::mgm::response::ModeResponse>; 2],
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransitionState {
|
||||
#[default]
|
||||
Idle,
|
||||
AwaitingReplies,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub struct MgmInfo {
|
||||
reply_received: bool,
|
||||
mode: Option<DeviceMode>,
|
||||
}
|
||||
|
||||
/// MGM assembly component.
|
||||
pub struct Assembly {
|
||||
mode_helper: ModeHelper<Mode, TransitionState>,
|
||||
/// This boolean is used for the distinction between transitions commanded by the parent
|
||||
/// or by ground, and transitions which were commanded autonomously as part of children
|
||||
/// mode keeping.
|
||||
mode_keeping_transition: bool,
|
||||
tmtc_queues: TmtcQueues,
|
||||
mgm_modes: [MgmInfo; 2],
|
||||
parent_queues: ParentQueueHelper,
|
||||
pub(crate) children_queues: ChildrenQueueHelper,
|
||||
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
|
||||
|
||||
pub fn new(
|
||||
parent_queues: ParentQueueHelper,
|
||||
children_queues: ChildrenQueueHelper,
|
||||
tmtc_queues: TmtcQueues,
|
||||
mode_timeout: Duration,
|
||||
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mode_helper: ModeHelper::new(Mode::NoModeKeeping, mode_timeout),
|
||||
mode_keeping_transition: false,
|
||||
tmtc_queues,
|
||||
mgm_modes: [MgmInfo::default(); 2],
|
||||
parent_queues,
|
||||
children_queues,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
self.handle_parent_mode_queue();
|
||||
self.handle_children_mode_queues();
|
||||
|
||||
if self.mode_helper.transition_active() {
|
||||
self.handle_mode_transition();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_telecommands(&mut self) {
|
||||
loop {
|
||||
match self.tmtc_queues.tc_rx.try_recv() {
|
||||
Ok(packet) => {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
match postcard::from_bytes::<types::acs::mgm_assembly::request::Request>(
|
||||
&packet.payload,
|
||||
) {
|
||||
Ok(request) => match request {
|
||||
types::acs::mgm_assembly::request::Request::Ping => {
|
||||
self.send_telemetry(Some(tc_id), response::Response::Ok)
|
||||
}
|
||||
types::acs::mgm_assembly::request::Request::Mode(request) => {
|
||||
match request {
|
||||
request::ModeRequest::SetMode(assembly_mode) => {
|
||||
self.start_transition(false, assembly_mode, Some(tc_id))
|
||||
}
|
||||
request::ModeRequest::ReadMode => self.send_telemetry(
|
||||
Some(tc_id),
|
||||
response::Response::Mode(response::ModeResponse::Mode(
|
||||
self.mode(),
|
||||
)),
|
||||
),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_telemetry(
|
||||
&self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
response: types::acs::mgm_assembly::response::Response,
|
||||
) {
|
||||
match pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||
log::warn!("failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to pack TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_parent_mode_queue(&mut self) {
|
||||
loop {
|
||||
match self.parent_queues.request_rx.try_recv() {
|
||||
Ok(request) => match request {
|
||||
request::ModeRequest::SetMode(assembly_mode) => match assembly_mode {
|
||||
Mode::Device(_device_mode) => {
|
||||
self.start_transition(false, assembly_mode, None);
|
||||
}
|
||||
Mode::NoModeKeeping => {
|
||||
self.mode_helper.current = Mode::NoModeKeeping;
|
||||
}
|
||||
},
|
||||
request::ModeRequest::ReadMode => self
|
||||
.parent_queues
|
||||
.report_tx
|
||||
.send(response::ModeResponse::Mode(self.mode_helper.current))
|
||||
.unwrap(),
|
||||
},
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("packet sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_children_mode_queues(&mut self) {
|
||||
let mut mode_report_received = false;
|
||||
for (idx, rx) in self.children_queues.report_rx_queues.iter_mut().enumerate() {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(report) => match report {
|
||||
types::acs::mgm::response::ModeResponse::Mode(device_mode) => {
|
||||
self.mgm_modes[idx].mode = Some(device_mode);
|
||||
self.mgm_modes[idx].reply_received = true;
|
||||
mode_report_received = true;
|
||||
}
|
||||
types::acs::mgm::response::ModeResponse::SetModeTimeout => {
|
||||
// Ignore, handle this with our own timeout.
|
||||
log::warn!("MGM {} mode timeout", idx);
|
||||
}
|
||||
},
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("packet sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if !mode_report_received {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transition is active, check for completion.
|
||||
if self.mode_helper.transition_active()
|
||||
&& self.mgm_modes.iter().all(|i| i.reply_received)
|
||||
&& let Mode::Device(device_mode) = self.mode_helper.target.unwrap()
|
||||
{
|
||||
// If at least one child reached the correct mode, we are done.
|
||||
if self.mgm_modes.iter().any(|i| i.mode == Some(device_mode)) {
|
||||
self.handle_mode_reached(true);
|
||||
} else {
|
||||
let report = if self.mode_keeping_transition {
|
||||
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||
} else {
|
||||
response::ModeResponse::WrongMode(self.mgm_modes.map(|info| info.mode))
|
||||
};
|
||||
self.handle_mode_transition_failure(report);
|
||||
}
|
||||
}
|
||||
|
||||
// Mode keeping active: Check children modes.
|
||||
if let Mode::Device(device_mode) = self.mode_helper.current
|
||||
&& self
|
||||
.mgm_modes
|
||||
.iter()
|
||||
.all(|info| info.mode != Some(device_mode))
|
||||
{
|
||||
// Children lost mode. Try to command them back to the correct
|
||||
// mode.
|
||||
self.start_transition(true, self.mode_helper.current, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mode_transition(&mut self) {
|
||||
if self.mode_helper.target.is_none() {
|
||||
self.handle_mode_reached(true);
|
||||
return;
|
||||
}
|
||||
let target = self.mode_helper.target.unwrap();
|
||||
let device_mode = match target {
|
||||
Mode::Device(device_mode) => device_mode,
|
||||
Mode::NoModeKeeping => {
|
||||
self.handle_mode_reached(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if self.mode_helper.transition_state == TransitionState::Idle {
|
||||
self.command_children(device_mode);
|
||||
self.mode_helper.transition_state = TransitionState::AwaitingReplies;
|
||||
}
|
||||
if self.mode_helper.transition_state == TransitionState::AwaitingReplies
|
||||
&& self.mode_helper.timed_out()
|
||||
{
|
||||
let report = if self.mode_keeping_transition {
|
||||
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||
} else {
|
||||
response::ModeResponse::SetModeTimeout(self.mgm_modes.map(|info| info.mode))
|
||||
};
|
||||
self.handle_mode_transition_failure(report);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mode_reached(&mut self, success: bool) {
|
||||
let tc_commander = self.mode_helper.finish(success);
|
||||
self.announce_mode();
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, response::Response::Ok);
|
||||
}
|
||||
self.parent_queues
|
||||
.report_tx
|
||||
.send(response::ModeResponse::Mode(self.mode_helper.current))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn handle_mode_transition_failure(&mut self, report: response::ModeResponse) {
|
||||
if self.mode_helper.tc_commander.is_some() {
|
||||
self.send_telemetry(
|
||||
self.mode_helper.tc_commander,
|
||||
response::Response::Mode(response::ModeResponse::SetModeTimeout(
|
||||
self.mgm_modes.map(|info| info.mode),
|
||||
)),
|
||||
);
|
||||
}
|
||||
self.parent_queues.report_tx.send(report).unwrap();
|
||||
self.mode_helper.finish(false);
|
||||
}
|
||||
|
||||
pub fn command_children(&self, mode: DeviceMode) {
|
||||
for tx in &self.children_queues.request_tx_queues {
|
||||
tx.send(types::acs::mgm::request::ModeRequest::SetMode(mode))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_transition(
|
||||
&mut self,
|
||||
mode_keeping: bool,
|
||||
target: Mode,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
self.mode_keeping_transition = mode_keeping;
|
||||
self.mode_helper.tc_commander = tc_id;
|
||||
self.mgm_modes
|
||||
.iter_mut()
|
||||
.for_each(|m| m.reply_received = false);
|
||||
self.mode_helper.start(target);
|
||||
}
|
||||
|
||||
fn announce_mode(&self) {
|
||||
log::info!(
|
||||
"{:?} announcing mode: {:?}",
|
||||
Self::ID,
|
||||
self.mode_helper.current
|
||||
);
|
||||
if let Err(e) = self
|
||||
.event_tx
|
||||
.send(types::acs::mgm_assembly::Event::ModeChanged(
|
||||
self.mode_helper.current,
|
||||
))
|
||||
{
|
||||
log::warn!("{:?}: failed to send mode changed event: {}", Self::ID, e);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode_helper.current
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(test)]
|
||||
fn mode_transition_active(&self) -> bool {
|
||||
self.mode_helper.transition_active()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, Message, MessageType, TcHeader,
|
||||
acs::mgm_assembly,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct Testbench {
|
||||
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
|
||||
subsystem_report_rx: mpsc::Receiver<response::ModeResponse>,
|
||||
mgm_request_rx: [mpsc::Receiver<types::acs::mgm::request::ModeRequest>; 2],
|
||||
mgm_report_tx: [mpsc::SyncSender<types::acs::mgm::response::ModeResponse>; 2],
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_rx: mpsc::Receiver<mgm_assembly::Event>,
|
||||
assembly: Assembly,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
pub fn new() -> Self {
|
||||
let (subsystem_req_tx, subsystem_req_rx) = mpsc::sync_channel(5);
|
||||
let (subsystem_report_tx, subsystem_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(5);
|
||||
|
||||
Self {
|
||||
subsystem_req_tx,
|
||||
subsystem_report_rx,
|
||||
mgm_request_rx: [mgm_0_mode_request_rx, mgm_1_mode_request_rx],
|
||||
mgm_report_tx: [mgm_0_mode_report_tx, mgm_1_mode_report_tx],
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
event_rx,
|
||||
assembly: Assembly::new(
|
||||
ParentQueueHelper {
|
||||
request_rx: subsystem_req_rx,
|
||||
report_tx: subsystem_report_tx,
|
||||
},
|
||||
ChildrenQueueHelper {
|
||||
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
|
||||
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
|
||||
},
|
||||
TmtcQueues { tc_rx, tm_tx },
|
||||
Duration::from_millis(20),
|
||||
event_tx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_all_queues_empty(&self) {
|
||||
assert!(
|
||||
matches!(self.tm_rx.try_recv().unwrap_err(), TryRecvError::Empty),
|
||||
"TM queue not empty"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
self.subsystem_report_rx.try_recv().unwrap_err(),
|
||||
TryRecvError::Empty
|
||||
),
|
||||
"subsystem report queue not empty"
|
||||
);
|
||||
for rx in self.mgm_request_rx.iter() {
|
||||
assert!(
|
||||
matches!(rx.try_recv().unwrap_err(), TryRecvError::Empty),
|
||||
"mgm request queue not empty"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_request_tc(
|
||||
request: types::acs::mgm_assembly::request::Request,
|
||||
) -> types::ccsds::CcsdsTcPacketOwned {
|
||||
types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(Assembly::ID, request.message_type()),
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_test() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.assert_all_queues_empty();
|
||||
tb.assembly.periodic_operation();
|
||||
tb.assert_all_queues_empty();
|
||||
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tc_commanded_transition() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(create_request_tc(mgm_assembly::request::Request::Mode(
|
||||
request::ModeRequest::SetMode(Mode::Device(DeviceMode::Normal)),
|
||||
)))
|
||||
.unwrap();
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let response = tb.tm_rx.try_recv().unwrap();
|
||||
assert_eq!(response.tm_header.sender_id, Assembly::ID);
|
||||
assert_eq!(response.tm_header.message_type, MessageType::Verification);
|
||||
let response: response::Response = postcard::from_bytes(&response.payload).unwrap();
|
||||
assert_eq!(response, response::Response::Ok);
|
||||
|
||||
let event = tb.event_rx.try_recv().expect("expected mode changed event");
|
||||
assert!(matches!(
|
||||
event,
|
||||
mgm_assembly::Event::ModeChanged(Mode::Device(DeviceMode::Normal))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_commanded_transition() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::Device(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_mgm_is_sufficient() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::Device(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// One device is sufficient.
|
||||
tb.mgm_report_tx[0]
|
||||
.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
tb.mgm_report_tx[1]
|
||||
.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_commanding_fails() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::Device(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::WrongMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_keeping_fails() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::Device(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
// This should start mode keeping.
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(tb.assembly.mode_transition_active());
|
||||
|
||||
for rx in tb.mgm_request_rx.iter() {
|
||||
let request = rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Let the mode keeping fail.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
tb.assembly.periodic_operation();
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use example_std::{HkHelperSingleSet, TmtcQueues};
|
||||
use minisim_types::acs::mgt as sim_mgt;
|
||||
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
|
||||
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
|
||||
use satrs::health::HealthTableMapSync;
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::acs::mgt::{
|
||||
self, HkSet,
|
||||
request::{ModeRequest, Request},
|
||||
response::{ModeResponse, Response},
|
||||
};
|
||||
use types::pcdu::SwitchId;
|
||||
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
use crate::device_fdir::{DeviceFdir, FdirEvent};
|
||||
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
|
||||
use crate::eps::PowerSwitchHelper;
|
||||
|
||||
// The handler blocks while waiting for a reply, so this must be well below the cycle time
|
||||
// of the ACS thread.
|
||||
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
|
||||
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
|
||||
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Interface for ideal device which never fails.
|
||||
#[derive(Default)]
|
||||
pub struct DummyInterface {
|
||||
dipole: mgt::Dipole,
|
||||
torque_end: Option<Instant>,
|
||||
}
|
||||
|
||||
impl DummyInterface {
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
|
||||
sim_mgt::Request::ApplyTorque { duration, dipole } => {
|
||||
self.dipole = dipole;
|
||||
self.torque_end = Some(Instant::now() + duration);
|
||||
sim_mgt::Reply::Ack
|
||||
}
|
||||
sim_mgt::Request::RequestHk => {
|
||||
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
|
||||
sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: if torquing {
|
||||
self.dipole
|
||||
} else {
|
||||
mgt::Dipole::default()
|
||||
},
|
||||
torquing,
|
||||
})
|
||||
}
|
||||
};
|
||||
Some(reply.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
/// Records all sent frames and returns injected reply frames.
|
||||
#[derive(Default)]
|
||||
pub struct TestInterface {
|
||||
pub sent_frames: Vec<Vec<u8>>,
|
||||
pub replies: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct SimInterface {
|
||||
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
pub sim_reply_rx: mpsc::Receiver<SimReply>,
|
||||
}
|
||||
|
||||
impl SimInterface {
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
// Replies which arrived after a previous timeout must not be mistaken for this reply.
|
||||
while self.sim_reply_rx.try_recv().is_ok() {}
|
||||
if let Err(e) = self
|
||||
.sim_request_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
|
||||
frame.to_vec(),
|
||||
)))
|
||||
{
|
||||
log::error!("failed to send MGT SIM request: {e}");
|
||||
return None;
|
||||
}
|
||||
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
|
||||
SimReply::Mgt(frame) => Some(frame),
|
||||
sim_reply => {
|
||||
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame based transport to the device. The handler implements the protocol on top of it.
|
||||
pub enum MgtCommunication {
|
||||
Dummy(DummyInterface),
|
||||
Sim(SimInterface),
|
||||
#[allow(dead_code)]
|
||||
Test(TestInterface),
|
||||
}
|
||||
|
||||
impl MgtCommunication {
|
||||
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
|
||||
/// was no reply in time.
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
|
||||
MgtCommunication::Sim(sim) => sim.transfer(frame),
|
||||
MgtCommunication::Test(test) => {
|
||||
test.sent_frames.push(frame.to_vec());
|
||||
test.replies.pop_front()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually an assembly
|
||||
/// or a subsystem.
|
||||
pub struct ModeLeafHelper {
|
||||
pub request_rx: mpsc::Receiver<ModeRequest>,
|
||||
pub report_tx: mpsc::SyncSender<ModeResponse>,
|
||||
}
|
||||
|
||||
/// Magnetorquer (MGT) device handler.
|
||||
///
|
||||
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
|
||||
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
|
||||
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
|
||||
/// a fault which is handled by the FDIR.
|
||||
///
|
||||
/// This device handler includes several components beyond the scope of commanding the device:
|
||||
///
|
||||
/// - The [MgtCommunication] structure models different communication interfaces to the
|
||||
/// physical device.
|
||||
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
|
||||
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
|
||||
/// helper.
|
||||
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
|
||||
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
|
||||
pub struct MgtHandler {
|
||||
tmtc_queues: TmtcQueues,
|
||||
pub com: MgtCommunication,
|
||||
hk_set: HkSet,
|
||||
hk_helper: HkHelperSingleSet,
|
||||
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
fdir: DeviceFdir,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
}
|
||||
|
||||
impl MgtHandler {
|
||||
pub fn new(
|
||||
tmtc_queues: TmtcQueues,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
com: MgtCommunication,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
mode_timeout: Duration,
|
||||
health_table: HealthTableMapSync,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tmtc_queues,
|
||||
com,
|
||||
hk_set: HkSet::default(),
|
||||
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
|
||||
switch_and_mode_helper: SwitchAndModeHelper::new(
|
||||
DeviceMode::Off,
|
||||
mode_timeout,
|
||||
switch_helper,
|
||||
SwitchId::Mgt,
|
||||
),
|
||||
mode_leaf_helper,
|
||||
fdir: DeviceFdir::new(
|
||||
"MGT",
|
||||
ComponentId::AcsMgt,
|
||||
health_table,
|
||||
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
|
||||
),
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> DeviceMode {
|
||||
self.switch_and_mode_helper.mode()
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
self.handle_mode_leaf_handling();
|
||||
|
||||
self.fdir
|
||||
.periodic_operation(&mut self.switch_and_mode_helper);
|
||||
self.handle_fdir_events();
|
||||
|
||||
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
|
||||
match event {
|
||||
ModeTransitionEvent::Reached(tc_commander) => {
|
||||
self.handle_mode_reached(tc_commander)
|
||||
}
|
||||
ModeTransitionEvent::Failed(tc_commander) => {
|
||||
self.handle_mode_transition_failure(tc_commander)
|
||||
}
|
||||
ModeTransitionEvent::PowerCycleDone => {
|
||||
self.fdir.handle_power_cycle_done();
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
|
||||
self.fdir
|
||||
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.ready_for_commanding() {
|
||||
self.poll_hk();
|
||||
}
|
||||
|
||||
if self.hk_helper.needs_generation() {
|
||||
self.send_telemetry(None, Response::Hk(self.hk_set));
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_hk(&mut self) {
|
||||
match self.transfer(sim_mgt::Request::RequestHk) {
|
||||
Some(sim_mgt::Reply::Hk(hk)) => {
|
||||
self.fdir.register_success();
|
||||
self.hk_set = HkSet {
|
||||
valid: true,
|
||||
dipole: hk.dipole,
|
||||
torquing: hk.torquing,
|
||||
};
|
||||
}
|
||||
reply => self.register_reply_fault(reply),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [None] if there was no reply in time or the reply frame was invalid.
|
||||
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
|
||||
let frame = self.com.transfer(&request.to_frame())?;
|
||||
sim_mgt::Reply::from_frame(&frame)
|
||||
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
|
||||
log::warn!("MGT: missing or unexpected reply {reply:?}");
|
||||
self.hk_set.valid = false;
|
||||
self.fdir.register_fault(&mut self.switch_and_mode_helper);
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
|
||||
fn handle_fdir_events(&mut self) {
|
||||
while let Some(event) = self.fdir.next_event() {
|
||||
let event = match event {
|
||||
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
|
||||
FdirEvent::Recovery(recovery_event) => {
|
||||
// The device is power cycled or switched off.
|
||||
if matches!(
|
||||
recovery_event,
|
||||
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
|
||||
) {
|
||||
self.hk_set = HkSet::default();
|
||||
}
|
||||
mgt::Event::Recovery(recovery_event)
|
||||
}
|
||||
};
|
||||
self.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn ready_for_commanding(&self) -> bool {
|
||||
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
|
||||
}
|
||||
|
||||
fn handle_telecommands(&mut self) {
|
||||
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
let request = match postcard::from_bytes::<Request>(&packet.payload) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("MGT: failed to deserialize request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"MGT: received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
|
||||
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
|
||||
Request::Mode(ModeRequest::SetMode(mode)) => {
|
||||
self.start_transition(mode, Some(tc_id))
|
||||
}
|
||||
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
|
||||
Some(tc_id),
|
||||
Response::Mode(ModeResponse::Mode(
|
||||
self.switch_and_mode_helper.reported_mode(),
|
||||
)),
|
||||
),
|
||||
Request::ApplyTorque { dipole, duration } => {
|
||||
self.handle_torque_command(tc_id, dipole, duration)
|
||||
}
|
||||
Request::Health(HealthRequest::SetHealth(health)) => {
|
||||
log::info!("MGT: setting health to {health:?} via ground command");
|
||||
self.fdir.set_health(health);
|
||||
self.send_telemetry(Some(tc_id), Response::Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
|
||||
match request {
|
||||
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
|
||||
ModeRequest::ReadMode => self.report_mode_to_parent(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
|
||||
match hk_request {
|
||||
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
|
||||
HkRequestType::EnablePeriodic(opt_interval) => {
|
||||
self.hk_helper.enabled = true;
|
||||
if let Some(interval) = opt_interval {
|
||||
self.hk_helper.frequency = interval;
|
||||
}
|
||||
}
|
||||
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
|
||||
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
|
||||
_ => log::warn!("MGT: unhandled HK request"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_torque_command(
|
||||
&mut self,
|
||||
tc_id: CcsdsPacketIdAndPsc,
|
||||
dipole: mgt::Dipole,
|
||||
duration: Duration,
|
||||
) {
|
||||
if !self.ready_for_commanding() {
|
||||
log::warn!("MGT: rejecting torque command, device not in normal mode");
|
||||
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
|
||||
return;
|
||||
}
|
||||
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
|
||||
Some(sim_mgt::Reply::Ack) => {
|
||||
self.fdir.register_success();
|
||||
self.send_telemetry(Some(tc_id), Response::Ok);
|
||||
}
|
||||
reply => {
|
||||
self.register_reply_fault(reply);
|
||||
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_transition(
|
||||
&mut self,
|
||||
target_mode: DeviceMode,
|
||||
tc_commander: Option<CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
log::info!("MGT: transitioning to mode {:?}", target_mode);
|
||||
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
|
||||
if target_mode == DeviceMode::Off {
|
||||
self.hk_set = HkSet::default();
|
||||
}
|
||||
self.switch_and_mode_helper
|
||||
.start_transition(target_mode, tc_commander);
|
||||
}
|
||||
|
||||
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
log::info!("MGT: mode {:?} reached", self.mode());
|
||||
self.send_event(mgt::Event::ModeChanged(self.mode()));
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Ok);
|
||||
}
|
||||
self.report_mode_to_parent();
|
||||
}
|
||||
|
||||
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
|
||||
}
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeResponse::SetModeTimeout)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn report_mode_to_parent(&self) {
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeResponse::Mode(
|
||||
self.switch_and_mode_helper.reported_mode(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_event(&self, event: mgt::Event) {
|
||||
if let Err(e) = self.event_tx.send(event) {
|
||||
log::warn!("MGT: failed to send event {:?}: {}", event, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||
log::warn!("MGT: failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::health::{HealthState, HealthTableProvider};
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, Message as _, TcHeader,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use crate::device_fdir::RECOVERY_THRESHOLD;
|
||||
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl TestInterface {
|
||||
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
|
||||
self.sent_frames
|
||||
.iter()
|
||||
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn push_reply(&mut self, reply: sim_mgt::Reply) {
|
||||
self.replies.push_back(reply.to_frame());
|
||||
}
|
||||
}
|
||||
|
||||
struct MgtTestbench {
|
||||
parent_request_tx: mpsc::SyncSender<ModeRequest>,
|
||||
parent_report_rx: mpsc::Receiver<ModeResponse>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
switch_rx: mpsc::Receiver<SwitchRequest>,
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_rx: mpsc::Receiver<mgt::Event>,
|
||||
health_table: HealthTableMapSync,
|
||||
handler: MgtHandler,
|
||||
}
|
||||
|
||||
impl MgtTestbench {
|
||||
fn new() -> Self {
|
||||
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
|
||||
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(20);
|
||||
let mut switch_map = SwitchMap::new();
|
||||
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
|
||||
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
|
||||
let health_table = HealthTableMapSync::default();
|
||||
let mut handler = MgtHandler::new(
|
||||
TmtcQueues { tc_rx, tm_tx },
|
||||
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
|
||||
MgtCommunication::Test(TestInterface::default()),
|
||||
ModeLeafHelper {
|
||||
request_rx,
|
||||
report_tx,
|
||||
},
|
||||
Duration::from_millis(100),
|
||||
health_table.clone(),
|
||||
event_tx,
|
||||
);
|
||||
handler.fdir.recovery_off_duration = Duration::ZERO;
|
||||
Self {
|
||||
parent_request_tx,
|
||||
parent_report_rx,
|
||||
shared_switch_set,
|
||||
switch_rx,
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
event_rx,
|
||||
health_table,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_tc(&self, request: Request) {
|
||||
self.tc_tx
|
||||
.send(CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn next_response(&self) -> Response {
|
||||
let tm = self.tm_rx.try_recv().expect("no TM generated");
|
||||
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
|
||||
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
|
||||
}
|
||||
|
||||
/// Completes the power switch handshake for a commanded switch-on.
|
||||
fn switch_to_normal(&mut self) {
|
||||
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::On);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(self.next_response(), Response::Ok);
|
||||
}
|
||||
|
||||
fn test_interface(&mut self) -> &mut TestInterface {
|
||||
match &mut self.handler.com {
|
||||
MgtCommunication::Test(test) => test,
|
||||
_ => panic!("unexpected MGT interface"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_switch_state(&self, state: SwitchState) {
|
||||
self.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgt, state);
|
||||
}
|
||||
|
||||
fn health(&self) -> Option<HealthState> {
|
||||
self.health_table.health(ComponentId::AcsMgt.into())
|
||||
}
|
||||
|
||||
fn drain_events(&self) -> Vec<mgt::Event> {
|
||||
self.event_rx.try_iter().collect()
|
||||
}
|
||||
|
||||
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
|
||||
/// reached normal mode already registered the first fault.
|
||||
fn exceed_reply_fault_threshold(&mut self) {
|
||||
for _ in 0..REPLY_FAULT_THRESHOLD {
|
||||
self.handler.periodic_operation();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drives a started power cycle recovery to completion, completing both power switch
|
||||
/// handshakes.
|
||||
fn complete_power_cycle(&mut self) {
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::Off);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Off);
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::On);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_state_no_polling() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
assert!(testbench.test_interface().sent_frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_to_normal() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
|
||||
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
|
||||
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
|
||||
testbench.set_switch_state(SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert!(matches!(
|
||||
testbench.event_rx.try_recv(),
|
||||
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_command_from_parent() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench
|
||||
.parent_request_tx
|
||||
.send(ModeRequest::SetMode(DeviceMode::Normal))
|
||||
.unwrap();
|
||||
testbench.handler.periodic_operation();
|
||||
testbench.set_switch_state(SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
// Commanded by the parent, so no TC response is expected.
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_rejected_when_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
|
||||
assert!(testbench.test_interface().sent_frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_forwarded_in_normal_mode() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.test_interface().sent_frames.clear();
|
||||
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
|
||||
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert_eq!(
|
||||
testbench.test_interface().sent_requests().first(),
|
||||
Some(&sim_mgt::Request::ApplyTorque {
|
||||
duration: Duration::from_millis(100),
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_without_ack() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_polling_updates_hk_set() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
assert!(
|
||||
testbench
|
||||
.test_interface()
|
||||
.sent_requests()
|
||||
.contains(&sim_mgt::Request::RequestHk)
|
||||
);
|
||||
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
}));
|
||||
testbench.handler.periodic_operation();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.next_response(),
|
||||
Response::Hk(HkSet {
|
||||
valid: true,
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.drain_events();
|
||||
while testbench.parent_report_rx.try_recv().is_ok() {}
|
||||
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_request = testbench
|
||||
.switch_rx
|
||||
.try_iter()
|
||||
.last()
|
||||
.expect("no switch request");
|
||||
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
|
||||
testbench.set_switch_state(SwitchState::Off);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Off)]
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Off))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_set_invalid_after_switch_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
}));
|
||||
testbench.handler.periodic_operation();
|
||||
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_hk() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
|
||||
Duration::ZERO,
|
||||
))));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(matches!(testbench.next_response(), Response::Hk(_)));
|
||||
let tm = testbench.tm_rx.try_recv();
|
||||
assert!(tm.is_err(), "only one HK packet per cycle expected");
|
||||
|
||||
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_reply_no_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
}));
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 0);
|
||||
assert!(testbench.handler.hk_set.valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(!testbench.handler.hk_set.valid);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.test_interface().replies.push_back(vec![0xff]);
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_fault_threshold_starts_recovery() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.drain_events();
|
||||
while testbench.parent_report_rx.try_recv().is_ok() {}
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[
|
||||
mgt::Event::ReplyFaultThresholdExceeded,
|
||||
mgt::Event::Recovery(RecoveryEvent::Started)
|
||||
]
|
||||
));
|
||||
|
||||
testbench.complete_power_cycle();
|
||||
assert_eq!(testbench.health(), Some(HealthState::Healthy));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::Recovery(RecoveryEvent::Done)]
|
||||
));
|
||||
// The power cycle is hidden from the parent.
|
||||
assert!(testbench.parent_report_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unresponsive_device_marked_faulty() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
for _ in 0..RECOVERY_THRESHOLD {
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
|
||||
testbench.complete_power_cycle();
|
||||
}
|
||||
testbench.drain_events();
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::Faulty));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[
|
||||
mgt::Event::ReplyFaultThresholdExceeded,
|
||||
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
|
||||
]
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.handler.switch_and_mode_helper.target(),
|
||||
Some(DeviceMode::Off)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_health() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
|
||||
HealthState::Faulty,
|
||||
)));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert_eq!(testbench.health(), Some(HealthState::Faulty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod ctrl;
|
||||
pub mod mgm;
|
||||
pub mod mgm_assembly;
|
||||
pub mod mgt;
|
||||
pub mod subsystem;
|
||||
@@ -0,0 +1,406 @@
|
||||
#![allow(dead_code)]
|
||||
use std::{
|
||||
sync::mpsc::{self, Receiver, SyncSender},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use example_std::{ModeHelper, TmtcQueues};
|
||||
use satrs::{
|
||||
mode_tree::{
|
||||
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
|
||||
SequenceTableMapTable, SequenceTablesMapValue, TargetModeTables,
|
||||
},
|
||||
spacepackets::CcsdsPacketIdAndPsc,
|
||||
subsystem::{
|
||||
ModeCommandingResult, ModeRaw, ModeResponse, ModeSetRequest, ModeTreeHelperError,
|
||||
SubsystemCommandingHelper, SubsystemHelperResult,
|
||||
},
|
||||
};
|
||||
use types::{
|
||||
ComponentId,
|
||||
acs::subsystem::{Mode, response},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransitionState {
|
||||
#[default]
|
||||
Idle,
|
||||
AwaitingReplies,
|
||||
}
|
||||
|
||||
fn build_sequence_tables() -> SequenceModeTables {
|
||||
let mut seq_tables = SequenceModeTables::default();
|
||||
|
||||
let mut off_table = SequenceTablesMapValue::new("OFF");
|
||||
let mut off_step_0 = SequenceTableMapTable::new("OFF_STEP_0");
|
||||
off_step_0.add_entry(SequenceTableEntry::new(
|
||||
"OFF_CTRL_PASSIVE",
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Passive.into(),
|
||||
true,
|
||||
));
|
||||
off_table.add_sequence_table(off_step_0);
|
||||
let mut off_step_1 = SequenceTableMapTable::new("OFF_STEP_1");
|
||||
off_step_1.add_entry(SequenceTableEntry::new(
|
||||
"OFF_MGM_ASSY_OFF",
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off).into(),
|
||||
false,
|
||||
));
|
||||
off_step_1.add_entry(SequenceTableEntry::new(
|
||||
"OFF_MGT_OFF",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Off.into(),
|
||||
false,
|
||||
));
|
||||
off_table.add_sequence_table(off_step_1);
|
||||
seq_tables.0.insert(Mode::Off as ModeRaw, off_table);
|
||||
|
||||
let mut safe_table = SequenceTablesMapValue::new("SAFE");
|
||||
let mut safe_step_0 = SequenceTableMapTable::new("SAFE_STEP_0");
|
||||
safe_step_0.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_MGM_ASSY_NORMAL",
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal).into(),
|
||||
false,
|
||||
));
|
||||
safe_step_0.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_MGT_NORMAL",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Normal.into(),
|
||||
false,
|
||||
));
|
||||
safe_table.add_sequence_table(safe_step_0);
|
||||
let mut safe_step_1 = SequenceTableMapTable::new("SAFE_STEP_1");
|
||||
safe_step_1.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_CTRL_SAFE",
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Safe.into(),
|
||||
false,
|
||||
));
|
||||
safe_table.add_sequence_table(safe_step_1);
|
||||
seq_tables.0.insert(Mode::Safe as ModeRaw, safe_table);
|
||||
|
||||
seq_tables
|
||||
}
|
||||
|
||||
fn ctrl_response_to_mode_response(
|
||||
response: types::acs::ctrl::response::ModeReport,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsController as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::ctrl::response::ModeReport::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::ctrl::response::ModeReport::WrongMode(_) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mgm_assy_response_to_mode_response(
|
||||
response: types::acs::mgm_assembly::response::ModeResponse,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsMgmAssembly as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::mgm_assembly::response::ModeResponse::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::mgm_assembly::response::ModeResponse::SetModeTimeout(_)
|
||||
| types::acs::mgm_assembly::response::ModeResponse::WrongMode(_)
|
||||
| types::acs::mgm_assembly::response::ModeResponse::CanNotKeepMode(_) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mgt_response_to_mode_response(
|
||||
response: types::acs::mgt::response::ModeResponse,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModeRequestSenders {
|
||||
pub mode_request_ctrl: SyncSender<types::acs::ctrl::request::ModeRequest>,
|
||||
pub mode_request_mgm_assy: SyncSender<types::acs::mgm_assembly::request::ModeRequest>,
|
||||
pub mode_request_mgt: SyncSender<types::acs::mgt::request::ModeRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModeReportReceivers {
|
||||
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
|
||||
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
|
||||
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Subsystem {
|
||||
mode_helper: ModeHelper<types::acs::subsystem::Mode, TransitionState>,
|
||||
mode_request_senders: ModeRequestSenders,
|
||||
mode_report_receivers: ModeReportReceivers,
|
||||
tmtc_queues: TmtcQueues,
|
||||
subsystem_helper: SubsystemCommandingHelper,
|
||||
}
|
||||
|
||||
impl Subsystem {
|
||||
pub const ID: ComponentId = ComponentId::AcsSubsystem;
|
||||
|
||||
pub fn new(
|
||||
mode_request_senders: ModeRequestSenders,
|
||||
mode_report_receivers: ModeReportReceivers,
|
||||
tmtc_queues: TmtcQueues,
|
||||
) -> Self {
|
||||
let mut mode_store_vec = ModeStoreVec::default();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::NoModeKeeping.into(),
|
||||
)
|
||||
.unwrap();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Passive.into(),
|
||||
)
|
||||
.unwrap();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Off.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let target_tables = TargetModeTables::default();
|
||||
|
||||
let sequence_tables = build_sequence_tables();
|
||||
|
||||
Self {
|
||||
mode_helper: ModeHelper::new(
|
||||
types::acs::subsystem::Mode::Off,
|
||||
Duration::from_millis(2000),
|
||||
),
|
||||
mode_request_senders,
|
||||
mode_report_receivers,
|
||||
tmtc_queues,
|
||||
subsystem_helper: SubsystemCommandingHelper::new(
|
||||
mode_store_vec,
|
||||
target_tables,
|
||||
sequence_tables,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
let mut mode_requests = Vec::new();
|
||||
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_ctrl.try_recv() {
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(Some(ctrl_response_to_mode_response(response)), |request| {
|
||||
mode_requests.push(request)
|
||||
});
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_mgm_assy.try_recv() {
|
||||
let result = self.subsystem_helper.state_machine(
|
||||
Some(mgm_assy_response_to_mode_response(response)),
|
||||
|request| mode_requests.push(request),
|
||||
);
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_mgt.try_recv() {
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(Some(mgt_response_to_mode_response(response)), |request| {
|
||||
mode_requests.push(request)
|
||||
});
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(None, |request| mode_requests.push(request));
|
||||
self.handle_state_machine_result(result);
|
||||
|
||||
for request in mode_requests {
|
||||
self.handle_mode_set_request(request);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_state_machine_result(
|
||||
&mut self,
|
||||
result: Result<SubsystemHelperResult, ModeTreeHelperError>,
|
||||
) {
|
||||
match result {
|
||||
Ok(result) => match result {
|
||||
SubsystemHelperResult::Idle => (),
|
||||
SubsystemHelperResult::TargetKeeping => (),
|
||||
SubsystemHelperResult::ModeCommanding(mode_commanding_result) => {
|
||||
match mode_commanding_result {
|
||||
ModeCommandingResult::Done => {
|
||||
let target_mode_typed =
|
||||
self.subsystem_helper.seq_exec_helper.target_mode();
|
||||
let target_mode = target_mode_typed.map(Mode::try_from);
|
||||
log::info!(
|
||||
"ACS SS: mode commanding for target mode {:?} done",
|
||||
target_mode
|
||||
);
|
||||
self.finish_mode_transition();
|
||||
}
|
||||
ModeCommandingResult::StepDone => log::info!("mode commanding step done"),
|
||||
ModeCommandingResult::AwaitingSuccessCheck => {
|
||||
log::info!("ACS SS: mode commanding awaiting success check")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("mode tree helper error: {}", e);
|
||||
self.mode_helper.finish(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_mode_transition(&mut self) {
|
||||
let tc_commander = self.mode_helper.finish(true);
|
||||
if let Some(requestor) = tc_commander {
|
||||
self.send_telemetry(
|
||||
Some(requestor),
|
||||
response::Response::Mode(response::ModeResponse::Mode(self.mode_helper.current)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_set_request(&mut self, request: ModeSetRequest) {
|
||||
let target_id = ComponentId::try_from(request.target_id);
|
||||
if let Ok(target_id) = target_id {
|
||||
match target_id {
|
||||
ComponentId::AcsMgmAssembly => self
|
||||
.mode_request_senders
|
||||
.mode_request_mgm_assy
|
||||
.send(types::acs::mgm_assembly::request::ModeRequest::SetMode(
|
||||
types::acs::mgm_assembly::Mode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
ComponentId::AcsController => self
|
||||
.mode_request_senders
|
||||
.mode_request_ctrl
|
||||
.send(types::acs::ctrl::request::ModeRequest::SetMode(
|
||||
types::acs::ctrl::Mode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
ComponentId::AcsMgt => self
|
||||
.mode_request_senders
|
||||
.mode_request_mgt
|
||||
.send(types::acs::mgt::request::ModeRequest::SetMode(
|
||||
types::DeviceMode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
_ => {
|
||||
log::error!("invalid target ID {:?} for mode command", target_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_telecommands(&mut self) {
|
||||
loop {
|
||||
match self.tmtc_queues.tc_rx.try_recv() {
|
||||
Ok(packet) => {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
match postcard::from_bytes::<types::acs::subsystem::request::Request>(
|
||||
&packet.payload,
|
||||
) {
|
||||
Ok(request) => match request {
|
||||
types::acs::subsystem::request::Request::Ping => {
|
||||
self.send_telemetry(Some(tc_id), response::Response::Ok)
|
||||
}
|
||||
types::acs::subsystem::request::Request::Mode(mode_request) => {
|
||||
self.handle_mode_request(Some(tc_id), mode_request);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mode_request(
|
||||
&mut self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
mode_request: types::acs::subsystem::request::ModeRequest,
|
||||
) {
|
||||
match mode_request {
|
||||
types::acs::subsystem::request::ModeRequest::SetMode(target_mode) => {
|
||||
self.mode_helper.start(target_mode);
|
||||
self.mode_helper.tc_commander = tc_id;
|
||||
if let Err(e) = self
|
||||
.subsystem_helper
|
||||
.start_command_sequence(target_mode as ModeRaw)
|
||||
{
|
||||
log::error!("error starting command sequence: {}", e);
|
||||
}
|
||||
}
|
||||
types::acs::subsystem::request::ModeRequest::ReadMode => {
|
||||
self.send_telemetry(
|
||||
None,
|
||||
response::Response::Mode(response::ModeResponse::Mode(
|
||||
self.mode_helper.current,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_telemetry(
|
||||
&self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
response: types::acs::subsystem::response::Response,
|
||||
) {
|
||||
match crate::ccsds::pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||
log::warn!("failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to pack TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{
|
||||
CcsdsPacketIdAndPsc, SpHeader,
|
||||
time::{StdTimestampError, cds::CdsTime},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use types::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CcsdsTmCreationError {
|
||||
#[error("postcard error: {0}")]
|
||||
Postcard(#[from] postcard::Error),
|
||||
#[error("timestamp error: {0}")]
|
||||
Time(#[from] StdTimestampError),
|
||||
}
|
||||
|
||||
pub fn pack_ccsds_tm_packet_for_now(
|
||||
sender_id: ComponentId,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
payload: &(impl Serialize + Message),
|
||||
) -> Result<CcsdsTmPacketOwned, CcsdsTmCreationError> {
|
||||
let now = CdsTime::now_with_u16_days()?;
|
||||
let sp_header = SpHeader::new_from_apid(u11::new(Apid::Tmtc as u16));
|
||||
let tm_header = TmHeader::new(
|
||||
sender_id,
|
||||
ComponentId::Ground,
|
||||
payload.message_type(),
|
||||
tc_id,
|
||||
&now,
|
||||
);
|
||||
Ok(CcsdsTmPacketOwned::new_with_serde_payload(
|
||||
sp_header, &tm_header, payload,
|
||||
)?)
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
use arbitrary_int::u11;
|
||||
use lazy_static::lazy_static;
|
||||
use satrs::{
|
||||
res_code::ResultU16,
|
||||
legacy::res_code::ResultU16,
|
||||
spacepackets::{PacketId, PacketType},
|
||||
};
|
||||
use satrs_mib::res_code::ResultU16Info;
|
||||
use satrs_mib::resultcode;
|
||||
use std::{collections::HashSet, net::Ipv4Addr};
|
||||
use strum::IntoEnumIterator;
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs::{
|
||||
events::{EventU32TypedSev, SeverityInfo},
|
||||
pool::{StaticMemoryPool, StaticPoolConfig},
|
||||
};
|
||||
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
@@ -38,20 +36,17 @@ pub enum GroupId {
|
||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||
pub const SERVER_PORT: u16 = 7301;
|
||||
|
||||
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
|
||||
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
|
||||
let mut set = HashSet::new();
|
||||
for id in components::Apid::iter() {
|
||||
set.insert(PacketId::new(PacketType::Tc, true, id as u16));
|
||||
for id in types::Apid::iter() {
|
||||
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
|
||||
}
|
||||
set
|
||||
};
|
||||
pub static ref APID_VALIDATOR: HashSet<u16> = {
|
||||
let mut set = HashSet::new();
|
||||
for id in components::Apid::iter() {
|
||||
for id in types::Apid::iter() {
|
||||
set.insert(id as u16);
|
||||
}
|
||||
set
|
||||
@@ -123,67 +118,16 @@ pub mod mode_err {
|
||||
}
|
||||
|
||||
pub mod components {
|
||||
use satrs::request::UniqueApidTargetId;
|
||||
use strum::EnumIter;
|
||||
use satrs::ComponentId;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, EnumIter)]
|
||||
pub enum Apid {
|
||||
Sched = 1,
|
||||
GenericPus = 2,
|
||||
Acs = 3,
|
||||
Cfdp = 4,
|
||||
Tmtc = 5,
|
||||
}
|
||||
|
||||
// Component IDs for components with the PUS APID.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PusId {
|
||||
PusEventManagement = 0,
|
||||
PusRouting = 1,
|
||||
PusTest = 2,
|
||||
PusAction = 3,
|
||||
PusMode = 4,
|
||||
PusHk = 5,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum AcsId {
|
||||
Mgm0 = 0,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TmtcId {
|
||||
UdpServer = 0,
|
||||
TcpServer = 1,
|
||||
}
|
||||
|
||||
pub const PUS_ACTION_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusAction as u32);
|
||||
pub const PUS_EVENT_MANAGEMENT: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, 0);
|
||||
pub const PUS_ROUTING_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusRouting as u32);
|
||||
pub const PUS_TEST_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusTest as u32);
|
||||
pub const PUS_MODE_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusMode as u32);
|
||||
pub const PUS_HK_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusHk as u32);
|
||||
pub const PUS_SCHED_SERVICE: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::Sched as u16, 0);
|
||||
pub const MGM_HANDLER_0: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::Acs as u16, AcsId::Mgm0 as u32);
|
||||
pub const UDP_SERVER: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::UdpServer as u32);
|
||||
pub const TCP_SERVER: UniqueApidTargetId =
|
||||
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::TcpServer as u32);
|
||||
pub const NO_SENDER: ComponentId = ComponentId::MAX;
|
||||
}
|
||||
|
||||
pub mod pool {
|
||||
use super::*;
|
||||
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
|
||||
(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
@@ -194,7 +138,7 @@ pub mod pool {
|
||||
],
|
||||
true,
|
||||
)),
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
@@ -209,7 +153,7 @@ pub mod pool {
|
||||
}
|
||||
|
||||
pub fn create_sched_tc_pool() -> StaticMemoryPool {
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
@@ -225,7 +169,7 @@ pub mod pool {
|
||||
|
||||
pub mod tasks {
|
||||
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
||||
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
|
||||
pub const FREQ_MS_AOCS: u64 = 500;
|
||||
pub const FREQ_MS_PUS_STACK: u64 = 200;
|
||||
pub const FREQ_MS_AOCS: u64 = 200;
|
||||
pub const FREQ_MS_CONTROLLER: u64 = 200;
|
||||
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
control,
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
pub struct Controller {
|
||||
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_rx,
|
||||
tm_tx,
|
||||
event_ctrl_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
}
|
||||
|
||||
pub fn handle_telecommands(&mut self) {
|
||||
loop {
|
||||
match self.tc_rx.try_recv() {
|
||||
Ok(packet) => {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
|
||||
Ok(request) => {
|
||||
log::info!(
|
||||
"received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
control::request::Request::Ping => (),
|
||||
control::request::Request::TestEvent => {
|
||||
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
|
||||
}
|
||||
control::request::Request::SimConnect(_ipv4_addr) => {
|
||||
// TODO: Does this make sense here? I guess it does to try
|
||||
// a connect when the minisim is started after the OBSW?
|
||||
log::warn!("sim connect request not supported yet");
|
||||
}
|
||||
}
|
||||
self.send_telemetry(Some(tc_id), control::response::Response::Ok);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
std::sync::mpsc::TryRecvError::Empty => break,
|
||||
std::sync::mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("packet sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_telemetry(
|
||||
&self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
response: control::response::Response,
|
||||
) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tm_tx.send(packet) {
|
||||
log::warn!("failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to pack TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
|
||||
use satrs::health::{HealthState, HealthTableMapSync};
|
||||
use types::{ComponentId, DeviceMode};
|
||||
|
||||
use crate::device_mode::SwitchAndModeHelper;
|
||||
|
||||
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
|
||||
// before the counter is decremented again.
|
||||
pub const RECOVERY_THRESHOLD: u32 = 2;
|
||||
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
|
||||
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
|
||||
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Generic FDIR events. The device handler maps them to its own event type.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum FdirEvent {
|
||||
FaultThresholdExceeded,
|
||||
Recovery(RecoveryEvent),
|
||||
}
|
||||
|
||||
/// Fault counting and power cycle recovery for device handlers which own the power switch of
|
||||
/// their device.
|
||||
///
|
||||
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
|
||||
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
|
||||
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
|
||||
/// events with [Self::next_event].
|
||||
pub struct DeviceFdir {
|
||||
name: &'static str,
|
||||
fault_counter: FaultCounterStd,
|
||||
recovery: RecoveryFdir<HealthTableMapSync>,
|
||||
pub recovery_off_duration: Duration,
|
||||
events: VecDeque<FdirEvent>,
|
||||
}
|
||||
|
||||
impl DeviceFdir {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
component_id: ComponentId,
|
||||
health_table: HealthTableMapSync,
|
||||
fault_counter: FaultCounterStd,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
fault_counter,
|
||||
recovery: RecoveryFdir::new(
|
||||
component_id.into(),
|
||||
health_table,
|
||||
RECOVERY_THRESHOLD,
|
||||
RECOVERY_DECREMENT_AFTER,
|
||||
),
|
||||
recovery_off_duration: RECOVERY_OFF_DURATION,
|
||||
events: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fault_count(&self) -> u32 {
|
||||
self.fault_counter.fault_count()
|
||||
}
|
||||
|
||||
pub fn set_health(&mut self, health: HealthState) {
|
||||
self.recovery.set_health(health);
|
||||
}
|
||||
|
||||
pub fn next_event(&mut self) -> Option<FdirEvent> {
|
||||
self.events.pop_front()
|
||||
}
|
||||
|
||||
/// Should be called once per cycle, before the mode transition is handled. Starts a power
|
||||
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
|
||||
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
self.recovery.periodic_operation();
|
||||
self.check_needs_recovery(modes);
|
||||
}
|
||||
|
||||
pub fn register_success(&mut self) {
|
||||
self.fault_counter.try_decrement();
|
||||
}
|
||||
|
||||
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
|
||||
/// too often, the component is marked faulty and commanded off instead.
|
||||
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
if !self.fault_counter.increment_and_check() {
|
||||
return;
|
||||
}
|
||||
match self.recovery.handle_fault() {
|
||||
FaultResponse::Ignored => {
|
||||
log::info!(
|
||||
"{}: fault threshold exceeded, but component is already faulty, \
|
||||
recovering or externally controlled",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
FaultResponse::Recover => {
|
||||
log::warn!(
|
||||
"{}: fault threshold exceeded, power cycling device",
|
||||
self.name
|
||||
);
|
||||
self.events.push_back(FdirEvent::FaultThresholdExceeded);
|
||||
self.check_needs_recovery(modes);
|
||||
}
|
||||
FaultResponse::SetFaulty => {
|
||||
log::error!(
|
||||
"{}: fault threshold exceeded after too many recoveries, marking \
|
||||
component faulty",
|
||||
self.name
|
||||
);
|
||||
self.events.push_back(FdirEvent::FaultThresholdExceeded);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
|
||||
self.switch_off_faulty_device(modes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mode commands from ground or the parent abort a running recovery. Must be called before
|
||||
/// the commanded transition is started.
|
||||
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
|
||||
if modes.power_cycle_active() {
|
||||
log::warn!("{}: mode command aborts power cycle recovery", self.name);
|
||||
// Otherwise, the recovery would restart right away.
|
||||
self.recovery.recovery_done();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_power_cycle_done(&mut self) {
|
||||
log::info!("{}: power cycle recovery done", self.name);
|
||||
// Faults registered while the device was switched off do not count anymore.
|
||||
self.fault_counter.clear();
|
||||
self.recovery.recovery_done();
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
|
||||
}
|
||||
|
||||
/// A failed power cycle costs a recovery attempt like any other fault.
|
||||
pub fn handle_power_cycle_failed(
|
||||
&mut self,
|
||||
modes: &mut SwitchAndModeHelper<DeviceMode>,
|
||||
restore_mode: DeviceMode,
|
||||
) {
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
|
||||
match self.recovery.recovery_failed() {
|
||||
FaultResponse::Recover => {
|
||||
log::warn!("{}: power cycle recovery failed, retrying", self.name);
|
||||
self.start_recovery(modes, restore_mode);
|
||||
}
|
||||
FaultResponse::SetFaulty => {
|
||||
log::error!(
|
||||
"{}: power cycle recovery failed too often, marking component faulty",
|
||||
self.name
|
||||
);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
|
||||
self.switch_off_faulty_device(modes);
|
||||
}
|
||||
// Ground changed the health during the recovery and is in charge now.
|
||||
FaultResponse::Ignored => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if modes.mode() == DeviceMode::Off {
|
||||
// Nothing to power cycle, the next switch-on is a fresh start anyway.
|
||||
log::info!("{}: device is off, no recovery required", self.name);
|
||||
self.recovery.recovery_done();
|
||||
return;
|
||||
}
|
||||
let restore_mode = modes.mode();
|
||||
self.start_recovery(modes, restore_mode);
|
||||
}
|
||||
|
||||
fn start_recovery(
|
||||
&mut self,
|
||||
modes: &mut SwitchAndModeHelper<DeviceMode>,
|
||||
restore_mode: DeviceMode,
|
||||
) {
|
||||
log::warn!("{}: starting power cycle recovery", self.name);
|
||||
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
|
||||
}
|
||||
|
||||
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
// Do not restart an already pending Off transition, which would reset the transition
|
||||
// state machine before it can finish.
|
||||
if modes.target() != Some(DeviceMode::Off) {
|
||||
log::warn!("{}: commanding device off due to fault", self.name);
|
||||
modes.start_transition(DeviceMode::Off, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use types::pcdu::SwitchId;
|
||||
|
||||
use crate::eps::PowerSwitchHelper;
|
||||
|
||||
/// This is a helper trait required to make [SwitchAndModeHelper] generic.
|
||||
///
|
||||
/// It allows distinguish a powered-off state from one or more powered-on states, so
|
||||
/// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode.
|
||||
pub trait PowerSwitchedMode: Copy + PartialEq {
|
||||
const OFF: Self;
|
||||
fn requires_power(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PowerSwitchedMode for types::DeviceMode {
|
||||
const OFF: Self = types::DeviceMode::Off;
|
||||
|
||||
fn requires_power(&self) -> bool {
|
||||
*self != types::DeviceMode::Off
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
enum SwitchTransitionState {
|
||||
#[default]
|
||||
Idle,
|
||||
PowerSwitching,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Outcome of a single power switch transition.
|
||||
enum SwitchOutcome {
|
||||
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
}
|
||||
|
||||
/// Dedicated states for power cycling a device.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PowerCycleState<Mode> {
|
||||
Idle,
|
||||
SwitchingOff { restore_mode: Mode },
|
||||
WaitingOff { restore_mode: Mode, since: Instant },
|
||||
SwitchingOn { restore_mode: Mode },
|
||||
}
|
||||
|
||||
/// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`]
|
||||
/// has driven it to completion. Carries back whichever TC commanded the transition, if any, so
|
||||
/// the caller can reply to it -- what that reply looks like is handler-specific, so this stays
|
||||
/// out of the helper.
|
||||
pub enum ModeTransitionEvent<Mode> {
|
||||
/// The target mode was reached.
|
||||
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
/// The target mode could not be reached.
|
||||
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
/// The power cycle completed and the mode before the power cycle was restored.
|
||||
PowerCycleDone,
|
||||
/// Power switching failed during the power cycle. The power cycle is not hidden anymore,
|
||||
/// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. `restore_mode` is
|
||||
/// the mode the power cycle should have restored, which can be used to retry it.
|
||||
PowerCycleFailed { restore_mode: Mode },
|
||||
}
|
||||
|
||||
/// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done)
|
||||
/// shared by every device handler that owns a single power switch of its own.
|
||||
///
|
||||
/// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to
|
||||
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
|
||||
/// transition finishes (or fails) and leaves what to do about it to the caller.
|
||||
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
|
||||
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
switch_id: SwitchId,
|
||||
power_cycle_state: PowerCycleState<Mode>,
|
||||
power_cycle_off_duration: Duration,
|
||||
}
|
||||
|
||||
impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
|
||||
pub fn new(
|
||||
init_mode: Mode,
|
||||
timeout: Duration,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
switch_id: SwitchId,
|
||||
) -> Self {
|
||||
Self {
|
||||
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
|
||||
switch_helper,
|
||||
switch_id,
|
||||
power_cycle_state: PowerCycleState::Idle,
|
||||
power_cycle_off_duration: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode_helper.current
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn target(&self) -> Option<Mode> {
|
||||
self.mode_helper.target
|
||||
}
|
||||
|
||||
/// Mode which should be reported to other components. A power cycle is hidden from them,
|
||||
/// so this is the mode which is restored after the power cycle while one is active.
|
||||
pub fn reported_mode(&self) -> Mode {
|
||||
match self.power_cycle_state {
|
||||
PowerCycleState::SwitchingOff { restore_mode }
|
||||
| PowerCycleState::WaitingOff { restore_mode, .. }
|
||||
| PowerCycleState::SwitchingOn { restore_mode } => restore_mode,
|
||||
PowerCycleState::Idle => self.mode(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn power_cycle_active(&self) -> bool {
|
||||
self.power_cycle_state != PowerCycleState::Idle
|
||||
}
|
||||
|
||||
/// Starts a new transition, aborting a running power cycle.
|
||||
pub fn start_transition(
|
||||
&mut self,
|
||||
target_mode: Mode,
|
||||
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
self.start_transition_internal(target_mode, tc_commander);
|
||||
}
|
||||
|
||||
/// Switches the device off, keeps it off for `off_duration` and then switches it to
|
||||
/// `restore_mode`. Reaching the intermediate off mode does not generate an event.
|
||||
pub fn start_power_cycle(&mut self, restore_mode: Mode, off_duration: Duration) {
|
||||
self.power_cycle_state = PowerCycleState::SwitchingOff { restore_mode };
|
||||
self.power_cycle_off_duration = off_duration;
|
||||
self.start_transition_internal(Mode::OFF, None);
|
||||
}
|
||||
|
||||
fn start_transition_internal(
|
||||
&mut self,
|
||||
target_mode: Mode,
|
||||
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
self.mode_helper.tc_commander = tc_commander;
|
||||
self.mode_helper.start(target_mode);
|
||||
}
|
||||
|
||||
/// This is the main API that the periodic handler of a device handler should call.
|
||||
///
|
||||
/// It handles the switch commanding and returns relevant events.
|
||||
pub fn handle_mode_transition(&mut self) -> Option<ModeTransitionEvent<Mode>> {
|
||||
// The most probable case: Nothing to do.
|
||||
if self.target().is_none() && !self.power_cycle_active() {
|
||||
return None;
|
||||
}
|
||||
// Handle this as an extra step so the switch transition after this can proceed.
|
||||
self.handle_waiting_for_off_when_power_cycling();
|
||||
// Core logic: Command the switches, check whether target switch state was reached.
|
||||
// Note the ?: if a switch transition is on-going, we might do an early return.
|
||||
let outcome = self.handle_switch_transition()?;
|
||||
// Regular mode transition without power cycling.
|
||||
if self.power_cycle_state == PowerCycleState::Idle {
|
||||
return Some(match outcome {
|
||||
SwitchOutcome::Reached(tc_commander) => ModeTransitionEvent::Reached(tc_commander),
|
||||
SwitchOutcome::Failed(tc_commander) => ModeTransitionEvent::Failed(tc_commander),
|
||||
});
|
||||
}
|
||||
// Power cycling, where a bit more logic is required.
|
||||
// Handle the error case first.
|
||||
if let SwitchOutcome::Failed(_) = outcome {
|
||||
let restore_mode = self.reported_mode();
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
return Some(ModeTransitionEvent::PowerCycleFailed { restore_mode });
|
||||
}
|
||||
// At this point: The switching was succesfull, so we only match on the
|
||||
// power cycle state.
|
||||
match self.power_cycle_state {
|
||||
// No switching going on for thse cases.
|
||||
PowerCycleState::Idle | PowerCycleState::WaitingOff { .. } => None,
|
||||
PowerCycleState::SwitchingOff { restore_mode } => {
|
||||
self.power_cycle_state = PowerCycleState::WaitingOff {
|
||||
restore_mode,
|
||||
since: Instant::now(),
|
||||
};
|
||||
None
|
||||
}
|
||||
PowerCycleState::SwitchingOn { .. } => {
|
||||
// Power is back and we are done.
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
Some(ModeTransitionEvent::PowerCycleDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_waiting_for_off_when_power_cycling(&mut self) {
|
||||
if let PowerCycleState::WaitingOff {
|
||||
restore_mode,
|
||||
since,
|
||||
} = self.power_cycle_state
|
||||
&& since.elapsed() >= self.power_cycle_off_duration
|
||||
{
|
||||
self.power_cycle_state = PowerCycleState::SwitchingOn { restore_mode };
|
||||
self.start_transition_internal(restore_mode, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_switch_transition(&mut self) -> Option<SwitchOutcome> {
|
||||
let target_mode = self.mode_helper.target?;
|
||||
let switch_target_on = target_mode.requires_power();
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::Idle {
|
||||
let result = if switch_target_on {
|
||||
self.switch_helper.send_switch_on_cmd(self.switch_id)
|
||||
} else {
|
||||
self.switch_helper.send_switch_off_cmd(self.switch_id)
|
||||
};
|
||||
if result.is_err() {
|
||||
// Could not send switch command.. still continue with transition.
|
||||
log::error!(
|
||||
"failed to send switch {} command",
|
||||
if switch_target_on { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching;
|
||||
}
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching {
|
||||
if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on {
|
||||
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
|
||||
self.mode_helper.transition_state = SwitchTransitionState::Done;
|
||||
} else if self.mode_helper.timed_out() {
|
||||
return Some(SwitchOutcome::Failed(self.mode_helper.finish(false)));
|
||||
}
|
||||
}
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::Done {
|
||||
return Some(SwitchOutcome::Reached(self.mode_helper.finish(true)));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||
use types::{
|
||||
DeviceMode,
|
||||
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_millis(50);
|
||||
|
||||
struct Testbench {
|
||||
helper: SwitchAndModeHelper<DeviceMode>,
|
||||
switch_rx: mpsc::Receiver<SwitchRequest>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
|
||||
let mut switch_map = SwitchMap::new();
|
||||
switch_map.insert(SwitchId::Mgm0, SwitchState::Off);
|
||||
let shared_switch_set: SharedSwitchSet =
|
||||
Arc::new(Mutex::new(SwitchSet::new(switch_map)));
|
||||
Self {
|
||||
helper: SwitchAndModeHelper::new(
|
||||
DeviceMode::Off,
|
||||
TIMEOUT,
|
||||
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
|
||||
SwitchId::Mgm0,
|
||||
),
|
||||
switch_rx,
|
||||
shared_switch_set,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_switch_state(&self, state: SwitchState) {
|
||||
self.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgm0, state);
|
||||
}
|
||||
|
||||
fn switch_requests(&self) -> Vec<SwitchStateBinary> {
|
||||
self.switch_rx
|
||||
.try_iter()
|
||||
.map(|req| req.target_state)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Drives a transition to `Normal` to completion.
|
||||
fn switch_to_normal(&mut self) {
|
||||
self.helper.start_transition(DeviceMode::Normal, None);
|
||||
self.set_switch_state(SwitchState::On);
|
||||
assert!(matches!(
|
||||
self.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
self.switch_requests();
|
||||
}
|
||||
|
||||
/// Starts a power cycle from `Normal` and drives it until the device is off.
|
||||
fn power_cycle_until_off(&mut self, off_duration: Duration) {
|
||||
self.switch_to_normal();
|
||||
self.helper
|
||||
.start_power_cycle(DeviceMode::Normal, off_duration);
|
||||
assert!(self.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]);
|
||||
self.set_switch_state(SwitchState::Off);
|
||||
assert!(self.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(self.helper.mode(), DeviceMode::Off);
|
||||
}
|
||||
}
|
||||
|
||||
fn tc_id() -> CcsdsPacketIdAndPsc {
|
||||
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_transition() {
|
||||
let mut tb = Testbench::new();
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_on() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.helper
|
||||
.start_transition(DeviceMode::Normal, Some(tc_id()));
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
match tb.helper.handle_mode_transition() {
|
||||
Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()),
|
||||
_ => panic!("expected mode reached event with TC commander"),
|
||||
}
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_off() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper.start_transition(DeviceMode::Off, None);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]);
|
||||
tb.set_switch_state(SwitchState::Off);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_already_in_target_state() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
tb.helper.start_transition(DeviceMode::On, None);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
// The switch command is still sent.
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::On);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.helper
|
||||
.start_transition(DeviceMode::Normal, Some(tc_id()));
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
match tb.helper.handle_mode_transition() {
|
||||
Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()),
|
||||
_ => panic!("expected mode failed event with TC commander"),
|
||||
}
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.power_cycle_active());
|
||||
|
||||
// The off duration elapsed, so switching on starts right away.
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleDone)
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_reports_restored_mode() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper
|
||||
.start_power_cycle(DeviceMode::Normal, Duration::from_secs(60));
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
tb.helper.handle_mode_transition();
|
||||
tb.set_switch_state(SwitchState::Off);
|
||||
tb.helper.handle_mode_transition();
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_reports_restored_mode_while_switching_on() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_waits_off_duration() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::from_secs(60));
|
||||
for _ in 0..3 {
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
}
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_switch_off_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper
|
||||
.start_power_cycle(DeviceMode::Normal, Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleFailed {
|
||||
restore_mode: DeviceMode::Normal
|
||||
})
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_switch_on_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleFailed {
|
||||
restore_mode: DeviceMode::Normal
|
||||
})
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
// The failed power cycle is not hidden anymore.
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transition_aborts_power_cycle() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::from_secs(60));
|
||||
tb.helper.start_transition(DeviceMode::On, Some(tc_id()));
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
// A regular transition event instead of a power cycle event.
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(Some(_)))
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::On);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use derive_new::new;
|
||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
|
||||
use types::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
|
||||
|
||||
use satrs::{queue::GenericSendError, request::MessageMetadata};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::eps::pcdu::SwitchMapWrapper;
|
||||
|
||||
use self::pcdu::SharedSwitchSet;
|
||||
|
||||
pub mod pcdu;
|
||||
|
||||
#[derive(new, Clone)]
|
||||
pub struct PowerSwitchHelper {
|
||||
switcher_tx: mpsc::SyncSender<SwitchRequest>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum SwitchCommandingError {
|
||||
#[error("send error: {0}")]
|
||||
Send(#[from] GenericSendError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum SwitchInfoError {
|
||||
/// This is a configuration error which should not occur.
|
||||
#[error("switch ID not in map")]
|
||||
SwitchIdNotInMap(SwitchId),
|
||||
#[error("switch set invalid")]
|
||||
SwitchSetInvalid,
|
||||
}
|
||||
|
||||
impl PowerSwitchHelper {
|
||||
pub fn send_switch_on_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
|
||||
self.switcher_tx
|
||||
.send(SwitchRequest::new(switch_id, SwitchStateBinary::On))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_switch_off_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
|
||||
self.switcher_tx
|
||||
.send(SwitchRequest::new(switch_id, SwitchStateBinary::Off))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, SwitchInfoError> {
|
||||
let switch_set = self
|
||||
.shared_switch_set
|
||||
.lock()
|
||||
.expect("failed to lock switch set");
|
||||
if !switch_set.valid {
|
||||
return Err(SwitchInfoError::SwitchSetInvalid);
|
||||
}
|
||||
|
||||
if let Some(state) = switch_set.switch_map.get(&switch_id) {
|
||||
return Ok(*state);
|
||||
}
|
||||
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn switch_delay_ms(&self) -> Duration {
|
||||
// Here, we could set device specific switch delays theoretically. Set it to this value
|
||||
// for now.
|
||||
Duration::from_millis(1000)
|
||||
}
|
||||
|
||||
pub fn is_switch_on(&self, switch_id: SwitchId) -> bool {
|
||||
if let Ok(state) = self.switch_state(switch_id) {
|
||||
state == SwitchState::On
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(new)]
|
||||
pub struct SwitchRequestInfo {
|
||||
pub requestor_info: MessageMetadata,
|
||||
pub switch_id: SwitchId,
|
||||
pub target_state: SwitchStateBinary,
|
||||
}
|
||||
|
||||
// Test switch helper which can be used for unittests.
|
||||
#[allow(dead_code)]
|
||||
pub struct TestSwitchHelper {
|
||||
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
|
||||
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
|
||||
#[allow(dead_code)]
|
||||
pub switch_delay_request_count: u32,
|
||||
pub next_switch_delay: Duration,
|
||||
pub switch_map: RefCell<SwitchMapWrapper>,
|
||||
pub switch_map_valid: bool,
|
||||
}
|
||||
|
||||
impl Default for TestSwitchHelper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
switch_requests: Default::default(),
|
||||
switch_info_requests: Default::default(),
|
||||
switch_delay_request_count: Default::default(),
|
||||
next_switch_delay: Duration::from_millis(1000),
|
||||
switch_map: Default::default(),
|
||||
switch_map_valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestSwitchHelper {
|
||||
// Helper function which can be used to force a switch to another state for test purposes.
|
||||
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
|
||||
self.switch_map.get_mut().0.insert(switch, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
};
|
||||
|
||||
use derive_new::new;
|
||||
use example_std::TimestampHelper;
|
||||
use minisim_types::{
|
||||
SimReply, SimRequestWithTime,
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator as _;
|
||||
use types::{
|
||||
ComponentId, DeviceMode,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
pcdu::{
|
||||
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
|
||||
SwitchStateBinary, SwitchesBitfield,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SwitchSet {
|
||||
pub valid: bool,
|
||||
pub switch_map: SwitchMap,
|
||||
}
|
||||
|
||||
impl SwitchSet {
|
||||
pub fn new(switch_map: SwitchMap) -> Self {
|
||||
Self {
|
||||
valid: true,
|
||||
switch_map,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_init_switches_unknown() -> Self {
|
||||
let wrapper = SwitchMapWrapper::default();
|
||||
Self::new(wrapper.0)
|
||||
}
|
||||
|
||||
pub fn as_bitfield(&self) -> Option<SwitchesBitfield> {
|
||||
for entry in SwitchId::iter() {
|
||||
if !self.switch_map.contains_key(&entry) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(
|
||||
SwitchesBitfield::builder()
|
||||
.with_magnetorquer(*self.switch_map.get(&SwitchId::Mgt).unwrap() == SwitchState::On)
|
||||
.with_mgm1(*self.switch_map.get(&SwitchId::Mgm1).unwrap() == SwitchState::On)
|
||||
.with_mgm0(*self.switch_map.get(&SwitchId::Mgm0).unwrap() == SwitchState::On)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_switch_state(&mut self, switch_id: SwitchId, state: SwitchState) -> bool {
|
||||
if !self.switch_map.contains_key(&switch_id) {
|
||||
return false;
|
||||
}
|
||||
*self.switch_map.get_mut(&switch_id).unwrap() = state;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub type SwitchMap = HashMap<SwitchId, SwitchState>;
|
||||
|
||||
pub struct SwitchMapWrapper(pub SwitchMap);
|
||||
|
||||
impl Default for SwitchMapWrapper {
|
||||
fn default() -> Self {
|
||||
let mut switch_map = SwitchMap::default();
|
||||
for entry in SwitchId::iter() {
|
||||
switch_map.insert(entry, SwitchState::Unknown);
|
||||
}
|
||||
Self(switch_map)
|
||||
}
|
||||
}
|
||||
|
||||
impl SwitchMapWrapper {
|
||||
#[allow(dead_code)]
|
||||
pub fn new_with_init_switches_off() -> Self {
|
||||
let mut switch_map = SwitchMap::default();
|
||||
for entry in SwitchId::iter() {
|
||||
switch_map.insert(entry, SwitchState::Off);
|
||||
}
|
||||
Self(switch_map)
|
||||
}
|
||||
|
||||
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
|
||||
Self(
|
||||
switch_map
|
||||
.iter()
|
||||
.map(|(key, value)| (*key, SwitchState::from(*value)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
|
||||
|
||||
pub trait SerialInterface {
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// Send some data via the serial interface.
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
|
||||
/// Receive all replies received on the serial interface so far. This function takes a closure
|
||||
/// and call its for each received packet, passing the received packet into it.
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
f: ReplyHandler,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(new)]
|
||||
pub struct SerialInterfaceToSim {
|
||||
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
pub sim_reply_rx: mpsc::Receiver<SimReply>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum SetId {
|
||||
SwitcherSet = 0,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceToSim {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
|
||||
self.sim_request_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(request))
|
||||
.expect("failed to send request to simulation");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
loop {
|
||||
match self.sim_reply_rx.try_recv() {
|
||||
Ok(reply) => {
|
||||
let reply = postcard::to_allocvec(&reply).unwrap();
|
||||
f(&reply);
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("sim reply sender has disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SerialInterfaceDummy {
|
||||
// Need interior mutability here for both fields.
|
||||
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
|
||||
pub reply_deque: RefCell<VecDeque<SimReply>>,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceDummy {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
|
||||
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
|
||||
match pcdu_req {
|
||||
PcduRequest::SwitchDevice { switch, state } => {
|
||||
switch_map_mut
|
||||
.insert(switch, state)
|
||||
.expect("switch map capacity exceeded");
|
||||
}
|
||||
PcduRequest::RequestSwitchInfo => {
|
||||
let mut reply_deque_mut = self.reply_deque.borrow_mut();
|
||||
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
|
||||
switch_map_mut.clone(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.reply_queue_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let reply = self.next_reply_serialized();
|
||||
f(&reply);
|
||||
if self.reply_queue_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialInterfaceDummy {
|
||||
fn next_reply_serialized(&self) -> Vec<u8> {
|
||||
let mut reply_deque_mut = self.reply_deque.borrow_mut();
|
||||
let next_reply = reply_deque_mut.pop_front().unwrap();
|
||||
postcard::to_allocvec(&next_reply).unwrap()
|
||||
}
|
||||
|
||||
fn reply_queue_empty(&self) -> bool {
|
||||
self.reply_deque.borrow().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SerialSimInterfaceWrapper {
|
||||
Dummy(SerialInterfaceDummy),
|
||||
Sim(SerialInterfaceToSim),
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialSimInterfaceWrapper {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
|
||||
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
|
||||
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum OpCode {
|
||||
RegularOp = 0,
|
||||
PollAndRecvReplies = 1,
|
||||
}
|
||||
|
||||
/// Example PCDU device handler.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub struct PcduHandler<ComInterface: SerialInterface> {
|
||||
dev_str: &'static str,
|
||||
switch_request_rx: mpsc::Receiver<SwitchRequest>,
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
pub com_interface: ComInterface,
|
||||
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||
mode: DeviceMode,
|
||||
stamp_helper: TimestampHelper,
|
||||
event_tx: mpsc::SyncSender<pcdu::Event>,
|
||||
}
|
||||
|
||||
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
||||
pub fn new(
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
switch_request_rx: mpsc::Receiver<SwitchRequest>,
|
||||
com_interface: ComInterface,
|
||||
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||
init_mode: DeviceMode,
|
||||
event_tx: mpsc::SyncSender<pcdu::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
dev_str: "PCDU",
|
||||
tc_rx,
|
||||
switch_request_rx,
|
||||
tm_tx,
|
||||
com_interface,
|
||||
shared_switch_map,
|
||||
stamp_helper: TimestampHelper::default(),
|
||||
// Start in normal mode by default. Assume that the PCDU itself is on by default.
|
||||
mode: init_mode,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self, op_code: OpCode) {
|
||||
match op_code {
|
||||
OpCode::RegularOp => {
|
||||
self.stamp_helper.update_from_now();
|
||||
// Handle requests.
|
||||
self.handle_telecommands();
|
||||
self.handle_switch_requests();
|
||||
// Poll the switch states and/or telemetry regularly here.
|
||||
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
|
||||
self.handle_periodic_commands();
|
||||
}
|
||||
}
|
||||
OpCode::PollAndRecvReplies => {
|
||||
self.poll_and_handle_replies();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> DeviceMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn handle_telecommands(&mut self) {
|
||||
loop {
|
||||
match self.tc_rx.try_recv() {
|
||||
Ok(packet) => {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
match postcard::from_bytes::<pcdu::request::Request>(&packet.payload) {
|
||||
Ok(request) => {
|
||||
log::info!(
|
||||
"received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
pcdu::request::Request::Ping => {
|
||||
self.send_tm(Some(tc_id), pcdu::response::Response::Ok)
|
||||
}
|
||||
pcdu::request::Request::GetSwitches => self.send_tm(
|
||||
Some(tc_id),
|
||||
pcdu::response::Response::Switches(
|
||||
self.shared_switch_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_bitfield()
|
||||
.expect("could not build switches response"),
|
||||
),
|
||||
),
|
||||
pcdu::request::Request::EnableSwitches(switches) => {
|
||||
self.handle_switches_bitfield_request(
|
||||
switches,
|
||||
SwitchStateBinary::On,
|
||||
);
|
||||
}
|
||||
pcdu::request::Request::DisableSwitches(switches) => {
|
||||
self.handle_switches_bitfield_request(
|
||||
switches,
|
||||
SwitchStateBinary::Off,
|
||||
);
|
||||
}
|
||||
pcdu::request::Request::Mode(device_mode) => {
|
||||
self.switch_mode(tc_id, device_mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
std::sync::mpsc::TryRecvError::Empty => break,
|
||||
std::sync::mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("packet sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_switches_bitfield_request(
|
||||
&mut self,
|
||||
switches: SwitchesBitfield,
|
||||
state: SwitchStateBinary,
|
||||
) {
|
||||
if switches.mgm0() {
|
||||
self.handle_device_switching(SwitchId::Mgm0, state);
|
||||
}
|
||||
if switches.mgm1() {
|
||||
self.handle_device_switching(SwitchId::Mgm1, state);
|
||||
}
|
||||
if switches.magnetorquer() {
|
||||
self.handle_device_switching(SwitchId::Mgt, state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_tm(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: pcdu::response::Response) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tm_tx.send(packet) {
|
||||
log::warn!("failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to pack TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, requestor: CcsdsPacketIdAndPsc, mode: DeviceMode) {
|
||||
log::info!("{}: transitioning to mode {:?}", self.dev_str, mode);
|
||||
self.mode = mode;
|
||||
if self.mode() == DeviceMode::Off {
|
||||
self.shared_switch_map.lock().unwrap().valid = false;
|
||||
}
|
||||
log::info!("{} announcing mode: {:?}", self.dev_str, self.mode);
|
||||
self.send_telemetry(Some(requestor), pcdu::response::Response::Ok);
|
||||
}
|
||||
|
||||
pub fn send_telemetry(
|
||||
&self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
response: pcdu::response::Response,
|
||||
) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tm_tx.send(packet) {
|
||||
log::warn!("failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to pack TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_periodic_commands(&self) {
|
||||
let pcdu_req = PcduRequest::RequestSwitchInfo;
|
||||
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
|
||||
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
|
||||
log::warn!("polling PCDU switch info failed");
|
||||
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
|
||||
log::warn!("failed to send comm error event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn handle_mode_requests(&mut self) {
|
||||
loop {
|
||||
// TODO: Only allow one set mode request per cycle?
|
||||
match self.mode_node.try_recv_mode_request() {
|
||||
Ok(opt_msg) => {
|
||||
if let Some(msg) = opt_msg {
|
||||
let result = self.handle_mode_request(msg);
|
||||
// TODO: Trigger event?
|
||||
if result.is_err() {
|
||||
log::warn!(
|
||||
"{}: mode request failed with error {:?}",
|
||||
self.dev_str,
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
satrs::queue::GenericReceiveError::Empty => {
|
||||
break;
|
||||
}
|
||||
satrs::queue::GenericReceiveError::TxDisconnected(_) => {
|
||||
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn handle_device_switching(&mut self, switch_id: SwitchId, state: SwitchStateBinary) {
|
||||
let pcdu_req = PcduRequest::SwitchDevice {
|
||||
switch: switch_id,
|
||||
state,
|
||||
};
|
||||
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
|
||||
self.com_interface
|
||||
.send(&pcdu_req_ser)
|
||||
.expect("failed to send switch request to PCDU");
|
||||
}
|
||||
|
||||
pub fn handle_switch_requests(&mut self) {
|
||||
loop {
|
||||
match self.switch_request_rx.try_recv() {
|
||||
Ok(switch_req) => {
|
||||
self.handle_device_switching(switch_req.switch_id(), switch_req.target_state());
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("switch request receiver has disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_and_handle_replies(&mut self) {
|
||||
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
|
||||
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
|
||||
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
|
||||
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
|
||||
return;
|
||||
};
|
||||
match pcdu_reply {
|
||||
PcduReply::SwitchInfo(switch_info) => {
|
||||
let switch_map_wrapper =
|
||||
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
|
||||
let mut shared_switch_map = self
|
||||
.shared_switch_map
|
||||
.lock()
|
||||
.expect("failed to lock switch map");
|
||||
shared_switch_map.switch_map = switch_map_wrapper.0;
|
||||
shared_switch_map.valid = true;
|
||||
}
|
||||
}
|
||||
}) {
|
||||
log::warn!("receiving PCDU replies failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, TcHeader,
|
||||
pcdu::{SwitchMapBinary, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn create_request_tc(
|
||||
request: types::pcdu::request::Request,
|
||||
) -> types::ccsds::CcsdsTcPacketOwned {
|
||||
types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Eps as u16)),
|
||||
TcHeader::new(ComponentId::EpsPcdu, request.message_type()),
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SerialInterfaceTest {
|
||||
pub inner: SerialInterfaceDummy,
|
||||
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
|
||||
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
|
||||
/// Makes the next `send` call fail, to exercise comm-error handling.
|
||||
pub fail_next_send: RefCell<bool>,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceTest {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
if self.fail_next_send.replace(false) {
|
||||
return Err(());
|
||||
}
|
||||
let mut send_queue_mut = self.send_queue.borrow_mut();
|
||||
send_queue_mut.push_back(data.to_vec());
|
||||
self.inner.send(data)
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.inner.reply_queue_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let reply = self.inner.next_reply_serialized();
|
||||
self.reply_queue.borrow_mut().push_back(reply.clone());
|
||||
f(&reply);
|
||||
if self.inner.reply_queue_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PcduTestbench {
|
||||
pub mode_request_tx: mpsc::SyncSender<types::pcdu::request::Request>,
|
||||
pub mode_reply_rx_to_parent: mpsc::Receiver<types::pcdu::response::Response>,
|
||||
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
pub switch_request_tx: mpsc::Sender<SwitchRequest>,
|
||||
pub event_rx: mpsc::Receiver<pcdu::Event>,
|
||||
pub handler: PcduHandler<SerialInterfaceTest>,
|
||||
}
|
||||
|
||||
impl PcduTestbench {
|
||||
pub fn new() -> Self {
|
||||
let (mode_request_tx, _mode_request_rx) = mpsc::sync_channel(5);
|
||||
let (_mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(5);
|
||||
let shared_switch_map =
|
||||
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
|
||||
let handler = PcduHandler::new(
|
||||
tc_rx,
|
||||
tm_tx.clone(),
|
||||
switch_reqest_rx,
|
||||
SerialInterfaceTest::default(),
|
||||
shared_switch_map,
|
||||
DeviceMode::Off,
|
||||
event_tx,
|
||||
);
|
||||
Self {
|
||||
mode_request_tx,
|
||||
mode_reply_rx_to_parent,
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
switch_request_tx,
|
||||
event_rx,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
|
||||
// Check that there is now communication happening.
|
||||
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||
drop(send_queue_mut);
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
|
||||
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
|
||||
}
|
||||
|
||||
pub fn verify_switch_req_was_sent(
|
||||
&self,
|
||||
expected_queue_len: usize,
|
||||
switch_id: SwitchId,
|
||||
target_state: SwitchStateBinary,
|
||||
) {
|
||||
// Check that there is now communication happening.
|
||||
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||
drop(send_queue_mut);
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
|
||||
assert_eq!(
|
||||
pcdu_req,
|
||||
PcduRequest::SwitchDevice {
|
||||
switch: switch_id,
|
||||
state: target_state
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_switch_reply_received(
|
||||
&self,
|
||||
expected_queue_len: usize,
|
||||
expected_map: SwitchMapBinary,
|
||||
) {
|
||||
// Check that a switch reply was read back.
|
||||
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
|
||||
assert_eq!(reply_received_mut.len(), expected_queue_len);
|
||||
let reply_received = reply_received_mut.pop_front().unwrap();
|
||||
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
|
||||
assert_eq!(
|
||||
sim_reply,
|
||||
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_command_send_failure_sends_event() {
|
||||
let testbench = PcduTestbench::new();
|
||||
*testbench.handler.com_interface.fail_next_send.borrow_mut() = true;
|
||||
testbench.handler.handle_periodic_commands();
|
||||
let event = testbench
|
||||
.event_rx
|
||||
.try_recv()
|
||||
.expect("expected comm error event");
|
||||
assert!(matches!(event, pcdu::Event::SerialCommError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_handler() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||
assert_eq!(
|
||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
// Handler is OFF, no changes expected.
|
||||
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||
assert_eq!(
|
||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_mode() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
testbench
|
||||
.tc_tx
|
||||
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
// Check correctness of mode.
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||
|
||||
testbench.verify_switch_info_req_was_sent(1);
|
||||
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
|
||||
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_request_handling() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
testbench
|
||||
.tc_tx
|
||||
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
testbench
|
||||
.switch_request_tx
|
||||
.send(SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On))
|
||||
.expect("failed to send switch request");
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
|
||||
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
|
||||
testbench.verify_switch_info_req_was_sent(1);
|
||||
let mut switch_map = SwitchMapBinaryWrapper::default().0;
|
||||
*switch_map
|
||||
.get_mut(&SwitchId::Mgm0)
|
||||
.expect("switch state setting failed") = SwitchStateBinary::On;
|
||||
testbench.verify_switch_reply_received(1, switch_map);
|
||||
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId, Event, EventId, Message,
|
||||
acs::{mgm, mgm_assembly, mgt},
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
control,
|
||||
event_manager::{request::Request, response::Response},
|
||||
pcdu, tmtc,
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
pub struct EventManager {
|
||||
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||
/// Shared by all MGM instances, which is why the sender ID is part of the message.
|
||||
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
/// Shared by all TC sources, which is why the sender ID is part of the message.
|
||||
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
/// Senders with all their event TM generation disabled.
|
||||
disabled_components: HashSet<ComponentId>,
|
||||
/// Individual (sender, event ID) pairs with their TM generation disabled.
|
||||
disabled_events: HashSet<(ComponentId, u16)>,
|
||||
}
|
||||
|
||||
impl EventManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_rx,
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
disabled_components: HashSet::new(),
|
||||
disabled_events: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Silences all event TM generation for `sender_id`, regardless of the specific event.
|
||||
fn disable_component(&mut self, sender_id: ComponentId) {
|
||||
self.disabled_components.insert(sender_id);
|
||||
}
|
||||
|
||||
fn enable_component(&mut self, sender_id: ComponentId) {
|
||||
self.disabled_components.remove(&sender_id);
|
||||
}
|
||||
|
||||
/// Silences TM generation for one specific event of `sender_id`, leaving its other events
|
||||
/// unaffected.
|
||||
fn disable_event(&mut self, sender_id: ComponentId, event_id: u16) {
|
||||
self.disabled_events.insert((sender_id, event_id));
|
||||
}
|
||||
|
||||
fn enable_event(&mut self, sender_id: ComponentId, event_id: u16) {
|
||||
self.disabled_events.remove(&(sender_id, event_id));
|
||||
}
|
||||
|
||||
fn tm_generation_enabled(&self, sender_id: ComponentId, event_id: u16) -> bool {
|
||||
!self.disabled_components.contains(&sender_id)
|
||||
&& !self.disabled_events.contains(&(sender_id, event_id))
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
// Telecommands first, so filter changes already apply to the events of this cycle.
|
||||
self.handle_telecommands();
|
||||
while let Ok(event) = self.ctrl_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
|
||||
}
|
||||
while let Ok((sender_id, event)) = self.mgm_rx.try_recv() {
|
||||
self.event_to_tm(sender_id, &event);
|
||||
}
|
||||
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
|
||||
}
|
||||
while let Ok(event) = self.mgt_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgt, &event);
|
||||
}
|
||||
while let Ok(event) = self.pcdu_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::EpsPcdu, &event);
|
||||
}
|
||||
while let Ok((sender_id, event)) = self.tc_source_rx.try_recv() {
|
||||
self.event_to_tm(sender_id, &event);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_telecommands(&mut self) {
|
||||
while let Ok(packet) = self.tc_rx.try_recv() {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
let request = match postcard::from_bytes::<Request>(&packet.payload) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize event manager request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
Request::EnableComponent(sender_id) => self.enable_component(sender_id),
|
||||
Request::DisableComponent(sender_id) => self.disable_component(sender_id),
|
||||
Request::EnableEvent {
|
||||
sender_id,
|
||||
event_id,
|
||||
} => self.enable_event(sender_id, event_id),
|
||||
Request::DisableEvent {
|
||||
sender_id,
|
||||
event_id,
|
||||
} => self.disable_event(sender_id, event_id),
|
||||
}
|
||||
self.send_tm(ComponentId::EventManager, Some(tc_id), &Response::Ok);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_to_tm(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
event: &(impl serde::Serialize + Message + EventId + core::fmt::Debug),
|
||||
) {
|
||||
let tm_enabled = self.tm_generation_enabled(sender_id, event.event_id());
|
||||
log::info!(
|
||||
"event {:?} (ID {}) from {:?}{}",
|
||||
event,
|
||||
event.event_id(),
|
||||
sender_id,
|
||||
if tm_enabled { "" } else { ", TM disabled" }
|
||||
);
|
||||
if !tm_enabled {
|
||||
return;
|
||||
}
|
||||
self.send_tm(sender_id, None, event);
|
||||
}
|
||||
|
||||
fn send_tm(
|
||||
&self,
|
||||
sender_id: ComponentId,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
payload: &(impl serde::Serialize + Message),
|
||||
) {
|
||||
match pack_ccsds_tm_packet_for_now(sender_id, tc_id, payload) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tm_tx.send(packet) {
|
||||
log::warn!("error sending TM packet: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error packing TM packet: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{Apid, MessageType, TcHeader, acs::mgm};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Testbench {
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
mgm_event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_manager: EventManager,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
|
||||
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
|
||||
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
|
||||
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
|
||||
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
Self {
|
||||
tc_tx,
|
||||
mgm_event_tx,
|
||||
tm_rx,
|
||||
event_manager: EventManager::new(
|
||||
tc_rx,
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request(&self, request: Request) {
|
||||
self.tc_tx
|
||||
.send(CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(ComponentId::EventManager, MessageType::Event),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_events_enabled_by_default() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_component_silences_all_its_events() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm0);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.mgm_event_tx
|
||||
.send((
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::ModeChanged(types::DeviceMode::Normal),
|
||||
))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_component_does_not_affect_other_components() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm1);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_event_only_silences_that_specific_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_event(
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.mgm_event_tx
|
||||
.send((
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::ModeChanged(types::DeviceMode::Normal),
|
||||
))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
let tm = tb.tm_rx.try_recv().expect("expected event TM");
|
||||
let event: mgm::Event = postcard::from_bytes(&tm.payload).unwrap();
|
||||
assert!(matches!(event, mgm::Event::ModeChanged(_)));
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_re_enabling_a_component_restores_its_events() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm0);
|
||||
tb.event_manager.enable_component(ComponentId::AcsMgm0);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_component_request() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.send_request(Request::DisableComponent(ComponentId::AcsMgm0));
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
|
||||
let tm = tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
assert_eq!(tm.tm_header.sender_id, ComponentId::EventManager);
|
||||
assert!(tm.tm_header.tc_id.is_some());
|
||||
let response: Response = postcard::from_bytes(&tm.payload).unwrap();
|
||||
assert_eq!(response, Response::Ok);
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_event_request() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.send_request(Request::DisableEvent {
|
||||
sender_id: ComponentId::AcsMgm0,
|
||||
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
});
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
|
||||
tb.send_request(Request::EnableEvent {
|
||||
sender_id: ComponentId::AcsMgm0,
|
||||
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
});
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//! This module contains all component related to the direct interface of the example.
|
||||
pub mod sim_client_udp;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
@@ -0,0 +1,402 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
|
||||
use minisim_types::{SimCtrlReply, SimCtrlRequest};
|
||||
use satrs::HandlingStatus;
|
||||
|
||||
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
|
||||
|
||||
pub fn create_sim_client(
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
) -> Option<SimClientUdp> {
|
||||
match SimClientUdp::new(
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
|
||||
sim_request_rx,
|
||||
) {
|
||||
Ok(sim_client) => {
|
||||
log::info!("simulator client connection success");
|
||||
return Some(sim_client);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("sim client creation error: {e}");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SimClientCreationError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("timeout when trying to connect to sim UDP server")]
|
||||
Timeout,
|
||||
#[error("invalid ping reply when trying connection to UDP sim server: {0}")]
|
||||
InvalidReply(#[from] postcard::Error),
|
||||
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
|
||||
ReplyIsNotPong(Box<SimReply>),
|
||||
}
|
||||
|
||||
pub struct SimClientUdp {
|
||||
udp_client: UdpSocket,
|
||||
simulator_addr: SocketAddr,
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
reply_map: SimReplyMap,
|
||||
reply_buf: [u8; 4096],
|
||||
}
|
||||
|
||||
impl SimClientUdp {
|
||||
pub fn new(
|
||||
simulator_addr: SocketAddr,
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
) -> Result<Self, SimClientCreationError> {
|
||||
let mut reply_buf: [u8; 4096] = [0; 4096];
|
||||
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
|
||||
udp_client.set_read_timeout(Some(Duration::from_millis(100)))?;
|
||||
Self::attempt_connection(&mut udp_client, simulator_addr, &mut reply_buf)?;
|
||||
udp_client.set_nonblocking(true)?;
|
||||
Ok(Self {
|
||||
udp_client,
|
||||
simulator_addr,
|
||||
sim_request_rx,
|
||||
reply_map: SimReplyMap(HashMap::new()),
|
||||
reply_buf,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn attempt_connection(
|
||||
udp_client: &mut UdpSocket,
|
||||
simulator_addr: SocketAddr,
|
||||
reply_buf: &mut [u8],
|
||||
) -> Result<(), SimClientCreationError> {
|
||||
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
let sim_req_raw = postcard::to_allocvec(&sim_req).expect("failed to serialize SimRequest");
|
||||
udp_client.send_to(&sim_req_raw, simulator_addr)?;
|
||||
match udp_client.recv(reply_buf) {
|
||||
Ok(reply_len) => {
|
||||
let sim_reply: SimReply = postcard::from_bytes(&reply_buf[0..reply_len])?;
|
||||
match sim_reply {
|
||||
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
|
||||
_ => Err(SimClientCreationError::ReplyIsNotPong(Box::new(sim_reply))),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::TimedOut
|
||||
|| e.kind() == std::io::ErrorKind::WouldBlock
|
||||
{
|
||||
Err(SimClientCreationError::Timeout)
|
||||
} else {
|
||||
Err(SimClientCreationError::Io(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) -> HandlingStatus {
|
||||
let mut no_sim_requests_handled = true;
|
||||
let mut no_data_from_udp_server_received = true;
|
||||
loop {
|
||||
match self.sim_request_rx.try_recv() {
|
||||
Ok(request) => {
|
||||
let request_raw =
|
||||
postcard::to_allocvec(&request).expect("failed to serialize SimRequest");
|
||||
if let Err(e) = self.udp_client.send_to(&request_raw, self.simulator_addr) {
|
||||
log::error!("error sending data to UDP SIM server: {e}");
|
||||
break;
|
||||
} else {
|
||||
no_sim_requests_handled = false;
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => {
|
||||
break;
|
||||
}
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("SIM request sender disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match self.udp_client.recv(&mut self.reply_buf) {
|
||||
Ok(recvd_bytes) => {
|
||||
no_data_from_udp_server_received = false;
|
||||
let sim_reply_result: postcard::Result<SimReply> =
|
||||
postcard::from_bytes(&self.reply_buf[0..recvd_bytes]);
|
||||
match sim_reply_result {
|
||||
Ok(sim_reply) => {
|
||||
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
|
||||
sender.send(sim_reply).expect("failed to send SIM reply");
|
||||
} else {
|
||||
log::warn!(
|
||||
"no recipient for SIM reply from component {:?}",
|
||||
sim_reply.component()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize SIM reply: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut
|
||||
{
|
||||
break;
|
||||
}
|
||||
log::error!("error receiving data from UDP SIM server: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if no_sim_requests_handled && no_data_from_udp_server_received {
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
|
||||
pub fn add_reply_recipient(
|
||||
&mut self,
|
||||
component: ComponentId,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
) {
|
||||
self.reply_map.0.insert(component, reply_sender);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{
|
||||
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
|
||||
use types::pcdu::SwitchMapBinary;
|
||||
|
||||
use super::SimClientUdp;
|
||||
|
||||
struct UdpSimTestServer {
|
||||
udp_server: UdpSocket,
|
||||
request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_rx: mpsc::Receiver<SimReply>,
|
||||
last_sender: Option<SocketAddr>,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
recv_buf: [u8; 1024],
|
||||
}
|
||||
|
||||
impl UdpSimTestServer {
|
||||
pub fn new(
|
||||
request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_rx: mpsc::Receiver<SimReply>,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let udp_server = UdpSocket::bind("127.0.0.1:0").expect("creating UDP server failed");
|
||||
udp_server
|
||||
.set_nonblocking(true)
|
||||
.expect("failed to set UDP server to non-blocking");
|
||||
Self {
|
||||
udp_server,
|
||||
request_tx,
|
||||
reply_rx,
|
||||
last_sender: None,
|
||||
stop_signal,
|
||||
recv_buf: [0; 1024],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
loop {
|
||||
let mut no_sim_replies_handled = true;
|
||||
let mut no_data_received = true;
|
||||
if self.stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Some(last_sender) = self.last_sender {
|
||||
loop {
|
||||
match self.reply_rx.try_recv() {
|
||||
Ok(sim_reply) => {
|
||||
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
|
||||
.expect("failed to serialize SimReply");
|
||||
self.udp_server
|
||||
.send_to(&sim_reply_raw, last_sender)
|
||||
.expect("failed to send reply to client from UDP server");
|
||||
no_sim_replies_handled = false;
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
panic!("reply sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.udp_server.recv_from(&mut self.recv_buf) {
|
||||
Ok((read_bytes, from)) => {
|
||||
let sim_request: SimRequestWithTime =
|
||||
postcard::from_bytes(&self.recv_buf[0..read_bytes])
|
||||
.expect("failed to deserialize SimRequest");
|
||||
// For a ping, we perform the reply handling here directly
|
||||
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
|
||||
no_data_received = false;
|
||||
self.last_sender = Some(from);
|
||||
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
|
||||
.expect("failed to serialize SimReply");
|
||||
self.udp_server
|
||||
.send_to(&sim_reply_raw, from)
|
||||
.expect("failed to send reply to client from UDP server");
|
||||
}
|
||||
// Forward each SIM request for testing purposes.
|
||||
self.request_tx
|
||||
.send(sim_request)
|
||||
.expect("failed to send request");
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock
|
||||
&& e.kind() != std::io::ErrorKind::TimedOut
|
||||
{
|
||||
panic!("UDP server error: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if no_sim_replies_handled && no_data_received {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.udp_server.local_addr().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_connection_test() {
|
||||
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
|
||||
let (_server_sim_reply_tx, server_sim_reply_rx) = mpsc::channel();
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let mut udp_server = UdpSimTestServer::new(
|
||||
server_sim_request_tx,
|
||||
server_sim_reply_rx,
|
||||
stop_signal.clone(),
|
||||
);
|
||||
let server_addr = udp_server.local_addr();
|
||||
let (_client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
|
||||
// Need to spawn the simulator UDP server before calling the client constructor.
|
||||
let jh0 = std::thread::spawn(move || {
|
||||
udp_server.operation();
|
||||
});
|
||||
// Creating the client also performs the connection test.
|
||||
SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::SimCtrl(SimCtrlRequest::Ping)
|
||||
);
|
||||
// Stop the server.
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
jh0.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_request_reply_test() {
|
||||
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
|
||||
let (server_sim_reply_tx, sever_sim_reply_rx) = mpsc::channel();
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let mut udp_server = UdpSimTestServer::new(
|
||||
server_sim_request_tx,
|
||||
sever_sim_reply_rx,
|
||||
stop_signal.clone(),
|
||||
);
|
||||
let server_addr = udp_server.local_addr();
|
||||
let (client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
|
||||
let (client_pcdu_reply_tx, client_pcdu_reply_rx) = mpsc::channel();
|
||||
// Need to spawn the simulator UDP server before calling the client constructor.
|
||||
let jh0 = std::thread::spawn(move || {
|
||||
udp_server.operation();
|
||||
});
|
||||
|
||||
// Creating the client also performs the connection test.
|
||||
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
|
||||
client.add_reply_recipient(ComponentId::Pcdu, client_pcdu_reply_tx);
|
||||
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::SimCtrl(SimCtrlRequest::Ping)
|
||||
);
|
||||
|
||||
let pcdu_req = PcduRequest::RequestSwitchInfo;
|
||||
client_sim_req_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(pcdu_req))
|
||||
.expect("send failed");
|
||||
client.operation();
|
||||
|
||||
// Check that the request arrives properly at the server.
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::Pcdu(PcduRequest::RequestSwitchInfo)
|
||||
);
|
||||
|
||||
// We inject the reply ourselves.
|
||||
let pcdu_reply = PcduReply::SwitchInfo(SwitchMapBinary::default());
|
||||
server_sim_reply_tx
|
||||
.send(SimReply::from(pcdu_reply.clone()))
|
||||
.expect("sending PCDU reply failed");
|
||||
|
||||
// Now we verify that the reply is sent by the UDP server back to the client, and then
|
||||
// forwarded by the clients internal map.
|
||||
let mut pcdu_reply_received = false;
|
||||
for _ in 0..3 {
|
||||
client.operation();
|
||||
|
||||
match client_pcdu_reply_rx.try_recv() {
|
||||
Ok(sim_reply) => {
|
||||
assert_eq!(sim_reply.component(), ComponentId::Pcdu);
|
||||
assert_eq!(sim_reply, SimReply::Pcdu(pcdu_reply.clone()));
|
||||
pcdu_reply_received = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => std::thread::sleep(Duration::from_millis(10)),
|
||||
mpsc::TryRecvError::Disconnected => panic!("reply sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if !pcdu_reply_received {
|
||||
panic!("no reply received");
|
||||
}
|
||||
|
||||
// Stop the server.
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
jh0.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use log::{info, warn};
|
||||
use satrs::hal::std::tcp_spacepackets_server::CcsdsPacketParser;
|
||||
use satrs::{
|
||||
encoding::ccsds::{SpValidity, SpacePacketValidator},
|
||||
hal::std::tcp_server::{HandledConnectionHandler, ServerConfig, TcpSpacepacketsServer},
|
||||
spacepackets::{CcsdsPacket, PacketId},
|
||||
tmtc::{PacketSenderRaw, PacketSource},
|
||||
tmtc::PacketSource,
|
||||
};
|
||||
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConnectionFinishedHandler {}
|
||||
|
||||
@@ -29,7 +31,7 @@ impl SpacePacketValidator for SimplePacketValidator {
|
||||
if self.valid_ids.contains(&sp_header.packet_id()) {
|
||||
return SpValidity::Valid;
|
||||
}
|
||||
log::warn!("ignoring space packet with header {:?}", sp_header);
|
||||
log::warn!("ignoring space packet with header {sp_header:?}");
|
||||
// We could perform a CRC check.. but lets keep this simple and assume that TCP ensures
|
||||
// data integrity.
|
||||
SpValidity::Skip
|
||||
@@ -101,50 +103,39 @@ impl PacketSource for SyncTcpTmSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub type TcpServer<ReceivesTc, SendError> = TcpSpacepacketsServer<
|
||||
pub type TcpServer<ReceivesTc> = TcpSpacepacketsServer<
|
||||
SyncTcpTmSource,
|
||||
ReceivesTc,
|
||||
SimplePacketValidator,
|
||||
ConnectionFinishedHandler,
|
||||
(),
|
||||
SendError,
|
||||
>;
|
||||
|
||||
pub struct TcpTask<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>(
|
||||
pub TcpServer<TcSender, SendError>,
|
||||
PhantomData<SendError>,
|
||||
);
|
||||
pub struct TcpTask(pub TcpServer<TmTcSender>);
|
||||
|
||||
impl<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>
|
||||
TcpTask<TcSender, SendError>
|
||||
{
|
||||
impl TcpTask {
|
||||
pub fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: SyncTcpTmSource,
|
||||
tc_sender: TcSender,
|
||||
tc_sender: TmTcSender,
|
||||
valid_ids: HashSet<PacketId>,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
Ok(Self(
|
||||
TcpSpacepacketsServer::new(
|
||||
cfg,
|
||||
tm_source,
|
||||
tc_sender,
|
||||
SimplePacketValidator { valid_ids },
|
||||
ConnectionFinishedHandler::default(),
|
||||
None,
|
||||
)?,
|
||||
PhantomData,
|
||||
))
|
||||
Ok(Self(TcpSpacepacketsServer::new(
|
||||
cfg,
|
||||
tm_source,
|
||||
CcsdsPacketParser::new(cfg.id, 2048, tc_sender, SimplePacketValidator { valid_ids }),
|
||||
ConnectionFinishedHandler::default(),
|
||||
None,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
loop {
|
||||
let result = self.0.handle_all_connections(None);
|
||||
match result {
|
||||
Ok(_conn_result) => (),
|
||||
Err(e) => {
|
||||
warn!("TCP server error: {e:?}");
|
||||
}
|
||||
let result = self
|
||||
.0
|
||||
.handle_all_connections(Some(Duration::from_millis(400)));
|
||||
match result {
|
||||
Ok(_conn_result) => (),
|
||||
Err(e) => {
|
||||
warn!("TCP server error: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
#![allow(dead_code)]
|
||||
use std::collections::VecDeque;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use log::warn;
|
||||
use satrs::HandlingStatus;
|
||||
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
||||
use satrs::queue::GenericSendError;
|
||||
use types::ccsds::CcsdsTmPacketOwned;
|
||||
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
pub trait UdpTmHandlerProvider {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
|
||||
}
|
||||
|
||||
pub struct UdpTmHandlerWithChannel {
|
||||
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
}
|
||||
|
||||
impl UdpTmHandlerProvider for UdpTmHandlerWithChannel {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
while let Ok(tm) = self.tm_rx.try_recv() {
|
||||
log::debug!("sending TM from sender {:?}", tm.tm_header.sender_id);
|
||||
let result = socket.send_to(&tm.to_vec(), recv_addr);
|
||||
if let Err(e) = result {
|
||||
warn!("Sending TM with UDP socket failed: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TestTmHandler {
|
||||
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
|
||||
}
|
||||
|
||||
impl UdpTmHandlerProvider for TestTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UdpTmHandler {
|
||||
Normal(UdpTmHandlerWithChannel),
|
||||
Test(TestTmHandler),
|
||||
}
|
||||
|
||||
impl From<UdpTmHandlerWithChannel> for UdpTmHandler {
|
||||
fn from(handler: UdpTmHandlerWithChannel) -> Self {
|
||||
UdpTmHandler::Normal(handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TestTmHandler> for UdpTmHandler {
|
||||
fn from(handler: TestTmHandler) -> Self {
|
||||
UdpTmHandler::Test(handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpTmHandlerProvider for UdpTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
match self {
|
||||
UdpTmHandler::Normal(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
|
||||
UdpTmHandler::Test(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpTmtcServer {
|
||||
pub udp_tc_server: UdpTcServer<TmTcSender, GenericSendError>,
|
||||
pub tm_handler: UdpTmHandler,
|
||||
}
|
||||
|
||||
impl UdpTmtcServer {
|
||||
pub fn periodic_operation(&mut self) {
|
||||
loop {
|
||||
if self.poll_tc_server() == HandlingStatus::Empty {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
|
||||
self.tm_handler
|
||||
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_tc_server(&mut self) -> HandlingStatus {
|
||||
match self.udp_tc_server.try_recv_tc() {
|
||||
Ok(_) => HandlingStatus::HandledOne,
|
||||
Err(e) => {
|
||||
match e {
|
||||
ReceiveResult::NothingReceived => (),
|
||||
ReceiveResult::Io(e) => {
|
||||
warn!("IO error {e}");
|
||||
}
|
||||
ReceiveResult::Send(send_error) => {
|
||||
warn!("send error {send_error:?}");
|
||||
}
|
||||
}
|
||||
HandlingStatus::Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u14;
|
||||
use example_std::config::OBSW_SERVER_ADDR;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
ComponentId,
|
||||
spacepackets::{
|
||||
SpHeader,
|
||||
ecss::{WritablePusPacket, tc::PusTcCreator},
|
||||
},
|
||||
};
|
||||
use types::Apid;
|
||||
|
||||
use crate::tmtc::sender::MockSender;
|
||||
|
||||
use super::*;
|
||||
|
||||
const UDP_SERVER_ID: ComponentId = 0x05;
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
||||
let test_receiver = TmTcSender::Mock(MockSender::default());
|
||||
let udp_tc_server =
|
||||
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: tm_handler.into(),
|
||||
};
|
||||
udp_dyn_server.periodic_operation();
|
||||
let queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow();
|
||||
assert!(queue.is_empty());
|
||||
assert!(tm_handler_calls.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transactions() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
||||
let test_receiver = TmTcSender::Mock(MockSender::default());
|
||||
let udp_tc_server =
|
||||
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
|
||||
let server_addr = udp_tc_server.socket.local_addr().unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: tm_handler.into(),
|
||||
};
|
||||
let sph = SpHeader::new_for_unseg_tc(Apid::Tmtc.raw_value(), u14::ZERO, 0);
|
||||
let ping_tc = PusTcCreator::new_simple(
|
||||
sph,
|
||||
MessageTypeId::new(17, 1),
|
||||
&[],
|
||||
CreatorConfig::default(),
|
||||
)
|
||||
.to_vec()
|
||||
.unwrap();
|
||||
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
|
||||
let client_addr = client.local_addr().unwrap();
|
||||
println!("{}", server_addr);
|
||||
client.send_to(&ping_tc, server_addr).unwrap();
|
||||
udp_dyn_server.periodic_operation();
|
||||
{
|
||||
let mut queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow_mut();
|
||||
assert!(!queue.is_empty());
|
||||
let packet_with_sender = queue.pop_front().unwrap();
|
||||
assert_eq!(packet_with_sender.packet, ping_tc);
|
||||
assert_eq!(packet_with_sender.sender_id, UDP_SERVER_ID);
|
||||
}
|
||||
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
udp_dyn_server.periodic_operation();
|
||||
let queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow();
|
||||
assert!(queue.is_empty());
|
||||
drop(queue);
|
||||
// Still tries to send to the same client.
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u11;
|
||||
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
|
||||
use satrs::pus::event::EventTmHook;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::EcssTmSender;
|
||||
use satrs::request::UniqueApidTargetId;
|
||||
use satrs::{
|
||||
event_man_legacy::{EventManagerWithBoundedMpsc, EventSendProvider, EventU32SenderMpscBounded},
|
||||
pus::{
|
||||
event_man::{
|
||||
DefaultPusEventU32TmCreator, EventReporter, EventRequest, EventRequestWithToken,
|
||||
},
|
||||
verification::{TcStateStarted, VerificationReportingProvider, VerificationToken},
|
||||
},
|
||||
spacepackets::time::cds::CdsTime,
|
||||
};
|
||||
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
|
||||
|
||||
use crate::update_time;
|
||||
|
||||
// This helper sets the APID of the event sender for the PUS telemetry.
|
||||
#[derive(Default)]
|
||||
pub struct EventApidSetter {
|
||||
pub next_apid: u11,
|
||||
}
|
||||
|
||||
impl EventTmHook for EventApidSetter {
|
||||
fn modify_tm(&self, tm: &mut satrs::spacepackets::ecss::tm::PusTmCreator) {
|
||||
tm.set_apid(self.next_apid);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// The PUS event handler subscribes for all events and converts them into ECSS PUS 5 event
|
||||
/// packets. It also handles the verification completion of PUS event service requests.
|
||||
pub struct PusEventHandler<TmSender: EcssTmSender> {
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
pus_event_tm_creator: DefaultPusEventU32TmCreator<EventApidSetter>,
|
||||
pus_event_man_rx: mpsc::Receiver<EventMessageU32>,
|
||||
tm_sender: TmSender,
|
||||
time_provider: CdsTime,
|
||||
timestamp: [u8; 7],
|
||||
small_data_buf: [u8; 64],
|
||||
verif_handler: VerificationReporter,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender> PusEventHandler<TmSender> {
|
||||
pub fn new(
|
||||
tm_sender: TmSender,
|
||||
verif_handler: VerificationReporter,
|
||||
event_manager: &mut EventManagerWithBoundedMpsc,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
let event_queue_cap = 30;
|
||||
let (pus_event_man_tx, pus_event_man_rx) = mpsc::sync_channel(event_queue_cap);
|
||||
|
||||
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
|
||||
// telemetry for each event.
|
||||
let event_reporter = EventReporter::new_with_hook(
|
||||
PUS_EVENT_MANAGEMENT.raw(),
|
||||
u11::ZERO,
|
||||
0,
|
||||
128,
|
||||
EventApidSetter::default(),
|
||||
);
|
||||
let pus_event_dispatcher =
|
||||
DefaultPusEventU32TmCreator::new_with_default_backend(event_reporter);
|
||||
let pus_event_man_send_provider = EventU32SenderMpscBounded::new(
|
||||
PUS_EVENT_MANAGEMENT.raw(),
|
||||
pus_event_man_tx,
|
||||
event_queue_cap,
|
||||
);
|
||||
|
||||
event_manager.subscribe_all(pus_event_man_send_provider.target_id());
|
||||
event_manager.add_sender(pus_event_man_send_provider);
|
||||
|
||||
Self {
|
||||
event_request_rx,
|
||||
pus_event_tm_creator: pus_event_dispatcher,
|
||||
pus_event_man_rx,
|
||||
time_provider: CdsTime::new_with_u16_days(0, 0),
|
||||
timestamp: [0; 7],
|
||||
small_data_buf: [0; 64],
|
||||
verif_handler,
|
||||
tm_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event_requests(&mut self) {
|
||||
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
|
||||
let started_token: VerificationToken<TcStateStarted> = event_req
|
||||
.token
|
||||
.try_into()
|
||||
.expect("expected start verification token");
|
||||
self.verif_handler
|
||||
.completion_success(&self.tm_sender, started_token, timestamp)
|
||||
.expect("Sending completion success failed");
|
||||
};
|
||||
loop {
|
||||
// handle event requests
|
||||
match self.event_request_rx.try_recv() {
|
||||
Ok(event_req) => match event_req.request {
|
||||
EventRequest::Enable(event) => {
|
||||
self.pus_event_tm_creator
|
||||
.enable_tm_for_event(&event)
|
||||
.expect("Enabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
EventRequest::Disable(event) => {
|
||||
self.pus_event_tm_creator
|
||||
.disable_tm_for_event(&event)
|
||||
.expect("Disabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
},
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("all event request senders have disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_pus_event_tm(&mut self) {
|
||||
loop {
|
||||
// Perform the generation of PUS event packets
|
||||
match self.pus_event_man_rx.try_recv() {
|
||||
Ok(event_msg) => {
|
||||
// We use the TM modification hook to set the sender APID for each event.
|
||||
self.pus_event_tm_creator.reporter.tm_hook.next_apid =
|
||||
UniqueApidTargetId::from(event_msg.sender_id()).apid;
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
let generation_result = self
|
||||
.pus_event_tm_creator
|
||||
.generate_pus_event_tm_generic_with_generic_params(
|
||||
&self.tm_sender,
|
||||
&self.timestamp,
|
||||
event_msg.event(),
|
||||
&mut self.small_data_buf,
|
||||
event_msg.params(),
|
||||
)
|
||||
.expect("Sending TM as event failed");
|
||||
if !generation_result.params_were_propagated {
|
||||
log::warn!(
|
||||
"Event TM parameters were not propagated: {:?}",
|
||||
event_msg.params()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("All event senders have disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventHandler<TmSender: EcssTmSender> {
|
||||
pub pus_event_handler: PusEventHandler<TmSender>,
|
||||
event_manager: EventManagerWithBoundedMpsc,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender> EventHandler<TmSender> {
|
||||
pub fn new(
|
||||
tm_sender: TmSender,
|
||||
event_rx: mpsc::Receiver<EventMessageU32>,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
|
||||
let pus_event_handler = PusEventHandler::new(
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
|
||||
&mut event_manager,
|
||||
event_request_rx,
|
||||
);
|
||||
|
||||
Self {
|
||||
pus_event_handler,
|
||||
event_manager,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn event_manager(&mut self) -> &mut EventManagerWithBoundedMpsc {
|
||||
&mut self.event_manager
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.pus_event_handler.handle_event_requests();
|
||||
self.try_event_routing();
|
||||
self.pus_event_handler.generate_pus_event_tm();
|
||||
}
|
||||
|
||||
pub fn try_event_routing(&mut self) {
|
||||
let error_handler = |event_msg: &EventMessageU32, error: EventRoutingError| {
|
||||
self.routing_error_handler(event_msg, error)
|
||||
};
|
||||
// Perform the event routing.
|
||||
self.event_manager.try_event_handling(error_handler);
|
||||
}
|
||||
|
||||
pub fn routing_error_handler(&self, event_msg: &EventMessageU32, error: EventRoutingError) {
|
||||
log::warn!("event routing error for event {event_msg:?}: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::u21;
|
||||
use satrs::{
|
||||
events_legacy::EventU32,
|
||||
pus::verification::VerificationReporterConfig,
|
||||
spacepackets::ecss::{tm::PusTmReader, PusPacket},
|
||||
tmtc::PacketAsVec,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_CREATOR_ID: UniqueApidTargetId = UniqueApidTargetId::new(u11::new(1), u21::new(2));
|
||||
const TEST_EVENT: EventU32 = EventU32::new(satrs::events_legacy::Severity::Info, 1, 1);
|
||||
|
||||
pub struct EventManagementTestbench {
|
||||
pub event_tx: mpsc::SyncSender<EventMessageU32>,
|
||||
pub event_manager: EventManagerWithBoundedMpsc,
|
||||
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
pub pus_event_handler: PusEventHandler<mpsc::Sender<PacketAsVec>>,
|
||||
}
|
||||
|
||||
impl EventManagementTestbench {
|
||||
pub fn new() -> Self {
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(10);
|
||||
let (_event_req_tx, event_req_rx) = mpsc::sync_channel(10);
|
||||
let (tm_sender, tm_receiver) = mpsc::channel();
|
||||
let verif_reporter_cfg = VerificationReporterConfig::new(u11::new(0x05), 2, 2, 128);
|
||||
let verif_reporter =
|
||||
VerificationReporter::new(PUS_EVENT_MANAGEMENT.id(), &verif_reporter_cfg);
|
||||
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
|
||||
let pus_event_handler = PusEventHandler::<mpsc::Sender<PacketAsVec>>::new(
|
||||
tm_sender,
|
||||
verif_reporter,
|
||||
&mut event_manager,
|
||||
event_req_rx,
|
||||
);
|
||||
Self {
|
||||
event_tx,
|
||||
tm_receiver,
|
||||
event_manager,
|
||||
pus_event_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_event_generation() {
|
||||
let mut testbench = EventManagementTestbench::new();
|
||||
testbench
|
||||
.event_tx
|
||||
.send(EventMessageU32::new(
|
||||
TEST_CREATOR_ID.id(),
|
||||
EventU32::new(satrs::events_legacy::Severity::Info, 1, 1),
|
||||
))
|
||||
.expect("failed to send event");
|
||||
testbench.pus_event_handler.handle_event_requests();
|
||||
testbench.event_manager.try_event_handling(|_, _| {});
|
||||
testbench.pus_event_handler.generate_pus_event_tm();
|
||||
let tm_packet = testbench
|
||||
.tm_receiver
|
||||
.try_recv()
|
||||
.expect("failed to receive TM packet");
|
||||
assert_eq!(tm_packet.sender_id, PUS_EVENT_MANAGEMENT.id());
|
||||
let tm_reader = PusTmReader::new(&tm_packet.packet, 7).expect("failed to create TM reader");
|
||||
assert_eq!(tm_reader.apid(), TEST_CREATOR_ID.apid);
|
||||
assert_eq!(tm_reader.user_data().len(), 4);
|
||||
let event_read_back = EventU32::from_be_bytes(tm_reader.user_data().try_into().unwrap());
|
||||
assert_eq!(event_read_back, TEST_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_event_disabled() {
|
||||
// TODO: Add test.
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,74 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use derive_new::new;
|
||||
use satrs::hk::UniqueId;
|
||||
use satrs::request::UniqueApidTargetId;
|
||||
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
|
||||
use satrs::spacepackets::ecss::{hk, CreatorConfig, MessageTypeId};
|
||||
use satrs::spacepackets::{ByteConversionError, SpHeader};
|
||||
|
||||
#[derive(Debug, new, Copy, Clone)]
|
||||
pub struct HkUniqueId {
|
||||
target_id: UniqueApidTargetId,
|
||||
set_id: UniqueId,
|
||||
}
|
||||
|
||||
impl HkUniqueId {
|
||||
#[allow(dead_code)]
|
||||
pub fn target_id(&self) -> UniqueApidTargetId {
|
||||
self.target_id
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn set_id(&self) -> UniqueId {
|
||||
self.set_id
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
|
||||
if buf.len() < 8 {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: 8,
|
||||
});
|
||||
}
|
||||
buf[0..4].copy_from_slice(&self.target_id.unique_id.as_u32().to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
|
||||
|
||||
Ok(8)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(new)]
|
||||
pub struct PusHkHelper {
|
||||
component_id: UniqueApidTargetId,
|
||||
}
|
||||
|
||||
impl PusHkHelper {
|
||||
pub fn generate_hk_report_packet<
|
||||
'a,
|
||||
'b,
|
||||
HkWriter: FnMut(&mut [u8]) -> Result<usize, ByteConversionError>,
|
||||
>(
|
||||
&self,
|
||||
timestamp: &'a [u8],
|
||||
set_id: u32,
|
||||
hk_data_writer: &mut HkWriter,
|
||||
buf: &'b mut [u8],
|
||||
) -> Result<PusTmCreator<'a, 'b>, ByteConversionError> {
|
||||
let sec_header = PusTmSecondaryHeader::new(
|
||||
MessageTypeId::new(3, hk::MessageSubtypeId::TmHkPacket as u8),
|
||||
0,
|
||||
0,
|
||||
timestamp,
|
||||
);
|
||||
buf[0..4].copy_from_slice(&self.component_id.unique_id.as_u32().to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&set_id.to_be_bytes());
|
||||
let (_, second_half) = buf.split_at_mut(8);
|
||||
let hk_data_len = hk_data_writer(second_half)?;
|
||||
Ok(PusTmCreator::new(
|
||||
SpHeader::new_from_apid(self.component_id.apid),
|
||||
sec_header,
|
||||
&buf[0..8 + hk_data_len],
|
||||
CreatorConfig::default(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
use log::{error, warn};
|
||||
use log::warn;
|
||||
use satrs::action::{ActionRequest, ActionRequestVariant};
|
||||
use satrs::params::WritableToBeBytes;
|
||||
use satrs::pool::SharedStaticMemoryPool;
|
||||
use satrs::pus::action::{
|
||||
ActionReplyPus, ActionReplyVariant, ActivePusActionRequestStd, DefaultActiveActionRequestMap,
|
||||
};
|
||||
use satrs::pus::verification::{
|
||||
FailParams, FailParamsWithStep, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
handle_completion_failure_with_generic_params, handle_step_failure_with_generic_params,
|
||||
FailParamHelper, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActiveRequestProvider, EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter,
|
||||
EcssTcInVecConverter, EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver,
|
||||
MpscTmAsVecSender, PusPacketHandlerResult, PusReplyHandler, PusServiceHelper,
|
||||
PusTcToRequestConverter,
|
||||
ActiveRequest, EcssTcAndToken, EcssTcCacher, EcssTmSender, EcssTmtcError,
|
||||
GenericConversionError, MpscTcReceiver, PusPacketHandlingError, PusReplyHandler,
|
||||
PusServiceHelper, PusTcToRequestConverter,
|
||||
};
|
||||
use satrs::request::{GenericMessage, UniqueApidTargetId};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket};
|
||||
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
|
||||
use satrs_example::config::components::PUS_ACTION_SERVICE;
|
||||
use satrs_example::config::tmtc_err;
|
||||
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket, PusServiceId};
|
||||
use example_std::config::tmtc_err;
|
||||
use example_std::ids;
|
||||
use example_std::ids::generic_pus::PUS_ACTION;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
use super::{
|
||||
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
|
||||
@@ -61,7 +60,7 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
|
||||
active_request: &ActivePusActionRequestStd,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
timestamp: &[u8],
|
||||
) -> Result<bool, Self::Error> {
|
||||
let verif_token: VerificationToken<TcStateStarted> = active_request
|
||||
.token()
|
||||
@@ -69,15 +68,23 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
|
||||
.expect("invalid token state");
|
||||
let remove_entry = match &reply.message.variant {
|
||||
ActionReplyVariant::CompletionFailed { error_code, params } => {
|
||||
let mut fail_data_len = 0;
|
||||
if let Some(params) = params {
|
||||
fail_data_len = params.write_to_be_bytes(&mut self.fail_data_buf)?;
|
||||
}
|
||||
verification_handler.completion_failure(
|
||||
let error_propagated = handle_completion_failure_with_generic_params(
|
||||
tm_sender,
|
||||
verif_token,
|
||||
FailParams::new(time_stamp, error_code, &self.fail_data_buf[..fail_data_len]),
|
||||
verification_handler,
|
||||
FailParamHelper {
|
||||
error_code,
|
||||
params: params.as_ref(),
|
||||
timestamp,
|
||||
small_data_buf: &mut self.fail_data_buf,
|
||||
},
|
||||
)?;
|
||||
if !error_propagated {
|
||||
log::warn!(
|
||||
"error params for completion failure were not propated: {:?}",
|
||||
params.as_ref()
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::StepFailed {
|
||||
@@ -85,31 +92,35 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
|
||||
step,
|
||||
params,
|
||||
} => {
|
||||
let mut fail_data_len = 0;
|
||||
if let Some(params) = params {
|
||||
fail_data_len = params.write_to_be_bytes(&mut self.fail_data_buf)?;
|
||||
}
|
||||
verification_handler.step_failure(
|
||||
let error_propagated = handle_step_failure_with_generic_params(
|
||||
tm_sender,
|
||||
verif_token,
|
||||
FailParamsWithStep::new(
|
||||
time_stamp,
|
||||
&EcssEnumU16::new(*step),
|
||||
verification_handler,
|
||||
FailParamHelper {
|
||||
error_code,
|
||||
&self.fail_data_buf[..fail_data_len],
|
||||
),
|
||||
params: params.as_ref(),
|
||||
timestamp,
|
||||
small_data_buf: &mut self.fail_data_buf,
|
||||
},
|
||||
&EcssEnumU16::new(*step),
|
||||
)?;
|
||||
if !error_propagated {
|
||||
log::warn!(
|
||||
"error params for completion failure were not propated: {:?}",
|
||||
params.as_ref()
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::Completed => {
|
||||
verification_handler.completion_success(tm_sender, verif_token, time_stamp)?;
|
||||
verification_handler.completion_success(tm_sender, verif_token, timestamp)?;
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::StepSuccess { step } => {
|
||||
verification_handler.step_success(
|
||||
tm_sender,
|
||||
&verif_token,
|
||||
time_stamp,
|
||||
timestamp,
|
||||
EcssEnumU16::new(*step),
|
||||
)?;
|
||||
false
|
||||
@@ -150,7 +161,7 @@ impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for Actio
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(ActivePusActionRequestStd, ActionRequest), Self::Error> {
|
||||
let subservice = tc.subservice();
|
||||
let subservice = tc.message_subtype_id();
|
||||
let user_data = tc.user_data();
|
||||
if user_data.len() < 8 {
|
||||
verif_reporter
|
||||
@@ -195,20 +206,20 @@ impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for Actio
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_action_service_static(
|
||||
tm_sender: PacketSenderWithSharedPool,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pub fn create_action_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
action_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
|
||||
) -> ActionServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
|
||||
) -> ActionServiceWrapper {
|
||||
let action_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_ACTION_SERVICE.id(),
|
||||
ids::generic_pus::PUS_ACTION.id(),
|
||||
pus_action_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_ACTION_SERVICE.id(), PUS_ACTION_SERVICE.apid),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
|
||||
create_verification_reporter(PUS_ACTION.id(), PUS_ACTION.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
ActionRequestConverter::default(),
|
||||
// TODO: Implementation which does not use run-time allocation? Maybe something like
|
||||
@@ -223,36 +234,9 @@ pub fn create_action_service_static(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_action_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
action_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
|
||||
) -> ActionServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
|
||||
let action_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_ACTION_SERVICE.id(),
|
||||
pus_action_rx,
|
||||
tm_funnel_tx,
|
||||
create_verification_reporter(PUS_ACTION_SERVICE.id(), PUS_ACTION_SERVICE.apid),
|
||||
EcssTcInVecConverter::default(),
|
||||
),
|
||||
ActionRequestConverter::default(),
|
||||
DefaultActiveActionRequestMap::default(),
|
||||
ActionReplyHandler::default(),
|
||||
action_router,
|
||||
reply_receiver,
|
||||
);
|
||||
ActionServiceWrapper {
|
||||
service: action_request_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActionServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub struct ActionServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
TmSender,
|
||||
TcInMemConverter,
|
||||
VerificationReporter,
|
||||
ActionRequestConverter,
|
||||
ActionReplyHandler,
|
||||
@@ -263,57 +247,38 @@ pub struct ActionServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTc
|
||||
>,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> TargetedPusService
|
||||
for ActionServiceWrapper<TmSender, TcInMemConverter>
|
||||
{
|
||||
/// Returns [true] if the packet handling is finished.
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
match self.service.poll_and_handle_next_tc(time_stamp) {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
warn!("PUS 8 partial packet handling success: {e:?}")
|
||||
}
|
||||
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
|
||||
warn!("PUS 8 invalid subservice {invalid}");
|
||||
}
|
||||
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
warn!("PUS 8 subservice {subservice} not implemented");
|
||||
}
|
||||
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
|
||||
},
|
||||
Err(error) => {
|
||||
error!("PUS packet handling error: {error:?}");
|
||||
// To avoid permanent loops on error cases.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
impl TargetedPusService for ActionServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Action as u8;
|
||||
const SERVICE_STR: &'static str = "action";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
// This only fails if all senders disconnected. Treat it like an empty queue.
|
||||
self.service
|
||||
.poll_and_check_next_reply(time_stamp)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("PUS 8: Handling reply failed with error {e:?}");
|
||||
HandlingStatus::Empty
|
||||
})
|
||||
}
|
||||
|
||||
fn check_for_request_timeouts(&mut self) {
|
||||
self.service.check_for_request_timeouts();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use satrs::pus::test_util::{
|
||||
TEST_APID, TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
|
||||
};
|
||||
use satrs::pus::verification;
|
||||
use satrs::pus::verification::test_util::TestVerificationReporter;
|
||||
use satrs::pus::{verification, EcssTcVecCacher};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::tmtc::PacketAsVec;
|
||||
use satrs::ComponentId;
|
||||
use satrs::{
|
||||
res_code::ResultU16,
|
||||
@@ -346,10 +311,10 @@ mod tests {
|
||||
{
|
||||
pub fn new_for_action(owner_id: ComponentId, target_id: ComponentId) -> Self {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = mpsc::channel();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = mpsc::sync_channel(5);
|
||||
let (pus_action_tx, pus_action_rx) = mpsc::channel();
|
||||
let (action_reply_tx, action_reply_rx) = mpsc::channel();
|
||||
let (action_req_tx, action_req_rx) = mpsc::channel();
|
||||
let (action_req_tx, action_req_rx) = mpsc::sync_channel(10);
|
||||
let verif_reporter = TestVerificationReporter::new(owner_id);
|
||||
let mut generic_req_router = GenericRequestRouter::default();
|
||||
generic_req_router
|
||||
@@ -360,9 +325,9 @@ mod tests {
|
||||
PusServiceHelper::new(
|
||||
owner_id,
|
||||
pus_action_rx,
|
||||
tm_funnel_tx.clone(),
|
||||
TmTcSender::Heap(tm_funnel_tx.clone()),
|
||||
verif_reporter,
|
||||
EcssTcInVecConverter::default(),
|
||||
EcssTcCacher::Heap(EcssTcVecCacher::default()),
|
||||
),
|
||||
ActionRequestConverter::default(),
|
||||
DefaultActiveActionRequestMap::default(),
|
||||
@@ -405,7 +370,7 @@ mod tests {
|
||||
if let Err(mpsc::TryRecvError::Empty) = packet {
|
||||
} else {
|
||||
let tm = packet.unwrap();
|
||||
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap().0;
|
||||
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap();
|
||||
panic!("unexpected TM packet {unexpected_tm:?}");
|
||||
}
|
||||
}
|
||||
@@ -417,7 +382,7 @@ mod tests {
|
||||
}
|
||||
let result = result.unwrap();
|
||||
match result {
|
||||
PusPacketHandlerResult::RequestHandled => (),
|
||||
HandlingStatus::HandledOne => (),
|
||||
_ => panic!("unexpected result {result:?}"),
|
||||
}
|
||||
}
|
||||
@@ -429,26 +394,30 @@ mod tests {
|
||||
}
|
||||
let result = result.unwrap();
|
||||
match result {
|
||||
PusPacketHandlerResult::Empty => (),
|
||||
HandlingStatus::Empty => (),
|
||||
_ => panic!("unexpected result {result:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_next_reply_is_handled_properly(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_check_next_reply(time_stamp);
|
||||
let result = self.service.poll_and_handle_next_reply(time_stamp);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), HandlingStatus::HandledOne);
|
||||
}
|
||||
|
||||
pub fn verify_all_replies_handled(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_check_next_reply(time_stamp);
|
||||
let result = self.service.poll_and_handle_next_reply(time_stamp);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), HandlingStatus::Empty);
|
||||
}
|
||||
|
||||
pub fn add_tc(&mut self, tc: &PusTcCreator) {
|
||||
self.request_id = Some(verification::RequestId::new(tc).into());
|
||||
let token = self.service.service_helper.verif_reporter_mut().add_tc(tc);
|
||||
let token = self
|
||||
.service
|
||||
.service_helper
|
||||
.verif_reporter_mut()
|
||||
.start_verification(tc);
|
||||
let accepted_token = self
|
||||
.service
|
||||
.service_helper
|
||||
@@ -481,12 +450,13 @@ mod tests {
|
||||
);
|
||||
// Create a basic action request and verify forwarding.
|
||||
let sp_header = SpHeader::new_from_apid(TEST_APID);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
let pus8_packet = PusTcCreator::new(sp_header, sec_header, &app_data, true);
|
||||
let pus8_packet =
|
||||
PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
testbench.add_tc(&pus8_packet);
|
||||
let time_stamp: [u8; 7] = [0; 7];
|
||||
testbench.verify_next_tc_is_handled_properly(&time_stamp);
|
||||
@@ -522,7 +492,7 @@ mod tests {
|
||||
TEST_COMPONENT_ID_1.id(),
|
||||
);
|
||||
// Create a basic action request and verify forwarding.
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
// Invalid ID, routing should fail.
|
||||
@@ -532,7 +502,7 @@ mod tests {
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
testbench.add_tc(&pus8_packet);
|
||||
let time_stamp: [u8; 7] = [0; 7];
|
||||
@@ -548,17 +518,17 @@ mod tests {
|
||||
TEST_COMPONENT_ID_0.raw(),
|
||||
ActionRequestConverter::default(),
|
||||
);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
// Invalid ID, routing should fail.
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
let pus8_packet = PusTcCreator::new(
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let token = testbench.add_tc(&pus8_packet);
|
||||
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||
@@ -584,11 +554,11 @@ mod tests {
|
||||
fn converter_action_req_with_data() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ActionRequestConverter::default());
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 16] = [0; 16];
|
||||
// Invalid ID, routing should fail.
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
for i in 0..8 {
|
||||
app_data[i + 8] = i as u8;
|
||||
@@ -597,7 +567,7 @@ mod tests {
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let token = testbench.add_tc(&pus8_packet);
|
||||
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||
@@ -727,7 +697,7 @@ mod tests {
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_reply = ActionReplyPus::new(5_u32, ActionReplyVariant::Completed);
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), action_reply);
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
@@ -0,0 +1,91 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use satrs::pus::event_man::EventRequestWithToken;
|
||||
use satrs::pus::event_srv::PusEventServiceHandler;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
|
||||
PartialPusHandlingError, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::PusServiceId;
|
||||
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub fn create_event_service(
|
||||
tm_sender: TmTcSender,
|
||||
tm_in_pool_converter: EcssTcCacher,
|
||||
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
event_request_tx: mpsc::Sender<EventRequestWithToken>,
|
||||
) -> EventServiceWrapper {
|
||||
let pus_5_handler = PusEventServiceHandler::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_EVENT_MANAGEMENT.id(),
|
||||
pus_event_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
|
||||
tm_in_pool_converter,
|
||||
),
|
||||
event_request_tx,
|
||||
);
|
||||
EventServiceWrapper {
|
||||
handler: pus_5_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventServiceWrapper {
|
||||
pub handler:
|
||||
PusEventServiceHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
}
|
||||
|
||||
impl DirectPusService for EventServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Event as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "events";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
let result = self
|
||||
.handler
|
||||
.poll_and_handle_next_tc(error_handler, time_stamp);
|
||||
if let Err(e) = result {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match result.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,28 @@
|
||||
use derive_new::new;
|
||||
use log::{error, warn};
|
||||
use satrs::hk::{CollectionIntervalFactor, HkRequest, HkRequestVariant, UniqueId};
|
||||
use satrs::pool::SharedStaticMemoryPool;
|
||||
use satrs::pus::verification::{
|
||||
FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActivePusRequestStd, ActiveRequestProvider, DefaultActiveRequestMap, EcssTcAndToken,
|
||||
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter, EcssTmSender,
|
||||
EcssTmtcError, GenericConversionError, MpscTcReceiver, MpscTmAsVecSender,
|
||||
PusPacketHandlerResult, PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
|
||||
ActivePusRequestStd, ActiveRequest, DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher,
|
||||
EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver, PusPacketHandlingError,
|
||||
PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
|
||||
};
|
||||
use satrs::request::{GenericMessage, UniqueApidTargetId};
|
||||
use satrs::res_code::ResultU16;
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{hk, PusPacket};
|
||||
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
|
||||
use satrs_example::config::components::PUS_HK_SERVICE;
|
||||
use satrs_example::config::{hk_err, tmtc_err};
|
||||
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
|
||||
use example_std::config::{hk_err, tmtc_err};
|
||||
use example_std::ids::generic_pus::PUS_HK;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::pus::{create_verification_reporter, generic_pus_request_timeout_handler};
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
use super::{HandlingStatus, PusTargetedRequestService};
|
||||
use super::{HandlingStatus, PusTargetedRequestService, TargetedPusService};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, new)]
|
||||
pub struct HkReply {
|
||||
@@ -33,8 +31,10 @@ pub struct HkReply {
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum HkReplyVariant {
|
||||
Ack,
|
||||
Failed(ResultU16),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -70,6 +70,15 @@ impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
|
||||
.completion_success(tm_sender, started_token, time_stamp)
|
||||
.expect("sending completion success verification failed");
|
||||
}
|
||||
HkReplyVariant::Failed(failure_code) => {
|
||||
verification_handler
|
||||
.completion_failure(
|
||||
tm_sender,
|
||||
started_token,
|
||||
FailParams::new(time_stamp, &failure_code, &[]),
|
||||
)
|
||||
.expect("sending completion success verification failed");
|
||||
}
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
@@ -155,11 +164,11 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
|
||||
found: 4,
|
||||
});
|
||||
}
|
||||
let subservice = tc.subservice();
|
||||
let subservice = tc.message_subtype_id();
|
||||
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).expect("invalid tc format");
|
||||
let unique_id = u32::from_be_bytes(tc.user_data()[4..8].try_into().unwrap());
|
||||
|
||||
let standard_subservice = hk::Subservice::try_from(subservice);
|
||||
let standard_subservice = hk::MessageSubtypeId::try_from(subservice);
|
||||
if standard_subservice.is_err() {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
@@ -171,19 +180,22 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
|
||||
return Err(GenericConversionError::InvalidSubservice(subservice));
|
||||
}
|
||||
let request = match standard_subservice.unwrap() {
|
||||
hk::Subservice::TcEnableHkGeneration | hk::Subservice::TcEnableDiagGeneration => {
|
||||
hk::MessageSubtypeId::TcEnableHkGeneration
|
||||
| hk::MessageSubtypeId::TcEnableDiagGeneration => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::EnablePeriodic)
|
||||
}
|
||||
hk::Subservice::TcDisableHkGeneration | hk::Subservice::TcDisableDiagGeneration => {
|
||||
hk::MessageSubtypeId::TcDisableHkGeneration
|
||||
| hk::MessageSubtypeId::TcDisableDiagGeneration => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::DisablePeriodic)
|
||||
}
|
||||
hk::Subservice::TcReportHkReportStructures => todo!(),
|
||||
hk::Subservice::TmHkPacket => todo!(),
|
||||
hk::Subservice::TcGenerateOneShotHk | hk::Subservice::TcGenerateOneShotDiag => {
|
||||
hk::MessageSubtypeId::TcReportHkReportStructures => todo!(),
|
||||
hk::MessageSubtypeId::TmHkPacket => todo!(),
|
||||
hk::MessageSubtypeId::TcGenerateOneShotHk
|
||||
| hk::MessageSubtypeId::TcGenerateOneShotDiag => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::OneShot)
|
||||
}
|
||||
hk::Subservice::TcModifyDiagCollectionInterval
|
||||
| hk::Subservice::TcModifyHkCollectionInterval => {
|
||||
hk::MessageSubtypeId::TcModifyDiagCollectionInterval
|
||||
| hk::MessageSubtypeId::TcModifyHkCollectionInterval => {
|
||||
if user_data.len() < 12 {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
@@ -231,20 +243,20 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_hk_service_static(
|
||||
tm_sender: PacketSenderWithSharedPool,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pub fn create_hk_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
|
||||
) -> HkServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
|
||||
) -> HkServiceWrapper {
|
||||
let pus_3_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_HK_SERVICE.id(),
|
||||
PUS_HK.id(),
|
||||
pus_hk_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_HK_SERVICE.id(), PUS_HK_SERVICE.apid),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
|
||||
create_verification_reporter(PUS_HK.id(), PUS_HK.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
HkRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
@@ -257,36 +269,9 @@ pub fn create_hk_service_static(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_hk_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
|
||||
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
|
||||
) -> HkServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
|
||||
let pus_3_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_HK_SERVICE.id(),
|
||||
pus_hk_rx,
|
||||
tm_funnel_tx,
|
||||
create_verification_reporter(PUS_HK_SERVICE.id(), PUS_HK_SERVICE.apid),
|
||||
EcssTcInVecConverter::default(),
|
||||
),
|
||||
HkRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
HkReplyHandler::default(),
|
||||
request_router,
|
||||
reply_receiver,
|
||||
);
|
||||
HkServiceWrapper {
|
||||
service: pus_3_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HkServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub struct HkServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
TmSender,
|
||||
TcInMemConverter,
|
||||
VerificationReporter,
|
||||
HkRequestConverter,
|
||||
HkReplyHandler,
|
||||
@@ -297,64 +282,46 @@ pub struct HkServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMe
|
||||
>,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter>
|
||||
HkServiceWrapper<TmSender, TcInMemConverter>
|
||||
{
|
||||
pub fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
match self.service.poll_and_handle_next_tc(time_stamp) {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
warn!("PUS 3 partial packet handling success: {e:?}")
|
||||
}
|
||||
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
|
||||
warn!("PUS 3 invalid subservice {invalid}");
|
||||
}
|
||||
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
warn!("PUS 3 subservice {subservice} not implemented");
|
||||
}
|
||||
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
|
||||
},
|
||||
Err(error) => {
|
||||
error!("PUS packet handling error: {error:?}");
|
||||
// To avoid permanent loops on error cases.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
impl TargetedPusService for HkServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Housekeeping as u8;
|
||||
const SERVICE_STR: &'static str = "housekeeping";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
|
||||
pub fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
// This only fails if all senders disconnected. Treat it like an empty queue.
|
||||
self.service
|
||||
.poll_and_check_next_reply(time_stamp)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("PUS 3: Handling reply failed with error {e:?}");
|
||||
HandlingStatus::Empty
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_for_request_timeouts(&mut self) {
|
||||
self.service.check_for_request_timeouts();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::{u14, u21};
|
||||
use satrs::pus::test_util::{
|
||||
TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
|
||||
};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
hk::HkRequestVariant,
|
||||
pus::test_util::TEST_APID,
|
||||
request::GenericMessage,
|
||||
spacepackets::{
|
||||
ecss::{hk::Subservice, tc::PusTcCreator},
|
||||
ecss::{hk::MessageSubtypeId, tc::PusTcCreator},
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
use satrs_example::config::tmtc_err;
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
use crate::pus::{
|
||||
hk::HkReplyVariant,
|
||||
@@ -367,19 +334,18 @@ mod tests {
|
||||
fn hk_converter_one_shot_req() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
|
||||
let hk_req = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcGenerateOneShotHk as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcGenerateOneShotHk as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let accepted_token = hk_bench.add_tc(&hk_req);
|
||||
let (_active_req, req) = hk_bench
|
||||
@@ -397,11 +363,11 @@ mod tests {
|
||||
fn hk_converter_enable_periodic_generation() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
let mut generic_check = |tc: &PusTcCreator| {
|
||||
let accepted_token = hk_bench.add_tc(tc);
|
||||
@@ -416,18 +382,16 @@ mod tests {
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcEnableHkGeneration as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcEnableHkGeneration as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcEnableDiagGeneration as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcEnableDiagGeneration as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
@@ -436,11 +400,11 @@ mod tests {
|
||||
fn hk_conversion_disable_periodic_generation() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
let mut generic_check = |tc: &PusTcCreator| {
|
||||
let accepted_token = hk_bench.add_tc(tc);
|
||||
@@ -455,18 +419,16 @@ mod tests {
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcDisableHkGeneration as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcDisableHkGeneration as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcDisableDiagGeneration as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcDisableDiagGeneration as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
@@ -475,12 +437,12 @@ mod tests {
|
||||
fn hk_conversion_modify_interval() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 12] = [0; 12];
|
||||
let collection_interval_factor = 5_u32;
|
||||
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
app_data[8..12].copy_from_slice(&collection_interval_factor.to_be_bytes());
|
||||
|
||||
@@ -498,18 +460,16 @@ mod tests {
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcModifyHkCollectionInterval as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcModifyHkCollectionInterval as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
3,
|
||||
Subservice::TcModifyDiagCollectionInterval as u8,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcModifyDiagCollectionInterval as u8),
|
||||
&app_data,
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
@@ -518,8 +478,8 @@ mod tests {
|
||||
fn hk_reply_handler() {
|
||||
let mut reply_testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), HkReplyHandler::default());
|
||||
let sender_id = 2_u64;
|
||||
let apid_target_id = 3_u32;
|
||||
let sender_id = 2_u32;
|
||||
let apid_target_id = u21::new(3);
|
||||
let unique_id = 5_u32;
|
||||
let (req_id, active_req) = reply_testbench.add_tc(TEST_APID, apid_target_id, &[]);
|
||||
let reply = GenericMessage::new(
|
||||
@@ -540,7 +500,7 @@ mod tests {
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
|
||||
let action_reply = HkReply::new(5_u32, HkReplyVariant::Ack);
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), action_reply);
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::warn;
|
||||
use satrs::pool::PoolAddr;
|
||||
use satrs::pus::verification::{
|
||||
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReporterCfg, VerificationReportingProvider, VerificationToken,
|
||||
VerificationReporterConfig, VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActiveRequestMapProvider, ActiveRequestProvider, EcssTcAndToken, EcssTcInMemConverter,
|
||||
ActiveRequest, ActiveRequestStore, CacheAndReadRawEcssTc, EcssTcAndToken, EcssTcCacher,
|
||||
EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericConversionError, GenericRoutingError,
|
||||
PusPacketHandlerResult, PusPacketHandlingError, PusReplyHandler, PusRequestRouter,
|
||||
PusServiceHelper, PusTcToRequestConverter, TcInMemory,
|
||||
HandlingStatus, PusPacketHandlingError, PusReplyHandler, PusRequestRouter, PusServiceHelper,
|
||||
PusTcToRequestConverter, TcInMemory,
|
||||
};
|
||||
use satrs::queue::{GenericReceiveError, GenericSendError};
|
||||
use satrs::request::{Apid, GenericMessage, MessageMetadata};
|
||||
@@ -17,11 +18,11 @@ use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
|
||||
use satrs::tmtc::{PacketAsVec, PacketInPool};
|
||||
use satrs::ComponentId;
|
||||
use satrs_example::config::components::PUS_ROUTING_SERVICE;
|
||||
use satrs_example::config::{tmtc_err, CustomPusServiceId};
|
||||
use satrs_example::TimeStampHelper;
|
||||
use example_std::config::{tmtc_err, CustomPusServiceId};
|
||||
use example_std::ids::generic_pus::PUS_ROUTING;
|
||||
use example_std::TimestampHelper;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub mod action;
|
||||
pub mod event;
|
||||
@@ -31,55 +32,49 @@ pub mod scheduler;
|
||||
pub mod stack;
|
||||
pub mod test;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum HandlingStatus {
|
||||
Empty,
|
||||
HandledOne,
|
||||
}
|
||||
|
||||
pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> VerificationReporter {
|
||||
let verif_cfg = VerificationReporterCfg::new(apid, 1, 2, 8).unwrap();
|
||||
let verif_cfg = VerificationReporterConfig::new(apid, 1, 2, 8);
|
||||
// Every software component which needs to generate verification telemetry, gets a cloned
|
||||
// verification reporter.
|
||||
VerificationReporter::new(owner_id, &verif_cfg)
|
||||
}
|
||||
|
||||
/*
|
||||
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
|
||||
pub struct PusTcMpscRouter {
|
||||
pub test_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub event_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub sched_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub hk_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub action_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub mode_tc_sender: Sender<EcssTcAndToken>,
|
||||
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub event_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub sched_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub hk_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
#[allow(dead_code)]
|
||||
pub action_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub mode_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
}
|
||||
|
||||
pub struct PusTcDistributor<TmSender: EcssTmSender> {
|
||||
pub struct PusTcDistributor {
|
||||
#[allow(dead_code)]
|
||||
pub id: ComponentId,
|
||||
pub tm_sender: TmSender,
|
||||
pub tm_sender: TmTcSender,
|
||||
pub verif_reporter: VerificationReporter,
|
||||
pub pus_router: PusTcMpscRouter,
|
||||
stamp_helper: TimeStampHelper,
|
||||
stamp_helper: TimestampHelper,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
|
||||
pub fn new(tm_sender: TmSender, pus_router: PusTcMpscRouter) -> Self {
|
||||
impl PusTcDistributor {
|
||||
pub fn new(tm_sender: TmTcSender, pus_router: PusTcMpscRouter) -> Self {
|
||||
Self {
|
||||
id: PUS_ROUTING_SERVICE.raw(),
|
||||
id: PUS_ROUTING.raw(),
|
||||
tm_sender,
|
||||
verif_reporter: create_verification_reporter(
|
||||
PUS_ROUTING_SERVICE.id(),
|
||||
PUS_ROUTING_SERVICE.apid,
|
||||
),
|
||||
verif_reporter: create_verification_reporter(PUS_ROUTING.id(), PUS_ROUTING.apid),
|
||||
pus_router,
|
||||
stamp_helper: TimeStampHelper::default(),
|
||||
stamp_helper: TimestampHelper::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_tc_packet_vec(
|
||||
&mut self,
|
||||
packet_as_vec: PacketAsVec,
|
||||
) -> Result<PusPacketHandlerResult, GenericSendError> {
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
self.handle_tc_generic(packet_as_vec.sender_id, None, &packet_as_vec.packet)
|
||||
}
|
||||
|
||||
@@ -87,7 +82,7 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
|
||||
&mut self,
|
||||
packet_in_pool: PacketInPool,
|
||||
pus_tc_copy: &[u8],
|
||||
) -> Result<PusPacketHandlerResult, GenericSendError> {
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
self.handle_tc_generic(
|
||||
packet_in_pool.sender_id,
|
||||
Some(packet_in_pool.store_addr),
|
||||
@@ -100,7 +95,7 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
|
||||
sender_id: ComponentId,
|
||||
addr_opt: Option<PoolAddr>,
|
||||
raw_tc: &[u8],
|
||||
) -> Result<PusPacketHandlerResult, GenericSendError> {
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
let pus_tc_result = PusTcReader::new(raw_tc);
|
||||
if pus_tc_result.is_err() {
|
||||
log::warn!(
|
||||
@@ -108,17 +103,18 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
|
||||
sender_id,
|
||||
pus_tc_result.unwrap_err()
|
||||
);
|
||||
log::warn!("raw data: {:x?}", raw_tc);
|
||||
return Ok(PusPacketHandlerResult::RequestHandled);
|
||||
log::warn!("raw data: {raw_tc:x?}");
|
||||
// TODO: Shouldn't this be an error?
|
||||
return Ok(HandlingStatus::HandledOne);
|
||||
}
|
||||
let pus_tc = pus_tc_result.unwrap().0;
|
||||
let init_token = self.verif_reporter.add_tc(&pus_tc);
|
||||
let pus_tc = pus_tc_result.unwrap();
|
||||
let init_token = self.verif_reporter.start_verification(&pus_tc);
|
||||
self.stamp_helper.update_from_now();
|
||||
let accepted_token = self
|
||||
.verif_reporter
|
||||
.acceptance_success(&self.tm_sender, init_token, self.stamp_helper.stamp())
|
||||
.expect("Acceptance success failure");
|
||||
let service = PusServiceId::try_from(pus_tc.service());
|
||||
let service = PusServiceId::try_from(pus_tc.service_type_id());
|
||||
let tc_in_memory: TcInMemory = if let Some(store_addr) = addr_opt {
|
||||
PacketInPool::new(sender_id, store_addr).into()
|
||||
} else {
|
||||
@@ -189,17 +185,66 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
Ok(HandlingStatus::HandledOne)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub trait TargetedPusService {
|
||||
/// Returns [true] if the packet handling is finished.
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus;
|
||||
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus;
|
||||
const SERVICE_ID: u8;
|
||||
const SERVICE_STR: &'static str;
|
||||
|
||||
fn poll_and_handle_next_tc_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let result = self.poll_and_handle_next_tc(time_stamp);
|
||||
if let Err(e) = result {
|
||||
log::error!(
|
||||
"PUS service {}({})packet handling error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on error cases.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
result.unwrap()
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_reply_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
// This only fails if all senders disconnected. Treat it like an empty queue.
|
||||
self.poll_and_handle_next_reply(time_stamp)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"PUS servce {}({}): Handling reply failed with error {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
HandlingStatus::Empty
|
||||
})
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
|
||||
/// Generic trait for services which handle packets directly. Kept minimal right now because
|
||||
/// of the difficulty to allow flexible user code for these services..
|
||||
pub trait DirectPusService {
|
||||
const SERVICE_ID: u8;
|
||||
const SERVICE_STR: &'static str;
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus;
|
||||
}
|
||||
|
||||
/// This is a generic handler class for all PUS services where a PUS telecommand is converted
|
||||
/// to a targeted request.
|
||||
///
|
||||
@@ -223,21 +268,19 @@ pub trait TargetedPusService {
|
||||
/// 3. [Self::check_for_request_timeouts] which checks for request timeouts, covering step 7.
|
||||
pub struct PusTargetedRequestService<
|
||||
TcReceiver: EcssTcReceiver,
|
||||
TmSender: EcssTmSender,
|
||||
TcInMemConverter: EcssTcInMemConverter,
|
||||
VerificationReporter: VerificationReportingProvider,
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
> {
|
||||
pub service_helper:
|
||||
PusServiceHelper<TcReceiver, TmSender, TcInMemConverter, VerificationReporter>,
|
||||
PusServiceHelper<TcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
pub request_router: GenericRequestRouter,
|
||||
pub request_converter: RequestConverter,
|
||||
pub active_request_map: ActiveRequestMap,
|
||||
pub active_request_map: ActiveRequestMapInstance,
|
||||
pub reply_handler: ReplyHandler,
|
||||
pub reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||
phantom: std::marker::PhantomData<(RequestType, ActiveRequestInfo, ReplyType)>,
|
||||
@@ -245,24 +288,20 @@ pub struct PusTargetedRequestService<
|
||||
|
||||
impl<
|
||||
TcReceiver: EcssTcReceiver,
|
||||
TmSender: EcssTmSender,
|
||||
TcInMemConverter: EcssTcInMemConverter,
|
||||
VerificationReporter: VerificationReportingProvider,
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
>
|
||||
PusTargetedRequestService<
|
||||
TcReceiver,
|
||||
TmSender,
|
||||
TcInMemConverter,
|
||||
VerificationReporter,
|
||||
RequestConverter,
|
||||
ReplyHandler,
|
||||
ActiveRequestMap,
|
||||
ActiveRequestMapInstance,
|
||||
ActiveRequestInfo,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
@@ -273,12 +312,12 @@ where
|
||||
pub fn new(
|
||||
service_helper: PusServiceHelper<
|
||||
TcReceiver,
|
||||
TmSender,
|
||||
TcInMemConverter,
|
||||
TmTcSender,
|
||||
EcssTcCacher,
|
||||
VerificationReporter,
|
||||
>,
|
||||
request_converter: RequestConverter,
|
||||
active_request_map: ActiveRequestMap,
|
||||
active_request_map: ActiveRequestMapInstance,
|
||||
reply_hook: ReplyHandler,
|
||||
request_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||
@@ -297,10 +336,10 @@ where
|
||||
pub fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError> {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
return Ok(HandlingStatus::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
self.service_helper
|
||||
@@ -356,7 +395,7 @@ where
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
Ok(HandlingStatus::HandledOne)
|
||||
}
|
||||
|
||||
fn handle_conversion_to_request_error(
|
||||
@@ -409,7 +448,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_and_check_next_reply(
|
||||
pub fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError> {
|
||||
@@ -439,20 +478,17 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
let active_request = active_req_opt.unwrap();
|
||||
let request_finished = self
|
||||
.reply_handler
|
||||
.handle_reply(
|
||||
reply,
|
||||
active_request,
|
||||
&self.service_helper.common.tm_sender,
|
||||
&self.service_helper.common.verif_reporter,
|
||||
time_stamp,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if request_finished {
|
||||
let result = self.reply_handler.handle_reply(
|
||||
reply,
|
||||
active_request,
|
||||
&self.service_helper.common.tm_sender,
|
||||
&self.service_helper.common.verif_reporter,
|
||||
time_stamp,
|
||||
);
|
||||
if result.is_err() || (result.is_ok() && *result.as_ref().unwrap()) {
|
||||
self.active_request_map.remove(reply.request_id());
|
||||
}
|
||||
Ok(())
|
||||
result.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn check_for_request_timeouts(&mut self) {
|
||||
@@ -475,7 +511,7 @@ where
|
||||
/// and also log the error.
|
||||
pub fn generic_pus_request_timeout_handler(
|
||||
sender: &(impl EcssTmSender + ?Sized),
|
||||
active_request: &(impl ActiveRequestProvider + Debug),
|
||||
active_request: &(impl ActiveRequest + Debug),
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
service_str: &'static str,
|
||||
@@ -497,13 +533,15 @@ pub fn generic_pus_request_timeout_handler(
|
||||
pub(crate) mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use arbitrary_int::{u11, u21};
|
||||
use satrs::pus::test_util::TEST_COMPONENT_ID_0;
|
||||
use satrs::pus::{MpscTmAsVecSender, PusTmVariant};
|
||||
use satrs::request::RequestId;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
pus::{
|
||||
verification::test_util::TestVerificationReporter, ActivePusRequestStd,
|
||||
ActiveRequestMapProvider, EcssTcInVecConverter, MpscTcReceiver,
|
||||
ActiveRequestStore, MpscTcReceiver,
|
||||
},
|
||||
request::UniqueApidTargetId,
|
||||
spacepackets::{
|
||||
@@ -522,7 +560,7 @@ pub(crate) mod tests {
|
||||
// Testbench dedicated to the testing of [PusReplyHandler]s
|
||||
pub struct ReplyHandlerTestbench<
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Reply,
|
||||
> {
|
||||
pub id: ComponentId,
|
||||
@@ -536,7 +574,7 @@ pub(crate) mod tests {
|
||||
|
||||
impl<
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Reply,
|
||||
> ReplyHandlerTestbench<ReplyHandler, ActiveRequestInfo, Reply>
|
||||
{
|
||||
@@ -556,17 +594,17 @@ pub(crate) mod tests {
|
||||
|
||||
pub fn add_tc(
|
||||
&mut self,
|
||||
apid: u16,
|
||||
apid_target: u32,
|
||||
apid: u11,
|
||||
apid_target: u21,
|
||||
time_stamp: &[u8],
|
||||
) -> (verification::RequestId, ActivePusRequestStd) {
|
||||
let sp_header = SpHeader::new_from_apid(apid);
|
||||
let sec_header_dummy = PusTcSecondaryHeader::new_simple(0, 0);
|
||||
let init = self.verif_reporter.add_tc(&PusTcCreator::new(
|
||||
let sec_header_dummy = PusTcSecondaryHeader::new_simple(MessageTypeId::new(0, 0));
|
||||
let init = self.verif_reporter.start_verification(&PusTcCreator::new(
|
||||
sp_header,
|
||||
sec_header_dummy,
|
||||
&[],
|
||||
true,
|
||||
CreatorConfig::default(),
|
||||
));
|
||||
let accepted = self
|
||||
.verif_reporter
|
||||
@@ -637,7 +675,7 @@ pub(crate) mod tests {
|
||||
// Testbench dedicated to the testing of [PusTcToRequestConverter]s
|
||||
pub struct PusConverterTestbench<
|
||||
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Request,
|
||||
> {
|
||||
pub id: ComponentId,
|
||||
@@ -651,7 +689,7 @@ pub(crate) mod tests {
|
||||
|
||||
impl<
|
||||
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Request,
|
||||
> PusConverterTestbench<Converter, ActiveRequestInfo, Request>
|
||||
{
|
||||
@@ -669,7 +707,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
pub fn add_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
|
||||
let token = self.verif_reporter.add_tc(tc);
|
||||
let token = self.verif_reporter.start_verification(tc);
|
||||
self.current_request_id = Some(verification::RequestId::new(tc));
|
||||
self.current_packet = Some(tc.to_vec().unwrap());
|
||||
self.verif_reporter
|
||||
@@ -685,8 +723,8 @@ pub(crate) mod tests {
|
||||
&mut self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
time_stamp: &[u8],
|
||||
expected_apid: u16,
|
||||
expected_apid_target: u32,
|
||||
expected_apid: u11,
|
||||
expected_apid_target: u21,
|
||||
) -> Result<(ActiveRequestInfo, Request), Converter::Error> {
|
||||
if self.current_packet.is_none() {
|
||||
return Err(GenericConversionError::InvalidAppData(
|
||||
@@ -697,7 +735,7 @@ pub(crate) mod tests {
|
||||
let tc_reader = PusTcReader::new(¤t_packet).unwrap();
|
||||
let (active_info, request) = self.converter.convert(
|
||||
token,
|
||||
&tc_reader.0,
|
||||
&tc_reader,
|
||||
&self.dummy_sender,
|
||||
&self.verif_reporter,
|
||||
time_stamp,
|
||||
@@ -717,19 +755,17 @@ pub(crate) mod tests {
|
||||
pub struct TargetedPusRequestTestbench<
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequestProvider,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
> {
|
||||
pub service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
MpscTmAsVecSender,
|
||||
EcssTcInVecConverter,
|
||||
TestVerificationReporter,
|
||||
RequestConverter,
|
||||
ReplyHandler,
|
||||
ActiveRequestMap,
|
||||
ActiveRequestMapInstance,
|
||||
ActiveRequestInfo,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
@@ -1,15 +1,17 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u14;
|
||||
use derive_new::new;
|
||||
use log::{error, warn};
|
||||
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
|
||||
use satrs::mode_tree::{ModeNode, ModeParent};
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use example_std::ids;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use satrs::pool::SharedStaticMemoryPool;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DefaultActiveRequestMap, EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter,
|
||||
EcssTcInVecConverter, MpscTcReceiver, MpscTmAsVecSender, PusPacketHandlerResult,
|
||||
DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher, MpscTcReceiver, PusPacketHandlingError,
|
||||
PusServiceHelper,
|
||||
};
|
||||
use satrs::request::GenericMessage;
|
||||
@@ -21,8 +23,8 @@ use satrs::{
|
||||
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReportingProvider,
|
||||
VerificationToken,
|
||||
},
|
||||
ActivePusRequestStd, ActiveRequestProvider, EcssTmSender, EcssTmtcError,
|
||||
GenericConversionError, PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
|
||||
ActivePusRequestStd, ActiveRequest, EcssTmSender, EcssTmtcError, GenericConversionError,
|
||||
PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
|
||||
},
|
||||
request::UniqueApidTargetId,
|
||||
spacepackets::{
|
||||
@@ -35,8 +37,7 @@ use satrs::{
|
||||
},
|
||||
ComponentId,
|
||||
};
|
||||
use satrs_example::config::components::PUS_MODE_SERVICE;
|
||||
use satrs_example::config::{mode_err, tmtc_err};
|
||||
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
|
||||
|
||||
use super::{
|
||||
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
|
||||
@@ -79,10 +80,19 @@ impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
|
||||
.write_to_be_bytes(&mut source_data)
|
||||
.expect("writing mode reply failed");
|
||||
let req_id = verification::RequestId::from(reply.request_id());
|
||||
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), 0, 0);
|
||||
let sec_header =
|
||||
PusTmSecondaryHeader::new(200, Subservice::TmModeReply as u8, 0, 0, time_stamp);
|
||||
let pus_tm = PusTmCreator::new(sp_header, sec_header, &source_data, true);
|
||||
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), u14::ZERO, 0);
|
||||
let sec_header = PusTmSecondaryHeader::new(
|
||||
MessageTypeId::new(200, Subservice::TmModeReply as u8),
|
||||
0,
|
||||
0,
|
||||
time_stamp,
|
||||
);
|
||||
let pus_tm = PusTmCreator::new(
|
||||
sp_header,
|
||||
sec_header,
|
||||
&source_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
tm_sender.send_tm(self.owner_id, PusTmVariant::Direct(pus_tm))?;
|
||||
verification_handler.completion_success(tm_sender, started_token, time_stamp)?;
|
||||
}
|
||||
@@ -111,6 +121,7 @@ impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
|
||||
),
|
||||
)?;
|
||||
}
|
||||
ModeReply::ModeInfo(_mode_and_submode) => (),
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
@@ -147,7 +158,7 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(ActivePusRequestStd, ModeRequest), Self::Error> {
|
||||
let subservice = tc.subservice();
|
||||
let subservice = tc.message_subtype_id();
|
||||
let user_data = tc.user_data();
|
||||
let not_enough_app_data = |expected: usize| {
|
||||
verif_reporter
|
||||
@@ -191,7 +202,13 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
|
||||
}
|
||||
let mode_and_submode = ModeAndSubmode::from_be_bytes(&tc.user_data()[4..])
|
||||
.expect("mode and submode extraction failed");
|
||||
Ok((active_request, ModeRequest::SetMode(mode_and_submode)))
|
||||
Ok((
|
||||
active_request,
|
||||
ModeRequest::SetMode {
|
||||
mode_and_submode,
|
||||
forced: false,
|
||||
},
|
||||
))
|
||||
}
|
||||
Subservice::TcReadMode => Ok((active_request, ModeRequest::ReadMode)),
|
||||
Subservice::TcAnnounceMode => Ok((active_request, ModeRequest::AnnounceMode)),
|
||||
@@ -203,24 +220,27 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_mode_service_static(
|
||||
tm_sender: PacketSenderWithSharedPool,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pub fn create_mode_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
mode_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
|
||||
) -> ModeServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
|
||||
) -> ModeServiceWrapper {
|
||||
let mode_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_MODE_SERVICE.id(),
|
||||
ids::generic_pus::PUS_MODE.id(),
|
||||
pus_action_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_MODE_SERVICE.id(), PUS_MODE_SERVICE.apid),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
|
||||
create_verification_reporter(
|
||||
ids::generic_pus::PUS_MODE.id(),
|
||||
ids::generic_pus::PUS_MODE.apid,
|
||||
),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
ModeRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
ModeReplyHandler::new(PUS_MODE_SERVICE.id()),
|
||||
ModeReplyHandler::new(ids::generic_pus::PUS_MODE.id()),
|
||||
mode_router,
|
||||
reply_receiver,
|
||||
);
|
||||
@@ -229,36 +249,9 @@ pub fn create_mode_service_static(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_mode_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
mode_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
|
||||
) -> ModeServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
|
||||
let mode_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_MODE_SERVICE.id(),
|
||||
pus_action_rx,
|
||||
tm_funnel_tx,
|
||||
create_verification_reporter(PUS_MODE_SERVICE.id(), PUS_MODE_SERVICE.apid),
|
||||
EcssTcInVecConverter::default(),
|
||||
),
|
||||
ModeRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
ModeReplyHandler::new(PUS_MODE_SERVICE.id()),
|
||||
mode_router,
|
||||
reply_receiver,
|
||||
);
|
||||
ModeServiceWrapper {
|
||||
service: mode_request_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModeServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub struct ModeServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
TmSender,
|
||||
TcInMemConverter,
|
||||
VerificationReporter,
|
||||
ModeRequestConverter,
|
||||
ModeReplyHandler,
|
||||
@@ -269,51 +262,51 @@ pub struct ModeServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcIn
|
||||
>,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> TargetedPusService
|
||||
for ModeServiceWrapper<TmSender, TcInMemConverter>
|
||||
{
|
||||
/// Returns [true] if the packet handling is finished.
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
match self.service.poll_and_handle_next_tc(time_stamp) {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
warn!("PUS mode service: partial packet handling success: {e:?}")
|
||||
}
|
||||
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
|
||||
warn!("PUS mode service: invalid subservice {invalid}");
|
||||
}
|
||||
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
warn!("PUS mode service: {subservice} not implemented");
|
||||
}
|
||||
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
|
||||
},
|
||||
Err(error) => {
|
||||
error!("PUS mode service: packet handling error: {error:?}");
|
||||
// To avoid permanent loops on error cases.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
self.service
|
||||
.poll_and_check_next_reply(time_stamp)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("PUS action service: Handling reply failed with error {e:?}");
|
||||
HandlingStatus::HandledOne
|
||||
})
|
||||
}
|
||||
|
||||
fn check_for_request_timeouts(&mut self) {
|
||||
self.service.check_for_request_timeouts();
|
||||
impl ModeNode for ModeServiceWrapper {
|
||||
fn id(&self) -> ComponentId {
|
||||
self.service.service_helper.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeParent for ModeServiceWrapper {
|
||||
type Sender = mpsc::SyncSender<GenericMessage<ModeRequest>>;
|
||||
|
||||
fn add_mode_child(&mut self, id: ComponentId, request_sender: Self::Sender) {
|
||||
self.service
|
||||
.request_router
|
||||
.mode_router_map
|
||||
.insert(id, request_sender);
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetedPusService for ModeServiceWrapper {
|
||||
const SERVICE_ID: u8 = CustomPusServiceId::Mode as u8;
|
||||
const SERVICE_STR: &'static str = "mode";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer;
|
||||
use arbitrary_int::u14;
|
||||
use satrs::pus::test_util::{TEST_APID, TEST_COMPONENT_ID_0, TEST_UNIQUE_ID_0};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
mode::{ModeAndSubmode, ModeReply, ModeRequest},
|
||||
pus::mode::Subservice,
|
||||
@@ -323,7 +316,7 @@ mod tests {
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
use satrs_example::config::tmtc_err;
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
use crate::pus::{
|
||||
mode::ModeReplyHandler,
|
||||
@@ -336,11 +329,12 @@ mod tests {
|
||||
fn mode_converter_read_mode_request() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcReadMode as u8);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcReadMode as u8));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
@@ -352,31 +346,41 @@ mod tests {
|
||||
fn mode_converter_set_mode_request() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcSetMode as u8);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcSetMode as u8));
|
||||
let mut app_data: [u8; 4 + ModeAndSubmode::RAW_LEN] = [0; 4 + ModeAndSubmode::RAW_LEN];
|
||||
let mode_and_submode = ModeAndSubmode::new(2, 1);
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
mode_and_submode
|
||||
.write_to_be_bytes(&mut app_data[4..])
|
||||
.unwrap();
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion has failed");
|
||||
assert_eq!(req, ModeRequest::SetMode(mode_and_submode));
|
||||
assert_eq!(
|
||||
req,
|
||||
ModeRequest::SetMode {
|
||||
mode_and_submode,
|
||||
forced: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_converter_announce_mode() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcAnnounceMode as u8);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
|
||||
200,
|
||||
Subservice::TcAnnounceMode as u8,
|
||||
));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
@@ -388,12 +392,14 @@ mod tests {
|
||||
fn mode_converter_announce_mode_recursively() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(200, Subservice::TcAnnounceModeRecursive as u8);
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
|
||||
200,
|
||||
Subservice::TcAnnounceModeRecursive as u8,
|
||||
));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
@@ -409,7 +415,7 @@ mod tests {
|
||||
);
|
||||
let mode_reply = ModeReply::ModeReply(ModeAndSubmode::new(5, 1));
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), mode_reply);
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), mode_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::info;
|
||||
use satrs::pool::{PoolProvider, StaticMemoryPool};
|
||||
use satrs::pus::scheduler::{PusSchedulerAlloc, TcInfo};
|
||||
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
|
||||
PartialPusHandlingError, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::PusServiceId;
|
||||
use satrs::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool};
|
||||
use satrs::ComponentId;
|
||||
use example_std::ids::sched::PUS_SCHED;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub trait TcReleaseProvider {
|
||||
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for PacketSenderWithSharedPool {
|
||||
fn release(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
enabled: bool,
|
||||
_info: &TcInfo,
|
||||
tc: &[u8],
|
||||
) -> bool {
|
||||
if enabled {
|
||||
let shared_pool = self.shared_pool.get_mut();
|
||||
// Transfer TC from scheduler TC pool to shared TC pool.
|
||||
let released_tc_addr = shared_pool
|
||||
.0
|
||||
.write()
|
||||
.expect("locking pool failed")
|
||||
.add(tc)
|
||||
.expect("adding TC to shared pool failed");
|
||||
self.sender
|
||||
.send(PacketInPool::new(sender_id, released_tc_addr))
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for mpsc::SyncSender<PacketAsVec> {
|
||||
fn release(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
enabled: bool,
|
||||
_info: &TcInfo,
|
||||
tc: &[u8],
|
||||
) -> bool {
|
||||
if enabled {
|
||||
// Send released TC to centralized TC source.
|
||||
self.send(PacketAsVec::new(sender_id, tc.to_vec()))
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum TcReleaser {
|
||||
Static(PacketSenderWithSharedPool),
|
||||
Heap(mpsc::SyncSender<PacketAsVec>),
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for TcReleaser {
|
||||
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool {
|
||||
match self {
|
||||
TcReleaser::Static(sender) => sender.release(sender_id, enabled, info, tc),
|
||||
TcReleaser::Heap(sender) => sender.release(sender_id, enabled, info, tc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SchedulingServiceWrapper {
|
||||
pub pus_11_handler: PusSchedServiceHandler<
|
||||
MpscTcReceiver,
|
||||
TmTcSender,
|
||||
EcssTcCacher,
|
||||
VerificationReporter,
|
||||
PusSchedulerAlloc,
|
||||
>,
|
||||
pub sched_tc_pool: StaticMemoryPool,
|
||||
pub releaser_buf: [u8; 4096],
|
||||
pub tc_releaser: TcReleaser,
|
||||
}
|
||||
|
||||
impl DirectPusService for SchedulingServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Verification as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "verification";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
|
||||
let result = self.pus_11_handler.poll_and_handle_next_tc(
|
||||
error_handler,
|
||||
time_stamp,
|
||||
&mut self.sched_tc_pool,
|
||||
);
|
||||
if let Err(e) = result {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match result.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
|
||||
impl SchedulingServiceWrapper {
|
||||
pub fn release_tcs(&mut self) {
|
||||
let id = self.pus_11_handler.service_helper.id();
|
||||
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
|
||||
self.tc_releaser.release(id, enabled, info, tc)
|
||||
};
|
||||
|
||||
self.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.update_time_from_now()
|
||||
.unwrap();
|
||||
let released_tcs = self
|
||||
.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.release_telecommands_with_buffer(
|
||||
releaser,
|
||||
&mut self.sched_tc_pool,
|
||||
&mut self.releaser_buf,
|
||||
)
|
||||
.expect("releasing TCs failed");
|
||||
if released_tcs > 0 {
|
||||
info!("{released_tcs} TC(s) released from scheduler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_scheduler_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
tc_releaser: TcReleaser,
|
||||
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
sched_tc_pool: StaticMemoryPool,
|
||||
) -> SchedulingServiceWrapper {
|
||||
let scheduler = PusSchedulerAlloc::new_with_current_init_time(Duration::from_secs(5))
|
||||
.expect("Creating PUS Scheduler failed");
|
||||
let pus_11_handler = PusSchedServiceHandler::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_SCHED.id(),
|
||||
pus_sched_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_SCHED.id(), PUS_SCHED.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
scheduler,
|
||||
);
|
||||
SchedulingServiceWrapper {
|
||||
pus_11_handler,
|
||||
sched_tc_pool,
|
||||
releaser_buf: [0; 4096],
|
||||
tc_releaser,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use crate::pus::mode::ModeServiceWrapper;
|
||||
use derive_new::new;
|
||||
use satrs::spacepackets::time::{cds, TimeWriter};
|
||||
|
||||
use super::{
|
||||
action::ActionServiceWrapper, event::EventServiceWrapper, hk::HkServiceWrapper,
|
||||
scheduler::SchedulingServiceWrapper, test::TestCustomServiceWrapper, DirectPusService,
|
||||
HandlingStatus, TargetedPusService,
|
||||
};
|
||||
|
||||
#[derive(new)]
|
||||
pub struct PusStack {
|
||||
pub test_srv: TestCustomServiceWrapper,
|
||||
pub hk_srv_wrapper: HkServiceWrapper,
|
||||
pub event_srv: EventServiceWrapper,
|
||||
pub action_srv_wrapper: ActionServiceWrapper,
|
||||
pub schedule_srv: SchedulingServiceWrapper,
|
||||
pub mode_srv: ModeServiceWrapper,
|
||||
}
|
||||
|
||||
impl PusStack {
|
||||
pub fn periodic_operation(&mut self) {
|
||||
// Release all telecommands which reached their release time before calling the service
|
||||
// handlers.
|
||||
self.schedule_srv.release_tcs();
|
||||
let timestamp = cds::CdsTime::now_with_u16_days()
|
||||
.expect("time stamp generation error")
|
||||
.to_vec()
|
||||
.unwrap();
|
||||
let mut loop_count = 0_u32;
|
||||
// Hot loop which will run continuously until all request and reply handling is done.
|
||||
loop {
|
||||
let mut nothing_to_do = true;
|
||||
Self::direct_service_checker(&mut self.test_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::direct_service_checker(&mut self.schedule_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::direct_service_checker(&mut self.event_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::targeted_service_checker(
|
||||
&mut self.action_srv_wrapper,
|
||||
×tamp,
|
||||
&mut nothing_to_do,
|
||||
);
|
||||
Self::targeted_service_checker(
|
||||
&mut self.hk_srv_wrapper,
|
||||
×tamp,
|
||||
&mut nothing_to_do,
|
||||
);
|
||||
Self::targeted_service_checker(&mut self.mode_srv, ×tamp, &mut nothing_to_do);
|
||||
if nothing_to_do {
|
||||
// Timeout checking is only done once.
|
||||
self.action_srv_wrapper.check_for_request_timeouts();
|
||||
self.hk_srv_wrapper.check_for_request_timeouts();
|
||||
self.mode_srv.check_for_request_timeouts();
|
||||
break;
|
||||
}
|
||||
// Safety mechanism to avoid infinite loops.
|
||||
loop_count += 1;
|
||||
if loop_count >= 500 {
|
||||
log::warn!("reached PUS stack loop count 500, breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn direct_service_checker<S: DirectPusService>(
|
||||
service: &mut S,
|
||||
timestamp: &[u8],
|
||||
nothing_to_do: &mut bool,
|
||||
) {
|
||||
let handling_status = service.poll_and_handle_next_tc(timestamp);
|
||||
if handling_status == HandlingStatus::HandledOne {
|
||||
*nothing_to_do = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn targeted_service_checker<S: TargetedPusService>(
|
||||
service: &mut S,
|
||||
timestamp: &[u8],
|
||||
nothing_to_do: &mut bool,
|
||||
) {
|
||||
let request_handling = service.poll_and_handle_next_tc_default_handler(timestamp);
|
||||
let reply_handling = service.poll_and_handle_next_reply_default_handler(timestamp);
|
||||
if request_handling == HandlingStatus::HandledOne
|
||||
|| reply_handling == HandlingStatus::HandledOne
|
||||
{
|
||||
*nothing_to_do = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::info;
|
||||
use satrs::event_man_legacy::{EventMessage, EventMessageU32};
|
||||
use satrs::pus::test::PusService17TestHandler;
|
||||
use satrs::pus::verification::{FailParams, VerificationReporter, VerificationReportingProvider};
|
||||
use satrs::pus::PartialPusHandlingError;
|
||||
use satrs::pus::{
|
||||
CacheAndReadRawEcssTc, DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher,
|
||||
MpscTcReceiver, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
|
||||
use example_std::config::{tmtc_err, TEST_EVENT};
|
||||
use example_std::ids::generic_pus::PUS_TEST;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub fn create_test_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
event_sender: mpsc::SyncSender<EventMessageU32>,
|
||||
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
) -> TestCustomServiceWrapper {
|
||||
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
|
||||
PUS_TEST.id(),
|
||||
pus_test_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_TEST.id(), PUS_TEST.apid),
|
||||
tc_in_mem_converter,
|
||||
));
|
||||
TestCustomServiceWrapper {
|
||||
handler: pus17_handler,
|
||||
event_tx: event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestCustomServiceWrapper {
|
||||
pub handler:
|
||||
PusService17TestHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
pub event_tx: mpsc::SyncSender<EventMessageU32>,
|
||||
}
|
||||
|
||||
impl DirectPusService for TestCustomServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Test as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "test";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
let res = self
|
||||
.handler
|
||||
.poll_and_handle_next_tc(error_handler, timestamp);
|
||||
if let Err(e) = res {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match res.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => {
|
||||
if handling_status == HandlingStatus::HandledOne {
|
||||
info!("Received PUS ping command TC[17,1]");
|
||||
info!("Sent ping reply PUS TM[17,2]");
|
||||
}
|
||||
return handling_status;
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, token) => {
|
||||
let tc = PusTcReader::new(
|
||||
self.handler
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.tc_slice_raw(),
|
||||
)
|
||||
.unwrap();
|
||||
if subservice == 128 {
|
||||
info!("generating test event");
|
||||
self.event_tx
|
||||
.send(EventMessage::new(PUS_TEST.id(), TEST_EVENT.into()))
|
||||
.expect("Sending test event failed");
|
||||
match self.handler.service_helper.verif_reporter().start_success(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
token,
|
||||
timestamp,
|
||||
) {
|
||||
Ok(started_token) => {
|
||||
if let Err(e) = self
|
||||
.handler
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.completion_success(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
started_token,
|
||||
timestamp,
|
||||
)
|
||||
{
|
||||
error_handler(&PartialPusHandlingError::Verification(e));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error_handler(&PartialPusHandlingError::Verification(e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let fail_data = [tc.message_subtype_id()];
|
||||
self.handler
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.start_failure(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
token,
|
||||
FailParams::new(
|
||||
timestamp,
|
||||
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||
&fail_data,
|
||||
),
|
||||
)
|
||||
.expect("Sending start failure verification failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,15 @@ use satrs::mode::ModeRequest;
|
||||
use satrs::pus::verification::{
|
||||
FailParams, TcStateAccepted, VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{ActiveRequestProvider, EcssTmSender, GenericRoutingError, PusRequestRouter};
|
||||
use satrs::pus::{ActiveRequest, EcssTmSender, GenericRoutingError, PusRequestRouter};
|
||||
use satrs::queue::GenericSendError;
|
||||
use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::PusPacket;
|
||||
use satrs::ComponentId;
|
||||
use satrs_example::config::components::PUS_ROUTING_SERVICE;
|
||||
use satrs_example::config::tmtc_err;
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
/*
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum CompositeRequest {
|
||||
@@ -26,16 +26,18 @@ pub enum CompositeRequest {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GenericRequestRouter {
|
||||
#[allow(dead_code)]
|
||||
pub id: ComponentId,
|
||||
// All messages which do not have a dedicated queue.
|
||||
pub composite_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<CompositeRequest>>>,
|
||||
pub mode_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<ModeRequest>>>,
|
||||
pub composite_router_map:
|
||||
HashMap<ComponentId, mpsc::SyncSender<GenericMessage<CompositeRequest>>>,
|
||||
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
|
||||
}
|
||||
|
||||
impl Default for GenericRequestRouter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: PUS_ROUTING_SERVICE.raw(),
|
||||
id: ids::generic_pus::PUS_ROUTING.raw(),
|
||||
composite_router_map: Default::default(),
|
||||
mode_router_map: Default::default(),
|
||||
}
|
||||
@@ -44,7 +46,7 @@ impl Default for GenericRequestRouter {
|
||||
impl GenericRequestRouter {
|
||||
pub(crate) fn handle_error_generic(
|
||||
&self,
|
||||
active_request: &impl ActiveRequestProvider,
|
||||
active_request: &impl ActiveRequest,
|
||||
tc: &PusTcReader,
|
||||
error: GenericRoutingError,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
@@ -53,7 +55,7 @@ impl GenericRequestRouter {
|
||||
) {
|
||||
warn!(
|
||||
"Routing request for service {} failed: {error:?}",
|
||||
tc.service()
|
||||
tc.service_type_id()
|
||||
);
|
||||
let accepted_token: VerificationToken<TcStateAccepted> = active_request
|
||||
.token()
|
||||
@@ -64,7 +66,8 @@ impl GenericRequestRouter {
|
||||
let apid_target_id = UniqueApidTargetId::from(id);
|
||||
warn!("Target APID for request: {}", apid_target_id.apid);
|
||||
warn!("Target Unique ID for request: {}", apid_target_id.unique_id);
|
||||
let mut fail_data: [u8; 8] = [0; 8];
|
||||
let mut fail_data: [u8; core::mem::size_of::<ComponentId>()] =
|
||||
[0; core::mem::size_of::<ComponentId>()];
|
||||
fail_data.copy_from_slice(&id.to_be_bytes());
|
||||
verif_reporter
|
||||
.completion_failure(
|
||||
@@ -150,3 +153,4 @@ impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
|
||||
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,158 @@
|
||||
extern crate alloc;
|
||||
|
||||
use std::{
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
|
||||
pub use types::ComponentId;
|
||||
use types::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
|
||||
|
||||
pub mod config;
|
||||
|
||||
/// Simple type modelling packet stored in the heap. This structure is intended to
|
||||
/// be used when sending a packet via a message queue, so it also contains the sender ID.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct PacketAsVec {
|
||||
pub sender_id: ComponentId,
|
||||
pub packet: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PacketAsVec {
|
||||
pub fn new(sender_id: ComponentId, packet: Vec<u8>) -> Self {
|
||||
Self { sender_id, packet }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TimestampHelper {
|
||||
stamper: CdsTime,
|
||||
time_stamp: [u8; 7],
|
||||
}
|
||||
|
||||
impl TimestampHelper {
|
||||
pub fn stamp(&self) -> &[u8] {
|
||||
&self.time_stamp
|
||||
}
|
||||
|
||||
pub fn update_from_now(&mut self) {
|
||||
self.stamper
|
||||
.update_from_now()
|
||||
.expect("Updating timestamp failed");
|
||||
self.stamper
|
||||
.write_to_bytes(&mut self.time_stamp)
|
||||
.expect("Writing timestamp failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimestampHelper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),
|
||||
time_stamp: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper structure for periodic HK generation of a single set.
|
||||
#[derive(Debug)]
|
||||
pub struct HkHelperSingleSet {
|
||||
pub enabled: bool,
|
||||
pub frequency: Duration,
|
||||
pub last_generated: Option<Instant>,
|
||||
}
|
||||
|
||||
impl HkHelperSingleSet {
|
||||
#[inline]
|
||||
pub const fn new(enabled: bool, init_frequency: Duration) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
frequency: init_frequency,
|
||||
last_generated: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Check whether a new HK packet needs to be generated.
|
||||
pub fn needs_generation(&mut self) -> bool {
|
||||
if !self.enabled {
|
||||
return false;
|
||||
}
|
||||
if self.last_generated.is_none() {
|
||||
self.last_generated = Some(Instant::now());
|
||||
return true;
|
||||
}
|
||||
let last_generated = self.last_generated.unwrap();
|
||||
if Instant::now() - last_generated >= self.frequency {
|
||||
self.last_generated = Some(Instant::now());
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TmtcQueues {
|
||||
pub tc_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
pub tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModeHelper<Mode, TransitionState> {
|
||||
pub current: Mode,
|
||||
pub target: Option<Mode>,
|
||||
pub tc_commander: Option<CcsdsPacketIdAndPsc>,
|
||||
pub transition_start: Option<Instant>,
|
||||
pub timeout: Duration,
|
||||
pub transition_state: TransitionState,
|
||||
}
|
||||
|
||||
impl<Mode: Copy + Clone, TransitionState: Default> ModeHelper<Mode, TransitionState> {
|
||||
pub fn new(init_mode: Mode, timeout: Duration) -> Self {
|
||||
Self {
|
||||
current: init_mode,
|
||||
target: Default::default(),
|
||||
tc_commander: Default::default(),
|
||||
transition_start: None,
|
||||
timeout,
|
||||
transition_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, target: Mode) {
|
||||
self.target = Some(target);
|
||||
self.transition_start = Some(Instant::now());
|
||||
self.transition_state = TransitionState::default();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transition_active(&self) -> bool {
|
||||
self.target.is_some()
|
||||
}
|
||||
|
||||
pub fn timed_out(&self) -> bool {
|
||||
if self.target.is_none() {
|
||||
return false;
|
||||
}
|
||||
if let Some(transition_start) = self.transition_start {
|
||||
return Instant::now() - transition_start >= self.timeout;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn finish(&mut self, success: bool) -> Option<CcsdsPacketIdAndPsc> {
|
||||
self.target?;
|
||||
if success {
|
||||
self.current = self.target.take().unwrap();
|
||||
} else {
|
||||
self.target = None;
|
||||
}
|
||||
self.transition_state = Default::default();
|
||||
self.transition_start = None;
|
||||
self.tc_commander.take()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
use std::str::FromStr as _;
|
||||
|
||||
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
// Read the RUST_LOG environment variable and parse it.
|
||||
// If the variable is not set or is invalid, default to Debug.
|
||||
let mut log_level = log::LevelFilter::Info;
|
||||
if let Ok(log_level_str) = std::env::var("RUST_LOG") {
|
||||
log_level = log::LevelFilter::from_str(&log_level_str).unwrap_or(log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
@@ -9,7 +18,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.level(log_level)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
@@ -0,0 +1,475 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use eps::{
|
||||
PowerSwitchHelper,
|
||||
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
|
||||
};
|
||||
use example_std::{
|
||||
TmtcQueues,
|
||||
config::{
|
||||
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
|
||||
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
|
||||
},
|
||||
};
|
||||
use interface::{
|
||||
sim_client_udp::create_sim_client,
|
||||
tcp::{SyncTcpTmSource, TcpTask},
|
||||
udp::UdpTmtcServer,
|
||||
};
|
||||
use log::info;
|
||||
use logger::setup_logger;
|
||||
use satrs::{
|
||||
HandlingStatus,
|
||||
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
|
||||
spacepackets::time::cds::CdsTime,
|
||||
};
|
||||
use tmtc::sender::TmTcSender;
|
||||
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
|
||||
use types::{ComponentId, DeviceMode};
|
||||
|
||||
use crate::{
|
||||
acs::{ctrl, mgm, mgm_assembly, mgt, subsystem},
|
||||
controller::Controller,
|
||||
eps::pcdu::SwitchSet,
|
||||
event_manager::EventManager,
|
||||
interface::udp::UdpTmHandlerWithChannel,
|
||||
tmtc::tc_source::CcsdsDistributor,
|
||||
};
|
||||
|
||||
mod acs;
|
||||
mod ccsds;
|
||||
mod controller;
|
||||
mod device_fdir;
|
||||
mod device_mode;
|
||||
mod eps;
|
||||
mod event_manager;
|
||||
mod interface;
|
||||
mod logger;
|
||||
mod tmtc;
|
||||
fn main() {
|
||||
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
setup_logger().expect("setting up logging with fern failed");
|
||||
println!("Runng OBSW example");
|
||||
ctrlc::set_handler(move || {
|
||||
log::info!("Received Ctrl-C, shutting down");
|
||||
KILL_SIGNAL.store(true, Ordering::Relaxed);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
|
||||
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
|
||||
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
|
||||
|
||||
let (sim_request_tx, sim_request_rx) = mpsc::channel();
|
||||
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
|
||||
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
|
||||
let (mgt_sim_reply_tx, mgt_sim_reply_rx) = mpsc::channel();
|
||||
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
|
||||
let mut opt_sim_client = create_sim_client(sim_request_rx);
|
||||
|
||||
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
|
||||
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
|
||||
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
|
||||
let (event_manager_tc_tx, event_manager_tc_rx) = mpsc::sync_channel(10);
|
||||
|
||||
let (mgt_request_tx, mgt_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgt_report_tx, mgt_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (acs_ctrl_request_tx, acs_ctrl_request_rx) = mpsc::sync_channel(5);
|
||||
let (acs_ctrl_response_tx, acs_ctrl_response_rx) = mpsc::sync_channel(5);
|
||||
|
||||
// These message handles need to go into the MGM assembly and ACS subsystem.
|
||||
let (mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_assembly_report_tx, mgm_assembly_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
// These message handles need to go into the MGM assembly and MGM devices.
|
||||
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
|
||||
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
|
||||
let mut event_manager = EventManager::new(
|
||||
event_manager_tc_rx,
|
||||
event_ctrl_rx,
|
||||
mgm_event_rx,
|
||||
mgm_assembly_event_rx,
|
||||
mgt_event_rx,
|
||||
pcdu_event_rx,
|
||||
tc_source_event_rx,
|
||||
tm_sink_tx.clone(),
|
||||
);
|
||||
|
||||
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
|
||||
|
||||
let ccsds_distributor = CcsdsDistributor::default();
|
||||
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor, tc_source_event_tx);
|
||||
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
|
||||
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
|
||||
|
||||
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
|
||||
let udp_tm_handler = UdpTmHandlerWithChannel {
|
||||
tm_rx: tm_server_rx,
|
||||
};
|
||||
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let udp_tc_server = UdpTcServer::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
sock_addr,
|
||||
2048,
|
||||
tc_sender.clone(),
|
||||
)
|
||||
.expect("creating UDP TMTC server failed");
|
||||
let mut udp_tmtc_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: udp_tm_handler.into(),
|
||||
};
|
||||
|
||||
let tcp_server_cfg = ServerConfig::new(
|
||||
ComponentId::TcpServer as u32,
|
||||
sock_addr,
|
||||
Duration::from_millis(400),
|
||||
4096,
|
||||
8192,
|
||||
);
|
||||
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
|
||||
let mut tcp_server = TcpTask::new(
|
||||
tcp_server_cfg,
|
||||
sync_tm_tcp_source.clone(),
|
||||
tc_sender,
|
||||
PACKET_ID_VALIDATOR.clone(),
|
||||
)
|
||||
.expect("tcp server creation failed");
|
||||
|
||||
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
|
||||
|
||||
let shared_switch_set = Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
|
||||
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
|
||||
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
|
||||
|
||||
// Global FDIR health table, shared by all software objects.
|
||||
let health_table = satrs::health::HealthTableMapSync::default();
|
||||
|
||||
let shared_mgm_0_set = Arc::default();
|
||||
let shared_mgm_1_set = Arc::default();
|
||||
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
|
||||
opt_sim_client.as_mut()
|
||||
{
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
|
||||
(
|
||||
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||
id: mgm::MgmId::_0,
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgm_0_sim_reply_rx,
|
||||
}),
|
||||
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||
id: mgm::MgmId::_1,
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgm_1_sim_reply_rx,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||
)
|
||||
};
|
||||
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||
mgm::MgmId::_0,
|
||||
TmtcQueues {
|
||||
tc_rx: mgm_0_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgm_0_spi_interface,
|
||||
shared_mgm_0_set,
|
||||
mgm::ModeLeafHelper {
|
||||
request_rx: mgm_0_mode_request_rx,
|
||||
report_tx: mgm_0_mode_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgm_event_tx.clone(),
|
||||
);
|
||||
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||
mgm::MgmId::_1,
|
||||
TmtcQueues {
|
||||
tc_rx: mgm_1_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgm_1_spi_interface,
|
||||
shared_mgm_1_set,
|
||||
mgm::ModeLeafHelper {
|
||||
request_rx: mgm_1_mode_request_rx,
|
||||
report_tx: mgm_1_mode_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgm_event_tx,
|
||||
);
|
||||
let mut mgm_assembly = mgm_assembly::Assembly::new(
|
||||
mgm_assembly::ParentQueueHelper {
|
||||
request_rx: mgm_assembly_request_rx,
|
||||
report_tx: mgm_assembly_report_tx,
|
||||
},
|
||||
mgm_assembly::ChildrenQueueHelper {
|
||||
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
|
||||
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
|
||||
},
|
||||
TmtcQueues {
|
||||
tc_rx: mgm_assembly_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
Duration::from_millis(2000),
|
||||
mgm_assembly_event_tx,
|
||||
);
|
||||
|
||||
let mut acs_controller = ctrl::Controller::new(ctrl::ModeLeafHelper {
|
||||
request_rx: acs_ctrl_request_rx,
|
||||
report_tx: acs_ctrl_response_tx,
|
||||
});
|
||||
|
||||
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
|
||||
mgt::MgtCommunication::Sim(mgt::SimInterface {
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgt_sim_reply_rx,
|
||||
})
|
||||
} else {
|
||||
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
|
||||
};
|
||||
let mut mgt_handler = mgt::MgtHandler::new(
|
||||
TmtcQueues {
|
||||
tc_rx: mgt_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgt_com,
|
||||
mgt::ModeLeafHelper {
|
||||
request_rx: mgt_request_rx,
|
||||
report_tx: mgt_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgt_event_tx,
|
||||
);
|
||||
|
||||
let mut acs_subsystem = subsystem::Subsystem::new(
|
||||
subsystem::ModeRequestSenders {
|
||||
mode_request_ctrl: acs_ctrl_request_tx,
|
||||
mode_request_mgm_assy: mgm_assembly_request_tx,
|
||||
mode_request_mgt: mgt_request_tx,
|
||||
},
|
||||
subsystem::ModeReportReceivers {
|
||||
mode_response_ctrl: acs_ctrl_response_rx,
|
||||
mode_response_mgm_assy: mgm_assembly_report_rx,
|
||||
mode_response_mgt: mgt_report_rx,
|
||||
},
|
||||
TmtcQueues {
|
||||
tc_rx: acs_subsystem_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
|
||||
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
|
||||
sim_request_tx.clone(),
|
||||
pcdu_sim_reply_rx,
|
||||
))
|
||||
} else {
|
||||
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
|
||||
};
|
||||
let mut pcdu_handler = PcduHandler::new(
|
||||
pcdu_handler_tc_rx,
|
||||
tm_sink_tx.clone(),
|
||||
switch_request_rx,
|
||||
pcdu_serial_interface,
|
||||
shared_switch_set,
|
||||
DeviceMode::Normal,
|
||||
pcdu_event_tx,
|
||||
);
|
||||
|
||||
// The PCDU is a critical component which should be in normal mode immediately.
|
||||
pcdu_handler_mode_tx
|
||||
.send(types::pcdu::request::Request::Mode(DeviceMode::Normal))
|
||||
.expect("sending initial mode request failed");
|
||||
|
||||
info!("Starting TMTC and UDP task");
|
||||
let jh_udp_tmtc = thread::Builder::new()
|
||||
.name("TMTC & UDP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running UDP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
udp_tmtc_server.periodic_operation();
|
||||
tc_source.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TCP task");
|
||||
let jh_tcp = thread::Builder::new()
|
||||
.name("TCP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running TCP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
tcp_server.periodic_operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TM funnel task");
|
||||
let jh_tm_funnel = thread::Builder::new()
|
||||
.name("TM SINK".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
tm_sink.operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut opt_jh_sim_client = None;
|
||||
if let Some(mut sim_client) = opt_sim_client {
|
||||
info!("Starting UDP sim client task");
|
||||
opt_jh_sim_client = Some(
|
||||
thread::Builder::new()
|
||||
.name("SIM ADAPTER".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if sim_client.operation() == HandlingStatus::Empty {
|
||||
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Starting AOCS thread");
|
||||
let jh_aocs = thread::Builder::new()
|
||||
.name("AOCS".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
mgm_0_handler.periodic_operation();
|
||||
mgm_1_handler.periodic_operation();
|
||||
mgm_assembly.periodic_operation();
|
||||
acs_controller.periodic_operation();
|
||||
mgt_handler.periodic_operation();
|
||||
acs_subsystem.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting EPS thread");
|
||||
let jh_eps = thread::Builder::new()
|
||||
.name("EPS".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
// TODO: We should introduce something like a fixed timeslot helper to allow a more
|
||||
// declarative API. It would also be very useful for the AOCS task.
|
||||
//
|
||||
// TODO: The fixed timeslot handler exists.. use it.
|
||||
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
|
||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting controller thread");
|
||||
let jh_controller_thread = thread::Builder::new()
|
||||
.name("CTRL".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
controller.periodic_operation();
|
||||
event_manager.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
jh_udp_tmtc
|
||||
.join()
|
||||
.expect("Joining UDP TMTC server thread failed");
|
||||
jh_tcp
|
||||
.join()
|
||||
.expect("Joining TCP TMTC server thread failed");
|
||||
jh_tm_funnel
|
||||
.join()
|
||||
.expect("Joining TM Funnel thread failed");
|
||||
if let Some(jh_sim_client) = opt_jh_sim_client {
|
||||
jh_sim_client
|
||||
.join()
|
||||
.expect("Joining SIM client thread failed");
|
||||
}
|
||||
jh_aocs.join().expect("Joining AOCS thread failed");
|
||||
jh_eps.join().expect("Joining EPS thread failed");
|
||||
jh_controller_thread
|
||||
.join()
|
||||
.expect("Joining PUS handler thread failed");
|
||||
}
|
||||
|
||||
pub fn update_time(time_provider: &mut CdsTime, timestamp: &mut [u8]) {
|
||||
time_provider
|
||||
.update_from_now()
|
||||
.expect("Could not get current time");
|
||||
time_provider
|
||||
.write_to_bytes(timestamp)
|
||||
.expect("Writing timestamp failed");
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod sender;
|
||||
pub mod tc_source;
|
||||
pub mod tm_sink;
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
|
||||
|
||||
use satrs::{
|
||||
ComponentId,
|
||||
queue::GenericSendError,
|
||||
tmtc::{PacketAsVec, PacketHandler},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TmTcSender {
|
||||
Normal(mpsc::SyncSender<PacketAsVec>),
|
||||
Mock(MockSender),
|
||||
}
|
||||
|
||||
impl TmTcSender {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_mock_sender(&mut self) -> Option<&mut MockSender> {
|
||||
match self {
|
||||
TmTcSender::Mock(sender) => Some(sender),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketHandler for TmTcSender {
|
||||
type Error = GenericSendError;
|
||||
|
||||
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
TmTcSender::Normal(sync_sender) => {
|
||||
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
|
||||
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
|
||||
}
|
||||
}
|
||||
TmTcSender::Mock(sender) => {
|
||||
sender.handle_packet(sender_id, packet).unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketHandler for MockSender {
|
||||
type Error = GenericSendError;
|
||||
|
||||
fn handle_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> {
|
||||
let mut mut_queue = self.0.borrow_mut();
|
||||
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
use satrs::{
|
||||
ComponentId as RawComponentId, HandlingStatus,
|
||||
spacepackets::{CcsdsPacketReader, ChecksumType},
|
||||
tmtc::PacketAsVec,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::mpsc::{self, TryRecvError},
|
||||
};
|
||||
use types::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned, tmtc};
|
||||
|
||||
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
|
||||
|
||||
// TC source components where the heap is the backing memory of the received telecommands.
|
||||
pub struct TcSourceTask {
|
||||
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
ccsds_distributor: CcsdsDistributor,
|
||||
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
|
||||
}
|
||||
|
||||
impl TcSourceTask {
|
||||
pub fn new(
|
||||
tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
ccsds_distributor: CcsdsDistributor,
|
||||
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_receiver,
|
||||
ccsds_distributor,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_target(
|
||||
&mut self,
|
||||
target_id: ComponentId,
|
||||
sender: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
) {
|
||||
self.ccsds_distributor.insert(target_id, sender);
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
loop {
|
||||
if self.poll_tc() == HandlingStatus::Empty {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_tc(&mut self) -> HandlingStatus {
|
||||
match self.tc_receiver.try_recv() {
|
||||
Ok(packet) => {
|
||||
log::debug!("received raw packet: {:?}", packet);
|
||||
let ccsds_tc_reader_result =
|
||||
CcsdsPacketReader::new(&packet.packet, Some(ChecksumType::WithCrc16));
|
||||
if ccsds_tc_reader_result.is_err() {
|
||||
log::warn!(
|
||||
"received invalid CCSDS TC packet: {:?}",
|
||||
ccsds_tc_reader_result.err()
|
||||
);
|
||||
self.send_event(packet.sender_id, tmtc::Event::InvalidTcPacket);
|
||||
return HandlingStatus::HandledOne;
|
||||
}
|
||||
let ccsds_tc_reader = ccsds_tc_reader_result.unwrap();
|
||||
let tc_header_result =
|
||||
postcard::take_from_bytes::<TcHeader>(ccsds_tc_reader.user_data());
|
||||
if tc_header_result.is_err() {
|
||||
log::warn!(
|
||||
"received CCSDS TC packet with invalid TC header: {:?}",
|
||||
tc_header_result.err()
|
||||
);
|
||||
self.send_event(packet.sender_id, tmtc::Event::InvalidTcHeader);
|
||||
return HandlingStatus::HandledOne;
|
||||
}
|
||||
let (tc_header, payload) = tc_header_result.unwrap();
|
||||
if let Some(sender) = self.ccsds_distributor.get(&tc_header.target_id) {
|
||||
log::debug!("sending TC packet to target ID: {:?}", tc_header.target_id);
|
||||
sender
|
||||
.send(CcsdsTcPacketOwned {
|
||||
sp_header: *ccsds_tc_reader.sp_header(),
|
||||
tc_header,
|
||||
payload: payload.to_vec(),
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
log::warn!("no TC handler for target ID {:?}", tc_header.target_id);
|
||||
self.send_event(
|
||||
packet.sender_id,
|
||||
tmtc::Event::UnknownTargetId(tc_header.target_id),
|
||||
);
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => HandlingStatus::Empty,
|
||||
TryRecvError::Disconnected => {
|
||||
log::warn!("tmtc thread: sender disconnected");
|
||||
HandlingStatus::Empty
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `sender_id` is the raw ID tagged on the received packet, which is not necessarily a
|
||||
/// known [ComponentId] (e.g. a spoofed or garbled packet). Falls back to [ComponentId::Ground]
|
||||
/// as the event sender in that case.
|
||||
fn send_event(&self, sender_id: RawComponentId, event: tmtc::Event) {
|
||||
let sender_id = ComponentId::try_from(sender_id).unwrap_or_else(|_| {
|
||||
log::warn!("TC source event for unknown raw sender ID {}", sender_id);
|
||||
ComponentId::Ground
|
||||
});
|
||||
if let Err(e) = self.event_tx.send((sender_id, event)) {
|
||||
log::warn!("failed to send TC source event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{
|
||||
CcsdsPacketCreatorOwned, ChecksumType, PacketType, SpacePacketHeader,
|
||||
};
|
||||
use types::{Apid, MessageType, ccsds::CcsdsTcPacketOwned};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Testbench {
|
||||
tc_tx: mpsc::SyncSender<PacketAsVec>,
|
||||
target_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
event_rx: mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
tc_source: TcSourceTask,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (tc_tx, tc_source_rx) = mpsc::sync_channel(5);
|
||||
let (target_tx, target_rx) = mpsc::sync_channel(5);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(5);
|
||||
let mut tc_source =
|
||||
TcSourceTask::new(tc_source_rx, CcsdsDistributor::default(), event_tx);
|
||||
tc_source.add_target(ComponentId::EpsPcdu, target_tx);
|
||||
Self {
|
||||
tc_tx,
|
||||
target_rx,
|
||||
event_rx,
|
||||
tc_source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_tc_raw(target_id: ComponentId) -> Vec<u8> {
|
||||
CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, MessageType::Ping),
|
||||
(),
|
||||
)
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
/// A structurally valid CCSDS packet (correct length and CRC), but with arbitrary user data
|
||||
/// instead of a postcard-encoded [TcHeader].
|
||||
fn raw_ccsds_tc_with_user_data(user_data: &[u8]) -> Vec<u8> {
|
||||
CcsdsPacketCreatorOwned::new(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
PacketType::Tc,
|
||||
user_data,
|
||||
Some(ChecksumType::WithCrc16),
|
||||
)
|
||||
.unwrap()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_tc_is_routed_without_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
valid_tc_raw(ComponentId::EpsPcdu),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
tb.target_rx.try_recv().expect("TC was not routed");
|
||||
assert!(matches!(tb.event_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_ccsds_packet_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
vec![1, 2, 3],
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(event, tmtc::Event::InvalidTcPacket));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_tc_header_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
// A single byte is enough to decode the `ComponentId` discriminant, but not
|
||||
// enough for the trailing `MessageType`, so `TcHeader` deserialization fails
|
||||
// while the CCSDS packet itself stays valid.
|
||||
raw_ccsds_tc_with_user_data(&[0]),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(event, tmtc::Event::InvalidTcHeader));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_target_id_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
valid_tc_raw(ComponentId::Ground),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(
|
||||
event,
|
||||
tmtc::Event::UnknownTargetId(ComponentId::Ground)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::mpsc::{self},
|
||||
};
|
||||
|
||||
use arbitrary_int::{u11, u14};
|
||||
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
|
||||
use types::ccsds::CcsdsTmPacketOwned;
|
||||
|
||||
use crate::interface::tcp::SyncTcpTmSource;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CcsdsSeqCounterMap {
|
||||
apid_seq_counter_map: HashMap<u11, SequenceCounterCcsdsSimple>,
|
||||
}
|
||||
impl CcsdsSeqCounterMap {
|
||||
pub fn get_and_increment(&mut self, apid: u11) -> u14 {
|
||||
u14::new(
|
||||
self.apid_seq_counter_map
|
||||
.entry(apid)
|
||||
.or_default()
|
||||
.get_and_increment(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmSink {
|
||||
seq_counter_map: CcsdsSeqCounterMap,
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
}
|
||||
|
||||
impl TmSink {
|
||||
pub fn new(
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
) -> Self {
|
||||
Self {
|
||||
seq_counter_map: Default::default(),
|
||||
sync_tm_tcp_source,
|
||||
tm_funnel_rx,
|
||||
tm_server_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
|
||||
tm.sp_header
|
||||
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
|
||||
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
|
||||
self.tm_server_tx
|
||||
.send(tm)
|
||||
.expect("Sending TM to server failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "minisim-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
tai-time = { version = "1", default-features = false, features = ["serde"] }
|
||||
thiserror = { version = "2", default-features = false }
|
||||
types = { path = "../types" }
|
||||
|
||||
[dev-dependencies]
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
@@ -0,0 +1,472 @@
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tai_time::MonotonicTime;
|
||||
|
||||
use crate::{
|
||||
acs::{mgm, mgt},
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
|
||||
/// Used by clients to route replies to the component handling them.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum ComponentId {
|
||||
SimCtrl,
|
||||
Mgm0Lis3Mdl,
|
||||
Mgm1Lis3Mdl,
|
||||
Mgt,
|
||||
Pcdu,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SimRequest {
|
||||
SimCtrl(SimCtrlRequest),
|
||||
Mgm {
|
||||
id: mgm::Id,
|
||||
request: mgm::Request,
|
||||
},
|
||||
/// Raw frame of the MGT serial protocol.
|
||||
Mgt(Vec<u8>),
|
||||
Pcdu(PcduRequest),
|
||||
}
|
||||
|
||||
impl From<SimCtrlRequest> for SimRequest {
|
||||
fn from(request: SimCtrlRequest) -> Self {
|
||||
Self::SimCtrl(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mgt::Request> for SimRequest {
|
||||
fn from(request: mgt::Request) -> Self {
|
||||
Self::Mgt(request.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PcduRequest> for SimRequest {
|
||||
fn from(request: PcduRequest) -> Self {
|
||||
Self::Pcdu(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SimRequestWithTime {
|
||||
pub request: SimRequest,
|
||||
pub timestamp: MonotonicTime,
|
||||
}
|
||||
|
||||
impl SimRequestWithTime {
|
||||
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
|
||||
Self {
|
||||
request: request.into(),
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
|
||||
Self::new(request, MonotonicTime::EPOCH)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SimReply {
|
||||
SimCtrl(SimCtrlReply),
|
||||
Mgm {
|
||||
id: mgm::Id,
|
||||
reply: mgm::Reply,
|
||||
},
|
||||
/// Raw frame of the MGT serial protocol.
|
||||
Mgt(Vec<u8>),
|
||||
Pcdu(PcduReply),
|
||||
}
|
||||
|
||||
impl SimReply {
|
||||
pub fn component(&self) -> ComponentId {
|
||||
match self {
|
||||
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
|
||||
SimReply::Mgm { id, .. } => id.sim_component(),
|
||||
SimReply::Mgt(_) => ComponentId::Mgt,
|
||||
SimReply::Pcdu(_) => ComponentId::Pcdu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimCtrlReply> for SimReply {
|
||||
fn from(reply: SimCtrlReply) -> Self {
|
||||
Self::SimCtrl(reply)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mgt::Reply> for SimReply {
|
||||
fn from(reply: mgt::Reply) -> Self {
|
||||
Self::Mgt(reply.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PcduReply> for SimReply {
|
||||
fn from(reply: PcduReply) -> Self {
|
||||
Self::Pcdu(reply)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SimCtrlRequest {
|
||||
Ping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SimCtrlReply {
|
||||
Pong,
|
||||
}
|
||||
|
||||
pub mod eps {
|
||||
use super::*;
|
||||
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PcduRequest {
|
||||
SwitchDevice {
|
||||
switch: SwitchId,
|
||||
state: SwitchStateBinary,
|
||||
},
|
||||
RequestSwitchInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PcduReply {
|
||||
SwitchInfo(SwitchMapBinary),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod acs {
|
||||
/// MGM module strongly based on the LIS3MDL device.
|
||||
pub mod mgm {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::pcdu::SwitchStateBinary;
|
||||
|
||||
use crate::ComponentId;
|
||||
|
||||
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
|
||||
///
|
||||
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
|
||||
/// back as all-1s, a shorted/grounded one as all-0s.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SpiFaultMode {
|
||||
#[default]
|
||||
None,
|
||||
AllZeros,
|
||||
AllOnes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SpiFault {
|
||||
pub mode: SpiFaultMode,
|
||||
/// The fault is cleared when the device is switched off, so a power cycle recovers
|
||||
/// from it.
|
||||
pub cleared_by_power_cycle: bool,
|
||||
}
|
||||
|
||||
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
|
||||
// similar which needs to be converted to a signed float value with physical units. We will
|
||||
// simplify this now and generate the signed float values directly. The unit is micro tesla.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SensorValuesMicroTesla {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
|
||||
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
|
||||
x: 30.0,
|
||||
y: -30.0,
|
||||
z: 30.0,
|
||||
};
|
||||
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
|
||||
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
|
||||
|
||||
// Field data register scaling
|
||||
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
|
||||
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RawValues {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub z: i16,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
RequestSensorData,
|
||||
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
|
||||
/// Used to test FDIR handling of SPI bus faults.
|
||||
SetSpiFault(SpiFault),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Reply {
|
||||
pub switch_state: SwitchStateBinary,
|
||||
pub sensor_values: SensorValuesMicroTesla,
|
||||
// Raw sensor values which are transmitted by the LIS3 device in little-endian
|
||||
// order.
|
||||
pub raw: RawValues,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Id {
|
||||
Mgm0,
|
||||
Mgm1,
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub const fn sim_component(&self) -> ComponentId {
|
||||
match self {
|
||||
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
|
||||
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawValues {
|
||||
pub const fn splat(value: i16) -> Self {
|
||||
Self {
|
||||
x: value,
|
||||
y: value,
|
||||
z: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple serial protocol of the magnetorquer.
|
||||
///
|
||||
/// The first byte of each frame is the packet ID. The high bit of the ID is set for replies.
|
||||
/// All fields are big endian. Every command is answered with exactly one reply, but only
|
||||
/// if the device is powered. The device drops invalid frames.
|
||||
///
|
||||
/// A data link layer is deliberately skipped for simplicity. A real serial link would need
|
||||
/// framing and error detection, for example COBS encoding and a CRC. Here, the transport
|
||||
/// always delivers complete and intact frames.
|
||||
pub mod mgt {
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::time::Duration;
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
pub use types::acs::mgt::Dipole;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum RequestId {
|
||||
RequestHk = 0x01,
|
||||
/// Payload: dipole (3 x i16), duration in milliseconds (u32).
|
||||
ApplyTorque = 0x02,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ReplyId {
|
||||
/// Payload: dipole (3 x i16), torquing flag (u8).
|
||||
Hk = 0x81,
|
||||
/// Reply to [RequestId::ApplyTorque].
|
||||
Ack = 0x82,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum FrameError {
|
||||
#[error("empty frame")]
|
||||
Empty,
|
||||
#[error("unknown packet ID {0:#04x}")]
|
||||
UnknownPacketId(u8),
|
||||
#[error("invalid length {len} for packet ID {id:#04x}")]
|
||||
InvalidLength { id: u8, len: usize },
|
||||
}
|
||||
|
||||
fn split_packet_id(frame: &[u8]) -> Result<(u8, &[u8]), FrameError> {
|
||||
let (&id, payload) = frame.split_first().ok_or(FrameError::Empty)?;
|
||||
Ok((id, payload))
|
||||
}
|
||||
|
||||
fn payload_array<const N: usize>(id: u8, payload: &[u8]) -> Result<&[u8; N], FrameError> {
|
||||
payload.try_into().map_err(|_| FrameError::InvalidLength {
|
||||
id,
|
||||
len: payload.len() + 1,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Request {
|
||||
/// The duration has millisecond resolution on the wire.
|
||||
ApplyTorque {
|
||||
duration: Duration,
|
||||
dipole: Dipole,
|
||||
},
|
||||
RequestHk,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn to_frame(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Request::RequestHk => vec![RequestId::RequestHk.into()],
|
||||
Request::ApplyTorque { duration, dipole } => {
|
||||
let mut frame = vec![RequestId::ApplyTorque.into()];
|
||||
frame.extend_from_slice(&dipole.to_be_bytes());
|
||||
let duration_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX);
|
||||
frame.extend_from_slice(&duration_ms.to_be_bytes());
|
||||
frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
|
||||
let (id, payload) = split_packet_id(frame)?;
|
||||
match RequestId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
|
||||
RequestId::RequestHk => {
|
||||
payload_array::<0>(id, payload)?;
|
||||
Ok(Request::RequestHk)
|
||||
}
|
||||
RequestId::ApplyTorque => {
|
||||
let payload: &[u8; Dipole::LEN + 4] = payload_array(id, payload)?;
|
||||
let [dipole @ .., d0, d1, d2, d3] = *payload;
|
||||
Ok(Request::ApplyTorque {
|
||||
duration: Duration::from_millis(
|
||||
u32::from_be_bytes([d0, d1, d2, d3]).into(),
|
||||
),
|
||||
dipole: Dipole::from_be_bytes(&dipole),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HkSet {
|
||||
pub dipole: Dipole,
|
||||
pub torquing: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Reply {
|
||||
Hk(HkSet),
|
||||
Ack,
|
||||
}
|
||||
|
||||
impl Reply {
|
||||
pub fn to_frame(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Reply::Hk(hk) => {
|
||||
let mut frame = vec![ReplyId::Hk.into()];
|
||||
frame.extend_from_slice(&hk.dipole.to_be_bytes());
|
||||
frame.push(hk.torquing as u8);
|
||||
frame
|
||||
}
|
||||
Reply::Ack => vec![ReplyId::Ack.into()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
|
||||
let (id, payload) = split_packet_id(frame)?;
|
||||
match ReplyId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
|
||||
ReplyId::Hk => {
|
||||
let payload: &[u8; Dipole::LEN + 1] = payload_array(id, payload)?;
|
||||
let [dipole @ .., torquing] = *payload;
|
||||
Ok(Reply::Hk(HkSet {
|
||||
dipole: Dipole::from_be_bytes(&dipole),
|
||||
torquing: torquing != 0,
|
||||
}))
|
||||
}
|
||||
ReplyId::Ack => {
|
||||
payload_array::<0>(id, payload)?;
|
||||
Ok(Reply::Ack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_apply_torque_frame() {
|
||||
let request = Request::ApplyTorque {
|
||||
duration: Duration::from_millis(0x0102_0304),
|
||||
dipole: Dipole {
|
||||
x: -2,
|
||||
y: 0x0506,
|
||||
z: 0x0708,
|
||||
},
|
||||
};
|
||||
let frame = request.to_frame();
|
||||
assert_eq!(
|
||||
frame,
|
||||
[
|
||||
0x02, 0xff, 0xfe, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04
|
||||
]
|
||||
);
|
||||
assert_eq!(Request::from_frame(&frame), Ok(request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_hk_frame() {
|
||||
assert_eq!(Request::RequestHk.to_frame(), [0x01]);
|
||||
assert_eq!(Request::from_frame(&[0x01]), Ok(Request::RequestHk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_frames() {
|
||||
let hk = Reply::Hk(HkSet {
|
||||
dipole: Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
});
|
||||
let frame = hk.to_frame();
|
||||
assert_eq!(frame, [0x81, 0, 1, 0, 2, 0, 3, 1]);
|
||||
assert_eq!(Reply::from_frame(&frame), Ok(hk));
|
||||
assert_eq!(Reply::Ack.to_frame(), [0x82]);
|
||||
assert_eq!(Reply::from_frame(&[0x82]), Ok(Reply::Ack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_frames() {
|
||||
assert_eq!(Request::from_frame(&[]), Err(FrameError::Empty));
|
||||
assert_eq!(
|
||||
Request::from_frame(&[0x81]),
|
||||
Err(FrameError::UnknownPacketId(0x81))
|
||||
);
|
||||
assert_eq!(
|
||||
Request::from_frame(&[0x01, 0x00]),
|
||||
Err(FrameError::InvalidLength { id: 0x01, len: 2 })
|
||||
);
|
||||
assert_eq!(
|
||||
Reply::from_frame(&[0x81, 0, 1]),
|
||||
Err(FrameError::InvalidLength { id: 0x81, len: 3 })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod udp {
|
||||
pub const SIM_CTRL_PORT: u16 = 7303;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_request_serde_roundtrip() {
|
||||
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
let bytes = postcard::to_allocvec(&sim_request).unwrap();
|
||||
let deserialized: SimRequestWithTime = postcard::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(deserialized, sim_request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_serde_roundtrip() {
|
||||
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
|
||||
let bytes = postcard::to_allocvec(&sim_reply).unwrap();
|
||||
let deserialized: SimReply = postcard::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(deserialized, sim_reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "minisim"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
log = "0.4"
|
||||
thiserror = "2"
|
||||
fern = "0.7"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
num_enum = "0.7"
|
||||
humantime = "2"
|
||||
nexosim = "1"
|
||||
|
||||
satrs = { path = "../../satrs" }
|
||||
types = { path = "../types" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
delegate = "0.13"
|
||||
@@ -0,0 +1,32 @@
|
||||
sat-rs minisim
|
||||
======
|
||||
|
||||
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
|
||||
[nexosim](https://github.com/asynchronics/nexosim).
|
||||
|
||||
Right now, this crate is primarily used together with the
|
||||
[`example-std` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std)
|
||||
to simulate the devices connected to the example application.
|
||||
|
||||
You can simply run this application using
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
cargo run -p minisim
|
||||
```
|
||||
|
||||
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
|
||||
simulation replies with any other application.
|
||||
|
||||
The simulator was designed in a modular way to be scalable and adaptable to other communication
|
||||
schemes. This might allow it to serve a mini-simulator for other example applications which
|
||||
still have similar device handlers.
|
||||
|
||||
The following graph shows the high-level architecture of the mini-simulator.
|
||||
|
||||
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
||||
Loaded 100 of 300 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user