Compare commits
379
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||
|
|
f6f7519625 | ||
|
|
0f0fbc1a18
|
||
|
|
6e55e2ac95 | ||
|
|
2f96bfe992
|
||
|
|
52aafb3aab
|
||
|
|
6ce9cb5ead | ||
|
|
273f79d1e6
|
||
|
|
622221835e | ||
|
|
e396ad2e7a
|
||
|
|
772927d50b
|
||
|
|
be9a45e55f | ||
|
|
eee8a69550
|
||
|
|
f7a6d3ce47 | ||
|
|
df97a3a93e
|
||
|
|
42750e08c0
|
||
|
|
786671bbd7 | ||
|
|
63f37f0917 | ||
|
|
8cfe3b81e7 | ||
|
|
de50bec562
|
||
|
|
39ab9fa27b | ||
|
|
1dbc81a8f5
|
||
|
|
1ad74ee1d5 | ||
|
|
f96fe6bdc0
|
||
|
|
d43a8eb571 | ||
|
|
0bbada90ef | ||
|
|
3375780e00 | ||
|
|
de028ed827
|
||
|
|
d27ac5dfc9
|
||
|
|
c67b7cb93a
|
||
|
|
f71ba3e8d8 | ||
|
|
975cd927f4 | ||
|
|
3cc9dd3c48 | ||
|
|
0fec994028 | ||
|
|
226a134aff
|
||
|
|
aac59ec7c1 | ||
|
|
ce7eb8650f | ||
|
|
df2733a176
|
||
|
|
344fe6a4c0 | ||
|
|
a5941751d7
|
||
|
|
977e29894b | ||
|
|
dd1417a368
|
||
|
|
3195cf5111
|
||
|
|
8280c70682 | ||
|
|
19c5aa9b83
|
||
|
|
713b4e097b
|
||
|
|
9039c1b59a | ||
|
|
746b31ec5d | ||
|
|
2318cd4293
|
||
|
|
a6b57d3eb9 | ||
|
|
bddd3132d4 | ||
|
|
6a6ffba754 | ||
|
|
d27a41e4de | ||
|
|
972bf19188
|
||
|
|
9d711d2b73
|
||
|
|
d0005cdd63
|
||
|
|
f00e6cf50c
|
||
|
|
128df9a813 | ||
|
|
7387be3bc3
|
||
|
|
d3fb504545
|
||
|
|
ae8e39f626
|
||
|
|
ab3d907d4e | ||
|
|
3de5954898
|
||
|
|
5600aa576c | ||
|
|
88793cfa87
|
||
|
|
223b637eb8
|
||
|
|
cf9b115e1e | ||
|
|
eea9b11b39
|
||
|
|
f21ab0017e | ||
|
|
a7ca00317f
|
||
|
|
75fda42f4f
|
||
|
|
faf0f6f6c6 | ||
|
|
a690c7720d | ||
|
|
b48b5b8caa | ||
|
|
238c3a8d43 | ||
|
|
de8c0bc13e | ||
|
|
012eb82f42
|
||
|
|
d26f6cbe27 | ||
|
|
82d3215761
|
||
|
|
2b80244636
|
||
|
|
f1611cd5b8 | ||
|
|
808126ee41 | ||
|
|
05df24447b | ||
|
|
b229360233 | ||
|
|
52be26829b | ||
|
|
ca2c8aa359 | ||
|
|
ba03150178 | ||
|
|
4e45bfa7e6 | ||
|
|
93c01c8c22
|
||
|
|
2d062f3010 | ||
|
|
01d9a85976 | ||
|
|
fa7cd39f3e | ||
|
|
5bb37d8e87 |
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
|
||||||
+6
-1
@@ -1,5 +1,10 @@
|
|||||||
/target
|
target/
|
||||||
|
|
||||||
|
output.log
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
output.log
|
||||||
|
|
||||||
|
output.log
|
||||||
|
|
||||||
/.idea/*
|
/.idea/*
|
||||||
!/.idea/runConfigurations
|
!/.idea/runConfigurations
|
||||||
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>
|
|
||||||
+8
-3
@@ -3,11 +3,16 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"satrs",
|
"satrs",
|
||||||
"satrs-mib",
|
"satrs-mib",
|
||||||
"satrs-example",
|
"examples/example-std",
|
||||||
|
"examples/types",
|
||||||
|
"examples/client",
|
||||||
|
"examples/minisim",
|
||||||
|
"examples/minisim-types",
|
||||||
"satrs-shared",
|
"satrs-shared",
|
||||||
|
"tmtc-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"satrs-example-stm32f3-disco",
|
"examples/stm32h7-nucleo-rtic",
|
||||||
|
"examples/stm32h7-nucleo-embassy",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
<p align="center"> <img src="misc/satrs-logo.png" width="40%"> </p>
|
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p>
|
||||||
|
|
||||||
|
[](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
|
sat-rs
|
||||||
=========
|
=========
|
||||||
|
|
||||||
This is the repository of the sat-rs framework. Its primary goal is to provide re-usable components
|
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
|
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
|
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/)
|
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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
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
|
||||||
|
changelog files. You should only use this library if your are willing to work in this
|
||||||
|
environment.
|
||||||
|
|
||||||
A lot of the architecture and general design considerations are based on the
|
A lot of the architecture and general design considerations are based on the
|
||||||
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
|
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
|
||||||
@@ -16,24 +26,40 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
|
|||||||
|
|
||||||
# Overview
|
# 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):
|
* [`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
|
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.
|
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):
|
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
|
||||||
Primary crate.
|
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):
|
* [`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.
|
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 on-board software using 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`.
|
||||||
|
* [`stm32h7-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-rtic):
|
||||||
|
Simple example using sat-rs components on a bare-metal system with constrained resources.
|
||||||
|
This example uses the [RTIC](https://github.com/rtic-rs/rtic) framework on the NUCLEO-H753ZI
|
||||||
|
board.
|
||||||
|
* [`stm32h7-nucleo-embassy`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-embassy):
|
||||||
|
Same as `stm32h7-nucleo-rtic`, but using the [embassy](https://embassy.dev/) executor instead
|
||||||
|
of RTIC.
|
||||||
|
|
||||||
|
The library crates and the `example-std` application have their own `CHANGELOG.md`.
|
||||||
|
|
||||||
# Related projects
|
# Related projects
|
||||||
|
|
||||||
@@ -43,6 +69,23 @@ Each project has its own `CHANGELOG.md`.
|
|||||||
packet protocol implementations. This repository is re-exported in the
|
packet protocol implementations. This repository is re-exported in the
|
||||||
[`satrs`](https://egit.irs.uni-stuttgart.de/rust/satrs/src/branch/main/satrs)
|
[`satrs`](https://egit.irs.uni-stuttgart.de/rust/satrs/src/branch/main/satrs)
|
||||||
crate.
|
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
|
# Coverage
|
||||||
|
|
||||||
@@ -54,5 +97,5 @@ rustup component add llvm-tools-preview
|
|||||||
cargo install grcov --locked
|
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.
|
optionally supply the `--open` flag to open the coverage report in your webbrowser.
|
||||||
Vendored
+1
@@ -33,6 +33,7 @@ pipeline {
|
|||||||
stage('Test') {
|
stage('Test') {
|
||||||
steps {
|
steps {
|
||||||
sh 'cargo nextest r --all-features'
|
sh 'cargo nextest r --all-features'
|
||||||
|
sh 'cargo test --doc --all-features'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Check with all features') {
|
stage('Check with all features') {
|
||||||
|
|||||||
+8
-4
@@ -18,15 +18,19 @@ def generate_cov_report(open_report: bool, format: str, package: str):
|
|||||||
out_path = "./target/debug/coverage"
|
out_path = "./target/debug/coverage"
|
||||||
if format == "lcov":
|
if format == "lcov":
|
||||||
out_path = "./target/debug/lcov.info"
|
out_path = "./target/debug/lcov.info"
|
||||||
os.system(
|
grcov_cmd = (
|
||||||
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
|
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
|
||||||
f"-o {out_path}"
|
f"-o {out_path}"
|
||||||
)
|
)
|
||||||
|
print(f"Running: {grcov_cmd}")
|
||||||
|
os.system(grcov_cmd)
|
||||||
if format == "lcov":
|
if format == "lcov":
|
||||||
os.system(
|
lcov_cmd = (
|
||||||
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
|
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
|
||||||
"--legend ./target/debug/lcov.info"
|
"--legend ./target/debug/lcov.info"
|
||||||
)
|
)
|
||||||
|
print(f"Running: {lcov_cmd}")
|
||||||
|
os.system(lcov_cmd)
|
||||||
if open_report:
|
if open_report:
|
||||||
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
|
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
|
||||||
webbrowser.open_new_tab(coverage_report_path)
|
webbrowser.open_new_tab(coverage_report_path)
|
||||||
@@ -43,8 +47,8 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
"--package",
|
"--package",
|
||||||
choices=["satrs-core"],
|
choices=["satrs", "minisim", "example-std"],
|
||||||
default="satrs-core",
|
default="satrs",
|
||||||
help="Choose project to generate coverage for",
|
help="Choose project to generate coverage for",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@@ -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,909 @@
|
|||||||
|
use anyhow::{Context as _, bail};
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use clap::Parser as _;
|
||||||
|
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||||
|
use minisim_types::{
|
||||||
|
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
|
||||||
|
udp::SIM_CTRL_PORT,
|
||||||
|
};
|
||||||
|
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
test_event: bool,
|
||||||
|
/// Address of the commanded application. Overrides the address inside `config.toml`.
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
udp_addr: Option<SocketAddr>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
commands: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
interface: InterfaceConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
|
struct InterfaceConfig {
|
||||||
|
/// Defaults to the example-std OBSW on the local host.
|
||||||
|
udp_addr: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// The build script creates `config.toml` from the template. A missing file is still accepted,
|
||||||
|
/// because all parameters have defaults.
|
||||||
|
fn load() -> anyhow::Result<Self> {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(content) => toml::from_str(&content)
|
||||||
|
.with_context(|| format!("parsing {} failed", path.display())),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
||||||
|
Err(e) => Err(e).with_context(|| format!("reading {} failed", path.display())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Mgm0(MgmArgs),
|
||||||
|
Mgm1(MgmArgs),
|
||||||
|
MgmAssy(MgmAssemblyArgs),
|
||||||
|
Mgt(MgtArgs),
|
||||||
|
AcsSubsystem(SubsystemArgs),
|
||||||
|
EventManager(EventManagerArgs),
|
||||||
|
/// Blinking LEDs of the embedded examples.
|
||||||
|
Led(LedArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
struct EventManagerArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: EventFilterAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
enum EventFilterAction {
|
||||||
|
/// Enable event TM generation.
|
||||||
|
Enable(EventFilterArgs),
|
||||||
|
/// Disable event TM generation.
|
||||||
|
Disable(EventFilterArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Args)]
|
||||||
|
struct EventFilterArgs {
|
||||||
|
#[arg(value_enum)]
|
||||||
|
component: EventSenderSelect,
|
||||||
|
/// Raw event ID. Without it, the filter applies to all events of the component.
|
||||||
|
#[arg(short, long)]
|
||||||
|
event_id: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Components which emit events.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum EventSenderSelect {
|
||||||
|
Controller,
|
||||||
|
Mgm0,
|
||||||
|
Mgm1,
|
||||||
|
MgmAssy,
|
||||||
|
Mgt,
|
||||||
|
Pcdu,
|
||||||
|
UdpServer,
|
||||||
|
TcpServer,
|
||||||
|
Ground,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventSenderSelect> for types::ComponentId {
|
||||||
|
fn from(sender: EventSenderSelect) -> Self {
|
||||||
|
match sender {
|
||||||
|
EventSenderSelect::Controller => types::ComponentId::Controller,
|
||||||
|
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
|
||||||
|
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
|
||||||
|
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
|
||||||
|
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
|
||||||
|
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
|
||||||
|
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
|
||||||
|
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
|
||||||
|
EventSenderSelect::Ground => types::ComponentId::Ground,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum FaultMode {
|
||||||
|
None,
|
||||||
|
/// SPI communication is all zeroes, modelling an unconnected sensor.
|
||||||
|
AllZeros,
|
||||||
|
/// SPI communication is all ones, modelling a broken sensor.
|
||||||
|
AllOnes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FaultMode> for mgm::SpiFaultMode {
|
||||||
|
fn from(mode: FaultMode) -> Self {
|
||||||
|
match mode {
|
||||||
|
FaultMode::None => mgm::SpiFaultMode::None,
|
||||||
|
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
|
||||||
|
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum FaultKind {
|
||||||
|
/// Cleared when the device is switched off, so a power cycle recovers from it.
|
||||||
|
Transient,
|
||||||
|
/// Survives power cycles.
|
||||||
|
#[default]
|
||||||
|
Permanent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum HkSelect {
|
||||||
|
OneShot,
|
||||||
|
EnablePeriodic,
|
||||||
|
DisablePeriodic,
|
||||||
|
ModifyInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum HealthStateSelect {
|
||||||
|
Healthy,
|
||||||
|
Faulty,
|
||||||
|
PermanentFaulty,
|
||||||
|
ExternalControl,
|
||||||
|
NeedsRecovery,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HealthStateSelect> for satrs::health::HealthState {
|
||||||
|
fn from(state: HealthStateSelect) -> Self {
|
||||||
|
match state {
|
||||||
|
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
|
||||||
|
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
|
||||||
|
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
|
||||||
|
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
|
||||||
|
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgmArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
/// Housekeeping request for the sensor data set.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
hk: Option<HkSelect>,
|
||||||
|
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||||
|
#[arg(long)]
|
||||||
|
hk_interval_ms: Option<u64>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<DeviceModeSelect>,
|
||||||
|
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
fault: Option<FaultMode>,
|
||||||
|
/// Whether a power cycle clears the injected SPI fault.
|
||||||
|
#[arg(long, value_enum, default_value_t)]
|
||||||
|
fault_kind: FaultKind,
|
||||||
|
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
|
||||||
|
/// the handler after the underlying issue has been fixed or worked around.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
health: Option<HealthStateSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgtArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
/// Housekeeping request for the status data set.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
hk: Option<HkSelect>,
|
||||||
|
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||||
|
#[arg(long)]
|
||||||
|
hk_interval_ms: Option<u64>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<DeviceModeSelect>,
|
||||||
|
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
|
||||||
|
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
|
||||||
|
torque: Option<types::acs::mgt::Dipole>,
|
||||||
|
#[arg(long, default_value_t = 1000)]
|
||||||
|
torque_duration_ms: u64,
|
||||||
|
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
health: Option<HealthStateSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct LedArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
/// Mode of the red and the orange LED.
|
||||||
|
#[arg(short, long, value_enum)]
|
||||||
|
mode: Option<LedModeSelect>,
|
||||||
|
/// Toggle period of the toggle modes.
|
||||||
|
#[arg(long, default_value_t = 500)]
|
||||||
|
toggle_period_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
enum LedModeSelect {
|
||||||
|
AllOff,
|
||||||
|
RedOn,
|
||||||
|
OrangeOn,
|
||||||
|
AlternatingToggle,
|
||||||
|
UnifiedToggle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgmAssemblyArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<AssemblyModeSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct SubsystemArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<SubsystemModeSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum DeviceModeSelect {
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum AssemblyModeSelect {
|
||||||
|
NoModeKeeping,
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum SubsystemModeSelect {
|
||||||
|
Off,
|
||||||
|
Safe,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hk_request_type(
|
||||||
|
hk: HkSelect,
|
||||||
|
hk_interval_ms: Option<u64>,
|
||||||
|
) -> anyhow::Result<types::HkRequestType> {
|
||||||
|
let opt_interval = hk_interval_ms.map(Duration::from_millis);
|
||||||
|
Ok(match hk {
|
||||||
|
HkSelect::OneShot => types::HkRequestType::OneShot,
|
||||||
|
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
|
||||||
|
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
|
||||||
|
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
|
||||||
|
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
|
||||||
|
let axes: Vec<i16> = value
|
||||||
|
.split(',')
|
||||||
|
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
let [x, y, z] = axes[..] else {
|
||||||
|
return Err(format!("expected 3 values, got {}", axes.len()));
|
||||||
|
};
|
||||||
|
Ok(types::acs::mgt::Dipole { x, y, z })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mgt_request(
|
||||||
|
client: &UdpSocket,
|
||||||
|
addr: SocketAddr,
|
||||||
|
request: types::acs::mgt::request::Request,
|
||||||
|
) {
|
||||||
|
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending MGT request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::request::Request) {
|
||||||
|
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(types::ComponentId::Led, request.message_type()),
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending LED request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) {
|
||||||
|
use types::led::request::Request;
|
||||||
|
|
||||||
|
if args.ping {
|
||||||
|
send_led_request(client, addr, Request::Ping);
|
||||||
|
}
|
||||||
|
if let Some(mode) = args.mode {
|
||||||
|
let toggle_period = Duration::from_millis(args.toggle_period_ms);
|
||||||
|
let mode = match mode {
|
||||||
|
LedModeSelect::AllOff => types::led::Mode::AllOff,
|
||||||
|
LedModeSelect::RedOn => types::led::Mode::RedOn,
|
||||||
|
LedModeSelect::OrangeOn => types::led::Mode::OrangeOn,
|
||||||
|
LedModeSelect::AlternatingToggle => types::led::Mode::AlternatingToggle(toggle_period),
|
||||||
|
LedModeSelect::UnifiedToggle => types::led::Mode::UnifiedToggle(toggle_period),
|
||||||
|
};
|
||||||
|
send_led_request(client, addr, Request::SetMode(mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
|
||||||
|
use types::acs::mgt::request::{ModeRequest, Request};
|
||||||
|
|
||||||
|
if args.ping {
|
||||||
|
send_mgt_request(client, addr, Request::Ping);
|
||||||
|
}
|
||||||
|
if let Some(hk) = args.hk {
|
||||||
|
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||||
|
send_mgt_request(client, addr, Request::Hk(req_type));
|
||||||
|
}
|
||||||
|
if let Some(mode) = args.mode {
|
||||||
|
let mode = match mode {
|
||||||
|
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||||
|
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||||
|
};
|
||||||
|
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
|
||||||
|
}
|
||||||
|
if let Some(dipole) = args.torque {
|
||||||
|
let request = Request::ApplyTorque {
|
||||||
|
dipole,
|
||||||
|
duration: Duration::from_millis(args.torque_duration_ms),
|
||||||
|
};
|
||||||
|
send_mgt_request(client, addr, request);
|
||||||
|
}
|
||||||
|
if let Some(health) = args.health {
|
||||||
|
send_mgt_request(
|
||||||
|
client,
|
||||||
|
addr,
|
||||||
|
Request::Health(types::HealthRequest::SetHealth(health.into())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgm_command(
|
||||||
|
client: &UdpSocket,
|
||||||
|
addr: SocketAddr,
|
||||||
|
target_id: types::ComponentId,
|
||||||
|
args: MgmArgs,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(mode) = args.fault {
|
||||||
|
inject_mgm_failure(
|
||||||
|
target_id,
|
||||||
|
mgm::SpiFault {
|
||||||
|
mode: mode.into(),
|
||||||
|
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if args.ping {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Ping),
|
||||||
|
types::acs::mgm::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} ping request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(hk) = args.hk {
|
||||||
|
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Hk),
|
||||||
|
types::acs::mgm::request::Request::Hk(HkRequest {
|
||||||
|
id: types::acs::mgm::request::HkId::Sensor,
|
||||||
|
req_type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(mode) = args.mode {
|
||||||
|
let dev_mode = match mode {
|
||||||
|
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||||
|
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Mode),
|
||||||
|
types::acs::mgm::request::Request::Mode(
|
||||||
|
types::acs::mgm::request::ModeRequest::SetMode(dev_mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(health) = args.health {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Health),
|
||||||
|
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
|
||||||
|
health.into(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} set-health request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
|
||||||
|
use types::event_manager::request::Request;
|
||||||
|
|
||||||
|
let request = match args.action {
|
||||||
|
EventFilterAction::Enable(filter) => match filter.event_id {
|
||||||
|
Some(event_id) => Request::EnableEvent {
|
||||||
|
sender_id: filter.component.into(),
|
||||||
|
event_id,
|
||||||
|
},
|
||||||
|
None => Request::EnableComponent(filter.component.into()),
|
||||||
|
},
|
||||||
|
EventFilterAction::Disable(filter) => match filter.event_id {
|
||||||
|
Some(event_id) => Request::DisableEvent {
|
||||||
|
sender_id: filter.component.into(),
|
||||||
|
event_id,
|
||||||
|
},
|
||||||
|
None => Request::DisableComponent(filter.component.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending event manager request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
client.send_to(&request_packet.to_vec(), addr).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{} {} {}] {}",
|
||||||
|
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(level)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
setup_logger(log::LevelFilter::Debug).unwrap();
|
||||||
|
let kill_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let ctrl_kill_signal = kill_signal.clone();
|
||||||
|
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = Config::load()?;
|
||||||
|
|
||||||
|
let addr = cli
|
||||||
|
.udp_addr
|
||||||
|
.or(config.interface.udp_addr)
|
||||||
|
.unwrap_or(SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT));
|
||||||
|
// Bind to all interfaces, so embedded targets inside the local network can be reached too.
|
||||||
|
let client = UdpSocket::bind("0.0.0.0:7302").expect("Connecting to UDP server failed");
|
||||||
|
client.set_nonblocking(true)?;
|
||||||
|
client.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||||
|
|
||||||
|
if cli.ping {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
|
||||||
|
types::control::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if cli.test_event {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
|
||||||
|
types::control::request::Request::TestEvent,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending event request with TC ID {:#010x}",
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(cmd) = cli.commands {
|
||||||
|
match cmd {
|
||||||
|
Commands::Mgm0(args) => {
|
||||||
|
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
|
||||||
|
}
|
||||||
|
Commands::Mgm1(args) => {
|
||||||
|
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
|
||||||
|
}
|
||||||
|
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
|
||||||
|
Commands::MgmAssy(mgm_assembly_args) => {
|
||||||
|
let target_id = types::ComponentId::AcsMgmAssembly;
|
||||||
|
if mgm_assembly_args.ping {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Ping),
|
||||||
|
types::acs::mgm::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} ping request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(mode) = mgm_assembly_args.mode {
|
||||||
|
let assembly_mode = match mode {
|
||||||
|
AssemblyModeSelect::NoModeKeeping => {
|
||||||
|
types::acs::mgm_assembly::Mode::NoModeKeeping
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Off => {
|
||||||
|
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Normal => {
|
||||||
|
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Mode),
|
||||||
|
types::acs::mgm_assembly::request::Request::Mode(
|
||||||
|
types::acs::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::AcsSubsystem(subsystem_args) => {
|
||||||
|
let target_id = types::ComponentId::AcsSubsystem;
|
||||||
|
if subsystem_args.ping {
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Ping),
|
||||||
|
types::acs::subsystem::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} ping request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(mode) = subsystem_args.mode {
|
||||||
|
let subsystem_mode = match mode {
|
||||||
|
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
|
||||||
|
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, types::MessageType::Mode),
|
||||||
|
types::acs::subsystem::request::Request::Mode(
|
||||||
|
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} mode request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
|
||||||
|
Commands::Led(args) => handle_led_command(&client, addr, args),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
|
||||||
|
log::info!("entering listening loop");
|
||||||
|
loop {
|
||||||
|
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
log::info!("received kill signal, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match client.recv(recv_buf.as_mut_slice()) {
|
||||||
|
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::TimedOut
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log::warn!("UDP reception error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
|
||||||
|
///
|
||||||
|
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
|
||||||
|
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
|
||||||
|
/// UDP send would otherwise silently do nothing if minisim is not running.
|
||||||
|
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
|
||||||
|
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
|
||||||
|
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||||
|
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||||
|
|
||||||
|
let mut reply_buf = [0u8; 4096];
|
||||||
|
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||||
|
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
|
||||||
|
match sim_socket.recv(&mut reply_buf) {
|
||||||
|
Ok(len) => {
|
||||||
|
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
|
||||||
|
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
|
||||||
|
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
if matches!(
|
||||||
|
e.kind(),
|
||||||
|
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = match target_id {
|
||||||
|
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
|
||||||
|
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
|
||||||
|
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
|
||||||
|
};
|
||||||
|
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
|
||||||
|
id,
|
||||||
|
request: mgm::Request::SetSpiFault(fault),
|
||||||
|
});
|
||||||
|
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
|
||||||
|
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each component has its own event type, so the sender ID determines how to decode the event.
|
||||||
|
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
|
||||||
|
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
|
||||||
|
sender_id: types::ComponentId,
|
||||||
|
data: &[u8],
|
||||||
|
) {
|
||||||
|
match postcard::from_bytes::<E>(data) {
|
||||||
|
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
|
||||||
|
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match sender_id {
|
||||||
|
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
|
||||||
|
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
|
||||||
|
log_event::<types::acs::mgm::Event>(sender_id, data)
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsMgmAssembly => {
|
||||||
|
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
|
||||||
|
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
|
||||||
|
// TC source events are sent with the ID of the packet source.
|
||||||
|
types::ComponentId::UdpServer
|
||||||
|
| types::ComponentId::TcpServer
|
||||||
|
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
|
||||||
|
_ => log::warn!(
|
||||||
|
"Received event from {:?} with unknown event type",
|
||||||
|
sender_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
|
||||||
|
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
|
||||||
|
Ok(packet) => {
|
||||||
|
let tm_header_result = postcard::take_from_bytes::<types::TmHeader>(packet.user_data());
|
||||||
|
if let Err(e) = tm_header_result {
|
||||||
|
bail!("Failed to deserialize TM header: {}", e);
|
||||||
|
}
|
||||||
|
let (tm_header, remainder) = tm_header_result.unwrap();
|
||||||
|
if let Some(tc_id) = tm_header.tc_id {
|
||||||
|
log::info!(
|
||||||
|
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
|
||||||
|
packet.apid(),
|
||||||
|
tm_header.sender_id,
|
||||||
|
tc_id.raw()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tm_header.message_type == MessageType::Event {
|
||||||
|
handle_event(tm_header.sender_id, remainder);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match tm_header.sender_id {
|
||||||
|
types::ComponentId::EpsPcdu => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
|
||||||
|
log::info!("Received response from PCDU: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
types::ComponentId::Controller => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::control::response::Response>(remainder);
|
||||||
|
log::info!("Received response from controller: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsMgmAssembly => {
|
||||||
|
let response = postcard::from_bytes::<
|
||||||
|
types::acs::mgm_assembly::response::Response,
|
||||||
|
>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received response from MGM Assembly: {:?}",
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsMgm0 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM0: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsMgm1 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM1: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsSubsystem => {
|
||||||
|
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
|
||||||
|
remainder,
|
||||||
|
);
|
||||||
|
log::info!(
|
||||||
|
"Received response from ACS subsystem: {:?}",
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
types::ComponentId::EpsSubsystem => todo!(),
|
||||||
|
types::ComponentId::UdpServer => todo!(),
|
||||||
|
types::ComponentId::TcpServer => todo!(),
|
||||||
|
types::ComponentId::Ground => todo!(),
|
||||||
|
types::ComponentId::EventManager => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received response from event manager: {:?}",
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
types::ComponentId::AcsController => todo!(),
|
||||||
|
types::ComponentId::AcsMgt => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGT: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
types::ComponentId::Led => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<types::led::response::Response>(remainder);
|
||||||
|
log::info!("Received response from LED: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_dipole() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_dipole("-200, 200,1000"),
|
||||||
|
Ok(types::acs::mgt::Dipole {
|
||||||
|
x: -200,
|
||||||
|
y: 200,
|
||||||
|
z: 1000
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert!(parse_dipole("1,2").is_err());
|
||||||
|
assert!(parse_dipole("1,2,3,4").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_template_is_valid() {
|
||||||
|
let template = include_str!("../config.toml.template");
|
||||||
|
let config: Config = toml::from_str(template).unwrap();
|
||||||
|
assert_eq!(config.interface.udp_addr, None);
|
||||||
|
let config: Config =
|
||||||
|
toml::from_str("[interface]\nudp_addr = \"192.168.1.50:7301\"").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
config.interface.udp_addr,
|
||||||
|
Some("192.168.1.50:7301".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_negative_torque_argument() {
|
||||||
|
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
|
||||||
|
let Some(Commands::Mgt(args)) = cli.commands else {
|
||||||
|
panic!("expected mgt subcommand");
|
||||||
|
};
|
||||||
|
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
|
||||||
|
}
|
||||||
|
}
|
||||||
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/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
# [unreleased]
|
# [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"
|
||||||
File renamed without changes.
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,15 +1,16 @@
|
|||||||
use satrs::res_code::ResultU16;
|
use arbitrary_int::u11;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use satrs::{
|
||||||
|
legacy::res_code::ResultU16,
|
||||||
|
spacepackets::{PacketId, PacketType},
|
||||||
|
};
|
||||||
use satrs_mib::res_code::ResultU16Info;
|
use satrs_mib::res_code::ResultU16Info;
|
||||||
use satrs_mib::resultcode;
|
use satrs_mib::resultcode;
|
||||||
use std::net::Ipv4Addr;
|
use std::{collections::HashSet, net::Ipv4Addr};
|
||||||
|
use strum::IntoEnumIterator as _;
|
||||||
|
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use satrs::{
|
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
|
||||||
events::{EventU32TypedSev, SeverityInfo},
|
|
||||||
pool::{StaticMemoryPool, StaticPoolConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PUS_APID: u16 = 0x02;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -29,13 +30,28 @@ pub const AOCS_APID: u16 = 1;
|
|||||||
pub enum GroupId {
|
pub enum GroupId {
|
||||||
Tmtc = 0,
|
Tmtc = 0,
|
||||||
Hk = 1,
|
Hk = 1,
|
||||||
|
Mode = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||||
pub const SERVER_PORT: u16 = 7301;
|
pub const SERVER_PORT: u16 = 7301;
|
||||||
|
|
||||||
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
|
lazy_static! {
|
||||||
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
|
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
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 types::Apid::iter() {
|
||||||
|
set.insert(id as u16);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub mod tmtc_err {
|
pub mod tmtc_err {
|
||||||
|
|
||||||
@@ -48,7 +64,13 @@ pub mod tmtc_err {
|
|||||||
#[resultcode]
|
#[resultcode]
|
||||||
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
|
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
|
||||||
#[resultcode]
|
#[resultcode]
|
||||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 3);
|
pub const PUS_SUBSERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 3);
|
||||||
|
#[resultcode]
|
||||||
|
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 4);
|
||||||
|
#[resultcode]
|
||||||
|
pub const ROUTING_ERROR: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 5);
|
||||||
|
#[resultcode(info = "Request timeout for targeted PUS request. P1: Request ID. P2: Target ID")]
|
||||||
|
pub const REQUEST_TIMEOUT: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 6);
|
||||||
|
|
||||||
#[resultcode(
|
#[resultcode(
|
||||||
info = "Not enough data inside the TC application data field. Optionally includes: \
|
info = "Not enough data inside the TC application data field. Optionally includes: \
|
||||||
@@ -60,6 +82,9 @@ pub mod tmtc_err {
|
|||||||
pub const TMTC_RESULTS: &[ResultU16Info] = &[
|
pub const TMTC_RESULTS: &[ResultU16Info] = &[
|
||||||
INVALID_PUS_SERVICE_EXT,
|
INVALID_PUS_SERVICE_EXT,
|
||||||
INVALID_PUS_SUBSERVICE_EXT,
|
INVALID_PUS_SUBSERVICE_EXT,
|
||||||
|
PUS_SERVICE_NOT_IMPLEMENTED_EXT,
|
||||||
|
UNKNOWN_TARGET_ID_EXT,
|
||||||
|
ROUTING_ERROR_EXT,
|
||||||
NOT_ENOUGH_APP_DATA_EXT,
|
NOT_ENOUGH_APP_DATA_EXT,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -76,34 +101,33 @@ pub mod hk_err {
|
|||||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Hk as u8, 2);
|
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Hk as u8, 2);
|
||||||
#[resultcode]
|
#[resultcode]
|
||||||
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 3);
|
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 3);
|
||||||
|
|
||||||
|
pub const HK_ERR_RESULTS: &[ResultU16Info] = &[
|
||||||
|
TARGET_ID_MISSING_EXT,
|
||||||
|
UNKNOWN_TARGET_ID_EXT,
|
||||||
|
UNKNOWN_TARGET_ID_EXT,
|
||||||
|
COLLECTION_INTERVAL_MISSING_EXT,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
pub mod mode_err {
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
use super::*;
|
||||||
pub enum TmSenderId {
|
|
||||||
PusVerification = 0,
|
#[resultcode]
|
||||||
PusTest = 1,
|
pub const WRONG_MODE: ResultU16 = ResultU16::new(GroupId::Mode as u8, 0);
|
||||||
PusEvent = 2,
|
|
||||||
PusHk = 3,
|
|
||||||
PusAction = 4,
|
|
||||||
PusSched = 5,
|
|
||||||
AllEvents = 6,
|
|
||||||
AcsSubsystem = 7,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
pub mod components {
|
||||||
pub enum TcReceiverId {
|
use satrs::ComponentId;
|
||||||
PusTest = 1,
|
|
||||||
PusEvent = 2,
|
pub const NO_SENDER: ComponentId = ComponentId::MAX;
|
||||||
PusHk = 3,
|
|
||||||
PusAction = 4,
|
|
||||||
PusSched = 5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod pool {
|
pub mod pool {
|
||||||
use super::*;
|
use super::*;
|
||||||
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
|
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
|
||||||
(
|
(
|
||||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||||
vec![
|
vec![
|
||||||
(30, 32),
|
(30, 32),
|
||||||
(15, 64),
|
(15, 64),
|
||||||
@@ -114,7 +138,7 @@ pub mod pool {
|
|||||||
],
|
],
|
||||||
true,
|
true,
|
||||||
)),
|
)),
|
||||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||||
vec![
|
vec![
|
||||||
(30, 32),
|
(30, 32),
|
||||||
(15, 64),
|
(15, 64),
|
||||||
@@ -129,7 +153,7 @@ pub mod pool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_sched_tc_pool() -> StaticMemoryPool {
|
pub fn create_sched_tc_pool() -> StaticMemoryPool {
|
||||||
StaticMemoryPool::new(StaticPoolConfig::new(
|
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||||
vec![
|
vec![
|
||||||
(30, 32),
|
(30, 32),
|
||||||
(15, 64),
|
(15, 64),
|
||||||
@@ -145,7 +169,7 @@ pub mod pool {
|
|||||||
|
|
||||||
pub mod tasks {
|
pub mod tasks {
|
||||||
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
||||||
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
|
pub const FREQ_MS_AOCS: u64 = 200;
|
||||||
pub const FREQ_MS_AOCS: u64 = 500;
|
pub const FREQ_MS_CONTROLLER: u64 = 200;
|
||||||
pub const FREQ_MS_PUS_STACK: u64 = 200;
|
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||||
|
use types::{
|
||||||
|
ComponentId,
|
||||||
|
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||||
|
control,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||||
|
|
||||||
|
pub struct Controller {
|
||||||
|
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Controller {
|
||||||
|
pub fn new(
|
||||||
|
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tc_rx,
|
||||||
|
tm_tx,
|
||||||
|
event_ctrl_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn periodic_operation(&mut self) {
|
||||||
|
self.handle_telecommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_telecommands(&mut self) {
|
||||||
|
loop {
|
||||||
|
match self.tc_rx.try_recv() {
|
||||||
|
Ok(packet) => {
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
|
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
|
||||||
|
Ok(request) => {
|
||||||
|
log::info!(
|
||||||
|
"received request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
tc_id.raw()
|
||||||
|
);
|
||||||
|
match request {
|
||||||
|
control::request::Request::Ping => self
|
||||||
|
.send_telemetry(Some(tc_id), control::response::Response::Ok),
|
||||||
|
control::request::Request::TestEvent => {
|
||||||
|
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
|
||||||
|
self.send_telemetry(
|
||||||
|
Some(tc_id),
|
||||||
|
control::response::Response::Ok,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to deserialize request: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
std::sync::mpsc::TryRecvError::Empty => break,
|
||||||
|
std::sync::mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::warn!("packet sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_telemetry(
|
||||||
|
&self,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
response: control::response::Response,
|
||||||
|
) {
|
||||||
|
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
|
||||||
|
Ok(packet) => {
|
||||||
|
if let Err(e) = self.tm_tx.send(packet) {
|
||||||
|
log::warn!("failed to send TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to pack TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
use std::{
|
||||||
|
collections::{HashSet, VecDeque},
|
||||||
|
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::PacketSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tmtc::sender::TmTcSender;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ConnectionFinishedHandler {}
|
||||||
|
|
||||||
|
pub struct SimplePacketValidator {
|
||||||
|
pub valid_ids: HashSet<PacketId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpacePacketValidator for SimplePacketValidator {
|
||||||
|
fn validate(
|
||||||
|
&self,
|
||||||
|
sp_header: &satrs::spacepackets::SpHeader,
|
||||||
|
_raw_buf: &[u8],
|
||||||
|
) -> satrs::encoding::ccsds::SpValidity {
|
||||||
|
if self.valid_ids.contains(&sp_header.packet_id()) {
|
||||||
|
return SpValidity::Valid;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HandledConnectionHandler for ConnectionFinishedHandler {
|
||||||
|
fn handled_connection(&mut self, info: satrs::hal::std::tcp_server::HandledConnectionInfo) {
|
||||||
|
info!(
|
||||||
|
"Served {} TMs and {} TCs for client {:?}",
|
||||||
|
info.num_sent_tms, info.num_received_tcs, info.addr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct SyncTcpTmSource {
|
||||||
|
tm_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||||
|
max_packets_stored: usize,
|
||||||
|
pub silent_packet_overwrite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncTcpTmSource {
|
||||||
|
pub fn new(max_packets_stored: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
tm_queue: Arc::default(),
|
||||||
|
max_packets_stored,
|
||||||
|
silent_packet_overwrite: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_tm(&mut self, tm: &[u8]) {
|
||||||
|
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec");
|
||||||
|
if tm_queue.len() > self.max_packets_stored {
|
||||||
|
if !self.silent_packet_overwrite {
|
||||||
|
warn!("TPC TM source is full, deleting oldest packet");
|
||||||
|
}
|
||||||
|
tm_queue.pop_front();
|
||||||
|
}
|
||||||
|
tm_queue.push_back(tm.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketSource for SyncTcpTmSource {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
|
||||||
|
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed");
|
||||||
|
if !tm_queue.is_empty() {
|
||||||
|
let next_vec = tm_queue.front().unwrap();
|
||||||
|
if buffer.len() < next_vec.len() {
|
||||||
|
panic!(
|
||||||
|
"provided buffer too small, must be at least {} bytes",
|
||||||
|
next_vec.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let next_vec = tm_queue.pop_front().unwrap();
|
||||||
|
buffer[0..next_vec.len()].copy_from_slice(&next_vec);
|
||||||
|
if next_vec.len() > 9 {
|
||||||
|
let service = next_vec[7];
|
||||||
|
let subservice = next_vec[8];
|
||||||
|
info!("Sending PUS TM[{service},{subservice}]")
|
||||||
|
} else {
|
||||||
|
info!("Sending PUS TM");
|
||||||
|
}
|
||||||
|
return Ok(next_vec.len());
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TcpServer<ReceivesTc> = TcpSpacepacketsServer<
|
||||||
|
SyncTcpTmSource,
|
||||||
|
ReceivesTc,
|
||||||
|
SimplePacketValidator,
|
||||||
|
ConnectionFinishedHandler,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub struct TcpTask(pub TcpServer<TmTcSender>);
|
||||||
|
|
||||||
|
impl TcpTask {
|
||||||
|
pub fn new(
|
||||||
|
cfg: ServerConfig,
|
||||||
|
tm_source: SyncTcpTmSource,
|
||||||
|
tc_sender: TmTcSender,
|
||||||
|
valid_ids: HashSet<PacketId>,
|
||||||
|
) -> Result<Self, std::io::Error> {
|
||||||
|
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) {
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
use log::warn;
|
||||||
|
use satrs::action::{ActionRequest, ActionRequestVariant};
|
||||||
|
use satrs::pus::action::{
|
||||||
|
ActionReplyPus, ActionReplyVariant, ActivePusActionRequestStd, DefaultActiveActionRequestMap,
|
||||||
|
};
|
||||||
|
use satrs::pus::verification::{
|
||||||
|
handle_completion_failure_with_generic_params, handle_step_failure_with_generic_params,
|
||||||
|
FailParamHelper, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||||
|
VerificationReportingProvider, VerificationToken,
|
||||||
|
};
|
||||||
|
use satrs::pus::{
|
||||||
|
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, 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,
|
||||||
|
PusTargetedRequestService, TargetedPusService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ActionReplyHandler {
|
||||||
|
fail_data_buf: [u8; 128],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ActionReplyHandler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
fail_data_buf: [0; 128],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyHandler {
|
||||||
|
type Error = EcssTmtcError;
|
||||||
|
|
||||||
|
fn handle_unrequested_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<ActionReplyPus>,
|
||||||
|
_tm_sender: &impl EcssTmSender,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
warn!("received unexpected reply for service 8: {reply:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<ActionReplyPus>,
|
||||||
|
active_request: &ActivePusActionRequestStd,
|
||||||
|
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
timestamp: &[u8],
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
let verif_token: VerificationToken<TcStateStarted> = active_request
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid token state");
|
||||||
|
let remove_entry = match &reply.message.variant {
|
||||||
|
ActionReplyVariant::CompletionFailed { error_code, params } => {
|
||||||
|
let error_propagated = handle_completion_failure_with_generic_params(
|
||||||
|
tm_sender,
|
||||||
|
verif_token,
|
||||||
|
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 {
|
||||||
|
error_code,
|
||||||
|
step,
|
||||||
|
params,
|
||||||
|
} => {
|
||||||
|
let error_propagated = handle_step_failure_with_generic_params(
|
||||||
|
tm_sender,
|
||||||
|
verif_token,
|
||||||
|
verification_handler,
|
||||||
|
FailParamHelper {
|
||||||
|
error_code,
|
||||||
|
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, timestamp)?;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ActionReplyVariant::StepSuccess { step } => {
|
||||||
|
verification_handler.step_success(
|
||||||
|
tm_sender,
|
||||||
|
&verif_token,
|
||||||
|
timestamp,
|
||||||
|
EcssEnumU16::new(*step),
|
||||||
|
)?;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
Ok(remove_entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request_timeout(
|
||||||
|
&mut self,
|
||||||
|
active_request: &ActivePusActionRequestStd,
|
||||||
|
tm_sender: &impl EcssTmSender,
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
generic_pus_request_timeout_handler(
|
||||||
|
tm_sender,
|
||||||
|
active_request,
|
||||||
|
verification_handler,
|
||||||
|
time_stamp,
|
||||||
|
"action",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ActionRequestConverter {}
|
||||||
|
|
||||||
|
impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for ActionRequestConverter {
|
||||||
|
type Error = GenericConversionError;
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&mut self,
|
||||||
|
token: VerificationToken<TcStateAccepted>,
|
||||||
|
tc: &PusTcReader,
|
||||||
|
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
verif_reporter: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(ActivePusActionRequestStd, ActionRequest), Self::Error> {
|
||||||
|
let subservice = tc.message_subtype_id();
|
||||||
|
let user_data = tc.user_data();
|
||||||
|
if user_data.len() < 8 {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
return Err(GenericConversionError::NotEnoughAppData {
|
||||||
|
expected: 8,
|
||||||
|
found: user_data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
|
||||||
|
let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap());
|
||||||
|
if subservice == 128 {
|
||||||
|
let req_variant = if user_data.len() == 8 {
|
||||||
|
ActionRequestVariant::NoData
|
||||||
|
} else {
|
||||||
|
ActionRequestVariant::VecData(user_data[8..].to_vec())
|
||||||
|
};
|
||||||
|
Ok((
|
||||||
|
ActivePusActionRequestStd::new(
|
||||||
|
action_id,
|
||||||
|
target_id_and_apid.into(),
|
||||||
|
token.into(),
|
||||||
|
Duration::from_secs(30),
|
||||||
|
),
|
||||||
|
ActionRequest::new(action_id, req_variant),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
Err(GenericConversionError::InvalidSubservice(subservice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let action_request_handler = PusTargetedRequestService::new(
|
||||||
|
PusServiceHelper::new(
|
||||||
|
ids::generic_pus::PUS_ACTION.id(),
|
||||||
|
pus_action_rx,
|
||||||
|
tm_sender,
|
||||||
|
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
|
||||||
|
// a bounded wrapper which pre-allocates using [HashMap::with_capacity]..
|
||||||
|
DefaultActiveActionRequestMap::default(),
|
||||||
|
ActionReplyHandler::default(),
|
||||||
|
action_router,
|
||||||
|
reply_receiver,
|
||||||
|
);
|
||||||
|
ActionServiceWrapper {
|
||||||
|
service: action_request_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActionServiceWrapper {
|
||||||
|
pub(crate) service: PusTargetedRequestService<
|
||||||
|
MpscTcReceiver,
|
||||||
|
VerificationReporter,
|
||||||
|
ActionRequestConverter,
|
||||||
|
ActionReplyHandler,
|
||||||
|
DefaultActiveActionRequestMap,
|
||||||
|
ActivePusActionRequestStd,
|
||||||
|
ActionRequest,
|
||||||
|
ActionReplyPus,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::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,
|
||||||
|
spacepackets::{
|
||||||
|
ecss::{
|
||||||
|
tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||||
|
tm::PusTmReader,
|
||||||
|
WritablePusPacket,
|
||||||
|
},
|
||||||
|
SpHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
pus::tests::{PusConverterTestbench, ReplyHandlerTestbench, TargetedPusRequestTestbench},
|
||||||
|
requests::CompositeRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl
|
||||||
|
TargetedPusRequestTestbench<
|
||||||
|
ActionRequestConverter,
|
||||||
|
ActionReplyHandler,
|
||||||
|
DefaultActiveActionRequestMap,
|
||||||
|
ActivePusActionRequestStd,
|
||||||
|
ActionRequest,
|
||||||
|
ActionReplyPus,
|
||||||
|
>
|
||||||
|
{
|
||||||
|
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::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::sync_channel(10);
|
||||||
|
let verif_reporter = TestVerificationReporter::new(owner_id);
|
||||||
|
let mut generic_req_router = GenericRequestRouter::default();
|
||||||
|
generic_req_router
|
||||||
|
.composite_router_map
|
||||||
|
.insert(target_id, action_req_tx);
|
||||||
|
Self {
|
||||||
|
service: PusTargetedRequestService::new(
|
||||||
|
PusServiceHelper::new(
|
||||||
|
owner_id,
|
||||||
|
pus_action_rx,
|
||||||
|
TmTcSender::Heap(tm_funnel_tx.clone()),
|
||||||
|
verif_reporter,
|
||||||
|
EcssTcCacher::Heap(EcssTcVecCacher::default()),
|
||||||
|
),
|
||||||
|
ActionRequestConverter::default(),
|
||||||
|
DefaultActiveActionRequestMap::default(),
|
||||||
|
ActionReplyHandler::default(),
|
||||||
|
generic_req_router,
|
||||||
|
action_reply_rx,
|
||||||
|
),
|
||||||
|
request_id: None,
|
||||||
|
pus_packet_tx: pus_action_tx,
|
||||||
|
tm_funnel_rx,
|
||||||
|
reply_tx: action_reply_tx,
|
||||||
|
request_rx: action_req_rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_packet_started(&self) {
|
||||||
|
self.service
|
||||||
|
.service_helper
|
||||||
|
.common
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_started_success(
|
||||||
|
self.service.service_helper.id(),
|
||||||
|
self.request_id.expect("request ID not set").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_packet_completed(&self) {
|
||||||
|
self.service
|
||||||
|
.service_helper
|
||||||
|
.common
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_completion_success(
|
||||||
|
self.service.service_helper.id(),
|
||||||
|
self.request_id.expect("request ID not set").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_tm_empty(&self) {
|
||||||
|
let packet = self.tm_funnel_rx.try_recv();
|
||||||
|
if let Err(mpsc::TryRecvError::Empty) = packet {
|
||||||
|
} else {
|
||||||
|
let tm = packet.unwrap();
|
||||||
|
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap();
|
||||||
|
panic!("unexpected TM packet {unexpected_tm:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_next_tc_is_handled_properly(&mut self, time_stamp: &[u8]) {
|
||||||
|
let result = self.service.poll_and_handle_next_tc(time_stamp);
|
||||||
|
if let Err(e) = result {
|
||||||
|
panic!("unexpected error {:?}", e);
|
||||||
|
}
|
||||||
|
let result = result.unwrap();
|
||||||
|
match result {
|
||||||
|
HandlingStatus::HandledOne => (),
|
||||||
|
_ => panic!("unexpected result {result:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_all_tcs_handled(&mut self, time_stamp: &[u8]) {
|
||||||
|
let result = self.service.poll_and_handle_next_tc(time_stamp);
|
||||||
|
if let Err(e) = result {
|
||||||
|
panic!("unexpected error {:?}", e);
|
||||||
|
}
|
||||||
|
let result = result.unwrap();
|
||||||
|
match result {
|
||||||
|
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_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_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()
|
||||||
|
.start_verification(tc);
|
||||||
|
let accepted_token = self
|
||||||
|
.service
|
||||||
|
.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.acceptance_success(self.service.service_helper.tm_sender(), token, &[0; 7])
|
||||||
|
.expect("TC acceptance failed");
|
||||||
|
self.service
|
||||||
|
.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.check_next_was_added(accepted_token.request_id());
|
||||||
|
let id = self.service.service_helper.id();
|
||||||
|
self.service
|
||||||
|
.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.check_next_is_acceptance_success(id, accepted_token.request_id());
|
||||||
|
self.pus_packet_tx
|
||||||
|
.send(EcssTcAndToken::new(
|
||||||
|
PacketAsVec::new(self.service.service_helper.id(), tc.to_vec().unwrap()),
|
||||||
|
accepted_token,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_request() {
|
||||||
|
let mut testbench = TargetedPusRequestTestbench::new_for_action(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
TEST_COMPONENT_ID_1.id(),
|
||||||
|
);
|
||||||
|
// Create a basic action request and verify forwarding.
|
||||||
|
let sp_header = SpHeader::new_from_apid(TEST_APID);
|
||||||
|
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.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, CreatorConfig::default());
|
||||||
|
testbench.add_tc(&pus8_packet);
|
||||||
|
let time_stamp: [u8; 7] = [0; 7];
|
||||||
|
testbench.verify_next_tc_is_handled_properly(&time_stamp);
|
||||||
|
testbench.verify_all_tcs_handled(&time_stamp);
|
||||||
|
|
||||||
|
testbench.verify_packet_started();
|
||||||
|
|
||||||
|
let possible_req = testbench.request_rx.try_recv();
|
||||||
|
assert!(possible_req.is_ok());
|
||||||
|
let req = possible_req.unwrap();
|
||||||
|
if let CompositeRequest::Action(action_req) = req.message {
|
||||||
|
assert_eq!(action_req.action_id, action_id);
|
||||||
|
assert_eq!(action_req.variant, ActionRequestVariant::NoData);
|
||||||
|
let action_reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
|
||||||
|
testbench
|
||||||
|
.reply_tx
|
||||||
|
.send(GenericMessage::new(req.requestor_info, action_reply))
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
panic!("unexpected request type");
|
||||||
|
}
|
||||||
|
testbench.verify_next_reply_is_handled_properly(&time_stamp);
|
||||||
|
testbench.verify_all_replies_handled(&time_stamp);
|
||||||
|
|
||||||
|
testbench.verify_packet_completed();
|
||||||
|
testbench.verify_tm_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_request_routing_error() {
|
||||||
|
let mut testbench = TargetedPusRequestTestbench::new_for_action(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
TEST_COMPONENT_ID_1.id(),
|
||||||
|
);
|
||||||
|
// Create a basic action request and verify forwarding.
|
||||||
|
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(&0_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,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
testbench.add_tc(&pus8_packet);
|
||||||
|
let time_stamp: [u8; 7] = [0; 7];
|
||||||
|
|
||||||
|
let result = testbench.service.poll_and_handle_next_tc(&time_stamp);
|
||||||
|
assert!(result.is_err());
|
||||||
|
// Verify the correct result and completion failure.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converter_action_req_no_data() {
|
||||||
|
let mut testbench = PusConverterTestbench::new(
|
||||||
|
TEST_COMPONENT_ID_0.raw(),
|
||||||
|
ActionRequestConverter::default(),
|
||||||
|
);
|
||||||
|
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.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,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
let token = testbench.add_tc(&pus8_packet);
|
||||||
|
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let (active_req, request) = result.unwrap();
|
||||||
|
if let ActionRequestVariant::NoData = request.variant {
|
||||||
|
assert_eq!(request.action_id, action_id);
|
||||||
|
assert_eq!(active_req.action_id, action_id);
|
||||||
|
assert_eq!(
|
||||||
|
active_req.target_id(),
|
||||||
|
UniqueApidTargetId::new(TEST_APID, TEST_UNIQUE_ID_0).raw()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
active_req.token().request_id(),
|
||||||
|
testbench.request_id().unwrap()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("unexpected action request variant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converter_action_req_with_data() {
|
||||||
|
let mut testbench =
|
||||||
|
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ActionRequestConverter::default());
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
let pus8_packet = PusTcCreator::new(
|
||||||
|
SpHeader::new_from_apid(TEST_APID),
|
||||||
|
sec_header,
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
let token = testbench.add_tc(&pus8_packet);
|
||||||
|
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let (active_req, request) = result.unwrap();
|
||||||
|
if let ActionRequestVariant::VecData(vec) = request.variant {
|
||||||
|
assert_eq!(request.action_id, action_id);
|
||||||
|
assert_eq!(active_req.action_id, action_id);
|
||||||
|
assert_eq!(vec, app_data[8..].to_vec());
|
||||||
|
} else {
|
||||||
|
panic!("unexpected action request variant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_completion_success() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||||
|
let action_id = 5_u32;
|
||||||
|
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let active_action_req =
|
||||||
|
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||||
|
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
|
||||||
|
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||||
|
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap());
|
||||||
|
testbench.verif_reporter.assert_full_completion_success(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
req_id,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_completion_failure() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||||
|
let action_id = 5_u32;
|
||||||
|
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let active_action_req =
|
||||||
|
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||||
|
let error_code = ResultU16::new(2, 3);
|
||||||
|
let reply = ActionReplyPus::new(
|
||||||
|
action_id,
|
||||||
|
ActionReplyVariant::CompletionFailed {
|
||||||
|
error_code,
|
||||||
|
params: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||||
|
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap());
|
||||||
|
testbench.verif_reporter.assert_completion_failure(
|
||||||
|
TEST_COMPONENT_ID_0.into(),
|
||||||
|
req_id,
|
||||||
|
None,
|
||||||
|
error_code.raw() as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_step_success() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||||
|
let action_id = 5_u32;
|
||||||
|
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let active_action_req =
|
||||||
|
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||||
|
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::StepSuccess { step: 1 });
|
||||||
|
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||||
|
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// Entry should not be removed, completion not done yet.
|
||||||
|
assert!(!result.unwrap());
|
||||||
|
testbench.verif_reporter.check_next_was_added(req_id);
|
||||||
|
testbench
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.raw(), req_id);
|
||||||
|
testbench
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_started_success(TEST_COMPONENT_ID_0.raw(), req_id);
|
||||||
|
testbench
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_step_success(TEST_COMPONENT_ID_0.raw(), req_id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_step_failure() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||||
|
let action_id = 5_u32;
|
||||||
|
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let active_action_req =
|
||||||
|
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||||
|
let error_code = ResultU16::new(2, 3);
|
||||||
|
let reply = ActionReplyPus::new(
|
||||||
|
action_id,
|
||||||
|
ActionReplyVariant::StepFailed {
|
||||||
|
error_code,
|
||||||
|
step: 1,
|
||||||
|
params: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||||
|
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap());
|
||||||
|
testbench.verif_reporter.check_next_was_added(req_id);
|
||||||
|
testbench
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.id(), req_id);
|
||||||
|
testbench
|
||||||
|
.verif_reporter
|
||||||
|
.check_next_is_started_success(TEST_COMPONENT_ID_0.id(), req_id);
|
||||||
|
testbench.verif_reporter.check_next_is_step_failure(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
req_id,
|
||||||
|
error_code.raw().into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_unrequested_reply() {
|
||||||
|
let mut testbench =
|
||||||
|
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_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);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_reply_timeout() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||||
|
let action_id = 5_u32;
|
||||||
|
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let result = testbench.handle_request_timeout(
|
||||||
|
&ActivePusActionRequestStd::new_from_common_req(action_id, active_request),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
testbench.verif_reporter.assert_completion_failure(
|
||||||
|
TEST_COMPONENT_ID_0.raw(),
|
||||||
|
req_id,
|
||||||
|
None,
|
||||||
|
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
use derive_new::new;
|
||||||
|
use satrs::hk::{CollectionIntervalFactor, HkRequest, HkRequestVariant, UniqueId};
|
||||||
|
use satrs::pus::verification::{
|
||||||
|
FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||||
|
VerificationReportingProvider, VerificationToken,
|
||||||
|
};
|
||||||
|
use satrs::pus::{
|
||||||
|
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, 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, TargetedPusService};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, new)]
|
||||||
|
pub struct HkReply {
|
||||||
|
pub unique_id: UniqueId,
|
||||||
|
pub variant: HkReplyVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum HkReplyVariant {
|
||||||
|
Ack,
|
||||||
|
Failed(ResultU16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct HkReplyHandler {}
|
||||||
|
|
||||||
|
impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
|
||||||
|
type Error = EcssTmtcError;
|
||||||
|
|
||||||
|
fn handle_unrequested_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<HkReply>,
|
||||||
|
_tm_sender: &impl EcssTmSender,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
log::warn!("received unexpected reply for service 3: {reply:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<HkReply>,
|
||||||
|
active_request: &ActivePusRequestStd,
|
||||||
|
tm_sender: &impl EcssTmSender,
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid token state");
|
||||||
|
match reply.message.variant {
|
||||||
|
HkReplyVariant::Ack => {
|
||||||
|
verification_handler
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request_timeout(
|
||||||
|
&mut self,
|
||||||
|
active_request: &ActivePusRequestStd,
|
||||||
|
tm_sender: &impl EcssTmSender,
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
generic_pus_request_timeout_handler(
|
||||||
|
tm_sender,
|
||||||
|
active_request,
|
||||||
|
verification_handler,
|
||||||
|
time_stamp,
|
||||||
|
"HK",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HkRequestConverter {
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HkRequestConverter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
timeout: Duration::from_secs(60),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConverter {
|
||||||
|
type Error = GenericConversionError;
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&mut self,
|
||||||
|
token: VerificationToken<TcStateAccepted>,
|
||||||
|
tc: &PusTcReader,
|
||||||
|
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
verif_reporter: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(ActivePusRequestStd, HkRequest), Self::Error> {
|
||||||
|
let user_data = tc.user_data();
|
||||||
|
if user_data.is_empty() {
|
||||||
|
let user_data_len = user_data.len() as u32;
|
||||||
|
let user_data_len_raw = user_data_len.to_be_bytes();
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new(
|
||||||
|
time_stamp,
|
||||||
|
&tmtc_err::NOT_ENOUGH_APP_DATA,
|
||||||
|
&user_data_len_raw,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure TM failed");
|
||||||
|
return Err(GenericConversionError::NotEnoughAppData {
|
||||||
|
expected: 4,
|
||||||
|
found: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if user_data.len() < 8 {
|
||||||
|
let err = if user_data.len() < 4 {
|
||||||
|
&hk_err::TARGET_ID_MISSING
|
||||||
|
} else {
|
||||||
|
&hk_err::UNIQUE_ID_MISSING
|
||||||
|
};
|
||||||
|
let user_data_len = user_data.len() as u32;
|
||||||
|
let user_data_len_raw = user_data_len.to_be_bytes();
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new(time_stamp, err, &user_data_len_raw),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure TM failed");
|
||||||
|
return Err(GenericConversionError::NotEnoughAppData {
|
||||||
|
expected: 8,
|
||||||
|
found: 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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::MessageSubtypeId::try_from(subservice);
|
||||||
|
if standard_subservice.is_err() {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE, &[subservice]),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure TM failed");
|
||||||
|
return Err(GenericConversionError::InvalidSubservice(subservice));
|
||||||
|
}
|
||||||
|
let request = match standard_subservice.unwrap() {
|
||||||
|
hk::MessageSubtypeId::TcEnableHkGeneration
|
||||||
|
| hk::MessageSubtypeId::TcEnableDiagGeneration => {
|
||||||
|
HkRequest::new(unique_id, HkRequestVariant::EnablePeriodic)
|
||||||
|
}
|
||||||
|
hk::MessageSubtypeId::TcDisableHkGeneration
|
||||||
|
| hk::MessageSubtypeId::TcDisableDiagGeneration => {
|
||||||
|
HkRequest::new(unique_id, HkRequestVariant::DisablePeriodic)
|
||||||
|
}
|
||||||
|
hk::MessageSubtypeId::TcReportHkReportStructures => todo!(),
|
||||||
|
hk::MessageSubtypeId::TmHkPacket => todo!(),
|
||||||
|
hk::MessageSubtypeId::TcGenerateOneShotHk
|
||||||
|
| hk::MessageSubtypeId::TcGenerateOneShotDiag => {
|
||||||
|
HkRequest::new(unique_id, HkRequestVariant::OneShot)
|
||||||
|
}
|
||||||
|
hk::MessageSubtypeId::TcModifyDiagCollectionInterval
|
||||||
|
| hk::MessageSubtypeId::TcModifyHkCollectionInterval => {
|
||||||
|
if user_data.len() < 12 {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new_no_fail_data(
|
||||||
|
time_stamp,
|
||||||
|
&tmtc_err::NOT_ENOUGH_APP_DATA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure TM failed");
|
||||||
|
return Err(GenericConversionError::NotEnoughAppData {
|
||||||
|
expected: 12,
|
||||||
|
found: user_data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HkRequest::new(
|
||||||
|
unique_id,
|
||||||
|
HkRequestVariant::ModifyCollectionInterval(
|
||||||
|
CollectionIntervalFactor::from_be_bytes(
|
||||||
|
user_data[8..12].try_into().unwrap(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new(
|
||||||
|
time_stamp,
|
||||||
|
&tmtc_err::PUS_SUBSERVICE_NOT_IMPLEMENTED,
|
||||||
|
&[subservice],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure TM failed");
|
||||||
|
return Err(GenericConversionError::InvalidSubservice(subservice));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((
|
||||||
|
ActivePusRequestStd::new(target_id_and_apid.into(), token, self.timeout),
|
||||||
|
request,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let pus_3_handler = PusTargetedRequestService::new(
|
||||||
|
PusServiceHelper::new(
|
||||||
|
PUS_HK.id(),
|
||||||
|
pus_hk_rx,
|
||||||
|
tm_sender,
|
||||||
|
create_verification_reporter(PUS_HK.id(), PUS_HK.apid),
|
||||||
|
tc_in_mem_converter,
|
||||||
|
),
|
||||||
|
HkRequestConverter::default(),
|
||||||
|
DefaultActiveRequestMap::default(),
|
||||||
|
HkReplyHandler::default(),
|
||||||
|
request_router,
|
||||||
|
reply_receiver,
|
||||||
|
);
|
||||||
|
HkServiceWrapper {
|
||||||
|
service: pus_3_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HkServiceWrapper {
|
||||||
|
pub(crate) service: PusTargetedRequestService<
|
||||||
|
MpscTcReceiver,
|
||||||
|
VerificationReporter,
|
||||||
|
HkRequestConverter,
|
||||||
|
HkReplyHandler,
|
||||||
|
DefaultActiveRequestMap<ActivePusRequestStd>,
|
||||||
|
ActivePusRequestStd,
|
||||||
|
HkRequest,
|
||||||
|
HkReply,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::MessageSubtypeId, tc::PusTcCreator},
|
||||||
|
SpHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use example_std::config::tmtc_err;
|
||||||
|
|
||||||
|
use crate::pus::{
|
||||||
|
hk::HkReplyVariant,
|
||||||
|
tests::{PusConverterTestbench, ReplyHandlerTestbench},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{HkReply, HkReplyHandler, HkRequestConverter};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcGenerateOneShotHk as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
let accepted_token = hk_bench.add_tc(&hk_req);
|
||||||
|
let (_active_req, req) = hk_bench
|
||||||
|
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||||
|
.expect("conversion failed");
|
||||||
|
|
||||||
|
assert_eq!(req.unique_id, unique_id);
|
||||||
|
if let HkRequestVariant::OneShot = req.variant {
|
||||||
|
} else {
|
||||||
|
panic!("unexpected HK request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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);
|
||||||
|
let (_active_req, req) = hk_bench
|
||||||
|
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||||
|
.expect("conversion failed");
|
||||||
|
assert_eq!(req.unique_id, unique_id);
|
||||||
|
if let HkRequestVariant::EnablePeriodic = req.variant {
|
||||||
|
} else {
|
||||||
|
panic!("unexpected HK request")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tc0 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcEnableHkGeneration as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc0);
|
||||||
|
let tc1 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcEnableDiagGeneration as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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);
|
||||||
|
let (_active_req, req) = hk_bench
|
||||||
|
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||||
|
.expect("conversion failed");
|
||||||
|
assert_eq!(req.unique_id, unique_id);
|
||||||
|
if let HkRequestVariant::DisablePeriodic = req.variant {
|
||||||
|
} else {
|
||||||
|
panic!("unexpected HK request")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tc0 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcDisableHkGeneration as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc0);
|
||||||
|
let tc1 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcDisableDiagGeneration as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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());
|
||||||
|
|
||||||
|
let mut generic_check = |tc: &PusTcCreator| {
|
||||||
|
let accepted_token = hk_bench.add_tc(tc);
|
||||||
|
let (_active_req, req) = hk_bench
|
||||||
|
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||||
|
.expect("conversion failed");
|
||||||
|
assert_eq!(req.unique_id, unique_id);
|
||||||
|
if let HkRequestVariant::ModifyCollectionInterval(interval_factor) = req.variant {
|
||||||
|
assert_eq!(interval_factor, collection_interval_factor);
|
||||||
|
} else {
|
||||||
|
panic!("unexpected HK request")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tc0 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcModifyHkCollectionInterval as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc0);
|
||||||
|
let tc1 = PusTcCreator::new_simple(
|
||||||
|
sp_header,
|
||||||
|
MessageTypeId::new(3, MessageSubtypeId::TcModifyDiagCollectionInterval as u8),
|
||||||
|
&app_data,
|
||||||
|
CreatorConfig::default(),
|
||||||
|
);
|
||||||
|
generic_check(&tc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hk_reply_handler() {
|
||||||
|
let mut reply_testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), HkReplyHandler::default());
|
||||||
|
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(
|
||||||
|
MessageMetadata::new(req_id.into(), sender_id),
|
||||||
|
HkReply::new(unique_id, HkReplyVariant::Ack),
|
||||||
|
);
|
||||||
|
let result = reply_testbench.handle_reply(&reply, &active_req, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap());
|
||||||
|
reply_testbench
|
||||||
|
.verif_reporter
|
||||||
|
.assert_full_completion_success(TEST_COMPONENT_ID_0.raw(), req_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_unrequested_reply() {
|
||||||
|
let mut testbench =
|
||||||
|
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_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);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_reply_timeout() {
|
||||||
|
let mut testbench =
|
||||||
|
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
|
||||||
|
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_1, &[]);
|
||||||
|
let result = testbench.handle_request_timeout(&active_request, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
testbench.verif_reporter.assert_completion_failure(
|
||||||
|
TEST_COMPONENT_ID_1.raw(),
|
||||||
|
req_id,
|
||||||
|
None,
|
||||||
|
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,779 @@
|
|||||||
|
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,
|
||||||
|
VerificationReporterConfig, VerificationReportingProvider, VerificationToken,
|
||||||
|
};
|
||||||
|
use satrs::pus::{
|
||||||
|
ActiveRequest, ActiveRequestStore, CacheAndReadRawEcssTc, EcssTcAndToken, EcssTcCacher,
|
||||||
|
EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericConversionError, GenericRoutingError,
|
||||||
|
HandlingStatus, PusPacketHandlingError, PusReplyHandler, PusRequestRouter, PusServiceHelper,
|
||||||
|
PusTcToRequestConverter, TcInMemory,
|
||||||
|
};
|
||||||
|
use satrs::queue::{GenericReceiveError, GenericSendError};
|
||||||
|
use satrs::request::{Apid, GenericMessage, MessageMetadata};
|
||||||
|
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||||
|
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
|
||||||
|
use satrs::tmtc::{PacketAsVec, PacketInPool};
|
||||||
|
use satrs::ComponentId;
|
||||||
|
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;
|
||||||
|
|
||||||
|
pub mod action;
|
||||||
|
pub mod event;
|
||||||
|
pub mod hk;
|
||||||
|
pub mod mode;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub mod stack;
|
||||||
|
pub mod test;
|
||||||
|
|
||||||
|
pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> VerificationReporter {
|
||||||
|
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: 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 {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub id: ComponentId,
|
||||||
|
pub tm_sender: TmTcSender,
|
||||||
|
pub verif_reporter: VerificationReporter,
|
||||||
|
pub pus_router: PusTcMpscRouter,
|
||||||
|
stamp_helper: TimestampHelper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusTcDistributor {
|
||||||
|
pub fn new(tm_sender: TmTcSender, pus_router: PusTcMpscRouter) -> Self {
|
||||||
|
Self {
|
||||||
|
id: PUS_ROUTING.raw(),
|
||||||
|
tm_sender,
|
||||||
|
verif_reporter: create_verification_reporter(PUS_ROUTING.id(), PUS_ROUTING.apid),
|
||||||
|
pus_router,
|
||||||
|
stamp_helper: TimestampHelper::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_tc_packet_vec(
|
||||||
|
&mut self,
|
||||||
|
packet_as_vec: PacketAsVec,
|
||||||
|
) -> Result<HandlingStatus, GenericSendError> {
|
||||||
|
self.handle_tc_generic(packet_as_vec.sender_id, None, &packet_as_vec.packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_tc_packet_in_store(
|
||||||
|
&mut self,
|
||||||
|
packet_in_pool: PacketInPool,
|
||||||
|
pus_tc_copy: &[u8],
|
||||||
|
) -> Result<HandlingStatus, GenericSendError> {
|
||||||
|
self.handle_tc_generic(
|
||||||
|
packet_in_pool.sender_id,
|
||||||
|
Some(packet_in_pool.store_addr),
|
||||||
|
pus_tc_copy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_tc_generic(
|
||||||
|
&mut self,
|
||||||
|
sender_id: ComponentId,
|
||||||
|
addr_opt: Option<PoolAddr>,
|
||||||
|
raw_tc: &[u8],
|
||||||
|
) -> Result<HandlingStatus, GenericSendError> {
|
||||||
|
let pus_tc_result = PusTcReader::new(raw_tc);
|
||||||
|
if pus_tc_result.is_err() {
|
||||||
|
log::warn!(
|
||||||
|
"error creating PUS TC from raw data received from {}: {}",
|
||||||
|
sender_id,
|
||||||
|
pus_tc_result.unwrap_err()
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
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_type_id());
|
||||||
|
let tc_in_memory: TcInMemory = if let Some(store_addr) = addr_opt {
|
||||||
|
PacketInPool::new(sender_id, store_addr).into()
|
||||||
|
} else {
|
||||||
|
PacketAsVec::new(sender_id, Vec::from(raw_tc)).into()
|
||||||
|
};
|
||||||
|
match service {
|
||||||
|
Ok(standard_service) => match standard_service {
|
||||||
|
PusServiceId::Test => self.pus_router.test_tc_sender.send(EcssTcAndToken {
|
||||||
|
tc_in_memory,
|
||||||
|
token: Some(accepted_token.into()),
|
||||||
|
})?,
|
||||||
|
PusServiceId::Housekeeping => {
|
||||||
|
self.pus_router.hk_tc_sender.send(EcssTcAndToken {
|
||||||
|
tc_in_memory,
|
||||||
|
token: Some(accepted_token.into()),
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
PusServiceId::Event => self.pus_router.event_tc_sender.send(EcssTcAndToken {
|
||||||
|
tc_in_memory,
|
||||||
|
token: Some(accepted_token.into()),
|
||||||
|
})?,
|
||||||
|
PusServiceId::Scheduling => {
|
||||||
|
self.pus_router.sched_tc_sender.send(EcssTcAndToken {
|
||||||
|
tc_in_memory,
|
||||||
|
token: Some(accepted_token.into()),
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let result = self.verif_reporter.start_failure(
|
||||||
|
&self.tm_sender,
|
||||||
|
accepted_token,
|
||||||
|
FailParams::new(
|
||||||
|
self.stamp_helper.stamp(),
|
||||||
|
&tmtc_err::PUS_SERVICE_NOT_IMPLEMENTED,
|
||||||
|
&[standard_service as u8],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if result.is_err() {
|
||||||
|
warn!("Sending verification failure failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
if let Ok(custom_service) = CustomPusServiceId::try_from(e.number) {
|
||||||
|
match custom_service {
|
||||||
|
CustomPusServiceId::Mode => self
|
||||||
|
.pus_router
|
||||||
|
.mode_tc_sender
|
||||||
|
.send(EcssTcAndToken {
|
||||||
|
tc_in_memory,
|
||||||
|
token: Some(accepted_token.into()),
|
||||||
|
})
|
||||||
|
.map_err(|_| GenericSendError::RxDisconnected)?,
|
||||||
|
CustomPusServiceId::Health => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
&self.tm_sender,
|
||||||
|
accepted_token,
|
||||||
|
FailParams::new(
|
||||||
|
self.stamp_helper.stamp(),
|
||||||
|
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||||
|
&[e.number],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("Start failure verification failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(HandlingStatus::HandledOne)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub trait TargetedPusService {
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// The generic steps for this process are the following
|
||||||
|
///
|
||||||
|
/// 1. Poll for TC packets
|
||||||
|
/// 2. Convert the raw packets to a [PusTcReader].
|
||||||
|
/// 3. Convert the PUS TC to a typed request using the [PusTcToRequestConverter].
|
||||||
|
/// 4. Route the requests using the [GenericRequestRouter].
|
||||||
|
/// 5. Add the request to the active request map using the [ActiveRequestMapProvider] abstraction.
|
||||||
|
/// 6. Check for replies which complete the forwarded request. The handler takes care of
|
||||||
|
/// the verification process.
|
||||||
|
/// 7. Check for timeouts of active requests. Generally, the timeout on the service level should
|
||||||
|
/// be highest expected timeout for the given target.
|
||||||
|
///
|
||||||
|
/// The handler exposes the following API:
|
||||||
|
///
|
||||||
|
/// 1. [Self::poll_and_handle_next_tc] which tries to poll and handle one TC packet, covering
|
||||||
|
/// steps 1-5.
|
||||||
|
/// 2. [Self::poll_and_check_next_reply] which tries to poll and handle one reply, covering step 6.
|
||||||
|
/// 3. [Self::check_for_request_timeouts] which checks for request timeouts, covering step 7.
|
||||||
|
pub struct PusTargetedRequestService<
|
||||||
|
TcReceiver: EcssTcReceiver,
|
||||||
|
VerificationReporter: VerificationReportingProvider,
|
||||||
|
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||||
|
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||||
|
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
RequestType,
|
||||||
|
ReplyType,
|
||||||
|
> {
|
||||||
|
pub service_helper:
|
||||||
|
PusServiceHelper<TcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||||
|
pub request_router: GenericRequestRouter,
|
||||||
|
pub request_converter: RequestConverter,
|
||||||
|
pub active_request_map: ActiveRequestMapInstance,
|
||||||
|
pub reply_handler: ReplyHandler,
|
||||||
|
pub reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||||
|
phantom: std::marker::PhantomData<(RequestType, ActiveRequestInfo, ReplyType)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
TcReceiver: EcssTcReceiver,
|
||||||
|
VerificationReporter: VerificationReportingProvider,
|
||||||
|
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||||
|
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||||
|
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
RequestType,
|
||||||
|
ReplyType,
|
||||||
|
>
|
||||||
|
PusTargetedRequestService<
|
||||||
|
TcReceiver,
|
||||||
|
VerificationReporter,
|
||||||
|
RequestConverter,
|
||||||
|
ReplyHandler,
|
||||||
|
ActiveRequestMapInstance,
|
||||||
|
ActiveRequestInfo,
|
||||||
|
RequestType,
|
||||||
|
ReplyType,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
GenericRequestRouter: PusRequestRouter<RequestType, Error = GenericRoutingError>,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
service_helper: PusServiceHelper<
|
||||||
|
TcReceiver,
|
||||||
|
TmTcSender,
|
||||||
|
EcssTcCacher,
|
||||||
|
VerificationReporter,
|
||||||
|
>,
|
||||||
|
request_converter: RequestConverter,
|
||||||
|
active_request_map: ActiveRequestMapInstance,
|
||||||
|
reply_hook: ReplyHandler,
|
||||||
|
request_router: GenericRequestRouter,
|
||||||
|
reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
service_helper,
|
||||||
|
request_converter,
|
||||||
|
active_request_map,
|
||||||
|
reply_handler: reply_hook,
|
||||||
|
request_router,
|
||||||
|
reply_receiver,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll_and_handle_next_tc(
|
||||||
|
&mut self,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<HandlingStatus, PusPacketHandlingError> {
|
||||||
|
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||||
|
if possible_packet.is_none() {
|
||||||
|
return Ok(HandlingStatus::Empty);
|
||||||
|
}
|
||||||
|
let ecss_tc_and_token = possible_packet.unwrap();
|
||||||
|
self.service_helper
|
||||||
|
.tc_in_mem_converter_mut()
|
||||||
|
.cache(&ecss_tc_and_token.tc_in_memory)?;
|
||||||
|
let tc = self.service_helper.tc_in_mem_converter().convert()?;
|
||||||
|
let (mut request_info, request) = match self.request_converter.convert(
|
||||||
|
ecss_tc_and_token.token,
|
||||||
|
&tc,
|
||||||
|
self.service_helper.tm_sender(),
|
||||||
|
&self.service_helper.common.verif_reporter,
|
||||||
|
time_stamp,
|
||||||
|
) {
|
||||||
|
Ok((info, req)) => (info, req),
|
||||||
|
Err(e) => {
|
||||||
|
self.handle_conversion_to_request_error(&e, ecss_tc_and_token.token, time_stamp);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let accepted_token: VerificationToken<TcStateAccepted> = request_info
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("token not in expected accepted state");
|
||||||
|
let verif_request_id = verification::RequestId::new(&tc).raw();
|
||||||
|
match self.request_router.route(
|
||||||
|
MessageMetadata::new(verif_request_id, self.service_helper.id()),
|
||||||
|
request_info.target_id(),
|
||||||
|
request,
|
||||||
|
) {
|
||||||
|
Ok(()) => {
|
||||||
|
let started_token = self
|
||||||
|
.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.start_success(
|
||||||
|
&self.service_helper.common.tm_sender,
|
||||||
|
accepted_token,
|
||||||
|
time_stamp,
|
||||||
|
)
|
||||||
|
.expect("Start success failure");
|
||||||
|
request_info.set_token(started_token.into());
|
||||||
|
self.active_request_map
|
||||||
|
.insert(&verif_request_id, request_info);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.request_router.handle_error_generic(
|
||||||
|
&request_info,
|
||||||
|
&tc,
|
||||||
|
e.clone(),
|
||||||
|
self.service_helper.tm_sender(),
|
||||||
|
self.service_helper.verif_reporter(),
|
||||||
|
time_stamp,
|
||||||
|
);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(HandlingStatus::HandledOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_conversion_to_request_error(
|
||||||
|
&mut self,
|
||||||
|
error: &GenericConversionError,
|
||||||
|
token: VerificationToken<TcStateAccepted>,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) {
|
||||||
|
match error {
|
||||||
|
GenericConversionError::WrongService(service) => {
|
||||||
|
let service_slice: [u8; 1] = [*service];
|
||||||
|
self.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.completion_failure(
|
||||||
|
self.service_helper.tm_sender(),
|
||||||
|
token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SERVICE, &service_slice),
|
||||||
|
)
|
||||||
|
.expect("Sending completion failure failed");
|
||||||
|
}
|
||||||
|
GenericConversionError::InvalidSubservice(subservice) => {
|
||||||
|
let subservice_slice: [u8; 1] = [*subservice];
|
||||||
|
self.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.completion_failure(
|
||||||
|
self.service_helper.tm_sender(),
|
||||||
|
token,
|
||||||
|
FailParams::new(
|
||||||
|
time_stamp,
|
||||||
|
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||||
|
&subservice_slice,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("Sending completion failure failed");
|
||||||
|
}
|
||||||
|
GenericConversionError::NotEnoughAppData { expected, found } => {
|
||||||
|
let mut context_info = (*found as u32).to_be_bytes().to_vec();
|
||||||
|
context_info.extend_from_slice(&(*expected as u32).to_be_bytes());
|
||||||
|
self.service_helper
|
||||||
|
.verif_reporter()
|
||||||
|
.completion_failure(
|
||||||
|
self.service_helper.tm_sender(),
|
||||||
|
token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA, &context_info),
|
||||||
|
)
|
||||||
|
.expect("Sending completion failure failed");
|
||||||
|
}
|
||||||
|
// Do nothing.. this is service-level and can not be handled generically here.
|
||||||
|
GenericConversionError::InvalidAppData(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll_and_handle_next_reply(
|
||||||
|
&mut self,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<HandlingStatus, EcssTmtcError> {
|
||||||
|
match self.reply_receiver.try_recv() {
|
||||||
|
Ok(reply) => {
|
||||||
|
self.handle_reply(&reply, time_stamp)?;
|
||||||
|
Ok(HandlingStatus::HandledOne)
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => Ok(HandlingStatus::Empty),
|
||||||
|
mpsc::TryRecvError::Disconnected => Err(EcssTmtcError::Receive(
|
||||||
|
GenericReceiveError::TxDisconnected(None),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<ReplyType>,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(), EcssTmtcError> {
|
||||||
|
let active_req_opt = self.active_request_map.get(reply.request_id());
|
||||||
|
if active_req_opt.is_none() {
|
||||||
|
self.reply_handler
|
||||||
|
.handle_unrequested_reply(reply, &self.service_helper.common.tm_sender)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let active_request = active_req_opt.unwrap();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
result.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_for_request_timeouts(&mut self) {
|
||||||
|
let mut requests_to_delete = Vec::new();
|
||||||
|
self.active_request_map
|
||||||
|
.for_each(|request_id, request_info| {
|
||||||
|
if request_info.has_timed_out() {
|
||||||
|
requests_to_delete.push(*request_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if !requests_to_delete.is_empty() {
|
||||||
|
for request_id in requests_to_delete {
|
||||||
|
self.active_request_map.remove(request_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic timeout handling: Handle the verification failure with a dedicated return code
|
||||||
|
/// and also log the error.
|
||||||
|
pub fn generic_pus_request_timeout_handler(
|
||||||
|
sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
active_request: &(impl ActiveRequest + Debug),
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
service_str: &'static str,
|
||||||
|
) -> Result<(), EcssTmtcError> {
|
||||||
|
log::warn!("timeout for active request {active_request:?} on {service_str} service");
|
||||||
|
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("token not in expected started state");
|
||||||
|
verification_handler.completion_failure(
|
||||||
|
sender,
|
||||||
|
started_token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::REQUEST_TIMEOUT, &[]),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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,
|
||||||
|
ActiveRequestStore, MpscTcReceiver,
|
||||||
|
},
|
||||||
|
request::UniqueApidTargetId,
|
||||||
|
spacepackets::{
|
||||||
|
ecss::{
|
||||||
|
tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||||
|
WritablePusPacket,
|
||||||
|
},
|
||||||
|
SpHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::requests::CompositeRequest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Testbench dedicated to the testing of [PusReplyHandler]s
|
||||||
|
pub struct ReplyHandlerTestbench<
|
||||||
|
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
Reply,
|
||||||
|
> {
|
||||||
|
pub id: ComponentId,
|
||||||
|
pub verif_reporter: TestVerificationReporter,
|
||||||
|
pub reply_handler: ReplyHandler,
|
||||||
|
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
|
||||||
|
pub default_timeout: Duration,
|
||||||
|
tm_sender: MpscTmAsVecSender,
|
||||||
|
phantom: std::marker::PhantomData<(ActiveRequestInfo, Reply)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
Reply,
|
||||||
|
> ReplyHandlerTestbench<ReplyHandler, ActiveRequestInfo, Reply>
|
||||||
|
{
|
||||||
|
pub fn new(owner_id: ComponentId, reply_handler: ReplyHandler) -> Self {
|
||||||
|
let test_verif_reporter = TestVerificationReporter::new(owner_id);
|
||||||
|
let (tm_sender, tm_receiver) = mpsc::channel();
|
||||||
|
Self {
|
||||||
|
id: TEST_COMPONENT_ID_0.raw(),
|
||||||
|
verif_reporter: test_verif_reporter,
|
||||||
|
reply_handler,
|
||||||
|
default_timeout: Duration::from_secs(30),
|
||||||
|
tm_sender,
|
||||||
|
tm_receiver,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_tc(
|
||||||
|
&mut self,
|
||||||
|
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(MessageTypeId::new(0, 0));
|
||||||
|
let init = self.verif_reporter.start_verification(&PusTcCreator::new(
|
||||||
|
sp_header,
|
||||||
|
sec_header_dummy,
|
||||||
|
&[],
|
||||||
|
CreatorConfig::default(),
|
||||||
|
));
|
||||||
|
let accepted = self
|
||||||
|
.verif_reporter
|
||||||
|
.acceptance_success(&self.tm_sender, init, time_stamp)
|
||||||
|
.expect("acceptance failed");
|
||||||
|
let started = self
|
||||||
|
.verif_reporter
|
||||||
|
.start_success(&self.tm_sender, accepted, time_stamp)
|
||||||
|
.expect("start failed");
|
||||||
|
(
|
||||||
|
started.request_id(),
|
||||||
|
ActivePusRequestStd::new(
|
||||||
|
UniqueApidTargetId::new(apid, apid_target).raw(),
|
||||||
|
started,
|
||||||
|
self.default_timeout,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<Reply>,
|
||||||
|
active_request: &ActiveRequestInfo,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<bool, ReplyHandler::Error> {
|
||||||
|
self.reply_handler.handle_reply(
|
||||||
|
reply,
|
||||||
|
active_request,
|
||||||
|
&self.tm_sender,
|
||||||
|
&self.verif_reporter,
|
||||||
|
time_stamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_unrequested_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<Reply>,
|
||||||
|
) -> Result<(), ReplyHandler::Error> {
|
||||||
|
self.reply_handler
|
||||||
|
.handle_unrequested_reply(reply, &self.tm_sender)
|
||||||
|
}
|
||||||
|
pub fn handle_request_timeout(
|
||||||
|
&mut self,
|
||||||
|
active_request_info: &ActiveRequestInfo,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(), ReplyHandler::Error> {
|
||||||
|
self.reply_handler.handle_request_timeout(
|
||||||
|
active_request_info,
|
||||||
|
&self.tm_sender,
|
||||||
|
&self.verif_reporter,
|
||||||
|
time_stamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DummySender {}
|
||||||
|
|
||||||
|
/// Dummy sender component which does nothing on the [Self::send_tm] call.
|
||||||
|
///
|
||||||
|
/// Useful for unit tests.
|
||||||
|
impl EcssTmSender for DummySender {
|
||||||
|
fn send_tm(&self, _source_id: ComponentId, _tm: PusTmVariant) -> Result<(), EcssTmtcError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testbench dedicated to the testing of [PusTcToRequestConverter]s
|
||||||
|
pub struct PusConverterTestbench<
|
||||||
|
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
Request,
|
||||||
|
> {
|
||||||
|
pub id: ComponentId,
|
||||||
|
pub verif_reporter: TestVerificationReporter,
|
||||||
|
pub converter: Converter,
|
||||||
|
dummy_sender: DummySender,
|
||||||
|
current_request_id: Option<verification::RequestId>,
|
||||||
|
current_packet: Option<Vec<u8>>,
|
||||||
|
phantom: std::marker::PhantomData<(ActiveRequestInfo, Request)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
Request,
|
||||||
|
> PusConverterTestbench<Converter, ActiveRequestInfo, Request>
|
||||||
|
{
|
||||||
|
pub fn new(owner_id: ComponentId, converter: Converter) -> Self {
|
||||||
|
let test_verif_reporter = TestVerificationReporter::new(owner_id);
|
||||||
|
Self {
|
||||||
|
id: owner_id,
|
||||||
|
verif_reporter: test_verif_reporter,
|
||||||
|
converter,
|
||||||
|
dummy_sender: DummySender::default(),
|
||||||
|
current_request_id: None,
|
||||||
|
current_packet: None,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
|
||||||
|
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
|
||||||
|
.acceptance_success(&self.dummy_sender, token, &[])
|
||||||
|
.expect("acceptance failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_id(&self) -> Option<verification::RequestId> {
|
||||||
|
self.current_request_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert(
|
||||||
|
&mut self,
|
||||||
|
token: VerificationToken<TcStateAccepted>,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
expected_apid: u11,
|
||||||
|
expected_apid_target: u21,
|
||||||
|
) -> Result<(ActiveRequestInfo, Request), Converter::Error> {
|
||||||
|
if self.current_packet.is_none() {
|
||||||
|
return Err(GenericConversionError::InvalidAppData(
|
||||||
|
"call add_tc first".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let current_packet = self.current_packet.take().unwrap();
|
||||||
|
let tc_reader = PusTcReader::new(¤t_packet).unwrap();
|
||||||
|
let (active_info, request) = self.converter.convert(
|
||||||
|
token,
|
||||||
|
&tc_reader,
|
||||||
|
&self.dummy_sender,
|
||||||
|
&self.verif_reporter,
|
||||||
|
time_stamp,
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
active_info.token().request_id(),
|
||||||
|
self.request_id().expect("no request id is set")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
active_info.target_id(),
|
||||||
|
UniqueApidTargetId::new(expected_apid, expected_apid_target).raw()
|
||||||
|
);
|
||||||
|
Ok((active_info, request))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TargetedPusRequestTestbench<
|
||||||
|
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||||
|
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||||
|
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||||
|
ActiveRequestInfo: ActiveRequest,
|
||||||
|
RequestType,
|
||||||
|
ReplyType,
|
||||||
|
> {
|
||||||
|
pub service: PusTargetedRequestService<
|
||||||
|
MpscTcReceiver,
|
||||||
|
TestVerificationReporter,
|
||||||
|
RequestConverter,
|
||||||
|
ReplyHandler,
|
||||||
|
ActiveRequestMapInstance,
|
||||||
|
ActiveRequestInfo,
|
||||||
|
RequestType,
|
||||||
|
ReplyType,
|
||||||
|
>,
|
||||||
|
pub request_id: Option<RequestId>,
|
||||||
|
pub tm_funnel_rx: mpsc::Receiver<PacketAsVec>,
|
||||||
|
pub pus_packet_tx: mpsc::Sender<EcssTcAndToken>,
|
||||||
|
pub reply_tx: mpsc::Sender<GenericMessage<ReplyType>>,
|
||||||
|
pub request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
use arbitrary_int::traits::Integer as _;
|
||||||
|
use arbitrary_int::u14;
|
||||||
|
use derive_new::new;
|
||||||
|
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 crate::tmtc::sender::TmTcSender;
|
||||||
|
use satrs::pus::verification::VerificationReporter;
|
||||||
|
use satrs::pus::{
|
||||||
|
DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher, MpscTcReceiver, PusPacketHandlingError,
|
||||||
|
PusServiceHelper,
|
||||||
|
};
|
||||||
|
use satrs::request::GenericMessage;
|
||||||
|
use satrs::{
|
||||||
|
mode::{ModeAndSubmode, ModeReply, ModeRequest},
|
||||||
|
pus::{
|
||||||
|
mode::Subservice,
|
||||||
|
verification::{
|
||||||
|
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReportingProvider,
|
||||||
|
VerificationToken,
|
||||||
|
},
|
||||||
|
ActivePusRequestStd, ActiveRequest, EcssTmSender, EcssTmtcError, GenericConversionError,
|
||||||
|
PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
|
||||||
|
},
|
||||||
|
request::UniqueApidTargetId,
|
||||||
|
spacepackets::{
|
||||||
|
ecss::{
|
||||||
|
tc::PusTcReader,
|
||||||
|
tm::{PusTmCreator, PusTmSecondaryHeader},
|
||||||
|
PusPacket,
|
||||||
|
},
|
||||||
|
SpHeader,
|
||||||
|
},
|
||||||
|
ComponentId,
|
||||||
|
};
|
||||||
|
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
|
||||||
|
PusTargetedRequestService, TargetedPusService,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(new)]
|
||||||
|
pub struct ModeReplyHandler {
|
||||||
|
owner_id: ComponentId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
|
||||||
|
type Error = EcssTmtcError;
|
||||||
|
|
||||||
|
fn handle_unrequested_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<ModeReply>,
|
||||||
|
_tm_sender: &impl EcssTmSender,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
log::warn!("received unexpected reply for mode service 5: {reply:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: &GenericMessage<ModeReply>,
|
||||||
|
active_request: &ActivePusRequestStd,
|
||||||
|
tm_sender: &impl EcssTmSender,
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid token state");
|
||||||
|
match reply.message {
|
||||||
|
ModeReply::ModeReply(mode_reply) => {
|
||||||
|
let mut source_data: [u8; 12] = [0; 12];
|
||||||
|
mode_reply
|
||||||
|
.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(), 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)?;
|
||||||
|
}
|
||||||
|
ModeReply::CantReachMode(error_code) => {
|
||||||
|
verification_handler.completion_failure(
|
||||||
|
tm_sender,
|
||||||
|
started_token,
|
||||||
|
FailParams::new(time_stamp, &error_code, &[]),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
ModeReply::WrongMode { expected, reached } => {
|
||||||
|
let mut error_info: [u8; 24] = [0; 24];
|
||||||
|
let mut written_len = expected
|
||||||
|
.write_to_be_bytes(&mut error_info[0..ModeAndSubmode::RAW_LEN])
|
||||||
|
.expect("writing expected mode failed");
|
||||||
|
written_len += reached
|
||||||
|
.write_to_be_bytes(&mut error_info[ModeAndSubmode::RAW_LEN..])
|
||||||
|
.expect("writing reached mode failed");
|
||||||
|
verification_handler.completion_failure(
|
||||||
|
tm_sender,
|
||||||
|
started_token,
|
||||||
|
FailParams::new(
|
||||||
|
time_stamp,
|
||||||
|
&mode_err::WRONG_MODE,
|
||||||
|
&error_info[..written_len],
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
ModeReply::ModeInfo(_mode_and_submode) => (),
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request_timeout(
|
||||||
|
&mut self,
|
||||||
|
active_request: &ActivePusRequestStd,
|
||||||
|
tm_sender: &impl EcssTmSender,
|
||||||
|
verification_handler: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
generic_pus_request_timeout_handler(
|
||||||
|
tm_sender,
|
||||||
|
active_request,
|
||||||
|
verification_handler,
|
||||||
|
time_stamp,
|
||||||
|
"HK",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ModeRequestConverter {}
|
||||||
|
|
||||||
|
impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestConverter {
|
||||||
|
type Error = GenericConversionError;
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&mut self,
|
||||||
|
token: VerificationToken<TcStateAccepted>,
|
||||||
|
tc: &PusTcReader,
|
||||||
|
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
verif_reporter: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) -> Result<(ActivePusRequestStd, ModeRequest), Self::Error> {
|
||||||
|
let subservice = tc.message_subtype_id();
|
||||||
|
let user_data = tc.user_data();
|
||||||
|
let not_enough_app_data = |expected: usize| {
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
Err(GenericConversionError::NotEnoughAppData {
|
||||||
|
expected,
|
||||||
|
found: user_data.len(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if user_data.len() < core::mem::size_of::<u32>() {
|
||||||
|
return not_enough_app_data(4);
|
||||||
|
}
|
||||||
|
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
|
||||||
|
let active_request =
|
||||||
|
ActivePusRequestStd::new(target_id_and_apid.into(), token, Duration::from_secs(30));
|
||||||
|
let subservice_typed = Subservice::try_from(subservice);
|
||||||
|
let invalid_subservice = || {
|
||||||
|
// Invalid subservice
|
||||||
|
verif_reporter
|
||||||
|
.start_failure(
|
||||||
|
tm_sender,
|
||||||
|
token,
|
||||||
|
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
Err(GenericConversionError::InvalidSubservice(subservice))
|
||||||
|
};
|
||||||
|
if subservice_typed.is_err() {
|
||||||
|
return invalid_subservice();
|
||||||
|
}
|
||||||
|
let subservice_typed = subservice_typed.unwrap();
|
||||||
|
match subservice_typed {
|
||||||
|
Subservice::TcSetMode => {
|
||||||
|
if user_data.len() < core::mem::size_of::<u32>() + ModeAndSubmode::RAW_LEN {
|
||||||
|
return not_enough_app_data(4 + ModeAndSubmode::RAW_LEN);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
forced: false,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Subservice::TcReadMode => Ok((active_request, ModeRequest::ReadMode)),
|
||||||
|
Subservice::TcAnnounceMode => Ok((active_request, ModeRequest::AnnounceMode)),
|
||||||
|
Subservice::TcAnnounceModeRecursive => {
|
||||||
|
Ok((active_request, ModeRequest::AnnounceModeRecursive))
|
||||||
|
}
|
||||||
|
_ => invalid_subservice(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let mode_request_handler = PusTargetedRequestService::new(
|
||||||
|
PusServiceHelper::new(
|
||||||
|
ids::generic_pus::PUS_MODE.id(),
|
||||||
|
pus_action_rx,
|
||||||
|
tm_sender,
|
||||||
|
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(ids::generic_pus::PUS_MODE.id()),
|
||||||
|
mode_router,
|
||||||
|
reply_receiver,
|
||||||
|
);
|
||||||
|
ModeServiceWrapper {
|
||||||
|
service: mode_request_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModeServiceWrapper {
|
||||||
|
pub(crate) service: PusTargetedRequestService<
|
||||||
|
MpscTcReceiver,
|
||||||
|
VerificationReporter,
|
||||||
|
ModeRequestConverter,
|
||||||
|
ModeReplyHandler,
|
||||||
|
DefaultActiveRequestMap<ActivePusRequestStd>,
|
||||||
|
ActivePusRequestStd,
|
||||||
|
ModeRequest,
|
||||||
|
ModeReply,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
request::GenericMessage,
|
||||||
|
spacepackets::{
|
||||||
|
ecss::tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||||
|
SpHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use example_std::config::tmtc_err;
|
||||||
|
|
||||||
|
use crate::pus::{
|
||||||
|
mode::ModeReplyHandler,
|
||||||
|
tests::{PusConverterTestbench, ReplyHandlerTestbench},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ModeRequestConverter;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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)
|
||||||
|
.expect("conversion has failed");
|
||||||
|
assert_eq!(req, ModeRequest::ReadMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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, 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,
|
||||||
|
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, 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.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)
|
||||||
|
.expect("conversion has failed");
|
||||||
|
assert_eq!(req, ModeRequest::AnnounceMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, 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.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)
|
||||||
|
.expect("conversion has failed");
|
||||||
|
assert_eq!(req, ModeRequest::AnnounceModeRecursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_unrequested_reply() {
|
||||||
|
let mut testbench = ReplyHandlerTestbench::new(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
|
||||||
|
);
|
||||||
|
let mode_reply = ModeReply::ModeReply(ModeAndSubmode::new(5, 1));
|
||||||
|
let unrequested_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);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_handling_reply_timeout() {
|
||||||
|
let mut testbench = ReplyHandlerTestbench::new(
|
||||||
|
TEST_COMPONENT_ID_0.id(),
|
||||||
|
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
|
||||||
|
);
|
||||||
|
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||||
|
let result = testbench.handle_request_timeout(&active_request, &[]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
testbench.verif_reporter.assert_completion_failure(
|
||||||
|
TEST_COMPONENT_ID_0.raw(),
|
||||||
|
req_id,
|
||||||
|
None,
|
||||||
|
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
use satrs::action::ActionRequest;
|
||||||
|
use satrs::hk::HkRequest;
|
||||||
|
use satrs::mode::ModeRequest;
|
||||||
|
use satrs::pus::verification::{
|
||||||
|
FailParams, TcStateAccepted, VerificationReportingProvider, VerificationToken,
|
||||||
|
};
|
||||||
|
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 example_std::config::tmtc_err;
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum CompositeRequest {
|
||||||
|
Hk(HkRequest),
|
||||||
|
Action(ActionRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::SyncSender<GenericMessage<CompositeRequest>>>,
|
||||||
|
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GenericRequestRouter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: ids::generic_pus::PUS_ROUTING.raw(),
|
||||||
|
composite_router_map: Default::default(),
|
||||||
|
mode_router_map: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl GenericRequestRouter {
|
||||||
|
pub(crate) fn handle_error_generic(
|
||||||
|
&self,
|
||||||
|
active_request: &impl ActiveRequest,
|
||||||
|
tc: &PusTcReader,
|
||||||
|
error: GenericRoutingError,
|
||||||
|
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||||
|
verif_reporter: &impl VerificationReportingProvider,
|
||||||
|
time_stamp: &[u8],
|
||||||
|
) {
|
||||||
|
warn!(
|
||||||
|
"Routing request for service {} failed: {error:?}",
|
||||||
|
tc.service_type_id()
|
||||||
|
);
|
||||||
|
let accepted_token: VerificationToken<TcStateAccepted> = active_request
|
||||||
|
.token()
|
||||||
|
.try_into()
|
||||||
|
.expect("token is not in accepted state");
|
||||||
|
match error {
|
||||||
|
GenericRoutingError::UnknownTargetId(id) => {
|
||||||
|
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; core::mem::size_of::<ComponentId>()] =
|
||||||
|
[0; core::mem::size_of::<ComponentId>()];
|
||||||
|
fail_data.copy_from_slice(&id.to_be_bytes());
|
||||||
|
verif_reporter
|
||||||
|
.completion_failure(
|
||||||
|
tm_sender,
|
||||||
|
accepted_token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::UNKNOWN_TARGET_ID, &fail_data),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
}
|
||||||
|
GenericRoutingError::Send(_) => {
|
||||||
|
let mut fail_data: [u8; 8] = [0; 8];
|
||||||
|
fail_data.copy_from_slice(&active_request.target_id().to_be_bytes());
|
||||||
|
verif_reporter
|
||||||
|
.completion_failure(
|
||||||
|
tm_sender,
|
||||||
|
accepted_token,
|
||||||
|
FailParams::new(time_stamp, &tmtc_err::ROUTING_ERROR, &fail_data),
|
||||||
|
)
|
||||||
|
.expect("Sending start failure failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PusRequestRouter<HkRequest> for GenericRequestRouter {
|
||||||
|
type Error = GenericRoutingError;
|
||||||
|
|
||||||
|
fn route(
|
||||||
|
&self,
|
||||||
|
requestor_info: MessageMetadata,
|
||||||
|
target_id: ComponentId,
|
||||||
|
hk_request: HkRequest,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if let Some(sender) = self.composite_router_map.get(&target_id) {
|
||||||
|
sender
|
||||||
|
.send(GenericMessage::new(
|
||||||
|
requestor_info,
|
||||||
|
CompositeRequest::Hk(hk_request),
|
||||||
|
))
|
||||||
|
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusRequestRouter<ActionRequest> for GenericRequestRouter {
|
||||||
|
type Error = GenericRoutingError;
|
||||||
|
|
||||||
|
fn route(
|
||||||
|
&self,
|
||||||
|
requestor_info: MessageMetadata,
|
||||||
|
target_id: ComponentId,
|
||||||
|
action_request: ActionRequest,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if let Some(sender) = self.composite_router_map.get(&target_id) {
|
||||||
|
sender
|
||||||
|
.send(GenericMessage::new(
|
||||||
|
requestor_info,
|
||||||
|
CompositeRequest::Action(action_request),
|
||||||
|
))
|
||||||
|
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
|
||||||
|
type Error = GenericRoutingError;
|
||||||
|
|
||||||
|
fn route(
|
||||||
|
&self,
|
||||||
|
requestor_info: MessageMetadata,
|
||||||
|
target_id: ComponentId,
|
||||||
|
request: ModeRequest,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if let Some(sender) = self.mode_router_map.get(&target_id) {
|
||||||
|
sender
|
||||||
|
.send(GenericMessage::new(requestor_info, request))
|
||||||
|
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
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> {
|
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()
|
fern::Dispatch::new()
|
||||||
.format(|out, message, record| {
|
.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
@@ -9,7 +18,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
|||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(log::LevelFilter::Debug)
|
.level(log_level)
|
||||||
.chain(std::io::stdout())
|
.chain(std::io::stdout())
|
||||||
.chain(fern::log_file("output.log")?)
|
.chain(fern::log_file("output.log")?)
|
||||||
.apply()?;
|
.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");
|
||||||
|
}
|
||||||
@@ -0,0 +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"/>
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use minisim_types::{SimReply, acs::mgm};
|
||||||
|
use nexosim::{
|
||||||
|
model::{Context, Model},
|
||||||
|
ports::Output,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use types::pcdu::SwitchStateBinary;
|
||||||
|
|
||||||
|
use crate::time::current_millis;
|
||||||
|
// Earth magnetic field varies between roughly -30 uT and 30 uT
|
||||||
|
const AMPLITUDE_MGM_UT: f32 = 30.0;
|
||||||
|
// Lets start with a simple frequency here.
|
||||||
|
const FREQUENCY_MGM: f32 = 1.0;
|
||||||
|
const PHASE_X: f32 = 0.0;
|
||||||
|
// Different phases to have different values on the other axes.
|
||||||
|
const PHASE_Y: f32 = 0.1;
|
||||||
|
const PHASE_Z: f32 = 0.2;
|
||||||
|
|
||||||
|
/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves.
|
||||||
|
///
|
||||||
|
/// An ideal sensor would sample the magnetic field at a high fixed rate. This might not be
|
||||||
|
/// possible for a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms)
|
||||||
|
/// might still be possible and is probably sufficient for many OBSW needs.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MgmModel {
|
||||||
|
id: mgm::Id,
|
||||||
|
switch_state: SwitchStateBinary,
|
||||||
|
external_mag_field: Option<mgm::SensorValuesMicroTesla>,
|
||||||
|
spi_fault: mgm::SpiFault,
|
||||||
|
pub reply: Output<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Model]
|
||||||
|
impl MgmModel {
|
||||||
|
pub fn new(mgm_id: mgm::Id) -> Self {
|
||||||
|
Self {
|
||||||
|
id: mgm_id,
|
||||||
|
switch_state: SwitchStateBinary::Off,
|
||||||
|
external_mag_field: None,
|
||||||
|
spi_fault: mgm::SpiFault::default(),
|
||||||
|
reply: Output::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||||
|
self.switch_state = switch_state;
|
||||||
|
if switch_state == SwitchStateBinary::Off && self.spi_fault.cleared_by_power_cycle {
|
||||||
|
self.spi_fault = mgm::SpiFault::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force (or clear) a stuck-bus SPI fault, for FDIR testing purposes.
|
||||||
|
pub async fn set_spi_fault(&mut self, fault: mgm::SpiFault) {
|
||||||
|
self.spi_fault = fault;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
|
||||||
|
let reply = SimReply::Mgm {
|
||||||
|
id: self.id,
|
||||||
|
reply: create_reply(
|
||||||
|
self.switch_state,
|
||||||
|
self.calculate_current_mgm_tuple(current_millis(cx.time())),
|
||||||
|
self.spi_fault.mode,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.reply.send(reply).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devices like magnetorquers generate a strong magnetic field which overrides the default
|
||||||
|
// model for the measured magnetic field.
|
||||||
|
pub async fn apply_external_magnetic_field(&mut self, field: mgm::SensorValuesMicroTesla) {
|
||||||
|
self.external_mag_field = Some(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_external_magnetic_field(&mut self, _: ()) {
|
||||||
|
self.external_mag_field = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> mgm::SensorValuesMicroTesla {
|
||||||
|
if SwitchStateBinary::On == self.switch_state {
|
||||||
|
if let Some(ext_field) = self.external_mag_field {
|
||||||
|
return ext_field;
|
||||||
|
}
|
||||||
|
let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0);
|
||||||
|
return mgm::SensorValuesMicroTesla {
|
||||||
|
x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
|
||||||
|
y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
|
||||||
|
z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mgm::SensorValuesMicroTesla {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the reply of the simulated LIS3MDL, including the raw register values.
|
||||||
|
fn create_reply(
|
||||||
|
switch_state: SwitchStateBinary,
|
||||||
|
sensor_values: mgm::SensorValuesMicroTesla,
|
||||||
|
fault_mode: mgm::SpiFaultMode,
|
||||||
|
) -> mgm::Reply {
|
||||||
|
// An injected fault always wins. A switched off device reads back like an undriven bus.
|
||||||
|
let raw = match (fault_mode, switch_state) {
|
||||||
|
(mgm::SpiFaultMode::AllZeros, _) => mgm::RawValues::splat(mgm::ALL_ZEROS_SENSOR_VAL),
|
||||||
|
(mgm::SpiFaultMode::AllOnes, _) | (mgm::SpiFaultMode::None, SwitchStateBinary::Off) => {
|
||||||
|
mgm::RawValues::splat(mgm::ALL_ONES_SENSOR_VAL)
|
||||||
|
}
|
||||||
|
(mgm::SpiFaultMode::None, SwitchStateBinary::On) => {
|
||||||
|
raw_values_from_microtesla(sensor_values)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mgm::Reply {
|
||||||
|
switch_state,
|
||||||
|
sensor_values,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_values_from_microtesla(values: mgm::SensorValuesMicroTesla) -> mgm::RawValues {
|
||||||
|
let to_raw = |microtesla: f32| {
|
||||||
|
(microtesla / (mgm::GAUSS_TO_MICROTESLA_FACTOR as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS))
|
||||||
|
.round() as i16
|
||||||
|
};
|
||||||
|
mgm::RawValues {
|
||||||
|
x: to_raw(values.x),
|
||||||
|
y: to_raw(values.y),
|
||||||
|
z: to_raw(values.z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use minisim_types::{SimReply, SimRequest, acs::mgm};
|
||||||
|
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
eps::tests::{switch_device_off, switch_device_on},
|
||||||
|
test_helpers::SimTestbench,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn request_sensor_data(sim_testbench: &mut SimTestbench, id: mgm::Id) -> mgm::Reply {
|
||||||
|
let sim_reply = sim_testbench
|
||||||
|
.request_reply(SimRequest::Mgm {
|
||||||
|
id,
|
||||||
|
request: mgm::Request::RequestSensorData,
|
||||||
|
})
|
||||||
|
.expect("no MGM reply received");
|
||||||
|
let SimReply::Mgm {
|
||||||
|
id: reply_id,
|
||||||
|
reply,
|
||||||
|
} = sim_reply
|
||||||
|
else {
|
||||||
|
panic!("unexpected reply {sim_reply:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(reply_id, id);
|
||||||
|
reply
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_spi_fault(sim_testbench: &mut SimTestbench, cleared_by_power_cycle: bool) {
|
||||||
|
sim_testbench.send_and_step(SimRequest::Mgm {
|
||||||
|
id: mgm::Id::Mgm0,
|
||||||
|
request: mgm::Request::SetSpiFault(mgm::SpiFault {
|
||||||
|
mode: mgm::SpiFaultMode::AllOnes,
|
||||||
|
cleared_by_power_cycle,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_stuck_bus_reply(reply: &mgm::Reply) -> bool {
|
||||||
|
reply.raw.x == -1 && reply.raw.y == -1 && reply.raw.z == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgm_request() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
assert_eq!(reply.switch_state, SwitchStateBinary::Off);
|
||||||
|
assert_eq!(reply.sensor_values.x, 0.0);
|
||||||
|
assert_eq!(reply.sensor_values.y, 0.0);
|
||||||
|
assert_eq!(reply.sensor_values.z, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_spi_fault_injection_all_ones() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
inject_spi_fault(&mut sim_testbench, false);
|
||||||
|
|
||||||
|
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
// Even though the device is switched on, the injected fault forces a stuck-bus reply.
|
||||||
|
assert_eq!(reply.switch_state, SwitchStateBinary::On);
|
||||||
|
assert!(is_stuck_bus_reply(&reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_spi_fault_cleared_by_power_cycle() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
inject_spi_fault(&mut sim_testbench, true);
|
||||||
|
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
assert!(is_stuck_bus_reply(&reply));
|
||||||
|
|
||||||
|
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
|
||||||
|
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
assert!(!is_stuck_bus_reply(&reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_spi_fault_persists_after_power_cycle() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
inject_spi_fault(&mut sim_testbench, false);
|
||||||
|
|
||||||
|
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
assert_eq!(reply.switch_state, SwitchStateBinary::On);
|
||||||
|
assert!(is_stuck_bus_reply(&reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgm_request_switched_on() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
|
||||||
|
let first_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
|
||||||
|
let second_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
|
||||||
|
let to_microtesla = |raw: i16| {
|
||||||
|
raw as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS * mgm::GAUSS_TO_MICROTESLA_FACTOR as f32
|
||||||
|
};
|
||||||
|
let values = second_reply.sensor_values;
|
||||||
|
let raw = second_reply.raw;
|
||||||
|
for (value, raw) in [(values.x, raw.x), (values.y, raw.y), (values.z, raw.z)] {
|
||||||
|
let diff = (value - to_microtesla(raw)).abs();
|
||||||
|
assert!(diff < 0.01, "raw value conversion diff too large: {diff}");
|
||||||
|
}
|
||||||
|
// Check that the values are changing.
|
||||||
|
assert_ne!(first_reply, second_reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_1_request_switched_on() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm1);
|
||||||
|
|
||||||
|
let mgm_0_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||||
|
assert_eq!(mgm_0_reply.switch_state, SwitchStateBinary::Off);
|
||||||
|
let mgm_1_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm1);
|
||||||
|
assert_eq!(mgm_1_reply.switch_state, SwitchStateBinary::On);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
use minisim_types::{
|
||||||
|
SimReply,
|
||||||
|
acs::{mgm, mgt},
|
||||||
|
};
|
||||||
|
use nexosim::{
|
||||||
|
model::{Context, Model, schedulable},
|
||||||
|
ports::Output,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
use types::pcdu::SwitchStateBinary;
|
||||||
|
|
||||||
|
/// Time the device needs to answer a command.
|
||||||
|
const REPLY_DELAY: Duration = Duration::from_millis(15);
|
||||||
|
|
||||||
|
/// Simple magnetorquer simulation model.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MgtModel {
|
||||||
|
switch_state: SwitchStateBinary,
|
||||||
|
torquing: bool,
|
||||||
|
torque_dipole: mgt::Dipole,
|
||||||
|
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
|
||||||
|
pub clear_magnetic_field: Output<()>,
|
||||||
|
pub reply: Output<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Model]
|
||||||
|
impl MgtModel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
switch_state: SwitchStateBinary::Off,
|
||||||
|
torquing: false,
|
||||||
|
torque_dipole: mgt::Dipole::default(),
|
||||||
|
gen_magnetic_field: Output::new(),
|
||||||
|
clear_magnetic_field: Output::new(),
|
||||||
|
reply: Output::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_torque(
|
||||||
|
&mut self,
|
||||||
|
duration_and_dipole: (Duration, mgt::Dipole),
|
||||||
|
cx: &Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.switch_state != SwitchStateBinary::On {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.torque_dipole = duration_and_dipole.1;
|
||||||
|
self.torquing = true;
|
||||||
|
if cx
|
||||||
|
.schedule_event(duration_and_dipole.0, schedulable!(Self::clear_torque), ())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
log::warn!("torque clearing can only be set for a future time.");
|
||||||
|
}
|
||||||
|
self.generate_magnetic_field(()).await;
|
||||||
|
self.schedule_reply(mgt::Reply::Ack, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[nexosim(schedulable)]
|
||||||
|
async fn clear_torque(&mut self) {
|
||||||
|
self.torque_dipole = mgt::Dipole::default();
|
||||||
|
self.torquing = false;
|
||||||
|
self.clear_magnetic_field.send(()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||||
|
self.switch_state = switch_state;
|
||||||
|
match switch_state {
|
||||||
|
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
|
||||||
|
SwitchStateBinary::Off => self.clear_torque().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_housekeeping_data(&mut self, _: (), cx: &Context<Self>) {
|
||||||
|
if self.switch_state != SwitchStateBinary::On {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The HK is sampled when the command is processed, not when the reply is sent.
|
||||||
|
let hk = mgt::HkSet {
|
||||||
|
dipole: self.torque_dipole,
|
||||||
|
torquing: self.torquing,
|
||||||
|
};
|
||||||
|
self.schedule_reply(mgt::Reply::Hk(hk), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_reply(&self, reply: mgt::Reply, cx: &Context<Self>) {
|
||||||
|
cx.schedule_event(REPLY_DELAY, schedulable!(Self::send_reply), reply)
|
||||||
|
.expect("scheduling MGT reply failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[nexosim(schedulable)]
|
||||||
|
async fn send_reply(&mut self, reply: mgt::Reply) {
|
||||||
|
self.reply.send(SimReply::from(reply)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
|
||||||
|
// Simplified model: Just returns some fixed magnetic field for now.
|
||||||
|
// Later, we could make this more fancy by incorporating the commanded dipole.
|
||||||
|
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A torquing magnetorquer generates a magnetic field. This function can be used to apply
|
||||||
|
/// the magnetic field.
|
||||||
|
async fn generate_magnetic_field(&mut self, _: ()) {
|
||||||
|
if self.switch_state != SwitchStateBinary::On || !self.torquing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.gen_magnetic_field
|
||||||
|
.send(self.calc_magnetic_field(self.torque_dipole))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use minisim_types::{
|
||||||
|
SimReply, SimRequest, SimRequestWithTime,
|
||||||
|
acs::{mgm, mgt},
|
||||||
|
eps::PcduRequest,
|
||||||
|
};
|
||||||
|
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||||
|
|
||||||
|
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
|
||||||
|
|
||||||
|
fn decode_reply(sim_reply: SimReply) -> mgt::Reply {
|
||||||
|
let SimReply::Mgt(frame) = sim_reply else {
|
||||||
|
panic!("unexpected reply {sim_reply:?}");
|
||||||
|
};
|
||||||
|
mgt::Reply::from_frame(&frame).expect("invalid MGT reply frame")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
|
||||||
|
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
|
||||||
|
let mgt::Reply::Hk(hk) = decode_reply(sim_reply) else {
|
||||||
|
panic!("unexpected MGT reply");
|
||||||
|
};
|
||||||
|
Some(hk)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgt_request_is_off() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
assert!(request_hk(&mut sim_testbench).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgt_request_is_on() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
|
assert_eq!(
|
||||||
|
request_hk(&mut sim_testbench),
|
||||||
|
Some(mgt::HkSet {
|
||||||
|
dipole: mgt::Dipole::default(),
|
||||||
|
torquing: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgt_request_is_on_and_torquing() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
|
let commanded_dipole = mgt::Dipole {
|
||||||
|
x: -200,
|
||||||
|
y: 200,
|
||||||
|
z: 1000,
|
||||||
|
};
|
||||||
|
let request = SimRequestWithTime::new_with_epoch_time(mgt::Request::ApplyTorque {
|
||||||
|
duration: Duration::from_millis(100),
|
||||||
|
dipole: commanded_dipole,
|
||||||
|
});
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGT request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step_until(Duration::from_millis(20)).unwrap();
|
||||||
|
let ack = sim_testbench
|
||||||
|
.try_receive_next_reply()
|
||||||
|
.expect("no torque command ack");
|
||||||
|
assert_eq!(decode_reply(ack), mgt::Reply::Ack);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request_hk(&mut sim_testbench),
|
||||||
|
Some(mgt::HkSet {
|
||||||
|
dipole: commanded_dipole,
|
||||||
|
torquing: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
sim_testbench
|
||||||
|
.step_until(Duration::from_millis(100))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request_hk(&mut sim_testbench),
|
||||||
|
Some(mgt::HkSet {
|
||||||
|
dipole: mgt::Dipole::default(),
|
||||||
|
torquing: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_torque_command_not_acked_when_off() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let reply = sim_testbench.request_reply(mgt::Request::ApplyTorque {
|
||||||
|
duration: Duration::from_millis(100),
|
||||||
|
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||||
|
});
|
||||||
|
assert!(reply.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_frame_is_dropped() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
|
assert!(
|
||||||
|
sim_testbench
|
||||||
|
.request_reply(SimRequest::Mgt(vec![0x01, 0x00]))
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the request without stepping, so scheduled events like the torque clearing do
|
||||||
|
/// not fire.
|
||||||
|
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
|
||||||
|
sim_testbench
|
||||||
|
.send_request(SimRequestWithTime::new_with_epoch_time(request))
|
||||||
|
.expect("sending request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_mgm_0_field(sim_testbench: &mut SimTestbench) -> mgm::SensorValuesMicroTesla {
|
||||||
|
process_without_step(
|
||||||
|
sim_testbench,
|
||||||
|
SimRequest::Mgm {
|
||||||
|
id: mgm::Id::Mgm0,
|
||||||
|
request: mgm::Request::RequestSensorData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Skips pending MGT replies, for example torque command acks.
|
||||||
|
loop {
|
||||||
|
let sim_reply = sim_testbench
|
||||||
|
.try_receive_next_reply()
|
||||||
|
.expect("no MGM reply received");
|
||||||
|
if let SimReply::Mgm { reply, .. } = sim_reply {
|
||||||
|
return reply.sensor_values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
|
||||||
|
switch_device_on(sim_testbench, SwitchId::Mgm0);
|
||||||
|
switch_device_on(sim_testbench, SwitchId::Mgt);
|
||||||
|
process_without_step(
|
||||||
|
sim_testbench,
|
||||||
|
mgt::Request::ApplyTorque {
|
||||||
|
duration,
|
||||||
|
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(read_mgm_0_field(sim_testbench), mgm::MGT_GEN_MAGNETIC_FIELD);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_field_cleared_after_torquing() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||||
|
sim_testbench
|
||||||
|
.step_until(Duration::from_millis(100))
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(
|
||||||
|
read_mgm_0_field(&mut sim_testbench),
|
||||||
|
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mgm_field_cleared_by_switching_mgt_off() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||||
|
process_without_step(
|
||||||
|
&mut sim_testbench,
|
||||||
|
PcduRequest::SwitchDevice {
|
||||||
|
switch: SwitchId::Mgt,
|
||||||
|
state: SwitchStateBinary::Off,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
read_mgm_0_field(&mut sim_testbench),
|
||||||
|
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod mgm;
|
||||||
|
pub mod mgt;
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
use std::{
|
||||||
|
sync::mpsc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use minisim_types::{
|
||||||
|
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
|
||||||
|
acs::{mgm, mgt},
|
||||||
|
eps::PcduRequest,
|
||||||
|
};
|
||||||
|
use nexosim::{
|
||||||
|
ports::{EventQueueReader, EventSinkReader, EventSource, SinkState, event_queue},
|
||||||
|
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
|
||||||
|
time::{Clock, Deadline, MonotonicTime, SystemClock},
|
||||||
|
};
|
||||||
|
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
acs::{mgm::MgmModel, mgt::MgtModel},
|
||||||
|
eps::PcduModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const WARNING_FOR_STALE_DATA: bool = false;
|
||||||
|
|
||||||
|
const SIM_CTRL_REQ_WIRETAPPING: bool = false;
|
||||||
|
const MGM_REQ_WIRETAPPING: bool = false;
|
||||||
|
const PCDU_REQ_WIRETAPPING: bool = false;
|
||||||
|
const MGT_REQ_WIRETAPPING: bool = false;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ThreadingModel {
|
||||||
|
Default = 0,
|
||||||
|
Single = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MgmInputs {
|
||||||
|
send_sensor_values: EventId<()>,
|
||||||
|
set_spi_fault: EventId<mgm::SpiFault>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MgmInputs {
|
||||||
|
fn register(sim_init: &mut SimInit, mailbox: &Mailbox<MgmModel>) -> Self {
|
||||||
|
Self {
|
||||||
|
send_sensor_values: EventSource::new()
|
||||||
|
.connect(MgmModel::send_sensor_values, mailbox)
|
||||||
|
.register(sim_init),
|
||||||
|
set_spi_fault: EventSource::new()
|
||||||
|
.connect(MgmModel::set_spi_fault, mailbox)
|
||||||
|
.register(sim_init),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Model inputs which are driven by simulation requests.
|
||||||
|
struct ModelInputs {
|
||||||
|
mgm_0: MgmInputs,
|
||||||
|
mgm_1: MgmInputs,
|
||||||
|
pcdu_request_switch_info: EventId<()>,
|
||||||
|
pcdu_switch_device: EventId<(SwitchId, SwitchStateBinary)>,
|
||||||
|
mgt_apply_torque: EventId<(Duration, mgt::Dipole)>,
|
||||||
|
mgt_request_hk: EventId<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The simulation controller processes requests and drives the simulation.
|
||||||
|
pub struct SimController {
|
||||||
|
sys_clock: SystemClock,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
simulation: Simulation,
|
||||||
|
inputs: ModelInputs,
|
||||||
|
model_replies: EventQueueReader<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimController {
|
||||||
|
pub fn new(
|
||||||
|
threading_model: ThreadingModel,
|
||||||
|
start_time: MonotonicTime,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||||
|
) -> Self {
|
||||||
|
let mut mgm_0_model = MgmModel::new(mgm::Id::Mgm0);
|
||||||
|
let mut mgm_1_model = MgmModel::new(mgm::Id::Mgm1);
|
||||||
|
let mut pcdu_model = PcduModel::new();
|
||||||
|
let mut mgt_model = MgtModel::new();
|
||||||
|
|
||||||
|
let mgm_0_mailbox = Mailbox::new();
|
||||||
|
let mgm_1_mailbox = Mailbox::new();
|
||||||
|
let pcdu_mailbox = Mailbox::new();
|
||||||
|
let mgt_mailbox = Mailbox::new();
|
||||||
|
|
||||||
|
pcdu_model
|
||||||
|
.mgm_0_switch
|
||||||
|
.connect(MgmModel::switch_device, &mgm_0_mailbox);
|
||||||
|
pcdu_model
|
||||||
|
.mgm_1_switch
|
||||||
|
.connect(MgmModel::switch_device, &mgm_1_mailbox);
|
||||||
|
pcdu_model
|
||||||
|
.mgt_switch
|
||||||
|
.connect(MgtModel::switch_device, &mgt_mailbox);
|
||||||
|
mgt_model
|
||||||
|
.gen_magnetic_field
|
||||||
|
.connect(MgmModel::apply_external_magnetic_field, &mgm_0_mailbox);
|
||||||
|
mgt_model
|
||||||
|
.gen_magnetic_field
|
||||||
|
.connect(MgmModel::apply_external_magnetic_field, &mgm_1_mailbox);
|
||||||
|
mgt_model
|
||||||
|
.clear_magnetic_field
|
||||||
|
.connect(MgmModel::clear_external_magnetic_field, &mgm_0_mailbox);
|
||||||
|
mgt_model
|
||||||
|
.clear_magnetic_field
|
||||||
|
.connect(MgmModel::clear_external_magnetic_field, &mgm_1_mailbox);
|
||||||
|
|
||||||
|
let (reply_sink, model_replies) = event_queue(SinkState::Enabled);
|
||||||
|
mgm_0_model.reply.connect_sink(reply_sink.clone());
|
||||||
|
mgm_1_model.reply.connect_sink(reply_sink.clone());
|
||||||
|
pcdu_model.reply.connect_sink(reply_sink.clone());
|
||||||
|
mgt_model.reply.connect_sink(reply_sink);
|
||||||
|
|
||||||
|
let mut sim_init = if threading_model == ThreadingModel::Single {
|
||||||
|
SimInit::with_num_threads(1)
|
||||||
|
} else {
|
||||||
|
SimInit::new()
|
||||||
|
};
|
||||||
|
let inputs = ModelInputs {
|
||||||
|
mgm_0: MgmInputs::register(&mut sim_init, &mgm_0_mailbox),
|
||||||
|
mgm_1: MgmInputs::register(&mut sim_init, &mgm_1_mailbox),
|
||||||
|
pcdu_request_switch_info: EventSource::new()
|
||||||
|
.connect(PcduModel::request_switch_info, &pcdu_mailbox)
|
||||||
|
.register(&mut sim_init),
|
||||||
|
pcdu_switch_device: EventSource::new()
|
||||||
|
.connect(PcduModel::switch_device, &pcdu_mailbox)
|
||||||
|
.register(&mut sim_init),
|
||||||
|
mgt_apply_torque: EventSource::new()
|
||||||
|
.connect(MgtModel::apply_torque, &mgt_mailbox)
|
||||||
|
.register(&mut sim_init),
|
||||||
|
mgt_request_hk: EventSource::new()
|
||||||
|
.connect(MgtModel::request_housekeeping_data, &mgt_mailbox)
|
||||||
|
.register(&mut sim_init),
|
||||||
|
};
|
||||||
|
let simulation = sim_init
|
||||||
|
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
|
||||||
|
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
|
||||||
|
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
|
||||||
|
.add_model(mgt_model, mgt_mailbox, "MGT model")
|
||||||
|
.init(start_time)
|
||||||
|
.unwrap();
|
||||||
|
Self {
|
||||||
|
sys_clock: SystemClock::from_system_time(start_time, SystemTime::now()),
|
||||||
|
request_receiver,
|
||||||
|
reply_sender,
|
||||||
|
simulation,
|
||||||
|
inputs,
|
||||||
|
model_replies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn step(&mut self) -> Result<(), ExecutionError> {
|
||||||
|
self.simulation.step()?;
|
||||||
|
self.forward_model_replies();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_until(&mut self, deadline: impl Deadline) -> Result<(), ExecutionError> {
|
||||||
|
self.simulation.step_until(deadline)?;
|
||||||
|
self.forward_model_replies();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forward_model_replies(&mut self) {
|
||||||
|
while let Some(reply) = self.model_replies.try_read() {
|
||||||
|
self.reply_sender
|
||||||
|
.send(reply)
|
||||||
|
.expect("sending model reply failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self, start_time: MonotonicTime, udp_polling_interval_ms: u64) {
|
||||||
|
let mut t = start_time;
|
||||||
|
loop {
|
||||||
|
let t_old = t;
|
||||||
|
// Check for UDP requests every millisecond. Shift the simulator ahead here to prevent
|
||||||
|
// replies lying in the past.
|
||||||
|
t += Duration::from_millis(udp_polling_interval_ms);
|
||||||
|
let _synch_status = self.sys_clock.synchronize(t);
|
||||||
|
self.handle_sim_requests(t_old);
|
||||||
|
self.step_until(t).expect("simulation step failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime) {
|
||||||
|
loop {
|
||||||
|
match self.request_receiver.try_recv() {
|
||||||
|
Ok(request) => {
|
||||||
|
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
|
||||||
|
log::warn!("stale data with timestamp {:?} received", request.timestamp);
|
||||||
|
}
|
||||||
|
match request.request {
|
||||||
|
SimRequest::SimCtrl(request) => self.handle_ctrl_request(request),
|
||||||
|
SimRequest::Mgm { id, request } => self.handle_mgm_request(id, request),
|
||||||
|
SimRequest::Mgt(request) => self.handle_mgt_request(request),
|
||||||
|
SimRequest::Pcdu(request) => self.handle_pcdu_request(request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => break,
|
||||||
|
mpsc::TryRecvError::Disconnected => {
|
||||||
|
panic!("all request sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.forward_model_replies();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
|
||||||
|
if SIM_CTRL_REQ_WIRETAPPING {
|
||||||
|
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
|
||||||
|
}
|
||||||
|
match sim_ctrl_request {
|
||||||
|
SimCtrlRequest::Ping => {
|
||||||
|
log::info!("received ping request, a client is connecting");
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::from(SimCtrlReply::Pong))
|
||||||
|
.expect("sending reply from sim controller failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgm_request(&mut self, mgm_id: mgm::Id, mgm_request: mgm::Request) {
|
||||||
|
let inputs = match mgm_id {
|
||||||
|
mgm::Id::Mgm0 => &self.inputs.mgm_0,
|
||||||
|
mgm::Id::Mgm1 => &self.inputs.mgm_1,
|
||||||
|
};
|
||||||
|
if MGM_REQ_WIRETAPPING {
|
||||||
|
log::info!("received {mgm_id:?} request: {mgm_request:?}");
|
||||||
|
}
|
||||||
|
match mgm_request {
|
||||||
|
mgm::Request::RequestSensorData => {
|
||||||
|
self.simulation
|
||||||
|
.process_event(&inputs.send_sensor_values, ())
|
||||||
|
.expect("event execution error for mgm");
|
||||||
|
}
|
||||||
|
mgm::Request::SetSpiFault(fault_mode) => {
|
||||||
|
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
|
||||||
|
self.simulation
|
||||||
|
.process_event(&inputs.set_spi_fault, fault_mode)
|
||||||
|
.expect("event execution error for mgm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_pcdu_request(&mut self, pcdu_request: PcduRequest) {
|
||||||
|
if PCDU_REQ_WIRETAPPING {
|
||||||
|
log::info!("received PCDU request: {pcdu_request:?}");
|
||||||
|
}
|
||||||
|
match pcdu_request {
|
||||||
|
PcduRequest::RequestSwitchInfo => {
|
||||||
|
self.simulation
|
||||||
|
.process_event(&self.inputs.pcdu_request_switch_info, ())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
PcduRequest::SwitchDevice { switch, state } => {
|
||||||
|
self.simulation
|
||||||
|
.process_event(&self.inputs.pcdu_switch_device, (switch, state))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgt_request(&mut self, frame: Vec<u8>) {
|
||||||
|
let mgt_request = match mgt::Request::from_frame(&frame) {
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("dropping invalid MGT frame {frame:02x?}: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if MGT_REQ_WIRETAPPING {
|
||||||
|
log::info!("received MGT request: {mgt_request:?}");
|
||||||
|
}
|
||||||
|
match mgt_request {
|
||||||
|
mgt::Request::ApplyTorque { duration, dipole } => self
|
||||||
|
.simulation
|
||||||
|
.process_event(&self.inputs.mgt_apply_torque, (duration, dipole))
|
||||||
|
.unwrap(),
|
||||||
|
mgt::Request::RequestHk => self
|
||||||
|
.simulation
|
||||||
|
.process_event(&self.inputs.mgt_request_hk, ())
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_ping() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
assert_eq!(
|
||||||
|
sim_testbench.request_reply(SimCtrlRequest::Ping),
|
||||||
|
Some(SimReply::SimCtrl(SimCtrlReply::Pong))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use minisim_types::{SimReply, eps::PcduReply};
|
||||||
|
use nexosim::{
|
||||||
|
model::{Context, Model, schedulable},
|
||||||
|
ports::Output,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
|
||||||
|
|
||||||
|
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct PcduModel {
|
||||||
|
switcher_map: SwitchMapBinary,
|
||||||
|
pub mgm_0_switch: Output<SwitchStateBinary>,
|
||||||
|
pub mgm_1_switch: Output<SwitchStateBinary>,
|
||||||
|
pub mgt_switch: Output<SwitchStateBinary>,
|
||||||
|
pub reply: Output<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Model]
|
||||||
|
impl PcduModel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
switcher_map: SwitchMapBinaryWrapper::default().0,
|
||||||
|
mgm_0_switch: Output::new(),
|
||||||
|
mgm_1_switch: Output::new(),
|
||||||
|
mgt_switch: Output::new(),
|
||||||
|
reply: Output::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_switch_info(&mut self, _: (), cx: &Context<Self>) {
|
||||||
|
cx.schedule_event(
|
||||||
|
Duration::from_millis(SWITCH_INFO_DELAY_MS),
|
||||||
|
schedulable!(Self::send_switch_info),
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.expect("requesting switch info failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[nexosim(schedulable)]
|
||||||
|
async fn send_switch_info(&mut self) {
|
||||||
|
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.clone()));
|
||||||
|
self.reply.send(reply).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
|
||||||
|
log::info!(
|
||||||
|
"switching {:?} to {:?}",
|
||||||
|
switch_and_target_state.0,
|
||||||
|
switch_and_target_state.1
|
||||||
|
);
|
||||||
|
let val = self
|
||||||
|
.switcher_map
|
||||||
|
.get_mut(&switch_and_target_state.0)
|
||||||
|
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
|
||||||
|
*val = switch_and_target_state.1;
|
||||||
|
match switch_and_target_state.0 {
|
||||||
|
SwitchId::Mgm0 => {
|
||||||
|
self.mgm_0_switch.send(switch_and_target_state.1).await;
|
||||||
|
}
|
||||||
|
SwitchId::Mgm1 => {
|
||||||
|
self.mgm_1_switch.send(switch_and_target_state.1).await;
|
||||||
|
}
|
||||||
|
SwitchId::Mgt => {
|
||||||
|
self.mgt_switch.send(switch_and_target_state.1).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use minisim_types::{SimRequestWithTime, eps::PcduRequest};
|
||||||
|
use types::pcdu::SwitchMapBinary;
|
||||||
|
|
||||||
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
|
fn switch_device(
|
||||||
|
sim_testbench: &mut SimTestbench,
|
||||||
|
switch: SwitchId,
|
||||||
|
target: SwitchStateBinary,
|
||||||
|
) {
|
||||||
|
sim_testbench.send_and_step(PcduRequest::SwitchDevice {
|
||||||
|
switch,
|
||||||
|
state: target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||||
|
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
|
||||||
|
}
|
||||||
|
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||||
|
switch_device(sim_testbench, switch, SwitchStateBinary::On);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_all_off_switch_map() -> SwitchMapBinary {
|
||||||
|
SwitchMapBinaryWrapper::default().0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unwrap_switch_map(sim_reply: SimReply) -> SwitchMapBinary {
|
||||||
|
let SimReply::Pcdu(PcduReply::SwitchInfo(switch_map)) = sim_reply else {
|
||||||
|
panic!("unexpected reply {sim_reply:?}");
|
||||||
|
};
|
||||||
|
switch_map
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
|
||||||
|
let sim_reply = sim_testbench
|
||||||
|
.request_reply(PcduRequest::RequestSwitchInfo)
|
||||||
|
.expect("no PCDU reply received");
|
||||||
|
assert_eq!(unwrap_switch_map(sim_reply), *expected_switch_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device(&mut sim_testbench, switch, target);
|
||||||
|
let mut switcher_map = get_all_off_switch_map();
|
||||||
|
*switcher_map.get_mut(&switch).unwrap() = target;
|
||||||
|
check_switch_state(&mut sim_testbench, &switcher_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pcdu_switcher_request() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending PCDU request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
|
||||||
|
assert!(sim_testbench.try_receive_next_reply().is_none());
|
||||||
|
|
||||||
|
// The reply is delayed by SWITCH_INFO_DELAY_MS.
|
||||||
|
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
|
||||||
|
let sim_reply = sim_testbench
|
||||||
|
.try_receive_next_reply()
|
||||||
|
.expect("no PCDU reply received");
|
||||||
|
assert_eq!(unwrap_switch_map(sim_reply), get_all_off_switch_map());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pcdu_switching_mgm_on() {
|
||||||
|
test_pcdu_switching_single_switch(SwitchId::Mgm0, SwitchStateBinary::On);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pcdu_switching_mgt_on() {
|
||||||
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pcdu_switching_mgt_off() {
|
||||||
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||||
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::Off);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
use controller::{SimController, ThreadingModel};
|
||||||
|
use minisim_types::udp::SIM_CTRL_PORT;
|
||||||
|
use nexosim::time::MonotonicTime;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use udp::SimUdpServer;
|
||||||
|
|
||||||
|
mod acs;
|
||||||
|
mod controller;
|
||||||
|
mod eps;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_helpers;
|
||||||
|
mod time;
|
||||||
|
mod udp;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let (request_sender, request_receiver) = mpsc::channel();
|
||||||
|
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||||
|
let t0 = MonotonicTime::EPOCH;
|
||||||
|
let mut sim_ctrl =
|
||||||
|
SimController::new(ThreadingModel::Default, t0, reply_sender, request_receiver);
|
||||||
|
// Configure logger at runtime
|
||||||
|
fern::Dispatch::new()
|
||||||
|
// Perform allocation-free log formatting
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{} {} {}] {}",
|
||||||
|
humantime::format_rfc3339(std::time::SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
// Add blanket level filter -
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
// - and per-module overrides
|
||||||
|
// Output to stdout, files, and other Dispatch configurations
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log").expect("could not open log output file"))
|
||||||
|
// Apply globally
|
||||||
|
.apply()
|
||||||
|
.expect("could not apply logger configuration");
|
||||||
|
|
||||||
|
log::info!("starting simulation thread");
|
||||||
|
// This thread schedules the simulator.
|
||||||
|
let sim_thread = thread::spawn(move || {
|
||||||
|
sim_ctrl.run(t0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut udp_server =
|
||||||
|
SimUdpServer::new(SIM_CTRL_PORT, request_sender, reply_receiver, 200, None)
|
||||||
|
.expect("could not create UDP request server");
|
||||||
|
log::info!("starting UDP server on port {SIM_CTRL_PORT}");
|
||||||
|
// This thread manages the simulator UDP server.
|
||||||
|
let udp_tc_thread = thread::spawn(move || {
|
||||||
|
udp_server.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
sim_thread.join().expect("joining simulation thread failed");
|
||||||
|
udp_tc_thread
|
||||||
|
.join()
|
||||||
|
.expect("joining UDP server thread failed");
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use delegate::delegate;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
|
||||||
|
use nexosim::{
|
||||||
|
simulation::ExecutionError,
|
||||||
|
time::{Deadline, MonotonicTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::controller::{SimController, ThreadingModel};
|
||||||
|
|
||||||
|
pub struct SimTestbench {
|
||||||
|
pub sim_controller: SimController,
|
||||||
|
pub reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
pub request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimTestbench {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (request_sender, request_receiver) = mpsc::channel();
|
||||||
|
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||||
|
let t0 = MonotonicTime::EPOCH;
|
||||||
|
let sim_ctrl =
|
||||||
|
SimController::new(ThreadingModel::Single, t0, reply_sender, request_receiver);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sim_controller: sim_ctrl,
|
||||||
|
reply_receiver,
|
||||||
|
request_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn handle_sim_requests_time_agnostic(&mut self) {
|
||||||
|
self.handle_sim_requests(MonotonicTime::EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate! {
|
||||||
|
to self.sim_controller {
|
||||||
|
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime);
|
||||||
|
pub fn step(&mut self) -> Result<(), ExecutionError>;
|
||||||
|
pub fn step_until(&mut self, duration: impl Deadline) -> Result<(), ExecutionError>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_request(
|
||||||
|
&self,
|
||||||
|
request: SimRequestWithTime,
|
||||||
|
) -> Result<(), mpsc::SendError<SimRequestWithTime>> {
|
||||||
|
self.request_sender.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the request and steps the simulation to the next scheduled event.
|
||||||
|
pub fn send_and_step(&mut self, request: impl Into<SimRequest>) {
|
||||||
|
self.send_request(SimRequestWithTime::new_with_epoch_time(request))
|
||||||
|
.expect("sending request failed");
|
||||||
|
self.handle_sim_requests_time_agnostic();
|
||||||
|
self.step().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_reply(&mut self, request: impl Into<SimRequest>) -> Option<SimReply> {
|
||||||
|
self.send_and_step(request);
|
||||||
|
self.try_receive_next_reply()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_receive_next_reply(&self) -> Option<SimReply> {
|
||||||
|
match self.reply_receiver.try_recv() {
|
||||||
|
Ok(reply) => Some(reply),
|
||||||
|
Err(e) => {
|
||||||
|
if e == mpsc::TryRecvError::Empty {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
panic!("reply_receiver disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use nexosim::time::MonotonicTime;
|
||||||
|
|
||||||
|
pub fn current_millis(time: MonotonicTime) -> u64 {
|
||||||
|
(time.as_secs() as u64 * 1000) + (time.subsec_nanos() as u64 / 1_000_000)
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::ErrorKind,
|
||||||
|
net::{SocketAddr, UdpSocket},
|
||||||
|
sync::{Arc, atomic::AtomicBool, mpsc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use minisim_types::{SimReply, SimRequestWithTime};
|
||||||
|
|
||||||
|
// A UDP server which handles all TC received by a client application.
|
||||||
|
pub struct SimUdpServer {
|
||||||
|
socket: UdpSocket,
|
||||||
|
request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||||
|
reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
reply_queue: VecDeque<SimReply>,
|
||||||
|
max_num_replies: usize,
|
||||||
|
// Stop signal to stop the server. Required for unittests and useful to allow clean shutdown
|
||||||
|
// of the application.
|
||||||
|
stop_signal: Option<Arc<AtomicBool>>,
|
||||||
|
idle_sleep_period_ms: u64,
|
||||||
|
req_buf: [u8; 4096],
|
||||||
|
sender_addr: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimUdpServer {
|
||||||
|
pub fn new(
|
||||||
|
local_port: u16,
|
||||||
|
request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||||
|
reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
max_num_replies: usize,
|
||||||
|
stop_signal: Option<Arc<AtomicBool>>,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
|
let socket = UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], local_port)))?;
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
Ok(Self {
|
||||||
|
socket,
|
||||||
|
request_sender,
|
||||||
|
reply_receiver,
|
||||||
|
reply_queue: VecDeque::new(),
|
||||||
|
max_num_replies,
|
||||||
|
stop_signal,
|
||||||
|
idle_sleep_period_ms: 3,
|
||||||
|
req_buf: [0; 4096],
|
||||||
|
sender_addr: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn server_addr(&self) -> std::io::Result<SocketAddr> {
|
||||||
|
self.socket.local_addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self) {
|
||||||
|
loop {
|
||||||
|
if let Some(stop_signal) = &self.stop_signal
|
||||||
|
&& stop_signal.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let processed_requests = self.process_requests();
|
||||||
|
let processed_replies = self.process_replies();
|
||||||
|
let sent_replies = self.send_replies();
|
||||||
|
// Sleep for a bit if there is nothing to do to prevent burning CPU cycles. Delay
|
||||||
|
// should be kept short to ensure responsiveness of the system.
|
||||||
|
if !processed_requests && !processed_replies && !sent_replies {
|
||||||
|
std::thread::sleep(Duration::from_millis(self.idle_sleep_period_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_requests(&mut self) -> bool {
|
||||||
|
let mut processed_requests = false;
|
||||||
|
loop {
|
||||||
|
// Blocks for a certain amount of time until data is received to allow doing periodic
|
||||||
|
// work like checking the stop signal.
|
||||||
|
let (bytes_read, src) = match self.socket.recv_from(&mut self.req_buf) {
|
||||||
|
Ok((bytes_read, src)) => (bytes_read, src),
|
||||||
|
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||||
|
// Continue to perform regular checks like the stop signal.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Handle unexpected errors (e.g., socket closed) here.
|
||||||
|
log::error!("unexpected request server error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sender_addr = Some(src);
|
||||||
|
|
||||||
|
let sim_req = postcard::from_bytes::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
|
||||||
|
if let Err(e) = sim_req {
|
||||||
|
log::warn!("received UDP request with invalid format: {}", e);
|
||||||
|
return processed_requests;
|
||||||
|
}
|
||||||
|
self.request_sender.send(sim_req.unwrap()).unwrap();
|
||||||
|
processed_requests = true;
|
||||||
|
}
|
||||||
|
processed_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_replies(&mut self) -> bool {
|
||||||
|
let mut processed_replies = false;
|
||||||
|
loop {
|
||||||
|
match self.reply_receiver.try_recv() {
|
||||||
|
Ok(reply) => {
|
||||||
|
if self.reply_queue.len() >= self.max_num_replies {
|
||||||
|
self.reply_queue.pop_front();
|
||||||
|
}
|
||||||
|
self.reply_queue.push_back(reply);
|
||||||
|
processed_replies = true;
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => return processed_replies,
|
||||||
|
mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::error!("all UDP reply senders disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_replies(&mut self) -> bool {
|
||||||
|
if self.sender_addr.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut sent_replies = false;
|
||||||
|
while !self.reply_queue.is_empty() {
|
||||||
|
let next_reply_to_send = self.reply_queue.pop_front().unwrap();
|
||||||
|
self.socket
|
||||||
|
.send_to(
|
||||||
|
&postcard::to_allocvec(&next_reply_to_send).unwrap(),
|
||||||
|
self.sender_addr.unwrap(),
|
||||||
|
)
|
||||||
|
.expect("sending reply failed");
|
||||||
|
sent_replies = true;
|
||||||
|
}
|
||||||
|
sent_replies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
io::ErrorKind,
|
||||||
|
net::{SocketAddr, UdpSocket},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use minisim_types::{
|
||||||
|
SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime,
|
||||||
|
eps::{PcduReply, PcduRequest},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::eps::tests::get_all_off_switch_map;
|
||||||
|
use delegate::delegate;
|
||||||
|
|
||||||
|
use super::SimUdpServer;
|
||||||
|
|
||||||
|
// Wait time to ensure even possibly laggy systems like CI servers can run the tests.
|
||||||
|
const SERVER_WAIT_TIME_MS: u64 = 50;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ReceptionError {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("postcard error: {0}")]
|
||||||
|
Postcard(#[from] postcard::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SimUdpTestClient {
|
||||||
|
socket: UdpSocket,
|
||||||
|
pub reply_buf: [u8; 4096],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimUdpTestClient {
|
||||||
|
pub fn new(
|
||||||
|
server_addr: &SocketAddr,
|
||||||
|
non_blocking: bool,
|
||||||
|
read_timeot_ms: Option<u64>,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
|
let socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||||
|
socket.set_nonblocking(non_blocking)?;
|
||||||
|
socket
|
||||||
|
.connect(server_addr)
|
||||||
|
.expect("could not connect to server addr");
|
||||||
|
if let Some(read_timeout) = read_timeot_ms {
|
||||||
|
// Set a read timeout so the test does not hang on failures.
|
||||||
|
socket.set_read_timeout(Some(Duration::from_millis(read_timeout)))?;
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
socket,
|
||||||
|
reply_buf: [0; 4096],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize> {
|
||||||
|
self.socket.send(
|
||||||
|
&postcard::to_allocvec(sim_request)
|
||||||
|
.expect("conversion of request to vector failed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv_raw(&mut self) -> std::io::Result<usize> {
|
||||||
|
self.socket.recv(&mut self.reply_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
|
||||||
|
let read_len = self.recv_raw()?;
|
||||||
|
Ok(postcard::from_bytes(&self.reply_buf[0..read_len])?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct UdpTestbench {
|
||||||
|
client: SimUdpTestClient,
|
||||||
|
stop_signal: Arc<AtomicBool>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpTestbench {
|
||||||
|
pub fn new(
|
||||||
|
client_non_blocking: bool,
|
||||||
|
client_read_timeout_ms: Option<u64>,
|
||||||
|
max_num_replies: usize,
|
||||||
|
) -> std::io::Result<(Self, SimUdpServer)> {
|
||||||
|
let (request_sender, request_receiver) = mpsc::channel();
|
||||||
|
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||||
|
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let server = SimUdpServer::new(
|
||||||
|
0,
|
||||||
|
request_sender,
|
||||||
|
reply_receiver,
|
||||||
|
max_num_replies,
|
||||||
|
Some(stop_signal.clone()),
|
||||||
|
)?;
|
||||||
|
let server_addr = server.server_addr()?;
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
client: SimUdpTestClient::new(
|
||||||
|
&server_addr,
|
||||||
|
client_non_blocking,
|
||||||
|
client_read_timeout_ms,
|
||||||
|
)?,
|
||||||
|
stop_signal,
|
||||||
|
request_receiver,
|
||||||
|
reply_sender,
|
||||||
|
},
|
||||||
|
server,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_recv_request(&self) -> Result<SimRequestWithTime, mpsc::TryRecvError> {
|
||||||
|
self.request_receiver.try_recv()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.stop_signal.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_reply(&self, sim_reply: &SimReply) {
|
||||||
|
self.reply_sender
|
||||||
|
.send(sim_reply.clone())
|
||||||
|
.expect("sending sim reply failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate! {
|
||||||
|
to self.client {
|
||||||
|
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize>;
|
||||||
|
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_no_sim_reply_available(&mut self) {
|
||||||
|
if let Err(ReceptionError::Io(ref io_error)) = self.recv_sim_reply() {
|
||||||
|
if io_error.kind() == ErrorKind::WouldBlock {
|
||||||
|
// Continue to perform regular checks like the stop signal.
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Handle unexpected errors (e.g., socket closed) here.
|
||||||
|
panic!("unexpected request server error: {io_error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("unexpected reply available");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_next_sim_reply(&mut self, expected_reply: &SimReply) {
|
||||||
|
match self.recv_sim_reply() {
|
||||||
|
Ok(received_sim_reply) => assert_eq!(expected_reply, &received_sim_reply),
|
||||||
|
Err(e) => match e {
|
||||||
|
ReceptionError::Io(ref io_error) => {
|
||||||
|
if io_error.kind() == ErrorKind::WouldBlock {
|
||||||
|
// Continue to perform regular checks like the stop signal.
|
||||||
|
panic!("no simulation reply received");
|
||||||
|
} else {
|
||||||
|
// Handle unexpected errors (e.g., socket closed) here.
|
||||||
|
panic!("unexpected request server error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReceptionError::Postcard(postcard_error) => {
|
||||||
|
panic!("unexpected postcard error: {postcard_error}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_basic_udp_request_reception() {
|
||||||
|
let (udp_testbench, mut udp_server) =
|
||||||
|
UdpTestbench::new(true, Some(SERVER_WAIT_TIME_MS), 10)
|
||||||
|
.expect("could not create testbench");
|
||||||
|
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||||
|
let sim_request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||||
|
udp_testbench
|
||||||
|
.send_request(&sim_request)
|
||||||
|
.expect("sending request failed");
|
||||||
|
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||||
|
// Check that the sim request has arrives and was forwarded.
|
||||||
|
let received_sim_request = udp_testbench
|
||||||
|
.try_recv_request()
|
||||||
|
.expect("did not receive request");
|
||||||
|
assert_eq!(sim_request, received_sim_request);
|
||||||
|
// Stop the server.
|
||||||
|
udp_testbench.stop();
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_udp_reply_server() {
|
||||||
|
let (mut udp_testbench, mut udp_server) =
|
||||||
|
UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10)
|
||||||
|
.expect("could not create testbench");
|
||||||
|
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||||
|
udp_testbench
|
||||||
|
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||||
|
SimCtrlRequest::Ping,
|
||||||
|
))
|
||||||
|
.expect("sending request failed");
|
||||||
|
|
||||||
|
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||||
|
udp_testbench.send_reply(&sim_reply);
|
||||||
|
|
||||||
|
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||||
|
|
||||||
|
// Stop the server.
|
||||||
|
udp_testbench.stop();
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_udp_req_server_and_reply_sender() {
|
||||||
|
let (mut udp_testbench, mut udp_server) =
|
||||||
|
UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10)
|
||||||
|
.expect("could not create testbench");
|
||||||
|
|
||||||
|
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||||
|
|
||||||
|
// Send a ping so that the server knows the address of the client.
|
||||||
|
// Do not check that the request arrives on the receiver side, is done by other test.
|
||||||
|
udp_testbench
|
||||||
|
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||||
|
SimCtrlRequest::Ping,
|
||||||
|
))
|
||||||
|
.expect("sending request failed");
|
||||||
|
|
||||||
|
// Send a reply to the server, ensure it gets forwarded to the client.
|
||||||
|
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||||
|
udp_testbench.send_reply(&sim_reply);
|
||||||
|
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||||
|
|
||||||
|
// Now we check that the reply server can send back replies to the client.
|
||||||
|
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||||
|
|
||||||
|
udp_testbench.stop();
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_udp_replies_client_unconnected() {
|
||||||
|
let (mut udp_testbench, mut udp_server) =
|
||||||
|
UdpTestbench::new(true, None, 10).expect("could not create testbench");
|
||||||
|
|
||||||
|
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||||
|
|
||||||
|
// Send a reply to the server. The client is not connected, so it won't get forwarded.
|
||||||
|
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||||
|
udp_testbench.send_reply(&sim_reply);
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
udp_testbench.check_no_sim_reply_available();
|
||||||
|
|
||||||
|
// Connect by sending a ping.
|
||||||
|
udp_testbench
|
||||||
|
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||||
|
SimCtrlRequest::Ping,
|
||||||
|
))
|
||||||
|
.expect("sending request failed");
|
||||||
|
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||||
|
|
||||||
|
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||||
|
|
||||||
|
// Now we check that the reply server can sent back replies to the client.
|
||||||
|
udp_testbench.stop();
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_udp_reply_server_old_replies_overwritten() {
|
||||||
|
let (mut udp_testbench, mut udp_server) =
|
||||||
|
UdpTestbench::new(true, None, 3).expect("could not create testbench");
|
||||||
|
|
||||||
|
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||||
|
|
||||||
|
// The server only caches up to 3 replies.
|
||||||
|
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||||
|
for _ in 0..4 {
|
||||||
|
udp_testbench.send_reply(&sim_reply);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
|
||||||
|
udp_testbench.check_no_sim_reply_available();
|
||||||
|
|
||||||
|
// Connect by sending a ping.
|
||||||
|
udp_testbench
|
||||||
|
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||||
|
SimCtrlRequest::Ping,
|
||||||
|
))
|
||||||
|
.expect("sending request failed");
|
||||||
|
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||||
|
|
||||||
|
for _ in 0..3 {
|
||||||
|
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||||
|
}
|
||||||
|
udp_testbench.check_no_sim_reply_available();
|
||||||
|
udp_testbench.stop();
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[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"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
/.cargo/config.toml
|
||||||
|
/.vscode
|
||||||
|
/app.map
|
||||||
+1635
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
|||||||
|
[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]
|
||||||
|
types = { path = "../types" }
|
||||||
|
|
||||||
|
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||||
|
arbitrary-int = "2"
|
||||||
|
cortex-m-rt = "0.7"
|
||||||
|
defmt = "1"
|
||||||
|
defmt-rtt = "1"
|
||||||
|
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||||
|
embedded-alloc = "0.7"
|
||||||
|
static_cell = "2"
|
||||||
|
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
|
||||||
|
postcard = "1"
|
||||||
|
serde = { version = "1", default-features = false }
|
||||||
|
|
||||||
|
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
|
||||||
|
embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] }
|
||||||
|
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms"] }
|
||||||
|
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||||
|
embassy-sync = "0.8"
|
||||||
|
embassy-futures = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
defmt-test = "0.5"
|
||||||
|
|
||||||
|
# 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 # <-
|
||||||
File renamed without changes.
@@ -0,0 +1,116 @@
|
|||||||
|
sat-rs embassy example for the STM32H753ZI-Nucleo board
|
||||||
|
=======
|
||||||
|
|
||||||
|
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||||
|
can be used on an embedded target.
|
||||||
|
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
|
||||||
|
It uses the [embassy](https://embassy.dev/) executor as the concurrency framework and the
|
||||||
|
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
|
||||||
|
|
||||||
|
This is the embassy variant of the
|
||||||
|
[`stm32h7-nucleo-rtic`](../stm32h7-nucleo-rtic) example. Both provide the same functionality.
|
||||||
|
|
||||||
|
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 example is 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
|
||||||
|
```
|
||||||
|
|
||||||
|
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
|
||||||
|
copies 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 simply build the application with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flashing from the command line
|
||||||
|
|
||||||
|
You can flash the application from the command line using `probe-rs`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
probe-rs run --chip STM32H753ZITx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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,14 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let cargo_dir = manifest_dir.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,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
targets = ["thumbv7em-none-eabihf"]
|
||||||
@@ -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,393 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use arbitrary_int::u14;
|
||||||
|
use defmt::Debug2Format;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_futures::select::{Either, Either3, select, select3};
|
||||||
|
use embassy_net::StackResources;
|
||||||
|
use embassy_net::udp::{PacketMetadata, UdpSocket};
|
||||||
|
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
|
||||||
|
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
||||||
|
use embassy_sync::channel::{Channel, Receiver, Sender};
|
||||||
|
use embassy_sync::signal::Signal;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
|
||||||
|
use static_cell::{ConstStaticCell, StaticCell};
|
||||||
|
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
|
||||||
|
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
|
||||||
|
|
||||||
|
const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500);
|
||||||
|
const DEFAULT_LED_MODE: led::Mode =
|
||||||
|
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
|
||||||
|
const PORT: u16 = 7301;
|
||||||
|
const MTU: usize = 1500;
|
||||||
|
|
||||||
|
/// Locally administered MAC address
|
||||||
|
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
|
||||||
|
|
||||||
|
const TC_QUEUE_DEPTH: usize = 32;
|
||||||
|
const TM_QUEUE_DEPTH: usize = 32;
|
||||||
|
|
||||||
|
static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
ETH => eth::InterruptHandler;
|
||||||
|
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||||
|
});
|
||||||
|
|
||||||
|
type Device = eth::Ethernet<
|
||||||
|
'static,
|
||||||
|
peripherals::ETH,
|
||||||
|
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
|
||||||
|
type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
|
||||||
|
type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
|
||||||
|
type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
|
||||||
|
|
||||||
|
struct Leds {
|
||||||
|
red: gpio::Output<'static>,
|
||||||
|
orange: gpio::Output<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = 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
|
||||||
|
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);
|
||||||
|
|
||||||
|
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<Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>> =
|
||||||
|
ConstStaticCell::new(Channel::new());
|
||||||
|
let tc_channel = TC_CHANNEL.take();
|
||||||
|
static TM_CHANNEL: ConstStaticCell<Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>> =
|
||||||
|
ConstStaticCell::new(Channel::new());
|
||||||
|
let tm_channel = TM_CHANNEL.take();
|
||||||
|
|
||||||
|
spawner.spawn(net_stack_task(runner).expect("spawning net stack 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(),
|
||||||
|
Telemetry {
|
||||||
|
tx: tm_channel.sender(),
|
||||||
|
sequence_count: u14::new(0),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("spawning TC handler task failed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn heartbeat(mut led: 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.
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn led_task(mut leds: 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn net_stack_task(mut runner: embassy_net::Runner<'static, Device>) -> ! {
|
||||||
|
runner.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
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);
|
||||||
|
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
|
||||||
|
tc_tx.send(rx_buffer[0..len].to_vec()).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) => match remote_endpoint {
|
||||||
|
Some(endpoint) => match udp.send_to(&packet, endpoint).await {
|
||||||
|
Ok(_) => defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint),
|
||||||
|
Err(e) => defmt::warn!("udp send error: {}", e),
|
||||||
|
},
|
||||||
|
None => defmt::warn!("dropping TM, no remote endpoint known"),
|
||||||
|
},
|
||||||
|
Either3::Third(()) => {
|
||||||
|
defmt::warn!("Network link is down");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn tc_handler(tc_rx: TcReceiver, mut telemetry: 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(&mut 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(&mut telemetry, tmtc::Event::InvalidTcHeader).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match tc_header.target_id {
|
||||||
|
ComponentId::Controller => handle_controller_tc(payload, tc_id, &mut telemetry).await,
|
||||||
|
ComponentId::Led => handle_led_tc(payload, tc_id, &mut telemetry).await,
|
||||||
|
target_id => {
|
||||||
|
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
|
||||||
|
send_tmtc_event(&mut 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
struct Telemetry {
|
||||||
|
tx: TmSender,
|
||||||
|
sequence_count: u14,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Telemetry {
|
||||||
|
/// 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,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,29 @@
|
|||||||
|
[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"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
/.cargo/config.toml
|
||||||
|
/.vscode
|
||||||
|
/app.map
|
||||||
Generated
+1400
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
|||||||
|
[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]
|
||||||
|
types = { path = "../types" }
|
||||||
|
|
||||||
|
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||||
|
arbitrary-int = "2"
|
||||||
|
cortex-m-rt = "0.7"
|
||||||
|
defmt = "1"
|
||||||
|
defmt-rtt = "1"
|
||||||
|
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||||
|
embedded-alloc = "0.7"
|
||||||
|
static_cell = "2"
|
||||||
|
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||||
|
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
|
||||||
|
postcard = "1"
|
||||||
|
serde = { version = "1", default-features = false }
|
||||||
|
|
||||||
|
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
|
||||||
|
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||||
|
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"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
defmt-test = "0.5"
|
||||||
|
|
||||||
|
# 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,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.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
sat-rs example for the STM32H753ZI-Nucleo board
|
||||||
|
=======
|
||||||
|
|
||||||
|
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||||
|
can be used on an embedded target.
|
||||||
|
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
|
||||||
|
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
|
||||||
|
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
|
||||||
|
|
||||||
|
The 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 example is 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
|
||||||
|
```
|
||||||
|
|
||||||
|
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
|
||||||
|
copies 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 simply build the application with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flashing from the command line
|
||||||
|
|
||||||
|
You can flash the application from the command line using `probe-rs`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
probe-rs run --chip STM32H753ZITx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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,14 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let cargo_dir = manifest_dir.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 309 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user