Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fafe149c1 | |||
| bcc1cc850f | |||
| 2cab8d7fed | |||
| b6612ed861 | |||
| 9d15dbcf09 | |||
| 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 |
@@ -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 satrs-example --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
|
||||||
|
|||||||
+7
-2
@@ -4,10 +4,15 @@ members = [
|
|||||||
"satrs",
|
"satrs",
|
||||||
"satrs-mib",
|
"satrs-mib",
|
||||||
"satrs-example",
|
"satrs-example",
|
||||||
|
"satrs-example/models",
|
||||||
|
"satrs-example/client",
|
||||||
|
"satrs-example/minisim",
|
||||||
"satrs-shared",
|
"satrs-shared",
|
||||||
|
"embedded-examples/embedded-client",
|
||||||
|
"embedded-examples/models",
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"satrs-example-stm32f3-disco",
|
"embedded-examples/stm32f3-disco-rtic",
|
||||||
|
"embedded-examples/stm32h7-nucleo-rtic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
<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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
[](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
||||||
[](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/)
|
[](https://robamu.github.io/sat-rs/book/)
|
||||||
[](https://crates.io/crates/satrs)
|
[](https://crates.io/crates/satrs)
|
||||||
[](https://docs.rs/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://robamu.github.io/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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||||
|
|
||||||
|
This is early-stage software. Important features are missing. New releases
|
||||||
|
with breaking changes are released regularly, with all changes documented inside respective
|
||||||
|
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
|
||||||
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||||
@@ -32,22 +38,47 @@ This project currently contains following crates:
|
|||||||
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
|
* [`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
|
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.
|
on a host computer or on any system with a standard runtime like a Raspberry Pi.
|
||||||
|
* [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim):
|
||||||
|
Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which
|
||||||
|
simulates some physical devices for the `satrs-example` application device handlers.
|
||||||
* [`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):
|
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32f3-disco-rtic):
|
||||||
Example of a simple example on-board software using sat-rs components on a bare-metal system
|
Example of a simple example using low-level sat-rs components on a bare-metal system
|
||||||
with constrained resources.
|
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
||||||
|
framework on the STM32F3-Discovery device.
|
||||||
|
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32h7-nucleo-rtic):
|
||||||
|
Example of a simple example using sat-rs components on a bare-metal system
|
||||||
|
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
||||||
|
framework on the STM32H743ZIT device.
|
||||||
|
|
||||||
Each project has its own `CHANGELOG.md`.
|
Each project has its own `CHANGELOG.md`.
|
||||||
|
|
||||||
# Related projects
|
# Related projects
|
||||||
|
|
||||||
In addition to the crates in this repository, the sat-rs project also maintains other libraries.
|
In addition to the crates in this repository, the sat-rs project also maintains other libraries.
|
||||||
|
|
||||||
* [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS
|
* [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS
|
||||||
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
|
||||||
|
|
||||||
@@ -59,5 +90,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') {
|
||||||
|
|||||||
+7
-3
@@ -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,7 +47,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
"--package",
|
"--package",
|
||||||
choices=["satrs"],
|
choices=["satrs", "satrs-minisim", "satrs-example"],
|
||||||
default="satrs",
|
default="satrs",
|
||||||
help="Choose project to generate coverage for",
|
help="Choose project to generate coverage for",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
|
||||||
|
cargo +nightly doc --all-features --open
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "embedded-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serialport = "4"
|
||||||
|
toml = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
satrs-stm32f3-disco-rtic = { path = "../stm32f3-disco-rtic" }
|
||||||
|
spacepackets = { version = "0.17" }
|
||||||
|
embedded-models = { path = "../models" }
|
||||||
|
tmtc-utils = { git = "https://egit.irs.uni-stuttgart.de/rust/tmtc-utils.git", version = "0.1" }
|
||||||
|
postcard = { version = "1", features = ["alloc"] }
|
||||||
|
cobs = "0.5"
|
||||||
|
fern = "0.7"
|
||||||
|
humantime = "2"
|
||||||
|
log = "0.4"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[interface]
|
||||||
|
serial_port = "/dev/ttyUSB0"
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use cobs::CobsDecoderOwned;
|
||||||
|
use embedded_client::setup_logger;
|
||||||
|
use embedded_models::stm32f3;
|
||||||
|
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||||
|
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
|
||||||
|
/// Set frequency in milliseconds.
|
||||||
|
#[arg(short, long)]
|
||||||
|
set_led_frequency: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
setup_logger().expect("failed to initialize logger");
|
||||||
|
println!("-- STM32F3 TMTC client --");
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = embedded_client::Config::new_from_file();
|
||||||
|
|
||||||
|
let serial = serialport::new(config.interface.serial_port, 115200)
|
||||||
|
.open()
|
||||||
|
.expect("opening serial port failed");
|
||||||
|
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
|
||||||
|
|
||||||
|
if cli.ping {
|
||||||
|
let tc = create_stm32f3_tc(&embedded_models::stm32f3::Request::Ping);
|
||||||
|
log::info!(
|
||||||
|
"Sending ping request with TC ID: {:#010x}",
|
||||||
|
tc.ccsds_packet_id_and_psc().raw()
|
||||||
|
);
|
||||||
|
transport.send(&tc.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(freq_ms) = cli.set_led_frequency {
|
||||||
|
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||||
|
let tc = create_stm32f3_tc(&request);
|
||||||
|
log::info!(
|
||||||
|
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||||
|
request,
|
||||||
|
tc.ccsds_packet_id_and_psc().raw()
|
||||||
|
);
|
||||||
|
transport.send(&tc.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Waiting for response...");
|
||||||
|
loop {
|
||||||
|
transport
|
||||||
|
.receive(|packet: &[u8]| {
|
||||||
|
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||||
|
log::info!("Received packet: {:?}", reader);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
|
||||||
|
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||||
|
let sp_header = SpHeader::new_from_apid(satrs_stm32f3_disco_rtic::APID);
|
||||||
|
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use cobs::CobsDecoderOwned;
|
||||||
|
use embedded_client::setup_logger;
|
||||||
|
use embedded_models::{stm32f3, stm32h7};
|
||||||
|
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||||
|
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
|
||||||
|
/// Set frequency in milliseconds.
|
||||||
|
#[arg(short, long)]
|
||||||
|
set_led_frequency: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
setup_logger().expect("failed to initialize logger");
|
||||||
|
println!("-- STM32H7 TMTC client --");
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = embedded_client::Config::new_from_file();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
if cli.ping {
|
||||||
|
let tc = create_stm32f3_tc(&embedded_models::stm32f3::Request::Ping);
|
||||||
|
log::info!(
|
||||||
|
"Sending ping request with TC ID: {:#010x}",
|
||||||
|
tc.ccsds_packet_id_and_psc().raw()
|
||||||
|
);
|
||||||
|
transport.send(&tc.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(freq_ms) = cli.set_led_frequency {
|
||||||
|
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||||
|
let tc = create_stm32f3_tc(&request);
|
||||||
|
log::info!(
|
||||||
|
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||||
|
request,
|
||||||
|
tc.ccsds_packet_id_and_psc().raw()
|
||||||
|
);
|
||||||
|
transport.send(&tc.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Waiting for response...");
|
||||||
|
loop {
|
||||||
|
transport
|
||||||
|
.receive(|packet: &[u8]| {
|
||||||
|
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||||
|
log::info!("Received packet: {:?}", reader);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned {
|
||||||
|
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||||
|
let sp_header = SpHeader::new_from_apid(satrs_stm32f3_disco_rtic::APID);
|
||||||
|
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use std::{fs::File, io::Read as _, path::Path, time::SystemTime};
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub interface: Interface,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Interface {
|
||||||
|
pub serial_port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new_from_file() -> Self {
|
||||||
|
let mut config_file =
|
||||||
|
File::open(Path::new("config.toml")).expect("opening config.toml file failed");
|
||||||
|
let mut toml_str = String::new();
|
||||||
|
config_file
|
||||||
|
.read_to_string(&mut toml_str)
|
||||||
|
.expect("reading config.toml file failed");
|
||||||
|
let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed");
|
||||||
|
println!("Connecting to serial port {}", config.interface.serial_port);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{} {} {}] {}",
|
||||||
|
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "embedded-models"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", default-features = false }
|
||||||
|
defmt = { version = "1", optional = true }
|
||||||
|
spacepackets = { version = "0.17", default-features = false, features = ["defmt", "serde"] }
|
||||||
|
postcard = { version = "1", features = ["defmt"] }
|
||||||
|
arbitrary-int = "2"
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, CcsdsPacketIdAndPsc,
|
||||||
|
SpacePacketHeader, ccsds_packet_len_for_user_data_len_with_checksum,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod stm32f3 {
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
pub const PUS_APID: u11 = u11::new(0x02);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
ChangeBlinkFrequency(Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This might look like a duplication, but we intentionally keep those separate so they can
|
||||||
|
/// change independently.
|
||||||
|
pub mod stm32h7 {
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
pub const PUS_APID: u11 = u11::new(0x03);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
ChangeBlinkFrequency(Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub struct TmHeader {
|
||||||
|
pub tc_packet_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
pub uptime_millis: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tm_size<Response: serde::Serialize>(tm_header: &TmHeader, response: &Response) -> usize {
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||||
|
postcard::experimental::serialized_size(tm_header).unwrap()
|
||||||
|
+ postcard::experimental::serialized_size(response).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_tm_packet<Response: serde::Serialize>(
|
||||||
|
buf: &mut [u8],
|
||||||
|
sp_header: SpacePacketHeader,
|
||||||
|
tm_header: TmHeader,
|
||||||
|
response: Response,
|
||||||
|
) -> Result<usize, CcsdsPacketCreationError> {
|
||||||
|
let packet_data_size = postcard::experimental::serialized_size(&tm_header).unwrap()
|
||||||
|
+ postcard::experimental::serialized_size(&response).unwrap();
|
||||||
|
let mut creator =
|
||||||
|
CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(sp_header, packet_data_size, buf)?;
|
||||||
|
|
||||||
|
let current_index = postcard::to_slice(&tm_header, creator.packet_data_mut())
|
||||||
|
.unwrap()
|
||||||
|
.len();
|
||||||
|
postcard::to_slice(&response, &mut creator.packet_data_mut()[current_index..]).unwrap();
|
||||||
|
Ok(creator.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||||
|
# uncomment ONE of these three option to make `cargo run` start a GDB session
|
||||||
|
# which option to pick depends on your system
|
||||||
|
# You can also replace openocd.gdb by jlink.gdb when using a J-Link.
|
||||||
|
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
|
||||||
|
# runner = "gdb-multiarch -q -x openocd.gdb"
|
||||||
|
# runner = "gdb -q -x openocd.gdb"
|
||||||
|
runner = "probe-rs run --chip STM32F303VCTx"
|
||||||
|
|
||||||
|
rustflags = [
|
||||||
|
"-C", "linker=flip-link",
|
||||||
|
# LLD (shipped with the Rust toolchain) is used as the default linker
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "link-arg=-Tdefmt.x",
|
||||||
|
|
||||||
|
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
|
||||||
|
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
|
||||||
|
"-C", "link-arg=--nmagic",
|
||||||
|
|
||||||
|
# if you run into problems with LLD switch to the GNU linker by commenting out
|
||||||
|
# this line
|
||||||
|
# "-C", "linker=arm-none-eabi-ld",
|
||||||
|
|
||||||
|
# if you need to link to pre-compiled C libraries provided by a C toolchain
|
||||||
|
# use GCC as the linker by commenting out both lines above and then
|
||||||
|
# uncommenting the three lines below
|
||||||
|
# "-C", "linker=arm-none-eabi-gcc",
|
||||||
|
# "-C", "link-arg=-Wl,-Tlink.x",
|
||||||
|
# "-C", "link-arg=-nostartfiles",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
# comment out the following line if you intend to run unit tests on host machine
|
||||||
|
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
|
||||||
|
|
||||||
|
[env]
|
||||||
|
DEFMT_LOG = "info"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
/itm.txt
|
||||||
|
/.cargo/config.toml
|
||||||
|
/.vscode
|
||||||
+1594
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
[package]
|
||||||
|
name = "satrs-stm32f3-disco-rtic"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "satrs-stm32f3-disco-rtic"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
embedded-models = { path = "../models", features = ["defmt"] }
|
||||||
|
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||||
|
cortex-m-rt = "0.7"
|
||||||
|
defmt = "1"
|
||||||
|
defmt-rtt = { version = "1" }
|
||||||
|
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||||
|
embedded-hal = "1"
|
||||||
|
cortex-m-semihosting = "0.5.0"
|
||||||
|
embassy-stm32 = { version = "0.4", features = ["defmt", "stm32f303vc", "unstable-pac"] }
|
||||||
|
enumset = "1"
|
||||||
|
heapless = "0.9"
|
||||||
|
spacepackets = { version = "0.17", default-features = false, features = ["defmt", "serde"] }
|
||||||
|
static_cell = "2"
|
||||||
|
cobs = { version = "0.5", default-features = false, features = ["defmt"] }
|
||||||
|
postcard = { version = "1" }
|
||||||
|
arbitrary-int = "2"
|
||||||
|
thiserror = { version = "2", default-features = false }
|
||||||
|
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
|
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||||
|
rtic-sync = { version = "1" }
|
||||||
|
rtic-monotonics = { version = "2", features = ["cortex-m-systick"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
defmt-test = "0.4"
|
||||||
|
|
||||||
|
# cargo test
|
||||||
|
[profile.test]
|
||||||
|
codegen-units = 1
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = true # <-
|
||||||
|
incremental = false
|
||||||
|
opt-level = "s" # <-
|
||||||
|
overflow-checks = true # <-
|
||||||
|
|
||||||
|
# cargo build/run --release
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = false # <-
|
||||||
|
incremental = false
|
||||||
|
lto = 'fat'
|
||||||
|
opt-level = "s" # <-
|
||||||
|
overflow-checks = false # <-
|
||||||
|
|
||||||
|
# cargo test --release
|
||||||
|
[profile.bench]
|
||||||
|
codegen-units = 1
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = false # <-
|
||||||
|
incremental = false
|
||||||
|
lto = 'fat'
|
||||||
|
opt-level = "s" # <-
|
||||||
|
overflow-checks = false # <-
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
sat-rs example for the STM32F3-Discovery board
|
||||||
|
=======
|
||||||
|
|
||||||
|
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||||
|
can be used on an embedded target.
|
||||||
|
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
|
||||||
|
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
|
||||||
|
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
|
||||||
|
|
||||||
|
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
|
||||||
|
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
|
||||||
|
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
|
||||||
|
to embedded Rust.
|
||||||
|
|
||||||
|
## Pre-Requisites
|
||||||
|
|
||||||
|
Make sure the following tools are installed:
|
||||||
|
|
||||||
|
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
|
||||||
|
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
|
||||||
|
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
|
||||||
|
for debugging.
|
||||||
|
|
||||||
|
## Preparing Rust and the repository
|
||||||
|
|
||||||
|
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
||||||
|
If you have not installed it yet, you can do so with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rustup target add thumbv7em-none-eabihf
|
||||||
|
```
|
||||||
|
|
||||||
|
A default `.cargo` config file is provided for this project, but needs to be copied to have
|
||||||
|
the correct name. This is so that the config file can be updated or edited for custom needs
|
||||||
|
without being tracked by git.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp def_config.toml config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
The configuration file will also set the target so it does not always have to be specified with
|
||||||
|
the `--target` argument.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
||||||
|
you can simply build the application with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flashing from the command line
|
||||||
|
|
||||||
|
You can flash the application from the command line using `probe-rs`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
probe-rs run --chip STM32F303VCTx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging with VS Code
|
||||||
|
|
||||||
|
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
|
||||||
|
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
|
||||||
|
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
|
||||||
|
Make sure to install this plugin first.
|
||||||
|
|
||||||
|
Sample configuration files are provided inside the `vscode` folder.
|
||||||
|
Use `cp vscode .vscode -r` to use them for your project.
|
||||||
|
|
||||||
|
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
|
||||||
|
to automatically rebuild and flash your application.
|
||||||
|
|
||||||
|
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
|
||||||
|
the folder in VS code or adding it to a workspace.
|
||||||
|
|
||||||
|
## Commanding with Python
|
||||||
|
|
||||||
|
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
|
||||||
|
using COBS encoded PUS packets.
|
||||||
|
|
||||||
|
It is recommended to use a virtual environment to do this. To set up one in the command line,
|
||||||
|
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
|
||||||
|
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
|
||||||
|
on how to activate the environment and then use the following command to install the required
|
||||||
|
dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
|
||||||
|
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
|
||||||
|
|
||||||
|
A default configuration file for the python application is provided and can be used by running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp def_tmtc_conf.json tmtc_conf.json
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can for example send a ping to the MCU using the following command
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./main.py -p /ping
|
||||||
|
```
|
||||||
|
|
||||||
|
You can configure the blinky frequency using
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./main.py -p /change_blink_freq
|
||||||
|
```
|
||||||
|
|
||||||
|
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
|
||||||
|
format as the packet framing format.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use panic_probe as _;
|
||||||
|
use rtic::app;
|
||||||
|
|
||||||
|
#[app(device = embassy_stm32)]
|
||||||
|
mod app {
|
||||||
|
use rtic_monotonics::fugit::ExtU32;
|
||||||
|
use rtic_monotonics::Monotonic as _;
|
||||||
|
use satrs_stm32f3_disco_rtic::{Direction, LedPinSet, Leds};
|
||||||
|
|
||||||
|
rtic_monotonics::systick_monotonic!(Mono, 1000);
|
||||||
|
|
||||||
|
#[shared]
|
||||||
|
struct Shared {}
|
||||||
|
|
||||||
|
#[local]
|
||||||
|
struct Local {
|
||||||
|
leds: Leds,
|
||||||
|
current_dir: Direction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init(cx: init::Context) -> (Shared, Local) {
|
||||||
|
let p = embassy_stm32::init(Default::default());
|
||||||
|
|
||||||
|
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery using RTICv2");
|
||||||
|
|
||||||
|
let led_pin_set = LedPinSet {
|
||||||
|
pin_n: p.PE8,
|
||||||
|
pin_ne: p.PE9,
|
||||||
|
pin_e: p.PE10,
|
||||||
|
pin_se: p.PE11,
|
||||||
|
pin_s: p.PE12,
|
||||||
|
pin_sw: p.PE13,
|
||||||
|
pin_w: p.PE14,
|
||||||
|
pin_nw: p.PE15,
|
||||||
|
};
|
||||||
|
let leds = Leds::new(led_pin_set);
|
||||||
|
|
||||||
|
// Initialize the systick interrupt & obtain the token to prove that we did
|
||||||
|
Mono::start(cx.core.SYST, 8_000_000);
|
||||||
|
blinky::spawn().expect("failed to spawn blinky task");
|
||||||
|
(
|
||||||
|
Shared {},
|
||||||
|
Local {
|
||||||
|
leds,
|
||||||
|
current_dir: Direction::North,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local = [leds, current_dir])]
|
||||||
|
async fn blinky(cx: blinky::Context) {
|
||||||
|
loop {
|
||||||
|
cx.local.leds.blink_next(cx.local.current_dir);
|
||||||
|
Mono::delay(200.millis()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use defmt_rtt as _;
|
||||||
|
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use embassy_stm32::gpio::Output;
|
||||||
|
|
||||||
|
pub const APID: u11 = u11::new(0x02);
|
||||||
|
|
||||||
|
#[derive(defmt::Format, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum Direction {
|
||||||
|
North,
|
||||||
|
NorthEast,
|
||||||
|
East,
|
||||||
|
SouthEast,
|
||||||
|
South,
|
||||||
|
SouthWest,
|
||||||
|
West,
|
||||||
|
NorthWest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction {
|
||||||
|
pub fn switch_to_next(&mut self) -> (Self, Self) {
|
||||||
|
let curr = *self;
|
||||||
|
*self = match self {
|
||||||
|
Direction::North => Direction::NorthEast,
|
||||||
|
Direction::NorthEast => Direction::East,
|
||||||
|
Direction::East => Direction::SouthEast,
|
||||||
|
Direction::SouthEast => Direction::South,
|
||||||
|
Direction::South => Direction::SouthWest,
|
||||||
|
Direction::SouthWest => Direction::West,
|
||||||
|
Direction::West => Direction::NorthWest,
|
||||||
|
Direction::NorthWest => Direction::North,
|
||||||
|
};
|
||||||
|
(curr, *self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Leds {
|
||||||
|
pub north: Output<'static>,
|
||||||
|
pub north_east: Output<'static>,
|
||||||
|
pub east: Output<'static>,
|
||||||
|
pub south_east: Output<'static>,
|
||||||
|
pub south: Output<'static>,
|
||||||
|
pub south_west: Output<'static>,
|
||||||
|
pub west: Output<'static>,
|
||||||
|
pub north_west: Output<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Leds {
|
||||||
|
pub fn blink_next(&mut self, current_dir: &mut Direction) {
|
||||||
|
let (prev, curr) = current_dir.switch_to_next();
|
||||||
|
self.set_dir_low(prev);
|
||||||
|
self.set_dir_high(curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dir(&mut self, dir: Direction, level: embassy_stm32::gpio::Level) {
|
||||||
|
match dir {
|
||||||
|
Direction::North => self.north.set_level(level),
|
||||||
|
Direction::NorthEast => self.north_east.set_level(level),
|
||||||
|
Direction::East => self.east.set_level(level),
|
||||||
|
Direction::SouthEast => self.south_east.set_level(level),
|
||||||
|
Direction::South => self.south.set_level(level),
|
||||||
|
Direction::SouthWest => self.south_west.set_level(level),
|
||||||
|
Direction::West => self.west.set_level(level),
|
||||||
|
Direction::NorthWest => self.north_west.set_level(level),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dir_low(&mut self, dir: Direction) {
|
||||||
|
self.set_dir(dir, embassy_stm32::gpio::Level::Low);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dir_high(&mut self, dir: Direction) {
|
||||||
|
self.set_dir(dir, embassy_stm32::gpio::Level::High);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LedPinSet {
|
||||||
|
pub pin_n: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE8>,
|
||||||
|
pub pin_ne: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE9>,
|
||||||
|
pub pin_e: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE10>,
|
||||||
|
pub pin_se: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE11>,
|
||||||
|
pub pin_s: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE12>,
|
||||||
|
pub pin_sw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE13>,
|
||||||
|
pub pin_w: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE14>,
|
||||||
|
pub pin_nw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE15>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Leds {
|
||||||
|
pub fn new(pin_set: LedPinSet) -> Self {
|
||||||
|
let led_n = Output::new(
|
||||||
|
pin_set.pin_n,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_ne = Output::new(
|
||||||
|
pin_set.pin_ne,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_e = Output::new(
|
||||||
|
pin_set.pin_e,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_se = Output::new(
|
||||||
|
pin_set.pin_se,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_s = Output::new(
|
||||||
|
pin_set.pin_s,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_sw = Output::new(
|
||||||
|
pin_set.pin_sw,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_w = Output::new(
|
||||||
|
pin_set.pin_w,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
let led_nw = Output::new(
|
||||||
|
pin_set.pin_nw,
|
||||||
|
embassy_stm32::gpio::Level::Low,
|
||||||
|
embassy_stm32::gpio::Speed::Medium,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
north: led_n,
|
||||||
|
north_east: led_ne,
|
||||||
|
east: led_e,
|
||||||
|
south_east: led_se,
|
||||||
|
south: led_s,
|
||||||
|
south_west: led_sw,
|
||||||
|
west: led_w,
|
||||||
|
north_west: led_nw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
use arbitrary_int::u14;
|
||||||
|
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
|
use embedded_models::{create_tm_packet, stm32f3, tm_size, TmHeader};
|
||||||
|
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
|
||||||
|
|
||||||
|
use defmt_rtt as _; // global logger
|
||||||
|
|
||||||
|
use panic_probe as _;
|
||||||
|
|
||||||
|
use rtic::app;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use rtic_monotonics::fugit::{MillisDurationU32, TimerInstantU32};
|
||||||
|
use rtic_monotonics::systick::prelude::*;
|
||||||
|
|
||||||
|
use crate::app::Mono;
|
||||||
|
|
||||||
|
const UART_BAUD: u32 = 115200;
|
||||||
|
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
||||||
|
const TX_HANDLER_FREQ_MS: u32 = 20;
|
||||||
|
const MAX_TC_LEN: usize = 128;
|
||||||
|
const MAX_TM_LEN: usize = 128;
|
||||||
|
|
||||||
|
// This is the predictable maximum overhead of the COBS encoding scheme.
|
||||||
|
// It is simply the maximum packet lenght dividied by 254 rounded up.
|
||||||
|
const COBS_TM_OVERHEAD: usize = cobs::max_encoding_overhead(MAX_TM_LEN);
|
||||||
|
|
||||||
|
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
|
||||||
|
|
||||||
|
const TC_DMA_BUF_LEN: usize = 512;
|
||||||
|
|
||||||
|
type TmPacket = heapless::Vec<u8, MAX_TM_LEN>;
|
||||||
|
|
||||||
|
static TM_QUEUE: heapless::mpmc::Queue<TmPacket, 16> = heapless::mpmc::Queue::new();
|
||||||
|
|
||||||
|
#[derive(Debug, defmt::Format, thiserror::Error)]
|
||||||
|
pub enum TmSendError {
|
||||||
|
#[error("packet creation error: {0}")]
|
||||||
|
PacketCreation(#[from] CcsdsPacketCreationError),
|
||||||
|
#[error("queue error")]
|
||||||
|
Queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, defmt::Format)]
|
||||||
|
pub struct RequestWithTcId {
|
||||||
|
pub request: stm32f3::Request,
|
||||||
|
pub tc_id: CcsdsPacketIdAndPsc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[app(device = embassy_stm32)]
|
||||||
|
mod app {
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use arbitrary_int::u14;
|
||||||
|
use embedded_models::stm32f3::{Request, Response};
|
||||||
|
use rtic::Mutex;
|
||||||
|
use rtic_sync::{
|
||||||
|
channel::{Receiver, Sender},
|
||||||
|
make_channel,
|
||||||
|
};
|
||||||
|
use satrs_stm32f3_disco_rtic::LedPinSet;
|
||||||
|
use spacepackets::CcsdsPacketReader;
|
||||||
|
|
||||||
|
systick_monotonic!(Mono, 1000);
|
||||||
|
|
||||||
|
embassy_stm32::bind_interrupts!(struct Irqs {
|
||||||
|
USART2 => embassy_stm32::usart::InterruptHandler<embassy_stm32::peripherals::USART2>;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[shared]
|
||||||
|
struct Shared {
|
||||||
|
blink_freq: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[local]
|
||||||
|
struct Local {
|
||||||
|
leds: satrs_stm32f3_disco_rtic::Leds,
|
||||||
|
current_dir: satrs_stm32f3_disco_rtic::Direction,
|
||||||
|
seq_count: u14,
|
||||||
|
tx: embassy_stm32::usart::UartTx<'static, embassy_stm32::mode::Async>,
|
||||||
|
rx: embassy_stm32::usart::RingBufferedUartRx<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init(cx: init::Context) -> (Shared, Local) {
|
||||||
|
static DMA_BUF: static_cell::ConstStaticCell<[u8; TC_DMA_BUF_LEN]> =
|
||||||
|
static_cell::ConstStaticCell::new([0; TC_DMA_BUF_LEN]);
|
||||||
|
|
||||||
|
let p = embassy_stm32::init(Default::default());
|
||||||
|
|
||||||
|
let (req_sender, req_receiver) = make_channel!(RequestWithTcId, 16);
|
||||||
|
// Initialize the systick interrupt & obtain the token to prove that we did
|
||||||
|
Mono::start(cx.core.SYST, 8_000_000);
|
||||||
|
|
||||||
|
defmt::info!("sat-rs demo application for the STM32F3-Discovery with RTICv2");
|
||||||
|
let led_pin_set = LedPinSet {
|
||||||
|
pin_n: p.PE8,
|
||||||
|
pin_ne: p.PE9,
|
||||||
|
pin_e: p.PE10,
|
||||||
|
pin_se: p.PE11,
|
||||||
|
pin_s: p.PE12,
|
||||||
|
pin_sw: p.PE13,
|
||||||
|
pin_w: p.PE14,
|
||||||
|
pin_nw: p.PE15,
|
||||||
|
};
|
||||||
|
let leds = satrs_stm32f3_disco_rtic::Leds::new(led_pin_set);
|
||||||
|
|
||||||
|
let mut config = embassy_stm32::usart::Config::default();
|
||||||
|
config.baudrate = UART_BAUD;
|
||||||
|
let uart = embassy_stm32::usart::Uart::new(
|
||||||
|
p.USART2, p.PA3, p.PA2, Irqs, p.DMA1_CH7, p.DMA1_CH6, config,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (tx, rx) = uart.split();
|
||||||
|
defmt::info!("Spawning tasks");
|
||||||
|
blinky::spawn().unwrap();
|
||||||
|
serial_tx_handler::spawn().unwrap();
|
||||||
|
serial_rx_handler::spawn(req_sender).unwrap();
|
||||||
|
req_handler::spawn(req_receiver).unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
Shared {
|
||||||
|
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||||
|
},
|
||||||
|
Local {
|
||||||
|
leds,
|
||||||
|
tx,
|
||||||
|
seq_count: u14::new(0),
|
||||||
|
rx: rx.into_ring_buffered(DMA_BUF.take()),
|
||||||
|
current_dir: satrs_stm32f3_disco_rtic::Direction::North,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local = [leds, current_dir], shared=[blink_freq])]
|
||||||
|
async fn blinky(mut cx: blinky::Context) {
|
||||||
|
loop {
|
||||||
|
cx.local.leds.blink_next(cx.local.current_dir);
|
||||||
|
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||||
|
Mono::delay(MillisDurationU32::from_ticks(
|
||||||
|
current_blink_freq.as_millis() as u32,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(
|
||||||
|
local = [
|
||||||
|
tx,
|
||||||
|
encoded_buf: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN]
|
||||||
|
],
|
||||||
|
shared = [],
|
||||||
|
)]
|
||||||
|
async fn serial_tx_handler(cx: serial_tx_handler::Context) {
|
||||||
|
loop {
|
||||||
|
while let Some(vec) = TM_QUEUE.dequeue() {
|
||||||
|
let encoded_len =
|
||||||
|
cobs::encode_including_sentinels(&vec[0..vec.len()], cx.local.encoded_buf);
|
||||||
|
defmt::debug!("sending {} bytes over UART", encoded_len);
|
||||||
|
cx.local
|
||||||
|
.tx
|
||||||
|
.write(&cx.local.encoded_buf[0..encoded_len])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Mono::delay(TX_HANDLER_FREQ_MS.millis()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(
|
||||||
|
local = [
|
||||||
|
rx,
|
||||||
|
read_buf: [u8; 128] = [0; 128],
|
||||||
|
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
|
||||||
|
],
|
||||||
|
shared = [blink_freq]
|
||||||
|
)]
|
||||||
|
async fn serial_rx_handler(
|
||||||
|
cx: serial_rx_handler::Context,
|
||||||
|
mut sender: Sender<'static, RequestWithTcId, 16>,
|
||||||
|
) {
|
||||||
|
let mut decoder = cobs::CobsDecoder::new(cx.local.decode_buf);
|
||||||
|
loop {
|
||||||
|
match cx.local.rx.read(cx.local.read_buf).await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
defmt::debug!("received {} bytes over UART", bytes);
|
||||||
|
for byte in cx.local.read_buf[0..bytes].iter() {
|
||||||
|
match decoder.feed(*byte) {
|
||||||
|
Ok(None) => (),
|
||||||
|
Ok(Some(packet_size)) => {
|
||||||
|
match CcsdsPacketReader::new_with_checksum(
|
||||||
|
&decoder.dest()[0..packet_size],
|
||||||
|
) {
|
||||||
|
Ok(packet) => {
|
||||||
|
let packet_id = packet.packet_id();
|
||||||
|
let psc = packet.psc();
|
||||||
|
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
|
||||||
|
if let Ok(request) =
|
||||||
|
postcard::from_bytes::<Request>(packet.packet_data())
|
||||||
|
{
|
||||||
|
sender
|
||||||
|
.send(RequestWithTcId {
|
||||||
|
request,
|
||||||
|
tc_id: tc_packet_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
defmt::error!("error unpacking ccsds packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
defmt::error!("cobs decoding error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
defmt::error!("uart read error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(shared = [blink_freq], local = [seq_count])]
|
||||||
|
async fn req_handler(
|
||||||
|
mut cx: req_handler::Context,
|
||||||
|
mut receiver: Receiver<'static, RequestWithTcId, 16>,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
match receiver.recv().await {
|
||||||
|
Ok(request_with_tc_id) => {
|
||||||
|
let tm_send_result = match request_with_tc_id.request {
|
||||||
|
Request::Ping => handle_ping_request(&mut cx, request_with_tc_id.tc_id),
|
||||||
|
Request::ChangeBlinkFrequency(duration) => {
|
||||||
|
handle_change_blink_frequency_request(
|
||||||
|
&mut cx,
|
||||||
|
request_with_tc_id.tc_id,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = tm_send_result {
|
||||||
|
defmt::error!("error sending TM response: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_e) => defmt::error!("request receive error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_ping_request(
|
||||||
|
cx: &mut req_handler::Context,
|
||||||
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
|
) -> Result<(), TmSendError> {
|
||||||
|
defmt::info!("Received PUS ping telecommand, sending ping reply");
|
||||||
|
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count)?;
|
||||||
|
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_change_blink_frequency_request(
|
||||||
|
cx: &mut req_handler::Context,
|
||||||
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
|
duration: Duration,
|
||||||
|
) -> Result<(), TmSendError> {
|
||||||
|
defmt::info!(
|
||||||
|
"Received ChangeBlinkFrequency request, new frequency: {} ms",
|
||||||
|
duration.as_millis()
|
||||||
|
);
|
||||||
|
cx.shared
|
||||||
|
.blink_freq
|
||||||
|
.lock(|blink_freq| *blink_freq = duration);
|
||||||
|
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count)?;
|
||||||
|
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_tm(
|
||||||
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
|
response: stm32f3::Response,
|
||||||
|
current_seq_count: u14,
|
||||||
|
) -> Result<(), TmSendError> {
|
||||||
|
let sp_header = SpHeader::new_for_unseg_tc(stm32f3::PUS_APID, current_seq_count, 0);
|
||||||
|
let tm_header = TmHeader {
|
||||||
|
tc_packet_id: Some(tc_packet_id),
|
||||||
|
uptime_millis: Mono::now().duration_since_epoch().to_millis() as u64,
|
||||||
|
};
|
||||||
|
let mut tm_packet = TmPacket::new();
|
||||||
|
let tm_size = tm_size(&tm_header, &response);
|
||||||
|
tm_packet.resize(tm_size, 0).expect("vec resize failed");
|
||||||
|
create_tm_packet(&mut tm_packet, sp_header, tm_header, response)?;
|
||||||
|
if TM_QUEUE.enqueue(tm_packet).is_err() {
|
||||||
|
defmt::warn!("TC queue full");
|
||||||
|
return Err(TmSendError::Queue);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||||
|
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
|
||||||
|
#[defmt::panic_handler]
|
||||||
|
fn panic() -> ! {
|
||||||
|
cortex_m::asm::udf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||||
|
/// with status code 0.
|
||||||
|
pub fn exit() -> ! {
|
||||||
|
loop {
|
||||||
|
debug::exit(EXIT_SUCCESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hardfault handler.
|
||||||
|
///
|
||||||
|
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||||
|
/// with an error. This seems better than the default, which is to spin in a
|
||||||
|
/// loop.
|
||||||
|
#[cortex_m_rt::exception]
|
||||||
|
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||||
|
loop {
|
||||||
|
debug::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||||
|
// once within a crate. the module can be in any file but there can only be at most
|
||||||
|
// one `#[tests]` module in this library crate
|
||||||
|
#[cfg(test)]
|
||||||
|
#[defmt_test::tests]
|
||||||
|
mod unit_tests {
|
||||||
|
use defmt::assert;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
assert!(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
// List of extensions which should be recommended for users of this workspace.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"rust-lang.rust",
|
"rust-lang.rust",
|
||||||
"marus25.cortex-debug",
|
"probe-rs.probe-rs-debugger"
|
||||||
],
|
],
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||||
"unwantedRecommendations": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"preLaunchTask": "${defaultBuildTask}",
|
||||||
|
"type": "probe-rs-debug",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "probe-rs Debugging ",
|
||||||
|
"flashingConfig": {
|
||||||
|
"flashingEnabled": true
|
||||||
|
},
|
||||||
|
"chip": "STM32F303VCTx",
|
||||||
|
"coreConfigs": [
|
||||||
|
{
|
||||||
|
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32f3-disco-rtic",
|
||||||
|
"rttEnabled": true,
|
||||||
|
"svdFile": "STM32F303.svd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
-1
@@ -11,7 +11,8 @@ proc CDSWOConfigure { CDCPUFreqHz CDSWOFreqHz CDSWOOutput } {
|
|||||||
# Alternative option: Pipe ITM output into itm.txt file
|
# Alternative option: Pipe ITM output into itm.txt file
|
||||||
# tpiu config internal itm.txt uart off $CDCPUFreqHz
|
# tpiu config internal itm.txt uart off $CDCPUFreqHz
|
||||||
|
|
||||||
# Default option so SWO display of VS code works.
|
# Default option so SWO display of VS code works. Please note that this might not be required
|
||||||
|
# anymore starting at openocd v0.12.0
|
||||||
tpiu config internal $CDSWOOutput uart off $CDCPUFreqHz $CDSWOFreqHz
|
tpiu config internal $CDSWOOutput uart off $CDCPUFreqHz $CDSWOFreqHz
|
||||||
itm port 0 on
|
itm port 0 on
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/itm.txt
|
|
||||||
/.cargo/config*
|
/.cargo/config*
|
||||||
|
/.vscode
|
||||||
|
/app.map
|
||||||
+1339
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
|
||||||
|
name = "satrs-stm32h7-nucleo-rtic"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
default-run = "satrs-stm32h7-nucleo-rtic"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
# needed for each integration test
|
||||||
|
[[test]]
|
||||||
|
name = "integration"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
embedded-models = { path = "../models", features = ["defmt"] }
|
||||||
|
|
||||||
|
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.17", default-features = false, features = ["defmt"] }
|
||||||
|
postcard = "1"
|
||||||
|
|
||||||
|
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.6", features = ["stm32h743zi", "memory-x", "defmt", "time-driver-any"]}
|
||||||
|
|
||||||
|
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||||
|
embassy-net = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||||
|
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
defmt-test = "0.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,118 @@
|
|||||||
|
sat-rs example for the STM32H73ZI-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 STM32H743ZIT device was picked because it is one of the more powerful Cortex-M based devices
|
||||||
|
available for STM with which also has a little bit more RAM available and also allows commanding
|
||||||
|
via TCP/IP.
|
||||||
|
|
||||||
|
## Pre-Requisites
|
||||||
|
|
||||||
|
Make sure the following tools are installed:
|
||||||
|
|
||||||
|
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
|
||||||
|
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
|
||||||
|
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
|
||||||
|
for debugging.
|
||||||
|
|
||||||
|
## Preparing Rust and the repository
|
||||||
|
|
||||||
|
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
||||||
|
If you have not installed it yet, you can do so with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rustup target add thumbv7em-none-eabihf
|
||||||
|
```
|
||||||
|
|
||||||
|
A default `.cargo` config file is provided for this project, but needs to be copied to have
|
||||||
|
the correct name. This is so that the config file can be updated or edited for custom needs
|
||||||
|
without being tracked by git.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp def_config.toml config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
The configuration file will also set the target so it does not always have to be specified with
|
||||||
|
the `--target` argument.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
||||||
|
you can simply build the application with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flashing from the command line
|
||||||
|
|
||||||
|
You can flash the application from the command line using `probe-rs`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
probe-rs run --chip STM32H743ZITx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging with VS Code
|
||||||
|
|
||||||
|
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
|
||||||
|
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
|
||||||
|
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
|
||||||
|
Make sure to install this plugin first.
|
||||||
|
|
||||||
|
Sample configuration files are provided inside the `vscode` folder.
|
||||||
|
Use `cp vscode .vscode -r` to use them for your project.
|
||||||
|
|
||||||
|
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
|
||||||
|
to automatically rebuild and flash your application.
|
||||||
|
|
||||||
|
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
|
||||||
|
the folder in VS code or adding it to a workspace.
|
||||||
|
|
||||||
|
## Commanding with Python
|
||||||
|
|
||||||
|
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
|
||||||
|
using COBS encoded PUS packets.
|
||||||
|
|
||||||
|
It is recommended to use a virtual environment to do this. To set up one in the command line,
|
||||||
|
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
|
||||||
|
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
|
||||||
|
on how to activate the environment and then use the following command to install the required
|
||||||
|
dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
|
||||||
|
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
|
||||||
|
|
||||||
|
A default configuration file for the python application is provided and can be used by running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp def_tmtc_conf.json tmtc_conf.json
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can for example send a ping to the MCU using the following command
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./main.py -p /ping
|
||||||
|
```
|
||||||
|
|
||||||
|
You can configure the blinky frequency using
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./main.py -p /change_blink_freq
|
||||||
|
```
|
||||||
|
|
||||||
|
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
|
||||||
|
format as the packet framing format.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
|
||||||
|
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
|
||||||
|
let config = cargo_dir.join("config.toml");
|
||||||
|
let config_template = cargo_dir.join("config.toml.template");
|
||||||
|
|
||||||
|
if !config.exists() && config_template.exists() {
|
||||||
|
fs::create_dir_all(&cargo_dir).unwrap();
|
||||||
|
fs::copy(&config_template, &config).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,119 @@
|
|||||||
|
/* Taken from https://github.com/stm32-rs/stm32h7xx-hal/pull/299, adapted slightly to work with */
|
||||||
|
/* flip-link */
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
/* This file is intended for parts in the STM32H743/743v/753/753v families (RM0433), */
|
||||||
|
/* with the exception of the STM32H742/742v parts which have a different RAM layout. */
|
||||||
|
/* - FLASH and RAM are mandatory memory sections. */
|
||||||
|
/* - The sum of all non-FLASH sections must add to 1060K total device RAM. */
|
||||||
|
/* - The FLASH section size must match your device, see table below. */
|
||||||
|
|
||||||
|
/* FLASH */
|
||||||
|
/* Flash is divided in two independent banks (except 750xB). */
|
||||||
|
/* Select the appropriate FLASH size for your device. */
|
||||||
|
/* - STM32H750xB 128K (only FLASH1) */
|
||||||
|
/* - STM32H750xB 1M (512K + 512K) */
|
||||||
|
/* - STM32H743xI/753xI 2M ( 1M + 1M) */
|
||||||
|
FLASH1 : ORIGIN = 0x08000000, LENGTH = 1M
|
||||||
|
FLASH2 : ORIGIN = 0x08100000, LENGTH = 1M
|
||||||
|
|
||||||
|
/* Data TCM */
|
||||||
|
/* - Two contiguous 64KB RAMs. */
|
||||||
|
/* - Used for interrupt handlers, stacks and general RAM. */
|
||||||
|
/* - Zero wait-states. */
|
||||||
|
/* - The DTCM is taken as the origin of the base ram. (See below.) */
|
||||||
|
/* This is also where the interrupt table and such will live, */
|
||||||
|
/* which is required for deterministic performance. */
|
||||||
|
/* Need a region called RAM */
|
||||||
|
/* DTCM : ORIGIN = 0x20000000, LENGTH = 128K */
|
||||||
|
RAM : ORIGIN = 0x20000000, LENGTH = 128K
|
||||||
|
|
||||||
|
/* Instruction TCM */
|
||||||
|
/* - Used for latency-critical interrupt handlers etc. */
|
||||||
|
/* - Zero wait-states. */
|
||||||
|
ITCM : ORIGIN = 0x00000000, LENGTH = 64K
|
||||||
|
|
||||||
|
/* AXI SRAM */
|
||||||
|
/* - AXISRAM is in D1 and accessible by all system masters except BDMA. */
|
||||||
|
/* - Suitable for application data not stored in DTCM. */
|
||||||
|
/* - Zero wait-states. */
|
||||||
|
AXISRAM : ORIGIN = 0x24000000, LENGTH = 512K
|
||||||
|
|
||||||
|
/* AHB SRAM */
|
||||||
|
/* - SRAM1-3 are in D2 and accessible by all system masters except BDMA. */
|
||||||
|
/* Suitable for use as DMA buffers. */
|
||||||
|
/* - SRAM4 is in D3 and additionally accessible by the BDMA. Used for BDMA */
|
||||||
|
/* buffers, for storing application data in lower-power modes. */
|
||||||
|
/* - Zero wait-states. */
|
||||||
|
SRAM1 : ORIGIN = 0x30000000, LENGTH = 128K
|
||||||
|
SRAM2 : ORIGIN = 0x30020000, LENGTH = 128K
|
||||||
|
SRAM3 : ORIGIN = 0x30040000, LENGTH = 32K
|
||||||
|
SRAM4 : ORIGIN = 0x38000000, LENGTH = 64K
|
||||||
|
|
||||||
|
/* Backup SRAM */
|
||||||
|
BSRAM : ORIGIN = 0x38800000, LENGTH = 4K
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
/* Assign the memory regions defined above for use. */
|
||||||
|
/*
|
||||||
|
|
||||||
|
/* Provide the mandatory FLASH and RAM definitions for cortex-m-rt's linker script. */
|
||||||
|
/* These do not work with flip-link */
|
||||||
|
REGION_ALIAS(FLASH, FLASH1);
|
||||||
|
/* REGION_ALIAS(RAM, DTCM); */
|
||||||
|
|
||||||
|
/* The location of the stack can be overridden using the `_stack_start` symbol. */
|
||||||
|
/* - Set the stack location at the end of RAM, using all remaining space. */
|
||||||
|
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
|
||||||
|
|
||||||
|
/* The location of the .text section can be overridden using the */
|
||||||
|
/* `_stext` symbol. By default it will place after .vector_table. */
|
||||||
|
/* _stext = ORIGIN(FLASH) + 0x40c; */
|
||||||
|
|
||||||
|
/* Define sections for placing symbols into the extra memory regions above. */
|
||||||
|
/* This makes them accessible from code. */
|
||||||
|
/* - ITCM, DTCM and AXISRAM connect to a 64-bit wide bus -> align to 8 bytes. */
|
||||||
|
/* - All other memories connect to a 32-bit wide bus -> align to 4 bytes. */
|
||||||
|
SECTIONS {
|
||||||
|
.flash2 (NOLOAD) : ALIGN(4) {
|
||||||
|
*(.flash2 .flash2.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > FLASH2
|
||||||
|
|
||||||
|
.itcm (NOLOAD) : ALIGN(8) {
|
||||||
|
*(.itcm .itcm.*);
|
||||||
|
. = ALIGN(8);
|
||||||
|
} > ITCM
|
||||||
|
|
||||||
|
.axisram (NOLOAD) : ALIGN(8) {
|
||||||
|
*(.axisram .axisram.*);
|
||||||
|
. = ALIGN(8);
|
||||||
|
} > AXISRAM
|
||||||
|
|
||||||
|
.sram1 (NOLOAD) : ALIGN(8) {
|
||||||
|
*(.sram1 .sram1.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > SRAM1
|
||||||
|
|
||||||
|
.sram2 (NOLOAD) : ALIGN(8) {
|
||||||
|
*(.sram2 .sram2.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > SRAM2
|
||||||
|
|
||||||
|
.sram3 (NOLOAD) : ALIGN(4) {
|
||||||
|
*(.sram3 .sram3.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > SRAM3
|
||||||
|
|
||||||
|
.sram4 (NOLOAD) : ALIGN(4) {
|
||||||
|
*(.sram4 .sram4.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > SRAM4
|
||||||
|
|
||||||
|
.bsram (NOLOAD) : ALIGN(4) {
|
||||||
|
*(.bsram .bsram.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > BSRAM
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
targets = ["thumbv7em-none-eabihf"]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//! Blinks an LED
|
||||||
|
//!
|
||||||
|
//! This assumes that LD2 (blue) is connected to pb7 and LD3 (red) is connected
|
||||||
|
//! to pb14. This assumption is true for the nucleo-h743zi board.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
use rtic::app;
|
||||||
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
|
|
||||||
|
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
|
||||||
|
mod app {
|
||||||
|
use embassy_stm32::gpio;
|
||||||
|
|
||||||
|
#[shared]
|
||||||
|
struct Shared {}
|
||||||
|
|
||||||
|
#[local]
|
||||||
|
struct Local {}
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||||
|
let p = embassy_stm32::init(Default::default());
|
||||||
|
defmt::info!("Hello World!");
|
||||||
|
// Configure gpio B pin 0 as a push-pull output.
|
||||||
|
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
let ld2 = gpio::Output::new(p.PB7, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
|
||||||
|
// Schedule the blinking task
|
||||||
|
blink::spawn(ld1, ld2, ld3).ok();
|
||||||
|
|
||||||
|
(Shared {}, Local {})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task()]
|
||||||
|
async fn blink(
|
||||||
|
_cx: blink::Context,
|
||||||
|
mut ld1: gpio::Output<'static>,
|
||||||
|
mut ld2: gpio::Output<'static>,
|
||||||
|
mut ld3: gpio::Output<'static>,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
defmt::info!("high");
|
||||||
|
ld1.set_high();
|
||||||
|
ld2.set_high();
|
||||||
|
ld3.set_high();
|
||||||
|
embassy_time::Timer::after_millis(500).await;
|
||||||
|
|
||||||
|
defmt::info!("low");
|
||||||
|
ld1.set_low();
|
||||||
|
ld2.set_low();
|
||||||
|
ld3.set_low();
|
||||||
|
embassy_time::Timer::after_millis(500).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
// global logger + panicking-behavior + memory layout
|
||||||
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
|
|
||||||
|
#[cortex_m_rt::entry]
|
||||||
|
fn main() -> ! {
|
||||||
|
loop {
|
||||||
|
defmt::println!("Hello, world!");
|
||||||
|
cortex_m::asm::delay(100_000_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use defmt_rtt as _;
|
||||||
|
use embassy_stm32 as _;
|
||||||
|
use panic_probe as _;
|
||||||
|
|
||||||
|
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||||
|
// once within a crate. the module can be in any file but there can only be at most
|
||||||
|
// one `#[tests]` module in this library crate
|
||||||
|
#[cfg(test)]
|
||||||
|
#[defmt_test::tests]
|
||||||
|
mod unit_tests {
|
||||||
|
use defmt::assert;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
assert!(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use rtic::app;
|
||||||
|
// global logger + panicking-behavior + memory layout
|
||||||
|
use embassy_stm32::bind_interrupts;
|
||||||
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
|
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
use embedded_alloc::LlffHeap as Heap;
|
||||||
|
|
||||||
|
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
||||||
|
const PORT: u16 = 7301;
|
||||||
|
|
||||||
|
const HEAP_SIZE: usize = 131_072;
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
#[app(device = embassy_stm32, peripherals = false)]
|
||||||
|
mod app {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use arbitrary_int::u14;
|
||||||
|
use embassy_net::udp::UdpSocket;
|
||||||
|
use embassy_net::StackResources;
|
||||||
|
use embassy_stm32::eth;
|
||||||
|
use embassy_stm32::gpio;
|
||||||
|
use embassy_stm32::peripherals;
|
||||||
|
use embassy_stm32::rng;
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
|
use embassy_time::Duration;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use embassy_time::WithTimeout as _;
|
||||||
|
use embedded_models::create_tm_packet;
|
||||||
|
use embedded_models::stm32h7;
|
||||||
|
use embedded_models::tm_size;
|
||||||
|
use embedded_models::TmHeader;
|
||||||
|
use spacepackets::CcsdsPacketCreationError;
|
||||||
|
use spacepackets::CcsdsPacketIdAndPsc;
|
||||||
|
use spacepackets::CcsdsPacketReader;
|
||||||
|
use spacepackets::SpHeader;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
ETH => eth::InterruptHandler;
|
||||||
|
RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||||
|
});
|
||||||
|
|
||||||
|
type Device = eth::Ethernet<
|
||||||
|
'static,
|
||||||
|
peripherals::ETH,
|
||||||
|
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
struct BlinkyLeds {
|
||||||
|
led1: gpio::Output<'static>,
|
||||||
|
led2: gpio::Output<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[local]
|
||||||
|
struct Local {
|
||||||
|
net_runner: embassy_net::Runner<'static, Device>,
|
||||||
|
net_stack: embassy_net::Stack<'static>,
|
||||||
|
leds: BlinkyLeds,
|
||||||
|
link_led: gpio::Output<'static>,
|
||||||
|
tc_rx: embassy_sync::channel::Receiver<
|
||||||
|
'static,
|
||||||
|
NoopRawMutex,
|
||||||
|
alloc::vec::Vec<u8>,
|
||||||
|
TC_QUEUE_DEPTH,
|
||||||
|
>,
|
||||||
|
tc_tx: embassy_sync::channel::Sender<
|
||||||
|
'static,
|
||||||
|
NoopRawMutex,
|
||||||
|
alloc::vec::Vec<u8>,
|
||||||
|
TC_QUEUE_DEPTH,
|
||||||
|
>,
|
||||||
|
tm_rx: embassy_sync::channel::Receiver<
|
||||||
|
'static,
|
||||||
|
NoopRawMutex,
|
||||||
|
alloc::vec::Vec<u8>,
|
||||||
|
TM_QUEUE_DEPTH,
|
||||||
|
>,
|
||||||
|
tm_tx: embassy_sync::channel::Sender<
|
||||||
|
'static,
|
||||||
|
NoopRawMutex,
|
||||||
|
alloc::vec::Vec<u8>,
|
||||||
|
TM_QUEUE_DEPTH,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[shared]
|
||||||
|
struct Shared {
|
||||||
|
sequence_count: u14,
|
||||||
|
blink_freq: embassy_time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||||
|
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
|
||||||
|
|
||||||
|
let mut config = embassy_stm32::Config::default();
|
||||||
|
{
|
||||||
|
use embassy_stm32::rcc::*;
|
||||||
|
config.rcc.hsi = Some(HSIPrescaler::Div1);
|
||||||
|
config.rcc.csi = true;
|
||||||
|
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
|
||||||
|
config.rcc.pll1 = Some(Pll {
|
||||||
|
source: PllSource::Hsi,
|
||||||
|
prediv: PllPreDiv::Div4,
|
||||||
|
mul: PllMul::Mul50,
|
||||||
|
fracn: None,
|
||||||
|
divp: Some(PllDiv::Div2),
|
||||||
|
divq: None,
|
||||||
|
divr: None,
|
||||||
|
});
|
||||||
|
config.rcc.sys = Sysclk::Pll1P; // 400 Mhz
|
||||||
|
config.rcc.ahb_pre = AHBPrescaler::Div2; // 200 Mhz
|
||||||
|
config.rcc.apb1_pre = APBPrescaler::Div2; // 100 Mhz
|
||||||
|
config.rcc.apb2_pre = APBPrescaler::Div2; // 100 Mhz
|
||||||
|
config.rcc.apb3_pre = APBPrescaler::Div2; // 100 Mhz
|
||||||
|
config.rcc.apb4_pre = APBPrescaler::Div2; // 100 Mhz
|
||||||
|
config.rcc.voltage_scale = VoltageScale::Scale1;
|
||||||
|
}
|
||||||
|
let periphs = embassy_stm32::init(config);
|
||||||
|
|
||||||
|
let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
|
let mut led1 = gpio::Output::new(periphs.PB7, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
|
let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
|
|
||||||
|
// Criss-cross pattern looks cooler.
|
||||||
|
led1.set_high();
|
||||||
|
led2.set_low();
|
||||||
|
let leds = BlinkyLeds { led1, led2 };
|
||||||
|
|
||||||
|
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 config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
|
||||||
|
|
||||||
|
// Generate random seed.
|
||||||
|
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
|
||||||
|
let mut seed = [0; 8];
|
||||||
|
rng.fill_bytes(&mut seed);
|
||||||
|
let seed = u64::from_le_bytes(seed);
|
||||||
|
|
||||||
|
// Init network stack
|
||||||
|
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||||
|
let (stack, runner) =
|
||||||
|
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
|
||||||
|
|
||||||
|
// Set up global allocator. Use AXISRAM for the heap.
|
||||||
|
#[link_section = ".axisram"]
|
||||||
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
|
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||||
|
|
||||||
|
static TC_CHANNEL: static_cell::ConstStaticCell<
|
||||||
|
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TC_QUEUE_DEPTH>,
|
||||||
|
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||||
|
let tc_channel = TC_CHANNEL.take();
|
||||||
|
let tc_sender = tc_channel.sender();
|
||||||
|
let tc_receiver = tc_channel.receiver();
|
||||||
|
|
||||||
|
static TM_CHANNEL: static_cell::ConstStaticCell<
|
||||||
|
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
|
||||||
|
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||||
|
let tm_channel = TM_CHANNEL.take();
|
||||||
|
let tm_sender = tm_channel.sender();
|
||||||
|
let tm_receiver = tm_channel.receiver();
|
||||||
|
|
||||||
|
net_lib_task::spawn().expect("spawning net library task failed");
|
||||||
|
net_app_task::spawn().expect("spawning net application task failed");
|
||||||
|
blinky::spawn().expect("spawning blink task failed");
|
||||||
|
tc_handler::spawn().expect("spawning TC handler task failed");
|
||||||
|
|
||||||
|
(
|
||||||
|
Shared {
|
||||||
|
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||||
|
sequence_count: u14::new(0),
|
||||||
|
},
|
||||||
|
Local {
|
||||||
|
link_led,
|
||||||
|
leds,
|
||||||
|
net_runner: runner,
|
||||||
|
net_stack: stack,
|
||||||
|
tc_tx: tc_sender,
|
||||||
|
tc_rx: tc_receiver,
|
||||||
|
tm_tx: tm_sender,
|
||||||
|
tm_rx: tm_receiver,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local = [leds], shared=[blink_freq])]
|
||||||
|
async fn blinky(mut cx: blinky::Context) {
|
||||||
|
let leds = cx.local.leds;
|
||||||
|
loop {
|
||||||
|
leds.led1.toggle();
|
||||||
|
leds.led2.toggle();
|
||||||
|
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||||
|
Timer::after_millis(current_blink_freq.as_millis()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local=[net_runner])]
|
||||||
|
async fn net_lib_task(cx: net_lib_task::Context) {
|
||||||
|
cx.local.net_runner.run().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local = [net_stack, link_led, tc_tx, tm_rx])]
|
||||||
|
async fn net_app_task(cx: net_app_task::Context) {
|
||||||
|
pub const MTU: usize = 1500;
|
||||||
|
|
||||||
|
// Ensure those are in the data section by making them static.
|
||||||
|
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||||
|
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||||
|
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||||
|
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||||
|
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||||
|
static_cell::ConstStaticCell::new([0; MTU]);
|
||||||
|
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||||
|
static_cell::ConstStaticCell::new([0; MTU]);
|
||||||
|
|
||||||
|
let rx_udp_meta = RX_UDP_META.take();
|
||||||
|
let rx_udp_bufs = RX_UDP_BUFS.take();
|
||||||
|
let tx_udp_meta = TX_UDP_META.take();
|
||||||
|
let tx_udp_bufs = TX_UDP_BUFS.take();
|
||||||
|
|
||||||
|
let mut rx_buffer = [0; MTU];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
cx.local.net_stack.wait_link_up().await;
|
||||||
|
cx.local.link_led.set_high();
|
||||||
|
defmt::info!("Network link is up");
|
||||||
|
|
||||||
|
// Ensure DHCP configuration is up before trying connect
|
||||||
|
cx.local.net_stack.wait_config_up().await;
|
||||||
|
|
||||||
|
let config = cx.local.net_stack.config_v4();
|
||||||
|
defmt::info!("Network task initialized, config: {}", config);
|
||||||
|
|
||||||
|
let mut udp = UdpSocket::new(
|
||||||
|
cx.local.net_stack.clone(),
|
||||||
|
rx_udp_meta,
|
||||||
|
rx_udp_bufs,
|
||||||
|
tx_udp_meta,
|
||||||
|
tx_udp_bufs,
|
||||||
|
);
|
||||||
|
defmt::info!("UDP socket bound to port {}", PORT);
|
||||||
|
udp.bind(PORT).expect("failed to bind UDP socket");
|
||||||
|
let mut remote_endpoint = None;
|
||||||
|
loop {
|
||||||
|
if !cx.local.net_stack.is_link_up() {
|
||||||
|
defmt::warn!("Network link is down");
|
||||||
|
cx.local.link_led.set_low();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match udp
|
||||||
|
.recv_from(&mut rx_buffer)
|
||||||
|
.with_timeout(Duration::from_millis(200))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => match result {
|
||||||
|
Ok((data, meta)) => {
|
||||||
|
remote_endpoint = Some(meta.endpoint);
|
||||||
|
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
|
||||||
|
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
defmt::warn!("udp receive error: {}", e);
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_e) => (),
|
||||||
|
}
|
||||||
|
if let Some(endpoint) = remote_endpoint {
|
||||||
|
while let Ok(packet) = cx.local.tm_rx.try_receive() {
|
||||||
|
match udp.send_to(&packet, endpoint).await {
|
||||||
|
Ok(_) => {
|
||||||
|
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
|
||||||
|
}
|
||||||
|
Err(e) => defmt::warn!("udp send error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])]
|
||||||
|
async fn tc_handler(mut cx: tc_handler::Context) {
|
||||||
|
loop {
|
||||||
|
let tc = cx.local.tc_rx.receive().await;
|
||||||
|
|
||||||
|
match CcsdsPacketReader::new_with_checksum(&tc) {
|
||||||
|
Ok(packet) => {
|
||||||
|
let packet_id = packet.packet_id();
|
||||||
|
let psc = packet.psc();
|
||||||
|
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
|
||||||
|
if let Ok(request) =
|
||||||
|
postcard::from_bytes::<stm32h7::Request>(packet.packet_data())
|
||||||
|
{
|
||||||
|
let response = match request {
|
||||||
|
stm32h7::Request::Ping => {
|
||||||
|
stm32h7::Response::Ok
|
||||||
|
}
|
||||||
|
stm32h7::Request::ChangeBlinkFrequency(duration) => {
|
||||||
|
cx.shared.blink_freq.lock(|current| {
|
||||||
|
*current = Duration::from_millis(duration.as_millis() as u64)
|
||||||
|
});
|
||||||
|
stm32h7::Response::Ok
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let sequence_count = cx.shared.sequence_count.lock(|v| {
|
||||||
|
let current = *v;
|
||||||
|
*v = v.wrapping_add(u14::new(1));
|
||||||
|
current
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send Pong/OK response immediately.
|
||||||
|
if let Err(e) =
|
||||||
|
send_tm(tc_packet_id, response, sequence_count, cx.local.tm_tx).await
|
||||||
|
{
|
||||||
|
defmt::warn!("Failed to send TM response: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => defmt::warn!("Failed to parse received TC packet: {}", e,),
|
||||||
|
}
|
||||||
|
defmt::info!("Received from UDP client: {}", tc.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_tm(
|
||||||
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
|
response: stm32h7::Response,
|
||||||
|
current_seq_count: u14,
|
||||||
|
sender: &embassy_sync::channel::Sender<
|
||||||
|
'static,
|
||||||
|
NoopRawMutex,
|
||||||
|
alloc::vec::Vec<u8>,
|
||||||
|
TM_QUEUE_DEPTH,
|
||||||
|
>,
|
||||||
|
) -> Result<(), CcsdsPacketCreationError> {
|
||||||
|
let sp_header = SpHeader::new_for_unseg_tc(stm32h7::PUS_APID, current_seq_count, 0);
|
||||||
|
let tm_header = TmHeader {
|
||||||
|
tc_packet_id: Some(tc_packet_id),
|
||||||
|
uptime_millis: embassy_time::Instant::now().as_millis(),
|
||||||
|
};
|
||||||
|
let tm_size = tm_size(&tm_header, &response);
|
||||||
|
let mut packet = alloc::vec![0; tm_size];
|
||||||
|
create_tm_packet(&mut packet, sp_header, tm_header, response)?;
|
||||||
|
sender.send(packet).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use satrs_stm32h7_nucleo_rtic as _; // memory layout + panic handler
|
||||||
|
|
||||||
|
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
|
||||||
|
// feature)
|
||||||
|
#[defmt_test::tests]
|
||||||
|
mod tests {
|
||||||
|
use defmt::assert;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
assert!(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "satrs-gen"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
toml = "0.8"
|
||||||
|
heck = "0.5"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[apid]
|
||||||
|
Sched = 1
|
||||||
|
GenericPus = 2
|
||||||
|
Acs = 3
|
||||||
|
Cfdp = 4
|
||||||
|
Tmtc = 5
|
||||||
|
Eps = 6
|
||||||
|
|
||||||
|
|
||||||
|
[ids]
|
||||||
|
[ids.Eps]
|
||||||
|
Pcdu = 0
|
||||||
|
Subsystem = 1
|
||||||
|
|
||||||
|
[ids.Tmtc]
|
||||||
|
UdpServer = 0
|
||||||
|
TcpServer = 1
|
||||||
|
|
||||||
|
[ids.GenericPus]
|
||||||
|
PusEventManagement = 0
|
||||||
|
PusRouting = 1
|
||||||
|
PusTest = 2
|
||||||
|
PusAction = 3
|
||||||
|
PusMode = 4
|
||||||
|
PusHk = 5
|
||||||
|
|
||||||
|
[ids.Sched]
|
||||||
|
PusSched = 0
|
||||||
|
|
||||||
|
[ids.Acs]
|
||||||
|
Subsystem = 1
|
||||||
|
Assembly = 2
|
||||||
|
Mgm0 = 3
|
||||||
|
Mgm1 = 4
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use heck::{ToShoutySnakeCase, ToSnakeCase};
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fs::{self, File},
|
||||||
|
io::{self, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use toml::{Value, map::Map};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
// Read the configuration file
|
||||||
|
let config_str = fs::read_to_string("components.toml").expect("Unable to read file");
|
||||||
|
let config: Value = toml::from_str(&config_str).expect("Unable to parse TOML");
|
||||||
|
|
||||||
|
let mut output = File::create("../satrs-example/src/ids.rs")?;
|
||||||
|
|
||||||
|
generate_rust_code(&config, &mut output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_enum_table(table_map: &Map<String, Value>) -> BTreeMap<u64, &str> {
|
||||||
|
// Collect entries into a BTreeMap to sort them by key
|
||||||
|
let mut sorted_entries: BTreeMap<u64, &str> = BTreeMap::new();
|
||||||
|
|
||||||
|
for (key, value) in table_map {
|
||||||
|
if let Some(value) = value.as_integer() {
|
||||||
|
if !(0..=0x7FF).contains(&value) {
|
||||||
|
panic!("Invalid APID value: {}", value);
|
||||||
|
}
|
||||||
|
sorted_entries.insert(value as u64, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sorted_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_rust_code(config: &Value, writer: &mut impl Write) {
|
||||||
|
writeln!(
|
||||||
|
writer,
|
||||||
|
"//! This is an auto-generated configuration module."
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(writer, "use satrs::request::UniqueApidTargetId;").unwrap();
|
||||||
|
writeln!(writer).unwrap();
|
||||||
|
|
||||||
|
// Generate the main module
|
||||||
|
writeln!(
|
||||||
|
writer,
|
||||||
|
"#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumIter)]"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(writer, "pub enum Apid {{").unwrap();
|
||||||
|
|
||||||
|
// Generate constants for the main module
|
||||||
|
if let Some(apid_table) = config.get("apid").and_then(Value::as_table) {
|
||||||
|
let sorted_entries = sort_enum_table(apid_table);
|
||||||
|
// Write the sorted entries to the writer
|
||||||
|
for (value, key) in sorted_entries {
|
||||||
|
writeln!(writer, " {} = {},", key, value).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(writer, "}}").unwrap();
|
||||||
|
|
||||||
|
// Generate ID tables.
|
||||||
|
if let Some(id_tables) = config.get("ids").and_then(Value::as_table) {
|
||||||
|
for (mod_name, table) in id_tables {
|
||||||
|
let mod_name_as_snake = mod_name.to_snake_case();
|
||||||
|
writeln!(writer).unwrap();
|
||||||
|
writeln!(writer, "pub mod {} {{", mod_name_as_snake).unwrap();
|
||||||
|
let sorted_entries = sort_enum_table(table.as_table().unwrap());
|
||||||
|
writeln!(writer, " #[derive(Debug, Copy, Clone, PartialEq, Eq)]").unwrap();
|
||||||
|
writeln!(writer, " pub enum Id {{").unwrap();
|
||||||
|
// Write the sorted entries to the writer
|
||||||
|
for (value, key) in &sorted_entries {
|
||||||
|
writeln!(writer, " {} = {},", key, value).unwrap();
|
||||||
|
}
|
||||||
|
writeln!(writer, " }}").unwrap();
|
||||||
|
writeln!(writer).unwrap();
|
||||||
|
|
||||||
|
for (_value, key) in sorted_entries {
|
||||||
|
let key_shouting = key.to_shouty_snake_case();
|
||||||
|
writeln!(
|
||||||
|
writer,
|
||||||
|
" pub const {}: super::UniqueApidTargetId = super::UniqueApidTargetId::new(super::Apid::{} as u16, Id::{} as u32);",
|
||||||
|
key_shouting, mod_name, key
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(writer, "}}").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||||
|
<!--Created by yEd 3.23.2-->
|
||||||
|
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||||
|
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||||
|
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||||
|
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||||
|
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||||
|
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||||
|
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||||
|
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||||
|
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||||
|
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||||
|
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||||
|
<graph edgedefault="directed" id="G">
|
||||||
|
<data key="d0" xml:space="preserve"/>
|
||||||
|
<node id="n0">
|
||||||
|
<data key="d5"/>
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="360.0" width="479.0" x="771.3047672479152" y="458.0"/>
|
||||||
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="237.5" y="178.0">
|
||||||
|
<y:LabelModel>
|
||||||
|
<y:SmartNodeLabelModel distance="4.0"/>
|
||||||
|
</y:LabelModel>
|
||||||
|
<y:ModelParameter>
|
||||||
|
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||||
|
</y:ModelParameter>
|
||||||
|
</y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n1">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="177.64799999999997" width="200.75199999999973" x="1037.5527672479152" y="470.15200000000027"/>
|
||||||
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.919921875" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n2">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="84.39999999999986" x="1068.8351781652768" y="508.2800000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.638671875" x="23.380664062499818" xml:space="preserve" y="8.015625">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n3">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="550.4800000000001"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.453125" x="13.973437499999818" xml:space="preserve" y="8.015625">Magnetometer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n4">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="594.9000000000001"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.83203125" x="15.783984374999818" xml:space="preserve" y="8.015625">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n5">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="783.4063563305535" y="545.2800000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.931640625" x="17.234179687499932" xml:space="preserve" y="8.015625">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n6">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="840.5407126611072" y="677.8000000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="105.05078125" x="7.674609374999932" xml:space="preserve" y="8.015625">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n7">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1005.2814253222144" y="677.8000000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.111328125" x="11.644335937499932" xml:space="preserve" y="8.015625">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n8">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="931.6174253222144" y="775.5920000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="38.740234375" x="40.82988281249993" xml:space="preserve" y="8.015625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<edge id="e0" source="n5" target="n3">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="-5.199999999999932"/>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e1" source="n5" target="n2">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="60.19999999999993" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
|
||||||
|
<y:Point x="1023.8695890826383" y="525.2800000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e2" source="n5" target="n4">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="48.72964366944643" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
|
||||||
|
<y:Point x="1023.8695890826383" y="611.9000000000001"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.955078125" x="12.686124396959713" xml:space="preserve" y="-22.50440429687478">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e3" source="n6" target="n5">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="-5.329643669446341" ty="0.0">
|
||||||
|
<y:Point x="900.7407126611072" y="628.5400000000002"/>
|
||||||
|
<y:Point x="838.2767126611071" y="628.5400000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.923828125" x="-87.89792405764274" xml:space="preserve" y="-40.606550292968564">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e4" source="n4" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="60.200000000000045" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="611.9000000000001"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e5" source="n3" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="567.4800000000001"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e6" source="n2" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="11.514125426161627" sy="-2.5781798912005343" tx="45.553752843062284" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="522.7018201087997"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="60.4140625" x="-2.4087265765670054" xml:space="preserve" y="145.1356018470808">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e7" source="n8" target="n6">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="-25.212712661107275" sy="0.0" tx="-11.264000000000124" ty="0.0">
|
||||||
|
<y:Point x="966.6047126611071" y="731.8000000000002"/>
|
||||||
|
<y:Point x="889.4767126611071" y="731.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="119.751953125" x="-132.27600022951788" xml:space="preserve" y="-32.03587548828091">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e8" source="n7" target="n8">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="29.18399999999997" sy="0.0" tx="24.28800000000001" ty="0.0">
|
||||||
|
<y:Point x="1094.6654253222143" y="731.8000000000002"/>
|
||||||
|
<y:Point x="1016.1054253222144" y="731.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.2421875" x="-62.15307370122309" xml:space="preserve" y="34.80927001953137">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e9" source="n5" target="n1">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="23.921741802203996" sy="-3.0501798912007416" tx="0.0" ty="-56.27417989120056">
|
||||||
|
<y:Point x="867.5280981327575" y="502.70182010879967"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.95703125" x="73.38950633588263" xml:space="preserve" y="-62.699758016200235">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
</graph>
|
||||||
|
<data key="d7">
|
||||||
|
<y:Resources/>
|
||||||
|
</data>
|
||||||
|
</graphml>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -166,7 +166,7 @@ Subsystem<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
|||||||
<y:Geometry height="30.0" width="125.0" x="1151.9280499999995" y="281.84403125000006"/>
|
<y:Geometry height="30.0" width="125.0" x="1151.9280499999995" y="281.84403125000006"/>
|
||||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.255859375" x="24.3720703125" xml:space="preserve" y="4.8515625">TM Funnel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.837890625" x="33.0810546875" xml:space="preserve" y="4.8515625">TM Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
<y:Shape type="rectangle"/>
|
<y:Shape type="rectangle"/>
|
||||||
</y:ShapeNode>
|
</y:ShapeNode>
|
||||||
</data>
|
</data>
|
||||||
@@ -260,7 +260,7 @@ Mode Tree<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
|||||||
<y:Geometry height="57.265600000000006" width="631.1152" x="810.8847999999999" y="411.39428125"/>
|
<y:Geometry height="57.265600000000006" width="631.1152" x="810.8847999999999" y="411.39428125"/>
|
||||||
<y:Fill hasColor="false" transparent="false"/>
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="261.8125" x="166.89412267941418" xml:space="preserve" y="3.144146301369915">satrs-satellite
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="261.8125" x="166.89412267941418" xml:space="preserve" y="3.144146301369915">satrs-minisim
|
||||||
Simulator based on asynchronix<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
Simulator based on asynchronix<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
<y:Shape type="rectangle"/>
|
<y:Shape type="rectangle"/>
|
||||||
</y:ShapeNode>
|
</y:ShapeNode>
|
||||||
@@ -272,7 +272,7 @@ Simulator based on asynchronix<y:LabelModel><y:SmartNodeLabelModel distance="4.0
|
|||||||
<y:Geometry height="50.0" width="631.1152000000002" x="810.8847999999998" y="476.2958625000002"/>
|
<y:Geometry height="50.0" width="631.1152000000002" x="810.8847999999998" y="476.2958625000002"/>
|
||||||
<y:Fill hasColor="false" transparent="false"/>
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="374.8359375" x="110.3824039294143" xml:space="preserve" y="0.12842465753431043">satrs-tmtc
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="374.8359375" x="110.3824039294143" xml:space="preserve" y="0.12842465753431043">pytmtc
|
||||||
Command-line interface based TMTC handling<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
Command-line interface based TMTC handling<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
<y:Shape type="rectangle"/>
|
<y:Shape type="rectangle"/>
|
||||||
</y:ShapeNode>
|
</y:ShapeNode>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
all: check build embedded test check-fmt clippy docs
|
||||||
|
|
||||||
|
check:
|
||||||
|
cargo check
|
||||||
|
cargo check -p satrs-example --no-default-features
|
||||||
|
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo nextest run --all-features
|
||||||
|
cargo test --doc --all-features
|
||||||
|
|
||||||
|
embedded: embedded-stm32h7 embedded-stm32f3
|
||||||
|
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
|
||||||
|
|
||||||
|
[working-directory:"embedded-examples/stm32h7-nucleo-rtic"]
|
||||||
|
embedded-stm32h7:
|
||||||
|
cargo build --target=thumbv7em-none-eabihf --release
|
||||||
|
|
||||||
|
[working-directory:"embedded-examples/stm32f3-disco-rtic"]
|
||||||
|
embedded-stm32f3:
|
||||||
|
cargo build --target=thumbv7em-none-eabihf --release
|
||||||
|
|
||||||
|
check-fmt:
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
docs-satrs:
|
||||||
|
RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc -p satrs --all-features
|
||||||
|
|
||||||
|
docs: docs-satrs
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -15,7 +15,7 @@ action commanding could look like.
|
|||||||
2. Target ID and Action String based. The target ID is the same as in the first proposal, but
|
2. Target ID and Action String based. The target ID is the same as in the first proposal, but
|
||||||
the unique action is identified by a string.
|
the unique action is identified by a string.
|
||||||
|
|
||||||
The framework provides an `ActionRequest` abstraction to model both of these cases.
|
The library provides an `ActionRequest` abstraction to model both of these cases.
|
||||||
|
|
||||||
## Commanding with ECSS PUS 8
|
## Commanding with ECSS PUS 8
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ it is still centered around small packets. `sat-rs` provides support for these E
|
|||||||
standards and also attempts to fill the gap to the internet protocol by providing the following
|
standards and also attempts to fill the gap to the internet protocol by providing the following
|
||||||
components.
|
components.
|
||||||
|
|
||||||
1. [UDP TMTC Server](https://docs.rs/satrs/latest/satrs/hal/host/udp_server/index.html).
|
1. [UDP TMTC Server](https://docs.rs/satrs/latest/satrs/hal/std/udp_server/index.html).
|
||||||
UDP is already packet based which makes it an excellent fit for exchanging space packets.
|
UDP is already packet based which makes it an excellent fit for exchanging space packets.
|
||||||
2. [TCP TMTC Server Components](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/index.html).
|
2. [TCP TMTC Server Components](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/index.html).
|
||||||
TCP is a stream based protocol, so the framework provides building blocks to parse telemetry
|
TCP is a stream based protocol, so the library provides building blocks to parse telemetry
|
||||||
from an arbitrary bytestream. Two concrete implementations are provided:
|
from an arbitrary bytestream. Two concrete implementations are provided:
|
||||||
- [TCP spacepackets server](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
|
- [TCP spacepackets server](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
|
||||||
to parse tightly packed CCSDS Spacepackets.
|
to parse tightly packed CCSDS Spacepackets.
|
||||||
@@ -39,8 +39,12 @@ task might be to store all arriving telemetry persistently. This is especially i
|
|||||||
space systems which do not have permanent contact like low-earth-orbit (LEO) satellites.
|
space systems which do not have permanent contact like low-earth-orbit (LEO) satellites.
|
||||||
|
|
||||||
The most important task of a TC source is to deliver the telecommands to the correct recipients.
|
The most important task of a TC source is to deliver the telecommands to the correct recipients.
|
||||||
For modern component oriented software using message passing, this usually includes staged
|
For component oriented software using message passing, this usually includes staged demultiplexing
|
||||||
demultiplexing components to determine where a command needs to be sent.
|
components to determine where a command needs to be sent.
|
||||||
|
|
||||||
|
Using a generic concept of a TC source and a TM sink as part of the software design simplifies
|
||||||
|
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receiver only have to
|
||||||
|
forward their generated or received packets to those handler objects.
|
||||||
|
|
||||||
# Low-level protocols and the bridge to the communcation subsystem
|
# Low-level protocols and the bridge to the communcation subsystem
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
Software for space systems oftentimes has different requirements than the software for host
|
Software for space systems oftentimes has different requirements than the software for host
|
||||||
systems or servers. Currently, most space systems are considered embedded systems.
|
systems or servers. Currently, most space systems are considered embedded systems.
|
||||||
|
|
||||||
For these systems, the computation power and the available heap are the most important resources
|
For these systems, the computation power and the available heap are important resources
|
||||||
which are constrained. This might make completeley heap based memory management schemes which
|
which are also constrained. This might make completeley heap based memory management schemes which
|
||||||
are oftentimes used on host and server based systems unfeasable. Still, completely forbidding
|
are oftentimes used on host and server based systems unfeasable. Still, completely forbidding
|
||||||
heap allocations might make software development unnecessarilly difficult, especially in a
|
heap allocations might make software development unnecessarilly difficult, especially in a
|
||||||
time where the OBSW might be running on Linux based systems with hundreds of MBs of RAM.
|
time where the OBSW might be running on Linux based systems with hundreds of MBs of RAM.
|
||||||
|
|
||||||
A useful pattern used commonly in space systems is to limit heap allocations to program
|
A useful pattern commonly used in space systems is to limit heap allocations to program
|
||||||
initialization time and avoid frequent run-time allocations. This prevents issues like
|
initialization time and avoid frequent run-time allocations. This prevents issues like
|
||||||
running out of memory (something even Rust can not protect from) or heap fragmentation on systems
|
running out of memory (something even Rust can not protect from) or heap fragmentation on systems
|
||||||
without a MMU.
|
without a MMU.
|
||||||
|
|
||||||
# Using pre-allocated pool structures
|
# Using pre-allocated pool structures
|
||||||
|
|
||||||
A huge candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
|
A candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
|
||||||
candidates where the data size might vary greatly. The regular solution for host systems
|
candidates where the data size might vary greatly. The regular solution for host systems
|
||||||
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
|
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
|
||||||
another solution to avoid run-time allocations by offering pre-allocated static
|
another solution to avoid run-time allocations by offering pre-allocated static
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# Framework Design
|
# Library Design
|
||||||
|
|
||||||
Satellites and space systems in general are complex systems with a wide range of requirements for
|
Satellites and space systems in general are complex systems with a wide range of requirements for
|
||||||
both the hardware and the software. Consequently, the general design of the framework is centered
|
both the hardware and the software. Consequently, the general design of the library is centered
|
||||||
around many light-weight components which try to impose as few restrictions as possible on how to
|
around many light-weight components which try to impose as few restrictions as possible on how to
|
||||||
solve certain problems.
|
solve certain problems. This is also the reason why sat-rs is explicitely called a library
|
||||||
|
instead of a framework.
|
||||||
|
|
||||||
There are still a lot of common patterns and architectures across these systems where guidance
|
There are still a lot of common patterns and architectures across these systems where guidance
|
||||||
of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls
|
of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls
|
||||||
which were already solved and to avoid boilerplate code. This framework tries to provide this
|
which were already solved and to avoid boilerplate code. This library tries to provide this
|
||||||
structure and guidance the following way:
|
structure and guidance the following way:
|
||||||
|
|
||||||
1. Providing this book which explains the architecture and design patterns in respect to common
|
1. Providing this book which explains the architecture and design patterns in respect to common
|
||||||
@@ -18,7 +19,7 @@ structure and guidance the following way:
|
|||||||
3. Providing a good test suite. This includes both unittests and integration tests. The integration
|
3. Providing a good test suite. This includes both unittests and integration tests. The integration
|
||||||
tests can also serve as smaller usage examples than the large `satrs-example` application.
|
tests can also serve as smaller usage examples than the large `satrs-example` application.
|
||||||
|
|
||||||
This framework has special support for standards used in the space industry. This especially
|
This library has special support for standards used in the space industry. This especially
|
||||||
includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European
|
includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European
|
||||||
Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards,
|
Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards,
|
||||||
but it is always recommended to use some sort of standard for interoperability.
|
but it is always recommended to use some sort of standard for interoperability.
|
||||||
@@ -30,10 +31,10 @@ Flying Laptop Project by the University of Stuttgart with Airbus Defence and Spa
|
|||||||
It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||||
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
|
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
|
||||||
Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs`
|
Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs`
|
||||||
framework.
|
library.
|
||||||
FLP is a medium-size small satellite with a higher budget and longer development time than EIVE,
|
FLP is a medium-size small satellite with a higher budget and longer development time than EIVE,
|
||||||
which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
|
which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
|
||||||
shorter development cycle and was built using cheaper COTS components. This framework also tries
|
shorter development cycle and was built using cheaper COTS components. This library also tries
|
||||||
to accumulate the knowledge of developing the OBSW and operating the satellite for both these
|
to accumulate the knowledge of developing the OBSW and operating the satellite for both these
|
||||||
different systems and provide a solution for a wider range of small satellite systems.
|
different systems and provide a solution for a wider range of small satellite systems.
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
# Events
|
# Events
|
||||||
|
|
||||||
Events can be an extremely important mechanism used for remote systems to monitor unexpected
|
Events are an important mechanism used for remote systems to monitor unexpected
|
||||||
or expected anomalies and events occuring on these systems. They are oftentimes tied to
|
or expected anomalies and events occuring on these systems.
|
||||||
|
|
||||||
|
One common use case for events on remote systems is to offer a light-weight publish-subscribe
|
||||||
|
mechanism and IPC mechanism for software and hardware events which are also packaged as telemetry
|
||||||
|
(TM) or can trigger a system response. They can also be tied to
|
||||||
Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously.
|
Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously.
|
||||||
|
|
||||||
Events can also be used as a convenient Inter-Process Communication (IPC) mechansism, which is
|
The PUS Service 5 standardizes how the ground interface for events might look like, but does not
|
||||||
also observable for the Ground segment. The PUS Service 5 standardizes how the ground interface
|
specify how other software components might react to those events. There is the PUS Service 19,
|
||||||
for events might look like, but does not specify how other software components might react
|
which might be used for that purpose, but the event components recommended by this framework do not
|
||||||
to those events. There is the PUS Service 19, which might be used for that purpose, but the
|
rely on the present of this service.
|
||||||
event components recommended by this framework do not really need this service.
|
|
||||||
|
|
||||||
The following images shows how the flow of events could look like in a system where components
|
The following images shows how the flow of events could look like in a system where components
|
||||||
can generate events, and where other system components might be interested in those events:
|
can generate events, and where other system components might be interested in those events:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
For the concrete implementation of your own event management and/or event routing system, you
|
||||||
|
can have a look at the event management documentation inside the
|
||||||
|
[API documentation](https://docs.rs/satrs/latest/satrs/event_man/index.html) where you can also
|
||||||
|
find references to all examples.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# sat-rs Example Application
|
# sat-rs Example Application
|
||||||
|
|
||||||
The `sat-rs` framework includes a monolithic example application which can be found inside
|
The `sat-rs` library includes a monolithic example application which can be found inside
|
||||||
the [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
the [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||||
subdirectory of the repository. The primary purpose of this example application is to show how
|
subdirectory of the repository. The primary purpose of this example application is to show how
|
||||||
the various components of the sat-rs framework could be used as part of a larger on-board
|
the various components of the sat-rs framework could be used as part of a larger on-board
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
The sat-rs book
|
The sat-rs book
|
||||||
======
|
======
|
||||||
|
|
||||||
This book is the primary information resource for the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
This book is the primary information resource for the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||||
in addition to the regular API documentation. It contains the following resources:
|
in addition to the regular API documentation. It contains the following resources:
|
||||||
|
|
||||||
1. Architecture informations and consideration which would exceeds the scope of the regular API.
|
1. Architecture informations and consideration which would exceeds the scope of the regular API.
|
||||||
2. General information on how to build On-Board Software and how `sat-rs` can help to fulfill
|
2. General information on how to build on-board Software and how `sat-rs` can help to fulfill
|
||||||
the unique requirements of writing software for remote systems.
|
the unique requirements of writing software for remote systems.
|
||||||
2. A Getting-Started workshop where a small On-Board Software is built from scratch using
|
|
||||||
sat-rs components.
|
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
The primary goal of the sat-rs framework is to provide re-usable components
|
The primary goal of the sat-rs library is to provide re-usable components
|
||||||
to write on-board software for remote systems like rovers or satellites. It is specifically written
|
to write on-board software for remote systems like rovers or satellites. It is specifically written
|
||||||
for the special requirements for these systems.
|
for the special requirements for these systems.
|
||||||
|
|
||||||
|
It should be noted that sat-rs is early-stage software. Important features are missing. New releases
|
||||||
|
with breaking changes are released regularly, with all changes documented inside respective
|
||||||
|
changelog files. You should only use this library if your are willing to work in this
|
||||||
|
environment.
|
||||||
|
|
||||||
A lot of the architecture and general design considerations are based on the
|
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
|
||||||
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||||
@@ -26,4 +29,21 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
|
|||||||
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||||
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
||||||
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
||||||
the example application.
|
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||||
|
applicatin complements the example application and can be used to simulate some physical devices
|
||||||
|
for the `satrs-example` device handlers.
|
||||||
|
|
||||||
|
# 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/).
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Modes
|
# Modes
|
||||||
|
|
||||||
Modes are an extremely useful concept for complex system in general. They also allow simplified
|
Modes are an extremely useful concept to model complex systems. They allow simplified
|
||||||
system reasoning for both system operators and OBSW developers. They model the behaviour of a
|
system reasoning for both system operators and OBSW developers. They also provide a way to alter
|
||||||
component and also provide observability of a system. A few examples of how to model
|
the behaviour of a component and also provide observability of a system. A few examples of how to
|
||||||
different components of a space system with modes will be given.
|
model the mode of different components within a space system with modes will be given.
|
||||||
|
|
||||||
## Modelling a pyhsical devices with modes
|
## Pyhsical device component with modes
|
||||||
|
|
||||||
The following simple mode scheme with the following three mode
|
The following simple mode scheme with the following three mode
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ The following simple mode scheme with the following three mode
|
|||||||
- `ON`
|
- `ON`
|
||||||
- `NORMAL`
|
- `NORMAL`
|
||||||
|
|
||||||
can be applied to a large number of simpler devices of a remote system, for example sensors.
|
can be applied to a large number of simpler device controllers of a remote system, for example
|
||||||
|
sensors.
|
||||||
|
|
||||||
1. `OFF` means that a device is physically switched off, and the corresponding software component
|
1. `OFF` means that a device is physically switched off, and the corresponding software component
|
||||||
does not poll the device regularly.
|
does not poll the device regularly.
|
||||||
@@ -31,7 +32,7 @@ for the majority of devices:
|
|||||||
2. `NORMAL` or `ON` to `OFF`: Any important shutdown configuration or handling must be performed
|
2. `NORMAL` or `ON` to `OFF`: Any important shutdown configuration or handling must be performed
|
||||||
before powering off the device.
|
before powering off the device.
|
||||||
|
|
||||||
## Modelling a controller with modes
|
## Controller components with modes
|
||||||
|
|
||||||
Controller components are not modelling physical devices, but a mode scheme is still the best
|
Controller components are not modelling physical devices, but a mode scheme is still the best
|
||||||
way to model most of these components.
|
way to model most of these components.
|
||||||
|
|||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
/*
|
|
||||||
* Requires the Rust Language Server (RLS) and Cortex-Debug extensions
|
|
||||||
* https://marketplace.visualstudio.com/items?itemName=rust-lang.rust
|
|
||||||
* https://marketplace.visualstudio.com/items?itemName=marus25.cortex-debug
|
|
||||||
*/
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
/* Launches debug session for currently open example */
|
|
||||||
"type": "cortex-debug",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug",
|
|
||||||
"servertype": "openocd",
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"preLaunchTask": "cargo build",
|
|
||||||
"runToEntryPoint": "true",
|
|
||||||
"executable": "./target/thumbv7em-none-eabihf/debug/satrs-example-stm32f3-disco",
|
|
||||||
"preLaunchCommands": ["break rust_begin_unwind"],
|
|
||||||
"device": "STM32F303VCT6",
|
|
||||||
"configFiles": [
|
|
||||||
"${workspaceRoot}/.vscode/openocd-helpers.tcl",
|
|
||||||
"interface/stlink.cfg",
|
|
||||||
"target/stm32f3x.cfg"
|
|
||||||
],
|
|
||||||
"svdFile": "${env:HOME}/.svd/STM32F303.svd",
|
|
||||||
"swoConfig": {
|
|
||||||
"enabled": true,
|
|
||||||
"cpuFrequency": 8000000,
|
|
||||||
"swoFrequency": 2000000,
|
|
||||||
"source": "probe",
|
|
||||||
"decoders": [
|
|
||||||
{ "type": "console", "label": "ITM", "port": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
/* Launches debug session for currently open example */
|
|
||||||
"type": "cortex-debug",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Release",
|
|
||||||
"servertype": "openocd",
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"preLaunchTask": "cargo build",
|
|
||||||
"runToEntryPoint": "true",
|
|
||||||
"executable": "./target/thumbv7em-none-eabihf/release/satrs-example-stm32f3-disco",
|
|
||||||
"preLaunchCommands": ["break rust_begin_unwind"],
|
|
||||||
"device": "STM32F303VCT6",
|
|
||||||
"configFiles": [
|
|
||||||
"${workspaceRoot}/.vscode/openocd-helpers.tcl",
|
|
||||||
"interface/stlink.cfg",
|
|
||||||
"target/stm32f3x.cfg"
|
|
||||||
],
|
|
||||||
"svdFile": "${env:HOME}/.svd/STM32F303.svd",
|
|
||||||
"swoConfig": {
|
|
||||||
"enabled": true,
|
|
||||||
"cpuFrequency": 8000000,
|
|
||||||
"swoFrequency": 2000000,
|
|
||||||
"source": "probe",
|
|
||||||
"decoders": [
|
|
||||||
{ "type": "console", "label": "ITM", "port": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"cortex-debug.gdbPath.linux": "gdb-multiarch"
|
|
||||||
}
|
|
||||||
Generated
-762
@@ -1,762 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "accelerometer"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a4586d95cb0695e748760c9a751141eebb68265b1b20392a0f14db608679f7a"
|
|
||||||
dependencies = [
|
|
||||||
"micromath",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomic-polyfill"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28"
|
|
||||||
dependencies = [
|
|
||||||
"critical-section",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bare-metal"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
|
|
||||||
dependencies = [
|
|
||||||
"rustc_version 0.2.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bare-metal"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitfield"
|
|
||||||
version = "0.13.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bxcan"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b13b4b2ea9ab2ba924063ebb86ad895cb79f4a79bf90f27949eb20c335b30f9"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"nb 1.0.0",
|
|
||||||
"vcell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cast"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
|
|
||||||
dependencies = [
|
|
||||||
"rustc_version 0.4.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chrono"
|
|
||||||
version = "0.4.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cortex-m"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70858629a458fdfd39f9675c4dc309411f2a3f83bede76988d81bf1a0ecee9e0"
|
|
||||||
dependencies = [
|
|
||||||
"bare-metal 0.2.5",
|
|
||||||
"bitfield",
|
|
||||||
"embedded-hal",
|
|
||||||
"volatile-register",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cortex-m-rt"
|
|
||||||
version = "0.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6d3328b8b5534f0c90acd66b68950f2763b37e0173cac4d8b4937c4a80761f9"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m-rt-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cortex-m-rt-macros"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cortex-m-rtic"
|
|
||||||
version = "1.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6b82f1c39acd6c3a35c2013b6110c20f5bc534522791fabadeed49ccada2dce"
|
|
||||||
dependencies = [
|
|
||||||
"bare-metal 1.0.0",
|
|
||||||
"cortex-m",
|
|
||||||
"cortex-m-rtic-macros",
|
|
||||||
"heapless",
|
|
||||||
"rtic-core",
|
|
||||||
"rtic-monotonic",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cortex-m-rtic-macros"
|
|
||||||
version = "1.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e8e9645ef54bec1cf70ac33e9bf9566e6507ab5b41ae6baf3735662194e8607"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-error",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rtic-syntax",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "critical-section"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.14.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.14.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.14.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embedded-dma"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
|
|
||||||
dependencies = [
|
|
||||||
"stable_deref_trait",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embedded-hal"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
|
|
||||||
dependencies = [
|
|
||||||
"nb 0.1.3",
|
|
||||||
"void",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embedded-time"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d7a4b4d10ac48d08bfe3db7688c402baadb244721f30a77ce360bd24c3dffe58"
|
|
||||||
dependencies = [
|
|
||||||
"num",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enumset"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19be8061a06ab6f3a6cf21106c873578bf01bd42ad15e0311a9c76161cb1c753"
|
|
||||||
dependencies = [
|
|
||||||
"enumset_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enumset_derive"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03e7b551eba279bf0fa88b83a46330168c1560a52a94f5126f892f0b364ab3e0"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fnv"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fugit"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7ab17bb279def6720d058cb6c052249938e7f99260ab534879281a95367a87e5"
|
|
||||||
dependencies = [
|
|
||||||
"gcd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gcd"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4b1b088ad0a967aa29540456b82fc8903f854775d33f71e9709c4efb3dfbfd2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.11.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "667f6ea017b297ec65b8a108c6e9ad6879460721fb3b6b23abf690970147fc28"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.14.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hash32"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heapless"
|
|
||||||
version = "0.7.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743"
|
|
||||||
dependencies = [
|
|
||||||
"atomic-polyfill",
|
|
||||||
"hash32",
|
|
||||||
"rustc_version 0.4.0",
|
|
||||||
"spin",
|
|
||||||
"stable_deref_trait",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itm_logger"
|
|
||||||
version = "0.1.3-pre.0"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lsm303dlhc"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e5d1a5c290951321d1b0d4a40edd828537de9889134a0e67c5146542ae57706"
|
|
||||||
dependencies = [
|
|
||||||
"cast",
|
|
||||||
"embedded-hal",
|
|
||||||
"generic-array 0.11.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "micromath"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc4010833aea396656c2f91ee704d51a6f1329ec2ab56ffd00bfd56f7481ea94"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nb"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
|
|
||||||
dependencies = [
|
|
||||||
"nb 1.0.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nb"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "546c37ac5d9e56f55e73b677106873d9d9f5190605e41a856503623648488cae"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
|
|
||||||
dependencies = [
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-complex"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-integer"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-iter"
|
|
||||||
version = "0.1.43"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-rational"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "panic-itm"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3d577d97d1b31268087b6dddf2470e6794ef5eee87d9dca7fcd0481695391a4c"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-error-attr",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error-attr"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.49"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtcc"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3623619ce77c09a7d87cf7c61c5c887b9c7dee8805f66af6c4aa5824be4d9930"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtic-core"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9369355b04d06a3780ec0f51ea2d225624db777acbc60abd8ca4832da5c1a42"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtic-monotonic"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fb8b0b822d1a366470b9cea83a1d4e788392db763539dc4ba022bcc787fece82"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtic-syntax"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3ad3ae243dd8d0a1b064615f664d4fa7e63929939074c564cbe5efdc4c503065"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
|
||||||
dependencies = [
|
|
||||||
"semver 0.9.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
|
||||||
dependencies = [
|
|
||||||
"semver 1.0.16",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sat-rs-example-stm32f-disco"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m",
|
|
||||||
"cortex-m-rt",
|
|
||||||
"cortex-m-rtic",
|
|
||||||
"embedded-hal",
|
|
||||||
"enumset",
|
|
||||||
"heapless",
|
|
||||||
"itm_logger",
|
|
||||||
"panic-itm",
|
|
||||||
"stm32f3-discovery",
|
|
||||||
"stm32f3xx-hal",
|
|
||||||
"systick-monotonic",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
|
||||||
dependencies = [
|
|
||||||
"semver-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver-parser"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slice-group-by"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stable_deref_trait"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stm32-usbd"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6c94998f166d66b210a164648a0b7866428d8f1e0740bf8a4c5edd89d4750c1"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m",
|
|
||||||
"usb-device",
|
|
||||||
"vcell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stm32f3"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "265cda62ac13307414de4aca58dbbbd8038ddba85cffbb335823aa216f2e3200"
|
|
||||||
dependencies = [
|
|
||||||
"bare-metal 1.0.0",
|
|
||||||
"cortex-m",
|
|
||||||
"cortex-m-rt",
|
|
||||||
"vcell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stm32f3-discovery"
|
|
||||||
version = "0.8.0-pre.0"
|
|
||||||
dependencies = [
|
|
||||||
"accelerometer",
|
|
||||||
"cortex-m",
|
|
||||||
"cortex-m-rt",
|
|
||||||
"lsm303dlhc",
|
|
||||||
"stm32f3xx-hal",
|
|
||||||
"switch-hal",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stm32f3xx-hal"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e422c5c044e8f3a068b1e14b83c071449e27c9d4bc0e24f972b552d79f2be03"
|
|
||||||
dependencies = [
|
|
||||||
"bare-metal 1.0.0",
|
|
||||||
"bxcan",
|
|
||||||
"cfg-if",
|
|
||||||
"cortex-m",
|
|
||||||
"cortex-m-rt",
|
|
||||||
"embedded-dma",
|
|
||||||
"embedded-hal",
|
|
||||||
"embedded-time",
|
|
||||||
"enumset",
|
|
||||||
"nb 1.0.0",
|
|
||||||
"paste",
|
|
||||||
"rtcc",
|
|
||||||
"slice-group-by",
|
|
||||||
"stm32-usbd",
|
|
||||||
"stm32f3",
|
|
||||||
"void",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "switch-hal"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90a4adc8cbd1726249b161898e48e0f3f1ce74d34dc784cbbc98fba4ed283fbf"
|
|
||||||
dependencies = [
|
|
||||||
"embedded-hal",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "1.0.107"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "systick-monotonic"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67fb822d5c615a0ae3a4795ee5b1d06381c7faf488d861c0a4fa8e6a88d5ff84"
|
|
||||||
dependencies = [
|
|
||||||
"cortex-m",
|
|
||||||
"fugit",
|
|
||||||
"rtic-monotonic",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typenum"
|
|
||||||
version = "1.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "usb-device"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1f6cc3adc849b5292b4075fc0d5fdcf2f24866e88e336dd27a8943090a520508"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vcell"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "void"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "volatile-register"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ee8f19f9d74293faf70901bc20ad067dc1ad390d2cbf1e3f75f721ffee908b6"
|
|
||||||
dependencies = [
|
|
||||||
"vcell",
|
|
||||||
]
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "satrs-example-stm32f3-disco"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cortex-m = "0.7"
|
|
||||||
cortex-m-rt = "0.7"
|
|
||||||
embedded-hal = "0.2.6"
|
|
||||||
cortex-m-rtic = "1.0"
|
|
||||||
enumset = "1.0"
|
|
||||||
heapless = "0.7"
|
|
||||||
systick-monotonic = "1.0"
|
|
||||||
|
|
||||||
[dependencies.cobs]
|
|
||||||
git = "https://github.com/robamu/cobs.rs.git"
|
|
||||||
branch = "all_features"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[dependencies.panic-itm]
|
|
||||||
version = "0.4"
|
|
||||||
|
|
||||||
[dependencies.itm_logger]
|
|
||||||
git = "https://github.com/robamu/itm_logger.rs.git"
|
|
||||||
branch = "all_features"
|
|
||||||
version = "0.1.3-alpha.0"
|
|
||||||
|
|
||||||
[dependencies.stm32f3xx-hal]
|
|
||||||
git = "https://github.com/robamu/stm32f3xx-hal"
|
|
||||||
version = "0.10.0-alpha.0"
|
|
||||||
features = ["stm32f303xc", "rt", "enumset"]
|
|
||||||
branch = "all_features"
|
|
||||||
# Can be used in workspace to develop and update HAL
|
|
||||||
# path = "../stm32f3xx-hal"
|
|
||||||
|
|
||||||
[dependencies.stm32f3-discovery]
|
|
||||||
git = "https://github.com/robamu/stm32f3-discovery"
|
|
||||||
version = "0.8.0-alpha.0"
|
|
||||||
branch = "all_features"
|
|
||||||
# Can be used in workspace to develop and update BSP
|
|
||||||
# path = "../stm32f3-discovery"
|
|
||||||
|
|
||||||
[dependencies.satrs-core]
|
|
||||||
git = "https://egit.irs.uni-stuttgart.de/rust/satrs-core.git"
|
|
||||||
version = "0.1.0-alpha.0"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
# this lets you use `cargo fix`!
|
|
||||||
# [[bin]]
|
|
||||||
# name = "stm32f3-blinky"
|
|
||||||
# test = false
|
|
||||||
# bench = false
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
codegen-units = 1 # better optimizations
|
|
||||||
debug = true # symbols are nice and they don't increase the size on Flash
|
|
||||||
lto = true # better optimizations
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
sat-rs example for the STM32F3-Discovery board
|
|
||||||
=======
|
|
||||||
|
|
||||||
This example application shows how the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad)
|
|
||||||
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/1/book/en/) as the concurrency
|
|
||||||
framework.
|
|
||||||
|
|
||||||
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
|
|
||||||
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
|
|
||||||
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
|
|
||||||
to embedded Rust.
|
|
||||||
|
|
||||||
## Preparing Rust and the repository
|
|
||||||
|
|
||||||
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
|
||||||
If you have not installed it yet, you can do so with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rustup target add thumbv7em-none-eabihf
|
|
||||||
```
|
|
||||||
|
|
||||||
A default `.cargo` config file is provided for this project, but needs to be copied to have
|
|
||||||
the correct name. This is so that the config file can be updated or edited for custom needs
|
|
||||||
without being tracked by git.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cp def_config.toml config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
The configuration file will also set the target so it does not always have to be specified with
|
|
||||||
the `--target` argument.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
|
||||||
you can simply build the application with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flashing and Debugging from the command line
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Debugging with VS Code
|
|
||||||
|
|
||||||
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
|
|
||||||
the board is a Mini-USB cable. The code in this repository was debugged using `openocd`
|
|
||||||
and the VS Code [`Cortex-Debug` plugin](https://marketplace.visualstudio.com/items?itemName=marus25.cortex-debug).
|
|
||||||
|
|
||||||
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
|
|
||||||
to automatically rebuild and flash your application.
|
|
||||||
|
|
||||||
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
|
|
||||||
the folder in VS code or adding it to a workspace.
|
|
||||||
|
|
||||||
If you would like to use a custom GDB application, you can specify the gdb binary in the following
|
|
||||||
configuration variables in your `settings.json`:
|
|
||||||
|
|
||||||
- `"cortex-debug.gdbPath"`
|
|
||||||
- `"cortex-debug.gdbPath.linux"`
|
|
||||||
- `"cortex-debug.gdbPath.windows"`
|
|
||||||
- `"cortex-debug.gdbPath.osx"`
|
|
||||||
|
|
||||||
## Commanding with Python
|
|
||||||
|
|
||||||
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
|
|
||||||
using COBS encoded CCSDS packets.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
- How and where to connect serial interface on the MCU
|
|
||||||
- How to set up Python venv (or at least strongly recommend it) and install deps
|
|
||||||
- How to copy `def_tmtc_conf.json` to `tmtc_conf.json` and adapt it for custom serial port
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Put the linker script somewhere the linker can find it
|
|
||||||
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
|
||||||
File::create(out.join("memory.x"))
|
|
||||||
.unwrap()
|
|
||||||
.write_all(include_bytes!("memory.x"))
|
|
||||||
.unwrap();
|
|
||||||
println!("cargo:rustc-link-search={}", out.display());
|
|
||||||
|
|
||||||
// Only re-run the build script when memory.x is changed,
|
|
||||||
// instead of when any part of the source code changes.
|
|
||||||
println!("cargo:rerun-if-changed=memory.x");
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
target extended-remote localhost:2331
|
|
||||||
|
|
||||||
monitor reset
|
|
||||||
|
|
||||||
# *try* to stop at the user entry point (it might be gone due to inlining)
|
|
||||||
break main
|
|
||||||
|
|
||||||
load
|
|
||||||
|
|
||||||
continue
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Sample OpenOCD configuration for the STM32F3DISCOVERY development board
|
|
||||||
|
|
||||||
# Depending on the hardware revision you got you'll have to pick ONE of these
|
|
||||||
# interfaces. At any time only one interface should be commented out.
|
|
||||||
|
|
||||||
# Revision C (newer revision)
|
|
||||||
source [find interface/stlink.cfg]
|
|
||||||
|
|
||||||
# Revision A and B (older revisions)
|
|
||||||
# source [find interface/stlink-v2.cfg]
|
|
||||||
|
|
||||||
source [find target/stm32f3x.cfg]
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
target extended-remote :3333
|
|
||||||
|
|
||||||
# print demangled symbols
|
|
||||||
set print asm-demangle on
|
|
||||||
|
|
||||||
# set backtrace limit to not have infinite backtrace loops
|
|
||||||
set backtrace limit 32
|
|
||||||
|
|
||||||
# detect unhandled exceptions, hard faults and panics
|
|
||||||
break DefaultHandler
|
|
||||||
break HardFault
|
|
||||||
break rust_begin_unwind
|
|
||||||
# # run the next few lines so the panic message is printed immediately
|
|
||||||
# # the number needs to be adjusted for your panic handler
|
|
||||||
# commands $bpnum
|
|
||||||
# next 4
|
|
||||||
# end
|
|
||||||
|
|
||||||
# *try* to stop at the user entry point (it might be gone due to inlining)
|
|
||||||
break main
|
|
||||||
|
|
||||||
# monitor arm semihosting enable
|
|
||||||
|
|
||||||
# # send captured ITM to the file itm.fifo
|
|
||||||
# # (the microcontroller SWO pin must be connected to the programmer SWO pin)
|
|
||||||
# # 8000000 must match the core clock frequency
|
|
||||||
monitor tpiu config internal itm.txt uart off 8000000
|
|
||||||
|
|
||||||
# # OR: make the microcontroller SWO pin output compatible with UART (8N1)
|
|
||||||
# # 8000000 must match the core clock frequency
|
|
||||||
# # 2000000 is the frequency of the SWO pin
|
|
||||||
# monitor tpiu config external uart off 8000000 2000000
|
|
||||||
|
|
||||||
# # enable ITM port 0
|
|
||||||
monitor itm port 0 on
|
|
||||||
|
|
||||||
load
|
|
||||||
|
|
||||||
# start the process but immediately halt the processor
|
|
||||||
stepi
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/venv
|
|
||||||
/log
|
|
||||||
/.idea/*
|
|
||||||
!/.idea/runConfigurations
|
|
||||||
|
|
||||||
/seqcnt.txt
|
|
||||||
/tmtc_conf.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"com_if": "serial_cobs",
|
|
||||||
"serial_baudrate": 115200
|
|
||||||
}
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Example client for the sat-rs example application"""
|
|
||||||
import enum
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from typing import Optional, cast
|
|
||||||
|
|
||||||
import tmtccmd
|
|
||||||
from spacepackets.ecss import PusTelemetry, PusTelecommand, PusVerificator
|
|
||||||
from spacepackets.ecss.pus_17_test import Service17Tm
|
|
||||||
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
|
|
||||||
|
|
||||||
from tmtccmd import CcsdsTmtcBackend, TcHandlerBase, ProcedureParamsWrapper
|
|
||||||
from tmtccmd.core.base import BackendRequest
|
|
||||||
from tmtccmd.pus import VerificationWrapper
|
|
||||||
from tmtccmd.tm import CcsdsTmHandler, SpecificApidHandlerBase
|
|
||||||
from tmtccmd.com_if import ComInterface
|
|
||||||
from tmtccmd.config import (
|
|
||||||
default_json_path,
|
|
||||||
SetupParams,
|
|
||||||
TmTcCfgHookBase,
|
|
||||||
TmtcDefinitionWrapper,
|
|
||||||
CoreServiceList,
|
|
||||||
OpCodeEntry,
|
|
||||||
params_to_procedure_conversion,
|
|
||||||
)
|
|
||||||
from tmtccmd.config.com_if import SerialCfgWrapper
|
|
||||||
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
|
|
||||||
from tmtccmd.logging import get_console_logger
|
|
||||||
from tmtccmd.logging.pus import (
|
|
||||||
RegularTmtcLogWrapper,
|
|
||||||
RawTmtcTimedLogWrapper,
|
|
||||||
TimedLogWhen,
|
|
||||||
)
|
|
||||||
from tmtccmd.tc import (
|
|
||||||
TcQueueEntryType,
|
|
||||||
ProcedureWrapper,
|
|
||||||
TcProcedureType,
|
|
||||||
FeedWrapper,
|
|
||||||
SendCbParams,
|
|
||||||
DefaultPusQueueHelper,
|
|
||||||
)
|
|
||||||
from tmtccmd.tm.pus_5_event import Service5Tm
|
|
||||||
from tmtccmd.util import FileSeqCountProvider, PusFileSeqCountProvider
|
|
||||||
from tmtccmd.util.obj_id import ObjectIdDictT
|
|
||||||
|
|
||||||
from tmtccmd.util.tmtc_printer import FsfwTmTcPrinter
|
|
||||||
|
|
||||||
LOGGER = get_console_logger()
|
|
||||||
|
|
||||||
EXAMPLE_PUS_APID = 0x02
|
|
||||||
|
|
||||||
|
|
||||||
class SatRsConfigHook(TmTcCfgHookBase):
|
|
||||||
def __init__(self, json_cfg_path: str):
|
|
||||||
super().__init__(json_cfg_path=json_cfg_path)
|
|
||||||
|
|
||||||
def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
|
|
||||||
from tmtccmd.config.com_if import (
|
|
||||||
create_com_interface_default,
|
|
||||||
create_com_interface_cfg_default,
|
|
||||||
)
|
|
||||||
|
|
||||||
cfg = create_com_interface_cfg_default(
|
|
||||||
com_if_key=com_if_key,
|
|
||||||
json_cfg_path=self.cfg_path,
|
|
||||||
space_packet_ids=None,
|
|
||||||
)
|
|
||||||
if cfg is None:
|
|
||||||
raise ValueError(
|
|
||||||
f"No valid configuration could be retrieved for the COM IF with key {com_if_key}"
|
|
||||||
)
|
|
||||||
if cfg.com_if_key == "serial_cobs":
|
|
||||||
cfg = cast(SerialCfgWrapper, cfg)
|
|
||||||
cfg.serial_cfg.serial_timeout = 0.5
|
|
||||||
return create_com_interface_default(cfg)
|
|
||||||
|
|
||||||
def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
|
|
||||||
from tmtccmd.config.globals import get_default_tmtc_defs
|
|
||||||
|
|
||||||
defs = get_default_tmtc_defs()
|
|
||||||
srv_5 = OpCodeEntry()
|
|
||||||
srv_5.add("0", "Event Test")
|
|
||||||
defs.add_service(
|
|
||||||
name=CoreServiceList.SERVICE_5.value,
|
|
||||||
info="PUS Service 5 Event",
|
|
||||||
op_code_entry=srv_5,
|
|
||||||
)
|
|
||||||
srv_17 = OpCodeEntry()
|
|
||||||
srv_17.add("0", "Ping Test")
|
|
||||||
defs.add_service(
|
|
||||||
name=CoreServiceList.SERVICE_17_ALT,
|
|
||||||
info="PUS Service 17 Test",
|
|
||||||
op_code_entry=srv_17,
|
|
||||||
)
|
|
||||||
srv_3 = OpCodeEntry()
|
|
||||||
defs.add_service(
|
|
||||||
name=CoreServiceList.SERVICE_3,
|
|
||||||
info="PUS Service 3 Housekeeping",
|
|
||||||
op_code_entry=srv_3,
|
|
||||||
)
|
|
||||||
return defs
|
|
||||||
|
|
||||||
def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int):
|
|
||||||
LOGGER.info("Mode operation hook was called")
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_object_ids(self) -> ObjectIdDictT:
|
|
||||||
from tmtccmd.config.objects import get_core_object_ids
|
|
||||||
|
|
||||||
return get_core_object_ids()
|
|
||||||
|
|
||||||
|
|
||||||
class PusHandler(SpecificApidHandlerBase):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
verif_wrapper: VerificationWrapper,
|
|
||||||
printer: FsfwTmTcPrinter,
|
|
||||||
raw_logger: RawTmtcTimedLogWrapper,
|
|
||||||
):
|
|
||||||
super().__init__(EXAMPLE_PUS_APID, None)
|
|
||||||
self.printer = printer
|
|
||||||
self.raw_logger = raw_logger
|
|
||||||
self.verif_wrapper = verif_wrapper
|
|
||||||
|
|
||||||
def handle_tm(self, packet: bytes, _user_args: any):
|
|
||||||
try:
|
|
||||||
tm_packet = PusTelemetry.unpack(packet)
|
|
||||||
except ValueError as e:
|
|
||||||
LOGGER.warning("Could not generate PUS TM object from raw data")
|
|
||||||
LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
|
|
||||||
raise e
|
|
||||||
service = tm_packet.service
|
|
||||||
dedicated_handler = False
|
|
||||||
if service == 1:
|
|
||||||
tm_packet = Service1Tm.unpack(data=packet, params=UnpackParams(1, 2))
|
|
||||||
res = self.verif_wrapper.add_tm(tm_packet)
|
|
||||||
if res is None:
|
|
||||||
LOGGER.info(
|
|
||||||
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
|
|
||||||
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
|
|
||||||
)
|
|
||||||
LOGGER.warning(
|
|
||||||
f"No matching telecommand found for {tm_packet.tc_req_id}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.verif_wrapper.log_to_console(tm_packet, res)
|
|
||||||
self.verif_wrapper.log_to_file(tm_packet, res)
|
|
||||||
dedicated_handler = True
|
|
||||||
if service == 3:
|
|
||||||
LOGGER.info("No handling for HK packets implemented")
|
|
||||||
LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
|
|
||||||
pus_tm = PusTelemetry.unpack(packet)
|
|
||||||
if pus_tm.subservice == 25:
|
|
||||||
if len(pus_tm.source_data) < 8:
|
|
||||||
raise ValueError("No addressable ID in HK packet")
|
|
||||||
json_str = pus_tm.source_data[8:]
|
|
||||||
dedicated_handler = True
|
|
||||||
if service == 5:
|
|
||||||
tm_packet = Service5Tm.unpack(packet)
|
|
||||||
if service == 17:
|
|
||||||
tm_packet = Service17Tm.unpack(packet)
|
|
||||||
dedicated_handler = True
|
|
||||||
if tm_packet.subservice == 2:
|
|
||||||
self.printer.file_logger.info("Received Ping Reply TM[17,2]")
|
|
||||||
LOGGER.info("Received Ping Reply TM[17,2]")
|
|
||||||
else:
|
|
||||||
self.printer.file_logger.info(
|
|
||||||
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
|
|
||||||
)
|
|
||||||
LOGGER.info(
|
|
||||||
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
|
|
||||||
)
|
|
||||||
if tm_packet is None:
|
|
||||||
LOGGER.info(
|
|
||||||
f"The service {service} is not implemented in Telemetry Factory"
|
|
||||||
)
|
|
||||||
tm_packet = PusTelemetry.unpack(packet)
|
|
||||||
self.raw_logger.log_tm(tm_packet)
|
|
||||||
if not dedicated_handler and tm_packet is not None:
|
|
||||||
self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet)
|
|
||||||
|
|
||||||
|
|
||||||
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
|
|
||||||
byte_string = bytearray(struct.pack("!I", target_id))
|
|
||||||
byte_string.extend(struct.pack("!I", unique_id))
|
|
||||||
return byte_string
|
|
||||||
|
|
||||||
|
|
||||||
class TcHandler(TcHandlerBase):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
seq_count_provider: FileSeqCountProvider,
|
|
||||||
verif_wrapper: VerificationWrapper,
|
|
||||||
):
|
|
||||||
super(TcHandler, self).__init__()
|
|
||||||
self.seq_count_provider = seq_count_provider
|
|
||||||
self.verif_wrapper = verif_wrapper
|
|
||||||
self.queue_helper = DefaultPusQueueHelper(
|
|
||||||
queue_wrapper=None,
|
|
||||||
seq_cnt_provider=seq_count_provider,
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_cb(self, send_params: SendCbParams):
|
|
||||||
entry_helper = send_params.entry
|
|
||||||
if entry_helper.is_tc:
|
|
||||||
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
|
|
||||||
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
|
|
||||||
pus_tc_wrapper.pus_tc.seq_count = (
|
|
||||||
self.seq_count_provider.get_and_increment()
|
|
||||||
)
|
|
||||||
self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc)
|
|
||||||
raw_tc = pus_tc_wrapper.pus_tc.pack()
|
|
||||||
LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
|
|
||||||
send_params.com_if.send(raw_tc)
|
|
||||||
elif entry_helper.entry_type == TcQueueEntryType.LOG:
|
|
||||||
log_entry = entry_helper.to_log_entry()
|
|
||||||
LOGGER.info(log_entry.log_str)
|
|
||||||
|
|
||||||
def queue_finished_cb(self, helper: ProcedureWrapper):
|
|
||||||
if helper.proc_type == TcProcedureType.DEFAULT:
|
|
||||||
def_proc = helper.to_def_procedure()
|
|
||||||
LOGGER.info(
|
|
||||||
f"Queue handling finished for service {def_proc.service} and "
|
|
||||||
f"op code {def_proc.op_code}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def feed_cb(self, helper: ProcedureWrapper, wrapper: FeedWrapper):
|
|
||||||
q = self.queue_helper
|
|
||||||
q.queue_wrapper = wrapper.queue_wrapper
|
|
||||||
if helper.proc_type == TcProcedureType.DEFAULT:
|
|
||||||
def_proc = helper.to_def_procedure()
|
|
||||||
service = def_proc.service
|
|
||||||
op_code = def_proc.op_code
|
|
||||||
if (
|
|
||||||
service == CoreServiceList.SERVICE_17
|
|
||||||
or service == CoreServiceList.SERVICE_17_ALT
|
|
||||||
):
|
|
||||||
q.add_log_cmd("Sending PUS ping telecommand")
|
|
||||||
return q.add_pus_tc(PusTelecommand(service=17, subservice=1))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
tmtccmd.init_printout(False)
|
|
||||||
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path())
|
|
||||||
parser_wrapper = PreArgsParsingWrapper()
|
|
||||||
parser_wrapper.create_default_parent_parser()
|
|
||||||
parser_wrapper.create_default_parser()
|
|
||||||
parser_wrapper.add_def_proc_args()
|
|
||||||
post_args_wrapper = parser_wrapper.parse(hook_obj)
|
|
||||||
params = SetupParams()
|
|
||||||
proc_wrapper = ProcedureParamsWrapper()
|
|
||||||
if post_args_wrapper.use_gui:
|
|
||||||
post_args_wrapper.set_params_without_prompts(params, proc_wrapper)
|
|
||||||
else:
|
|
||||||
post_args_wrapper.set_params_with_prompts(params, proc_wrapper)
|
|
||||||
params.apid = EXAMPLE_PUS_APID
|
|
||||||
setup_args = SetupWrapper(
|
|
||||||
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
|
|
||||||
)
|
|
||||||
# Create console logger helper and file loggers
|
|
||||||
tmtc_logger = RegularTmtcLogWrapper()
|
|
||||||
printer = FsfwTmTcPrinter(tmtc_logger.logger)
|
|
||||||
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
|
|
||||||
verificator = PusVerificator()
|
|
||||||
verification_wrapper = VerificationWrapper(verificator, LOGGER, printer.file_logger)
|
|
||||||
# Create primary TM handler and add it to the CCSDS Packet Handler
|
|
||||||
tm_handler = PusHandler(verification_wrapper, printer, raw_logger)
|
|
||||||
ccsds_handler = CcsdsTmHandler(generic_handler=None)
|
|
||||||
ccsds_handler.add_apid_handler(tm_handler)
|
|
||||||
|
|
||||||
# Create TC handler
|
|
||||||
seq_count_provider = PusFileSeqCountProvider()
|
|
||||||
tc_handler = TcHandler(seq_count_provider, verification_wrapper)
|
|
||||||
tmtccmd.setup(setup_args=setup_args)
|
|
||||||
init_proc = params_to_procedure_conversion(setup_args.proc_param_wrapper)
|
|
||||||
tmtc_backend = tmtccmd.create_default_tmtc_backend(
|
|
||||||
setup_wrapper=setup_args,
|
|
||||||
tm_handler=ccsds_handler,
|
|
||||||
tc_handler=tc_handler,
|
|
||||||
init_procedure=init_proc,
|
|
||||||
)
|
|
||||||
tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
state = tmtc_backend.periodic_op(None)
|
|
||||||
if state.request == BackendRequest.TERMINATION_NO_ERROR:
|
|
||||||
sys.exit(0)
|
|
||||||
elif state.request == BackendRequest.DELAY_IDLE:
|
|
||||||
LOGGER.info("TMTC Client in IDLE mode")
|
|
||||||
time.sleep(3.0)
|
|
||||||
elif state.request == BackendRequest.DELAY_LISTENER:
|
|
||||||
time.sleep(0.8)
|
|
||||||
elif state.request == BackendRequest.DELAY_CUSTOM:
|
|
||||||
if state.next_delay.total_seconds() <= 0.4:
|
|
||||||
time.sleep(state.next_delay.total_seconds())
|
|
||||||
else:
|
|
||||||
time.sleep(0.4)
|
|
||||||
elif state.request == BackendRequest.CALL_NEXT:
|
|
||||||
pass
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
tmtccmd == 4.0.0a0
|
|
||||||
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#![no_std]
|
|
||||||
#![no_main]
|
|
||||||
|
|
||||||
extern crate panic_itm;
|
|
||||||
|
|
||||||
use cortex_m_rt::entry;
|
|
||||||
|
|
||||||
use stm32f3_discovery::stm32f3xx_hal::delay::Delay;
|
|
||||||
use stm32f3_discovery::stm32f3xx_hal::{pac, prelude::*};
|
|
||||||
use stm32f3_discovery::leds::Leds;
|
|
||||||
use stm32f3_discovery::switch_hal::{OutputSwitch, ToggleableOutputSwitch};
|
|
||||||
|
|
||||||
#[entry]
|
|
||||||
fn main()-> ! {
|
|
||||||
let dp = pac::Peripherals::take().unwrap();
|
|
||||||
let mut rcc = dp.RCC.constrain();
|
|
||||||
let cp = cortex_m::Peripherals::take().unwrap();
|
|
||||||
let mut flash = dp.FLASH.constrain();
|
|
||||||
let clocks = rcc.cfgr.freeze(&mut flash.acr);
|
|
||||||
let mut delay = Delay::new(cp.SYST, clocks);
|
|
||||||
|
|
||||||
let mut gpioe = dp.GPIOE.split(&mut rcc.ahb);
|
|
||||||
let mut leds = Leds::new(
|
|
||||||
gpioe.pe8,
|
|
||||||
gpioe.pe9,
|
|
||||||
gpioe.pe10,
|
|
||||||
gpioe.pe11,
|
|
||||||
gpioe.pe12,
|
|
||||||
gpioe.pe13,
|
|
||||||
gpioe.pe14,
|
|
||||||
gpioe.pe15,
|
|
||||||
&mut gpioe.moder,
|
|
||||||
&mut gpioe.otyper
|
|
||||||
);
|
|
||||||
let delay_ms = 200u16;
|
|
||||||
loop {
|
|
||||||
leds.ld3.toggle().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld3.toggle().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
//explicit on/off
|
|
||||||
leds.ld4.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld4.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld5.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld5.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld6.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld6.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld7.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld7.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld8.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld8.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld9.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld9.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
|
|
||||||
leds.ld10.on().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
leds.ld10.off().ok();
|
|
||||||
delay.delay_ms(delay_ms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
#![no_std]
|
|
||||||
#![no_main]
|
|
||||||
extern crate panic_itm;
|
|
||||||
|
|
||||||
use rtic::app;
|
|
||||||
|
|
||||||
use heapless::{
|
|
||||||
mpmc::Q16,
|
|
||||||
pool,
|
|
||||||
pool::singleton::{Box, Pool},
|
|
||||||
};
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use itm_logger::{debug, info, logger_init, warn};
|
|
||||||
use satrs_core::spacepackets::{ecss::PusPacket, tm::PusTm};
|
|
||||||
use satrs_core::{
|
|
||||||
pus::{EcssTmErrorWithSend, EcssTmSenderCore},
|
|
||||||
seq_count::SequenceCountProviderCore,
|
|
||||||
};
|
|
||||||
use stm32f3xx_hal::dma::dma1;
|
|
||||||
use stm32f3xx_hal::gpio::{PushPull, AF7, PA2, PA3};
|
|
||||||
use stm32f3xx_hal::pac::USART2;
|
|
||||||
use stm32f3xx_hal::serial::{Rx, RxEvent, Serial, SerialDmaRx, SerialDmaTx, Tx, TxEvent};
|
|
||||||
use systick_monotonic::{fugit::Duration, Systick};
|
|
||||||
|
|
||||||
const UART_BAUD: u32 = 115200;
|
|
||||||
const BLINK_FREQ_MS: u64 = 1000;
|
|
||||||
const TX_HANDLER_FREQ_MS: u64 = 20;
|
|
||||||
const MIN_DELAY_BETWEEN_TX_PACKETS_MS: u16 = 5;
|
|
||||||
const MAX_TC_LEN: usize = 200;
|
|
||||||
const MAX_TM_LEN: usize = 200;
|
|
||||||
pub const PUS_APID: u16 = 0x02;
|
|
||||||
|
|
||||||
type TxType = Tx<USART2, PA2<AF7<PushPull>>>;
|
|
||||||
type RxType = Rx<USART2, PA3<AF7<PushPull>>>;
|
|
||||||
type MsDuration = Duration<u64, 1, 1000>;
|
|
||||||
type TxDmaTransferType = SerialDmaTx<&'static [u8], dma1::C7, TxType>;
|
|
||||||
type RxDmaTransferType = SerialDmaRx<&'static mut [u8], dma1::C6, RxType>;
|
|
||||||
|
|
||||||
// This is the predictable maximum overhead of the COBS encoding scheme.
|
|
||||||
// It is simply the maximum packet lenght dividied by 254 rounded up.
|
|
||||||
const COBS_TC_OVERHEAD: usize = (MAX_TC_LEN + 254 - 1) / 254;
|
|
||||||
const COBS_TM_OVERHEAD: usize = (MAX_TM_LEN + 254 - 1) / 254;
|
|
||||||
|
|
||||||
const TC_BUF_LEN: usize = MAX_TC_LEN + COBS_TC_OVERHEAD;
|
|
||||||
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
|
|
||||||
|
|
||||||
// This is a static buffer which should ONLY (!) be used as the TX DMA
|
|
||||||
// transfer buffer.
|
|
||||||
static mut DMA_TX_BUF: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN];
|
|
||||||
// This is a static buffer which should ONLY (!) be used as the RX DMA
|
|
||||||
// transfer buffer.
|
|
||||||
static mut DMA_RX_BUF: [u8; TC_BUF_LEN] = [0; TC_BUF_LEN];
|
|
||||||
|
|
||||||
static TX_REQUESTS: Q16<(Box<poolmod::TM>, usize)> = Q16::new();
|
|
||||||
|
|
||||||
const TC_POOL_SLOTS: usize = 12;
|
|
||||||
const TM_POOL_SLOTS: usize = 12;
|
|
||||||
use core::sync::atomic::{AtomicU16, Ordering};
|
|
||||||
|
|
||||||
pub struct SeqCountProviderAtomicRef {
|
|
||||||
atomic: AtomicU16,
|
|
||||||
ordering: Ordering,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SeqCountProviderAtomicRef {
|
|
||||||
pub const fn new(ordering: Ordering) -> Self {
|
|
||||||
Self {
|
|
||||||
atomic: AtomicU16::new(0),
|
|
||||||
ordering,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SequenceCountProviderCore<u16> for SeqCountProviderAtomicRef {
|
|
||||||
fn get(&self) -> u16 {
|
|
||||||
self.atomic.load(self.ordering)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&self) {
|
|
||||||
self.atomic.fetch_add(1, self.ordering);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_and_increment(&self) -> u16 {
|
|
||||||
self.atomic.fetch_add(1, self.ordering)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static SEQ_COUNT_PROVIDER: SeqCountProviderAtomicRef =
|
|
||||||
SeqCountProviderAtomicRef::new(Ordering::Relaxed);
|
|
||||||
|
|
||||||
// Otherwise, warnings because of heapless pool macro.
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
mod poolmod {
|
|
||||||
use super::*;
|
|
||||||
// Must hold full TC length including COBS overhead.
|
|
||||||
pool!(TC: [u8; TC_BUF_LEN]);
|
|
||||||
// Only encoded at the end, so no need to account for COBS overhead.
|
|
||||||
pool!(TM: [u8; MAX_TM_LEN]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TxIdle {
|
|
||||||
tx: TxType,
|
|
||||||
dma_channel: dma1::C7,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum TmStoreError {
|
|
||||||
StoreFull,
|
|
||||||
StoreSlotsTooSmall,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TmStoreError> for EcssTmErrorWithSend<TmStoreError> {
|
|
||||||
fn from(value: TmStoreError) -> Self {
|
|
||||||
Self::SendError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TmSender {
|
|
||||||
mem_block: Option<Box<poolmod::TM>>,
|
|
||||||
ctx: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TmSender {
|
|
||||||
pub fn new(mem_block: Box<poolmod::TM>, ctx: &'static str) -> Self {
|
|
||||||
Self {
|
|
||||||
mem_block: Some(mem_block),
|
|
||||||
ctx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EcssTmSenderCore for TmSender {
|
|
||||||
type Error = TmStoreError;
|
|
||||||
|
|
||||||
fn send_tm(
|
|
||||||
&mut self,
|
|
||||||
tm: PusTm,
|
|
||||||
) -> Result<(), satrs_core::pus::EcssTmErrorWithSend<Self::Error>> {
|
|
||||||
let mem_block = self.mem_block.take();
|
|
||||||
if mem_block.is_none() {
|
|
||||||
panic!("send_tm should only be called once");
|
|
||||||
}
|
|
||||||
let mut mem_block = mem_block.unwrap();
|
|
||||||
if tm.len_packed() > MAX_TM_LEN {
|
|
||||||
return Err(EcssTmErrorWithSend::SendError(
|
|
||||||
TmStoreError::StoreSlotsTooSmall,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
tm.write_to_bytes(mem_block.as_mut_slice())
|
|
||||||
.map_err(|e| EcssTmErrorWithSend::EcssTmError(e.into()))?;
|
|
||||||
info!(target: self.ctx, "Sending TM[{},{}] with size {}", tm.service(), tm.subservice(), tm.len_packed());
|
|
||||||
TX_REQUESTS
|
|
||||||
.enqueue((mem_block, tm.len_packed()))
|
|
||||||
.map_err(|_| TmStoreError::StoreFull)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum UartTxState {
|
|
||||||
// Wrapped in an option because we need an owned type later.
|
|
||||||
Idle(Option<TxIdle>),
|
|
||||||
// Same as above
|
|
||||||
Transmitting(Option<TxDmaTransferType>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[app(device = stm32f3xx_hal::pac, peripherals = true, dispatchers = [TIM20_BRK, TIM20_UP, TIM20_TRG_COM])]
|
|
||||||
mod app {
|
|
||||||
use super::*;
|
|
||||||
use core::slice::Iter;
|
|
||||||
use cortex_m::iprintln;
|
|
||||||
use satrs_core::pus::verification::FailParams;
|
|
||||||
use satrs_core::pus::verification::VerificationReporterCore;
|
|
||||||
use satrs_core::spacepackets::{
|
|
||||||
ecss::EcssEnumU16,
|
|
||||||
tc::PusTc,
|
|
||||||
time::cds::P_FIELD_BASE,
|
|
||||||
tm::{PusTm, PusTmSecondaryHeader},
|
|
||||||
CcsdsPacket, SpHeader,
|
|
||||||
};
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use stm32f3_discovery::leds::Direction;
|
|
||||||
use stm32f3_discovery::leds::Leds;
|
|
||||||
use stm32f3xx_hal::prelude::*;
|
|
||||||
use stm32f3xx_hal::Toggle;
|
|
||||||
|
|
||||||
use stm32f3_discovery::switch_hal::OutputSwitch;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
type SerialType = Serial<USART2, (PA2<AF7<PushPull>>, PA3<AF7<PushPull>>)>;
|
|
||||||
|
|
||||||
#[shared]
|
|
||||||
struct Shared {
|
|
||||||
tx_transfer: UartTxState,
|
|
||||||
rx_transfer: Option<RxDmaTransferType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[local]
|
|
||||||
struct Local {
|
|
||||||
leds: Leds,
|
|
||||||
last_dir: Direction,
|
|
||||||
verif_reporter: VerificationReporterCore,
|
|
||||||
curr_dir: Iter<'static, Direction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[monotonic(binds = SysTick, default = true)]
|
|
||||||
type MonoTimer = Systick<1000>;
|
|
||||||
|
|
||||||
#[init(local = [
|
|
||||||
tc_pool_mem: [u8; TC_BUF_LEN * TC_POOL_SLOTS] = [0; TC_BUF_LEN * TC_POOL_SLOTS],
|
|
||||||
tm_pool_mem: [u8; MAX_TM_LEN * TM_POOL_SLOTS] = [0; MAX_TM_LEN * TM_POOL_SLOTS]
|
|
||||||
])]
|
|
||||||
fn init(mut cx: init::Context) -> (Shared, Local, init::Monotonics) {
|
|
||||||
let mut rcc = cx.device.RCC.constrain();
|
|
||||||
|
|
||||||
let mono = Systick::new(cx.core.SYST, 8_000_000);
|
|
||||||
logger_init();
|
|
||||||
let mut flash = cx.device.FLASH.constrain();
|
|
||||||
let clocks = rcc
|
|
||||||
.cfgr
|
|
||||||
.use_hse(8.MHz())
|
|
||||||
.sysclk(8.MHz())
|
|
||||||
.pclk1(8.MHz())
|
|
||||||
.freeze(&mut flash.acr);
|
|
||||||
// setup ITM output
|
|
||||||
iprintln!(
|
|
||||||
&mut cx.core.ITM.stim[0],
|
|
||||||
"Starting sat-rs demo application for the STM32F3-Discovery"
|
|
||||||
);
|
|
||||||
let mut gpioe = cx.device.GPIOE.split(&mut rcc.ahb);
|
|
||||||
// Assign memory to the pools.
|
|
||||||
poolmod::TC::grow(cx.local.tc_pool_mem);
|
|
||||||
poolmod::TM::grow(cx.local.tm_pool_mem);
|
|
||||||
|
|
||||||
let verif_reporter = VerificationReporterCore::new(PUS_APID).unwrap();
|
|
||||||
|
|
||||||
let leds = Leds::new(
|
|
||||||
gpioe.pe8,
|
|
||||||
gpioe.pe9,
|
|
||||||
gpioe.pe10,
|
|
||||||
gpioe.pe11,
|
|
||||||
gpioe.pe12,
|
|
||||||
gpioe.pe13,
|
|
||||||
gpioe.pe14,
|
|
||||||
gpioe.pe15,
|
|
||||||
&mut gpioe.moder,
|
|
||||||
&mut gpioe.otyper,
|
|
||||||
);
|
|
||||||
let mut gpioa = cx.device.GPIOA.split(&mut rcc.ahb);
|
|
||||||
// USART2 pins
|
|
||||||
let mut pins = (
|
|
||||||
// TX pin: PA2
|
|
||||||
gpioa
|
|
||||||
.pa2
|
|
||||||
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
|
|
||||||
// RX pin: PA3
|
|
||||||
gpioa
|
|
||||||
.pa3
|
|
||||||
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
|
|
||||||
);
|
|
||||||
pins.1.internal_pull_up(&mut gpioa.pupdr, true);
|
|
||||||
let mut usart2 = Serial::new(
|
|
||||||
cx.device.USART2,
|
|
||||||
pins,
|
|
||||||
UART_BAUD.Bd(),
|
|
||||||
clocks,
|
|
||||||
&mut rcc.apb1,
|
|
||||||
);
|
|
||||||
usart2.configure_rx_interrupt(RxEvent::Idle, Toggle::On);
|
|
||||||
// This interrupt is enabled to re-schedule new transfers in the interrupt handler immediately.
|
|
||||||
usart2.configure_tx_interrupt(TxEvent::TransmissionComplete, Toggle::On);
|
|
||||||
|
|
||||||
let dma1 = cx.device.DMA1.split(&mut rcc.ahb);
|
|
||||||
let (tx_serial, mut rx_serial) = usart2.split();
|
|
||||||
|
|
||||||
// This interrupt is immediately triggered, clear it. It will only be reset
|
|
||||||
// by the hardware when data is received on RX (RXNE event)
|
|
||||||
rx_serial.clear_event(RxEvent::Idle);
|
|
||||||
let rx_transfer = rx_serial.read_exact(unsafe { DMA_RX_BUF.as_mut_slice() }, dma1.ch6);
|
|
||||||
info!(target: "init", "Spawning tasks");
|
|
||||||
blink::spawn().unwrap();
|
|
||||||
serial_tx_handler::spawn().unwrap();
|
|
||||||
(
|
|
||||||
Shared {
|
|
||||||
tx_transfer: UartTxState::Idle(Some(TxIdle {
|
|
||||||
tx: tx_serial,
|
|
||||||
dma_channel: dma1.ch7,
|
|
||||||
})),
|
|
||||||
rx_transfer: Some(rx_transfer),
|
|
||||||
},
|
|
||||||
Local {
|
|
||||||
leds,
|
|
||||||
last_dir: Direction::North,
|
|
||||||
curr_dir: Direction::iter(),
|
|
||||||
verif_reporter,
|
|
||||||
},
|
|
||||||
init::Monotonics(mono),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task(local = [leds, curr_dir, last_dir])]
|
|
||||||
fn blink(cx: blink::Context) {
|
|
||||||
let toggle_leds = |dir: &Direction| {
|
|
||||||
let leds = cx.local.leds;
|
|
||||||
let last_led = leds.for_direction(*cx.local.last_dir);
|
|
||||||
last_led.off().ok();
|
|
||||||
let led = leds.for_direction(*dir);
|
|
||||||
led.on().ok();
|
|
||||||
*cx.local.last_dir = *dir;
|
|
||||||
};
|
|
||||||
|
|
||||||
match cx.local.curr_dir.next() {
|
|
||||||
Some(dir) => {
|
|
||||||
toggle_leds(dir);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
*cx.local.curr_dir = Direction::iter();
|
|
||||||
toggle_leds(cx.local.curr_dir.next().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blink::spawn_after(MsDuration::from_ticks(BLINK_FREQ_MS)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task(
|
|
||||||
shared = [tx_transfer],
|
|
||||||
local = []
|
|
||||||
)]
|
|
||||||
fn serial_tx_handler(mut cx: serial_tx_handler::Context) {
|
|
||||||
if let Some((buf, len)) = TX_REQUESTS.dequeue() {
|
|
||||||
cx.shared.tx_transfer.lock(|tx_state| match tx_state {
|
|
||||||
UartTxState::Idle(tx) => {
|
|
||||||
//debug!(target: "serial_tx_handler", "bytes: {:x?}", &buf[0..len]);
|
|
||||||
// Safety: We only copy the data into the TX DMA buffer in this task.
|
|
||||||
// If the DMA is active, another branch will be taken.
|
|
||||||
let mut_tx_dma_buf = unsafe { &mut DMA_TX_BUF };
|
|
||||||
// 0 sentinel value as start marker
|
|
||||||
mut_tx_dma_buf[0] = 0;
|
|
||||||
// Should never panic, we accounted for the overhead.
|
|
||||||
// Write into transfer buffer directly, no need for intermediate
|
|
||||||
// encoding buffer.
|
|
||||||
let encoded_len = cobs::encode(&buf[0..len], &mut mut_tx_dma_buf[1..]);
|
|
||||||
// 0 end marker
|
|
||||||
mut_tx_dma_buf[encoded_len + 1] = 0;
|
|
||||||
//debug!(target: "serial_tx_handler", "Sending {} bytes", encoded_len + 2);
|
|
||||||
//debug!("sent: {:x?}", &mut_tx_dma_buf[0..encoded_len + 2]);
|
|
||||||
let tx_idle = tx.take().unwrap();
|
|
||||||
// Transfer completion and re-scheduling of new TX transfers will be done
|
|
||||||
// by the IRQ handler.
|
|
||||||
let transfer = tx_idle
|
|
||||||
.tx
|
|
||||||
.write_all(&mut_tx_dma_buf[0..encoded_len + 2], tx_idle.dma_channel);
|
|
||||||
*tx_state = UartTxState::Transmitting(Some(transfer));
|
|
||||||
// The memory block is automatically returned to the pool when it is dropped.
|
|
||||||
}
|
|
||||||
UartTxState::Transmitting(_) => {
|
|
||||||
// This is a SW configuration error. Only the ISR which
|
|
||||||
// detects transfer completion should be able to spawn a new
|
|
||||||
// task, and that ISR should set the state to IDLE.
|
|
||||||
panic!("invalid internal tx state detected")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
cx.shared.tx_transfer.lock(|tx_state| {
|
|
||||||
if let UartTxState::Idle(_) = tx_state {
|
|
||||||
serial_tx_handler::spawn_after(MsDuration::from_ticks(TX_HANDLER_FREQ_MS))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task(
|
|
||||||
local = [
|
|
||||||
stamp_buf: [u8; 7] = [0; 7],
|
|
||||||
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
|
|
||||||
src_data_buf: [u8; MAX_TM_LEN] = [0; MAX_TM_LEN],
|
|
||||||
verif_reporter
|
|
||||||
],
|
|
||||||
)]
|
|
||||||
fn serial_rx_handler(
|
|
||||||
cx: serial_rx_handler::Context,
|
|
||||||
received_packet: Box<poolmod::TC>,
|
|
||||||
rx_len: usize,
|
|
||||||
) {
|
|
||||||
let tgt: &'static str = "serial_rx_handler";
|
|
||||||
cx.local.stamp_buf[0] = P_FIELD_BASE;
|
|
||||||
info!(target: tgt, "Received packet with {} bytes", rx_len);
|
|
||||||
let decode_buf = cx.local.decode_buf;
|
|
||||||
let packet = received_packet.as_slice();
|
|
||||||
let mut start_idx = None;
|
|
||||||
for (idx, byte) in packet.iter().enumerate() {
|
|
||||||
if *byte != 0 {
|
|
||||||
start_idx = Some(idx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if start_idx.is_none() {
|
|
||||||
warn!(
|
|
||||||
target: tgt,
|
|
||||||
"decoding error, can only process cobs encoded frames, data is all 0"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let start_idx = start_idx.unwrap();
|
|
||||||
match cobs::decode(&received_packet.as_slice()[start_idx..], decode_buf) {
|
|
||||||
Ok(len) => {
|
|
||||||
info!(target: tgt, "Decoded packet length: {}", len);
|
|
||||||
let pus_tc = PusTc::from_bytes(decode_buf);
|
|
||||||
let verif_reporter = cx.local.verif_reporter;
|
|
||||||
match pus_tc {
|
|
||||||
Ok((tc, tc_len)) => handle_tc(
|
|
||||||
tc,
|
|
||||||
tc_len,
|
|
||||||
verif_reporter,
|
|
||||||
cx.local.src_data_buf,
|
|
||||||
cx.local.stamp_buf,
|
|
||||||
tgt,
|
|
||||||
),
|
|
||||||
Err(e) => {
|
|
||||||
warn!(target: tgt, "Error unpacking PUS TC: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!(
|
|
||||||
target: tgt,
|
|
||||||
"decoding error, can only process cobs encoded frames"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_tc(
|
|
||||||
tc: PusTc,
|
|
||||||
tc_len: usize,
|
|
||||||
verif_reporter: &mut VerificationReporterCore,
|
|
||||||
src_data_buf: &mut [u8; MAX_TM_LEN],
|
|
||||||
stamp_buf: &[u8; 7],
|
|
||||||
tgt: &'static str,
|
|
||||||
) {
|
|
||||||
info!(
|
|
||||||
target: tgt,
|
|
||||||
"Found PUS TC [{},{}] with length {}",
|
|
||||||
tc.service(),
|
|
||||||
tc.subservice(),
|
|
||||||
tc_len
|
|
||||||
);
|
|
||||||
|
|
||||||
let token = verif_reporter.add_tc(&tc);
|
|
||||||
if tc.apid() != PUS_APID {
|
|
||||||
warn!(target: tgt, "Received tc with unknown APID {}", tc.apid());
|
|
||||||
let sendable = verif_reporter
|
|
||||||
.acceptance_failure(
|
|
||||||
src_data_buf,
|
|
||||||
token,
|
|
||||||
&SEQ_COUNT_PROVIDER,
|
|
||||||
FailParams::new(stamp_buf, &EcssEnumU16::new(0), None),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let mem_block = poolmod::TM::alloc().unwrap().init([0u8; MAX_TM_LEN]);
|
|
||||||
let mut sender = TmSender::new(mem_block, tgt);
|
|
||||||
if let Err(e) =
|
|
||||||
verif_reporter.send_acceptance_failure(sendable, &SEQ_COUNT_PROVIDER, &mut sender)
|
|
||||||
{
|
|
||||||
warn!(target: tgt, "Sending acceptance failure failed: {:?}", e.0);
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let sendable = verif_reporter
|
|
||||||
.acceptance_success(src_data_buf, token, &SEQ_COUNT_PROVIDER, stamp_buf)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mem_block = poolmod::TM::alloc().unwrap().init([0u8; MAX_TM_LEN]);
|
|
||||||
let mut sender = TmSender::new(mem_block, tgt);
|
|
||||||
let accepted_token = match verif_reporter.send_acceptance_success(
|
|
||||||
sendable,
|
|
||||||
&SEQ_COUNT_PROVIDER,
|
|
||||||
&mut sender,
|
|
||||||
) {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => {
|
|
||||||
warn!(target: "serial_rx_handler", "Sending acceptance success failed: {:?}", e.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if tc.service() == 17 {
|
|
||||||
if tc.subservice() == 1 {
|
|
||||||
let sendable = verif_reporter
|
|
||||||
.start_success(src_data_buf, accepted_token, &SEQ_COUNT_PROVIDER, stamp_buf)
|
|
||||||
.unwrap();
|
|
||||||
let mem_block = poolmod::TM::alloc().unwrap().init([0u8; MAX_TM_LEN]);
|
|
||||||
let mut sender = TmSender::new(mem_block, tgt);
|
|
||||||
let started_token = match verif_reporter.send_start_success(
|
|
||||||
sendable,
|
|
||||||
&SEQ_COUNT_PROVIDER,
|
|
||||||
&mut sender,
|
|
||||||
) {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => {
|
|
||||||
warn!(target: tgt, "Sending acceptance success failed: {:?}", e.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
info!(
|
|
||||||
target: tgt,
|
|
||||||
"Received PUS ping telecommand, sending ping reply TM[17,2]"
|
|
||||||
);
|
|
||||||
let mut sp_header =
|
|
||||||
SpHeader::tc_unseg(PUS_APID, SEQ_COUNT_PROVIDER.get(), 0).unwrap();
|
|
||||||
let sec_header = PusTmSecondaryHeader::new_simple(17, 2, stamp_buf);
|
|
||||||
let ping_reply = PusTm::new(&mut sp_header, sec_header, None, true);
|
|
||||||
let mut mem_block = poolmod::TM::alloc().unwrap().init([0u8; MAX_TM_LEN]);
|
|
||||||
let reply_len = ping_reply.write_to_bytes(mem_block.as_mut_slice()).unwrap();
|
|
||||||
if TX_REQUESTS.enqueue((mem_block, reply_len)).is_err() {
|
|
||||||
warn!(target: tgt, "TC queue full");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SEQ_COUNT_PROVIDER.increment();
|
|
||||||
let sendable = verif_reporter
|
|
||||||
.completion_success(src_data_buf, started_token, &SEQ_COUNT_PROVIDER, stamp_buf)
|
|
||||||
.unwrap();
|
|
||||||
let mem_block = poolmod::TM::alloc().unwrap().init([0u8; MAX_TM_LEN]);
|
|
||||||
let mut sender = TmSender::new(mem_block, tgt);
|
|
||||||
if let Err(e) = verif_reporter.send_step_or_completion_success(
|
|
||||||
sendable,
|
|
||||||
&SEQ_COUNT_PROVIDER,
|
|
||||||
&mut sender,
|
|
||||||
) {
|
|
||||||
warn!(target: tgt, "Sending completion success failed: {:?}", e.0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: Invalid subservice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task(binds = DMA1_CH6, shared = [rx_transfer])]
|
|
||||||
fn rx_dma_isr(mut cx: rx_dma_isr::Context) {
|
|
||||||
cx.shared.rx_transfer.lock(|rx_transfer| {
|
|
||||||
let rx_ref = rx_transfer.as_ref().unwrap();
|
|
||||||
if rx_ref.is_complete() {
|
|
||||||
let uart_rx_owned = rx_transfer.take().unwrap();
|
|
||||||
let (buf, c, rx) = uart_rx_owned.stop();
|
|
||||||
// The received data is transferred to another task now to avoid any processing overhead
|
|
||||||
// during the interrupt. There are multiple ways to do this, we use a memory pool here
|
|
||||||
// to do this.
|
|
||||||
let mut mem_block = poolmod::TC::alloc()
|
|
||||||
.expect("allocating memory block for rx failed")
|
|
||||||
.init([0u8; TC_BUF_LEN]);
|
|
||||||
// Copy data into memory pool.
|
|
||||||
mem_block.copy_from_slice(buf);
|
|
||||||
*rx_transfer = Some(rx.read_exact(buf, c));
|
|
||||||
// Only send owning pointer to pool memory and the received packet length.
|
|
||||||
serial_rx_handler::spawn(mem_block, TC_BUF_LEN)
|
|
||||||
.expect("spawning rx handler task failed");
|
|
||||||
// If this happens, there is a high chance that the maximum packet length was
|
|
||||||
// exceeded. Circular mode is not used here, so data might be missed.
|
|
||||||
warn!(
|
|
||||||
"rx transfer with maximum length {}, might miss data",
|
|
||||||
TC_BUF_LEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task(binds = USART2_EXTI26, shared = [rx_transfer, tx_transfer])]
|
|
||||||
fn serial_isr(mut cx: serial_isr::Context) {
|
|
||||||
cx.shared.tx_transfer.lock(|tx_state| match tx_state {
|
|
||||||
UartTxState::Idle(_) => (),
|
|
||||||
UartTxState::Transmitting(transfer) => {
|
|
||||||
let transfer_ref = transfer.as_ref().unwrap();
|
|
||||||
if transfer_ref.is_complete() {
|
|
||||||
let transfer = transfer.take().unwrap();
|
|
||||||
let (_, dma_channel, tx) = transfer.stop();
|
|
||||||
*tx_state = UartTxState::Idle(Some(TxIdle { tx, dma_channel }));
|
|
||||||
serial_tx_handler::spawn_after(MsDuration::from_ticks(
|
|
||||||
MIN_DELAY_BETWEEN_TX_PACKETS_MS.into(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.shared.rx_transfer.lock(|rx_transfer| {
|
|
||||||
let rx_transfer_ref = rx_transfer.as_ref().unwrap();
|
|
||||||
// Received a partial packet.
|
|
||||||
if rx_transfer_ref.is_event_triggered(RxEvent::Idle) {
|
|
||||||
let rx_transfer_owned = rx_transfer.take().unwrap();
|
|
||||||
let (buf, ch, mut rx, rx_len) = rx_transfer_owned.stop_and_return_received_bytes();
|
|
||||||
// The received data is transferred to another task now to avoid any processing overhead
|
|
||||||
// during the interrupt. There are multiple ways to do this, we use a memory pool here
|
|
||||||
// to do this.
|
|
||||||
let mut mem_block = poolmod::TC::alloc()
|
|
||||||
.expect("allocating memory block for rx failed")
|
|
||||||
.init([0u8; TC_BUF_LEN]);
|
|
||||||
// Copy data into memory pool.
|
|
||||||
mem_block[0..rx_len as usize].copy_from_slice(&buf[0..rx_len as usize]);
|
|
||||||
rx.clear_event(RxEvent::Idle);
|
|
||||||
// Only send owning pointer to pool memory and the received packet length.
|
|
||||||
serial_rx_handler::spawn(mem_block, rx_len as usize)
|
|
||||||
.expect("spawning rx handler task failed");
|
|
||||||
*rx_transfer = Some(rx.read_exact(buf, ch));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
+24
-15
@@ -1,32 +1,41 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "satrs-example"
|
name = "satrs-example"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||||
default-run = "satrs-example"
|
default-run = "satrs-example"
|
||||||
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||||
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fern = "0.6"
|
fern = "0.7"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
delegate = "0.10"
|
delegate = "0.13"
|
||||||
zerocopy = "0.6"
|
zerocopy = "0.8"
|
||||||
csv = "1"
|
csv = "1"
|
||||||
num_enum = "0.7"
|
num_enum = "0.7"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
derive-new = "0.5"
|
lazy_static = "1"
|
||||||
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
|
derive-new = "0.7"
|
||||||
|
cfg-if = "1"
|
||||||
|
arbitrary-int = "2"
|
||||||
|
bitbybit = "2"
|
||||||
|
postcard = "1"
|
||||||
|
ctrlc = "3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
[dependencies.satrs]
|
satrs = { path = "../satrs", features = ["test_util"] }
|
||||||
version = "0.2.0-rc.0"
|
models = { path = "./models" }
|
||||||
# path = "../satrs"
|
satrs-minisim = { path = "./minisim" }
|
||||||
|
satrs-mib = { path = "../satrs-mib" }
|
||||||
[dependencies.satrs-mib]
|
|
||||||
version = "0.1.1"
|
|
||||||
# path = "../satrs-mib"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
dyn_tmtc = []
|
# default = ["heap_tmtc"]
|
||||||
default = ["dyn_tmtc"]
|
# heap_tmtc = []
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
|
|||||||
+23
-3
@@ -14,7 +14,7 @@ You can run the application using `cargo run`.
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
The example has the `dyn_tmtc` feature which is enabled by default. With this feature enabled,
|
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
|
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
|
||||||
stores.
|
stores.
|
||||||
|
|
||||||
@@ -48,16 +48,17 @@ It is recommended to use a virtual environment to do this. To set up one in the
|
|||||||
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
|
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
|
||||||
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
|
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
|
||||||
on how to activate the environment and then use the following command to install the required
|
on how to activate the environment and then use the following command to install the required
|
||||||
dependency:
|
dependency interactively:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install -r requirements.txt
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also
|
Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also
|
||||||
install it manually with
|
install it manually with
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
pip install -e .
|
||||||
pip install tmtccmd[gui]
|
pip install tmtccmd[gui]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -72,3 +73,22 @@ the `simpleclient`:
|
|||||||
```
|
```
|
||||||
|
|
||||||
You can also simply call the script without any arguments to view the command tree.
|
You can also simply call the script without any arguments to view the command tree.
|
||||||
|
|
||||||
|
## Adding the mini simulator application
|
||||||
|
|
||||||
|
This example application features a few device handlers. The
|
||||||
|
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||||
|
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 satrs-minisim
|
||||||
|
```
|
||||||
|
|
||||||
|
and then start the example using `cargo run -p satrs-example`.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[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" }
|
||||||
|
satrs-example = { path = ".." }
|
||||||
|
models = { path = "../models" }
|
||||||
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
|
bitbybit = "2"
|
||||||
|
arbitrary-int = "2"
|
||||||
|
ctrlc = { version = "3.5" }
|
||||||
|
postcard = { version = "1" }
|
||||||
|
anyhow = "1"
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use clap::Parser as _;
|
||||||
|
use models::{Apid, MessageType, TcHeader, mgm::request::HkRequest};
|
||||||
|
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||||
|
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, SocketAddr, UdpSocket},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
test_event: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
commands: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Mgm0(MgmArgs),
|
||||||
|
Mgm1(MgmArgs),
|
||||||
|
MgmAssy(MgmAssemblyArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commands {
|
||||||
|
#[inline]
|
||||||
|
pub fn target_id(&self) -> models::ComponentId {
|
||||||
|
match self {
|
||||||
|
Commands::Mgm0(_mgm_args) => models::ComponentId::AcsMgm0,
|
||||||
|
Commands::Mgm1(_mgm_args) => models::ComponentId::AcsMgm1,
|
||||||
|
Commands::MgmAssy(_mgm_assembly_args) => models::ComponentId::AcsMgmAssembly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgmArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
request_hk: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<DeviceModeSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::ValueEnum)]
|
||||||
|
pub enum DeviceModeSelect {
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum AssemblyModeSelect {
|
||||||
|
NoModeKeeping,
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{} {} {}] {}",
|
||||||
|
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
record.level(),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(level)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
setup_logger(log::LevelFilter::Debug).unwrap();
|
||||||
|
let kill_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let ctrl_kill_signal = kill_signal.clone();
|
||||||
|
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||||
|
let client = UdpSocket::bind("127.0.0.1:7302").expect("Connecting to UDP server failed");
|
||||||
|
client.set_nonblocking(true)?;
|
||||||
|
client.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||||
|
|
||||||
|
if cli.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(models::ComponentId::Controller, models::MessageType::Ping),
|
||||||
|
models::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 = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(models::ComponentId::Controller, models::MessageType::Event),
|
||||||
|
models::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 {
|
||||||
|
let target_id = cmd.target_id();
|
||||||
|
match cmd {
|
||||||
|
Commands::Mgm0(args) | Commands::Mgm1(args) => {
|
||||||
|
if args.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(cmd.target_id(), models::MessageType::Ping),
|
||||||
|
models::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 args.request_hk {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Hk),
|
||||||
|
models::mgm::request::Request::Hk(HkRequest {
|
||||||
|
id: models::mgm::request::HkId::Sensor,
|
||||||
|
req_type: models::HkRequestType::OneShot,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
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 => models::DeviceMode::Off,
|
||||||
|
DeviceModeSelect::Normal => models::DeviceMode::Normal,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Mode),
|
||||||
|
models::mgm::request::Request::Mode(
|
||||||
|
models::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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::MgmAssy(mgm_assembly_args) => {
|
||||||
|
if mgm_assembly_args.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(cmd.target_id(), models::MessageType::Ping),
|
||||||
|
models::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 => {
|
||||||
|
models::mgm_assembly::AssemblyMode::NoModeKeeping
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Off => {
|
||||||
|
models::mgm_assembly::AssemblyMode::Device(models::DeviceMode::Off)
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Normal => {
|
||||||
|
models::mgm_assembly::AssemblyMode::Device(models::DeviceMode::Normal)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Mode),
|
||||||
|
models::mgm_assembly::request::Request::Mode(
|
||||||
|
models::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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<models::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()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::info!(
|
||||||
|
"Received unsolicited TM with APID {} and from sender {:?}",
|
||||||
|
packet.apid(),
|
||||||
|
tm_header.sender_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tm_header.message_type == MessageType::Event {
|
||||||
|
let response = postcard::from_bytes::<models::Event>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received event from {:?}: {:?}",
|
||||||
|
tm_header.sender_id,
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match tm_header.sender_id {
|
||||||
|
models::ComponentId::EpsPcdu => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::pcdu::response::Response>(remainder);
|
||||||
|
log::info!("Received response from PCDU: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::Controller => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::control::response::Response>(remainder);
|
||||||
|
log::info!("Received response from controller: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsSubsystem => todo!(),
|
||||||
|
models::ComponentId::AcsMgmAssembly => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm_assembly::response::Response>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received response from MGM Assembly: {:?}",
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsMgm0 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM0: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsMgm1 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM1: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::EpsSubsystem => todo!(),
|
||||||
|
models::ComponentId::UdpServer => todo!(),
|
||||||
|
models::ComponentId::TcpServer => todo!(),
|
||||||
|
models::ComponentId::Ground => todo!(),
|
||||||
|
models::ComponentId::EventManager => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "satrs-minisim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
log = "0.4"
|
||||||
|
thiserror = "2"
|
||||||
|
fern = "0.7"
|
||||||
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
|
num_enum = "0.7"
|
||||||
|
humantime = "2"
|
||||||
|
tai-time = { version = "0.3", features = ["serde"] }
|
||||||
|
nexosim = { version = "0.3.1" }
|
||||||
|
|
||||||
|
satrs = { path = "../../satrs" }
|
||||||
|
models = { path = "../models" }
|
||||||
|
|
||||||
|
[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
|
||||||
|
[asynchronix](https://github.com/asynchronics/asynchronix).
|
||||||
|
|
||||||
|
Right now, this crate is primarily used together with the
|
||||||
|
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||||
|
to simulate the devices connected to the example application.
|
||||||
|
|
||||||
|
You can simply run this application using
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p satrs-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,360 @@
|
|||||||
|
use std::{f32::consts::PI, sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use models::pcdu::SwitchStateBinary;
|
||||||
|
use nexosim::{
|
||||||
|
model::{Context, Model},
|
||||||
|
ports::Output,
|
||||||
|
};
|
||||||
|
use satrs_minisim::{
|
||||||
|
acs::{
|
||||||
|
lis3mdl::MgmLis3MdlReply, MgmReplyCommon, MgmReplyProvider, MgmSensorValuesMicroTesla,
|
||||||
|
MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD,
|
||||||
|
},
|
||||||
|
SimReply,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
pub struct MagnetometerModel<ReplyProvider: MgmReplyProvider> {
|
||||||
|
pub switch_state: SwitchStateBinary,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub periodicity: Duration,
|
||||||
|
pub external_mag_field: Option<MgmSensorValuesMicroTesla>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
pub phatom: std::marker::PhantomData<ReplyProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagnetometerModel<MgmLis3MdlReply> {
|
||||||
|
pub fn new_for_lis3mdl(periodicity: Duration, reply_sender: mpsc::Sender<SimReply>) -> Self {
|
||||||
|
Self {
|
||||||
|
switch_state: SwitchStateBinary::Off,
|
||||||
|
periodicity,
|
||||||
|
external_mag_field: None,
|
||||||
|
reply_sender,
|
||||||
|
phatom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ReplyProvider: MgmReplyProvider> MagnetometerModel<ReplyProvider> {
|
||||||
|
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||||
|
self.switch_state = switch_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_sensor_values(&mut self, _: (), scheduler: &mut Context<Self>) {
|
||||||
|
self.reply_sender
|
||||||
|
.send(ReplyProvider::create_mgm_reply(MgmReplyCommon {
|
||||||
|
switch_state: self.switch_state,
|
||||||
|
sensor_values: self.calculate_current_mgm_tuple(current_millis(scheduler.time())),
|
||||||
|
}))
|
||||||
|
.expect("sending MGM sensor values failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: MgmSensorValuesMicroTesla) {
|
||||||
|
self.external_mag_field = Some(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> MgmSensorValuesMicroTesla {
|
||||||
|
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 MgmSensorValuesMicroTesla {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
MgmSensorValuesMicroTesla {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ReplyProvider: MgmReplyProvider> Model for MagnetometerModel<ReplyProvider> {}
|
||||||
|
|
||||||
|
pub struct MagnetorquerModel {
|
||||||
|
switch_state: SwitchStateBinary,
|
||||||
|
torquing: bool,
|
||||||
|
torque_dipole: MgtDipole,
|
||||||
|
pub gen_magnetic_field: Output<MgmSensorValuesMicroTesla>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagnetorquerModel {
|
||||||
|
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
|
||||||
|
Self {
|
||||||
|
switch_state: SwitchStateBinary::Off,
|
||||||
|
torquing: false,
|
||||||
|
torque_dipole: MgtDipole::default(),
|
||||||
|
gen_magnetic_field: Output::new(),
|
||||||
|
reply_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_torque(
|
||||||
|
&mut self,
|
||||||
|
duration_and_dipole: (Duration, MgtDipole),
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.torque_dipole = duration_and_dipole.1;
|
||||||
|
self.torquing = true;
|
||||||
|
if cx
|
||||||
|
.schedule_event(duration_and_dipole.0, Self::clear_torque, ())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
log::warn!("torque clearing can only be set for a future time.");
|
||||||
|
}
|
||||||
|
self.generate_magnetic_field(()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_torque(&mut self, _: ()) {
|
||||||
|
self.torque_dipole = MgtDipole::default();
|
||||||
|
self.torquing = false;
|
||||||
|
self.generate_magnetic_field(()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||||
|
self.switch_state = switch_state;
|
||||||
|
self.generate_magnetic_field(()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_housekeeping_data(&mut self, _: (), cx: &mut Context<Self>) {
|
||||||
|
if self.switch_state != SwitchStateBinary::On {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cx.schedule_event(Duration::from_millis(15), Self::send_housekeeping_data, ())
|
||||||
|
.expect("requesting housekeeping data failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_housekeeping_data(&mut self) {
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(&MgtReply::Hk(MgtHkSet {
|
||||||
|
dipole: self.torque_dipole,
|
||||||
|
torquing: self.torquing,
|
||||||
|
})))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_magnetic_field(&self, _: MgtDipole) -> MgmSensorValuesMicroTesla {
|
||||||
|
// Simplified model: Just returns some fixed magnetic field for now.
|
||||||
|
// Later, we could make this more fancy by incorporating the commanded dipole.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for MagnetorquerModel {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use models::pcdu::{SwitchId, SwitchStateBinary};
|
||||||
|
use satrs_minisim::{
|
||||||
|
acs::{
|
||||||
|
lis3mdl::{self, MgmLis3MdlReply},
|
||||||
|
MgmRequestLis3Mdl, MgtDipole, MgtHkSet, MgtReply, MgtRequest,
|
||||||
|
},
|
||||||
|
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgm_request() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply.is_some());
|
||||||
|
let sim_reply = sim_reply.unwrap();
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::Mgm0Lis3Mdl);
|
||||||
|
let reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
assert_eq!(reply.common.switch_state, SwitchStateBinary::Off);
|
||||||
|
assert_eq!(reply.common.sensor_values.x, 0.0);
|
||||||
|
assert_eq!(reply.common.sensor_values.y, 0.0);
|
||||||
|
assert_eq!(reply.common.sensor_values.z, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgm_request_switched_on() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
|
||||||
|
let mut request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let mut sim_reply_res = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply_res.is_some());
|
||||||
|
let mut sim_reply = sim_reply_res.unwrap();
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::Mgm0Lis3Mdl);
|
||||||
|
let first_reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
|
||||||
|
|
||||||
|
request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
sim_reply_res = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply_res.is_some());
|
||||||
|
sim_reply = sim_reply_res.unwrap();
|
||||||
|
|
||||||
|
let second_reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
let x_conv_back = second_reply.raw.x as f32
|
||||||
|
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
|
||||||
|
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
|
||||||
|
let y_conv_back = second_reply.raw.y as f32
|
||||||
|
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
|
||||||
|
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
|
||||||
|
let z_conv_back = second_reply.raw.z as f32
|
||||||
|
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
|
||||||
|
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
|
||||||
|
let diff_x = (second_reply.common.sensor_values.x - x_conv_back).abs();
|
||||||
|
assert!(diff_x < 0.01, "diff x too large: {}", diff_x);
|
||||||
|
let diff_y = (second_reply.common.sensor_values.y - y_conv_back).abs();
|
||||||
|
assert!(diff_y < 0.01, "diff y too large: {}", diff_y);
|
||||||
|
let diff_z = (second_reply.common.sensor_values.z - z_conv_back).abs();
|
||||||
|
assert!(diff_z < 0.01, "diff z too large: {}", diff_z);
|
||||||
|
// assert_eq!(second_reply.raw_reply, SwitchStateBinary::On);
|
||||||
|
// Check that the values are changing.
|
||||||
|
assert!(first_reply != second_reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgt_request_is_off() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply_res = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply_res.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_mgt_request_is_on() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
|
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
|
||||||
|
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply_res = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply_res.is_some());
|
||||||
|
let sim_reply = sim_reply_res.unwrap();
|
||||||
|
let mgt_reply = MgtReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
match mgt_reply {
|
||||||
|
MgtReply::Hk(hk) => {
|
||||||
|
assert_eq!(hk.dipole, MgtDipole::default());
|
||||||
|
assert!(!hk.torquing);
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected reply"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_mgt_hk(sim_testbench: &mut SimTestbench, expected_hk_set: MgtHkSet) {
|
||||||
|
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply_res = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply_res.is_some());
|
||||||
|
let sim_reply = sim_reply_res.unwrap();
|
||||||
|
let mgt_reply = MgtReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
match mgt_reply {
|
||||||
|
MgtReply::Hk(hk) => {
|
||||||
|
assert_eq!(hk, expected_hk_set);
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected reply"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = MgtDipole {
|
||||||
|
x: -200,
|
||||||
|
y: 200,
|
||||||
|
z: 1000,
|
||||||
|
};
|
||||||
|
let request = SimRequest::new_with_epoch_time(MgtRequest::ApplyTorque {
|
||||||
|
duration: Duration::from_millis(100),
|
||||||
|
dipole: commanded_dipole,
|
||||||
|
});
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step_until(Duration::from_millis(5)).unwrap();
|
||||||
|
|
||||||
|
check_mgt_hk(
|
||||||
|
&mut sim_testbench,
|
||||||
|
MgtHkSet {
|
||||||
|
dipole: commanded_dipole,
|
||||||
|
torquing: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sim_testbench
|
||||||
|
.step_until(Duration::from_millis(100))
|
||||||
|
.unwrap();
|
||||||
|
check_mgt_hk(
|
||||||
|
&mut sim_testbench,
|
||||||
|
MgtHkSet {
|
||||||
|
dipole: MgtDipole::default(),
|
||||||
|
torquing: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use nexosim::{
|
||||||
|
simulation::{Address, Scheduler, Simulation},
|
||||||
|
time::{Clock, MonotonicTime, SystemClock},
|
||||||
|
};
|
||||||
|
use satrs_minisim::{
|
||||||
|
acs::{lis3mdl::MgmLis3MdlReply, MgmRequestLis3Mdl, MgtRequest},
|
||||||
|
eps::PcduRequest,
|
||||||
|
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
|
||||||
|
SimReply, SimRequest, SimRequestError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
acs::{MagnetometerModel, MagnetorquerModel},
|
||||||
|
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;
|
||||||
|
|
||||||
|
pub struct ModelAddrWrapper {
|
||||||
|
mgm_0_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
|
||||||
|
mgm_1_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
|
||||||
|
pcdu_addr: Address<PcduModel>,
|
||||||
|
mgt_addr: Address<MagnetorquerModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The simulation controller processes requests and drives the simulation.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct SimController {
|
||||||
|
pub sys_clock: SystemClock,
|
||||||
|
pub request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
pub simulation: Simulation,
|
||||||
|
pub scheduler: Scheduler,
|
||||||
|
pub addr_wrapper: ModelAddrWrapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelAddrWrapper {
|
||||||
|
pub fn new(
|
||||||
|
mgm_0_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
|
||||||
|
mgm_1_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
|
||||||
|
pcdu_addr: Address<PcduModel>,
|
||||||
|
mgt_addr: Address<MagnetorquerModel>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
mgm_0_addr,
|
||||||
|
mgm_1_addr,
|
||||||
|
pcdu_addr,
|
||||||
|
mgt_addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SimController {
|
||||||
|
pub fn new(
|
||||||
|
sys_clock: SystemClock,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
simulation: Simulation,
|
||||||
|
scheduler: Scheduler,
|
||||||
|
addr_wrapper: ModelAddrWrapper,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
sys_clock,
|
||||||
|
request_receiver,
|
||||||
|
reply_sender,
|
||||||
|
simulation,
|
||||||
|
scheduler,
|
||||||
|
addr_wrapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.simulation
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
if let Err(e) = match request.component() {
|
||||||
|
SimComponent::SimCtrl => self.handle_ctrl_request(&request),
|
||||||
|
SimComponent::Mgm0Lis3Mdl => self.handle_mgm_request(0, &request),
|
||||||
|
SimComponent::Mgm1Lis3Mdl => self.handle_mgm_request(1, &request),
|
||||||
|
SimComponent::Mgt => self.handle_mgt_request(&request),
|
||||||
|
SimComponent::Pcdu => self.handle_pcdu_request(&request),
|
||||||
|
} {
|
||||||
|
self.handle_invalid_request_with_valid_target(e, &request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => break,
|
||||||
|
mpsc::TryRecvError::Disconnected => {
|
||||||
|
panic!("all request sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_ctrl_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
|
||||||
|
let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?;
|
||||||
|
if SIM_CTRL_REQ_WIRETAPPING {
|
||||||
|
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
|
||||||
|
}
|
||||||
|
match sim_ctrl_request {
|
||||||
|
SimCtrlRequest::Ping => {
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(&SimCtrlReply::Pong))
|
||||||
|
.expect("sending reply from sim controller failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgm_request(
|
||||||
|
&mut self,
|
||||||
|
mgm_idx: usize,
|
||||||
|
request: &SimRequest,
|
||||||
|
) -> Result<(), SimRequestError> {
|
||||||
|
let mgm_request = MgmRequestLis3Mdl::from_sim_message(request)?;
|
||||||
|
if MGM_REQ_WIRETAPPING {
|
||||||
|
log::info!("received MGM request: {mgm_request:?}");
|
||||||
|
}
|
||||||
|
match mgm_request {
|
||||||
|
MgmRequestLis3Mdl::RequestSensorData => {
|
||||||
|
let addr = match mgm_idx {
|
||||||
|
0 => &self.addr_wrapper.mgm_0_addr,
|
||||||
|
1 => &self.addr_wrapper.mgm_1_addr,
|
||||||
|
|
||||||
|
_ => panic!("invalid mgm index"),
|
||||||
|
};
|
||||||
|
self.simulation
|
||||||
|
.process_event(MagnetometerModel::send_sensor_values, (), addr)
|
||||||
|
.expect("event execution error for mgm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_pcdu_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
|
||||||
|
let pcdu_request = PcduRequest::from_sim_message(request)?;
|
||||||
|
if PCDU_REQ_WIRETAPPING {
|
||||||
|
log::info!("received PCDU request: {pcdu_request:?}");
|
||||||
|
}
|
||||||
|
match pcdu_request {
|
||||||
|
PcduRequest::RequestSwitchInfo => {
|
||||||
|
self.simulation
|
||||||
|
.process_event(
|
||||||
|
PcduModel::request_switch_info,
|
||||||
|
(),
|
||||||
|
&self.addr_wrapper.pcdu_addr,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
PcduRequest::SwitchDevice { switch, state } => {
|
||||||
|
self.simulation
|
||||||
|
.process_event(
|
||||||
|
PcduModel::switch_device,
|
||||||
|
(switch, state),
|
||||||
|
&self.addr_wrapper.pcdu_addr,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mgt_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
|
||||||
|
let mgt_request = MgtRequest::from_sim_message(request)?;
|
||||||
|
if MGT_REQ_WIRETAPPING {
|
||||||
|
log::info!("received MGT request: {mgt_request:?}");
|
||||||
|
}
|
||||||
|
match mgt_request {
|
||||||
|
MgtRequest::ApplyTorque { duration, dipole } => self
|
||||||
|
.simulation
|
||||||
|
.process_event(
|
||||||
|
MagnetorquerModel::apply_torque,
|
||||||
|
(duration, dipole),
|
||||||
|
&self.addr_wrapper.mgt_addr,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
MgtRequest::RequestHk => self
|
||||||
|
.simulation
|
||||||
|
.process_event(
|
||||||
|
MagnetorquerModel::request_housekeeping_data,
|
||||||
|
(),
|
||||||
|
&self.addr_wrapper.mgt_addr,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_invalid_request_with_valid_target(
|
||||||
|
&self,
|
||||||
|
error: SimRequestError,
|
||||||
|
request: &SimRequest,
|
||||||
|
) {
|
||||||
|
log::warn!(
|
||||||
|
"received invalid {:?} request: {:?}",
|
||||||
|
request.component(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(&SimCtrlReply::from(error)))
|
||||||
|
.expect("sending reply from sim controller failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_ping() {
|
||||||
|
let mut sim_testbench = SimTestbench::new();
|
||||||
|
let request = SimRequest::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending sim ctrl request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply.is_some());
|
||||||
|
let sim_reply = sim_reply.unwrap();
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
|
||||||
|
let reply = SimCtrlReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
assert_eq!(reply, SimCtrlReply::Pong);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use models::pcdu::{SwitchId, SwitchMapBinaryWrapper, SwitchStateBinary};
|
||||||
|
use nexosim::{
|
||||||
|
model::{Context, Model},
|
||||||
|
ports::Output,
|
||||||
|
};
|
||||||
|
use satrs_minisim::{eps::PcduReply, SimReply};
|
||||||
|
|
||||||
|
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
||||||
|
|
||||||
|
pub struct PcduModel {
|
||||||
|
pub switcher_map: SwitchMapBinaryWrapper,
|
||||||
|
pub mgm_0_switch: Output<SwitchStateBinary>,
|
||||||
|
pub mgm_1_switch: Output<SwitchStateBinary>,
|
||||||
|
pub mgt_switch: Output<SwitchStateBinary>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PcduModel {
|
||||||
|
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
|
||||||
|
Self {
|
||||||
|
switcher_map: Default::default(),
|
||||||
|
mgm_0_switch: Output::new(),
|
||||||
|
mgm_1_switch: Output::new(),
|
||||||
|
mgt_switch: Output::new(),
|
||||||
|
reply_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_switch_info(&mut self, _: (), cx: &mut Context<Self>) {
|
||||||
|
cx.schedule_event(
|
||||||
|
Duration::from_millis(SWITCH_INFO_DELAY_MS),
|
||||||
|
Self::send_switch_info,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.expect("requesting switch info failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_switch_info(&mut self) {
|
||||||
|
let reply = SimReply::new(&PcduReply::SwitchInfo(self.switcher_map.0.clone()));
|
||||||
|
self.reply_sender.send(reply).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.0
|
||||||
|
.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::Mgt => {
|
||||||
|
self.mgt_switch.send(switch_and_target_state.1).await;
|
||||||
|
}
|
||||||
|
SwitchId::Mgm1 => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for PcduModel {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use models::pcdu::SwitchMapBinary;
|
||||||
|
use satrs_minisim::{
|
||||||
|
eps::PcduRequest, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
|
fn switch_device(
|
||||||
|
sim_testbench: &mut SimTestbench,
|
||||||
|
switch: SwitchId,
|
||||||
|
target: SwitchStateBinary,
|
||||||
|
) {
|
||||||
|
let request = SimRequest::new_with_epoch_time(PcduRequest::SwitchDevice {
|
||||||
|
switch,
|
||||||
|
state: target,
|
||||||
|
});
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM switch request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
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 check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
|
||||||
|
let request = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step().unwrap();
|
||||||
|
let sim_reply = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply.is_some());
|
||||||
|
let sim_reply = sim_reply.unwrap();
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::Pcdu);
|
||||||
|
let pcdu_reply = PcduReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize PCDU switch info");
|
||||||
|
match pcdu_reply {
|
||||||
|
PcduReply::SwitchInfo(switch_map) => {
|
||||||
|
assert_eq!(switch_map, *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 = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||||
|
sim_testbench
|
||||||
|
.send_request(request)
|
||||||
|
.expect("sending MGM request failed");
|
||||||
|
sim_testbench.handle_sim_requests_time_agnostic();
|
||||||
|
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
|
||||||
|
|
||||||
|
let sim_reply = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply.is_none());
|
||||||
|
// Reply takes 20ms
|
||||||
|
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
|
||||||
|
let sim_reply = sim_testbench.try_receive_next_reply();
|
||||||
|
assert!(sim_reply.is_some());
|
||||||
|
let sim_reply = sim_reply.unwrap();
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::Pcdu);
|
||||||
|
let pcdu_reply = PcduReply::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize PCDU switch info");
|
||||||
|
match pcdu_reply {
|
||||||
|
PcduReply::SwitchInfo(switch_map) => {
|
||||||
|
assert_eq!(switch_map, 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,404 @@
|
|||||||
|
use nexosim::time::MonotonicTime;
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||||
|
pub enum SimComponent {
|
||||||
|
SimCtrl,
|
||||||
|
Mgm0Lis3Mdl,
|
||||||
|
Mgm1Lis3Mdl,
|
||||||
|
Mgt,
|
||||||
|
Pcdu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SimMessage {
|
||||||
|
pub target: SimComponent,
|
||||||
|
pub payload: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic simulation request type. Right now, the payload data is expected to be
|
||||||
|
/// JSON, which might be changed in the future.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SimRequest {
|
||||||
|
inner: SimMessage,
|
||||||
|
pub timestamp: MonotonicTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimMessageType {
|
||||||
|
Request,
|
||||||
|
Reply,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic trait implemented by simulation request or reply payloads. It ties the request or
|
||||||
|
/// reply to a specific target and provides an API which does boilerplate tasks like checking the
|
||||||
|
/// validity of the target.
|
||||||
|
pub trait SerializableSimMsgPayload<P: SimMessageProvider>:
|
||||||
|
Serialize + DeserializeOwned + Sized
|
||||||
|
{
|
||||||
|
const TARGET: SimComponent;
|
||||||
|
|
||||||
|
fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> {
|
||||||
|
if sim_message.component() == Self::TARGET {
|
||||||
|
return Ok(serde_json::from_str(sim_message.payload())?);
|
||||||
|
}
|
||||||
|
Err(SimMessageError::TargetRequestMissmatch(sim_message.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SimMessageProvider: Serialize + DeserializeOwned + Clone + Sized {
|
||||||
|
fn msg_type(&self) -> SimMessageType;
|
||||||
|
fn component(&self) -> SimComponent;
|
||||||
|
fn payload(&self) -> &String;
|
||||||
|
fn from_raw_data(data: &[u8]) -> serde_json::Result<Self> {
|
||||||
|
serde_json::from_slice(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimRequest {
|
||||||
|
pub fn new_with_epoch_time<T: SerializableSimMsgPayload<SimRequest>>(
|
||||||
|
serializable_request: T,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(serializable_request, MonotonicTime::EPOCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new<T: SerializableSimMsgPayload<SimRequest>>(
|
||||||
|
serializable_request: T,
|
||||||
|
timestamp: MonotonicTime,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: SimMessage {
|
||||||
|
target: T::TARGET,
|
||||||
|
payload: serde_json::to_string(&serializable_request).unwrap(),
|
||||||
|
},
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimMessageProvider for SimRequest {
|
||||||
|
fn component(&self) -> SimComponent {
|
||||||
|
self.inner.target
|
||||||
|
}
|
||||||
|
fn payload(&self) -> &String {
|
||||||
|
&self.inner.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_type(&self) -> SimMessageType {
|
||||||
|
SimMessageType::Request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic simulation reply type. Right now, the payload data is expected to be
|
||||||
|
/// JSON, which might be changed in the future.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SimReply {
|
||||||
|
inner: SimMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimReply {
|
||||||
|
pub fn new<T: SerializableSimMsgPayload<SimReply>>(serializable_reply: &T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: SimMessage {
|
||||||
|
target: T::TARGET,
|
||||||
|
payload: serde_json::to_string(serializable_reply).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimMessageProvider for SimReply {
|
||||||
|
fn component(&self) -> SimComponent {
|
||||||
|
self.inner.target
|
||||||
|
}
|
||||||
|
fn payload(&self) -> &String {
|
||||||
|
&self.inner.payload
|
||||||
|
}
|
||||||
|
fn msg_type(&self) -> SimMessageType {
|
||||||
|
SimMessageType::Reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimCtrlRequest {
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest {
|
||||||
|
const TARGET: SimComponent = SimComponent::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SimReplyError = SimMessageError<SimReply>;
|
||||||
|
pub type SimRequestError = SimMessageError<SimRequest>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimMessageError<P> {
|
||||||
|
SerdeJson(String),
|
||||||
|
TargetRequestMissmatch(P),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<serde_json::Error> for SimMessageError<P> {
|
||||||
|
fn from(error: serde_json::Error) -> SimMessageError<P> {
|
||||||
|
SimMessageError::SerdeJson(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimCtrlReply {
|
||||||
|
Pong,
|
||||||
|
InvalidRequest(SimRequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for SimCtrlReply {
|
||||||
|
const TARGET: SimComponent = SimComponent::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SimRequestError> for SimCtrlReply {
|
||||||
|
fn from(error: SimRequestError) -> Self {
|
||||||
|
SimCtrlReply::InvalidRequest(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod eps {
|
||||||
|
use super::*;
|
||||||
|
use models::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum PcduRequestId {
|
||||||
|
SwitchDevice = 0,
|
||||||
|
RequestSwitchInfo = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PcduRequest {
|
||||||
|
SwitchDevice {
|
||||||
|
switch: SwitchId,
|
||||||
|
state: SwitchStateBinary,
|
||||||
|
},
|
||||||
|
RequestSwitchInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for PcduRequest {
|
||||||
|
const TARGET: SimComponent = SimComponent::Pcdu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum PcduReply {
|
||||||
|
// Ack,
|
||||||
|
SwitchInfo(SwitchMapBinary),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for PcduReply {
|
||||||
|
const TARGET: SimComponent = SimComponent::Pcdu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod acs {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use models::pcdu::SwitchStateBinary;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub trait MgmReplyProvider: Send + 'static {
|
||||||
|
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MgmRequestLis3Mdl {
|
||||||
|
RequestSensorData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for MgmRequestLis3Mdl {
|
||||||
|
const TARGET: SimComponent = SimComponent::Mgm0Lis3Mdl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 MgmSensorValuesMicroTesla {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub z: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MgmReplyCommon {
|
||||||
|
pub switch_state: SwitchStateBinary,
|
||||||
|
pub sensor_values: MgmSensorValuesMicroTesla,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValuesMicroTesla = MgmSensorValuesMicroTesla {
|
||||||
|
x: 30.0,
|
||||||
|
y: -30.0,
|
||||||
|
z: 30.0,
|
||||||
|
};
|
||||||
|
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
|
||||||
|
|
||||||
|
pub mod lis3mdl {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
pub const FIELD_LSB_PER_GAUSS_8_SENS: f32 = 1.0 / 3421.0;
|
||||||
|
pub const FIELD_LSB_PER_GAUSS_12_SENS: f32 = 1.0 / 2281.0;
|
||||||
|
pub const FIELD_LSB_PER_GAUSS_16_SENS: f32 = 1.0 / 1711.0;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MgmLis3RawValues {
|
||||||
|
pub x: i16,
|
||||||
|
pub y: i16,
|
||||||
|
pub z: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MgmLis3MdlReply {
|
||||||
|
pub common: MgmReplyCommon,
|
||||||
|
// Raw sensor values which are transmitted by the LIS3 device in little-endian
|
||||||
|
// order.
|
||||||
|
pub raw: MgmLis3RawValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MgmLis3MdlReply {
|
||||||
|
pub fn new(common: MgmReplyCommon) -> Self {
|
||||||
|
match common.switch_state {
|
||||||
|
SwitchStateBinary::Off => Self {
|
||||||
|
common,
|
||||||
|
raw: MgmLis3RawValues {
|
||||||
|
x: ALL_ONES_SENSOR_VAL,
|
||||||
|
y: ALL_ONES_SENSOR_VAL,
|
||||||
|
z: ALL_ONES_SENSOR_VAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SwitchStateBinary::On => {
|
||||||
|
let mut raw_reply: [u8; 7] = [0; 7];
|
||||||
|
let raw_x: i16 = (common.sensor_values.x
|
||||||
|
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
|
||||||
|
.round() as i16;
|
||||||
|
let raw_y: i16 = (common.sensor_values.y
|
||||||
|
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
|
||||||
|
.round() as i16;
|
||||||
|
let raw_z: i16 = (common.sensor_values.z
|
||||||
|
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
|
||||||
|
.round() as i16;
|
||||||
|
// The first byte is a dummy byte.
|
||||||
|
raw_reply[1..3].copy_from_slice(&raw_x.to_be_bytes());
|
||||||
|
raw_reply[3..5].copy_from_slice(&raw_y.to_be_bytes());
|
||||||
|
raw_reply[5..7].copy_from_slice(&raw_z.to_be_bytes());
|
||||||
|
Self {
|
||||||
|
common,
|
||||||
|
raw: MgmLis3RawValues {
|
||||||
|
x: raw_x,
|
||||||
|
y: raw_y,
|
||||||
|
z: raw_z,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for MgmLis3MdlReply {
|
||||||
|
const TARGET: SimComponent = SimComponent::Mgm0Lis3Mdl;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MgmReplyProvider for MgmLis3MdlReply {
|
||||||
|
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply {
|
||||||
|
SimReply::new(&Self::new(common))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple model using i16 values.
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct MgtDipole {
|
||||||
|
pub x: i16,
|
||||||
|
pub y: i16,
|
||||||
|
pub z: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum MgtRequestType {
|
||||||
|
ApplyTorque,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MgtRequest {
|
||||||
|
ApplyTorque {
|
||||||
|
duration: Duration,
|
||||||
|
dipole: MgtDipole,
|
||||||
|
},
|
||||||
|
RequestHk,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for MgtRequest {
|
||||||
|
const TARGET: SimComponent = SimComponent::Mgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct MgtHkSet {
|
||||||
|
pub dipole: MgtDipole,
|
||||||
|
pub torquing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MgtReply {
|
||||||
|
Ack(MgtRequestType),
|
||||||
|
Nak(MgtRequestType),
|
||||||
|
Hk(MgtHkSet),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for MgtReply {
|
||||||
|
const TARGET: SimComponent = SimComponent::Mgm0Lis3Mdl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod udp {
|
||||||
|
pub const SIM_CTRL_PORT: u16 = 7303;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DummyRequest {
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for DummyRequest {
|
||||||
|
const TARGET: SimComponent = SimComponent::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DummyReply {
|
||||||
|
Pong,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for DummyReply {
|
||||||
|
const TARGET: SimComponent = SimComponent::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_request() {
|
||||||
|
let sim_request = SimRequest::new_with_epoch_time(DummyRequest::Ping);
|
||||||
|
assert_eq!(sim_request.component(), SimComponent::SimCtrl);
|
||||||
|
assert_eq!(sim_request.msg_type(), SimMessageType::Request);
|
||||||
|
let dummy_request =
|
||||||
|
DummyRequest::from_sim_message(&sim_request).expect("deserialization failed");
|
||||||
|
assert_eq!(dummy_request, DummyRequest::Ping);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_reply() {
|
||||||
|
let sim_reply = SimReply::new(&DummyReply::Pong);
|
||||||
|
assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
|
||||||
|
assert_eq!(sim_reply.msg_type(), SimMessageType::Reply);
|
||||||
|
let dummy_request =
|
||||||
|
DummyReply::from_sim_message(&sim_reply).expect("deserialization failed");
|
||||||
|
assert_eq!(dummy_request, DummyReply::Pong);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
use acs::{MagnetometerModel, MagnetorquerModel};
|
||||||
|
use controller::{ModelAddrWrapper, SimController};
|
||||||
|
use eps::PcduModel;
|
||||||
|
use nexosim::simulation::{Mailbox, SimInit};
|
||||||
|
use nexosim::time::{MonotonicTime, SystemClock};
|
||||||
|
use satrs_minisim::udp::SIM_CTRL_PORT;
|
||||||
|
use satrs_minisim::{SimReply, SimRequest};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use udp::SimUdpServer;
|
||||||
|
|
||||||
|
mod acs;
|
||||||
|
mod controller;
|
||||||
|
mod eps;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_helpers;
|
||||||
|
mod time;
|
||||||
|
mod udp;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ThreadingModel {
|
||||||
|
Default = 0,
|
||||||
|
Single = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_sim_controller(
|
||||||
|
threading_model: ThreadingModel,
|
||||||
|
start_time: MonotonicTime,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
) -> SimController {
|
||||||
|
// Instantiate models and their mailboxes.
|
||||||
|
let mgm_0_model =
|
||||||
|
MagnetometerModel::new_for_lis3mdl(Duration::from_millis(50), reply_sender.clone());
|
||||||
|
let mgm_1_model =
|
||||||
|
MagnetometerModel::new_for_lis3mdl(Duration::from_millis(50), reply_sender.clone());
|
||||||
|
|
||||||
|
let mgm_0_mailbox = Mailbox::new();
|
||||||
|
let mgm_0_addr = mgm_0_mailbox.address();
|
||||||
|
let mgm_1_mailbox = Mailbox::new();
|
||||||
|
let mgm_1_addr = mgm_1_mailbox.address();
|
||||||
|
let pcdu_mailbox = Mailbox::new();
|
||||||
|
let pcdu_addr = pcdu_mailbox.address();
|
||||||
|
let mgt_mailbox = Mailbox::new();
|
||||||
|
let mgt_addr = mgt_mailbox.address();
|
||||||
|
|
||||||
|
let mut pcdu_model = PcduModel::new(reply_sender.clone());
|
||||||
|
pcdu_model
|
||||||
|
.mgm_0_switch
|
||||||
|
.connect(MagnetometerModel::switch_device, &mgm_0_addr);
|
||||||
|
pcdu_model
|
||||||
|
.mgm_1_switch
|
||||||
|
.connect(MagnetometerModel::switch_device, &mgm_1_addr);
|
||||||
|
|
||||||
|
let mut mgt_model = MagnetorquerModel::new(reply_sender.clone());
|
||||||
|
// Input connections.
|
||||||
|
pcdu_model
|
||||||
|
.mgt_switch
|
||||||
|
.connect(MagnetorquerModel::switch_device, &mgt_addr);
|
||||||
|
// Output connections.
|
||||||
|
mgt_model.gen_magnetic_field.connect(
|
||||||
|
MagnetometerModel::apply_external_magnetic_field,
|
||||||
|
&mgm_0_addr,
|
||||||
|
);
|
||||||
|
mgt_model.gen_magnetic_field.connect(
|
||||||
|
MagnetometerModel::apply_external_magnetic_field,
|
||||||
|
&mgm_1_addr,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Instantiate the simulator
|
||||||
|
let sys_clock = SystemClock::from_system_time(start_time, SystemTime::now());
|
||||||
|
let sim_init = if threading_model == ThreadingModel::Single {
|
||||||
|
SimInit::with_num_threads(1)
|
||||||
|
} else {
|
||||||
|
SimInit::new()
|
||||||
|
};
|
||||||
|
let addrs = ModelAddrWrapper::new(mgm_0_addr, mgm_1_addr, pcdu_addr, mgt_addr);
|
||||||
|
let (simulation, scheduler) = 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();
|
||||||
|
SimController::new(
|
||||||
|
sys_clock,
|
||||||
|
request_receiver,
|
||||||
|
reply_sender,
|
||||||
|
simulation,
|
||||||
|
scheduler,
|
||||||
|
addrs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let (request_sender, request_receiver) = mpsc::channel();
|
||||||
|
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||||
|
let t0 = MonotonicTime::EPOCH;
|
||||||
|
let mut sim_ctrl =
|
||||||
|
create_sim_controller(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,62 @@
|
|||||||
|
use delegate::delegate;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use nexosim::{
|
||||||
|
simulation::ExecutionError,
|
||||||
|
time::{Deadline, MonotonicTime},
|
||||||
|
};
|
||||||
|
use satrs_minisim::{SimReply, SimRequest};
|
||||||
|
|
||||||
|
use crate::{controller::SimController, create_sim_controller, ThreadingModel};
|
||||||
|
|
||||||
|
pub struct SimTestbench {
|
||||||
|
pub sim_controller: SimController,
|
||||||
|
pub reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
pub request_sender: mpsc::Sender<SimRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
create_sim_controller(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);
|
||||||
|
}
|
||||||
|
to self.sim_controller.simulation {
|
||||||
|
pub fn step(&mut self) -> Result<(), ExecutionError>;
|
||||||
|
pub fn step_until(&mut self, duration: impl Deadline) -> Result<(), ExecutionError>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_request(&self, request: SimRequest) -> Result<(), mpsc::SendError<SimRequest>> {
|
||||||
|
self.request_sender.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,436 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::ErrorKind,
|
||||||
|
net::{SocketAddr, UdpSocket},
|
||||||
|
sync::{atomic::AtomicBool, mpsc, Arc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use satrs_minisim::{SimMessageProvider, SimReply, SimRequest};
|
||||||
|
|
||||||
|
// A UDP server which handles all TC received by a client application.
|
||||||
|
pub struct SimUdpServer {
|
||||||
|
socket: UdpSocket,
|
||||||
|
request_sender: mpsc::Sender<SimRequest>,
|
||||||
|
// shared_last_sender: SharedSocketAddr,
|
||||||
|
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<SimRequest>,
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
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 {
|
||||||
|
if 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 = SimRequest::from_raw_data(&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(
|
||||||
|
serde_json::to_string(&next_reply_to_send)
|
||||||
|
.unwrap()
|
||||||
|
.as_bytes(),
|
||||||
|
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::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc, Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use satrs_minisim::{
|
||||||
|
eps::{PcduReply, PcduRequest},
|
||||||
|
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
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("Serde JSON error: {0}")]
|
||||||
|
SerdeJson(#[from] serde_json::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: &SimRequest) -> std::io::Result<usize> {
|
||||||
|
self.socket.send(
|
||||||
|
&serde_json::to_vec(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(serde_json::from_slice(&self.reply_buf[0..read_len])?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct UdpTestbench {
|
||||||
|
client: SimUdpTestClient,
|
||||||
|
stop_signal: Arc<AtomicBool>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
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<SimRequest, 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: &SimRequest) -> 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::SerdeJson(json_error) => {
|
||||||
|
panic!("unexpected JSON error: {json_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 = SimRequest::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(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
|
||||||
|
.expect("sending request failed");
|
||||||
|
|
||||||
|
let sim_reply = SimReply::new(&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(&SimRequest::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::new(&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::new(&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(&SimRequest::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::new(&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(&SimRequest::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,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "models"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
|
satrs = { path = "../../satrs" }
|
||||||
|
num_enum = { version = "0.7" }
|
||||||
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
|
postcard = { version = "1" }
|
||||||
|
thiserror = { version = "2" }
|
||||||
|
bitbybit = "2"
|
||||||
|
arbitrary-int = "2"
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use crate::TmHeader;
|
||||||
|
use serde::Serialize;
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, SpHeader, SpacePacketHeader,
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::TcHeader;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CcsdsTcPacketOwned {
|
||||||
|
pub sp_header: SpacePacketHeader,
|
||||||
|
pub tc_header: TcHeader,
|
||||||
|
pub payload: alloc::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsTcPacketOwned {
|
||||||
|
pub fn new_with_request<R: serde::Serialize>(
|
||||||
|
sp_header: SpacePacketHeader,
|
||||||
|
tc_header: TcHeader,
|
||||||
|
request: R,
|
||||||
|
) -> Self {
|
||||||
|
let request_serialized = postcard::to_allocvec(&request).unwrap();
|
||||||
|
Self::new(sp_header, tc_header, request_serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
sp_header: SpacePacketHeader,
|
||||||
|
tc_header: TcHeader,
|
||||||
|
payload: alloc::vec::Vec<u8>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
sp_header,
|
||||||
|
tc_header,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, CcsdsCreationError> {
|
||||||
|
let response_len =
|
||||||
|
postcard::experimental::serialized_size(&self.tc_header)? + self.payload.len();
|
||||||
|
let mut ccsds_tc = CcsdsPacketCreatorWithReservedData::new_tc_with_checksum(
|
||||||
|
self.sp_header,
|
||||||
|
response_len,
|
||||||
|
buf,
|
||||||
|
)?;
|
||||||
|
let user_data = ccsds_tc.packet_data_mut();
|
||||||
|
let ser_len = postcard::to_slice(&self.tc_header, user_data)?.len();
|
||||||
|
user_data[ser_len..ser_len + self.payload.len()].copy_from_slice(&self.payload);
|
||||||
|
let ccsds_packet_len = ccsds_tc.finish();
|
||||||
|
Ok(ccsds_packet_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len_written(&self) -> usize {
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||||
|
postcard::experimental::serialized_size(&self.tc_header).unwrap() as usize
|
||||||
|
+ postcard::experimental::serialized_size(&self.payload).unwrap() as usize,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> alloc::vec::Vec<u8> {
|
||||||
|
let mut buf = alloc::vec![0u8; self.len_written()];
|
||||||
|
let len = self.write_to_bytes(&mut buf).unwrap();
|
||||||
|
buf.truncate(len);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcsdsCreationError {
|
||||||
|
#[error("CCSDS packet creation error: {0}")]
|
||||||
|
CcsdsPacketCreation(#[from] CcsdsPacketCreationError),
|
||||||
|
#[error("postcard error: {0}")]
|
||||||
|
Postcard(#[from] postcard::Error),
|
||||||
|
#[error("timestamp generation error")]
|
||||||
|
Time,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unserialized owned TM packet which can be cloned and sent around.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CcsdsTmPacketOwned {
|
||||||
|
pub sp_header: SpacePacketHeader,
|
||||||
|
pub tm_header: TmHeader,
|
||||||
|
pub payload: alloc::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsTmPacketOwned {
|
||||||
|
pub fn new_with_serde_payload(
|
||||||
|
sp_header: SpHeader,
|
||||||
|
tm_header: &TmHeader,
|
||||||
|
payload: &impl Serialize,
|
||||||
|
) -> Result<Self, postcard::Error> {
|
||||||
|
Ok(CcsdsTmPacketOwned {
|
||||||
|
sp_header,
|
||||||
|
tm_header: *tm_header,
|
||||||
|
payload: postcard::to_allocvec(&payload)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, CcsdsCreationError> {
|
||||||
|
let response_len =
|
||||||
|
postcard::experimental::serialized_size(&self.tm_header)? + self.payload.len();
|
||||||
|
let mut ccsds_tm = CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(
|
||||||
|
self.sp_header,
|
||||||
|
response_len,
|
||||||
|
buf,
|
||||||
|
)?;
|
||||||
|
let user_data = ccsds_tm.packet_data_mut();
|
||||||
|
let ser_len = postcard::to_slice(&self.tm_header, user_data)?.len();
|
||||||
|
user_data[ser_len..ser_len + self.payload.len()].copy_from_slice(&self.payload);
|
||||||
|
let ccsds_packet_len = ccsds_tm.finish();
|
||||||
|
Ok(ccsds_packet_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len_written(&self) -> usize {
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||||
|
postcard::experimental::serialized_size(&self.tm_header).unwrap() as usize
|
||||||
|
+ postcard::experimental::serialized_size(&self.payload).unwrap() as usize,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> alloc::vec::Vec<u8> {
|
||||||
|
let mut buf = alloc::vec![0u8; self.len_written()];
|
||||||
|
let len = self.write_to_bytes(&mut buf).unwrap();
|
||||||
|
buf.truncate(len);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
TestEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Event {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
crate::MessageType::Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
TestEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Event(super::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Event(_event) => crate::MessageType::Event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
extern crate alloc;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketIdAndPsc,
|
||||||
|
time::cds::{CdsTime, MIN_CDS_FIELD_LEN},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod ccsds;
|
||||||
|
pub mod control;
|
||||||
|
pub mod mgm;
|
||||||
|
pub mod mgm_assembly;
|
||||||
|
pub mod pcdu;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
num_enum::TryFromPrimitive,
|
||||||
|
num_enum::IntoPrimitive,
|
||||||
|
)]
|
||||||
|
#[repr(u64)]
|
||||||
|
pub enum ComponentId {
|
||||||
|
Controller,
|
||||||
|
|
||||||
|
AcsSubsystem,
|
||||||
|
AcsMgmAssembly,
|
||||||
|
AcsMgm0,
|
||||||
|
AcsMgm1,
|
||||||
|
|
||||||
|
EpsSubsystem,
|
||||||
|
EpsPcdu,
|
||||||
|
|
||||||
|
UdpServer,
|
||||||
|
TcpServer,
|
||||||
|
EventManager,
|
||||||
|
|
||||||
|
Ground,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, strum::EnumIter)]
|
||||||
|
#[bitbybit::bitenum(u11)]
|
||||||
|
pub enum Apid {
|
||||||
|
Tmtc = 1,
|
||||||
|
Cfdp = 2,
|
||||||
|
|
||||||
|
Acs = 3,
|
||||||
|
Eps = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum Event {
|
||||||
|
ControllerEvent(control::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Event {
|
||||||
|
fn message_type(&self) -> MessageType {
|
||||||
|
MessageType::Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct TmHeader {
|
||||||
|
pub sender_id: ComponentId,
|
||||||
|
pub target_id: ComponentId,
|
||||||
|
pub message_type: MessageType,
|
||||||
|
/// Telemetry can either be sent unsolicited, or as a response to telecommands.
|
||||||
|
pub tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
/// Raw CDS short timestamp.
|
||||||
|
pub timestamp: Option<[u8; 7]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TmHeader {
|
||||||
|
pub fn new(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
// Can not fail, CDS short always requires 7 bytes.
|
||||||
|
let mut stamp_buf: [u8; MIN_CDS_FIELD_LEN] = [0; MIN_CDS_FIELD_LEN];
|
||||||
|
cds_timestamp.write_to_bytes(&mut stamp_buf).unwrap();
|
||||||
|
Self {
|
||||||
|
sender_id,
|
||||||
|
target_id,
|
||||||
|
tc_id,
|
||||||
|
message_type,
|
||||||
|
timestamp: Some(stamp_buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_for_unsolicited_tm(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(sender_id, target_id, message_type, None, cds_timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_for_tc_response(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
tc_id: CcsdsPacketIdAndPsc,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
sender_id,
|
||||||
|
target_id,
|
||||||
|
message_type,
|
||||||
|
Some(tc_id),
|
||||||
|
cds_timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes_postcard(data: &[u8]) -> Result<(Self, &[u8]), postcard::Error> {
|
||||||
|
postcard::take_from_bytes::<TmHeader>(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct TcHeader {
|
||||||
|
pub target_id: ComponentId,
|
||||||
|
pub request_type: MessageType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TcHeader {
|
||||||
|
pub fn new(target_id: ComponentId, request_type: MessageType) -> Self {
|
||||||
|
Self {
|
||||||
|
target_id,
|
||||||
|
request_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum MessageType {
|
||||||
|
Ping,
|
||||||
|
Mode,
|
||||||
|
Hk,
|
||||||
|
Action,
|
||||||
|
Event,
|
||||||
|
Verification,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Message {
|
||||||
|
fn message_type(&self) -> MessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic device mode which covers the requirements of most devices.
|
||||||
|
///
|
||||||
|
/// The states are related both to the physical and the logical state of the device. Some
|
||||||
|
/// device handlers control the power supply of their own device and an off state might also
|
||||||
|
/// mean that the device is physically off.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum DeviceMode {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
/// Normal operation mode where periodic polling might be done as well.
|
||||||
|
Normal = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DeviceMode {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"off" => Ok(DeviceMode::Off),
|
||||||
|
"on" => Ok(DeviceMode::On),
|
||||||
|
"normal" => Ok(DeviceMode::Normal),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum HkRequestType {
|
||||||
|
OneShot,
|
||||||
|
/// Enable periodic HK generation with a specified frequency.
|
||||||
|
EnablePeriodic(core::time::Duration),
|
||||||
|
DisablePeriodic,
|
||||||
|
/// Modify periodic HK generation interval.
|
||||||
|
ModifyInterval(core::time::Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
pub mod request {
|
||||||
|
use crate::{DeviceMode, HkRequestType, Message};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ModeRequest {
|
||||||
|
SetMode(DeviceMode),
|
||||||
|
ReadMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum HkId {
|
||||||
|
Sensor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct HkRequest {
|
||||||
|
pub id: HkId,
|
||||||
|
pub req_type: HkRequestType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
Hk(HkRequest),
|
||||||
|
Mode(ModeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::Hk(_hk_request) => crate::MessageType::Hk,
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct MgmData {
|
||||||
|
pub valid: bool,
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub z: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::{DeviceMode, Message, mgm::MgmData};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum HkResponse {
|
||||||
|
MgmData(MgmData),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
|
||||||
|
pub enum ModeResponse {
|
||||||
|
/// New mode has been set.
|
||||||
|
Mode(DeviceMode),
|
||||||
|
/// Setting a mode timed out.
|
||||||
|
SetModeTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Hk(HkResponse),
|
||||||
|
Mode(ModeResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Hk(_hk_response) => crate::MessageType::Hk,
|
||||||
|
Response::Mode(_mode_failure) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use crate::DeviceMode;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AssemblyMode {
|
||||||
|
/// The assembly mode ressembles the modes of the devices it controls. It also tries to keep
|
||||||
|
/// the children in the correct mode by re-commanding them into the correct mode.
|
||||||
|
Device(DeviceMode),
|
||||||
|
/// Mode keeping disabled.
|
||||||
|
NoModeKeeping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AssemblyMode {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"off" => Ok(AssemblyMode::Device(DeviceMode::Off)),
|
||||||
|
"on" => Ok(AssemblyMode::Device(DeviceMode::On)),
|
||||||
|
"normal" => Ok(AssemblyMode::Device(DeviceMode::Normal)),
|
||||||
|
"no_mode_keeping" => Ok(AssemblyMode::NoModeKeeping),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
use crate::{HkRequestType, Message, mgm_assembly::AssemblyMode};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum HkId {
|
||||||
|
Sensor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ModeRequest {
|
||||||
|
SetMode(AssemblyMode),
|
||||||
|
ReadMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct HkRequest {
|
||||||
|
pub id: HkId,
|
||||||
|
pub req_type: HkRequestType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
Mode(ModeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::{DeviceMode, Message};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ModeCommandFailure {
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ModeReport {
|
||||||
|
/// Mode of the assembly.
|
||||||
|
Mode(super::AssemblyMode),
|
||||||
|
/// Timeout failure setting the children modes.
|
||||||
|
SetModeTimeout([Option<DeviceMode>; 2]),
|
||||||
|
/// Children are in wrong mode after commanding.
|
||||||
|
WrongMode([Option<DeviceMode>; 2]),
|
||||||
|
/// An assembly tried modekeeping but can not keep its mode.
|
||||||
|
CanNotKeepMode([Option<DeviceMode>; 2]),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Mode(ModeReport),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Mode(_mode_report) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use strum::IntoEnumIterator as _;
|
||||||
|
|
||||||
|
#[bitbybit::bitfield(u16, debug, default = 0x0)]
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SwitchesBitfield {
|
||||||
|
#[bit(2, rw)]
|
||||||
|
magnetorquer: bool,
|
||||||
|
#[bit(1, rw)]
|
||||||
|
mgm1: bool,
|
||||||
|
#[bit(0, rw)]
|
||||||
|
mgm0: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
Hash,
|
||||||
|
strum::EnumIter,
|
||||||
|
num_enum::IntoPrimitive,
|
||||||
|
num_enum::TryFromPrimitive,
|
||||||
|
)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum SwitchId {
|
||||||
|
Mgm0 = 0,
|
||||||
|
Mgm1 = 1,
|
||||||
|
Mgt = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum SwitchState {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
Unknown = 2,
|
||||||
|
Faulty = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwitchStateBinary> for SwitchState {
|
||||||
|
fn from(value: SwitchStateBinary) -> Self {
|
||||||
|
match value {
|
||||||
|
SwitchStateBinary::Off => SwitchState::Off,
|
||||||
|
SwitchStateBinary::On => SwitchState::On,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum SwitchStateBinary {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SwitchMapBinary = HashMap<SwitchId, SwitchStateBinary>;
|
||||||
|
|
||||||
|
pub struct SwitchMapBinaryWrapper(pub SwitchMapBinary);
|
||||||
|
|
||||||
|
impl Default for SwitchMapBinaryWrapper {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut switch_map = SwitchMapBinary::default();
|
||||||
|
for entry in SwitchId::iter() {
|
||||||
|
switch_map.insert(entry, SwitchStateBinary::Off);
|
||||||
|
}
|
||||||
|
Self(switch_map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SwitchRequest {
|
||||||
|
pub switch_id: SwitchId,
|
||||||
|
pub target_state: SwitchStateBinary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchRequest {
|
||||||
|
pub fn new(switch_id: SwitchId, target_state: SwitchStateBinary) -> Self {
|
||||||
|
Self {
|
||||||
|
switch_id,
|
||||||
|
target_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_id(&self) -> SwitchId {
|
||||||
|
self.switch_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_state(&self) -> SwitchStateBinary {
|
||||||
|
self.target_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
use crate::{DeviceMode, Message};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Mode(DeviceMode),
|
||||||
|
Ping,
|
||||||
|
GetSwitches,
|
||||||
|
EnableSwitches(SwitchesBitfield),
|
||||||
|
DisableSwitches(SwitchesBitfield),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::GetSwitches => crate::MessageType::Action,
|
||||||
|
Request::EnableSwitches(_switches) | Request::DisableSwitches(_switches) => {
|
||||||
|
crate::MessageType::Action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use super::*;
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Switches(SwitchesBitfield),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Switches(_switches) => crate::MessageType::Action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/tmtc_conf.json
|
||||||
|
__pycache__
|
||||||
|
|
||||||
|
/venv
|
||||||
|
/log
|
||||||
|
/.idea/*
|
||||||
|
!/.idea/runConfigurations
|
||||||
|
|
||||||
|
/seqcnt.txt
|
||||||
|
/.tmtc-history.txt
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user