Compare commits

..
Author SHA1 Message Date
muellerr 830df80291 Merge pull request 'typos and design chapter fixes' (#291) from book-design-update into main
Reviewed-on: #291
2026-09-24 19:18:36 +02:00
Robin Mueller 13325056f6 typos and design chapter fixes 2026-09-24 19:17:57 +02:00
muellerr d22ee524ff Merge pull request 'add justfile rule for manual book update command' (#290) from manual-book-update into main
Reviewed-on: #290
2026-09-24 19:17:46 +02:00
Robin Mueller 03fa49341b add justfile rule for manual book update command 2026-09-24 18:38:10 +02:00
muellerr 93fcd44f13 Merge pull request 'update nexosim to v1' (#289) from update-nexosim into main
Reviewed-on: #289
2026-09-24 16:56:42 +02:00
Robin Mueller 7ae61a81e6 update nexosim to v1 2026-09-24 16:54:57 +02:00
muellerr 9ff6600920 Merge pull request 'cleanup' (#288) from cleanup into main
Reviewed-on: #288
2026-09-24 16:32:16 +02:00
Robin Mueller 32834920d1 cleanup 2026-09-24 16:31:14 +02:00
muellerr b4869e0db4 Merge pull request 'Mgt handler' (#287) from mgt-handler into main
Reviewed-on: #287
2026-09-24 16:25:10 +02:00
Robin Mueller db906565c8 MGT handler 2026-09-24 16:23:31 +02:00
muellerr ba9ea3a12c Merge pull request 'periodic HK support for MGMs' (#286) from perioic-hk-support into main
Reviewed-on: #286
2026-09-24 15:42:21 +02:00
Robin Mueller 782f6c3936 periodic HK support for MGMs 2026-09-24 15:41:48 +02:00
muellerr 3fa655da59 Merge pull request 'simplify minisim' (#285) from simplify-minisim into main
Reviewed-on: #285
2026-09-24 15:27:57 +02:00
Robin Mueller 1aee1b8513 simplify minisim 2026-09-24 15:26:48 +02:00
muellerr d791ca2a5f Merge pull request 'Add MGM1 to simulator' (#284) from mgm1-in-sim into main
Reviewed-on: #284
2026-09-24 11:45:49 +02:00
Robin Mueller 7a2a317d58 Add MGM1 to simulator 2026-09-24 11:44:52 +02:00
muellerr d9ead9516e Merge pull request 'MGM FDIR' (#283) from mgm-recovery into main
Reviewed-on: #283
2026-09-23 18:57:30 +02:00
Robin Mueller 7b74bee581 FDIR extensions and improvements for MGM device handler
- try power cycling now instead of going to faulty immediately
- after too many power cycles in a short time frame, go to faulty
- new FDIR/recovery helper which is generic
- new failure variants for fault injection: transient failures to test
  that a power cycles could fix the issue
2026-09-23 18:55:52 +02:00
muellerr 1c6e777d24 Merge pull request 'Granular event generation control' (#282) from granular-event-generation-control into main
Reviewed-on: #282
2026-09-23 15:33:13 +02:00
Robin Mueller 919f1aa65d add granular event management control 2026-09-23 15:31:35 +02:00
muellerr 883998e31d Merge pull request 'more events' (#281) from more-events into main
Reviewed-on: #281
2026-09-22 11:54:28 +02:00
Robin Mueller 8d36fd3761 more events 2026-09-22 11:48:40 +02:00
muellerr 2a7b3796ec Merge pull request 'Try reducing boilerplate' (#280) from try-reducing-boilerplate into main
Reviewed-on: #280
2026-09-16 13:59:53 +02:00
Robin Mueller 1c98f3772f extract switch and mode helper 2026-09-16 13:58:11 +02:00
muellerr 76074d69e4 Merge pull request 'feat: add FDIR fault counter and wire it into the MGM device handler' (#277) from fdir-addition into main
Reviewed-on: #277
2026-09-16 13:57:50 +02:00
Robin Mueller 86d8528d21 feat: add FDIR fault counter and wire it into the MGM device handler
Add satrs::fdir::FaultCounter, an FSFW-style error threshold counter:
counts faults, decrements over time when faults stop, and reports
when a threshold is exceeded. Two variants for now, mirroring the
hk.rs helper pattern:
- FaultCounterStd, backed by std::time::Instant
- FaultCounterEmbassy, backed by embassy_time::Instant (embassy-time
  feature), with an optional defmt::Format impl gated on the defmt
  feature

Add satrs::health::HealthTableMapSync::default() for easy construction
of a shared, global health table.

Wire both into the example app's MGM device handler as the first real
FDIR use case:
- the minisim MGM model gains SpiFaultMode (None/AllZeros/AllOnes) and
  a SetSpiFault request, so a stuck SPI bus can be injected for testing,
  independent of switch state
- MgmHandlerLis3Mdl::poll_sensor checks the SPI transfer result: a
  comm timeout or an all-1s stuck-bus reply (the same pattern the sim
  already uses for "device off") counts as a fault. Above threshold,
  the component is marked Faulty in a HealthTableMapSync shared from
  main.rs. This logic lives in the device handler, not the SPI comm
  layer, since deciding what a failed transfer means for FDIR is a
  handler concern.
- an all-0s reply is deliberately not treated as a fault, since it
  collides with a legitimate zero-field reading
2026-09-16 13:56:47 +02:00
muellerr 7d6e4ca2f8 Merge pull request 'improve system view' (#279) from improve-system-view into main
Reviewed-on: #279
2026-09-16 12:30:57 +02:00
Robin Mueller eeb6fe17cc improve system view 2026-09-16 12:30:25 +02:00
muellerr 9de9ce3131 Merge pull request 'add sat-rs system view' (#278) from satrs-system-view into main
Reviewed-on: #278
2026-09-15 15:47:13 +02:00
Robin Mueller 87fc65fdd4 add sat-rs system view 2026-09-15 15:46:26 +02:00
Robin Mueller 877312d3c3 Merge pull request #4 from 0rlych1kk4/fix/ccsds-packet-length
fix: correct CCSDS packet length calculation
2026-09-01 12:50:12 +02:00
0rlych1kk4 1649306802 fix: correct CCSDS packet length calculation
Signed-off-by: 0rlych1kk4 <orlychikka@gmail.com>
2026-08-31 14:28:24 +08:00
muellerr 12824b2906 Merge pull request 'small documentation fix' (#276) from small-doc-fix into main
Reviewed-on: #276
2026-08-26 15:33:06 +02:00
Robin Mueller cbce7916c2 small documentation fix 2026-08-26 15:32:06 +02:00
muellerr 54971683bf Merge pull request 'continue PUS removal' (#275) from continue-pus-removal into main
Reviewed-on: #275
2026-08-26 15:24:32 +02:00
Robin Mueller 4ba4d3e6a3 try to update more chapters 2026-08-26 15:24:08 +02:00
muellerr 2d9b14e69e Merge pull request 'update satrs book' (#274) from update-satrs-book into main
Reviewed-on: #274
2026-08-25 18:56:14 +02:00
Robin Mueller d2b8bc6afb update satrs book 2026-08-25 18:55:52 +02:00
muellerr 2cf39a0e1c Merge pull request 'add tmtc-utils crate' (#273) from add-tmtc-utils-crate into main
Reviewed-on: #273
2026-08-25 17:54:14 +02:00
Robin Mueller 1e44b2d59f add tmtc-utils crate 2026-08-25 17:53:38 +02:00
muellerr c40b8f32c9 Merge pull request 'continue subsystem integration' (#272) from start-adding-acs-subsys into main
Reviewed-on: #272
2026-08-25 16:58:50 +02:00
Robin Mueller f6a87cff28 continue subsystem integration 2026-08-25 16:57:34 +02:00
muellerr 8195dac587 Merge pull request 'Rename example models crates to types' (#271) from rename-model-crates-to-types into main
Reviewed-on: #271
2026-08-25 14:19:17 +02:00
Robin MuellerandClaude Sonnet 5 5fffcc011f Rename example models crates to types
- satrs-example/models (package "models") -> satrs-example/types ("types")
- embedded-examples/models (package "embedded-models") -> embedded-examples/types ("embedded-types")

Updates all path dependencies and use/models:: references accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 14:13:02 +02:00
muellerr 3e4c3c4e64 Merge pull request 'start adding ACS subsystem' (#266) from start-adding-acs-subsys into main
Reviewed-on: #266
2026-07-28 17:07:25 +02:00
Robin Mueller 3f0937de83 start adding ACS subsystem 2026-07-28 17:00:26 +02:00
muellerr d2db28468f Merge pull request 'ran cargo update' (#270) from dep-update into main
Reviewed-on: #270
2026-07-10 12:05:22 +02:00
muellerr 3550026c00 ran cargo update 2026-07-07 13:27:43 +02:00
muellerr ad4908feef Merge pull request 'update STM32F3 example code' (#268) from update-stm32f3-example-code into main
Reviewed-on: #268
2026-07-07 13:24:51 +02:00
muellerr 20bc6c4fb6 update STM32F3 example code 2026-07-07 13:21:26 +02:00
muellerr e97e6f2a54 Merge pull request 'smaller fixes' (#269) from smaller-fixes into main
Reviewed-on: #269
2026-05-19 11:57:00 +02:00
Robin Mueller 1c9ebe8a75 smaller fixes 2026-05-19 11:56:44 +02:00
muellerr 7a464843ed Merge pull request 'update STM32H7 example code' (#267) from update-stm32h7-example-code into main
Reviewed-on: #267
2026-05-19 11:52:45 +02:00
Robin Mueller d58b1db55b update STM32H7 example code 2026-05-19 11:51:43 +02:00
muellerr b1253eaad4 Merge pull request 'Rework ACS' (#264) from rework-acs into main
Reviewed-on: #264
2026-03-18 11:19:24 +01:00
Robin Mueller ae4d26b8bd re-work ACS 2026-03-17 15:56:25 +01:00
muellerr cfcfabb5e3 Merge pull request 'fixes for switching' (#262) from fix-for-mgm-switching into main
Reviewed-on: #262
2026-03-12 13:48:04 +01:00
Robin Mueller 70f747ad86 fixes for switching 2026-03-12 13:45:19 +01:00
muellerr f44aac6ea2 Merge pull request 'probably need to re-work the mode model..' (#261) from continue-example-update into main
Reviewed-on: #261
2026-03-12 12:01:16 +01:00
Robin Mueller df517af85b probably need to re-work the mode model.. 2026-03-12 12:00:24 +01:00
muellerr ae9edf5888 Merge pull request 'minor clean up' (#260) from minor-cleanup into main
Reviewed-on: #260
2026-03-10 11:57:21 +01:00
Robin Mueller 512384026c minor clean up 2026-03-10 11:56:39 +01:00
muellerr 42c7a3b9ee Merge pull request 'Move to CCSDS + serde, rip out PUS' (#259) from move-to-ccsds-and-serde into main
Reviewed-on: #259
2026-03-10 11:56:16 +01:00
muellerr 1e15e3d501 move to CCSDS + serde for sat-rs example 2026-03-10 11:45:11 +01:00
muellerr d7e6732888 Merge pull request 'CCSDS scheduler' (#258) from ccsds-scheduler into main
Reviewed-on: #258
2025-11-27 16:09:52 +01:00
Robin Mueller c27569a526 new CCSDS packet scheduler 2025-11-27 16:02:39 +01:00
muellerr b2bc87641c Merge pull request 'bumped sat-rs' (#257) from bump-satrs into main
Reviewed-on: #257
2025-11-06 14:54:05 +01:00
Robin Mueller c8245772bb bumped sat-rs 2025-11-06 14:53:49 +01:00
224 changed files with 20157 additions and 118708 deletions

No files matched your search

+6 -1
View File
@@ -11,6 +11,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install libudev-dev on Ubuntu
if: ${{ matrix.os == 'ubuntu-latest' }}
run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo check
# Check example with static pool configuration
- run: cargo check -p satrs-example --no-default-features
@@ -23,6 +26,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo nextest run --all-features
- run: cargo test --doc --all-features
@@ -57,7 +61,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
clippy:
name: Clippy
@@ -67,4 +71,5 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo clippy -- -D warnings
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Check" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="check" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy Fix" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy --fix" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docs" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="doc --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Doctest" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test --doc" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Examples" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --example test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="fmt" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw example" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run -p satrs-example --bin satrs-example" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw simple client" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package fsrc-example --bin client" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
<option name="command" value="test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test All" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -- --include-ignored" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test satrs-core" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -p satrs-core --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+5 -1
View File
@@ -4,9 +4,13 @@ members = [
"satrs",
"satrs-mib",
"satrs-example",
"satrs-minisim",
"satrs-example/types",
"satrs-example/client",
"satrs-example/minisim",
"satrs-shared",
"tmtc-utils",
"embedded-examples/embedded-client",
"embedded-examples/types",
]
exclude = [
+4 -5
View File
@@ -1,7 +1,6 @@
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p>
[![sat-rs website](https://img.shields.io/badge/sat--rs-website-darkgreen?style=flat)](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
[![sat-rs book](https://img.shields.io/badge/sat--rs-book-darkgreen?style=flat)](https://robamu.github.io/sat-rs/book/)
[![sat-rs book](https://img.shields.io/badge/sat--rs-book-darkgreen?style=flat)](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
[![Crates.io](https://img.shields.io/crates/v/satrs)](https://crates.io/crates/satrs)
[![docs.rs](https://img.shields.io/docsrs/satrs)](https://docs.rs/satrs)
[![matrix chat](https://img.shields.io/matrix/sat-rs%3Amatrix.org)](https://matrix.to/#/#sat-rs:matrix.org)
@@ -12,8 +11,8 @@ sat-rs
This is the repository of the sat-rs library. Its primary goal is to provide re-usable components
to write on-board software for remote systems like rovers or satellites. It is specifically written
for the special requirements for these systems. You can find an overview of the project and the
link to the [more high-level sat-rs book](https://robamu.github.io/sat-rs/book/)
at the [IRS software projects website](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
link to the [more high-level sat-rs book](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
at the [IRS software projects website](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/).
This is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
@@ -31,7 +30,7 @@ This project currently contains following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
Primary information resource in addition to the API documentation, hosted
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/). It can be useful to read
this first before delving into the example application and the API documentation.
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
Primary crate.
+4 -3
View File
@@ -8,10 +8,11 @@ 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" }
tmtc-utils = { git = "https://egit.irs.uni-stuttgart.de/rust/tmtc-utils.git", version = "0.1" }
spacepackets = { version = "0.18" }
embedded-types = { path = "../types" }
tmtc-utils = { path = "../../tmtc-utils" }
postcard = { version = "1", features = ["alloc"] }
anyhow = "1"
cobs = "0.5"
fern = "0.7"
humantime = "2"
@@ -1,2 +1,3 @@
[interface]
serial_port = "/dev/ttyUSB0"
# udp_addr = "192.168.XXX.XX:7301"
@@ -0,0 +1,71 @@
use std::time::Duration;
use anyhow::bail;
use clap::Parser;
use cobs::CobsDecoderOwned;
use embedded_client::setup_logger;
use embedded_types::stm32f3;
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long)]
ping: bool,
/// Set frequency in milliseconds.
#[arg(short, long)]
set_led_frequency: Option<u32>,
}
fn main() -> anyhow::Result<()> {
setup_logger().expect("failed to initialize logger");
println!("-- STM32F3 TMTC client --");
let cli = Cli::parse();
let config = embedded_client::Config::new_from_file();
if config.interface.serial_port.is_none() {
bail!("Serial port not specified in configuration file.");
}
let serial_port = config.interface.serial_port.as_ref().unwrap();
let serial = serialport::new(serial_port, 115200)
.open()
.expect("opening serial port failed");
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
if cli.ping {
let tc = create_stm32f3_tc(&embedded_types::stm32f3::Request::Ping);
log::info!(
"Sending ping request with TC ID: {:#010x}",
tc.ccsds_packet_id_and_psc().raw()
);
transport.send(&tc.to_vec()).unwrap();
}
if let Some(freq_ms) = cli.set_led_frequency {
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
let tc = create_stm32f3_tc(&request);
log::info!(
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
request,
tc.ccsds_packet_id_and_psc().raw()
);
transport.send(&tc.to_vec()).unwrap();
}
log::info!("Waiting for response...");
loop {
transport
.receive(|packet: &[u8]| {
let reader = CcsdsPacketReader::new_with_checksum(packet);
log::info!("Received packet: {:?}", reader);
})
.unwrap();
}
}
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
let req_raw = postcard::to_allocvec(&request).unwrap();
let sp_header = SpHeader::new_from_apid(embedded_types::stm32f3::PUS_APID);
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
}
@@ -0,0 +1,95 @@
use std::{net::UdpSocket, time::Duration};
use anyhow::{Context as _, bail};
use clap::Parser;
use embedded_client::setup_logger;
use embedded_types::{TmHeader, stm32h7};
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
use tmtc_utils::transport::udp::PacketTransportUdp;
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long)]
ping: bool,
/// Set frequency in milliseconds.
#[arg(short, long)]
set_led_frequency: Option<u32>,
/// UDP address to bind to.
#[arg(short, long)]
udp_addr: Option<std::net::SocketAddr>,
}
fn main() -> anyhow::Result<()> {
setup_logger().expect("failed to initialize logger");
println!("-- STM32H7 TMTC client --");
let cli = Cli::parse();
let config = embedded_client::Config::new_from_file();
let mut udp_addr = cli.udp_addr;
if udp_addr.is_none() {
udp_addr = config.interface.udp_addr;
}
if udp_addr.is_none() {
bail!("UDP address not specified in config.toml or via command line");
}
let udp_addr = udp_addr.unwrap();
log::info!("binding to UDP address: {}", udp_addr);
let local_socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket");
let mut transport = PacketTransportUdp::new(local_socket, udp_addr)
.with_context(|| "crateing UDP transport failed")?;
if cli.ping {
let tc = create_stm32h7_tc(&embedded_types::stm32h7::Request::Ping);
log::info!(
"Sending ping request with TC ID: {:#010x}",
tc.ccsds_packet_id_and_psc().raw()
);
transport.send(&tc.to_vec()).unwrap();
}
if let Some(freq_ms) = cli.set_led_frequency {
let request = stm32h7::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
let tc = create_stm32h7_tc(&request);
log::info!(
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
request,
tc.ccsds_packet_id_and_psc().raw()
);
transport.send(&tc.to_vec()).unwrap();
}
log::info!("Waiting for response...");
loop {
transport
.receive(|packet: &[u8]| {
let reader = CcsdsPacketReader::new_with_checksum(packet);
log::debug!("Received packet: {:?}", reader);
if let Ok(reader) = reader {
let packet_data = reader.packet_data();
let tm_header = postcard::take_from_bytes::<TmHeader>(packet_data);
if let Ok((tm_header, remainder)) = tm_header {
let response = postcard::from_bytes::<stm32h7::Response>(remainder);
if let Ok(response) = response {
log::info!(
"Received TM with header: {:?} and response: {:?}",
tm_header,
response
);
} else {
log::error!("Failed to deserialize response: {:?}", response.err());
}
} else {
log::error!("Failed to deserialize TM header: {:?}", tm_header.err());
}
}
})
.unwrap();
}
}
fn create_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned {
let req_raw = postcard::to_allocvec(&request).unwrap();
let sp_header = SpHeader::new_from_apid(embedded_types::stm32h7::PUS_APID);
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
}
@@ -0,0 +1,43 @@
use std::{fs::File, io::Read as _, net::SocketAddr, path::Path, time::SystemTime};
#[derive(Debug, serde::Deserialize)]
pub struct Config {
pub interface: Interface,
}
#[derive(Debug, serde::Deserialize)]
pub struct Interface {
pub serial_port: Option<String>,
pub udp_addr: Option<SocketAddr>,
}
impl Config {
pub fn new_from_file() -> Self {
let mut config_file =
File::open(Path::new("config.toml")).expect("opening config.toml file failed");
let mut toml_str = String::new();
config_file
.read_to_string(&mut toml_str)
.expect("reading config.toml file failed");
let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed");
config
}
}
pub fn setup_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(log::LevelFilter::Info)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
@@ -1,107 +0,0 @@
use std::{
fs::File,
io::Read,
path::Path,
time::{Duration, SystemTime},
};
use clap::Parser;
use cobs::CobsDecoderOwned;
use satrs_stm32f3_disco_rtic::Request;
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>,
}
#[derive(Debug, serde::Deserialize)]
struct Config {
interface: Interface,
}
#[derive(Debug, serde::Deserialize)]
struct Interface {
serial_port: String,
}
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(())
}
fn main() {
setup_logger().expect("failed to initialize logger");
println!("sat-rs embedded examples TMTC client");
let cli = Cli::parse();
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);
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 request = Request::Ping;
let tc = create_stm32f3_tc(&request);
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 = 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: &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()
}
File diff suppressed because it is too large. Load diff
@@ -7,6 +7,7 @@ default-run = "satrs-stm32f3-disco-rtic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
embedded-types = { path = "../types", features = ["defmt"] }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "1"
@@ -14,10 +15,12 @@ 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"] }
embassy-stm32 = { version = "0.6", features = ["defmt", "stm32f303vc", "memory-x", "unstable-pac", "time-driver-any"] }
embassy-time = { version = "0.5", features = ["defmt", "generic-queue-16", "defmt-timestamp-uptime-ms"]}
enumset = "1"
heapless = "0.9"
spacepackets = { version = "0.17", default-features = false, features = ["defmt", "serde"] }
embassy-sync = "0.8"
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
static_cell = "2"
cobs = { version = "0.5", default-features = false, features = ["defmt"] }
postcard = { version = "1" }
@@ -27,15 +30,9 @@ 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"] }
#[dependencies.satrs]
# path = "../../satrs"
#default-features = false
# features = ["defmt"]
[dev-dependencies]
defmt-test = "0.4"
defmt-test = "0.5"
# cargo test
[profile.test]
@@ -1,33 +0,0 @@
/* Linker script for the STM32F303VCT6 */
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
FLASH : ORIGIN = 0x08000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 40K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* You may want to use this variable to locate the call stack and static
variables in different memory regions. Below is shown the default value */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* You can use this symbol to customize the location of the .text section */
/* If omitted the .text section will be placed right after the .vector_table
section */
/* This is required only on microcontrollers that store some configuration right
after the vector table */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* Example of putting non-initialized variables into custom RAM locations. */
/* This assumes you have defined a region RAM2 above, and in the Rust
sources added the attribute `#[link_section = ".ram2bss"]` to the data
you want to place there. */
/* Note that the section will not be zero-initialized by the runtime! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
@@ -6,12 +6,9 @@ use rtic::app;
#[app(device = embassy_stm32)]
mod app {
use rtic_monotonics::fugit::ExtU32;
use rtic_monotonics::Monotonic as _;
use embassy_time::Timer;
use satrs_stm32f3_disco_rtic::{Direction, LedPinSet, Leds};
rtic_monotonics::systick_monotonic!(Mono, 1000);
#[shared]
struct Shared {}
@@ -22,7 +19,7 @@ mod app {
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
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");
@@ -39,8 +36,6 @@ mod app {
};
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 {},
@@ -55,7 +50,7 @@ mod app {
async fn blinky(cx: blinky::Context) {
loop {
cx.local.leds.blink_next(cx.local.current_dir);
Mono::delay(200.millis()).await;
Timer::after_millis(200).await;
}
}
}
@@ -1,13 +1,11 @@
#![no_main]
#![no_std]
use defmt_rtt as _;
use panic_probe as _;
use arbitrary_int::u11;
use core::time::Duration;
use embassy_stm32::gpio::Output;
use spacepackets::{
ccsds_packet_len_for_user_data_len_with_checksum, CcsdsPacketCreationError,
CcsdsPacketCreatorWithReservedData, CcsdsPacketIdAndPsc, SpacePacketHeader,
};
pub const APID: u11 = u11::new(0x02);
@@ -40,49 +38,6 @@ impl Direction {
}
}
#[derive(Copy, Clone, Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
pub enum Request {
Ping,
ChangeBlinkFrequency(Duration),
}
#[derive(Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
pub struct TmHeader {
pub tc_packet_id: Option<CcsdsPacketIdAndPsc>,
pub uptime_millis: u32,
}
#[derive(Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
pub enum Response {
CommandDone,
}
pub fn tm_size(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(
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())
}
pub struct Leds {
pub north: Output<'static>,
pub north_east: Output<'static>,
@@ -1,30 +1,21 @@
#![no_std]
#![no_main]
use arbitrary_int::{u11, u14};
use arbitrary_int::u14;
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
use satrs_stm32f3_disco_rtic::{create_tm_packet, tm_size, CcsdsPacketId, Request, Response};
use spacepackets::{CcsdsPacketCreationError, SpHeader};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_types::{create_tm_packet, stm32f3, tm_size, TmHeader};
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
use defmt_rtt as _; // global logger
use panic_probe as _;
use rtic::app;
#[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;
pub const PUS_APID: u11 = u11::new(0x02);
// 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);
@@ -35,7 +26,8 @@ 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();
static TM_QUEUE: embassy_sync::channel::Channel<CriticalSectionRawMutex, TmPacket, 16> =
embassy_sync::channel::Channel::new();
#[derive(Debug, defmt::Format, thiserror::Error)]
pub enum TmSendError {
@@ -47,8 +39,8 @@ pub enum TmSendError {
#[derive(Debug, defmt::Format)]
pub struct RequestWithTcId {
pub request: Request,
pub tc_id: CcsdsPacketId,
pub request: stm32f3::Request,
pub tc_id: CcsdsPacketIdAndPsc,
}
#[app(device = embassy_stm32)]
@@ -57,18 +49,20 @@ mod app {
use super::*;
use arbitrary_int::u14;
use embassy_time::Timer;
use embedded_types::stm32f3::{Request, Response};
use rtic::Mutex;
use rtic_sync::{
channel::{Receiver, Sender},
make_channel,
};
use satrs_stm32f3_disco_rtic::{CcsdsPacketId, LedPinSet, Request, Response};
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>;
DMA1_CHANNEL6 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH6>;
DMA1_CHANNEL7 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH7>;
});
#[shared]
@@ -86,15 +80,13 @@ mod app {
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
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 {
@@ -112,7 +104,7 @@ mod app {
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,
p.USART2, p.PA3, p.PA2, p.DMA1_CH7, p.DMA1_CH6, Irqs, config,
)
.unwrap();
@@ -142,10 +134,7 @@ mod app {
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;
Timer::after_millis(current_blink_freq.as_millis() as u64).await;
}
}
@@ -158,7 +147,8 @@ mod app {
)]
async fn serial_tx_handler(cx: serial_tx_handler::Context) {
loop {
while let Some(vec) = TM_QUEUE.dequeue() {
loop {
let vec = TM_QUEUE.receive().await;
let encoded_len =
cobs::encode_including_sentinels(&vec[0..vec.len()], cx.local.encoded_buf);
defmt::debug!("sending {} bytes over UART", encoded_len);
@@ -167,9 +157,7 @@ mod app {
.write(&cx.local.encoded_buf[0..encoded_len])
.await
.unwrap();
continue;
}
Mono::delay(TX_HANDLER_FREQ_MS.millis()).await;
}
}
@@ -198,9 +186,8 @@ mod app {
&decoder.dest()[0..packet_size],
) {
Ok(packet) => {
let packet_id = packet.packet_id();
let psc = packet.psc();
let tc_packet_id = CcsdsPacketId { packet_id, psc };
let tc_packet_id =
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet);
if let Ok(request) =
postcard::from_bytes::<Request>(packet.packet_data())
{
@@ -240,13 +227,16 @@ mod app {
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::Ping => {
handle_ping_request(&mut cx, request_with_tc_id.tc_id).await
}
Request::ChangeBlinkFrequency(duration) => {
handle_change_blink_frequency_request(
&mut cx,
request_with_tc_id.tc_id,
duration,
)
.await
}
};
if let Err(e) = tm_send_result {
@@ -258,19 +248,19 @@ mod app {
}
}
fn handle_ping_request(
cx: &mut req_handler::Context,
tc_packet_id: CcsdsPacketId,
async fn handle_ping_request(
cx: &mut req_handler::Context<'_>,
tc_packet_id: CcsdsPacketIdAndPsc,
) -> Result<(), TmSendError> {
defmt::info!("Received PUS ping telecommand, sending ping reply");
send_tm(tc_packet_id, Response::CommandDone, *cx.local.seq_count)?;
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
Ok(())
}
fn handle_change_blink_frequency_request(
cx: &mut req_handler::Context,
tc_packet_id: CcsdsPacketId,
async fn handle_change_blink_frequency_request(
cx: &mut req_handler::Context<'_>,
tc_packet_id: CcsdsPacketIdAndPsc,
duration: Duration,
) -> Result<(), TmSendError> {
defmt::info!(
@@ -280,30 +270,27 @@ mod app {
cx.shared
.blink_freq
.lock(|blink_freq| *blink_freq = duration);
send_tm(tc_packet_id, Response::CommandDone, *cx.local.seq_count)?;
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
Ok(())
}
}
fn send_tm(
tc_packet_id: CcsdsPacketId,
response: Response,
async fn send_tm(
tc_packet_id: CcsdsPacketIdAndPsc,
response: stm32f3::Response,
current_seq_count: u14,
) -> Result<(), TmSendError> {
let sp_header = SpHeader::new_for_unseg_tc(PUS_APID, current_seq_count, 0);
let tm_header = satrs_stm32f3_disco_rtic::TmHeader {
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(),
uptime_millis: embassy_time::Instant::now().as_millis(),
};
let mut tm_packet = TmPacket::new();
let tm_size = tm_size(&tm_header, &response);
tm_packet.resize(tm_size, 0).expect("vec resize failed");
create_tm_packet(&mut tm_packet, sp_header, tm_header, response)?;
if TM_QUEUE.enqueue(tm_packet).is_err() {
defmt::warn!("TC queue full");
return Err(TmSendError::Queue);
}
TM_QUEUE.send(tm_packet).await;
Ok(())
}
@@ -1,29 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H743ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,4 @@
[package]
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
name = "satrs-stm32h7-nucleo-rtic"
edition = "2021"
version = "0.1.0"
@@ -14,38 +13,28 @@ name = "integration"
harness = false
[dependencies]
embedded-types = { path = "../types", features = ["defmt"] }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
arbitrary-int = "2"
cortex-m-rt = "0.7"
defmt = "1"
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }
cortex-m-semihosting = "0.5.0"
# TODO: Replace with embassy-hal.
stm32h7xx-hal = { version="0.16", features= ["stm32h743v", "ethernet"] }
embedded-alloc = "0.6"
rtic-sync = { version = "1", features = ["defmt-03"] }
embedded-alloc = "0.7"
static_cell = "2"
rtic = { version = "2", features = ["thumbv7-backend"] }
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
postcard = "1"
[dependencies.smoltcp]
version = "0.12"
default-features = false
features = ["medium-ethernet", "proto-ipv4", "socket-raw", "socket-dhcpv4", "socket-udp", "defmt"]
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.6", features = ["stm32h743zi", "memory-x", "defmt", "time-driver-any"]}
[dependencies.rtic]
version = "2"
features = ["thumbv7-backend"]
[dependencies.rtic-monotonics]
version = "2"
features = ["cortex-m-systick"]
[dependencies.satrs]
path = "../../satrs"
# version = "0.2"
default-features = false
features = ["defmt", "heapless"]
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
embassy-net = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c" }
[dev-dependencies]
defmt-test = "0.4"
defmt-test = "0.5"
# cargo build/run
[profile.dev]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,14 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
if !config.exists() && config_template.exists() {
fs::create_dir_all(&cargo_dir).unwrap();
fs::copy(&config_template, &config).unwrap();
}
}
@@ -1,8 +0,0 @@
/venv
/.tmtc-history.txt
/log
/.idea/*
!/.idea/runConfigurations
/seqcnt.txt
/tmtc_conf.json
@@ -1,4 +0,0 @@
{
"com_if": "udp",
"tcpip_udp_port": 7301
}
@@ -1,305 +0,0 @@
#!/usr/bin/env python3
"""Example client for the sat-rs example application"""
import struct
import logging
import sys
import time
from typing import Any, Optional, cast
from prompt_toolkit.history import FileHistory, History
from spacepackets.ecss.tm import CdsShortTimestamp
import tmtccmd
from spacepackets.ecss import PusTelemetry, PusTelecommand, PusTm, PusVerificator
from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest
from tmtccmd.core.ccsds_backend import QueueWrapper
from tmtccmd.logging import add_colorlog_console_logger
from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
from tmtccmd.com import ComInterface
from tmtccmd.config import (
CmdTreeNode,
default_json_path,
SetupParams,
HookBase,
params_to_procedure_conversion,
)
from tmtccmd.config.com import SerialCfgWrapper
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
from tmtccmd.logging.pus import (
RegularTmtcLogWrapper,
RawTmtcTimedLogWrapper,
TimedLogWhen,
)
from tmtccmd.tmtc import (
TcQueueEntryType,
ProcedureWrapper,
TcProcedureType,
FeedWrapper,
SendCbParams,
DefaultPusQueueHelper,
)
from tmtccmd.pus.s5_fsfw_event import Service5Tm
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
from tmtccmd.util.obj_id import ObjectIdDictT
_LOGGER = logging.getLogger()
EXAMPLE_PUS_APID = 0x02
class SatRsConfigHook(HookBase):
def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path)
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import (
create_com_interface_default,
create_com_interface_cfg_default,
)
assert self.cfg_path is not None
cfg = create_com_interface_cfg_default(
com_if_key=com_if_key,
json_cfg_path=self.cfg_path,
space_packet_ids=None,
)
if cfg is None:
raise ValueError(
f"No valid configuration could be retrieved for the COM IF with key {com_if_key}"
)
if cfg.com_if_key == "serial_cobs":
cfg = cast(SerialCfgWrapper, cfg)
cfg.serial_cfg.serial_timeout = 0.5
return create_com_interface_default(cfg)
def get_command_definitions(self) -> CmdTreeNode:
"""This function should return the root node of the command definition tree."""
return create_cmd_definition_tree()
def get_cmd_history(self) -> Optional[History]:
"""Optionlly return a history class for the past command paths which will be used
when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids
return get_core_object_ids()
def create_cmd_definition_tree() -> CmdTreeNode:
root_node = CmdTreeNode.root_node()
root_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
root_node.add_child(CmdTreeNode("change_blink_freq", "Change blink frequency"))
return root_node
class PusHandler(SpecificApidHandlerBase):
def __init__(
self,
file_logger: logging.Logger,
verif_wrapper: VerificationWrapper,
raw_logger: RawTmtcTimedLogWrapper,
):
super().__init__(EXAMPLE_PUS_APID, None)
self.file_logger = file_logger
self.raw_logger = raw_logger
self.verif_wrapper = verif_wrapper
def handle_tm(self, packet: bytes, _user_args: Any):
try:
pus_tm = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e
service = pus_tm.service
tm_packet = None
if service == 1:
tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
)
res = self.verif_wrapper.add_tm(tm_packet)
if res is None:
_LOGGER.info(
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
)
_LOGGER.warning(
f"No matching telecommand found for {tm_packet.tc_req_id}"
)
else:
self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res)
if service == 3:
_LOGGER.info("No handling for HK packets implemented")
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
pus_tm = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if pus_tm.subservice == 25:
if len(pus_tm.source_data) < 8:
raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:]
_LOGGER.info("received JSON string: " + json_str.decode("utf-8"))
if service == 5:
tm_packet = Service5Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if service == 17:
tm_packet = Service17Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
if tm_packet.subservice == 2:
_LOGGER.info("Received Ping Reply TM[17,2]")
else:
_LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
if tm_packet is None:
_LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory"
)
tm_packet = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
self.raw_logger.log_tm(pus_tm)
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
byte_string = bytearray(struct.pack("!I", target_id))
byte_string.extend(struct.pack("!I", unique_id))
return byte_string
class TcHandler(TcHandlerBase):
def __init__(
self,
seq_count_provider: FileSeqCountProvider,
verif_wrapper: VerificationWrapper,
):
super(TcHandler, self).__init__()
self.seq_count_provider = seq_count_provider
self.verif_wrapper = verif_wrapper
self.queue_helper = DefaultPusQueueHelper(
queue_wrapper=QueueWrapper.empty(),
tc_sched_timestamp_len=7,
seq_cnt_provider=seq_count_provider,
pus_verificator=verif_wrapper.pus_verificator,
default_pus_apid=EXAMPLE_PUS_APID,
)
def send_cb(self, send_params: SendCbParams):
entry_helper = send_params.entry
if entry_helper.is_tc:
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
pus_tc_wrapper.pus_tc.seq_count = (
self.seq_count_provider.get_and_increment()
)
self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc)
raw_tc = pus_tc_wrapper.pus_tc.pack()
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
send_params.com_if.send(raw_tc)
elif entry_helper.entry_type == TcQueueEntryType.LOG:
log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, info: ProcedureWrapper):
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
cmd_path = def_proc.cmd_path
if cmd_path == "/ping":
q.add_log_cmd("Sending PUS ping telecommand")
q.add_pus_tc(PusTelecommand(service=17, subservice=1))
if cmd_path == "/change_blink_freq":
self.create_change_blink_freq_command(q)
def create_change_blink_freq_command(self, q: DefaultPusQueueHelper):
q.add_log_cmd("Changing blink frequency")
while True:
blink_freq = int(
input(
"Please specify new blink frequency in ms. Valid Range [2..10000]: "
)
)
if blink_freq < 2 or blink_freq > 10000:
print(
"Invalid blink frequency. Please specify a value between 2 and 10000."
)
continue
break
app_data = struct.pack("!I", blink_freq)
q.add_pus_tc(PusTelecommand(service=8, subservice=1, app_data=app_data))
def main():
add_colorlog_console_logger(_LOGGER)
tmtccmd.init_printout(False)
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path())
parser_wrapper = PreArgsParsingWrapper()
parser_wrapper.create_default_parent_parser()
parser_wrapper.create_default_parser()
parser_wrapper.add_def_proc_args()
params = SetupParams()
post_args_wrapper = parser_wrapper.parse(hook_obj, params)
proc_wrapper = ProcedureParamsWrapper()
if post_args_wrapper.use_gui:
post_args_wrapper.set_params_without_prompts(proc_wrapper)
else:
post_args_wrapper.set_params_with_prompts(proc_wrapper)
params.apid = EXAMPLE_PUS_APID
setup_args = SetupWrapper(
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
)
# Create console logger helper and file loggers
tmtc_logger = RegularTmtcLogWrapper()
file_logger = tmtc_logger.logger
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
verificator = PusVerificator()
verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger)
# Create primary TM handler and add it to the CCSDS Packet Handler
tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger)
ccsds_handler = CcsdsTmHandler(generic_handler=None)
ccsds_handler.add_apid_handler(tm_handler)
# Create TC handler
seq_count_provider = PusFileSeqCountProvider()
tc_handler = TcHandler(seq_count_provider, verification_wrapper)
tmtccmd.setup(setup_args=setup_args)
init_proc = params_to_procedure_conversion(setup_args.proc_param_wrapper)
tmtc_backend = tmtccmd.create_default_tmtc_backend(
setup_wrapper=setup_args,
tm_handler=ccsds_handler,
tc_handler=tc_handler,
init_procedure=init_proc,
)
tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj)
try:
while True:
state = tmtc_backend.periodic_op(None)
if state.request == BackendRequest.TERMINATION_NO_ERROR:
sys.exit(0)
elif state.request == BackendRequest.DELAY_IDLE:
_LOGGER.info("TMTC Client in IDLE mode")
time.sleep(3.0)
elif state.request == BackendRequest.DELAY_LISTENER:
time.sleep(0.8)
elif state.request == BackendRequest.DELAY_CUSTOM:
if state.next_delay.total_seconds() <= 0.4:
time.sleep(state.next_delay.total_seconds())
else:
time.sleep(0.4)
elif state.request == BackendRequest.CALL_NEXT:
pass
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()
@@ -1,2 +0,0 @@
tmtccmd == 8.0.1
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
@@ -0,0 +1,2 @@
[toolchain]
targets = ["thumbv7em-none-eabihf"]
@@ -5,51 +5,53 @@
#![no_std]
#![no_main]
use rtic::app;
use satrs_stm32h7_nucleo_rtic as _;
use stm32h7xx_hal::{block, prelude::*, timer::Timer};
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
mod app {
use embassy_stm32::gpio;
use cortex_m_rt::entry;
#[shared]
struct Shared {}
#[entry]
fn main() -> ! {
defmt::println!("starting stm32h7 blinky example");
#[local]
struct Local {}
// Get access to the device specific peripherals from the peripheral access crate
let dp = stm32h7xx_hal::stm32::Peripherals::take().unwrap();
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
let p = embassy_stm32::init(Default::default());
defmt::info!("Hello World!");
// Configure gpio B pin 0 as a push-pull output.
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
let ld2 = gpio::Output::new(p.PB7, gpio::Level::High, gpio::Speed::Low);
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
// Take ownership over the RCC devices and convert them into the corresponding HAL structs
let rcc = dp.RCC.constrain();
// Schedule the blinking task
blink::spawn(ld1, ld2, ld3).ok();
let pwr = dp.PWR.constrain();
let pwrcfg = pwr.freeze();
(Shared {}, Local {})
}
// Freeze the configuration of all the clocks in the system and
// retrieve the Core Clock Distribution and Reset (CCDR) object
let rcc = rcc.use_hse(8.MHz()).bypass_hse();
let ccdr = rcc.freeze(pwrcfg, &dp.SYSCFG);
#[task()]
async fn blink(
_cx: blink::Context,
mut ld1: gpio::Output<'static>,
mut ld2: gpio::Output<'static>,
mut ld3: gpio::Output<'static>,
) {
loop {
defmt::info!("high");
ld1.set_high();
ld2.set_high();
ld3.set_high();
embassy_time::Timer::after_millis(500).await;
// Acquire the GPIOB peripheral
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
// Configure gpio B pin 0 as a push-pull output.
let mut ld1 = gpiob.pb0.into_push_pull_output();
// Configure gpio B pin 7 as a push-pull output.
let mut ld2 = gpiob.pb7.into_push_pull_output();
// Configure gpio B pin 14 as a push-pull output.
let mut ld3 = gpiob.pb14.into_push_pull_output();
// Configure the timer to trigger an update every second
let mut timer = Timer::tim1(dp.TIM1, ccdr.peripheral.TIM1, &ccdr.clocks);
timer.start(1.Hz());
// Wait for the timer to trigger an update and change the state of the LED
loop {
ld1.toggle();
ld2.toggle();
ld3.toggle();
block!(timer.wait()).unwrap();
defmt::info!("low");
ld1.set_low();
ld2.set_low();
ld3.set_low();
embassy_time::Timer::after_millis(500).await;
}
}
}
@@ -1,11 +1,13 @@
#![no_main]
#![no_std]
use satrs_stm32h7_nucleo_rtic as _; // global logger + panicking-behavior + memory layout
// global logger + panicking-behavior + memory layout
use satrs_stm32h7_nucleo_rtic as _;
#[cortex_m_rt::entry]
fn main() -> ! {
defmt::println!("Hello, world!");
satrs_stm32h7_nucleo_rtic::exit()
loop {
defmt::println!("Hello, world!");
cortex_m::asm::delay(100_000_000);
}
}
@@ -1,13 +1,8 @@
#![no_main]
#![no_std]
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
// TODO(5) adjust HAL import
use stm32h7xx_hal as _; // memory layout
use defmt_rtt as _;
use embassy_stm32 as _;
use panic_probe as _;
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
@@ -17,14 +12,6 @@ fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
@@ -32,9 +19,7 @@ pub fn exit() -> ! {
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
loop {
debug::exit(debug::EXIT_FAILURE);
}
panic!("unexpected hard fault");
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
+299 -436
View File
@@ -3,422 +3,215 @@
extern crate alloc;
use rtic::app;
use rtic_monotonics::systick::prelude::*;
use satrs::pool::{PoolAddr, PoolProvider, StaticHeaplessMemoryPool};
use satrs::static_subpool;
// global logger + panicking-behavior + memory layout
use embassy_stm32::bind_interrupts;
use satrs_stm32h7_nucleo_rtic as _;
use smoltcp::socket::udp::UdpMetadata;
use smoltcp::socket::{dhcpv4, udp};
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage};
use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr};
use stm32h7xx_hal::ethernet;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const PORT: u16 = 7301;
const HEAP_SIZE: usize = 131_072;
const TC_SOURCE_CHANNEL_DEPTH: usize = 16;
pub type SharedPool = StaticHeaplessMemoryPool<3>;
pub type TcSourceChannel = rtic_sync::channel::Channel<PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceTx = rtic_sync::channel::Sender<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceRx = rtic_sync::channel::Receiver<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
#[global_allocator]
static HEAP: Heap = Heap::empty();
systick_monotonic!(Mono, 1000);
// We place the memory pool buffers inside the larger AXISRAM.
pub const SUBPOOL_SMALL_NUM_BLOCKS: u16 = 32;
pub const SUBPOOL_SMALL_BLOCK_SIZE: usize = 32;
pub const SUBPOOL_MEDIUM_NUM_BLOCKS: u16 = 16;
pub const SUBPOOL_MEDIUM_BLOCK_SIZE: usize = 128;
pub const SUBPOOL_LARGE_NUM_BLOCKS: u16 = 8;
pub const SUBPOOL_LARGE_BLOCK_SIZE: usize = 2048;
// This data will be held by Net through a mutable reference
pub struct NetStorageStatic<'a> {
socket_storage: [SocketStorage<'a>; 8],
}
// MaybeUninit allows us write code that is correct even if STORE is not
// initialised by the runtime
static mut STORE: MaybeUninit<NetStorageStatic> = MaybeUninit::uninit();
static mut UDP_RX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_RX: [u8; 2048] = [0; 2048];
static mut UDP_TX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_TX: [u8; 2048] = [0; 2048];
/// Locally administered MAC address
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
pub struct Net {
iface: Interface,
ethdev: ethernet::EthernetDMA<4, 4>,
dhcp_handle: SocketHandle,
}
const TC_QUEUE_DEPTH: usize = 32;
const TM_QUEUE_DEPTH: usize = 32;
impl Net {
pub fn new(
sockets: &mut SocketSet<'static>,
mut ethdev: ethernet::EthernetDMA<4, 4>,
ethernet_addr: HardwareAddress,
) -> Self {
let config = Config::new(ethernet_addr);
let mut iface = Interface::new(
config,
&mut ethdev,
smoltcp::time::Instant::from_millis(Mono::now().duration_since_epoch().to_millis()),
);
// Create sockets
let dhcp_socket = dhcpv4::Socket::new();
iface.update_ip_addrs(|addrs| {
let _ = addrs.push(IpCidr::new(IpAddress::v4(192, 168, 1, 99), 0));
});
let dhcp_handle = sockets.add(dhcp_socket);
Net {
iface,
ethdev,
dhcp_handle,
}
}
/// Polls on the ethernet interface. You should refer to the smoltcp
/// documentation for poll() to understand how to call poll efficiently
pub fn poll<'a>(&mut self, sockets: &'a mut SocketSet) -> bool {
let uptime = Mono::now().duration_since_epoch();
let timestamp = smoltcp::time::Instant::from_millis(uptime.to_millis());
self.iface.poll(timestamp, &mut self.ethdev, sockets)
}
pub fn poll_dhcp<'a>(&mut self, sockets: &'a mut SocketSet) -> Option<dhcpv4::Event<'a>> {
let opt_event = sockets.get_mut::<dhcpv4::Socket>(self.dhcp_handle).poll();
if let Some(event) = &opt_event {
match event {
dhcpv4::Event::Deconfigured => {
defmt::info!("DHCP lost configuration");
self.iface.update_ip_addrs(|addrs| addrs.clear());
self.iface.routes_mut().remove_default_ipv4_route();
}
dhcpv4::Event::Configured(config) => {
defmt::info!("DHCP configuration acquired");
defmt::info!("IP address: {}", config.address);
self.iface.update_ip_addrs(|addrs| {
addrs.clear();
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
});
if let Some(router) = config.router {
defmt::debug!("Default gateway: {}", router);
self.iface
.routes_mut()
.add_default_ipv4_route(router)
.unwrap();
} else {
defmt::debug!("Default gateway: None");
self.iface.routes_mut().remove_default_ipv4_route();
}
}
}
}
opt_event
}
}
pub struct UdpNet {
udp_handle: SocketHandle,
last_client: Option<UdpMetadata>,
tc_source_tx: TcSourceTx,
}
impl UdpNet {
pub fn new<'sockets>(sockets: &mut SocketSet<'sockets>, tc_source_tx: TcSourceTx) -> Self {
// SAFETY: The RX and TX buffers are passed here and not used anywhere else.
let udp_rx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_RX_META[..] }, unsafe {
&mut UDP_RX[..]
});
let udp_tx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_TX_META[..] }, unsafe {
&mut UDP_TX[..]
});
let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
let udp_handle = sockets.add(udp_socket);
Self {
udp_handle,
last_client: None,
tc_source_tx,
}
}
pub fn poll<'sockets>(
&mut self,
sockets: &'sockets mut SocketSet,
shared_pool: &mut SharedPool,
) {
let socket = sockets.get_mut::<udp::Socket>(self.udp_handle);
if !socket.is_open() {
if let Err(e) = socket.bind(PORT) {
defmt::warn!("binding UDP socket failed: {}", e);
}
}
loop {
match socket.recv() {
Ok((data, client)) => {
match shared_pool.add(data) {
Ok(store_addr) => {
if let Err(e) = self.tc_source_tx.try_send(store_addr) {
defmt::warn!("TC source channel is full: {}", e);
}
}
Err(e) => {
defmt::warn!("could not add UDP packet to shared pool: {}", e);
}
}
self.last_client = Some(client);
// TODO: Implement packet wiretapping.
}
Err(e) => match e {
udp::RecvError::Exhausted => {
break;
}
udp::RecvError::Truncated => {
defmt::warn!("UDP packet was truncacted");
}
},
};
}
}
}
#[app(device = stm32h7xx_hal::stm32, peripherals = true)]
#[app(device = embassy_stm32, peripherals = false)]
mod app {
use core::ptr::addr_of_mut;
use super::*;
use rtic_monotonics::fugit::MillisDurationU32;
use satrs::spacepackets::ecss::tc::PusTcReader;
use stm32h7xx_hal::ethernet::{EthernetMAC, PHY};
use stm32h7xx_hal::gpio::{Output, Pin};
use stm32h7xx_hal::prelude::*;
use stm32h7xx_hal::stm32::Interrupt;
use arbitrary_int::u14;
use embassy_net::udp::UdpSocket;
use embassy_net::StackResources;
use embassy_stm32::eth;
use embassy_stm32::gpio;
use embassy_stm32::peripherals;
use embassy_stm32::rng;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_time::Duration;
use embassy_time::Timer;
use embassy_time::WithTimeout as _;
use embedded_types::create_tm_packet;
use embedded_types::stm32h7;
use embedded_types::tm_size;
use embedded_types::TmHeader;
use spacepackets::CcsdsPacketCreationError;
use spacepackets::CcsdsPacketIdAndPsc;
use spacepackets::CcsdsPacketReader;
use spacepackets::SpHeader;
use static_cell::StaticCell;
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
RNG => rng::InterruptHandler<peripherals::RNG>;
});
type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
struct BlinkyLeds {
led1: Pin<'B', 7, Output>,
led2: Pin<'B', 14, Output>,
led1: gpio::Output<'static>,
led2: gpio::Output<'static>,
}
#[local]
struct Local {
net_runner: embassy_net::Runner<'static, Device>,
net_stack: embassy_net::Stack<'static>,
leds: BlinkyLeds,
link_led: Pin<'B', 0, Output>,
net: Net,
udp: UdpNet,
tc_source_rx: TcSourceRx,
phy: ethernet::phy::LAN8742A<EthernetMAC>,
link_led: gpio::Output<'static>,
tc_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tc_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tm_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
tm_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
}
#[shared]
struct Shared {
blink_freq: MillisDurationU32,
eth_link_up: bool,
sockets: SocketSet<'static>,
shared_pool: SharedPool,
sequence_count: u14,
blink_freq: embassy_time::Duration,
}
#[init]
fn init(mut cx: init::Context) -> (Shared, Local) {
fn init(_cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
let pwr = cx.device.PWR.constrain();
let pwrcfg = pwr.freeze();
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::Div1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::Hsi,
prediv: PllPreDiv::Div4,
mul: PllMul::Mul50,
fracn: None,
divp: Some(PllDiv::Div2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::Pll1P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::Div2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let rcc = cx.device.RCC.constrain();
// Try to keep the clock configuration similar to one used in STM examples:
// https://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/NUCLEO-H743ZI/Examples/GPIO/GPIO_EXTI/Src/main.c
let ccdr = rcc
.sys_ck(400.MHz())
.hclk(200.MHz())
.use_hse(8.MHz())
.bypass_hse()
.pclk1(100.MHz())
.pclk2(100.MHz())
.pclk3(100.MHz())
.pclk4(100.MHz())
.freeze(pwrcfg, &cx.device.SYSCFG);
// Initialize the systick interrupt & obtain the token to prove that we did
Mono::start(cx.core.SYST, ccdr.clocks.sys_ck().to_Hz());
// Those are used in the smoltcp of the stm32h7xx-hal , I am not fully sure what they are
// good for.
cx.core.SCB.enable_icache();
cx.core.DWT.enable_cycle_counter();
let gpioa = cx.device.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = cx.device.GPIOB.split(ccdr.peripheral.GPIOB);
let gpioc = cx.device.GPIOC.split(ccdr.peripheral.GPIOC);
let gpiog = cx.device.GPIOG.split(ccdr.peripheral.GPIOG);
let link_led = gpiob.pb0.into_push_pull_output();
let mut led1 = gpiob.pb7.into_push_pull_output();
let mut led2 = gpiob.pb14.into_push_pull_output();
let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let mut led1 = gpio::Output::new(periphs.PB7, gpio::Level::Low, gpio::Speed::Medium);
let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium);
// Criss-cross pattern looks cooler.
led1.set_high();
led2.set_low();
let leds = BlinkyLeds { led1, led2 };
let rmii_ref_clk = gpioa.pa1.into_alternate::<11>();
let rmii_mdio = gpioa.pa2.into_alternate::<11>();
let rmii_mdc = gpioc.pc1.into_alternate::<11>();
let rmii_crs_dv = gpioa.pa7.into_alternate::<11>();
let rmii_rxd0 = gpioc.pc4.into_alternate::<11>();
let rmii_rxd1 = gpioc.pc5.into_alternate::<11>();
let rmii_tx_en = gpiog.pg11.into_alternate::<11>();
let rmii_txd0 = gpiog.pg13.into_alternate::<11>();
let rmii_txd1 = gpiob.pb13.into_alternate::<11>();
let mac_addr = smoltcp::wire::EthernetAddress::from_bytes(&MAC_ADDRESS);
/// Ethernet descriptor rings are a global singleton
#[link_section = ".sram3.eth"]
static mut DES_RING: MaybeUninit<ethernet::DesRing<4, 4>> = MaybeUninit::uninit();
let (eth_dma, eth_mac) = ethernet::new(
cx.device.ETHERNET_MAC,
cx.device.ETHERNET_MTL,
cx.device.ETHERNET_DMA,
(
rmii_ref_clk,
rmii_mdio,
rmii_mdc,
rmii_crs_dv,
rmii_rxd0,
rmii_rxd1,
rmii_tx_en,
rmii_txd0,
rmii_txd1,
),
// SAFETY: We do not move the returned DMA struct across thread boundaries, so this
// should be safe according to the docs.
unsafe { DES_RING.assume_init_mut() },
mac_addr,
ccdr.peripheral.ETH1MAC,
&ccdr.clocks,
);
// Initialise ethernet PHY...
let mut lan8742a = ethernet::phy::LAN8742A::new(eth_mac.set_phy_addr(0));
lan8742a.phy_reset();
lan8742a.phy_init();
unsafe {
ethernet::enable_interrupt();
cx.core.NVIC.set_priority(Interrupt::ETH, 196); // Mid prio
cortex_m::peripheral::NVIC::unmask(Interrupt::ETH);
}
// unsafe: mutable reference to static storage, we only do this once
let store = unsafe {
let store_ptr = STORE.as_mut_ptr();
// Initialise the socket_storage field. Using `write` instead of
// assignment via `=` to not call `drop` on the old, uninitialised
// value
addr_of_mut!((*store_ptr).socket_storage).write([SocketStorage::EMPTY; 8]);
// Now that all fields are initialised we can safely use
// assume_init_mut to return a mutable reference to STORE
STORE.assume_init_mut()
};
let (tc_source_tx, tc_source_rx) =
rtic_sync::make_channel!(PoolAddr, TC_SOURCE_CHANNEL_DEPTH);
let mut sockets = SocketSet::new(&mut store.socket_storage[..]);
let net = Net::new(&mut sockets, eth_dma, mac_addr.into());
let udp = UdpNet::new(&mut sockets, tc_source_tx);
let mut shared_pool: SharedPool = StaticHeaplessMemoryPool::new(true);
static_subpool!(
SUBPOOL_SMALL,
SUBPOOL_SMALL_SIZES,
SUBPOOL_SMALL_NUM_BLOCKS as usize,
SUBPOOL_SMALL_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_MEDIUM,
SUBPOOL_MEDIUM_SIZES,
SUBPOOL_MEDIUM_NUM_BLOCKS as usize,
SUBPOOL_MEDIUM_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_LARGE,
SUBPOOL_LARGE_SIZES,
SUBPOOL_LARGE_NUM_BLOCKS as usize,
SUBPOOL_LARGE_BLOCK_SIZE,
link_section = ".axisram"
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
shared_pool
.grow(
SUBPOOL_SMALL.get_mut().unwrap(),
SUBPOOL_SMALL_SIZES.get_mut().unwrap(),
SUBPOOL_SMALL_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
SUBPOOL_MEDIUM.get_mut().unwrap(),
SUBPOOL_MEDIUM_SIZES.get_mut().unwrap(),
SUBPOOL_MEDIUM_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
SUBPOOL_LARGE.get_mut().unwrap(),
SUBPOOL_LARGE_SIZES.get_mut().unwrap(),
SUBPOOL_LARGE_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// Init network stack
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) =
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
// Set up global allocator. Use AXISRAM for the heap.
#[link_section = ".axisram"]
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
eth_link_check::spawn().expect("eth link check failed");
static TC_CHANNEL: static_cell::ConstStaticCell<
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TC_QUEUE_DEPTH>,
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
let tc_channel = TC_CHANNEL.take();
let tc_sender = tc_channel.sender();
let tc_receiver = tc_channel.receiver();
static TM_CHANNEL: static_cell::ConstStaticCell<
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
let tm_channel = TM_CHANNEL.take();
let tm_sender = tm_channel.sender();
let tm_receiver = tm_channel.receiver();
net_lib_task::spawn().expect("spawning net library task failed");
net_app_task::spawn().expect("spawning net application task failed");
blinky::spawn().expect("spawning blink task failed");
udp_task::spawn().expect("spawning UDP task failed");
tc_source_task::spawn().expect("spawning TC source task failed");
tc_handler::spawn().expect("spawning TC handler task failed");
(
Shared {
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
eth_link_up: false,
sockets,
shared_pool,
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
sequence_count: u14::new(0),
},
Local {
link_led,
leds,
net,
udp,
tc_source_rx,
phy: lan8742a,
net_runner: runner,
net_stack: stack,
tc_tx: tc_sender,
tc_rx: tc_receiver,
tm_tx: tm_sender,
tm_rx: tm_receiver,
},
)
}
@@ -430,94 +223,164 @@ mod app {
leds.led1.toggle();
leds.led2.toggle();
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Mono::delay(current_blink_freq).await;
Timer::after_millis(current_blink_freq.as_millis()).await;
}
}
/// This task checks for the network link.
#[task(local=[link_led, phy], shared=[eth_link_up])]
async fn eth_link_check(mut cx: eth_link_check::Context) {
let phy = cx.local.phy;
let link_led = cx.local.link_led;
#[task(local=[net_runner])]
async fn net_lib_task(cx: net_lib_task::Context) {
cx.local.net_runner.run().await;
}
#[task(local = [net_stack, link_led, tc_tx, tm_rx])]
async fn net_app_task(cx: net_app_task::Context) {
pub const MTU: usize = 1500;
// Ensure those are in the data section by making them static.
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
let rx_udp_meta = RX_UDP_META.take();
let rx_udp_bufs = RX_UDP_BUFS.take();
let tx_udp_meta = TX_UDP_META.take();
let tx_udp_bufs = TX_UDP_BUFS.take();
let mut rx_buffer = [0; MTU];
loop {
let link_was_up = cx.shared.eth_link_up.lock(|link_up| *link_up);
if phy.poll_link() {
if !link_was_up {
link_led.set_high();
cx.shared.eth_link_up.lock(|link_up| *link_up = true);
defmt::info!("Ethernet link up");
cx.local.net_stack.wait_link_up().await;
cx.local.link_led.set_high();
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
cx.local.net_stack.wait_config_up().await;
let config = cx.local.net_stack.config_v4();
defmt::info!("Network task initialized, config: {}", config);
let mut udp = UdpSocket::new(
cx.local.net_stack.clone(),
rx_udp_meta,
rx_udp_bufs,
tx_udp_meta,
tx_udp_bufs,
);
defmt::info!("UDP socket bound to port {}", PORT);
udp.bind(PORT).expect("failed to bind UDP socket");
let mut remote_endpoint = None;
loop {
if !cx.local.net_stack.is_link_up() {
defmt::warn!("Network link is down");
cx.local.link_led.set_low();
break;
}
} else if link_was_up {
link_led.set_low();
cx.shared.eth_link_up.lock(|link_up| *link_up = false);
defmt::info!("Ethernet link down");
}
Mono::delay(100.millis()).await;
}
}
#[task(binds=ETH, local=[net], shared=[sockets])]
fn eth_isr(mut cx: eth_isr::Context) {
// SAFETY: We do not write the register mentioned inside the docs anywhere else.
unsafe {
ethernet::interrupt_handler();
}
// Check and process ETH frames and DHCP. UDP is checked in a different task.
cx.shared.sockets.lock(|sockets| {
cx.local.net.poll(sockets);
cx.local.net.poll_dhcp(sockets);
});
}
/// This task routes UDP packets.
#[task(local=[udp], shared=[sockets, shared_pool])]
async fn udp_task(mut cx: udp_task::Context) {
loop {
cx.shared.sockets.lock(|sockets| {
cx.shared.shared_pool.lock(|pool| {
cx.local.udp.poll(sockets, pool);
})
});
Mono::delay(40.millis()).await;
}
}
/// This task handles all the incoming telecommands.
#[task(local=[read_buf: [u8; 1024] = [0; 1024], tc_source_rx], shared=[shared_pool])]
async fn tc_source_task(mut cx: tc_source_task::Context) {
loop {
let recv_result = cx.local.tc_source_rx.recv().await;
match recv_result {
Ok(pool_addr) => {
cx.shared.shared_pool.lock(|pool| {
match pool.read(&pool_addr, cx.local.read_buf.as_mut()) {
Ok(packet_len) => {
defmt::info!("received {} bytes in the TC source task", packet_len);
match PusTcReader::new(&cx.local.read_buf[0..packet_len]) {
Ok((packet, _tc_len)) => {
// TODO: Handle packet here or dispatch to dedicated PUS
// handler? Dispatching could simplify some things and make
// the software more scalable..
defmt::info!("received PUS packet: {}", packet);
}
Err(e) => {
defmt::info!("invalid TC format, not a PUS packet: {}", e);
}
}
if let Err(e) = pool.delete(pool_addr) {
defmt::warn!("deleting TC data failed: {}", e);
}
}
Err(e) => {
defmt::warn!("TC packet read failed: {}", e);
}
match udp
.recv_from(&mut rx_buffer)
.with_timeout(Duration::from_millis(200))
.await
{
Ok(result) => match result {
Ok((data, meta)) => {
remote_endpoint = Some(meta.endpoint);
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
}
});
Err(e) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
},
Err(_e) => (),
}
Err(e) => {
defmt::warn!("TC source reception error: {}", e);
if let Some(endpoint) = remote_endpoint {
while let Ok(packet) = cx.local.tm_rx.try_receive() {
match udp.send_to(&packet, endpoint).await {
Ok(_) => {
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
}
Err(e) => defmt::warn!("udp send error: {}", e),
}
}
}
};
}
}
}
#[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])]
async fn tc_handler(mut cx: tc_handler::Context) {
loop {
let tc = cx.local.tc_rx.receive().await;
match CcsdsPacketReader::new_with_checksum(&tc) {
Ok(packet) => {
let packet_id = packet.packet_id();
let psc = packet.psc();
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
if let Ok(request) =
postcard::from_bytes::<stm32h7::Request>(packet.packet_data())
{
let response = match request {
stm32h7::Request::Ping => {
defmt::info!("Received Ping request");
stm32h7::Response::Ok
}
stm32h7::Request::ChangeBlinkFrequency(duration) => {
defmt::info!(
"Received blinky frequency change request: {} ms",
duration.as_millis()
);
cx.shared.blink_freq.lock(|current| {
*current = Duration::from_millis(duration.as_millis() as u64)
});
stm32h7::Response::Ok
}
};
let sequence_count = cx.shared.sequence_count.lock(|v| {
let current = *v;
*v = v.wrapping_add(u14::new(1));
current
});
// Send Pong/OK response immediately.
if let Err(e) =
send_tm(tc_packet_id, response, sequence_count, cx.local.tm_tx).await
{
defmt::warn!("Failed to send TM response: {}", e);
}
}
}
Err(e) => defmt::warn!("Failed to parse received TC packet: {}", e,),
}
defmt::info!("Received from UDP client: {}", tc.as_slice());
}
}
async fn send_tm(
tc_packet_id: CcsdsPacketIdAndPsc,
response: stm32h7::Response,
current_seq_count: u14,
sender: &embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
) -> Result<(), CcsdsPacketCreationError> {
let sp_header = SpHeader::new_for_unseg_tc(stm32h7::PUS_APID, current_seq_count, 0);
let tm_header = TmHeader {
tc_packet_id: Some(tc_packet_id),
uptime_millis: embassy_time::Instant::now().as_millis(),
};
let tm_size = tm_size(&tm_header, &response);
let mut packet = alloc::vec![0; tm_size];
create_tm_packet(&mut packet, sp_header, tm_header, response)?;
sender.send(packet).await;
Ok(())
}
}
@@ -1,7 +1,7 @@
#![no_std]
#![no_main]
use stm32h7_testapp as _; // memory layout + panic handler
use satrs_stm32h7_nucleo_rtic as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
@@ -1,2 +0,0 @@
/settings.json
/.cortex-debug.*
@@ -1,12 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"rust-lang.rust",
"probe-rs.probe-rs-debugger"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
@@ -1,22 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32H743ZITx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32h7-nucleo-rtic",
"rttEnabled": true,
"svdFile": "STM32H743.svd"
}
]
}
]
}
@@ -1,20 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "embedded-types"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", default-features = false }
defmt = { version = "1", optional = true }
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
postcard = { version = "1", features = ["defmt"] }
arbitrary-int = "2"
+84
View File
@@ -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 {}
+28 -18
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<!--Created by yEd 3.25.1-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
@@ -15,7 +15,6 @@
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="360.0" width="479.0" x="771.3047672479152" y="458.0"/>
@@ -39,7 +38,7 @@
<y:Geometry height="177.64799999999997" width="200.75199999999973" x="1037.5527672479152" y="470.15200000000027"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.919921875" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.25241088867188" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -50,7 +49,7 @@
<y:Geometry height="34.0" width="84.39999999999986" x="1068.8351781652768" y="508.2800000000002"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.638671875" x="23.380664062499818" xml:space="preserve" y="8.015625">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.37623596191406" x="24.011882019042787" xml:space="preserve" y="6.827942848205566">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -61,7 +60,7 @@
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="550.4800000000001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.453125" x="13.973437499999818" xml:space="preserve" y="8.015625">Magnetometer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.56465148925781" x="12.917674255370912" xml:space="preserve" y="6.827942848205566">Magnetometers<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -72,7 +71,7 @@
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="594.9000000000001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.83203125" x="15.783984374999818" xml:space="preserve" y="8.015625">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.24858093261719" x="17.075709533691224" xml:space="preserve" y="6.827942848205566">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -83,7 +82,7 @@
<y:Geometry height="34.0" width="120.39999999999986" x="783.4063563305535" y="545.2800000000002"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.931640625" x="17.234179687499932" xml:space="preserve" y="8.015625">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="81.62857055664062" x="19.38571472167962" xml:space="preserve" y="6.827942848205566">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -94,7 +93,7 @@
<y:Geometry height="34.0" width="120.39999999999986" x="840.5407126611072" y="677.8000000000002"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="105.05078125" x="7.674609374999932" xml:space="preserve" y="8.015625">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="96.96467590332031" x="11.717662048339776" xml:space="preserve" y="6.827942848205566">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -105,7 +104,7 @@
<y:Geometry height="34.0" width="120.39999999999986" x="1005.2814253222144" y="677.8000000000002"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.111328125" x="11.644335937499932" xml:space="preserve" y="8.015625">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.46463012695312" x="13.96768493652337" xml:space="preserve" y="6.827942848205566">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -116,7 +115,18 @@
<y:Geometry height="34.0" width="120.39999999999986" x="931.6174253222144" y="775.5920000000002"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="38.740234375" x="40.82988281249993" xml:space="preserve" y="8.015625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.292236328125" x="42.05388183593743" xml:space="preserve" y="6.827942848205566">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="34.0" width="120.39999999999986" x="1074.8237583924094" y="555.3663917525774"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.56465148925781" x="12.917674255370912" xml:space="preserve" y="6.827942848205566">Magnetometers<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
@@ -153,7 +163,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.955078125" x="12.686124396959713" xml:space="preserve" y="-22.50440429687478">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.06060791015625" x="13.086737058292329" xml:space="preserve" y="-23.692086448669215">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
@@ -167,7 +177,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.923828125" x="-87.89792405764274" xml:space="preserve" y="-40.606550292968564">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.78047180175781" x="-85.32624589602165" xml:space="preserve" y="-42.388073520660214">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
@@ -185,10 +195,10 @@
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n3" target="n7">
<edge id="e5" source="n9" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Path sx="57.449660948066594" sy="-4.88639175257731" tx="0.0" ty="0.0">
<y:Point x="1223.8814253222142" y="567.4800000000001"/>
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
</y:Path>
@@ -207,7 +217,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="60.4140625" x="-2.4087265765670054" xml:space="preserve" y="145.1356018470808">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.73236083984375" x="-1.658628279667937" xml:space="preserve" y="143.94791969528637">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
@@ -221,7 +231,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="119.751953125" x="-132.27600022951788" xml:space="preserve" y="-32.03587548828091">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.32476806640625" x="-128.562407700221" xml:space="preserve" y="-33.22355764007534">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
@@ -235,7 +245,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.2421875" x="-62.15307370122309" xml:space="preserve" y="34.80927001953137">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.27665710449219" x="-58.67030850346919" xml:space="preserve" y="33.62158786773705">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
@@ -248,7 +258,7 @@
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.95703125" x="73.38950633588263" xml:space="preserve" y="-62.699758016200235">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.228179931640625" x="74.7630257963599" xml:space="preserve" y="-63.88744016799478">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

+20 -3
View File
@@ -11,15 +11,23 @@ test:
cargo nextest run --all-features
cargo test --doc --all-features
embedded:
embedded: embedded-stm32h7 embedded-stm32f3
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
fmt:
cargo fmt --all
[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
@@ -27,3 +35,12 @@ docs-satrs:
RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc -p satrs --all-features
docs: docs-satrs
[working-directory:"satrs-book"]
book *args:
mdbook build {{args}}
# Pass --dry-run to preview the changes first.
[working-directory:"satrs-book"]
deploy-book *args: book
rsync -avz --delete {{args}} book/html/ numalfix@documentation.irs.uni-stuttgart.de:/home/numalfix/www/projects/sat-rs/book/
+1 -1
View File
@@ -1,7 +1,7 @@
sat-rs book
=========
High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
High-level documentation of the [sat-rs project](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/).
## Building
+8 -1
View File
@@ -1,9 +1,16 @@
[book]
authors = ["Robin Mueller"]
language = "en"
multilingual = false
src = "src"
title = "The sat-rs book"
[output.html]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
[output.linkcheck]
command = "mdbook-linkcheck2"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
+39
View File
@@ -0,0 +1,39 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
(() => {
const darkThemes = ['ayu', 'navy', 'coal'];
const lightThemes = ['light', 'rust'];
const classList = document.getElementsByTagName('html')[0].classList;
let lastThemeWasLight = true;
for (const cssClass of classList) {
if (darkThemes.includes(cssClass)) {
lastThemeWasLight = false;
break;
}
}
const theme = lastThemeWasLight ? 'default' : 'dark';
mermaid.initialize({ startOnLoad: true, theme });
// Simplest way to make mermaid re-render the diagrams in the new theme is via refreshing the page
for (const darkTheme of darkThemes) {
document.getElementById('mdbook-theme-' + darkTheme).addEventListener('click', () => {
if (lastThemeWasLight) {
window.location.reload();
}
});
}
for (const lightTheme of lightThemes) {
document.getElementById('mdbook-theme-' + lightTheme).addEventListener('click', () => {
if (!lastThemeWasLight) {
window.location.reload();
}
});
}
})();
+2609
View File
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -1,17 +1,22 @@
# Summary
- [Introduction](./introduction.md)
- [Design](./design.md)
# Basic concepts and components
- [Communication with Space Systems](./communication.md)
- [TMTC modelling](./tmtc-modelling.md)
- [Working with Constrained Systems](./constrained-systems.md)
- [Actions](./actions.md)
- [Modes and Health](./modes-and-health.md)
- [Housekeeping Data](./housekeeping.md)
- [Events](./events.md)
# Architecture
- [System View](./system-view.md)
- [Design](./design.md)
# Example project
- [The satrs-example application](./example.md)
+4
View File
@@ -7,3 +7,7 @@
- [Modelling space systems](./modelling-space-systems.md)
- [Ground Segments](./ground-segments.md)
Refer to new sections in the system view page:
- [Fault Detection, Isolation and Recovery (FDIR)](./fdir.md)
- and the [mode tree](./mode-tree.md)
+5 -36
View File
@@ -3,40 +3,9 @@
Space systems generally need to be commanded regularly. This can include commands periodically
required to ensure a healthy system, or commands to reach the mission goals.
These commands can be modelled using the concept of Actions. the ECSS PUS standard also provides
the PUS service 8 for actions, but provides few concrete subservices and specification on how
action commanding could look like.
`sat-rs` proposes two recommended ways to perform action commanding:
1. Target ID and Action ID based. The target ID is a 32-bit unsigned ID for an OBSW object entity
which can also accept Actions. The action ID is a 32-bit unsigned ID for each action that a
target is able to perform.
2. Target ID and Action String based. The target ID is the same as in the first proposal, but
the unique action is identified by a string.
The library provides an `ActionRequest` abstraction to model both of these cases.
## Commanding with ECSS PUS 8
`sat-rs` provides a generic ECSS PUS 8 action command handler. This handler can convert PUS 8
telecommands which use the commanding scheme 1 explained above to an `ActionRequest` which is
then forwarded to the target specified by the Target ID.
There are 3 requirements for the PUS 8 telecommand:
1. The subservice 128 must be used
2. Bytes 0 to 4 of application data must contain the target ID in `u32` big endian format.
3. Bytes 4 to 8 of application data must contain the action ID in `u32` big endian format.
4. The rest of the application data are assumed to be command specific additional parameters. They
will be added to an IPC store and the corresponding store address will be sent as part of the
`ActionRequest`.
## Sending back telemetry
There are some cases where the regular verification provided by PUS in response to PUS action
commands is not sufficient and some additional telemetry needs to be sent to ground. In that
case, it is recommended to chose some custom subservice for action TM data and then send the
telemetry using the same scheme as shown above, where the first 8 bytes of the application
data is reserved for the target ID and action ID.
These commands can be modelled using the concept of Actions. If you have not read the
[TMTC modelling](./tmtc-modelling.md) chapter yet, it is recommended to read it first.
For a low number of actions, it is recommended to add the actions as `enum` variants of your
`Request` type. For a higher number of actions, you can create a dedicated `ActionRequest`
structure.
+62 -25
View File
@@ -2,25 +2,20 @@
# Communication with sat-rs based software
Communication is a vital topic for remote system which are usually not (directly)
Communication is a vital topic for remote systems which are usually not (directly)
connected to the internet and only have 1-2 communication links during nominal operation. However,
most of these systems have internet access during development cycle. There are various standards
provided by CCSDS and ECSS which can be useful to determine how to communicate with the satellite
most of these systems have internet access during the development cycle. There are various standards
provided by CCSDS which can be useful to determine how to communicate with the satellite
and the primary On-Board Software.
# Application layer
Most communication with space systems is usually packet based. For example, the CCSDS space
packet standard only specifies a 6 byte header with at least 1 byte payload. The PUS packet
standard is a subset of the space packet standard, which adds some fields and a 16 bit CRC, but
it is still centered around small packets. `sat-rs` provides support for these ECSS and CCSDS
standards and also attempts to fill the gap to the internet protocol by providing the following
components.
packet standard only specifies a 6 byte header with at least 1 byte payload. The `sat-rs` library
provides some support for the [CCSDS space packet protocol](https://ccsds.org/Pubs/133x0b2e2.pdf).
1. [UDP TMTC Server](https://docs.rs/satrs/latest/satrs/hal/std/udp_server/index.html).
UDP is already packet based which makes it an excellent fit for exchanging space packets.
2. [TCP TMTC Server Components](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/index.html).
TCP is a stream based protocol, so the library provides building blocks to parse telemetry
TCP is a stream based protocol, so the library provides building blocks to parse telecommands
from an arbitrary bytestream. Two concrete implementations are provided:
- [TCP spacepackets server](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
to parse tightly packed CCSDS Spacepackets.
@@ -31,28 +26,70 @@ components.
# Working with telemetry and telecommands (TMTC)
The commands sent to a space system are commonly called telecommands (TC) while the data received
from it are called telemetry (TM). Keeping in mind the previous section, the concept of a TC source
and a TM sink can be applied to most satellites. The TM sink is the one entity where all generated
telemetry arrives in real-time. The most important task of the TM sink usually is to send all
arriving telemetry to the ground segment of a satellite mission immediately. Another important
task might be to store all arriving telemetry persistently. This is especially important for
space systems which do not have permanent contact like low-earth-orbit (LEO) satellites.
from it are called telemetry (TM). One way to model the packet handling, which can be applied to most
satellites, is to introduce the concept of a TC source and a TM sink. The TM sink is the one entity where
all generated telemetry arrives in real-time. The most important task of the TM sink usually is to
send all arriving telemetry to the ground segment of a satellite mission immediately.
Another important task might be to store all arriving telemetry persistently. This is especially
important for space systems which do not have permanent contact like low-earth-orbit (LEO)
satellites.
The diagram below shows one concrete example of what this could look like.
```mermaid
flowchart LR
Dev[Device Handlers] --> Sink[TM Sink]
Sub[Subsystem Handlers] --> Sink
Sink --> Ground[Ground Link]
Sink --> Store[Persistent Storage]
Sink --> Udp[UDP Server]
Sink --> Tcp[TCP Server]
```
The most important task of a TC source is to deliver the telecommands to the correct recipients.
For component oriented software using message passing, this usually includes staged demultiplexing
components to determine where a command needs to be sent.
For component oriented software using message passing, this usually includes demultiplexing
to determine where a command needs to be sent.
The diagram below shows one concrete example of what this could look like.
```mermaid
flowchart LR
Udp[UDP Server] --> Source[TC Source]
Tcp[TCP Server] --> Source
Radio[Radio Handler] --> Source
Source --> Dev[Device Handlers]
Source --> Sub[Subsystem Handlers]
Source --> File[File Service Handler]
```
Using a generic concept of a TC source and a TM sink as part of the software design simplifies
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receiver only have to
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receivers only have to
forward their generated or received packets to those handler objects.
# Low-level protocols and the bridge to the communcation subsystem
# Packet format
We talked about some basic support for the CCSDS space packet protocol. This is a really simple
protocol which just specifies a header that every exchanged TMTC packet has:
![Space Packet Header](./images/space-packet-standard.png)
This is a protocol which already provides us with some useful fields:
- ID field provided by the Application Process Identifier (APID). This can also be useful for packet
multiplexing
- Basic sequence counter which can be used to determine missed packets
However, what does the actual payload that we want to send to or from the satellite actually look
like? We recommend a payload format which is created with the excellent [`serde`](https://serde.rs/)
library. The [TMTC modelling](./tmtc-modelling.md) chapter provides more information.
# Low-level protocols and the bridge to the communication subsystem
Many satellite systems usually use the lower levels of the OSI layer in addition to the application
layer covered by the PUS standard or the CCSDS space packets standard. This oftentimes requires
special hardware like dedicated FPGAs to handle forward error correction fast enough. `sat-rs`
might provide components to handle standard like the Unified Space Data Link Standard (USLP) in
layer. This oftentimes requires special hardware like dedicated FPGAs to handle forward error
correction fast enough. `sat-rs`
might provide components to handle standards like the Unified Space Data Link Protocol (USLP) in
software but most of the time the handling of communication is performed through custom
software and hardware. Still, connecting this custom software and hardware to `sat-rs` can mostly
be done by using the concept of TC sources and TM sinks mentioned previously.
+14 -8
View File
@@ -3,20 +3,26 @@
Software for space systems oftentimes has different requirements than the software for host
systems or servers. Currently, most space systems are considered embedded systems.
For these systems, the computation power and the available heap are important resources
which are also constrained. This might make completeley heap based memory management schemes which
are oftentimes used on host and server based systems unfeasable. Still, completely forbidding
heap allocations might make software development unnecessarilly difficult, especially in a
For these systems, the computation power and the available memory are important resources
which are also constrained. This might make completely heap based memory management schemes which
are oftentimes used on host and server based systems infeasible. Still, completely forbidding
heap allocations might make software development unnecessarily difficult, especially in a
time where the OBSW might be running on Linux based systems with hundreds of MBs of RAM.
A useful pattern commonly used in space systems is to limit heap allocations to program
initialization time and avoid frequent run-time allocations. This prevents issues like
running out of memory (something even Rust can not protect from) or heap fragmentation on systems
without a MMU.
without an MMU.
# Using an embedded allocator
The [`embedded-alloc`](https://github.com/rust-embedded/embedded-alloc) library provides
a global allocator based on statically sized memory blocks. It also exposes an API
which allows run-time tracking of the memory usage.
# Using pre-allocated pool structures
A candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
A candidate for heap allocations is the TMTC handling. TC, TMs and IPC data are all
candidates where the data size might vary greatly. The regular solution for host systems
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
another solution to avoid run-time allocations by offering pre-allocated static
@@ -27,8 +33,8 @@ For example, a very small telecommand (TC) pool might look like this:
The core of the pool abstractions is the
[PoolProvider trait](https://docs.rs/satrs/latest/satrs/pool/trait.PoolProvider.html).
This trait specifies the general API a pool structure should have without making assumption
of how the data is stored.
This trait specifies the general API a pool structure should have without making assumptions
about how the data is stored.
This trait is implemented by a static memory pool implementation.
The code to generate this static pool would look like this:
+20 -16
View File
@@ -2,38 +2,43 @@
Satellites and space systems in general are complex systems with a wide range of requirements for
both the hardware and the software. Consequently, the general design of the library is centered
around many light-weight components which try to impose as few restrictions as possible on how to
solve certain problems. This is also the reason why sat-rs is explicitely called a library
instead of a framework.
around many light-weight components and a toolbox principle where you assemble everything
you need instead of plugging something into a larger framework. This approach allows the largest
amount of flexibility, including the operating system and platform choice. For example, `sat-rs`
can be used both in `async` and regular synchronous platforms.
There are still a lot of common patterns and architectures across these systems where guidance
of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls
which were already solved and to avoid boilerplate code. This library tries to provide this
structure and guidance the following way:
1. Providing this book which explains the architecture and design patterns in respect to common
1. Providing this book which explains the architecture and design patterns with respect to common
issues and requirements of space systems.
2. Providing an example application. Space systems still commonly have large monolithic
primary On-Board Softwares, so the choice was made to provide one example software which
primary On-Board Software, so the choice was made to provide one example software which
contains the various features provided by sat-rs.
3. Providing a good test suite. This includes both unittests and integration tests. The integration
3. Providing a good test suite. This includes both unit tests and integration tests. The integration
tests can also serve as smaller usage examples than the large `satrs-example` application.
This library has special support for standards used in the space industry. This especially
includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European
Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards,
but it is always recommended to use some sort of standard for interoperability.
This library has special support for standards used in the space industry. The recommended
standards are provided by the Consultative Committee for Space Data Systems (CCSDS):
- The CCSDS Space Packet Protocol as the basic packet format for telecommands and telemetry.
- The CCSDS File Delivery Protocol (CFDP) for file transfers.
The library does not enforce using any of those standards, but it is always recommended to use
some sort of standard for interoperability.
A lot of the modules and design considerations are based on the Flight Software Framework (FSFW).
The FSFW has its own [documentation](https://documentation.irs.uni-stuttgart.de/fsfw/), which
will be referred to when applicable. The FSFW was developed over a period of 10 years for the
Flying Laptop Project by the University of Stuttgart with Airbus Defence and Space GmbH.
It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
It has flight heritage through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs`
library.
FLP is a medium-size small satellite with a higher budget and longer development time than EIVE,
which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
which allowed building a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
shorter development cycle and was built using cheaper COTS components. This library also tries
to accumulate the knowledge of developing the OBSW and operating the satellite for both these
different systems and provide a solution for a wider range of small satellite systems.
@@ -43,16 +48,15 @@ engineering to provide a reliable and robust basis for space On-Board Software.
of using the Rust programming language was made for the following reasons:
1. Rust has safety guarantees which are a perfect fit for space systems which generally have high
robustness and reliablity guarantees.
robustness and reliability guarantees.
2. Rust is suitable for embedded systems. It can also be run on smaller embedded systems like the
STM32 which have also become common in the space sector. All space systems are embedded systems,
which makes using large languages like Python challenging even for OBCs with more performance.
3. Rust has support for linking C APIs through its excellent FFI support. This is especially
important because many vendor provided libaries are still C based.
4. Modern tooling like a package managers and various development helper, which can further reduce
important because many vendor provided libraries are still C based.
4. Modern tooling like a package manager and various development helpers, which can further reduce
development cycles for space systems. `cargo` provides tools like auto-formatters and linters
which can immediately ensure a high software quality throughout each development cycle.
5. A large ecosystem with excellent libraries which also leverages the excellent tooling provided
previously. Integrating these libraries is a lot easier compared to languages like C/C++ where
there is still no standardized way to use packages.
+88 -16
View File
@@ -1,24 +1,96 @@
# Events
Events are an important mechanism used for remote systems to monitor unexpected
or expected anomalies and events occuring on these systems.
or expected anomalies and events occurring on these systems.
They can improve the observability of a system significantly and provide a
"paper trail" of what is happening or has happened on a satellite where regular
housekeeping packets might not be sufficient. They can also be used for fault
detection, isolation and recovery (FDIR) purposes. For example, higher level
system objects can listen on certain high criticality events to initiate
custom system responses.
One common use case for events on remote systems is to offer a light-weight publish-subscribe
mechanism and IPC mechanism for software and hardware events which are also packaged as telemetry
(TM) or can trigger a system response. They can also be tied to
Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously.
The PUS Service 5 standardizes how the ground interface for events might look like, but does not
specify how other software components might react to those events. There is the PUS Service 19,
which might be used for that purpose, but the event components recommended by this framework do not
rely on the present of this service.
## Event Severity
The following images shows how the flow of events could look like in a system where components
can generate events, and where other system components might be interested in those events:
Generally, it also makes sense to classify events according to a severity system
so operators can quickly judge the importance of an event. `sat-rs` does not
constrain the severity classes or enforce their usage, but a severity
classification like this can make sense:
![Event flow](images/events/event_man_arch.png)
- INFO
- LOW ERROR
- MEDIUM ERROR
- HIGH ERROR
For the concrete implementation of your own event management and/or event routing system, you
can have a look at the event management documentation inside the
[API documentation](https://docs.rs/satrs/latest/satrs/event_man/index.html) where you can also
find references to all examples.
## Modelling Events with Rust
Usually, events will be associated with certain software objects or handlers.
Oftentimes, developers and operators want to supply parameters or metadata
associated with an event. This can all be done using the Rust `enum` type.
Let's start with an example: a camera device
handler might have the following events:
- Image taken event
- Communication error event including an error classifier
- Communication timeout event with the configured timeout
- Overheating event
You can model these events using the following data structure, also including
a `severity` method.
```rust
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub enum Event {
ImageTaken,
CommunicationError(ErrorType),
CommunicationTimeout(core::time::Duration),
Overheating
}
impl Event {
pub fn severity(&self) -> Severity {
match self {
Event::ImageTaken => Severity::Info,
Event::CommunicationError(_) => Severity::Low,
Event::CommunicationTimeout(_) => Severity::Low,
Event::Overheating => Severity::High,
}
}
}
```
Depending on the requirements of your system, you might want to filter which
events are packaged and sent as telemetry. This requires an identification
system. A simple scheme would be to add something like this:
```rust
impl Event {
pub fn id(&self) -> u32 {
match self {
Event::ImageTaken => 0,
Event::CommunicationError(_) => 1,
Event::CommunicationTimeout(_) => 2,
Event::Overheating => 3,
}
}
}
```
## Handling events
When an event occurs in the system, you want to trigger the event.
This usually includes sending the event to a centralized event funnel. The funnel
takes care of packing the event into a telemetry packet as well as forwarding
the event to any other objects which are interested in the event. A message
queue system is the best solution for this. For example, on an embedded Linux
system, you might have an event sender handle like this inside your camera
device handler:
```rust
pub struct CameraHandler {
// (...)
event_sender: std::sync::mpsc::SyncSender<Event>
}
```
+69 -101
View File
@@ -12,10 +12,59 @@ The example project contains components which could also be expected to be part
On-Board Software. A structural diagram of the example application is given to provide
a brief high-level view of the components used inside the example application:
![satrs-example component structure](images/satrs-example/satrs-example-structure.png)
```mermaid
flowchart TD
subgraph TMTC[TMTC Infrastructure]
subgraph TMTCRow1[ ]
direction LR
Udp[UDP Server]
Tcp[TCP Server]
end
subgraph TMTCRow2[ ]
direction LR
Source[TC Source]
Sink[TM Sink]
end
end
The dotted lines are used to denote optional components. In this case, the static pool components
are optional because the heap can be used as a simpler mechanism to store TMTC packets as well.
subgraph AOCS[AOCS Stack]
subgraph AOCSRow1[ ]
direction LR
Mgm0[MGM 0 Handler]
Mgm1[MGM 1 Handler]
Assy[MGM Assembly]
end
subgraph AOCSRow2[ ]
direction LR
AcsCtrl[ACS Controller]
Mgt[MGT Handler]
AcsSub[ACS Subsystem]
end
end
subgraph EPS[EPS Stack]
Pcdu[PCDU Handler]
end
subgraph Core[Core]
direction LR
Ctrl[Core Controller]
Evt[Event Manager]
end
Sim[Sim Client]:::optional
TMTC ~~~ EPS
AOCS ~~~ Core
Core ~~~ Sim
classDef optional stroke-dasharray: 5 5;
classDef invisible fill:none,stroke:none;
class TMTCRow1,TMTCRow2,AOCSRow1,AOCSRow2 invisible;
```
The dotted lines are used to denote optional components. In this case, the simulation client is
optional because a dummy interface can be used instead to run the example without the simulator.
Some additional explanation is provided for the various components.
### TCP/IP server components
@@ -37,116 +86,35 @@ telecommands from the client.
The most important components of the TMTC infrastructure include the following components:
- A TC source component which demultiplexes and routes telecommands based on parameters like
packet APID or PUS service and subservice type.
- A TM sink sink component which is the target of all sent telemetry and sends it to downlink
packet APID and a target ID which is part of the packet payload.
- A TM sink component which is the target of all sent telemetry and sends it to downlink
handlers like the UDP and TCP server.
You can read the [Communications chapter](./communication.md) for more
background information on the chosen TMTC infrastructure approach.
### PUS Service Components
A PUS service stack is provided which exposes some functionality conformant with the ECSS PUS
services. This currently includes the following services:
- Service 1 for telecommand verification. The verification handling is handled locally: Each
component which generates verification telemetry in some shape or form receives a
[reporter](https://docs.rs/satrs/latest/satrs/pus/verification/struct.VerificationReporterWithSender.html)
object which can be used to send PUS 1 verification telemetry to the TM funnel.
- Service 3 for housekeeping telemetry handling.
- Service 5 for management and downlink of on-board events.
- Service 8 for handling on-board actions.
- Service 11 for scheduling telecommands to be released at a specific time. This component
uses the [PUS scheduler class](https://docs.rs/satrs/latest/satrs/pus/scheduler/alloc_mod/struct.PusScheduler.html)
which performs the core logic of scheduling telecommands. All telecommands released by the
scheduler are sent to the central TC source using a message.
- Service 17 for test purposes like pings.
### Event Management Component
An event manager based on the sat-rs
[event manager component](https://docs.rs/satrs/latest/satrs/event_man/index.html)
is provided to handle the event IPC and FDIR mechanism. The event message are converted to PUS 5
telemetry by the
[PUS event dispatcher](https://docs.rs/satrs/latest/satrs/pus/event_man/alloc_mod/struct.PusEventDispatcher.html).
You can read the [events](./events.md) chapter for more in-depth information about event management.
### Sample Application Components
These components are example mission specific. They provide an idea how mission specific modules
would look like the sat-rs context. It currently includes the following components:
- An Attitute and Orbit Control (AOCS) example task which can also process some PUS commands.
## Dataflow
The interaction of the various components is provided in the following diagram:
### TMTC component group
![satrs-example dataflow diagram](images/satrs-example/satrs-example-dataflow.png)
It should be noted that an arrow coming out of a component group refers to multiple components
in that group. An explanation for important component groups will be given.
#### TMTC component group
This groups is the primary interface for clients to communicate with the on-board software
using a standardized TMTC protocol. The example uses the
[ECSS PUS protocol](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
This group is the primary interface for clients to communicate with the on-board software
using the combination of CCSDS space packets and `serde` serialized payloads.
In the future, this might be extended with the
[CCSDS File Delivery Protocol](https://public.ccsds.org/Pubs/727x0b5.pdf).
A client can connect to the UDP or TCP server to send these PUS packets to the on-board software.
These servers then forward the telecommads to a centralized TC source component using a dedicated
message abstraction.
A client can connect to the UDP or TCP server to send telecommands to the on-board software.
These servers forward all telecommands to a centralized TC source component, which demultiplexes
them and routes each one to its target component.
This TC source component then demultiplexes the message and forwards it to the relevant components.
Right now, it forwards all PUS requests to the respective PUS service handlers using the PUS
receiver component. The individual PUS services are running in a separate thread. In the future,
additional forwarding to components like a CFDP handler might be added as well. It should be noted
that PUS11 commands might contain other PUS commands which should be scheduled in the future.
These wrapped commands are forwarded to the PUS11 handler. When the schedule releases those
commands, it forwards the released commands to the TC source again. This allows the scheduler
and the TC source to run in separate threads and keeps them cleanly separated.
All telemetry generated by the on-board software is sent to a centralized TM sink. The core
controller also forwards events to the event manager, which converts them into telemetry and
sends it to the TM sink as well. The TM sink performs a demultiplexing step to forward all
telemetry to the relevant recipients, which in the example case are the last connected UDP
client and any connected TCP client.
All telemetry generated by the on-board software is sent to a centralized TM funnel. This component
also performs a demultiplexing step to forward all telemetry to the relevant TM recipients.
In the example case, this is the last UDP client, or a connected TCP client. In the future,
forwarding to a persistent telemetry store and a simulated communication component might be
added here as well. The centralized TM funnel also takes care of some packet processing steps which
need to be applied for each ECSS PUS packet, for example CCSDS specific APID incrementation and
PUS specific message counter incrementation.
### Application Group
#### Application Group
The application group contains some components you might also find in a real satellite software.
This includes an AOCS stack with various device handlers and system level objects.
The application components generally do not receive raw PUS packets directly, even though
this is certainly possible. Instead, they receive internalized messages from the PUS service
handlers. For example, instead of receiving a PUS 8 Action Telecommand directly, an application
component will receive a special `ActionRequest` message type reduced to the basic important
information required to execute a request. These special requests are denoted by the blue arrow
in the diagram.
It should be noted that the arrow pointing towards the event manager points in both directions.
This is because the application components might be interested in events generated by other
components as well. This mechanism is oftentimes used to implement the FDIR functionality on system
and component level.
#### Shared components and functional interfaces
It should be noted that sometimes, a functional interface is used instead of a message. This
is used for the generation of verification telemetry. The verification reporter is a clonable
component which generates and sends PUS1 verification telemetry directly to the TM funnel. This
introduces a loose coupling to the PUS standard but was considered the easiest solution for
a project which utilizes PUS as the main communication protocol. In the future, a generic
verification abstraction might be introduced to completely decouple the application layer from
PUS.
The same concept is applied if the backing store of TMTC packets are shared pools. Every
component which needs to read telecommands inside that shared pool or generate new telemetry
into that shared pool will received a clonable shared handle to that pool.
The same concept could be extended to power or thermal handling. For example, a shared power helper
component might be used to retrieve power state information and send power switch commands through
a functional interface. The actual implementation of the functional interface might still use
shared memory and/or messages, but the functional interface makes using and testing the interaction
with these components easier.
### Shared components and functional interfaces
+1 -1
View File
@@ -1 +1 @@
# Fault Detecion, Isolation And Recovery (FDIR)
# Fault Detection, Isolation And Recovery (FDIR)
+95 -12
View File
@@ -1,24 +1,107 @@
# Housekeeping Data
Remote systems like satellites and rovers oftentimes generate data autonomously and periodically.
The most common example for this is temperature or attitude data. Data like this is commonly
referred to as housekeeping data, and is usually one of the most important and most resource heavy
data sources received from a satellite. Standards like the PUS Service 3 make recommendation how to
expose housekeeping data, but the applicability of the interface offered by PUS 3 has proven to be
partially difficult and clunky for modular systems.
If you have not read [the TMTC modelling chapter](./tmtc-modelling.md) yet, it is recommended to
do that first.
First, we are going to list some assumption and requirements about Housekeeping (HK) data:
Remote systems like satellites and rovers oftentimes generate data autonomously and periodically.
An example for this could be temperature or attitude data. Data like this is commonly
referred to as housekeeping data, and is usually one of the most important and most resource heavy
data sources received from a satellite.
First, we are going to list some assumptions and requirements about Housekeeping (HK) data:
1. HK data is generated periodically by various system components throughout the
systems.
system.
2. An autonomous and periodic sampling of that HK data to be stored and sent to Ground is generally
required. A minimum interface consists of requesting a one-shot sample of HK, enabling and
disabling the periodic autonomous generation of samples and modifying the collection interval
of the periodic autonomous generation.
3. HK data often needs to be shared to other software components. For example, a thermal controller
3. HK data often needs to be shared with other software components. For example, a thermal controller
wants to read the data samples of all sensor components.
A commonly required way to model HK data in a clean way is also to group related HK data into sets,
which can then dumped via a similar interface.
## Modelling our data
TODO: Write down `sat-rs` recommendations how to expose and work with HK data.
Generally, it makes sense to model the data with Rust data structures for various reasons. For
example, the sensor data received from a 3-axis magnetometer might be modelled like this:
```rust
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct MgmData {
pub x: i16,
pub y: i16,
pub z: i16,
}
```
You can then re-use this data structure for various purposes. Also note the `serde` implementations,
which are useful for generating the housekeeping data sent to ground.
We can model the housekeeping requests for a handler with a single data set like this:
```rust
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum HkRequest {
OneShot,
EnablePeriodic,
DisablePeriodic,
ModifyInterval(core::time::Duration)
}
```
which might then be a part of a top level request type, e.g.
```rust
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Request {
Ping,
Hk(HkRequest)
}
```
A corresponding `Response` type might just include a HK data variant:
```rust
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Response {
Ok,
Hk(MgmData)
}
```
If the software object managed multiple data sets, you could model it like this:
```rust
/// Example set ID.
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum SetId {
Data,
Config
}
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Request {
Ping,
Hk {
set_id: SetId,
request: HkRequest
}
}
```
Sometimes, you need to share the generated data as well. Furthermore, it might make sense to
decouple the HK generation from the data acquisition and only return the latest snapshot
of the data. In this case, you can put the `MgmData` inside an appropriate lock structure for your
platform/runtime to share it safely with other software components. For example, in a `std` system,
you might simply use an `Arc<Mutex<MgmData>>` or an `Arc<RwLock<MgmData>>` for this.
Now, you can update that shared data structure when acquiring new data, and other software objects
or the HK generation routine can safely read from it.
## Helper components
You need some application logic to track whether periodic data generation is enabled, what
the current generation interval is and whether a HK set needs to be generated if the interval
period has elapsed.
`sat-rs` provides some simple helper components for this inside the [`hk`](https://docs.rs/satrs/latest/satrs/hk/index.html) module. The module documentation contains more information.
+1
View File
@@ -0,0 +1 @@
*.bkp
@@ -0,0 +1,99 @@
<mxfile host="Electron">
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
<mxGraphModel dx="808" dy="1600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Et7Is9MkKvAT9IauE9_I-4" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="114" width="443" x="482" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-27" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
<mxGeometry height="117" width="430" y="-3" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-46" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mission Logic&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="113.16" x="13" y="25" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-47" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;sat-rs&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="238" y="25" as="geometry" />
</mxCell>
<mxCell id="eFmRNKLwCo7I2zrsRzJH-1" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;spacepackets&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="113.16" x="13" y="55" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-1" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Events&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="86.24" x="136" y="55" as="geometry" />
</mxCell>
<mxCell id="Et7Is9MkKvAT9IauE9_I-3" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;cfdp&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="231" y="55" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-7" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Device Drivers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="115" x="12.999999999999968" y="85" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-5" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mode Tree&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="105.48" x="135.99999999999997" y="85" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-6" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Controllers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="86.24" x="333" y="55" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-8" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;TMTC&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="133.67" y="25" as="geometry" />
</mxCell>
<mxCell id="wZbC1GPg-bVreQhl9PIS-20" parent="Et7Is9MkKvAT9IauE9_I-4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;FDIR&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="79" x="342" y="25" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
<mxGeometry height="40" width="430" x="482" y="385" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-31" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;endArrow=classic;endFill=1;edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.506;entryY=0.034;entryDx=0;entryDy=0;entryPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="700" y="359" />
<mxPoint x="700" y="370" />
<mxPoint x="699.6" y="370" />
</Array>
<mxPoint x="700" y="370" as="sourcePoint" />
<mxPoint x="700.16" y="420" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-32" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;fontColor=default;exitX=0.502;exitY=0.992;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;strokeColor=#969696;" target="mcy6J03i3kvHfejOpHHr-16">
<mxGeometry relative="1" as="geometry">
<mxPoint x="680" y="104.48999999999998" as="sourcePoint" />
<mxPoint x="680.4399999999998" y="140.00000000000003" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;verticalAlign=middle;" value="" vertex="1">
<mxGeometry height="219" width="432" x="482" y="140" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="mcy6J03i3kvHfejOpHHr-43" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform&#xa;async-centric, embassy or RTICv2" vertex="1">
<mxGeometry height="219" width="432" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Logging&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;defmt + defmt-rtt&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="40" width="133.52063580591187" x="9.000332775608848" y="56.73391304347825" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Serialization&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;serde + postcard&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="128" x="155" y="57.34" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Containers&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;heapless&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="122" x="295" y="57" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Time&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Courier New&quot;&gt;embassy-time&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="131.43389604559735" x="9.004190097896517" y="104.99833992094861" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;IPC / Sync&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;embassy-sync&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="130.29" x="153" y="105" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Memory&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;embedded-alloc&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="123.45" x="296" y="105" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Tasks&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Courier New&quot;&gt;embassy-executor &lt;/font&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Helvetica&quot;&gt;or&amp;nbsp;&lt;/font&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Courier New&quot;&gt;rtic&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="55" width="130.29303773935672" x="8.99793084216397" y="153" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Hardware API&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;BSP + HAL + PAC&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="40" width="130.5" x="152" y="153" as="geometry" />
</mxCell>
<mxCell id="Et7Is9MkKvAT9IauE9_I-2" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Filesystem&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;embedded-sdmmc&lt;/font&gt; or&lt;br&gt;&lt;font face=&quot;Courier New&quot;&gt;embedded-fatfs&lt;/font&gt;&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="55" width="123.45" x="296" y="153" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

@@ -0,0 +1,85 @@
<mxfile host="Electron">
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
<mxGraphModel dx="975" dy="1703" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="mcy6J03i3kvHfejOpHHr-27" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
<mxGeometry height="122" width="426.02" x="490.98" y="-10" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
<mxGeometry height="40" width="420" x="490.5" y="290" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-32" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;exitX=0.496;exitY=0.984;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;" target="mcy6J03i3kvHfejOpHHr-16">
<mxGeometry relative="1" as="geometry">
<mxPoint x="680" y="104.48999999999998" as="sourcePoint" />
<mxPoint x="680.4399999999998" y="140.00000000000003" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;" value="" vertex="1">
<mxGeometry height="124" width="423" x="490.5" y="140" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="mcy6J03i3kvHfejOpHHr-43" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform" vertex="1">
<mxGeometry height="124" width="423" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Logging&lt;br&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="15" y="30" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Serialization&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="146.5" y="30" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Containers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="278.5" y="30" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;IPC / Sync&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="14.5" y="60" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Memory&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="278" y="60" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Tasks&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="146.5" y="60" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Hardware API&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="15" y="90" as="geometry" />
</mxCell>
<mxCell id="QQywTSC8lEcqjhdOS4oo-1" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Filesystem&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="146.5" y="90" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="mcy6J03i3kvHfejOpHHr-43" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Time&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="117.5" x="278.5" y="90" as="geometry" />
</mxCell>
<mxCell id="_ne4E4OgUf3ctT-MU7S6-3" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.506;entryY=0.031;entryDx=0;entryDy=0;fontColor=#000000;strokeColor=#969696;strokeWidth=2;exitX=0.503;exitY=1.009;exitDx=0;exitDy=0;entryPerimeter=0;exitPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="702.54" y="281.12" as="sourcePoint" />
<mxPoint x="701.46" y="303" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_ne4E4OgUf3ctT-MU7S6-4" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Command Scheduling&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="171" x="499" y="50" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-46" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mission Logic&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="105.48496240601504" x="498.9978345864661" y="20" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-47" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Events&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="94.67142857142858" x="719.0028571428571" y="20" as="geometry" />
</mxCell>
<mxCell id="nZ8_NfAIOoxuCbflcZYr-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;FDIR&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="80.57142857142857" x="824.0028571428571" y="20" as="geometry" />
</mxCell>
<mxCell id="nZ8_NfAIOoxuCbflcZYr-4" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;TMTC&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="94.67142857142858" x="614.0028571428571" y="20" as="geometry" />
</mxCell>
<mxCell id="932u3wCXrdYco77gi_Ax-1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mode Tree&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="105.48" x="680" y="50" as="geometry" />
</mxCell>
<mxCell id="932u3wCXrdYco77gi_Ax-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Controllers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="104.57" x="800" y="50" as="geometry" />
</mxCell>
<mxCell id="932u3wCXrdYco77gi_Ax-5" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Device Drivers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="115" x="499" y="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

@@ -0,0 +1,100 @@
<mxfile host="Electron">
<diagram name="Page-1" id="L7hDkg9csFOap1jIF0ey">
<mxGraphModel dx="571" dy="1434" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="mcy6J03i3kvHfejOpHHr-43" connectable="0" parent="1" style="group;fillColor=#FFF7ED;rounded=0;labelBackgroundColor=none;fontColor=#6B2F00;strokeColor=#D55E00;strokeWidth=3;perimeterSpacing=1;container=0;" value="" vertex="1">
<mxGeometry height="190" width="428" x="482" y="140" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-28" parent="1" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#E2E8F0;strokeColor=#475569;fontColor=#1E293B;labelBackgroundColor=none;fontStyle=1;fontSize=14;perimeterSpacing=1;" value="Hardware" vertex="1">
<mxGeometry height="40" width="430" x="482" y="352" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-31" edge="1" parent="1" source="mcy6J03i3kvHfejOpHHr-16" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;labelBackgroundColor=none;strokeColor=#969696;fontColor=default;entryX=0.52;entryY=-0.019;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=classic;endFill=1;edgeStyle=orthogonalEdgeStyle;exitX=0.518;exitY=1.002;exitDx=0;exitDy=0;exitPerimeter=0;" target="mcy6J03i3kvHfejOpHHr-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="703.6" y="330.4" />
</Array>
<mxPoint x="700" y="370" as="sourcePoint" />
<mxPoint x="680.36" y="340.24" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-27" connectable="0" parent="1" style="group" value=" " vertex="1">
<mxGeometry height="139" width="443" x="481" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-14" connectable="0" parent="T22fa2qrIJ5I1ijH7CBO-27" style="group" value="" vertex="1">
<mxGeometry height="114" width="443" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-15" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=0;arcSize=20;strokeWidth=3;fillColor=#BFDCEC;strokeColor=#0072B2;fontSize=14;fontColor=#003A5D;labelBackgroundColor=none;fontStyle=1;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=top;spacingTop=0;perimeterSpacing=1;" value="Application" vertex="1">
<mxGeometry height="117" width="430" y="-3" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-16" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mission Logic&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="113.16" x="13" y="25" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-17" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;sat-rs&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="238" y="25" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-18" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;spacepackets&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="113.16" x="14" y="55" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-19" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Events&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="86.24" x="136" y="55" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-20" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;cfdp&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="231" y="55" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-21" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Device Drivers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="114.16" x="13" y="85" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-22" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Mode Tree&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="105.48" x="135.99999999999997" y="85" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-23" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Controllers&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="86.24" x="333" y="55" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-24" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;TMTC&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="93" x="133.67" y="25" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-25" parent="T22fa2qrIJ5I1ijH7CBO-14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#BFDCEC;fontColor=#003A5D;strokeColor=#0072B2;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;spacingTop=0;arcSize=20;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot;&gt;FDIR&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="22" width="79" x="342" y="25" as="geometry" />
</mxCell>
<mxCell id="T22fa2qrIJ5I1ijH7CBO-26" edge="1" parent="T22fa2qrIJ5I1ijH7CBO-27" source="T22fa2qrIJ5I1ijH7CBO-15" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=-0.006;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;labelBackgroundColor=none;fontColor=default;exitX=0.502;exitY=0.992;exitDx=0;exitDy=0;exitPerimeter=0;edgeStyle=orthogonalEdgeStyle;curved=0;strokeColor=#969696;">
<mxGeometry relative="1" as="geometry">
<mxPoint x="198" y="104.49000000000001" as="sourcePoint" />
<mxPoint x="216" y="139" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-16" parent="1" style="whiteSpace=wrap;strokeWidth=2;verticalAlign=top;fillColor=#FED7AA;strokeColor=#EA580C;fontColor=#6B2F00;fontSize=14;rounded=0;labelBackgroundColor=none;fontStyle=1" value="System / platform&#xa;Embedded Linux" vertex="1">
<mxGeometry height="190" width="428" x="482" y="140" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-35" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Logging&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;log + fern&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="40" width="125" x="495" y="189.49" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-36" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Serialization&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;serde + postcard&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="134.66" x="634" y="189.18367088607596" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-37" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Containers&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;heapless + std&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="115" x="782.79" y="189.18" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-38" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Time&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Courier New&quot;&gt;std&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="125" x="494" y="236" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-39" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;IPC / Sync&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font style=&quot;font-size: 12px;&quot; face=&quot;Courier New&quot;&gt;std&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="40" width="135" x="633" y="236" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-40" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Memory&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;alloc&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="113.79" x="782" y="235.99628691983122" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-41" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Tasks&lt;br&gt;&lt;font face=&quot;Courier New&quot;&gt;&lt;span style=&quot;font-size: 12px;&quot;&gt;std&lt;/span&gt;&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="125" x="495" y="283" as="geometry" />
</mxCell>
<mxCell id="mcy6J03i3kvHfejOpHHr-42" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Hardware API&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;Linux + &lt;font face=&quot;Courier New&quot;&gt;uio&lt;/font&gt;&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="40" width="137.7" x="633" y="282.9989029535865" as="geometry" />
</mxCell>
<mxCell id="6sRxE-k6t311zxd993k4-5" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FED7AA;fontColor=#6B2F00;strokeColor=#EA580C;labelBackgroundColor=none;strokeWidth=2;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=14;fontStyle=1;aspect=fixed;" value="&lt;span style=&quot;font-weight: normal;&quot;&gt;Filesystem&lt;br&gt;&lt;font face=&quot;Courier New&quot; style=&quot;font-size: 12px;&quot;&gt;std&lt;/font&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="113.79" x="782" y="283" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+10 -11
View File
@@ -4,8 +4,8 @@ The sat-rs book
This book is the primary information resource for the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
in addition to the regular API documentation. It contains the following resources:
1. Architecture informations and consideration which would exceeds the scope of the regular API.
2. General information on how to build on-board Software and how `sat-rs` can help to fulfill
1. Architecture information and considerations which would exceed the scope of the regular API.
2. General information on how to build on-board software and how `sat-rs` can help to fulfill
the unique requirements of writing software for remote systems.
# Introduction
@@ -14,23 +14,22 @@ The primary goal of the sat-rs library is to provide re-usable components
to write on-board software for remote systems like rovers or satellites. It is specifically written
for the special requirements for these systems.
It should be noted that sat-rs is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
changelog files. You should only use this library if your are willing to work in this
environment.
A lot of the architecture and general design considerations are based on the
Some architecture and general design considerations are based on the
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
However, `sat-rs` has a significantly reduced scope compared to those frameworks. Rust provides
a great ecosystem and a powerful standard library which reduces the need for large and complex
frameworks.
# Getting started with the example
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
the practical application of `sat-rs` inside an application, it is recommended to have a look at
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
applicatin complements the example application and can be used to simulate some physical devices
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/minisim)
application complements the example application and can be used to simulate some physical devices
for the `satrs-example` device handlers.
# Flight Heritage
@@ -45,5 +44,5 @@ Currently this library has the following flight heritage:
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
alongside a Flight System Simulator in the context of a
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
[Bachelor's thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
+6 -10
View File
@@ -5,9 +5,9 @@ system reasoning for both system operators and OBSW developers. They also provid
the behaviour of a component and also provide observability of a system. A few examples of how to
model the mode of different components within a space system with modes will be given.
## Pyhsical device component with modes
## Physical device component with modes
The following simple mode scheme with the following three mode
The following simple mode scheme with the following three modes
- `OFF`
- `ON`
@@ -18,10 +18,10 @@ sensors.
1. `OFF` means that a device is physically switched off, and the corresponding software component
does not poll the device regularly.
2. `ON` means that a device is pyhsically switched on, but the device is not polled perically.
2. `ON` means that a device is physically switched on, but the device is not polled periodically.
3. `NORMAL` means that a device is powered on and polled periodically.
If a devices is `OFF`, the device handler will deny commands which include physical communication
If a device is `OFF`, the device handler will deny commands which include physical communication
with the connected devices. In `NORMAL` mode, it will autonomously perform periodic polling
of a connected physical device in addition to handling remote commands by the operator.
Using these three basic modes, there are two important transitions which need to be taken care of
@@ -73,8 +73,6 @@ In summary, a component which has modes has to expose the following 4 capabiliti
3. Announce the mode
4. Announce the mode recursively
## Using ECSS PUS to perform mode commanding
# Health
Health is an important concept for systems and components which might fail.
@@ -94,10 +92,8 @@ use-cases:
2. `FAULTY` means that a component does not work properly. This might also impact other system
components, so the passivation and isolation of that component is desirable for FDIR purposes.
3. `NEEDS RECOVERY` is used to attempt a recovery of a component. For example, a simple sensor
could be power-cycled if there were multiple communication issues in the last time.
could be power-cycled if there were multiple communication issues recently.
4. `EXTERNAL CONTROL` is used to isolate an individual component from the rest of the system. For
example, on operator might be interested in testing a component in isolation, and the interference
example, an operator might be interested in testing a component in isolation, and the interference
of the system is not desired. In that case, the `EXTERNAL CONTROL` health state might be used
to prevent mode commands from the system while allowing external mode commands.
-1
View File
@@ -1 +0,0 @@
# Serialization
+55
View File
@@ -0,0 +1,55 @@
# System View
This chapter gives a system level view of how a typical flight software built with `sat-rs`,
[`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets) and
[`cfdp`](https://egit.irs.uni-stuttgart.de/rust/cfdp) is layered. It complements the previous
chapters, which focus on individual components, by showing how those components fit together
and where the line between application and platform is usually drawn.
## Generic layering
Flight software built with `sat-rs` is generally structured into three layers.
![Generic architecture](./images/satrs-arch-generic.drawio.png)
- **Application**: The mission specific logic. This is the code a developer writes for a
particular mission.
- **System / platform**: The set of services the application is built on. This covers
concepts like logging, serialization, IPC, task and memory management, hardware
access, filesystem access and time. Most of these components are provided by external libraries
and APIs.
- **Hardware**: The physical target the software runs on.
The application layer stays largely the same across missions and targets. The system / platform
layer is where the target environment determines which concrete crates and mechanisms are used.
The book has dedicated chapters for some of the topics:
- [TMTC handling and Serialization](./tmtc-modelling.md)
- [Events](./events.md)
- [Modes](./modes-and-health.md)
## Embedded Linux
On an embedded Linux target, the platform layer is provided by the Rust standard library and a
small set of additional crates.
![Linux architecture](./images/satrs-arch-linux.drawio.png)
The application layer uses `sat-rs` together with `spacepackets` for CCSDS packet handling
and `cfdp` for file transfer. The platform layer relies on `std` for tasks, IPC, memory, time and
filesystem access, `serde` and `postcard` for serialization and `log`/`fern` for logging. Hardware
access typically goes through Linux mechanisms like `uio`.
## Embedded async targets (Embassy / RTIC)
On smaller microcontrollers without an operating system, the platform layer looks quite
different, even though the application layer stays the same.
![Embassy/RTIC architecture](./images/satrs-arch-embassy.drawio.png)
Here the platform layer is built around an async-centric executor, either
[Embassy](https://embassy.dev/) or [RTICv2](https://rtic.rs/). `no_std` crates like
`heapless` and `embedded-alloc` replace `std` collections and allocation, `defmt` replaces `log`
for logging and hardware access goes through a board support package (BSP), a hardware
abstraction layer (HAL) and a peripheral access crate (PAC) instead of the OS.
+86
View File
@@ -0,0 +1,86 @@
# TMTC modelling using Rust
Before we talk about how to model telecommand and telemetry data using Rust, we are going
to present some basic concepts and useful libraries first.
## Serialization
Serialization and deserialization is the process of converting (Rust) data structures into
some format which can be stored or transmitted. We can use this system for generating the payload
of our telecommand and telemetry packets. This allows us to model our payloads with Rust data
structures, fits perfectly into the data-driven approach that Rust programs tend to favor and
allows us to use the excellent type system.
The Rust ecosystem provides the [`serde`](https://serde.rs/) library for this task. The library
makes it trivial to add serialization support to custom data structures by providing a
[`derive`](https://serde.rs/derive.html) macro. In almost all cases, you can just add this derive
macro to a data structure to make it serializable with any `serde` compatible serializer.
There are various serializers available which are well suited to the requirements of space systems.
- Generally, we try to minimize the payload size to save data bandwidth.
- The data does not necessarily have to be human-readable
We recommend the [`postcard`](https://github.com/jamesmunns/postcard) serializer, which fulfills
these requirements and also works well for embedded systems.
## Modelling telecommands and telemetry
Using a serializer library like `serde` allows us to do some interesting things. For example,
let's assume you have a `Camera` object in software that you want to send some commands to.
This object should have the following capabilities:
- Process a ping command
- Capture an image
- Send back configuration data
You can now model a request to your `Camera` object using the following data structure
```rust
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum CameraRequest {
Ping,
CaptureImage,
RequestConfig,
}
```
This data structure models all the requests that the `Camera` provides.
On the telemetry side, you would have a similar object
```rust
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum CameraResponse {
Ok,
Config(ConfigStructure)
}
```
where `ConfigStructure` would be some other wrapped configuration structure, and the `Ok` response
would be the reply for successful execution for all other commands which do not have additional
telemetry information.
Rust makes it trivial to move components into a new shared library. You can now put these data
structures in a shared `types` or `data` library which can be re-used by both a ground system
library and by the on-board software.
On the ground system, you could use a function like [`postcard::to_allocvec`](https://docs.rs/postcard/latest/postcard/fn.to_allocvec.html)
to generate the byte representation of a `CameraRequest`, which is then sent as the payload
inside a CCSDS space packet. On the on-board software side, you can use
[`postcard::from_bytes`](https://docs.rs/postcard/latest/postcard/fn.from_bytes.html) to deserialize
the `CameraRequest` from the raw payload bytes. In both cases, you do not need to hand-write
the serialization and deserialization code anymore. The only trade-off is that you need a Rust
conversion layer if you want to create your telecommands in another language like Python.
Using Rust structures like this also has other advantages. Once you have the `CameraRequest`
structure, you can `match` on it to cover **all** commands that the device handler needs to cover.
If you add a new variant, you have to handle it as well and you can not forget to handle a
variant.
One trade-off to keep in mind is that a Rust `enum` will always have the size of its largest variant
in memory. If you need to send large payloads to and from the on-board software, you can also
add this data as a secondary data blob behind the primary `serde` payload, and still send something
like small metadata as part of the payload. `postcard` can tell you the size of the deserialized
payload which helps with determining the size of any additional payload data.
We recommend this approach for all TMTC definitions where you control all sides of the communication.
+11 -16
View File
@@ -1,8 +1,7 @@
[package]
name = "satrs-example"
version = "0.1.1"
edition = "2021"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
edition = "2024"
default-run = "satrs-example"
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
@@ -18,28 +17,24 @@ csv = "1"
num_enum = "0.7"
thiserror = "2"
lazy_static = "1"
strum = { version = "0.27", features = ["derive"] }
strum = { version = "0.28", features = ["derive"] }
derive-new = "0.7"
cfg-if = "1"
arbitrary-int = "2"
bitbybit = "1.4"
bitbybit = "2"
postcard = "1"
ctrlc = "3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dependencies.satrs]
path = "../satrs"
features = ["test_util"]
[dependencies.satrs-minisim]
path = "../satrs-minisim"
[dependencies.satrs-mib]
version = "0.1.1"
path = "../satrs-mib"
satrs = { path = "../satrs", features = ["test_util"] }
types = { path = "./types" }
satrs-minisim = { path = "./minisim" }
satrs-mib = { path = "../satrs-mib" }
[features]
default = ["heap_tmtc"]
heap_tmtc = []
# default = ["heap_tmtc"]
# heap_tmtc = []
[dev-dependencies]
env_logger = "0.11"
+8 -45
View File
@@ -5,7 +5,7 @@ This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
@@ -26,58 +26,21 @@ cargo run --no-default-features
# Interacting with the sat-rs example
## Simple Client
The `simpleclient` binary target sends a
ping telecommand and then verifies the telemetry generated by the example application.
It can be run like this:
```rs
cargo run --bin simpleclient
```
This repository also contains a more complex client using the
[Python tmtccmd](https://github.com/robamu-org/tmtccmd) module.
## <a id="tmtccmd"></a> Using the tmtccmd Python client
The python client requires a valid installation of the
[tmtccmd package](https://github.com/robamu-org/tmtccmd).
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency interactively:
The `client` crate is a command line client which sends telecommands to the example application
and prints the received telemetry. For example, you can ping the application or switch MGM 0
to normal mode like this:
```sh
pip install -e .
cargo run -p client -- --ping
cargo run -p client -- mgm0 -m normal
```
Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also
install it manually with
```sh
pip install -e .
pip install tmtccmd[gui]
```
After setting up the dependencies, you can simply run the `main.py` script to send commands
to the OBSW example and to view and handle incoming telemetry. The script and the `tmtccmd`
framework it uses allow to easily add and expose additional telecommand and telemetry handling
as Python code. For example, you can use the following command to send a ping like done with
the `simpleclient`:
```sh
./main.py -p /test/ping
```
You can also simply call the script without any arguments to view the command tree.
Use `cargo run -p client -- --help` to list all available commands.
## Adding the mini simulator application
This example application features a few device handlers. The
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/minisim)
can be used to simulate the physical devices managed by these device handlers.
The example application will attempt communication with the mini simulator on UDP port 7303.
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "client"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4", features = ["derive"] }
log = "0.4"
fern = "0.7"
humantime = "2"
serde = { version = "1" }
serde_json = "1"
satrs = { path = "../../satrs" }
satrs-example = { path = ".." }
satrs-minisim = { path = "../minisim" }
types = { path = "../types" }
spacepackets = { version = "0.18", default-features = false }
bitbybit = "2"
arbitrary-int = "2"
ctrlc = { version = "3.5" }
postcard = { version = "1" }
anyhow = "1"
+789
View File
@@ -0,0 +1,789 @@
use anyhow::{Context as _, bail};
use arbitrary_int::u11;
use clap::Parser as _;
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use satrs_minisim::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
udp::SIM_CTRL_PORT,
};
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, SystemTime},
};
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
#[derive(clap::Parser)]
pub struct Cli {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
test_event: bool,
#[command(subcommand)]
commands: Option<Commands>,
}
#[derive(clap::Subcommand)]
enum Commands {
Mgm0(MgmArgs),
Mgm1(MgmArgs),
MgmAssy(MgmAssemblyArgs),
Mgt(MgtArgs),
AcsSubsystem(SubsystemArgs),
EventManager(EventManagerArgs),
}
#[derive(clap::Parser)]
struct EventManagerArgs {
#[command(subcommand)]
action: EventFilterAction,
}
#[derive(clap::Subcommand)]
enum EventFilterAction {
/// Enable event TM generation.
Enable(EventFilterArgs),
/// Disable event TM generation.
Disable(EventFilterArgs),
}
#[derive(clap::Args)]
struct EventFilterArgs {
#[arg(value_enum)]
component: EventSenderSelect,
/// Raw event ID. Without it, the filter applies to all events of the component.
#[arg(short, long)]
event_id: Option<u16>,
}
/// Components which emit events.
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum EventSenderSelect {
Controller,
Mgm0,
Mgm1,
MgmAssy,
Mgt,
Pcdu,
UdpServer,
TcpServer,
Ground,
}
impl From<EventSenderSelect> for types::ComponentId {
fn from(sender: EventSenderSelect) -> Self {
match sender {
EventSenderSelect::Controller => types::ComponentId::Controller,
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
EventSenderSelect::Ground => types::ComponentId::Ground,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultMode {
None,
/// SPI communication is all zeroes, modelling an unconnected sensor.
AllZeros,
/// SPI communication is all ones, modelling a broken sensor.
AllOnes,
}
impl From<FaultMode> for mgm::SpiFaultMode {
fn from(mode: FaultMode) -> Self {
match mode {
FaultMode::None => mgm::SpiFaultMode::None,
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultKind {
/// Cleared when the device is switched off, so a power cycle recovers from it.
Transient,
/// Survives power cycles.
#[default]
Permanent,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HkSelect {
OneShot,
EnablePeriodic,
DisablePeriodic,
ModifyInterval,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HealthStateSelect {
Healthy,
Faulty,
PermanentFaulty,
ExternalControl,
NeedsRecovery,
}
impl From<HealthStateSelect> for satrs::health::HealthState {
fn from(state: HealthStateSelect) -> Self {
match state {
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the sensor data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
#[arg(long, value_enum)]
fault: Option<FaultMode>,
/// Whether a power cycle clears the injected SPI fault.
#[arg(long, value_enum, default_value_t)]
fault_kind: FaultKind,
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
/// the handler after the underlying issue has been fixed or worked around.
#[arg(long, value_enum)]
health: Option<HealthStateSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgtArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the status data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
torque: Option<types::acs::mgt::Dipole>,
#[arg(long, default_value_t = 1000)]
torque_duration_ms: u64,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmAssemblyArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<AssemblyModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct SubsystemArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<SubsystemModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum DeviceModeSelect {
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum AssemblyModeSelect {
NoModeKeeping,
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum SubsystemModeSelect {
Off,
Safe,
}
fn hk_request_type(
hk: HkSelect,
hk_interval_ms: Option<u64>,
) -> anyhow::Result<types::HkRequestType> {
let opt_interval = hk_interval_ms.map(Duration::from_millis);
Ok(match hk {
HkSelect::OneShot => types::HkRequestType::OneShot,
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
),
})
}
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
let axes: Vec<i16> = value
.split(',')
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
.collect::<Result<_, _>>()?;
let [x, y, z] = axes[..] else {
return Err(format!("expected 3 values, got {}", axes.len()));
};
Ok(types::acs::mgt::Dipole { x, y, z })
}
fn send_mgt_request(
client: &UdpSocket,
addr: SocketAddr,
request: types::acs::mgt::request::Request,
) {
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending MGT request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
use types::acs::mgt::request::{ModeRequest, Request};
if args.ping {
send_mgt_request(client, addr, Request::Ping);
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
send_mgt_request(client, addr, Request::Hk(req_type));
}
if let Some(mode) = args.mode {
let mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
}
if let Some(dipole) = args.torque {
let request = Request::ApplyTorque {
dipole,
duration: Duration::from_millis(args.torque_duration_ms),
};
send_mgt_request(client, addr, request);
}
Ok(())
}
fn handle_mgm_command(
client: &UdpSocket,
addr: SocketAddr,
target_id: types::ComponentId,
args: MgmArgs,
) -> anyhow::Result<()> {
if let Some(mode) = args.fault {
inject_mgm_failure(
target_id,
mgm::SpiFault {
mode: mode.into(),
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
},
)?;
}
if args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Hk),
types::acs::mgm::request::Request::Hk(HkRequest {
id: types::acs::mgm::request::HkId::Sensor,
req_type,
}),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = args.mode {
let dev_mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm::request::Request::Mode(
types::acs::mgm::request::ModeRequest::SetMode(dev_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(health) = args.health {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Health),
types::acs::mgm::request::Request::Health(
types::acs::mgm::request::HealthRequest::SetHealth(health.into()),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} set-health request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
Ok(())
}
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
use types::event_manager::request::Request;
let request = match args.action {
EventFilterAction::Enable(filter) => match filter.event_id {
Some(event_id) => Request::EnableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::EnableComponent(filter.component.into()),
},
EventFilterAction::Disable(filter) => match filter.event_id {
Some(event_id) => Request::DisableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::DisableComponent(filter.component.into()),
},
};
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
log::info!(
"sending event manager request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&request_packet.to_vec(), addr).unwrap();
}
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(level)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
fn main() -> anyhow::Result<()> {
setup_logger(log::LevelFilter::Debug).unwrap();
let kill_signal = Arc::new(AtomicBool::new(false));
let ctrl_kill_signal = kill_signal.clone();
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
let cli = Cli::parse();
let addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
let client = UdpSocket::bind("127.0.0.1:7302").expect("Connecting to UDP server failed");
client.set_nonblocking(true)?;
client.set_read_timeout(Some(Duration::from_millis(200)))?;
if cli.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
types::control::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if cli.test_event {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
types::control::request::Request::TestEvent,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending event request with TC ID {:#010x}",
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(cmd) = cli.commands {
match cmd {
Commands::Mgm0(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
}
Commands::Mgm1(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
}
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
Commands::MgmAssy(mgm_assembly_args) => {
let target_id = types::ComponentId::AcsMgmAssembly;
if mgm_assembly_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = mgm_assembly_args.mode {
let assembly_mode = match mode {
AssemblyModeSelect::NoModeKeeping => {
types::acs::mgm_assembly::Mode::NoModeKeeping
}
AssemblyModeSelect::Off => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
}
AssemblyModeSelect::Normal => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
}
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm_assembly::request::Request::Mode(
types::acs::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::AcsSubsystem(subsystem_args) => {
let target_id = types::ComponentId::AcsSubsystem;
if subsystem_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::subsystem::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = subsystem_args.mode {
let subsystem_mode = match mode {
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::subsystem::request::Request::Mode(
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} mode request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
}
}
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
log::info!("entering listening loop");
loop {
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
log::info!("received kill signal, exiting");
break;
}
match client.recv(recv_buf.as_mut_slice()) {
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
continue;
}
log::warn!("UDP reception error: {}", e)
}
}
}
Ok(())
}
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
///
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
/// UDP send would otherwise silently do nothing if minisim is not running.
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
let mut reply_buf = [0u8; 4096];
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
sim_socket.send_to(&serde_json::to_vec(&ping)?, sim_addr)?;
match sim_socket.recv(&mut reply_buf) {
Ok(len) => {
let reply: SimReply = serde_json::from_slice(&reply_buf[..len])?;
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
}
}
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
}
Err(e) => return Err(e.into()),
}
let id = match target_id {
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
};
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
id,
request: mgm::Request::SetSpiFault(fault),
});
sim_socket.send_to(&serde_json::to_vec(&request)?, sim_addr)?;
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
Ok(())
}
/// Each component has its own event type, so the sender ID determines how to decode the event.
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
sender_id: types::ComponentId,
data: &[u8],
) {
match postcard::from_bytes::<E>(data) {
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
}
}
match sender_id {
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
log_event::<types::acs::mgm::Event>(sender_id, data)
}
types::ComponentId::AcsMgmAssembly => {
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
}
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
// TC source events are sent with the ID of the packet source.
types::ComponentId::UdpServer
| types::ComponentId::TcpServer
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
_ => log::warn!(
"Received event from {:?} with unknown event type",
sender_id
),
}
}
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
Ok(packet) => {
let tm_header_result = postcard::take_from_bytes::<types::TmHeader>(packet.user_data());
if let Err(e) = tm_header_result {
bail!("Failed to deserialize TM header: {}", e);
}
let (tm_header, remainder) = tm_header_result.unwrap();
if let Some(tc_id) = tm_header.tc_id {
log::info!(
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
packet.apid(),
tm_header.sender_id,
tc_id.raw()
);
}
if tm_header.message_type == MessageType::Event {
handle_event(tm_header.sender_id, remainder);
return Ok(());
}
match tm_header.sender_id {
types::ComponentId::EpsPcdu => {
let response =
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
log::info!("Received response from PCDU: {:?}", response.unwrap());
}
types::ComponentId::Controller => {
let response =
postcard::from_bytes::<types::control::response::Response>(remainder);
log::info!("Received response from controller: {:?}", response.unwrap());
}
types::ComponentId::AcsMgmAssembly => {
let response = postcard::from_bytes::<
types::acs::mgm_assembly::response::Response,
>(remainder);
log::info!(
"Received response from MGM Assembly: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsMgm0 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM0: {:?}", response.unwrap());
}
types::ComponentId::AcsMgm1 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM1: {:?}", response.unwrap());
}
types::ComponentId::AcsSubsystem => {
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
remainder,
);
log::info!(
"Received response from ACS subsystem: {:?}",
response.unwrap()
);
}
types::ComponentId::EpsSubsystem => todo!(),
types::ComponentId::UdpServer => todo!(),
types::ComponentId::TcpServer => todo!(),
types::ComponentId::Ground => todo!(),
types::ComponentId::EventManager => {
let response =
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
log::info!(
"Received response from event manager: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsController => todo!(),
types::ComponentId::AcsMgt => {
let response =
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
log::info!("Received response from MGT: {:?}", response.unwrap());
}
}
}
Err(_) => todo!(),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dipole() {
assert_eq!(
parse_dipole("-200, 200,1000"),
Ok(types::acs::mgt::Dipole {
x: -200,
y: 200,
z: 1000
})
);
assert!(parse_dipole("1,2").is_err());
assert!(parse_dipole("1,2,3,4").is_err());
}
#[test]
fn test_negative_torque_argument() {
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
let Some(Commands::Mgt(args)) = cli.commands else {
panic!("expected mgt subcommand");
};
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
}
}
@@ -11,16 +11,14 @@ serde_json = "1"
log = "0.4"
thiserror = "2"
fern = "0.7"
strum = { version = "0.27", features = ["derive"] }
strum = { version = "0.28", features = ["derive"] }
num_enum = "0.7"
humantime = "2"
tai-time = { version = "0.3", features = ["serde"] }
nexosim = "1"
[dependencies.nexosim]
version = "0.3.1"
[dependencies.satrs]
path = "../satrs"
satrs = { path = "../../satrs" }
types = { path = "../types" }
[dev-dependencies]
delegate = "0.13"
@@ -2,7 +2,7 @@ sat-rs minisim
======
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
[asynchronix](https://github.com/asynchronics/asynchronix).
[nexosim](https://github.com/asynchronics/nexosim).
Right now, this crate is primarily used together with the
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
@@ -29,4 +29,4 @@ still have similar device handlers.
The following graph shows the high-level architecture of the mini-simulator.
<img src="../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
+227
View File
@@ -0,0 +1,227 @@
use std::f32::consts::PI;
use nexosim::{
model::{Context, Model},
ports::Output,
};
use satrs_minisim::{acs::mgm, SimReply};
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::time::current_millis;
// Earth magnetic field varies between roughly -30 uT and 30 uT
const AMPLITUDE_MGM_UT: f32 = 30.0;
// Lets start with a simple frequency here.
const FREQUENCY_MGM: f32 = 1.0;
const PHASE_X: f32 = 0.0;
// Different phases to have different values on the other axes.
const PHASE_Y: f32 = 0.1;
const PHASE_Z: f32 = 0.2;
/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves.
///
/// An ideal sensor would sample the magnetic field at a high fixed rate. This might not be
/// possible for a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms)
/// might still be possible and is probably sufficient for many OBSW needs.
#[derive(Serialize, Deserialize)]
pub struct MgmModel {
id: mgm::Id,
switch_state: SwitchStateBinary,
external_mag_field: Option<mgm::SensorValuesMicroTesla>,
spi_fault: mgm::SpiFault,
pub reply: Output<SimReply>,
}
#[Model]
impl MgmModel {
pub fn new(mgm_id: mgm::Id) -> Self {
Self {
id: mgm_id,
switch_state: SwitchStateBinary::Off,
external_mag_field: None,
spi_fault: mgm::SpiFault::default(),
reply: Output::new(),
}
}
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state;
if switch_state == SwitchStateBinary::Off && self.spi_fault.cleared_by_power_cycle {
self.spi_fault = mgm::SpiFault::default();
}
}
/// Force (or clear) a stuck-bus SPI fault, for FDIR testing purposes.
pub async fn set_spi_fault(&mut self, fault: mgm::SpiFault) {
self.spi_fault = fault;
}
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
let reply = SimReply::Mgm {
id: self.id,
reply: mgm::Reply::new(
self.switch_state,
self.calculate_current_mgm_tuple(current_millis(cx.time())),
self.spi_fault.mode,
),
};
self.reply.send(reply).await;
}
// Devices like magnetorquers generate a strong magnetic field which overrides the default
// model for the measured magnetic field.
pub async fn apply_external_magnetic_field(&mut self, field: mgm::SensorValuesMicroTesla) {
self.external_mag_field = Some(field);
}
pub async fn clear_external_magnetic_field(&mut self, _: ()) {
self.external_mag_field = None;
}
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> mgm::SensorValuesMicroTesla {
if SwitchStateBinary::On == self.switch_state {
if let Some(ext_field) = self.external_mag_field {
return ext_field;
}
let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0);
return mgm::SensorValuesMicroTesla {
x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
};
}
mgm::SensorValuesMicroTesla {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use satrs_minisim::{acs::mgm, SimReply, SimRequest};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
eps::tests::{switch_device_off, switch_device_on},
test_helpers::SimTestbench,
};
fn request_sensor_data(sim_testbench: &mut SimTestbench, id: mgm::Id) -> mgm::Reply {
let sim_reply = sim_testbench
.request_reply(SimRequest::Mgm {
id,
request: mgm::Request::RequestSensorData,
})
.expect("no MGM reply received");
let SimReply::Mgm {
id: reply_id,
reply,
} = sim_reply
else {
panic!("unexpected reply {sim_reply:?}");
};
assert_eq!(reply_id, id);
reply
}
fn inject_spi_fault(sim_testbench: &mut SimTestbench, cleared_by_power_cycle: bool) {
sim_testbench.send_and_step(SimRequest::Mgm {
id: mgm::Id::Mgm0,
request: mgm::Request::SetSpiFault(mgm::SpiFault {
mode: mgm::SpiFaultMode::AllOnes,
cleared_by_power_cycle,
}),
});
}
fn is_stuck_bus_reply(reply: &mgm::Reply) -> bool {
reply.raw.x == -1 && reply.raw.y == -1 && reply.raw.z == -1
}
#[test]
fn test_basic_mgm_request() {
let mut sim_testbench = SimTestbench::new();
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(reply.switch_state, SwitchStateBinary::Off);
assert_eq!(reply.sensor_values.x, 0.0);
assert_eq!(reply.sensor_values.y, 0.0);
assert_eq!(reply.sensor_values.z, 0.0);
}
#[test]
fn test_mgm_spi_fault_injection_all_ones() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, false);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
// Even though the device is switched on, the injected fault forces a stuck-bus reply.
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
}
#[test]
fn test_mgm_spi_fault_cleared_by_power_cycle() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, true);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(is_stuck_bus_reply(&reply));
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(!is_stuck_bus_reply(&reply));
}
#[test]
fn test_mgm_spi_fault_persists_after_power_cycle() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, false);
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
}
#[test]
fn test_basic_mgm_request_switched_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let first_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
let second_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
let to_microtesla = |raw: i16| {
raw as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS * mgm::GAUSS_TO_MICROTESLA_FACTOR as f32
};
let values = second_reply.sensor_values;
let raw = second_reply.raw;
for (value, raw) in [(values.x, raw.x), (values.y, raw.y), (values.z, raw.z)] {
let diff = (value - to_microtesla(raw)).abs();
assert!(diff < 0.01, "raw value conversion diff too large: {diff}");
}
// Check that the values are changing.
assert_ne!(first_reply, second_reply);
}
#[test]
fn test_mgm_1_request_switched_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm1);
let mgm_0_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(mgm_0_reply.switch_state, SwitchStateBinary::Off);
let mgm_1_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm1);
assert_eq!(mgm_1_reply.switch_state, SwitchStateBinary::On);
}
}
+254
View File
@@ -0,0 +1,254 @@
use nexosim::{
model::{schedulable, Context, Model},
ports::Output,
};
use satrs_minisim::{
acs::{mgm, mgt},
SimReply,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use types::pcdu::SwitchStateBinary;
/// Simple magnetorquer simulation model.
#[derive(Serialize, Deserialize)]
pub struct MgtModel {
switch_state: SwitchStateBinary,
torquing: bool,
torque_dipole: mgt::Dipole,
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
pub clear_magnetic_field: Output<()>,
pub reply: Output<SimReply>,
}
#[Model]
impl MgtModel {
pub fn new() -> Self {
Self {
switch_state: SwitchStateBinary::Off,
torquing: false,
torque_dipole: mgt::Dipole::default(),
gen_magnetic_field: Output::new(),
clear_magnetic_field: Output::new(),
reply: Output::new(),
}
}
pub async fn apply_torque(
&mut self,
duration_and_dipole: (Duration, mgt::Dipole),
cx: &Context<Self>,
) {
self.torque_dipole = duration_and_dipole.1;
self.torquing = true;
if cx
.schedule_event(duration_and_dipole.0, schedulable!(Self::clear_torque), ())
.is_err()
{
log::warn!("torque clearing can only be set for a future time.");
}
self.generate_magnetic_field(()).await;
}
#[nexosim(schedulable)]
async fn clear_torque(&mut self) {
self.torque_dipole = mgt::Dipole::default();
self.torquing = false;
self.clear_magnetic_field.send(()).await;
}
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state;
match switch_state {
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
SwitchStateBinary::Off => self.clear_torque().await,
}
}
pub async fn request_housekeeping_data(&mut self, _: (), cx: &Context<Self>) {
if self.switch_state != SwitchStateBinary::On {
return;
}
cx.schedule_event(
Duration::from_millis(15),
schedulable!(Self::send_housekeeping_data),
(),
)
.expect("requesting housekeeping data failed")
}
#[nexosim(schedulable)]
async fn send_housekeeping_data(&mut self) {
self.reply
.send(SimReply::from(mgt::Reply::Hk(mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
})))
.await;
}
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
// Simplified model: Just returns some fixed magnetic field for now.
// Later, we could make this more fancy by incorporating the commanded dipole.
mgm::MGT_GEN_MAGNETIC_FIELD
}
/// A torquing magnetorquer generates a magnetic field. This function can be used to apply
/// the magnetic field.
async fn generate_magnetic_field(&mut self, _: ()) {
if self.switch_state != SwitchStateBinary::On || !self.torquing {
return;
}
self.gen_magnetic_field
.send(self.calc_magnetic_field(self.torque_dipole))
.await;
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use satrs_minisim::{
acs::{mgm, mgt},
eps::PcduRequest,
SimReply, SimRequest, SimRequestWithTime,
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
let SimReply::Mgt(mgt::Reply::Hk(hk)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
Some(hk)
}
#[test]
fn test_basic_mgt_request_is_off() {
let mut sim_testbench = SimTestbench::new();
assert!(request_hk(&mut sim_testbench).is_none());
}
#[test]
fn test_basic_mgt_request_is_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
})
);
}
#[test]
fn test_basic_mgt_request_is_on_and_torquing() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
let commanded_dipole = mgt::Dipole {
x: -200,
y: 200,
z: 1000,
};
let request = SimRequestWithTime::new_with_epoch_time(mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: commanded_dipole,
});
sim_testbench
.send_request(request)
.expect("sending MGT request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(5)).unwrap();
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: commanded_dipole,
torquing: true,
})
);
sim_testbench
.step_until(Duration::from_millis(100))
.unwrap();
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
})
);
}
/// Processes the request without stepping, so scheduled events like the torque clearing do
/// not fire.
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
sim_testbench
.send_request(SimRequestWithTime::new_with_epoch_time(request))
.expect("sending request failed");
sim_testbench.handle_sim_requests_time_agnostic();
}
fn read_mgm_0_field(sim_testbench: &mut SimTestbench) -> mgm::SensorValuesMicroTesla {
process_without_step(
sim_testbench,
SimRequest::Mgm {
id: mgm::Id::Mgm0,
request: mgm::Request::RequestSensorData,
},
);
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM reply received");
let SimReply::Mgm { reply, .. } = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
reply.sensor_values
}
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
switch_device_on(sim_testbench, SwitchId::Mgm0);
switch_device_on(sim_testbench, SwitchId::Mgt);
process_without_step(
sim_testbench,
mgt::Request::ApplyTorque {
duration,
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
},
);
assert_eq!(read_mgm_0_field(sim_testbench), mgm::MGT_GEN_MAGNETIC_FIELD);
}
#[test]
fn test_mgm_field_cleared_after_torquing() {
let mut sim_testbench = SimTestbench::new();
start_torquing(&mut sim_testbench, Duration::from_millis(100));
sim_testbench
.step_until(Duration::from_millis(100))
.unwrap();
assert_ne!(
read_mgm_0_field(&mut sim_testbench),
mgm::MGT_GEN_MAGNETIC_FIELD
);
}
#[test]
fn test_mgm_field_cleared_by_switching_mgt_off() {
let mut sim_testbench = SimTestbench::new();
start_torquing(&mut sim_testbench, Duration::from_millis(100));
process_without_step(
&mut sim_testbench,
PcduRequest::SwitchDevice {
switch: SwitchId::Mgt,
state: SwitchStateBinary::Off,
},
);
assert_ne!(
read_mgm_0_field(&mut sim_testbench),
mgm::MGT_GEN_MAGNETIC_FIELD
);
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod mgm;
pub mod mgt;
+302
View File
@@ -0,0 +1,302 @@
use std::{
sync::mpsc,
time::{Duration, SystemTime},
};
use nexosim::{
ports::{event_queue, EventQueueReader, EventSinkReader, EventSource, SinkState},
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
time::{Clock, Deadline, MonotonicTime, SystemClock},
};
use satrs_minisim::{
acs::{mgm, mgt},
eps::PcduRequest,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
acs::{mgm::MgmModel, mgt::MgtModel},
eps::PcduModel,
};
const WARNING_FOR_STALE_DATA: bool = false;
const SIM_CTRL_REQ_WIRETAPPING: bool = false;
const MGM_REQ_WIRETAPPING: bool = false;
const PCDU_REQ_WIRETAPPING: bool = false;
const MGT_REQ_WIRETAPPING: bool = false;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ThreadingModel {
Default = 0,
Single = 1,
}
struct MgmInputs {
send_sensor_values: EventId<()>,
set_spi_fault: EventId<mgm::SpiFault>,
}
impl MgmInputs {
fn register(sim_init: &mut SimInit, mailbox: &Mailbox<MgmModel>) -> Self {
Self {
send_sensor_values: EventSource::new()
.connect(MgmModel::send_sensor_values, mailbox)
.register(sim_init),
set_spi_fault: EventSource::new()
.connect(MgmModel::set_spi_fault, mailbox)
.register(sim_init),
}
}
}
/// Model inputs which are driven by simulation requests.
struct ModelInputs {
mgm_0: MgmInputs,
mgm_1: MgmInputs,
pcdu_request_switch_info: EventId<()>,
pcdu_switch_device: EventId<(SwitchId, SwitchStateBinary)>,
mgt_apply_torque: EventId<(Duration, mgt::Dipole)>,
mgt_request_hk: EventId<()>,
}
// The simulation controller processes requests and drives the simulation.
pub struct SimController {
sys_clock: SystemClock,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
simulation: Simulation,
inputs: ModelInputs,
model_replies: EventQueueReader<SimReply>,
}
impl SimController {
pub fn new(
threading_model: ThreadingModel,
start_time: MonotonicTime,
reply_sender: mpsc::Sender<SimReply>,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
) -> Self {
let mut mgm_0_model = MgmModel::new(mgm::Id::Mgm0);
let mut mgm_1_model = MgmModel::new(mgm::Id::Mgm1);
let mut pcdu_model = PcduModel::new();
let mut mgt_model = MgtModel::new();
let mgm_0_mailbox = Mailbox::new();
let mgm_1_mailbox = Mailbox::new();
let pcdu_mailbox = Mailbox::new();
let mgt_mailbox = Mailbox::new();
pcdu_model
.mgm_0_switch
.connect(MgmModel::switch_device, &mgm_0_mailbox);
pcdu_model
.mgm_1_switch
.connect(MgmModel::switch_device, &mgm_1_mailbox);
pcdu_model
.mgt_switch
.connect(MgtModel::switch_device, &mgt_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &mgm_1_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &mgm_1_mailbox);
let (reply_sink, model_replies) = event_queue(SinkState::Enabled);
mgm_0_model.reply.connect_sink(reply_sink.clone());
mgm_1_model.reply.connect_sink(reply_sink.clone());
pcdu_model.reply.connect_sink(reply_sink.clone());
mgt_model.reply.connect_sink(reply_sink);
let mut sim_init = if threading_model == ThreadingModel::Single {
SimInit::with_num_threads(1)
} else {
SimInit::new()
};
let inputs = ModelInputs {
mgm_0: MgmInputs::register(&mut sim_init, &mgm_0_mailbox),
mgm_1: MgmInputs::register(&mut sim_init, &mgm_1_mailbox),
pcdu_request_switch_info: EventSource::new()
.connect(PcduModel::request_switch_info, &pcdu_mailbox)
.register(&mut sim_init),
pcdu_switch_device: EventSource::new()
.connect(PcduModel::switch_device, &pcdu_mailbox)
.register(&mut sim_init),
mgt_apply_torque: EventSource::new()
.connect(MgtModel::apply_torque, &mgt_mailbox)
.register(&mut sim_init),
mgt_request_hk: EventSource::new()
.connect(MgtModel::request_housekeeping_data, &mgt_mailbox)
.register(&mut sim_init),
};
let simulation = sim_init
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
.add_model(mgt_model, mgt_mailbox, "MGT model")
.init(start_time)
.unwrap();
Self {
sys_clock: SystemClock::from_system_time(start_time, SystemTime::now()),
request_receiver,
reply_sender,
simulation,
inputs,
model_replies,
}
}
#[cfg(test)]
pub fn step(&mut self) -> Result<(), ExecutionError> {
self.simulation.step()?;
self.forward_model_replies();
Ok(())
}
pub fn step_until(&mut self, deadline: impl Deadline) -> Result<(), ExecutionError> {
self.simulation.step_until(deadline)?;
self.forward_model_replies();
Ok(())
}
fn forward_model_replies(&mut self) {
while let Some(reply) = self.model_replies.try_read() {
self.reply_sender
.send(reply)
.expect("sending model reply failed");
}
}
pub fn run(&mut self, start_time: MonotonicTime, udp_polling_interval_ms: u64) {
let mut t = start_time;
loop {
let t_old = t;
// Check for UDP requests every millisecond. Shift the simulator ahead here to prevent
// replies lying in the past.
t += Duration::from_millis(udp_polling_interval_ms);
let _synch_status = self.sys_clock.synchronize(t);
self.handle_sim_requests(t_old);
self.step_until(t).expect("simulation step failed");
}
}
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime) {
loop {
match self.request_receiver.try_recv() {
Ok(request) => {
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
log::warn!("stale data with timestamp {:?} received", request.timestamp);
}
match request.request {
SimRequest::SimCtrl(request) => self.handle_ctrl_request(request),
SimRequest::Mgm { id, request } => self.handle_mgm_request(id, request),
SimRequest::Mgt(request) => self.handle_mgt_request(request),
SimRequest::Pcdu(request) => self.handle_pcdu_request(request),
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
panic!("all request sender disconnected")
}
},
}
}
self.forward_model_replies();
}
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
if SIM_CTRL_REQ_WIRETAPPING {
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
}
match sim_ctrl_request {
SimCtrlRequest::Ping => {
log::info!("received ping request, a client is connecting");
self.reply_sender
.send(SimReply::from(SimCtrlReply::Pong))
.expect("sending reply from sim controller failed");
}
}
}
fn handle_mgm_request(&mut self, mgm_id: mgm::Id, mgm_request: mgm::Request) {
let inputs = match mgm_id {
mgm::Id::Mgm0 => &self.inputs.mgm_0,
mgm::Id::Mgm1 => &self.inputs.mgm_1,
};
if MGM_REQ_WIRETAPPING {
log::info!("received {mgm_id:?} request: {mgm_request:?}");
}
match mgm_request {
mgm::Request::RequestSensorData => {
self.simulation
.process_event(&inputs.send_sensor_values, ())
.expect("event execution error for mgm");
}
mgm::Request::SetSpiFault(fault_mode) => {
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
self.simulation
.process_event(&inputs.set_spi_fault, fault_mode)
.expect("event execution error for mgm");
}
}
}
fn handle_pcdu_request(&mut self, pcdu_request: PcduRequest) {
if PCDU_REQ_WIRETAPPING {
log::info!("received PCDU request: {pcdu_request:?}");
}
match pcdu_request {
PcduRequest::RequestSwitchInfo => {
self.simulation
.process_event(&self.inputs.pcdu_request_switch_info, ())
.unwrap();
}
PcduRequest::SwitchDevice { switch, state } => {
self.simulation
.process_event(&self.inputs.pcdu_switch_device, (switch, state))
.unwrap();
}
}
}
fn handle_mgt_request(&mut self, mgt_request: mgt::Request) {
if MGT_REQ_WIRETAPPING {
log::info!("received MGT request: {mgt_request:?}");
}
match mgt_request {
mgt::Request::ApplyTorque { duration, dipole } => self
.simulation
.process_event(&self.inputs.mgt_apply_torque, (duration, dipole))
.unwrap(),
mgt::Request::RequestHk => self
.simulation
.process_event(&self.inputs.mgt_request_hk, ())
.unwrap(),
};
}
}
#[cfg(test)]
mod tests {
use crate::test_helpers::SimTestbench;
use super::*;
#[test]
fn test_basic_ping() {
let mut sim_testbench = SimTestbench::new();
assert_eq!(
sim_testbench.request_reply(SimCtrlRequest::Ping),
Some(SimReply::SimCtrl(SimCtrlReply::Pong))
);
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::time::Duration;
use nexosim::{
model::{schedulable, Context, Model},
ports::Output,
};
use satrs_minisim::{eps::PcduReply, SimReply};
use serde::{Deserialize, Serialize};
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
#[derive(Serialize, Deserialize)]
pub struct PcduModel {
switcher_map: SwitchMapBinary,
pub mgm_0_switch: Output<SwitchStateBinary>,
pub mgm_1_switch: Output<SwitchStateBinary>,
pub mgt_switch: Output<SwitchStateBinary>,
pub reply: Output<SimReply>,
}
#[Model]
impl PcduModel {
pub fn new() -> Self {
Self {
switcher_map: SwitchMapBinaryWrapper::default().0,
mgm_0_switch: Output::new(),
mgm_1_switch: Output::new(),
mgt_switch: Output::new(),
reply: Output::new(),
}
}
pub async fn request_switch_info(&mut self, _: (), cx: &Context<Self>) {
cx.schedule_event(
Duration::from_millis(SWITCH_INFO_DELAY_MS),
schedulable!(Self::send_switch_info),
(),
)
.expect("requesting switch info failed");
}
#[nexosim(schedulable)]
async fn send_switch_info(&mut self) {
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.clone()));
self.reply.send(reply).await;
}
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
log::info!(
"switching {:?} to {:?}",
switch_and_target_state.0,
switch_and_target_state.1
);
let val = self
.switcher_map
.get_mut(&switch_and_target_state.0)
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
*val = switch_and_target_state.1;
match switch_and_target_state.0 {
SwitchId::Mgm0 => {
self.mgm_0_switch.send(switch_and_target_state.1).await;
}
SwitchId::Mgm1 => {
self.mgm_1_switch.send(switch_and_target_state.1).await;
}
SwitchId::Mgt => {
self.mgt_switch.send(switch_and_target_state.1).await;
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::time::Duration;
use satrs_minisim::{eps::PcduRequest, SimRequestWithTime};
use types::pcdu::SwitchMapBinary;
use crate::test_helpers::SimTestbench;
fn switch_device(
sim_testbench: &mut SimTestbench,
switch: SwitchId,
target: SwitchStateBinary,
) {
sim_testbench.send_and_step(PcduRequest::SwitchDevice {
switch,
state: target,
});
}
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
}
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: SwitchId) {
switch_device(sim_testbench, switch, SwitchStateBinary::On);
}
pub(crate) fn get_all_off_switch_map() -> SwitchMapBinary {
SwitchMapBinaryWrapper::default().0
}
fn unwrap_switch_map(sim_reply: SimReply) -> SwitchMapBinary {
let SimReply::Pcdu(PcduReply::SwitchInfo(switch_map)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
switch_map
}
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
let sim_reply = sim_testbench
.request_reply(PcduRequest::RequestSwitchInfo)
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), *expected_switch_map);
}
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
let mut sim_testbench = SimTestbench::new();
switch_device(&mut sim_testbench, switch, target);
let mut switcher_map = get_all_off_switch_map();
*switcher_map.get_mut(&switch).unwrap() = target;
check_switch_state(&mut sim_testbench, &switcher_map);
}
#[test]
fn test_pcdu_switcher_request() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
sim_testbench
.send_request(request)
.expect("sending PCDU request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
assert!(sim_testbench.try_receive_next_reply().is_none());
// The reply is delayed by SWITCH_INFO_DELAY_MS.
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), get_all_off_switch_map());
}
#[test]
fn test_pcdu_switching_mgm_on() {
test_pcdu_switching_single_switch(SwitchId::Mgm0, SwitchStateBinary::On);
}
#[test]
fn test_pcdu_switching_mgt_on() {
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
}
#[test]
fn test_pcdu_switching_mgt_off() {
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::Off);
}
}
+322
View File
@@ -0,0 +1,322 @@
use nexosim::time::MonotonicTime;
use serde::{Deserialize, Serialize};
use crate::{
acs::{mgm, mgt},
eps::{PcduReply, PcduRequest},
};
/// Used by clients to route replies to the component handling them.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum ComponentId {
SimCtrl,
Mgm0Lis3Mdl,
Mgm1Lis3Mdl,
Mgt,
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimRequest {
SimCtrl(SimCtrlRequest),
Mgm { id: mgm::Id, request: mgm::Request },
Mgt(mgt::Request),
Pcdu(PcduRequest),
}
impl From<SimCtrlRequest> for SimRequest {
fn from(request: SimCtrlRequest) -> Self {
Self::SimCtrl(request)
}
}
impl From<mgt::Request> for SimRequest {
fn from(request: mgt::Request) -> Self {
Self::Mgt(request)
}
}
impl From<PcduRequest> for SimRequest {
fn from(request: PcduRequest) -> Self {
Self::Pcdu(request)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimRequestWithTime {
pub request: SimRequest,
pub timestamp: MonotonicTime,
}
impl SimRequestWithTime {
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
Self {
request: request.into(),
timestamp,
}
}
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
Self::new(request, MonotonicTime::EPOCH)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimReply {
SimCtrl(SimCtrlReply),
Mgm { id: mgm::Id, reply: mgm::Reply },
Mgt(mgt::Reply),
Pcdu(PcduReply),
}
impl SimReply {
pub fn component(&self) -> ComponentId {
match self {
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
SimReply::Mgm { id, .. } => id.sim_component(),
SimReply::Mgt(_) => ComponentId::Mgt,
SimReply::Pcdu(_) => ComponentId::Pcdu,
}
}
}
impl From<SimCtrlReply> for SimReply {
fn from(reply: SimCtrlReply) -> Self {
Self::SimCtrl(reply)
}
}
impl From<mgt::Reply> for SimReply {
fn from(reply: mgt::Reply) -> Self {
Self::Mgt(reply)
}
}
impl From<PcduReply> for SimReply {
fn from(reply: PcduReply) -> Self {
Self::Pcdu(reply)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlRequest {
Ping,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlReply {
Pong,
}
pub mod eps {
use super::*;
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
switch: SwitchId,
state: SwitchStateBinary,
},
RequestSwitchInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
SwitchInfo(SwitchMapBinary),
}
}
pub mod acs {
/// MGM module strongly based on the LIS3MDL device.
pub mod mgm {
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::ComponentId;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
// similar which needs to be converted to a signed float value with physical units. We will
// simplify this now and generate the signed float values directly. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reply {
pub switch_state: SwitchStateBinary,
pub sensor_values: SensorValuesMicroTesla,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: RawValues,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum Id {
Mgm0,
Mgm1,
}
impl Id {
pub const fn sim_component(&self) -> ComponentId {
match self {
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
}
}
}
impl RawValues {
pub const fn splat(value: i16) -> Self {
Self {
x: value,
y: value,
z: value,
}
}
pub fn from_microtesla(values: SensorValuesMicroTesla) -> Self {
let to_raw = |microtesla: f32| {
(microtesla / (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16
};
Self {
x: to_raw(values.x),
y: to_raw(values.y),
z: to_raw(values.z),
}
}
}
impl Reply {
pub fn new(
switch_state: SwitchStateBinary,
sensor_values: SensorValuesMicroTesla,
fault_mode: SpiFaultMode,
) -> Self {
// An injected fault always wins. A switched off device reads back like an
// undriven bus.
let raw = match (fault_mode, switch_state) {
(SpiFaultMode::AllZeros, _) => RawValues::splat(ALL_ZEROS_SENSOR_VAL),
(SpiFaultMode::AllOnes, _) | (SpiFaultMode::None, SwitchStateBinary::Off) => {
RawValues::splat(ALL_ONES_SENSOR_VAL)
}
(SpiFaultMode::None, SwitchStateBinary::On) => {
RawValues::from_microtesla(sensor_values)
}
};
Self {
switch_state,
sensor_values,
raw,
}
}
}
}
pub mod mgt {
use std::time::Duration;
use serde::{Deserialize, Serialize};
// Simple model using i16 values.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dipole {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
ApplyTorque { duration: Duration, dipole: Dipole },
RequestHk,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HkSet {
pub dipole: Dipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Reply {
Hk(HkSet),
}
}
}
pub mod udp {
pub const SIM_CTRL_PORT: u16 = 7303;
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn test_request_serde_roundtrip() {
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let json = serde_json::to_string(&sim_request).unwrap();
let deserialized: SimRequestWithTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, sim_request);
}
#[test]
fn test_reply_serde_roundtrip() {
let sim_reply = SimReply::from(SimCtrlReply::Pong);
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
let json = serde_json::to_string(&sim_reply).unwrap();
let deserialized: SimReply = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, sim_reply);
}
}
+63
View File
@@ -0,0 +1,63 @@
use controller::{SimController, ThreadingModel};
use nexosim::time::MonotonicTime;
use satrs_minisim::udp::SIM_CTRL_PORT;
use std::sync::mpsc;
use std::thread;
use udp::SimUdpServer;
mod acs;
mod controller;
mod eps;
#[cfg(test)]
mod test_helpers;
mod time;
mod udp;
fn main() {
let (request_sender, request_receiver) = mpsc::channel();
let (reply_sender, reply_receiver) = mpsc::channel();
let t0 = MonotonicTime::EPOCH;
let mut sim_ctrl =
SimController::new(ThreadingModel::Default, t0, reply_sender, request_receiver);
// Configure logger at runtime
fern::Dispatch::new()
// Perform allocation-free log formatting
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339(std::time::SystemTime::now()),
record.level(),
record.target(),
message
))
})
// Add blanket level filter -
.level(log::LevelFilter::Debug)
// - and per-module overrides
// Output to stdout, files, and other Dispatch configurations
.chain(std::io::stdout())
.chain(fern::log_file("output.log").expect("could not open log output file"))
// Apply globally
.apply()
.expect("could not apply logger configuration");
log::info!("starting simulation thread");
// This thread schedules the simulator.
let sim_thread = thread::spawn(move || {
sim_ctrl.run(t0, 1);
});
let mut udp_server =
SimUdpServer::new(SIM_CTRL_PORT, request_sender, reply_receiver, 200, None)
.expect("could not create UDP request server");
log::info!("starting UDP server on port {SIM_CTRL_PORT}");
// This thread manages the simulator UDP server.
let udp_tc_thread = thread::spawn(move || {
udp_server.run();
});
sim_thread.join().expect("joining simulation thread failed");
udp_tc_thread
.join()
.expect("joining UDP server thread failed");
}
@@ -5,14 +5,14 @@ use nexosim::{
simulation::ExecutionError,
time::{Deadline, MonotonicTime},
};
use satrs_minisim::{SimReply, SimRequest};
use satrs_minisim::{SimReply, SimRequest, SimRequestWithTime};
use crate::{controller::SimController, create_sim_controller, ThreadingModel};
use crate::controller::{SimController, ThreadingModel};
pub struct SimTestbench {
pub sim_controller: SimController,
pub reply_receiver: mpsc::Receiver<SimReply>,
pub request_sender: mpsc::Sender<SimRequest>,
pub request_sender: mpsc::Sender<SimRequestWithTime>,
}
impl SimTestbench {
@@ -21,7 +21,7 @@ impl SimTestbench {
let (reply_sender, reply_receiver) = mpsc::channel();
let t0 = MonotonicTime::EPOCH;
let sim_ctrl =
create_sim_controller(ThreadingModel::Single, t0, reply_sender, request_receiver);
SimController::new(ThreadingModel::Single, t0, reply_sender, request_receiver);
Self {
sim_controller: sim_ctrl,
@@ -36,17 +36,31 @@ impl SimTestbench {
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>> {
pub fn send_request(
&self,
request: SimRequestWithTime,
) -> Result<(), mpsc::SendError<SimRequestWithTime>> {
self.request_sender.send(request)
}
/// Sends the request and steps the simulation to the next scheduled event.
pub fn send_and_step(&mut self, request: impl Into<SimRequest>) {
self.send_request(SimRequestWithTime::new_with_epoch_time(request))
.expect("sending request failed");
self.handle_sim_requests_time_agnostic();
self.step().unwrap();
}
pub fn request_reply(&mut self, request: impl Into<SimRequest>) -> Option<SimReply> {
self.send_and_step(request);
self.try_receive_next_reply()
}
pub fn try_receive_next_reply(&self) -> Option<SimReply> {
match self.reply_receiver.try_recv() {
Ok(reply) => Some(reply),
File renamed without changes.
@@ -6,13 +6,12 @@ use std::{
time::Duration,
};
use satrs_minisim::{SimMessageProvider, SimReply, SimRequest};
use satrs_minisim::{SimReply, SimRequestWithTime};
// 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,
request_sender: mpsc::Sender<SimRequestWithTime>,
reply_receiver: mpsc::Receiver<SimReply>,
reply_queue: VecDeque<SimReply>,
max_num_replies: usize,
@@ -27,7 +26,7 @@ pub struct SimUdpServer {
impl SimUdpServer {
pub fn new(
local_port: u16,
request_sender: mpsc::Sender<SimRequest>,
request_sender: mpsc::Sender<SimRequestWithTime>,
reply_receiver: mpsc::Receiver<SimReply>,
max_num_replies: usize,
stop_signal: Option<Arc<AtomicBool>>,
@@ -47,7 +46,7 @@ impl SimUdpServer {
})
}
#[allow(dead_code)]
#[cfg(test)]
pub fn server_addr(&self) -> std::io::Result<SocketAddr> {
self.socket.local_addr()
}
@@ -90,12 +89,9 @@ impl SimUdpServer {
self.sender_addr = Some(src);
let sim_req = SimRequest::from_raw_data(&self.req_buf[..bytes_read]);
if sim_req.is_err() {
log::warn!(
"received UDP request with invalid format: {}",
sim_req.unwrap_err()
);
let sim_req = serde_json::from_slice::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
if let Err(e) = sim_req {
log::warn!("received UDP request with invalid format: {}", e);
return processed_requests;
}
self.request_sender.send(sim_req.unwrap()).unwrap();
@@ -160,7 +156,7 @@ mod tests {
use satrs_minisim::{
eps::{PcduReply, PcduRequest},
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime,
};
use crate::eps::tests::get_all_off_switch_map;
@@ -205,7 +201,7 @@ mod tests {
})
}
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize> {
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize> {
self.socket.send(
&serde_json::to_vec(sim_request).expect("conversion of request to vector failed"),
)
@@ -223,7 +219,7 @@ mod tests {
struct UdpTestbench {
client: SimUdpTestClient,
stop_signal: Arc<AtomicBool>,
request_receiver: mpsc::Receiver<SimRequest>,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
}
@@ -259,7 +255,7 @@ mod tests {
))
}
pub fn try_recv_request(&self) -> Result<SimRequest, mpsc::TryRecvError> {
pub fn try_recv_request(&self) -> Result<SimRequestWithTime, mpsc::TryRecvError> {
self.request_receiver.try_recv()
}
@@ -275,7 +271,7 @@ mod tests {
delegate! {
to self.client {
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize>;
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize>;
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError>;
}
}
@@ -319,7 +315,7 @@ mod tests {
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);
let sim_request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
udp_testbench
.send_request(&sim_request)
.expect("sending request failed");
@@ -341,10 +337,12 @@ mod tests {
.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))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
udp_testbench.check_next_sim_reply(&sim_reply);
@@ -365,11 +363,13 @@ mod tests {
// 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))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
// Send a reply to the server, ensure it gets forwarded to the client.
let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
@@ -388,7 +388,7 @@ mod tests {
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()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(10));
@@ -396,7 +396,9 @@ mod tests {
// Connect by sending a ping.
udp_testbench
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
@@ -415,7 +417,7 @@ mod tests {
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);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
for _ in 0..4 {
udp_testbench.send_reply(&sim_reply);
}
@@ -425,7 +427,9 @@ mod tests {
// Connect by sending a ping.
udp_testbench
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
-143
View File
@@ -1,143 +0,0 @@
/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
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""Example client for the sat-rs example application"""
import logging
import sys
import time
import tmtccmd
from spacepackets.ecss import PusVerificator
from tmtccmd import ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest
from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import CcsdsTmHandler
from tmtccmd.config import (
default_json_path,
SetupParams,
params_to_procedure_conversion,
)
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
from tmtccmd.logging import add_colorlog_console_logger
from tmtccmd.logging.pus import (
RegularTmtcLogWrapper,
RawTmtcTimedLogWrapper,
TimedLogWhen,
)
from spacepackets.seqcount import PusFileSeqCountProvider
from pytmtc.config import SatrsConfigHook
from pytmtc.pus_tc import TcHandler
from pytmtc.pus_tm import PusHandler
_LOGGER = logging.getLogger()
def main():
add_colorlog_console_logger(_LOGGER)
tmtccmd.init_printout(False)
hook_obj = SatrsConfigHook(json_cfg_path=default_json_path())
parser_wrapper = PreArgsParsingWrapper()
parser_wrapper.create_default_parent_parser()
parser_wrapper.create_default_parser()
parser_wrapper.add_def_proc_args()
params = SetupParams()
post_args_wrapper = parser_wrapper.parse(hook_obj, params)
proc_wrapper = ProcedureParamsWrapper()
if post_args_wrapper.use_gui:
post_args_wrapper.set_params_without_prompts(proc_wrapper)
else:
post_args_wrapper.set_params_with_prompts(proc_wrapper)
setup_args = SetupWrapper(
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
)
# Create console logger helper and file loggers
tmtc_logger = RegularTmtcLogWrapper()
file_logger = tmtc_logger.logger
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
verificator = PusVerificator()
verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger)
# Create primary TM handler and add it to the CCSDS Packet Handler
tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger)
ccsds_handler = CcsdsTmHandler(generic_handler=tm_handler)
# TODO: We could add the CFDP handler for the CFDP APID at a later stage.
# 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:
tmtc_backend.close_com_if()
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:
tmtc_backend.close_com_if()
sys.exit(0)
if __name__ == "__main__":
main()
-27
View File
@@ -1,27 +0,0 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pytmtc"
description = "Python TMTC client for OPS-SAT"
readme = "README.md"
version = "0.1.0"
requires-python = ">=3.8"
authors = [
{name = "Robin Mueller", email = "robin.mueller.m@gmail.com"},
]
dependencies = [
"tmtccmd~=8.1",
"pydantic~=2.7"
]
[tool.setuptools.packages]
find = {}
[tool.ruff]
extend-exclude = ["archive"]
[tool.ruff.lint]
ignore = ["E501"]
[tool.ruff.lint.extend-per-file-ignores]
"__init__.py" = ["F401"]
Whitespace-only changes.
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff. Show more