Compare commits

...
Author SHA1 Message Date
Robin Mueller ce016b1e3f embedded apps now connect to minisim 2026-09-30 12:39:54 +02:00
muellerr 1db716b166 Merge pull request 'removed missing file' (#299) from missing-file-removal into main
Reviewed-on: #299
2026-09-29 15:56:22 +02:00
Robin Mueller 2a5b8f95d3 removed missing file 2026-09-29 15:56:03 +02:00
muellerr 26c0b042c9 Merge pull request 'remove STM32F3 Disco example' (#298) from remove-stm32f3-example into main
Reviewed-on: #298
2026-09-29 15:54:25 +02:00
Robin Mueller e103702184 remove STM32F3 Disco example
- target MCU not large enough for larger example featuring sat-rs and also does not have
  ethernet/networking support
2026-09-29 15:51:08 +02:00
muellerr 233e5abb23 Merge pull request 'unified example types and clients' (#297) from unify-example-types-client into main
Reviewed-on: #297
2026-09-29 15:46:31 +02:00
Robin Mueller f3e5e06937 unified example types and clients 2026-09-29 15:45:03 +02:00
muellerr 7b33855ba1 Merge pull request 'STM32H7 embassy example' (#296) from stm32h7-embassy into main
Reviewed-on: #296
2026-09-29 15:02:30 +02:00
Robin Mueller 8660dbae90 STM32H7 embassy example 2026-09-29 15:01:30 +02:00
muellerr 394ffec6c9 Merge pull request 'Reuse simulator' (#295) from reuse-simulator into main
Reviewed-on: #295
2026-09-29 14:22:52 +02:00
Robin Mueller 7cc2ff3ff9 first step in unifying the examples: one example folder
One example folder for all examples, including std and embedded examples
2026-09-29 13:39:39 +02:00
muellerr bb8decfc74 Merge pull request 'MGT fdir' (#292) from mgt-fdir into main
Reviewed-on: #292
2026-09-29 11:50:39 +02:00
Robin Mueller 23ac7cf0ad MGT fdir 2026-09-29 11:49:37 +02:00
muellerr cb6c8b84bb Merge pull request 'switch to new nucleo H753 board' (#293) from embedded-readmes into main
Reviewed-on: #293
2026-09-24 19:39:25 +02:00
Robin Mueller 390d7aaff5 switch to new nucleo H753 board 2026-09-24 19:37:12 +02:00
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
muellerr 56d9cafa59 Merge pull request 'small changelog tweak' (#256) from small-changelog-tweak into main
Reviewed-on: #256
2025-11-06 14:51:07 +01:00
Robin Mueller 7de057aaf2 small changelog tweak 2025-11-06 14:50:38 +01:00
muellerr 2f4a11a0bc Merge pull request 'bump spacepackets version' (#255) from bump-spacepackets-version into main
Reviewed-on: #255
2025-11-06 14:48:06 +01:00
Robin Mueller 0cd929a19c bumped spacepackets version 2025-11-06 14:47:27 +01:00
muellerr d54167837e Merge pull request 'updated STM32F3 RTICv2 example' (#254) from update-embedded-examples into main
Reviewed-on: #254
2025-10-31 14:31:05 +01:00
Robin Mueller ecce07a471 updated STM32F3 RTICv2 example 2025-10-31 14:27:29 +01:00
muellerr 526254851a Merge pull request 'updated dependencies' (#253) from update-dependencies into main
Reviewed-on: #253
2025-10-29 16:41:58 +01:00
Robin Mueller 440e757141 updated dependencies 2025-10-29 16:37:41 +01:00
muellerr 02aa825783 Merge pull request 'Larger update' (#252) from larger-update into main
Reviewed-on: #252
2025-10-21 14:33:31 +02:00
Robin Mueller bc9e0e4a94 Larger update
- ComponentId is u32 now
- Simplified TCP servers
2025-10-21 13:28:24 +02:00
muellerr 06e713a557 Merge pull request 'Rework Event Management Module' (#251) from rework-event-management-module into main
Reviewed-on: #251
2025-10-20 11:21:44 +02:00
Robin Mueller 778512d50e rework event management module 2025-10-20 11:16:28 +02:00
muellerr 5d40638964 Merge pull request 'update spacepackets dependency' (#250) from update-dependencies into main
Reviewed-on: #250
2025-09-26 16:01:40 +02:00
Robin Mueller 1b07b845f2 update spacepackets dependency 2025-09-26 16:01:30 +02:00
muellerr 2e58a311c8 Merge pull request 'CI fix and naming improvement' (#249) from example-ci-fix into main
Reviewed-on: #249
2025-09-06 19:40:17 +02:00
Robin Mueller 62d64e692a CI fix and naming improvement 2025-09-06 19:36:56 +02:00
muellerr 3784f47b66 Merge pull request 'update heapless dependency' (#248) from update-deps into main
Reviewed-on: #248
2025-09-06 19:32:18 +02:00
Robin Mueller e1911f1b6e update heapless dependency 2025-09-06 19:32:06 +02:00
muellerr 2e53ce1871 Merge pull request 'update trait names' (#247) from update-trait-names into main
Reviewed-on: #247
2025-09-06 19:31:46 +02:00
Robin Mueller a6c460129b update trait names 2025-09-06 19:26:39 +02:00
muellerr d5ecefd683 Merge pull request 'small changelog adaption' (#246) from small-changelog-adaption into main
Reviewed-on: #246
2025-08-26 14:29:41 +02:00
muellerr 2f9f3b8183 small changelog adaption 2025-08-26 14:28:22 +02:00
muellerr b28cff85de Merge pull request 'prepare next MIB release' (#245) from satrs-mib-release into main
Reviewed-on: #245
2025-08-26 14:26:47 +02:00
muellerr da3de9f22c prepare next MIB release 2025-08-26 14:24:48 +02:00
muellerr aefdf0987d Merge pull request 'add matrix chat badge' (#243) from add-chat-badge into main
Reviewed-on: #243
2025-08-14 14:17:56 +02:00
Robin Mueller 7ae9e62d66 add matrix chat badge 2025-08-14 14:17:41 +02:00
muellerr fea1a9028b Merge pull request 'edition bump to 2024 ald clippy' (#244) from edition-bump-clippy-fixes into main
Reviewed-on: #244
2025-08-14 14:17:18 +02:00
Robin Mueller abc145fb2d edition bump to 2024 ald clippy 2025-08-14 14:10:00 +02:00
muellerr 490635dfcf Merge pull request 'prepare patch releases' (#242) from prep-patch-releases into main
Reviewed-on: #242
2025-07-23 14:09:59 +02:00
muellerr de3d22a022 prepare patch releases 2025-07-23 14:05:04 +02:00
muellerr cbe211fe8b Merge pull request 'CFDP reference' (#241) from cfdp-ref into main
Reviewed-on: #241
2025-07-22 10:47:08 +02:00
Robin Mueller e379bc3fd7 CFDP 2025-07-22 10:46:56 +02:00
muellerr a61ee85796 Merge pull request 'clippy fixes' (#240) from clippy-fixes into main
Reviewed-on: #240
2025-07-22 10:44:42 +02:00
Robin Mueller 81473b30f9 clippy fixes 2025-07-22 10:44:11 +02:00
muellerr 22675a73f9 Merge pull request 'changelog' (#239) from satrs-changelog into main
Reviewed-on: #239
2025-07-22 10:36:58 +02:00
Robin Mueller c68e3d4f75 changelog 2025-07-22 10:35:38 +02:00
muellerr 3deedfba17 Merge pull request 'bump all dependencies' (#238) from dependency-update into main
Reviewed-on: #238
2025-07-22 10:31:15 +02:00
Robin Mueller 533caea0fe bump all dependencies 2025-07-22 10:29:54 +02:00
muellerr 848fe6f207 Merge pull request 'spacepackets update' (#237) from update-satrs-deps into main
Reviewed-on: #237
2025-07-18 19:24:55 +02:00
Robin Mueller cf64fea7d9 bump cobs-rs dependency 2025-07-18 19:24:40 +02:00
muellerr 4948db3fa5 Merge pull request 'bump cobs-rs dependency' (#236) from update-satrs-deps into main
Reviewed-on: #236
2025-07-18 19:19:58 +02:00
Robin Mueller 1920e4878c bump cobs-rs dependency 2025-07-18 19:19:24 +02:00
muellerr f46bc94b04 Merge pull request 'some test fixes' (#235) from test-fix into main
Reviewed-on: #235
2025-05-19 15:49:02 +02:00
muellerr fb45da1890 some test fixes 2025-05-19 15:46:57 +02:00
muellerr f600ee499a Merge pull request 'add first generator' (#234) from generators into main
Reviewed-on: #234
2025-05-19 15:23:37 +02:00
muellerr 3f28f60c59 add first generator 2025-05-19 15:21:51 +02:00
muellerr 44b1f2b037 Merge pull request 'possible fix for clippy warning' (#233) from possible-fix-for-clippy-warning into main
Reviewed-on: #233
2025-05-19 15:15:24 +02:00
muellerr 4b22958b34 possible fix for clippy warning 2025-05-19 15:14:59 +02:00
muellerr a711c6acd9 Merge pull request 'bump spacepackets' (#232) from bump-spacepackets into main
Reviewed-on: #232
2025-05-19 14:19:00 +02:00
muellerr 99a954a1f5 some tweaks 2025-05-19 14:18:26 +02:00
muellerr 4a4fd7ac2c use git deps 2025-05-16 19:49:43 +02:00
muellerr 9bf08849a2 annoying 2025-05-16 19:43:45 +02:00
muellerr b8f7fefe26 Merge pull request 'should fix tests' (#231) from test-fix into main
Reviewed-on: #231
2025-05-10 16:31:14 +02:00
muellerr a501832698 should fix tests 2025-05-10 16:30:37 +02:00
muellerr c7284d3f1c Merge pull request 'minor wording improvements' (#230) from some-wording-improvements into main
Reviewed-on: #230
2025-05-10 16:27:37 +02:00
muellerr 18263d4568 Merge branch 'main' into some-wording-improvements 2025-05-10 16:27:29 +02:00
muellerr 95519c1363 minor wording improvements 2025-05-10 16:27:04 +02:00
muellerr 2ec32717d0 Merge pull request 'use released dependencies' (#229) from use-relesed-dependencies into main
Reviewed-on: #229
2025-05-10 16:24:37 +02:00
muellerr bfdd777685 use released dependencies 2025-05-10 16:24:06 +02:00
muellerr b54c2b7863 Merge pull request 'spacepackets update' (#227) from spacepackets-update into main
Reviewed-on: #227
2025-05-10 16:19:07 +02:00
muellerr 19f3355283 Merge pull request 'update SIM suite name' (#228) from update-sim-suite-name into main
Reviewed-on: #228
2025-05-10 16:18:16 +02:00
muellerr 4aeb28d2f1 update SIM suite name 2025-05-10 16:14:48 +02:00
muellerr ddc4544456 spacepackets update, clippy fixes 2025-05-10 16:13:45 +02:00
muellerr 9ab36c0362 Merge pull request 'update docs' (#226) from update-docs into main
Reviewed-on: #226
2025-05-06 16:21:17 +02:00
muellerr b95769c177 Merge branch 'main' into update-docs 2025-05-06 16:21:10 +02:00
muellerr bb20533ae1 Merge pull request 'remove token API for verification creator core' (#224) from simplify-verification-reporter-core into main
Reviewed-on: #224
2025-05-05 19:03:34 +02:00
muellerr 27cacd0f43 remove token API for verification creator core 2025-05-05 19:02:37 +02:00
muellerr bd6488e87b update docs 2025-04-01 20:58:18 +02:00
muellerr 52ec0d44aa Merge pull request 'bump README links' (#223) from update-readme-links into main
Reviewed-on: #223
2025-04-01 15:15:14 +02:00
muellerr 4fa9f8d685 bump README links 2025-04-01 15:05:28 +02:00
muellerr 767cf6b1a5 Merge pull request 'prepare alpha release' (#222) from prep-satrs-v0.3.0-alpha.0 into main
Reviewed-on: #222
2025-02-18 16:52:15 +01:00
muellerr 5bf5518591 prepare alpha release 2025-02-18 16:49:33 +01:00
muellerr f3e0609910 Merge pull request 'Mode Sequences and Tables' (#211) from mode-tables-sequence-tables into main
Reviewed-on: #211
2025-02-13 12:44:28 +01:00
muellerr 05106354e3 Mode Tree Feature Update 2025-02-13 12:18:05 +01:00
muellerr 719b70d834 Merge pull request 'use static cell instead of custom struct' (#220) from use-static-cell into main
Reviewed-on: #220
2025-02-07 16:21:23 +01:00
muellerr d4082fa098 use static cell instead of custom struct 2025-02-07 16:15:23 +01:00
muellerr c9c9e4f735 Merge pull request 'update STM32H7 project' (#221) from update-stm32h7-project into main
Reviewed-on: #221
2025-02-07 16:15:08 +01:00
muellerr 2c92a95184 update STM32H7 project 2025-02-07 15:43:03 +01:00
muellerr 1a1d330814 Merge pull request 'update some outdated dependencies' (#219) from update-outdated-deps into main
Reviewed-on: #219
2025-01-31 18:37:49 +01:00
muellerr ef7b5d66fe update some outdated dependencies 2025-01-31 18:37:12 +01:00
muellerr b3497e0592 Merge pull request 'update embedded examples' (#218) from update-embedded-examples into main
Reviewed-on: #218
2025-01-31 18:32:41 +01:00
muellerr 73286e36c2 update embedded examples 2025-01-31 18:20:06 +01:00
muellerr ed266a11f6 Merge pull request 'Avoid static muts for static pool buffer declaration' (#215) from avoid-static-muts-static-pools into main
Reviewed-on: #215
2025-01-31 12:12:57 +01:00
muellerr af972e174f macro fix 2025-01-31 12:04:40 +01:00
muellerr 3d12083c16 use full path to helper type 2025-01-31 11:56:38 +01:00
muellerr e8d2c020fa Merge pull request 'use released nexosim version' (#217) from use-released-nexosim into main
Reviewed-on: #217
2025-01-31 11:33:56 +01:00
muellerr 69e172b633 avoid static muts for static pools 2025-01-31 11:27:09 +01:00
muellerr e1dda751bc use released nexosim version 2025-01-31 11:24:56 +01:00
muellerr b01628d8ef Merge pull request 'fix minisim tests' (#216) from update-minisim into main
Reviewed-on: #216
2025-01-31 11:18:03 +01:00
muellerr 31844e4fe2 fix tests 2025-01-31 11:17:39 +01:00
muellerr 738872f421 Merge pull request 'update mini simulator' (#214) from update-minisim into main
Reviewed-on: #214
2025-01-30 18:57:27 +01:00
muellerr 309e39999f small tweak 2025-01-30 18:56:44 +01:00
muellerr 1c43c3adf9 update mini simulator 2025-01-30 18:55:47 +01:00
muellerr d9e0abffa7 Merge pull request 'clippy and test fix' (#213) from clippy-fixes into main
Reviewed-on: #213
2025-01-23 17:22:46 +01:00
muellerr abec9dd448 clippy and test fix 2025-01-23 17:19:26 +01:00
muellerr c18fbb59ad Merge pull request 're-run formatter' (#212) from run-afmt into main
Reviewed-on: #212
2025-01-23 17:04:58 +01:00
muellerr c91ddcd658 re-run formatter 2025-01-23 17:02:16 +01:00
muellerr c5fa1955d7 Merge pull request 'Scheduling Table for std runtimes' (#210) from scheduling-table into main
Reviewed-on: #210
2024-11-20 17:22:08 +01:00
muellerr c9708810e6 changelog 2024-11-20 17:21:54 +01:00
muellerr 79d0c2e222 Scheduling Table for std runtimes 2024-11-20 17:20:42 +01:00
muellerr 8e87875c0e Merge pull request 'use released satrs-shared' (#209) from use-released-satrs-shared into main
Reviewed-on: #209
2024-11-15 12:15:06 +01:00
muellerr 1ac6c02c06 use released satrs-shared 2024-11-15 12:14:33 +01:00
muellerr 6e7907522e Merge pull request 'changelog satrs-shared' (#208) from satrs-shared-changelog into main
Reviewed-on: #208
2024-11-15 12:08:27 +01:00
muellerr f747a5efdc changelog satrs-shared 2024-11-15 12:07:32 +01:00
muellerr 6ffd55cec2 Merge pull request 'bump satrs-shared' (#207) from satrs-shared-v0.2.1 into main
Reviewed-on: #207
2024-11-15 11:59:00 +01:00
muellerr 5e51b3de42 bump satrs-shared 2024-11-15 11:52:48 +01:00
muellerr bd059a2541 Merge pull request 'Bump dependecies' (#206) from bump-dependencies into main
Reviewed-on: #206
2024-11-15 11:49:19 +01:00
muellerr 1a4d764f25 bump dependencies 2024-11-15 11:37:14 +01:00
muellerr b38c617fae Merge pull request 'clippy fixes' (#205) from small-clippy-fix into main
Reviewed-on: #205
2024-11-04 13:55:27 +01:00
muellerr edcd5491f1 clippy fixes 2024-11-04 13:51:36 +01:00
muellerr b4cb034b73 Merge pull request 'Extract CFDP' (#202) from extract-cfdp into main
Reviewed-on: #202
2024-11-04 12:19:32 +01:00
muellerr 47c86aea5c Extracted CFDP components 2024-11-04 12:18:41 +01:00
muellerr c8bba48e76 Merge pull request 'bump spacepackets' (#204) from satrs-shared-spacepackets-bump into main
Reviewed-on: #204
2024-11-04 12:07:12 +01:00
muellerr 176da4838a bump spacepackets 2024-11-04 12:06:10 +01:00
muellerr 8114195bc6 Merge pull request 'satrs-shared v0.2.0' (#203) from bump-satrs-shared into main
Reviewed-on: #203
2024-11-04 11:51:20 +01:00
muellerr 1726e34fa7 satrs-shared v0.2.0 2024-11-04 11:50:17 +01:00
muellerr 1f2d6c9474 Merge pull request 'another small documentation fix' (#199) from another-small-doc-fix into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #199
2024-06-03 16:09:06 +02:00
muellerr 9960930339 another small documentation fix
Rust/sat-rs/pipeline/head Build queued...
2024-06-03 16:08:38 +02:00
muellerr cb270964a1 Merge pull request 'small README fix' (#198) from small-readme-fix into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #198
2024-06-03 16:02:33 +02:00
muellerr a45e634214 small README fix
Rust/sat-rs/pipeline/head Build started...
2024-06-03 15:52:57 +02:00
muellerr 9e4132706c Merge pull request 'update README and book' (#197) from readme-and-book-updates into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #197
2024-06-03 15:32:24 +02:00
muellerr 36d889a504 update README and book
Rust/sat-rs/pipeline/head Build queued...
2024-06-03 15:31:43 +02:00
muellerr 97a6510af7 Merge pull request 'Integrate Simulator into Example' (#185) from sim-mgm-update into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #185
2024-06-03 15:25:08 +02:00
muellerr 2e5d6a5c41 update example changelog
Rust/sat-rs/pipeline/pr-main Build started...
2024-06-03 15:22:27 +02:00
muellerr 29167736db Integration of the mini simulator into the sat-rs example
Rust/sat-rs/pipeline/pr-main Build started...
2024-06-03 15:18:23 +02:00
muellerr a4c433a7be Merge pull request 'fix sat-rs build by using released asynchronix build' (#196) from fix-minisim-deps into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #196
2024-06-02 00:10:19 +02:00
muellerr 472a8ce0f9 fix sat-rs build by using released asynchronix build
Rust/sat-rs/pipeline/head Build queued...
2024-06-02 00:09:08 +02:00
lkoester e753319dac Merge pull request 'Airbus BA ins Readme' (#195) from lkoester-patch-1 into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #195
Reviewed-by: Robin Müller <muellerr@irs.uni-stuttgart.de>
2024-05-29 10:11:42 +02:00
lkoester 99dddf36f3 Airbus BA ins Readme
Rust/sat-rs/pipeline/head There was a failure building this commit
Passt das in Flight Heritage? Ist ja nie geflogen aber heritage passt schon
2024-05-29 09:50:55 +02:00
muellerr a819feeaa2 Merge pull request 'Update embedded examples' (#194) from update-embedded-examples into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #194
2024-05-25 13:07:29 +02:00
muellerr 46ce3fc772 update folder name
Rust/sat-rs/pipeline/pr-main Build started...
2024-05-25 12:35:26 +02:00
muellerr 8d27bdf3bf Update embedded examples
Rust/sat-rs/pipeline/head Build queued...
2024-05-25 12:32:46 +02:00
muellerr 3d2a46f044 Merge pull request 'flight heritage segment in docs' (#193) from add-flight-heritage-docs into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #193
2024-05-22 19:13:18 +02:00
muellerr 1f192af262 improve phrasing and links again 2024-05-22 19:11:43 +02:00
muellerr 3f78c200ad improve phrasing
Rust/sat-rs/pipeline/pr-main Build queued...
2024-05-22 19:09:43 +02:00
muellerr d73dfcdd67 flight heritage segment in docs
Rust/sat-rs/pipeline/head Build started...
2024-05-22 19:05:05 +02:00
muellerr 5cae0f7036 Merge pull request 'Heapless memory pool' (#191) from heapless-mem-pool into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #191
2024-05-22 13:16:29 +02:00
muellerr 832250d211 minor improvements
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-22 12:55:50 +02:00
muellerr 3c3b4349e8 that should do the job
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-21 19:48:56 +02:00
muellerr acf73e93b1 Introduce heapless memory pools
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-21 18:31:19 +02:00
muellerr 0b2d4f6187 Merge pull request 'satrs v0.2.1' (#190) from prep-satrs-v0.2.1 into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #190
2024-05-19 13:19:23 +02:00
muellerr f7016b940a changelog
Rust/sat-rs/pipeline/head There was a failure building this commit
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-05-19 08:14:31 +02:00
muellerr 397ecd0c40 prep patch release 2024-05-19 08:13:32 +02:00
muellerr 422f2c11ab Merge pull request 'removed unsafe block which is not necessary' (#189) from remove-unnecessary-unsafe-block into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #189
2024-05-18 12:46:58 +02:00
muellerr 37e945fd91 Merge branch 'main' into remove-unnecessary-unsafe-block 2024-05-18 12:46:44 +02:00
muellerr 45379858f0 Merge pull request 'TCP server config default improvements' (#187) from tcp-server-cfg-improvements into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #187
2024-05-18 12:44:53 +02:00
muellerr 7c194ab543 Merge branch 'main' into tcp-server-cfg-improvements
Rust/sat-rs/pipeline/pr-main Build queued...
2024-05-18 12:44:42 +02:00
muellerr bca1d7292a removed unsafe block which is not necessary
Rust/sat-rs/pipeline/head This commit looks good
2024-05-13 17:01:26 +02:00
muellerr cdcb9cae1c Merge pull request 'cross ref docs for events' (#188) from cross-ref-docs-for-events into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #188
2024-05-13 15:57:00 +02:00
muellerr 9dcbd42862 cross ref docs
Rust/sat-rs/pipeline/head This commit looks good
2024-05-13 15:36:09 +02:00
muellerr da05efc16d TCP server config default improvements
Rust/sat-rs/pipeline/head Build started...
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-13 15:31:51 +02:00
muellerr e38e25a998 Merge pull request 'update the satrs example graph' (#186) from satrs-example-graph-update into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #186
2024-05-13 15:29:39 +02:00
muellerr 14b381cf4a update the satrs example graph
Rust/sat-rs/pipeline/head This commit looks good
2024-05-10 17:03:46 +02:00
muellerr 3746e9ebb0 Merge pull request 'Add timestamp to SimRequest' (#140) from add-timestamp-to-sim-request into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #140
2024-05-08 14:58:13 +02:00
muellerr d2fc783562 Merge remote-tracking branch 'origin/main' into add-timestamp-to-sim-request
Rust/sat-rs/pipeline/pr-main Build queued...
2024-05-08 14:57:12 +02:00
muellerr 282f799203 Merge pull request 'prep v0.2.0' (#184) from prep_v0.2.0 into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #184
2024-05-02 14:57:14 +02:00
muellerr 46dbb4309b new clippy check
Rust/sat-rs/pipeline/pr-main Build started...
2024-05-02 14:44:22 +02:00
muellerr 42d1257e83 prepare next release v0.2.0
Rust/sat-rs/pipeline/pr-main Build started...
2024-05-02 14:39:30 +02:00
muellerr 583f6ce4d2 Merge pull request 'small robustness fix' (#183) from robustness-fix into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #183
2024-05-02 13:41:55 +02:00
muellerr 408803fe86 small robustness fix
Rust/sat-rs/pipeline/head Build queued...
2024-05-02 13:41:27 +02:00
muellerr 9ffe4d0ae0 Merge pull request 'smaller improvements' (#182) from smaller-improvements into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #182
2024-05-02 12:28:39 +02:00
muellerr e37061dcf0 smaller improvements 2024-05-02 12:28:09 +02:00
muellerr 3a2ac11407 Merge pull request 'bounded the PUS stack hot loop' (#181) from pus-hot-loop-bounding into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #181
2024-05-02 12:02:02 +02:00
muellerr 23327a7786 bounded the PUS stack hot loop
Rust/sat-rs/pipeline/head Build queued...
2024-05-02 12:01:24 +02:00
muellerr 89d5a1022f Merge pull request 'optimize PUS stack code' (#180) from optimize-pus-stack-code into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #180
2024-05-02 11:59:26 +02:00
muellerr a00c843698 optimize PUS stack code
Rust/sat-rs/pipeline/head Build started...
2024-05-02 11:58:46 +02:00
muellerr c586fd7fef Merge pull request 'try unifying some direct PUS handler code' (#179) from unify-some-example-code into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #179
2024-05-02 11:29:11 +02:00
muellerr 7e78e70a17 try unifying some direct PUS handler code
Rust/sat-rs/pipeline/head This commit looks good
2024-05-02 11:14:05 +02:00
muellerr 424dfc439c Merge pull request 'simplified PUS stack' (#178) from simplify-pus-stack into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #178
2024-05-02 10:01:16 +02:00
muellerr 45eb2f1343 cargo fmt
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-05-01 21:16:26 +02:00
muellerr 736eb74e66 simplified PUS stack
Rust/sat-rs/pipeline/head Build started...
2024-05-01 21:13:08 +02:00
muellerr 29f71c2a57 Merge pull request 'Reworked generic parameter handling for PUS service 1 and 5' (#175) from rework-generic-params-for-pus into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #175
2024-04-30 15:42:05 +02:00
muellerr f0d08b65a4 Merge branch 'main' into rework-generic-params-for-pus
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-30 13:35:08 +02:00
muellerr c7a74a844c Merge pull request 'renamed thread name' (#176) from small-tweak into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #176
2024-04-30 13:31:46 +02:00
muellerr 9c60427f89 Reworked generic parameter handling for PUS service 1 and 5
Rust/sat-rs/pipeline/head Build queued...
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-30 13:29:55 +02:00
muellerr 958ab9bab6 renamed thread name
Rust/sat-rs/pipeline/head This commit looks good
2024-04-25 11:11:31 +02:00
muellerr 312849bddb Merge pull request 'More improvements for Event API' (#173) from improve-event-api into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #173
2024-04-24 19:34:33 +02:00
muellerr b0159a3ba7 prep next release candidate
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-24 19:18:45 +02:00
muellerr c477739f6d more improvements for API, tests for example event module
Rust/sat-rs/pipeline/head This commit looks good
2024-04-24 18:50:08 +02:00
muellerr b7ce039406 add optional defmt support for events
Rust/sat-rs/pipeline/head This commit looks good
2024-04-24 18:36:00 +02:00
muellerr 4736d40997 Merge pull request 'simplified event management' (#172) from simplify-event-management into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #172
2024-04-24 15:58:00 +02:00
muellerr 5ec5124ea3 Updated events modules and docs
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-24 14:30:45 +02:00
muellerr 5e43259d4f Merge branch 'main' into add-timestamp-to-sim-request
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-23 16:36:30 +02:00
muellerr bfaddd0ebb Merge pull request 'prep next release' (#171) from pre-v0.2.0-rc.4 into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #171
2024-04-23 16:32:03 +02:00
muellerr 423a068736 prep next release
Rust/sat-rs/pipeline/head This commit looks good
2024-04-23 14:55:19 +02:00
muellerr 8022af1bf2 Merge pull request 'update Python client for example' (#170) from update-example-pyclient into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #170
2024-04-23 14:52:04 +02:00
muellerr acd2260dfd update Python client for example
Rust/sat-rs/pipeline/head This commit looks good
2024-04-23 14:22:50 +02:00
muellerr e5ee698dc4 Merge pull request 'TCP server improvements' (#169) from tcp-ip-improvements into main
Rust/sat-rs/pipeline/head There was a failure building this commit
Reviewed-on: #169
2024-04-23 13:21:41 +02:00
muellerr e8907c74d4 changelog
Rust/sat-rs/pipeline/head This commit looks good
2024-04-23 11:23:00 +02:00
muellerr 536051e05b improvements and fixes
Rust/sat-rs/pipeline/head This commit looks good
2024-04-22 20:29:14 +02:00
muellerr 701db659e9 Merge pull request 'formatting' (#168) from fmt into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #168
2024-04-22 15:47:58 +02:00
muellerr 4b8e54b91b formatting
Rust/sat-rs/pipeline/head This commit looks good
Rust/sat-rs/pipeline/pr-main Build started...
2024-04-22 10:42:49 +02:00
muellerr 870d60cfd6 Merge pull request 'bugfix and improvements for CCSDS SP decoder' (#167) from ccsds-decoder-bugfix into main
Rust/sat-rs/pipeline/head There was a failure building this commit
Reviewed-on: #167
2024-04-22 10:23:12 +02:00
muellerr 9e62e4292c bugfix and improvements for CCSDS SP decoder
Rust/sat-rs/pipeline/pr-main Build started...
2024-04-20 11:19:46 +02:00
muellerr b2e77fbc09 Merge pull request 'requires another hotfix' (#166) from and-another-docs-rs-hotfix into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #166
2024-04-17 20:42:09 +02:00
muellerr 5371928496 docs_rs build argument hotfix
Rust/sat-rs/pipeline/pr-main Build queued...
2024-04-17 20:41:30 +02:00
muellerr 31cddbd99b Merge pull request 'bump msrv version' (#165) from bump-msrv into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #165
2024-04-17 18:56:21 +02:00
muellerr 7c00e13e70 bump msrv version
Rust/sat-rs/pipeline/head This commit looks good
2024-04-17 18:10:32 +02:00
muellerr aa72063454 Merge pull request 'prepare next release candidate' (#164) from prep-v0.2.0-rc.2 into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #164
2024-04-17 18:03:28 +02:00
muellerr 7b37b76695 prepare next release candidate
Rust/sat-rs/pipeline/head This commit looks good
2024-04-17 17:19:38 +02:00
muellerr ea5d95c12d Merge pull request 'why is this an issue for docs-rs?' (#163) from fix-for-docs-build-docs-rs into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #163
2024-04-17 17:09:36 +02:00
muellerr c62adbb300 Merge branch 'main' into fix-for-docs-build-docs-rs
Rust/sat-rs/pipeline/pr-main Build started...
2024-04-17 16:41:45 +02:00
muellerr 9242b8a607 Merge pull request 'prepare MIB release' (#162) from prepare-mib-release into main
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #162
2024-04-17 16:37:34 +02:00
muellerr 4a27d2605d why is this an issue for docs-rs?
Rust/sat-rs/pipeline/head Build queued...
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-17 16:34:56 +02:00
muellerr 8195245481 prepare MIB release
Rust/sat-rs/pipeline/head Build started...
Rust/sat-rs/pipeline/pr-main Build queued...
2024-04-17 16:17:30 +02:00
muellerr 975cd927f4 Merge branch 'main' into add-timestamp-to-sim-request
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-04-09 17:27:57 +02:00
muellerr 9039c1b59a Merge branch 'main' into add-timestamp-to-sim-request
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-03-25 16:14:04 +01:00
muellerr 972bf19188 cargo fmt
Rust/sat-rs/pipeline/head Build started...
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-03-13 12:03:11 +01:00
muellerr 9d711d2b73 add fern logging
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-03-13 10:49:24 +01:00
muellerr d0005cdd63 this works
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-03-13 10:36:08 +01:00
muellerr f00e6cf50c we require an asynchronix update here I guess
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-03-12 18:25:21 +01:00
300 changed files with 35892 additions and 55647 deletions

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
name: ci
on: [push, pull_request]
jobs:
check:
name: Check build
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install libudev-dev on Ubuntu
if: ${{ matrix.os == 'ubuntu-latest' }}
run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo check
# Check example with static pool configuration
- run: cargo check -p example-std --no-default-features
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo nextest run --all-features
- run: cargo test --doc --all-features
cross-check:
name: Check Cross-Compilation
runs-on: ubuntu-latest
strategy:
matrix:
target:
- armv7-unknown-linux-gnueabihf
- thumbv7em-none-eabihf
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
- run: cargo check -p satrs --target=${{matrix.target}} --no-default-features
fmt:
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check
docs:
name: Check Documentation Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo clippy -- -D warnings
+1
View File
@@ -2,6 +2,7 @@ target/
output.log
/Cargo.lock
output.log
output.log
-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>
+7 -4
View File
@@ -3,12 +3,15 @@ resolver = "2"
members = [
"satrs",
"satrs-mib",
"satrs-example",
"satrs-minisim",
"examples/example-std",
"examples/types",
"examples/client",
"examples/minisim",
"examples/minisim-types",
"satrs-shared",
"tmtc-utils",
]
exclude = [
"satrs-example-stm32f3-disco",
"examples/embedded",
]
+46 -15
View File
@@ -1,9 +1,9 @@
<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://absatsw.irs.uni-stuttgart.de/projects/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)
sat-rs
=========
@@ -11,8 +11,8 @@ sat-rs
This is the repository of the sat-rs library. Its primary goal is to provide re-usable components
to write on-board software for remote systems like rovers or satellites. It is specifically written
for the special requirements for these systems. You can find an overview of the project and the
link to the [more high-level sat-rs book](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS software projects website](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
link to the [more high-level sat-rs book](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/)
at the [IRS software projects website](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/).
This is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
@@ -26,33 +26,64 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
# Overview
This project currently contains following crates:
This project currently contains the following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
Primary information resource in addition to the API documentation, hosted
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/). It can be useful to read
this first before delving into the example application and the API documentation.
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
Primary crate.
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
Example of a simple example on-board software using various sat-rs components which can be run
on a host computer or on any system with a standard runtime like a Raspberry Pi.
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
Components to build a mission information base from the on-board software directly.
* [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example-stm32f3-disco):
Example of a simple example using low-level sat-rs components on a bare-metal system
with constrained resources.
Each project has its own `CHANGELOG.md`.
## Examples
All examples and their helper crates are located inside the
[`examples`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples) folder:
* [`example-std`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std):
Example on-board software using various sat-rs components which can be run on a host computer
or on any system with a standard runtime like a Raspberry Pi.
* [`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim):
Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which
simulates some physical devices for the `example-std` application device handlers.
* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application and the STM32H7 examples.
* [`types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/types): Telecommand and telemetry definitions shared by the
`example-std` application, the STM32H7 examples and the `client`.
* [`embedded`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded):
Simple examples using sat-rs components on a bare-metal system with constrained resources,
running on the NUCLEO-H753ZI board. `stm32h7-nucleo-rtic` uses the
[RTIC](https://github.com/rtic-rs/rtic) framework and `stm32h7-nucleo-embassy` uses the
[embassy](https://embassy.dev/) executor.
The library crates and the `example-std` application have their own `CHANGELOG.md`.
# Related projects
In addition to the crates in this repository, the sat-rs project also maintains other libraries.
* [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS
packet protocol implementations. This repository is re-exported in the
[`satrs`](https://egit.irs.uni-stuttgart.de/rust/satrs/src/branch/main/satrs)
crate.
* [`cfdp`](https://egit.irs.uni-stuttgart.de/rust/cfdp): CCSDS File Delivery Protocol
(CFDP) high-level library components.
# Flight Heritage
There is an active and continuous effort to get early flight heritage for the sat-rs library.
Currently this library has the following flight heritage:
- Submission as an [OPS-SAT experiment](https://www.esa.int/Enabling_Support/Operations/OPS-SAT)
which has also
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
The application is strongly based on the sat-rs example application. You can find the repository
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
alongside a Flight System Simulator in the context of a
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
# Coverage
@@ -64,5 +95,5 @@ rustup component add llvm-tools-preview
cargo install grcov --locked
```
After that, you can simply run `coverage.py` to test the `satrs-core` crate with coverage. You can
After that, you can simply run `coverage.py` to test the `satrs` crate with coverage. You can
optionally supply the `--open` flag to open the coverage report in your webbrowser.
+1
View File
@@ -33,6 +33,7 @@ pipeline {
stage('Test') {
steps {
sh 'cargo nextest r --all-features'
sh 'cargo test --doc --all-features'
}
}
stage('Check with all features') {
+1 -1
View File
@@ -47,7 +47,7 @@ def main():
parser.add_argument(
"-p",
"--package",
choices=["satrs", "satrs-minisim", "satrs-example"],
choices=["satrs", "minisim", "example-std"],
default="satrs",
help="Choose project to generate coverage for",
)
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
cargo +nightly doc --all-features --open
+1
View File
@@ -0,0 +1 @@
/config.toml
+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", features = ["derive"] }
toml = "1"
satrs = { path = "../../satrs" }
example-std = { path = "../example-std" }
minisim-types = { path = "../minisim-types" }
types = { path = "../types" }
spacepackets = { version = "0.18", default-features = false }
bitbybit = "2"
arbitrary-int = "2"
ctrlc = { version = "3.5" }
postcard = { version = "1", features = ["alloc"] }
anyhow = "1"
+13
View File
@@ -0,0 +1,13 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let config = manifest_dir.join("config.toml");
let config_template = manifest_dir.join("config.toml.template");
if !config.exists() {
fs::copy(&config_template, &config).unwrap();
}
println!("cargo::rerun-if-changed=config.toml.template");
}
+7
View File
@@ -0,0 +1,7 @@
# Copied to config.toml by the build script if config.toml does not exist yet. config.toml is not
# tracked by git, so it can hold local settings like the address of a development board.
[interface]
# Address of the commanded application. Defaults to the example-std OBSW on the local host.
# Can be overridden with the --udp-addr argument.
# udp_addr = "192.168.1.50:7301"
+945
View File
@@ -0,0 +1,945 @@
use anyhow::{Context as _, bail};
use arbitrary_int::u11;
use clap::Parser as _;
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
udp::SIM_CTRL_PORT,
};
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, SystemTime},
};
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
#[derive(clap::Parser)]
pub struct Cli {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
test_event: bool,
/// Address of the commanded application. Overrides the address inside `config.toml`.
#[arg(long, global = true)]
udp_addr: Option<SocketAddr>,
#[command(subcommand)]
commands: Option<Commands>,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Config {
#[serde(default)]
interface: InterfaceConfig,
}
#[derive(Debug, Default, serde::Deserialize)]
struct InterfaceConfig {
/// Defaults to the example-std OBSW on the local host.
udp_addr: Option<SocketAddr>,
}
impl Config {
/// The build script creates `config.toml` from the template. A missing file is still accepted,
/// because all parameters have defaults.
fn load() -> anyhow::Result<Self> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml");
match std::fs::read_to_string(&path) {
Ok(content) => toml::from_str(&content)
.with_context(|| format!("parsing {} failed", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(e) => Err(e).with_context(|| format!("reading {} failed", path.display())),
}
}
}
#[derive(clap::Subcommand)]
enum Commands {
Mgm0(MgmArgs),
Mgm1(MgmArgs),
MgmAssy(MgmAssemblyArgs),
Mgt(MgtArgs),
AcsSubsystem(SubsystemArgs),
EventManager(EventManagerArgs),
/// Blinking LEDs of the embedded examples.
Led(LedArgs),
Sim(SimArgs),
}
#[derive(clap::Parser)]
struct EventManagerArgs {
#[command(subcommand)]
action: EventFilterAction,
}
#[derive(clap::Parser)]
struct SimArgs {
#[command(subcommand)]
action: SimAction,
}
#[derive(clap::Subcommand)]
enum SimAction {
/// Let the OBSW connect to the minisim.
Connect {
/// IP address of the minisim host. Defaults to the address of this client if nothing
/// is specified.
ip: Option<Ipv4Addr>,
},
}
#[derive(clap::Subcommand)]
enum EventFilterAction {
/// Enable event TM generation.
Enable(EventFilterArgs),
/// Disable event TM generation.
Disable(EventFilterArgs),
}
#[derive(clap::Args)]
struct EventFilterArgs {
#[arg(value_enum)]
component: EventSenderSelect,
/// Raw event ID. Without it, the filter applies to all events of the component.
#[arg(short, long)]
event_id: Option<u16>,
}
/// Components which emit events.
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum EventSenderSelect {
Controller,
Mgm0,
Mgm1,
MgmAssy,
Mgt,
Pcdu,
UdpServer,
TcpServer,
Ground,
}
impl From<EventSenderSelect> for types::ComponentId {
fn from(sender: EventSenderSelect) -> Self {
match sender {
EventSenderSelect::Controller => types::ComponentId::Controller,
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
EventSenderSelect::Ground => types::ComponentId::Ground,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultMode {
None,
/// SPI communication is all zeroes, modelling an unconnected sensor.
AllZeros,
/// SPI communication is all ones, modelling a broken sensor.
AllOnes,
}
impl From<FaultMode> for mgm::SpiFaultMode {
fn from(mode: FaultMode) -> Self {
match mode {
FaultMode::None => mgm::SpiFaultMode::None,
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum FaultKind {
/// Cleared when the device is switched off, so a power cycle recovers from it.
Transient,
/// Survives power cycles.
#[default]
Permanent,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HkSelect {
OneShot,
EnablePeriodic,
DisablePeriodic,
ModifyInterval,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum HealthStateSelect {
Healthy,
Faulty,
PermanentFaulty,
ExternalControl,
NeedsRecovery,
}
impl From<HealthStateSelect> for satrs::health::HealthState {
fn from(state: HealthStateSelect) -> Self {
match state {
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the sensor data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
#[arg(long, value_enum)]
fault: Option<FaultMode>,
/// Whether a power cycle clears the injected SPI fault.
#[arg(long, value_enum, default_value_t)]
fault_kind: FaultKind,
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
/// the handler after the underlying issue has been fixed or worked around.
#[arg(long, value_enum)]
health: Option<HealthStateSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgtArgs {
#[arg(short, long)]
ping: bool,
/// Housekeeping request for the status data set.
#[arg(long, value_enum)]
hk: Option<HkSelect>,
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
#[arg(long)]
hk_interval_ms: Option<u64>,
#[arg(short, long)]
mode: Option<DeviceModeSelect>,
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
torque: Option<types::acs::mgt::Dipole>,
#[arg(long, default_value_t = 1000)]
torque_duration_ms: u64,
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
#[arg(long, value_enum)]
health: Option<HealthStateSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct LedArgs {
#[arg(short, long)]
ping: bool,
/// Mode of the red and the orange LED.
#[arg(short, long, value_enum)]
mode: Option<LedModeSelect>,
/// Toggle period of the toggle modes.
#[arg(long, default_value_t = 500)]
toggle_period_ms: u64,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
enum LedModeSelect {
AllOff,
RedOn,
OrangeOn,
AlternatingToggle,
UnifiedToggle,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct MgmAssemblyArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<AssemblyModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
struct SubsystemArgs {
#[arg(short, long)]
ping: bool,
#[arg(short, long)]
mode: Option<SubsystemModeSelect>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum DeviceModeSelect {
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum AssemblyModeSelect {
NoModeKeeping,
Off,
Normal,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
pub enum SubsystemModeSelect {
Off,
Safe,
}
fn hk_request_type(
hk: HkSelect,
hk_interval_ms: Option<u64>,
) -> anyhow::Result<types::HkRequestType> {
let opt_interval = hk_interval_ms.map(Duration::from_millis);
Ok(match hk {
HkSelect::OneShot => types::HkRequestType::OneShot,
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
),
})
}
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
let axes: Vec<i16> = value
.split(',')
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
.collect::<Result<_, _>>()?;
let [x, y, z] = axes[..] else {
return Err(format!("expected 3 values, got {}", axes.len()));
};
Ok(types::acs::mgt::Dipole { x, y, z })
}
fn send_mgt_request(
client: &UdpSocket,
addr: SocketAddr,
request: types::acs::mgt::request::Request,
) {
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending MGT request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::request::Request) {
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Led, request.message_type()),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending LED request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_sim_command(client: &UdpSocket, addr: SocketAddr, args: SimArgs) {
let request = match args.action {
SimAction::Connect { ip } => types::control::request::Request::SimConnect(ip),
};
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, MessageType::Action),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending SIM request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) {
use types::led::request::Request;
if args.ping {
send_led_request(client, addr, Request::Ping);
}
if let Some(mode) = args.mode {
let toggle_period = Duration::from_millis(args.toggle_period_ms);
let mode = match mode {
LedModeSelect::AllOff => types::led::Mode::AllOff,
LedModeSelect::RedOn => types::led::Mode::RedOn,
LedModeSelect::OrangeOn => types::led::Mode::OrangeOn,
LedModeSelect::AlternatingToggle => types::led::Mode::AlternatingToggle(toggle_period),
LedModeSelect::UnifiedToggle => types::led::Mode::UnifiedToggle(toggle_period),
};
send_led_request(client, addr, Request::SetMode(mode));
}
}
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
use types::acs::mgt::request::{ModeRequest, Request};
if args.ping {
send_mgt_request(client, addr, Request::Ping);
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
send_mgt_request(client, addr, Request::Hk(req_type));
}
if let Some(mode) = args.mode {
let mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
}
if let Some(dipole) = args.torque {
let request = Request::ApplyTorque {
dipole,
duration: Duration::from_millis(args.torque_duration_ms),
};
send_mgt_request(client, addr, request);
}
if let Some(health) = args.health {
send_mgt_request(
client,
addr,
Request::Health(types::HealthRequest::SetHealth(health.into())),
);
}
Ok(())
}
fn handle_mgm_command(
client: &UdpSocket,
addr: SocketAddr,
target_id: types::ComponentId,
args: MgmArgs,
) -> anyhow::Result<()> {
if let Some(mode) = args.fault {
inject_mgm_failure(
target_id,
mgm::SpiFault {
mode: mode.into(),
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
},
)?;
}
if args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(hk) = args.hk {
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Hk),
types::acs::mgm::request::Request::Hk(HkRequest {
id: types::acs::mgm::request::HkId::Sensor,
req_type,
}),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = args.mode {
let dev_mode = match mode {
DeviceModeSelect::Off => types::DeviceMode::Off,
DeviceModeSelect::Normal => types::DeviceMode::Normal,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm::request::Request::Mode(
types::acs::mgm::request::ModeRequest::SetMode(dev_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(health) = args.health {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Health),
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
health.into(),
)),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} set-health request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
Ok(())
}
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
use types::event_manager::request::Request;
let request = match args.action {
EventFilterAction::Enable(filter) => match filter.event_id {
Some(event_id) => Request::EnableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::EnableComponent(filter.component.into()),
},
EventFilterAction::Disable(filter) => match filter.event_id {
Some(event_id) => Request::DisableEvent {
sender_id: filter.component.into(),
event_id,
},
None => Request::DisableComponent(filter.component.into()),
},
};
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
log::info!(
"sending event manager request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&request_packet.to_vec(), addr).unwrap();
}
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(level)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
fn main() -> anyhow::Result<()> {
setup_logger(log::LevelFilter::Debug).unwrap();
let kill_signal = Arc::new(AtomicBool::new(false));
let ctrl_kill_signal = kill_signal.clone();
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
let cli = Cli::parse();
let config = Config::load()?;
let addr = cli
.udp_addr
.or(config.interface.udp_addr)
.unwrap_or(SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT));
// Bind to all interfaces, so embedded targets inside the local network can be reached too.
let client = UdpSocket::bind("0.0.0.0:7302").expect("Connecting to UDP server failed");
client.set_nonblocking(true)?;
client.set_read_timeout(Some(Duration::from_millis(200)))?;
if cli.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
types::control::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if cli.test_event {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
types::control::request::Request::TestEvent,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending event request with TC ID {:#010x}",
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(cmd) = cli.commands {
match cmd {
Commands::Mgm0(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
}
Commands::Mgm1(args) => {
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
}
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
Commands::MgmAssy(mgm_assembly_args) => {
let target_id = types::ComponentId::AcsMgmAssembly;
if mgm_assembly_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::mgm::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = mgm_assembly_args.mode {
let assembly_mode = match mode {
AssemblyModeSelect::NoModeKeeping => {
types::acs::mgm_assembly::Mode::NoModeKeeping
}
AssemblyModeSelect::Off => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
}
AssemblyModeSelect::Normal => {
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
}
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::mgm_assembly::request::Request::Mode(
types::acs::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} HK request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::AcsSubsystem(subsystem_args) => {
let target_id = types::ComponentId::AcsSubsystem;
if subsystem_args.ping {
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Ping),
types::acs::subsystem::request::Request::Ping,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} ping request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
if let Some(mode) = subsystem_args.mode {
let subsystem_mode = match mode {
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
};
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Mode),
types::acs::subsystem::request::Request::Mode(
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
"sending {:?} mode request with TC ID {:#010x}",
target_id,
sent_tc_id.raw()
);
let request_packet = request.to_vec();
client.send_to(&request_packet, addr).unwrap();
}
}
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
Commands::Led(args) => handle_led_command(&client, addr, args),
Commands::Sim(args) => handle_sim_command(&client, addr, args),
}
}
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
log::info!("entering listening loop");
loop {
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
log::info!("received kill signal, exiting");
break;
}
match client.recv(recv_buf.as_mut_slice()) {
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
continue;
}
log::warn!("UDP reception error: {}", e)
}
}
}
Ok(())
}
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
///
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
/// UDP send would otherwise silently do nothing if minisim is not running.
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
let mut reply_buf = [0u8; 4096];
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
match sim_socket.recv(&mut reply_buf) {
Ok(len) => {
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
}
}
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
}
Err(e) => return Err(e.into()),
}
let id = match target_id {
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
};
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
id,
request: mgm::Request::SetSpiFault(fault),
});
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
Ok(())
}
/// Each component has its own event type, so the sender ID determines how to decode the event.
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
sender_id: types::ComponentId,
data: &[u8],
) {
match postcard::from_bytes::<E>(data) {
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
}
}
match sender_id {
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
log_event::<types::acs::mgm::Event>(sender_id, data)
}
types::ComponentId::AcsMgmAssembly => {
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
}
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
// TC source events are sent with the ID of the packet source.
types::ComponentId::UdpServer
| types::ComponentId::TcpServer
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
_ => log::warn!(
"Received event from {:?} with unknown event type",
sender_id
),
}
}
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
Ok(packet) => {
let tm_header_result = postcard::take_from_bytes::<types::TmHeader>(packet.user_data());
if let Err(e) = tm_header_result {
bail!("Failed to deserialize TM header: {}", e);
}
let (tm_header, remainder) = tm_header_result.unwrap();
if let Some(tc_id) = tm_header.tc_id {
log::info!(
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
packet.apid(),
tm_header.sender_id,
tc_id.raw()
);
}
if tm_header.message_type == MessageType::Event {
handle_event(tm_header.sender_id, remainder);
return Ok(());
}
match tm_header.sender_id {
types::ComponentId::EpsPcdu => {
let response =
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
log::info!("Received response from PCDU: {:?}", response.unwrap());
}
types::ComponentId::Controller => {
let response =
postcard::from_bytes::<types::control::response::Response>(remainder);
log::info!("Received response from controller: {:?}", response.unwrap());
}
types::ComponentId::AcsMgmAssembly => {
let response = postcard::from_bytes::<
types::acs::mgm_assembly::response::Response,
>(remainder);
log::info!(
"Received response from MGM Assembly: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsMgm0 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM0: {:?}", response.unwrap());
}
types::ComponentId::AcsMgm1 => {
let response =
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
log::info!("Received response from MGM1: {:?}", response.unwrap());
}
types::ComponentId::AcsSubsystem => {
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
remainder,
);
log::info!(
"Received response from ACS subsystem: {:?}",
response.unwrap()
);
}
types::ComponentId::EpsSubsystem => todo!(),
types::ComponentId::UdpServer => todo!(),
types::ComponentId::TcpServer => todo!(),
types::ComponentId::Ground => todo!(),
types::ComponentId::EventManager => {
let response =
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
log::info!(
"Received response from event manager: {:?}",
response.unwrap()
);
}
types::ComponentId::AcsController => todo!(),
types::ComponentId::AcsMgt => {
let response =
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
log::info!("Received response from MGT: {:?}", response.unwrap());
}
types::ComponentId::Led => {
let response =
postcard::from_bytes::<types::led::response::Response>(remainder);
log::info!("Received response from LED: {:?}", response.unwrap());
}
}
}
Err(_) => todo!(),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dipole() {
assert_eq!(
parse_dipole("-200, 200,1000"),
Ok(types::acs::mgt::Dipole {
x: -200,
y: 200,
z: 1000
})
);
assert!(parse_dipole("1,2").is_err());
assert!(parse_dipole("1,2,3,4").is_err());
}
#[test]
fn test_config_template_is_valid() {
let template = include_str!("../config.toml.template");
let config: Config = toml::from_str(template).unwrap();
assert_eq!(config.interface.udp_addr, None);
let config: Config =
toml::from_str("[interface]\nudp_addr = \"192.168.1.50:7301\"").unwrap();
assert_eq!(
config.interface.udp_addr,
Some("192.168.1.50:7301".parse().unwrap())
);
}
#[test]
fn test_negative_torque_argument() {
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
let Some(Commands::Mgt(args)) = cli.commands else {
panic!("expected mgt subcommand");
};
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
}
}
@@ -0,0 +1,32 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H753ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
# IP address of the host running the minisim. Without it, the firmware does not try to connect
# to a simulator.
# SIM_IP_ADDR = "192.168.1.10"
+3
View File
@@ -0,0 +1,3 @@
/.cargo/config.toml
.vscode/
app.map
+1740
View File
File diff suppressed because it is too large. Load diff
+71
View File
@@ -0,0 +1,71 @@
[workspace]
resolver = "3"
members = [
"shared-embedded",
"stm32h7-nucleo-embassy",
"stm32h7-nucleo-rtic",
]
[workspace.dependencies]
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
shared-embedded = { path = "shared-embedded" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
arbitrary-int = "2"
defmt = "1"
defmt-rtt = "1"
defmt-test = "0.5"
panic-probe = { version = "1", features = ["print-defmt"] }
embedded-alloc = "0.7"
static_cell = "2"
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
postcard = "1"
serde = { version = "1", default-features = false }
rtic = { version = "2", features = ["thumbv7-backend"] }
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "defmt"] }
embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] }
embassy-time = "0.5"
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
embassy-sync = "0.8"
embassy-futures = "0.1"
# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 's' # <-
overflow-checks = true # <-
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 3 # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
+153
View File
@@ -0,0 +1,153 @@
sat-rs examples for the STM32H753ZI-Nucleo board
=======
These example applications show how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
They also show how a relatively simple OBSW could be built when no standard runtime is available.
Both use the [defmt](https://defmt.ferrous-systems.com/) framework for logging and provide the
same functionality with a different concurrency framework:
- [`stm32h7-nucleo-embassy`](./stm32h7-nucleo-embassy) uses the [embassy](https://embassy.dev/)
executor.
- [`stm32h7-nucleo-rtic`](./stm32h7-nucleo-rtic) uses [RTIC](https://rtic.rs/2/book/en/).
The application code is shared inside the [`shared-embedded`](./shared-embedded) crate.
Each application only initializes the hardware and wraps the shared code inside its own tasks.
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
devices. It has more RAM available and allows commanding via Ethernet. The examples are written
for the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
All crates in this directory form a separate workspace, because they are built for a different
target than the rest of the repository. They share one `.cargo` config file. A default is
provided as `.cargo/config.toml.template`. The build scripts copy it to `.cargo/config.toml`
if that file does not exist yet. The copy is not tracked by git, so you can change settings like
the runner for your setup.
Cargo reads the configuration before the build script runs, so the very first build on a fresh
checkout does not use it yet and might fail. Simply run the build again, or copy the file
manually beforehand:
```sh
cp .cargo/config.toml.template .cargo/config.toml
```
The configuration file also sets the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can build all applications from this directory with
```sh
cargo build
```
or a single application with `cargo build -p stm32h7-nucleo-embassy`, for example.
## Flashing from the command line
The configuration file sets `probe-rs` as the runner, so you can flash and run an application
with
```sh
cargo run -p stm32h7-nucleo-embassy
```
## Debugging with VS Code
The Nucleo board comes with an on-board ST-Link so all that is required to flash and debug
the board is a USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
## Commanding the board
The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be
connected to a network with a DHCP server. The network configuration including the IP address is
logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72
must be ON when using Ethernet. The board uses the same TMTC protocol as the
[`example-std`](../example-std) application, which is defined inside the [`types`](../types)
crate.
The [`client`](../client) application is used to command the board. Set the address of the board
inside `client/config.toml`, which is created from `client/config.toml.template` on the first
build:
```toml
[interface]
udp_addr = "192.168.1.50:7301"
```
For example, you can then send a ping to the MCU using
```sh
cargo run -p client -- --ping
```
Like the `example-std` application, the board has a controller component which handles pings and
test events. A test event can be triggered with `--test-event`.
The green LED blinks every 0.5 seconds as a heartbeat. The red and the orange LED are controlled
with a mode. For example, you can let both toggle together every 200 ms using
```sh
cargo run -p client -- led --mode unified-toggle --toggle-period-ms 200
```
Use `cargo run -p client -- led --help` to list all modes.
You can also pass the board address with `--udp-addr` instead of setting it inside the
configuration file.
## Connecting to the mini simulator
The firmware can connect to the [`minisim`](../minisim), which simulates the devices of the OBSW.
The simulator address can be passed with a sim connect request:
```sh
cargo run -p client -- sim connect
```
Without an IP address, the firmware uses the sender address of the request, which fits the
common setup where the client and the simulator run on the same host. Otherwise, pass the IP
address of the simulator host, for example `sim connect 192.168.1.10`.
The simulator address can also be set at build time inside `.cargo/config.toml`, so the firmware
connects on its own after startup:
```toml
[env]
SIM_IP_ADDR = "192.168.1.10"
```
The firmware pings the simulator on UDP port 7303 and logs the result. It reconnects after a
network link loss. A new sim connect request, for example after restarting the simulator,
triggers a new connection attempt.
Like the `example-std` application, the device handlers use dummy interfaces if no simulator
address is known or the simulator does not reply. The simulator sends its replies to the
last client which contacted it, so only one application can use it at a time.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
targets = ["thumbv7em-none-eabihf"]
@@ -0,0 +1,20 @@
[package]
name = "shared-embedded"
version = "0.1.0"
edition = "2024"
[dependencies]
types.workspace = true
minisim-types.workspace = true
arbitrary-int.workspace = true
defmt.workspace = true
spacepackets.workspace = true
postcard.workspace = true
serde.workspace = true
embassy-stm32.workspace = true
embassy-time.workspace = true
embassy-net.workspace = true
embassy-sync.workspace = true
embassy-futures.workspace = true
@@ -0,0 +1,68 @@
use embassy_futures::select::{Either, select};
use embassy_stm32::gpio;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use types::led;
pub static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500);
const DEFAULT_LED_MODE: led::Mode =
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
pub struct Leds {
pub red: gpio::Output<'static>,
pub orange: gpio::Output<'static>,
}
pub async fn heartbeat(led: &mut gpio::Output<'static>) {
loop {
led.toggle();
Timer::after(HEARTBEAT_PERIOD).await;
}
}
/// Applies the current mode to the red and orange LED. A new mode is applied immediately.
pub async fn led_task(leds: &mut Leds) {
let mut mode = DEFAULT_LED_MODE;
loop {
let toggle_period = match mode {
led::Mode::AllOff => {
leds.red.set_low();
leds.orange.set_low();
None
}
led::Mode::RedOn => {
leds.red.set_high();
leds.orange.set_low();
None
}
led::Mode::OrangeOn => {
leds.red.set_low();
leds.orange.set_high();
None
}
led::Mode::AlternatingToggle(period) => {
leds.red.toggle();
leds.orange.set_level((!leds.red.is_set_high()).into());
Some(period)
}
led::Mode::UnifiedToggle(period) => {
leds.red.toggle();
leds.orange.set_level(leds.red.is_set_high().into());
Some(period)
}
};
mode = match toggle_period {
Some(period) => {
let period = Duration::try_from(period).unwrap_or(Duration::MAX);
match select(Timer::after(period), LED_MODE.wait()).await {
Either::First(()) => mode,
Either::Second(new_mode) => new_mode,
}
}
None => LED_MODE.wait().await,
};
}
}
@@ -0,0 +1,9 @@
//! Application code shared by the STM32H7 Nucleo applications. Each application wraps the
//! async functions inside tasks of its own executor.
#![no_std]
extern crate alloc;
pub mod leds;
pub mod net;
pub mod sim_client;
pub mod tmtc;
@@ -0,0 +1,118 @@
use core::cell::Cell;
use embassy_futures::select::{Either3, select3};
use embassy_net::{
IpEndpoint,
udp::{PacketMetadata, UdpSocket},
};
use embassy_stm32::{eth, peripherals};
use embassy_sync::blocking_mutex::Mutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use crate::tmtc::{TcSender, TmReceiver};
const PORT: u16 = 7301;
const MTU: usize = 1500;
/// Locally administered MAC address
pub const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
pub static LAST_SENDER: Mutex<CriticalSectionRawMutex, Cell<Option<core::net::Ipv4Addr>>> =
Mutex::new(Cell::new(None));
pub type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
pub async fn net_stack_task(runner: &mut embassy_net::Runner<'static, Device>) -> ! {
runner.run().await
}
pub async fn udp_task(stack: embassy_net::Stack<'static>, tc_tx: TcSender, tm_rx: TmReceiver) {
// Task futures are allocated statically, so these buffers do not live on the stack.
let mut rx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut tx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut rx_udp_buf = [0; MTU];
let mut tx_udp_buf = [0; MTU];
let mut rx_buffer = [0; MTU];
loop {
stack.wait_link_up().await;
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
stack.wait_config_up().await;
defmt::info!("Network task initialized, config: {}", stack.config_v4());
let mut udp = UdpSocket::new(
stack,
&mut rx_udp_meta,
&mut rx_udp_buf,
&mut tx_udp_meta,
&mut tx_udp_buf,
);
if let Err(e) = udp.bind(PORT) {
defmt::error!("Failed to bind UDP socket: {}", e);
Timer::after_secs(1).await;
continue;
}
defmt::info!("UDP socket bound to port {}", PORT);
let mut remote_endpoint = None;
loop {
match select3(
udp.recv_from(&mut rx_buffer),
tm_rx.receive(),
stack.wait_link_down(),
)
.await
{
Either3::First(Ok((len, meta))) => {
remote_endpoint = Some(meta.endpoint);
let embassy_net::IpAddress::Ipv4(sender_ip) = meta.endpoint.addr;
LAST_SENDER.lock(|val| {
val.set(Some(sender_ip));
});
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
tc_tx.send(rx_buffer[0..len].to_vec()).await;
// Incoming TCs take priority in the select. Draining TM here prevents a burst
// of TCs from overflowing the TM queue. This could lead to a task deadlock
// where each task is waiting on each other.
while let Ok(packet) = tm_rx.try_receive() {
handle_tm(&packet, &mut udp, &remote_endpoint).await;
}
}
Either3::First(Err(e)) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
// TM is only generated as a response to a TC, so the endpoint is usually known.
Either3::Second(packet) => handle_tm(&packet, &mut udp, &remote_endpoint).await,
Either3::Third(()) => {
defmt::warn!("Network link is down");
break;
}
}
}
}
}
async fn handle_tm(
packet: &[u8],
udp_socket: &mut UdpSocket<'_>,
remote_endpoint: &Option<IpEndpoint>,
) {
match remote_endpoint {
Some(endpoint) => match udp_socket.send_to(packet, *endpoint).await {
Ok(_) => {
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
}
Err(e) => defmt::warn!("udp send error: {}", e),
},
None => defmt::warn!("dropping TM, no remote endpoint known"),
};
}
@@ -0,0 +1,116 @@
//! UDP client for the minisim, which simulates the devices of the OBSW.
use core::net::{Ipv4Addr, SocketAddrV4};
use defmt::Debug2Format;
use embassy_futures::select::{Either, select};
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_sync::watch::Watch;
use embassy_time::{Duration, WithTimeout as _};
use minisim_types::udp::SIM_CTRL_PORT;
use minisim_types::{SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime};
/// Set by a sim connect request. Each new address triggers a connection attempt.
pub static SIM_HOST: Signal<CriticalSectionRawMutex, Ipv4Addr> = Signal::new();
/// IP address of the host running the minisim, set at build time. Without it, no connection
/// to a simulator is attempted until a sim connect request is received.
const SIM_FROM_ENV: Option<&str> = option_env!("SIM_IP_ADDR");
const PING_ATTEMPTS: usize = 3;
const PING_TIMEOUT: Duration = Duration::from_millis(500);
const MAX_PACKET_LEN: usize = 1024;
/// Result of the connection check. Device handlers use it to pick either the simulator or a
/// dummy interface.
pub static SIM_AVAILABLE: Watch<CriticalSectionRawMutex, bool, 4> = Watch::new();
pub async fn sim_client_task(stack: embassy_net::Stack<'static>) {
let mut rx_meta = [PacketMetadata::EMPTY; 4];
let mut tx_meta = [PacketMetadata::EMPTY; 4];
let mut rx_buf = [0; MAX_PACKET_LEN];
let mut tx_buf = [0; MAX_PACKET_LEN];
let sim_available = SIM_AVAILABLE.sender();
sim_available.send(false);
// Bound to an ephemeral port without a fixed local address, so the socket survives a new
// DHCP lease and does not need to be recreated after a link loss.
let mut udp = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf);
if let Err(e) = udp.bind(0) {
defmt::error!("Failed to bind simulator UDP socket: {}", e);
return;
}
let mut sim_addr = sim_addr_from_env();
loop {
stack.wait_link_up().await;
stack.wait_config_up().await;
loop {
if let Some(addr) = sim_addr {
let connected = check_connection(&udp, addr).await;
if connected {
defmt::info!("Connected to simulator at {}", Debug2Format(&addr));
} else {
defmt::warn!(
"Simulator at {} not reachable, using dummy interfaces",
Debug2Format(&addr)
);
}
sim_available.send(connected);
}
match select(SIM_HOST.wait(), stack.wait_link_down()).await {
Either::First(ip_addr) => {
sim_addr = Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT));
}
Either::Second(()) => {
sim_available.send(false);
break;
}
}
}
}
}
fn sim_addr_from_env() -> Option<SocketAddrV4> {
let ip_addr = SIM_FROM_ENV?;
match ip_addr.parse::<Ipv4Addr>() {
Ok(ip_addr) => Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT)),
Err(_) => {
defmt::error!("Invalid simulator IP address {}", ip_addr);
None
}
}
}
/// Uses several attempts, because the first packet can get lost while the MAC address of the
/// simulator host is resolved.
async fn check_connection(udp: &UdpSocket<'_>, sim_addr: SocketAddrV4) -> bool {
let mut tx_buf = [0; MAX_PACKET_LEN];
let mut rx_buf = [0; MAX_PACKET_LEN];
let request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let ping = match postcard::to_slice(&request, &mut tx_buf) {
Ok(ping) => ping,
Err(e) => {
defmt::error!("Failed to serialize simulator ping: {}", Debug2Format(&e));
return false;
}
};
for _ in 0..PING_ATTEMPTS {
if let Err(e) = udp.send_to(ping, sim_addr).await {
defmt::warn!("Failed to send simulator ping: {}", e);
continue;
}
if let Ok(Ok((len, _))) = udp.recv_from(&mut rx_buf).with_timeout(PING_TIMEOUT).await
&& matches!(
postcard::from_bytes::<SimReply>(&rx_buf[..len]),
Ok(SimReply::SimCtrl(SimCtrlReply::Pong))
)
{
return true;
}
}
false
}
@@ -0,0 +1,157 @@
use alloc::vec::Vec;
use arbitrary_int::u14;
use defmt::Debug2Format;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::channel::{Channel, Receiver, Sender};
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
use crate::leds::LED_MODE;
use crate::net::LAST_SENDER;
use crate::sim_client;
pub const TC_QUEUE_DEPTH: usize = 32;
pub const TM_QUEUE_DEPTH: usize = 32;
pub type TcChannel = Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TmChannel = Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub async fn tc_handler(tc_rx: TcReceiver, telemetry: &mut Telemetry) {
loop {
let tc = tc_rx.receive().await;
let packet = match CcsdsPacketReader::new_with_checksum(&tc) {
Ok(packet) => packet,
Err(e) => {
defmt::warn!("Failed to parse received TC packet: {}", e);
send_tmtc_event(telemetry, tmtc::Event::InvalidTcPacket).await;
continue;
}
};
let tc_id = CcsdsPacketIdAndPsc {
packet_id: packet.packet_id(),
psc: packet.psc(),
};
let Ok((tc_header, payload)) = postcard::take_from_bytes::<TcHeader>(packet.user_data())
else {
defmt::warn!("Failed to deserialize TC header");
send_tmtc_event(telemetry, tmtc::Event::InvalidTcHeader).await;
continue;
};
match tc_header.target_id {
ComponentId::Controller => handle_controller_tc(payload, tc_id, telemetry).await,
ComponentId::Led => handle_led_tc(payload, tc_id, telemetry).await,
target_id => {
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
send_tmtc_event(telemetry, tmtc::Event::UnknownTargetId(target_id)).await;
}
}
}
}
/// All TCs are received via UDP, so the UDP server is the sender of TMTC events.
async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) {
telemetry.send(ComponentId::UdpServer, None, &event).await;
}
/// The controller does not control anything yet, but handles generic requests like pings.
async fn handle_controller_tc(
payload: &[u8],
tc_id: CcsdsPacketIdAndPsc,
telemetry: &mut Telemetry,
) {
let Ok(request) = postcard::from_bytes::<control::request::Request>(payload) else {
defmt::warn!("Failed to deserialize controller request");
return;
};
match request {
control::request::Request::Ping => defmt::info!("Received controller ping request"),
control::request::Request::TestEvent => {
defmt::info!("Received test event request");
let event = types::Event::ControllerEvent(control::Event::TestEvent);
telemetry.send(ComponentId::Controller, None, &event).await;
}
control::request::Request::SimConnect(opt_ipv4_addr) => match opt_ipv4_addr {
Some(ipv4_addr) => sim_client::SIM_HOST.signal(ipv4_addr),
None => {
// If the UDP socket has received anything, the last sender should be stored and
// we use that IP address.
let opt_last_sender = LAST_SENDER.lock(|val| val.clone());
match opt_last_sender.get() {
Some(last_sender) => sim_client::SIM_HOST.signal(last_sender),
None => defmt::warn!("SimConnect without IP and no known sender"),
}
}
},
}
telemetry
.send(
ComponentId::Controller,
Some(tc_id),
&control::response::Response::Ok,
)
.await;
}
async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) {
let Ok(request) = postcard::from_bytes::<led::request::Request>(payload) else {
defmt::warn!("Failed to deserialize LED request");
return;
};
match request {
led::request::Request::Ping => defmt::info!("Received LED ping request"),
led::request::Request::SetMode(mode) => {
defmt::info!("Received LED mode request: {}", Debug2Format(&mode));
LED_MODE.signal(mode);
}
}
telemetry
.send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok)
.await;
}
/// Packs TM and passes it to the UDP task.
pub struct Telemetry {
tx: TmSender,
sequence_count: u14,
}
impl Telemetry {
pub fn new(tx: TmSender) -> Self {
Self {
tx,
sequence_count: u14::new(0),
}
}
/// TM without a TC ID is sent unsolicited, for example events.
async fn send(
&mut self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
let sp_header = SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0);
let tm_header = TmHeader::new_without_timestamp(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
);
match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload)
.map_err(CcsdsCreationError::from)
.and_then(|packet| packet.try_to_vec())
{
Ok(raw_packet) => {
self.tx.send(raw_packet).await;
self.sequence_count = self.sequence_count.wrapping_add(u14::new(1));
}
Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)),
}
}
}
@@ -0,0 +1,32 @@
[package]
name = "stm32h7-nucleo-embassy"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-embassy"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
shared-embedded.workspace = true
cortex-m.workspace = true
cortex-m-rt.workspace = true
defmt.workspace = true
defmt-rtt.workspace = true
panic-probe.workspace = true
embedded-alloc.workspace = true
static_cell.workspace = true
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
embassy-executor.workspace = true
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms"] }
embassy-net.workspace = true
[dev-dependencies]
defmt-test.workspace = true
@@ -0,0 +1,15 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
// The configuration is shared by all applications of the embedded workspace.
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
if !config.exists() && config_template.exists() {
fs::create_dir_all(&cargo_dir).unwrap();
fs::copy(&config_template, &config).unwrap();
}
}
@@ -0,0 +1,43 @@
//! Blinks an LED
//!
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
//! NUCLEO-H753ZI.
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio;
use stm32h7_nucleo_embassy as _;
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
defmt::info!("Hello World!");
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
let ld2 = gpio::Output::new(p.PE1, gpio::Level::High, gpio::Speed::Low);
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
spawner.spawn(blink(ld1, ld2, ld3).expect("spawning blink task failed"));
}
#[embassy_executor::task]
async fn blink(
mut ld1: gpio::Output<'static>,
mut ld2: gpio::Output<'static>,
mut ld3: gpio::Output<'static>,
) {
loop {
defmt::info!("high");
ld1.set_high();
ld2.set_high();
ld3.set_high();
embassy_time::Timer::after_millis(500).await;
defmt::info!("low");
ld1.set_low();
ld2.set_low();
ld3.set_low();
embassy_time::Timer::after_millis(500).await;
}
}
@@ -0,0 +1,13 @@
#![no_main]
#![no_std]
// global logger + panicking-behavior + memory layout
use stm32h7_nucleo_embassy as _;
#[cortex_m_rt::entry]
fn main() -> ! {
loop {
defmt::println!("Hello, world!");
cortex_m::asm::delay(100_000_000);
}
}
@@ -0,0 +1,51 @@
#![no_main]
#![no_std]
use defmt_rtt as _;
use embassy_stm32 as _;
use panic_probe as _;
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
const HEAP_SIZE: usize = 131_072;
// Part of the library, because all binaries depend on crates which require an allocator.
#[global_allocator]
static HEAP: Heap = Heap::empty();
/// # Safety
///
/// Must be called exactly once, before the first allocation.
pub unsafe fn init_heap() {
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
}
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
cortex_m::asm::udf()
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with an error. This seems better than the default, which is to spin in a
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
panic!("unexpected hard fault");
}
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}
@@ -0,0 +1,150 @@
#![no_main]
#![no_std]
extern crate alloc;
use embassy_executor::Spawner;
use embassy_net::StackResources;
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
use static_cell::{ConstStaticCell, StaticCell};
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
#[embassy_executor::main]
async fn main(spawner: Spawner) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_embassy::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = shared_embedded::leds::Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
shared_embedded::net::MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let net_config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// DHCP, TMTC and simulator socket.
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) = embassy_net::new(
device,
net_config,
RESOURCES.init(StackResources::new()),
seed,
);
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
let tm_channel = TM_CHANNEL.take();
spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed"));
spawner.spawn(sim_client_task(stack).expect("spawning sim client task failed"));
spawner.spawn(
udp_task(stack, tc_channel.sender(), tm_channel.receiver())
.expect("spawning UDP task failed"),
);
spawner.spawn(heartbeat(green_led).expect("spawning heartbeat task failed"));
spawner.spawn(led_task(leds).expect("spawning LED task failed"));
spawner.spawn(
tc_handler(
tc_channel.receiver(),
shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
)
.expect("spawning TC handler task failed"),
);
}
#[embassy_executor::task]
async fn net_stack_task(
mut runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
) -> ! {
shared_embedded::net::net_stack_task(&mut runner).await
}
#[embassy_executor::task]
async fn udp_task(
stack: embassy_net::Stack<'static>,
tc_tx: shared_embedded::tmtc::TcSender,
tm_rx: shared_embedded::tmtc::TmReceiver,
) {
shared_embedded::net::udp_task(stack, tc_tx, tm_rx).await
}
#[embassy_executor::task]
async fn sim_client_task(stack: embassy_net::Stack<'static>) {
shared_embedded::sim_client::sim_client_task(stack).await
}
#[embassy_executor::task]
async fn heartbeat(mut led: gpio::Output<'static>) {
shared_embedded::leds::heartbeat(&mut led).await
}
#[embassy_executor::task]
async fn led_task(mut leds: shared_embedded::leds::Leds) {
shared_embedded::leds::led_task(&mut leds).await
}
#[embassy_executor::task]
async fn tc_handler(
tc_rx: shared_embedded::tmtc::TcReceiver,
mut telemetry: shared_embedded::tmtc::Telemetry,
) {
shared_embedded::tmtc::tc_handler(tc_rx, &mut telemetry).await
}
@@ -0,0 +1,16 @@
#![no_std]
#![no_main]
use stm32h7_nucleo_embassy as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}
@@ -0,0 +1,32 @@
[package]
name = "stm32h7-nucleo-rtic"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-rtic"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
shared-embedded.workspace = true
cortex-m.workspace = true
cortex-m-rt.workspace = true
defmt.workspace = true
defmt-rtt.workspace = true
panic-probe.workspace = true
embedded-alloc.workspace = true
static_cell.workspace = true
rtic.workspace = true
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
embassy-net.workspace = true
[dev-dependencies]
defmt-test.workspace = true
@@ -0,0 +1,15 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
// The configuration is shared by all applications of the embedded workspace.
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
if !config.exists() && config_template.exists() {
fs::create_dir_all(&cargo_dir).unwrap();
fs::copy(&config_template, &config).unwrap();
}
}
@@ -0,0 +1,58 @@
//! Blinks an LED
//!
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
//! NUCLEO-H753ZI.
#![no_std]
#![no_main]
use rtic::app;
use stm32h7_nucleo_rtic as _;
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
mod app {
use embassy_stm32::gpio;
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
let p = embassy_stm32::init(Default::default());
defmt::info!("Hello World!");
// Configure gpio B pin 0 as a push-pull output.
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
let ld2 = gpio::Output::new(p.PE1, gpio::Level::High, gpio::Speed::Low);
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
// Schedule the blinking task
blink::spawn(ld1, ld2, ld3).ok();
(Shared {}, Local {})
}
#[task()]
async fn blink(
_cx: blink::Context,
mut ld1: gpio::Output<'static>,
mut ld2: gpio::Output<'static>,
mut ld3: gpio::Output<'static>,
) {
loop {
defmt::info!("high");
ld1.set_high();
ld2.set_high();
ld3.set_high();
embassy_time::Timer::after_millis(500).await;
defmt::info!("low");
ld1.set_low();
ld2.set_low();
ld3.set_low();
embassy_time::Timer::after_millis(500).await;
}
}
}
@@ -0,0 +1,13 @@
#![no_main]
#![no_std]
// global logger + panicking-behavior + memory layout
use stm32h7_nucleo_rtic as _;
#[cortex_m_rt::entry]
fn main() -> ! {
loop {
defmt::println!("Hello, world!");
cortex_m::asm::delay(100_000_000);
}
}
@@ -1,14 +1,27 @@
#![no_main]
#![no_std]
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
use stm32f3xx_hal as _; // memory layout
use defmt_rtt as _;
use embassy_stm32 as _;
use panic_probe as _;
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
const HEAP_SIZE: usize = 131_072;
// Part of the library, because all binaries depend on crates which require an allocator.
#[global_allocator]
static HEAP: Heap = Heap::empty();
/// # Safety
///
/// Must be called exactly once, before the first allocation.
pub unsafe fn init_heap() {
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
}
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
@@ -16,14 +29,6 @@ fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
@@ -31,9 +36,7 @@ pub fn exit() -> ! {
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
loop {
debug::exit(debug::EXIT_FAILURE);
}
panic!("unexpected hard fault");
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
@@ -0,0 +1,163 @@
#![no_main]
#![no_std]
extern crate alloc;
use rtic::app;
#[app(device = embassy_stm32, peripherals = false)]
mod app {
use embassy_net::StackResources;
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
use static_cell::{ConstStaticCell, StaticCell};
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
#[local]
struct Local {
net_runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
net_stack: embassy_net::Stack<'static>,
sim_net_stack: embassy_net::Stack<'static>,
leds: shared_embedded::leds::Leds,
green_led: gpio::Output<'static>,
tc_rx: shared_embedded::tmtc::TcReceiver,
tc_tx: shared_embedded::tmtc::TcSender,
tm_rx: shared_embedded::tmtc::TmReceiver,
telemetry: shared_embedded::tmtc::Telemetry,
}
#[shared]
struct Shared {}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_rtic::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = shared_embedded::leds::Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
shared_embedded::net::MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// DHCP, TMTC and simulator socket.
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) =
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
let tm_channel = TM_CHANNEL.take();
net_lib_task::spawn().expect("spawning net library task failed");
udp_task::spawn().expect("spawning UDP task failed");
sim_client_task::spawn().expect("spawning sim client task failed");
heartbeat::spawn().expect("spawning heartbeat task failed");
led_task::spawn().expect("spawning LED task failed");
tc_handler::spawn().expect("spawning TC handler task failed");
(
Shared {},
Local {
green_led,
leds,
net_runner: runner,
net_stack: stack,
sim_net_stack: stack,
tc_tx: tc_channel.sender(),
tc_rx: tc_channel.receiver(),
telemetry: shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
tm_rx: tm_channel.receiver(),
},
)
}
#[task(local = [green_led])]
async fn heartbeat(cx: heartbeat::Context) {
shared_embedded::leds::heartbeat(cx.local.green_led).await;
}
#[task(local = [leds])]
async fn led_task(cx: led_task::Context) {
shared_embedded::leds::led_task(cx.local.leds).await;
}
#[task(local = [net_runner])]
async fn net_lib_task(cx: net_lib_task::Context) {
shared_embedded::net::net_stack_task(cx.local.net_runner).await;
}
#[task(local = [net_stack, tc_tx, tm_rx])]
async fn udp_task(cx: udp_task::Context) {
shared_embedded::net::udp_task(*cx.local.net_stack, *cx.local.tc_tx, *cx.local.tm_rx).await;
}
#[task(local = [sim_net_stack])]
async fn sim_client_task(cx: sim_client_task::Context) {
shared_embedded::sim_client::sim_client_task(*cx.local.sim_net_stack).await;
}
#[task(local = [tc_rx, telemetry])]
async fn tc_handler(cx: tc_handler::Context) {
shared_embedded::tmtc::tc_handler(*cx.local.tc_rx, cx.local.telemetry).await;
}
}
@@ -0,0 +1,16 @@
#![no_std]
#![no_main]
use stm32h7_nucleo_rtic as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}
File renamed without changes.
@@ -7,3 +7,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
# [v0.1.1] 2024-02-21
satrs v0.2.0-rc.0
satrs-mib v0.1.1
# [v0.1.0] 2024-02-13
satrs v0.1.1
satrs-mib v0.1.0
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "example-std"
version = "0.1.1"
edition = "2024"
default-run = "example-std"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
[dependencies]
fern = "0.7"
chrono = "0.4"
log = "0.4"
crossbeam-channel = "0.5"
delegate = "0.13"
zerocopy = "0.8"
csv = "1"
num_enum = "0.7"
thiserror = "2"
lazy_static = "1"
strum = { version = "0.28", features = ["derive"] }
derive-new = "0.7"
cfg-if = "1"
arbitrary-int = "2"
bitbybit = "2"
postcard = { version = "1", features = ["alloc"] }
ctrlc = "3"
serde = { version = "1", features = ["derive"] }
satrs = { path = "../../satrs", features = ["test_util"] }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
satrs-mib = { path = "../../satrs-mib" }
[features]
# default = ["heap_tmtc"]
# heap_tmtc = []
[dev-dependencies]
env_logger = "0.11"
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File renamed without changes.
+57
View File
@@ -0,0 +1,57 @@
sat-rs example
======
This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
You can run the application using `cargo run`.
# Features
The example has the `heap_tmtc` feature which is enabled by default. With this feature enabled,
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
stores.
You can run the application without this feature using
```sh
cargo run --no-default-features
```
# Interacting with the sat-rs example
The `client` crate is a command line client which sends telecommands to the example application
and prints the received telemetry. For example, you can ping the application or switch MGM 0
to normal mode like this:
```sh
cargo run -p client -- --ping
cargo run -p client -- mgm0 -m normal
```
Use `cargo run -p client -- --help` to list all available commands.
## Adding the mini simulator application
This example application features a few device handlers. The
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
can be used to simulate the physical devices managed by these device handlers.
The example application will attempt communication with the mini simulator on UDP port 7303.
If this works, the device handlers will use communication interfaces dedicated to the communication
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
return constant values or behave like ideal devices.
In summary, you can use the following command command to run the mini-simulator first:
```sh
cargo run -p minisim
```
and then start the example using `cargo run -p example-std`.
+118
View File
@@ -0,0 +1,118 @@
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use types::acs::ctrl::{Mode, request::ModeRequest, response::ModeReport};
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: Receiver<ModeRequest>,
pub report_tx: SyncSender<ModeReport>,
}
/// Dummy ACS controller. It has no actual control law and reaches any commanded mode
/// immediately, which is sufficient to exercise the mode commanding of its parent subsystem.
pub struct Controller {
mode: Mode,
mode_leaf_helper: ModeLeafHelper,
}
impl Controller {
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
Self {
mode: Mode::Passive,
mode_leaf_helper,
}
}
#[allow(dead_code)]
#[inline]
pub fn mode(&self) -> Mode {
self.mode
}
pub fn periodic_operation(&mut self) {
self.handle_mode_leaf_handling();
}
fn handle_mode_leaf_handling(&mut self) {
loop {
match self.mode_leaf_helper.request_rx.try_recv() {
Ok(request) => match request {
ModeRequest::SetMode(mode) => {
log::info!("ACS controller: transitioning to mode {:?}", mode);
self.mode = mode;
self.report_mode();
}
ModeRequest::ReadMode => self.report_mode(),
},
Err(e) => match e {
TryRecvError::Empty => break,
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
fn report_mode(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeReport::Mode(self.mode))
.expect("failed to send mode report to parent");
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use super::*;
struct ControllerTestbench {
request_tx: SyncSender<ModeRequest>,
report_rx: Receiver<ModeReport>,
controller: Controller,
}
impl ControllerTestbench {
fn new() -> Self {
let (request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, report_rx) = mpsc::sync_channel(5);
Self {
request_tx,
report_rx,
controller: Controller::new(ModeLeafHelper {
request_rx,
report_tx,
}),
}
}
}
#[test]
fn test_initial_mode() {
let testbench = ControllerTestbench::new();
assert_eq!(testbench.controller.mode(), Mode::Passive);
}
#[test]
fn test_set_mode() {
let mut testbench = ControllerTestbench::new();
testbench
.request_tx
.send(ModeRequest::SetMode(Mode::Safe))
.unwrap();
testbench.controller.periodic_operation();
assert_eq!(testbench.controller.mode(), Mode::Safe);
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Safe));
}
#[test]
fn test_read_mode() {
let mut testbench = ControllerTestbench::new();
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
testbench.controller.periodic_operation();
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Passive));
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,662 @@
use std::{sync::mpsc, time::Duration};
use example_std::{ModeHelper, TmtcQueues};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, DeviceMode,
acs::mgm_assembly::{Mode, request, response},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct ParentQueueHelper {
pub request_rx: mpsc::Receiver<request::ModeRequest>,
pub report_tx: mpsc::SyncSender<response::ModeResponse>,
}
/// Helper component for communication with a parent component, which is usually as assembly.
pub struct ChildrenQueueHelper {
pub request_tx_queues: [mpsc::SyncSender<types::acs::mgm::request::ModeRequest>; 2],
pub report_rx_queues: [mpsc::Receiver<types::acs::mgm::response::ModeResponse>; 2],
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
#[derive(Debug, Default, Copy, Clone)]
pub struct MgmInfo {
reply_received: bool,
mode: Option<DeviceMode>,
}
/// MGM assembly component.
pub struct Assembly {
mode_helper: ModeHelper<Mode, TransitionState>,
/// This boolean is used for the distinction between transitions commanded by the parent
/// or by ground, and transitions which were commanded autonomously as part of children
/// mode keeping.
mode_keeping_transition: bool,
tmtc_queues: TmtcQueues,
mgm_modes: [MgmInfo; 2],
parent_queues: ParentQueueHelper,
pub(crate) children_queues: ChildrenQueueHelper,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
}
impl Assembly {
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
pub fn new(
parent_queues: ParentQueueHelper,
children_queues: ChildrenQueueHelper,
tmtc_queues: TmtcQueues,
mode_timeout: Duration,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
) -> Self {
Self {
mode_helper: ModeHelper::new(Mode::NoModeKeeping, mode_timeout),
mode_keeping_transition: false,
tmtc_queues,
mgm_modes: [MgmInfo::default(); 2],
parent_queues,
children_queues,
event_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_parent_mode_queue();
self.handle_children_mode_queues();
if self.mode_helper.transition_active() {
self.handle_mode_transition();
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::mgm_assembly::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::mgm_assembly::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::mgm_assembly::request::Request::Mode(request) => {
match request {
request::ModeRequest::SetMode(assembly_mode) => {
self.start_transition(false, assembly_mode, Some(tc_id))
}
request::ModeRequest::ReadMode => self.send_telemetry(
Some(tc_id),
response::Response::Mode(response::ModeResponse::Mode(
self.mode(),
)),
),
}
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::mgm_assembly::response::Response,
) {
match pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_parent_mode_queue(&mut self) {
loop {
match self.parent_queues.request_rx.try_recv() {
Ok(request) => match request {
request::ModeRequest::SetMode(assembly_mode) => match assembly_mode {
Mode::Device(_device_mode) => {
self.start_transition(false, assembly_mode, None);
}
Mode::NoModeKeeping => {
self.mode_helper.current = Mode::NoModeKeeping;
}
},
request::ModeRequest::ReadMode => self
.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap(),
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_children_mode_queues(&mut self) {
let mut mode_report_received = false;
for (idx, rx) in self.children_queues.report_rx_queues.iter_mut().enumerate() {
loop {
match rx.try_recv() {
Ok(report) => match report {
types::acs::mgm::response::ModeResponse::Mode(device_mode) => {
self.mgm_modes[idx].mode = Some(device_mode);
self.mgm_modes[idx].reply_received = true;
mode_report_received = true;
}
types::acs::mgm::response::ModeResponse::SetModeTimeout => {
// Ignore, handle this with our own timeout.
log::warn!("MGM {} mode timeout", idx);
}
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
if !mode_report_received {
return;
}
// Transition is active, check for completion.
if self.mode_helper.transition_active()
&& self.mgm_modes.iter().all(|i| i.reply_received)
&& let Mode::Device(device_mode) = self.mode_helper.target.unwrap()
{
// If at least one child reached the correct mode, we are done.
if self.mgm_modes.iter().any(|i| i.mode == Some(device_mode)) {
self.handle_mode_reached(true);
} else {
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::WrongMode(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
// Mode keeping active: Check children modes.
if let Mode::Device(device_mode) = self.mode_helper.current
&& self
.mgm_modes
.iter()
.all(|info| info.mode != Some(device_mode))
{
// Children lost mode. Try to command them back to the correct
// mode.
self.start_transition(true, self.mode_helper.current, None);
}
}
pub fn handle_mode_transition(&mut self) {
if self.mode_helper.target.is_none() {
self.handle_mode_reached(true);
return;
}
let target = self.mode_helper.target.unwrap();
let device_mode = match target {
Mode::Device(device_mode) => device_mode,
Mode::NoModeKeeping => {
self.handle_mode_reached(true);
return;
}
};
if self.mode_helper.transition_state == TransitionState::Idle {
self.command_children(device_mode);
self.mode_helper.transition_state = TransitionState::AwaitingReplies;
}
if self.mode_helper.transition_state == TransitionState::AwaitingReplies
&& self.mode_helper.timed_out()
{
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::SetModeTimeout(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
pub fn handle_mode_reached(&mut self, success: bool) {
let tc_commander = self.mode_helper.finish(success);
self.announce_mode();
if tc_commander.is_some() {
self.send_telemetry(tc_commander, response::Response::Ok);
}
self.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap();
}
pub fn handle_mode_transition_failure(&mut self, report: response::ModeResponse) {
if self.mode_helper.tc_commander.is_some() {
self.send_telemetry(
self.mode_helper.tc_commander,
response::Response::Mode(response::ModeResponse::SetModeTimeout(
self.mgm_modes.map(|info| info.mode),
)),
);
}
self.parent_queues.report_tx.send(report).unwrap();
self.mode_helper.finish(false);
}
pub fn command_children(&self, mode: DeviceMode) {
for tx in &self.children_queues.request_tx_queues {
tx.send(types::acs::mgm::request::ModeRequest::SetMode(mode))
.unwrap();
}
}
pub fn start_transition(
&mut self,
mode_keeping: bool,
target: Mode,
tc_id: Option<CcsdsPacketIdAndPsc>,
) {
self.mode_keeping_transition = mode_keeping;
self.mode_helper.tc_commander = tc_id;
self.mgm_modes
.iter_mut()
.for_each(|m| m.reply_received = false);
self.mode_helper.start(target);
}
fn announce_mode(&self) {
log::info!(
"{:?} announcing mode: {:?}",
Self::ID,
self.mode_helper.current
);
if let Err(e) = self
.event_tx
.send(types::acs::mgm_assembly::Event::ModeChanged(
self.mode_helper.current,
))
{
log::warn!("{:?}: failed to send mode changed event: {}", Self::ID, e);
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
#[cfg(test)]
fn mode_transition_active(&self) -> bool {
self.mode_helper.transition_active()
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message, MessageType, TcHeader,
acs::mgm_assembly,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
};
use super::*;
pub struct Testbench {
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
subsystem_report_rx: mpsc::Receiver<response::ModeResponse>,
mgm_request_rx: [mpsc::Receiver<types::acs::mgm::request::ModeRequest>; 2],
mgm_report_tx: [mpsc::SyncSender<types::acs::mgm::response::ModeResponse>; 2],
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgm_assembly::Event>,
assembly: Assembly,
}
impl Testbench {
pub fn new() -> Self {
let (subsystem_req_tx, subsystem_req_rx) = mpsc::sync_channel(5);
let (subsystem_report_tx, subsystem_report_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
Self {
subsystem_req_tx,
subsystem_report_rx,
mgm_request_rx: [mgm_0_mode_request_rx, mgm_1_mode_request_rx],
mgm_report_tx: [mgm_0_mode_report_tx, mgm_1_mode_report_tx],
tc_tx,
tm_rx,
event_rx,
assembly: Assembly::new(
ParentQueueHelper {
request_rx: subsystem_req_rx,
report_tx: subsystem_report_tx,
},
ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues { tc_rx, tm_tx },
Duration::from_millis(20),
event_tx,
),
}
}
pub fn assert_all_queues_empty(&self) {
assert!(
matches!(self.tm_rx.try_recv().unwrap_err(), TryRecvError::Empty),
"TM queue not empty"
);
assert!(
matches!(
self.subsystem_report_rx.try_recv().unwrap_err(),
TryRecvError::Empty
),
"subsystem report queue not empty"
);
for rx in self.mgm_request_rx.iter() {
assert!(
matches!(rx.try_recv().unwrap_err(), TryRecvError::Empty),
"mgm request queue not empty"
)
}
}
}
pub fn create_request_tc(
request: types::acs::mgm_assembly::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(Assembly::ID, request.message_type()),
request,
)
}
#[test]
fn basic_test() {
let mut tb = Testbench::new();
tb.assert_all_queues_empty();
tb.assembly.periodic_operation();
tb.assert_all_queues_empty();
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
}
#[test]
fn test_tc_commanded_transition() {
let mut tb = Testbench::new();
tb.tc_tx
.send(create_request_tc(mgm_assembly::request::Request::Mode(
request::ModeRequest::SetMode(Mode::Device(DeviceMode::Normal)),
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let response = tb.tm_rx.try_recv().unwrap();
assert_eq!(response.tm_header.sender_id, Assembly::ID);
assert_eq!(response.tm_header.message_type, MessageType::Verification);
let response: response::Response = postcard::from_bytes(&response.payload).unwrap();
assert_eq!(response, response::Response::Ok);
let event = tb.event_rx.try_recv().expect("expected mode changed event");
assert!(matches!(
event,
mgm_assembly::Event::ModeChanged(Mode::Device(DeviceMode::Normal))
));
}
#[test]
fn test_parent_commanded_transition() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_one_mgm_is_sufficient() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// One device is sufficient.
tb.mgm_report_tx[0]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
tb.mgm_report_tx[1]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_mode_commanding_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::WrongMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
#[test]
fn test_mode_keeping_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
// This should start mode keeping.
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Let the mode keeping fail.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
}
+876
View File
@@ -0,0 +1,876 @@
use std::collections::VecDeque;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use example_std::{HkHelperSingleSet, TmtcQueues};
use minisim_types::acs::mgt as sim_mgt;
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::acs::mgt::{
self, HkSet,
request::{ModeRequest, Request},
response::{ModeResponse, Response},
};
use types::pcdu::SwitchId;
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
use crate::device_fdir::{DeviceFdir, FdirEvent};
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
use crate::eps::PowerSwitchHelper;
// The handler blocks while waiting for a reply, so this must be well below the cycle time
// of the ACS thread.
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
/// Interface for ideal device which never fails.
#[derive(Default)]
pub struct DummyInterface {
dipole: mgt::Dipole,
torque_end: Option<Instant>,
}
impl DummyInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
sim_mgt::Request::ApplyTorque { duration, dipole } => {
self.dipole = dipole;
self.torque_end = Some(Instant::now() + duration);
sim_mgt::Reply::Ack
}
sim_mgt::Request::RequestHk => {
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: if torquing {
self.dipole
} else {
mgt::Dipole::default()
},
torquing,
})
}
};
Some(reply.to_frame())
}
}
/// Records all sent frames and returns injected reply frames.
#[derive(Default)]
pub struct TestInterface {
pub sent_frames: Vec<Vec<u8>>,
pub replies: VecDeque<Vec<u8>>,
}
pub struct SimInterface {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
impl SimInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
// Replies which arrived after a previous timeout must not be mistaken for this reply.
while self.sim_reply_rx.try_recv().is_ok() {}
if let Err(e) = self
.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
frame.to_vec(),
)))
{
log::error!("failed to send MGT SIM request: {e}");
return None;
}
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
SimReply::Mgt(frame) => Some(frame),
sim_reply => {
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
None
}
}
}
}
/// Frame based transport to the device. The handler implements the protocol on top of it.
pub enum MgtCommunication {
Dummy(DummyInterface),
Sim(SimInterface),
#[allow(dead_code)]
Test(TestInterface),
}
impl MgtCommunication {
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
/// was no reply in time.
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
match self {
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
MgtCommunication::Sim(sim) => sim.transfer(frame),
MgtCommunication::Test(test) => {
test.sent_frames.push(frame.to_vec());
test.replies.pop_front()
}
}
}
}
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: mpsc::Receiver<ModeRequest>,
pub report_tx: mpsc::SyncSender<ModeResponse>,
}
/// Magnetorquer (MGT) device handler.
///
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
/// a fault which is handled by the FDIR.
///
/// This device handler includes several components beyond the scope of commanding the device:
///
/// - The [MgtCommunication] structure models different communication interfaces to the
/// physical device.
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
/// helper.
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
pub struct MgtHandler {
tmtc_queues: TmtcQueues,
pub com: MgtCommunication,
hk_set: HkSet,
hk_helper: HkHelperSingleSet,
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
mode_leaf_helper: ModeLeafHelper,
fdir: DeviceFdir,
event_tx: mpsc::SyncSender<mgt::Event>,
}
impl MgtHandler {
pub fn new(
tmtc_queues: TmtcQueues,
switch_helper: PowerSwitchHelper,
com: MgtCommunication,
mode_leaf_helper: ModeLeafHelper,
mode_timeout: Duration,
health_table: HealthTableMapSync,
event_tx: mpsc::SyncSender<mgt::Event>,
) -> Self {
Self {
tmtc_queues,
com,
hk_set: HkSet::default(),
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
switch_and_mode_helper: SwitchAndModeHelper::new(
DeviceMode::Off,
mode_timeout,
switch_helper,
SwitchId::Mgt,
),
mode_leaf_helper,
fdir: DeviceFdir::new(
"MGT",
ComponentId::AcsMgt,
health_table,
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
),
event_tx,
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.switch_and_mode_helper.mode()
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_mode_leaf_handling();
self.fdir
.periodic_operation(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
match event {
ModeTransitionEvent::Reached(tc_commander) => {
self.handle_mode_reached(tc_commander)
}
ModeTransitionEvent::Failed(tc_commander) => {
self.handle_mode_transition_failure(tc_commander)
}
ModeTransitionEvent::PowerCycleDone => {
self.fdir.handle_power_cycle_done();
self.handle_fdir_events();
}
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
self.fdir
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
self.handle_fdir_events();
}
}
}
if self.ready_for_commanding() {
self.poll_hk();
}
if self.hk_helper.needs_generation() {
self.send_telemetry(None, Response::Hk(self.hk_set));
}
}
fn poll_hk(&mut self) {
match self.transfer(sim_mgt::Request::RequestHk) {
Some(sim_mgt::Reply::Hk(hk)) => {
self.fdir.register_success();
self.hk_set = HkSet {
valid: true,
dipole: hk.dipole,
torquing: hk.torquing,
};
}
reply => self.register_reply_fault(reply),
}
}
/// Returns [None] if there was no reply in time or the reply frame was invalid.
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
let frame = self.com.transfer(&request.to_frame())?;
sim_mgt::Reply::from_frame(&frame)
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
.ok()
}
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
log::warn!("MGT: missing or unexpected reply {reply:?}");
self.hk_set.valid = false;
self.fdir.register_fault(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
}
fn handle_fdir_events(&mut self) {
while let Some(event) = self.fdir.next_event() {
let event = match event {
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
FdirEvent::Recovery(recovery_event) => {
// The device is power cycled or switched off.
if matches!(
recovery_event,
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
) {
self.hk_set = HkSet::default();
}
mgt::Event::Recovery(recovery_event)
}
};
self.send_event(event);
}
}
fn ready_for_commanding(&self) -> bool {
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("MGT: failed to deserialize request: {}", e);
continue;
}
};
log::info!(
"MGT: received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
Request::Mode(ModeRequest::SetMode(mode)) => {
self.start_transition(mode, Some(tc_id))
}
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
Some(tc_id),
Response::Mode(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
)),
),
Request::ApplyTorque { dipole, duration } => {
self.handle_torque_command(tc_id, dipole, duration)
}
Request::Health(HealthRequest::SetHealth(health)) => {
log::info!("MGT: setting health to {health:?} via ground command");
self.fdir.set_health(health);
self.send_telemetry(Some(tc_id), Response::Ok);
}
}
}
}
fn handle_mode_leaf_handling(&mut self) {
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
match request {
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
ModeRequest::ReadMode => self.report_mode_to_parent(),
}
}
}
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
match hk_request {
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
HkRequestType::EnablePeriodic(opt_interval) => {
self.hk_helper.enabled = true;
if let Some(interval) = opt_interval {
self.hk_helper.frequency = interval;
}
}
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
_ => log::warn!("MGT: unhandled HK request"),
}
}
fn handle_torque_command(
&mut self,
tc_id: CcsdsPacketIdAndPsc,
dipole: mgt::Dipole,
duration: Duration,
) {
if !self.ready_for_commanding() {
log::warn!("MGT: rejecting torque command, device not in normal mode");
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
return;
}
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
Some(sim_mgt::Reply::Ack) => {
self.fdir.register_success();
self.send_telemetry(Some(tc_id), Response::Ok);
}
reply => {
self.register_reply_fault(reply);
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
}
}
}
fn start_transition(
&mut self,
target_mode: DeviceMode,
tc_commander: Option<CcsdsPacketIdAndPsc>,
) {
log::info!("MGT: transitioning to mode {:?}", target_mode);
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
if target_mode == DeviceMode::Off {
self.hk_set = HkSet::default();
}
self.switch_and_mode_helper
.start_transition(target_mode, tc_commander);
}
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
log::info!("MGT: mode {:?} reached", self.mode());
self.send_event(mgt::Event::ModeChanged(self.mode()));
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Ok);
}
self.report_mode_to_parent();
}
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
}
self.mode_leaf_helper
.report_tx
.send(ModeResponse::SetModeTimeout)
.unwrap();
}
fn report_mode_to_parent(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
))
.unwrap();
}
fn send_event(&self, event: mgt::Event) {
if let Err(e) = self.event_tx.send(event) {
log::warn!("MGT: failed to send event {:?}: {}", event, e);
}
}
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("MGT: failed to send TM packet: {}", e);
}
}
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use arbitrary_int::u11;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message as _, TcHeader,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::device_fdir::RECOVERY_THRESHOLD;
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
impl TestInterface {
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
self.sent_frames
.iter()
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
.collect()
}
fn push_reply(&mut self, reply: sim_mgt::Reply) {
self.replies.push_back(reply.to_frame());
}
}
struct MgtTestbench {
parent_request_tx: mpsc::SyncSender<ModeRequest>,
parent_report_rx: mpsc::Receiver<ModeResponse>,
shared_switch_set: SharedSwitchSet,
switch_rx: mpsc::Receiver<SwitchRequest>,
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgt::Event>,
health_table: HealthTableMapSync,
handler: MgtHandler,
}
impl MgtTestbench {
fn new() -> Self {
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let (event_tx, event_rx) = mpsc::sync_channel(20);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
let health_table = HealthTableMapSync::default();
let mut handler = MgtHandler::new(
TmtcQueues { tc_rx, tm_tx },
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
MgtCommunication::Test(TestInterface::default()),
ModeLeafHelper {
request_rx,
report_tx,
},
Duration::from_millis(100),
health_table.clone(),
event_tx,
);
handler.fdir.recovery_off_duration = Duration::ZERO;
Self {
parent_request_tx,
parent_report_rx,
shared_switch_set,
switch_rx,
tc_tx,
tm_rx,
event_rx,
health_table,
handler,
}
}
fn send_tc(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
request,
))
.unwrap();
}
fn next_response(&self) -> Response {
let tm = self.tm_rx.try_recv().expect("no TM generated");
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
}
/// Completes the power switch handshake for a commanded switch-on.
fn switch_to_normal(&mut self) {
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
assert_eq!(self.next_response(), Response::Ok);
}
fn test_interface(&mut self) -> &mut TestInterface {
match &mut self.handler.com {
MgtCommunication::Test(test) => test,
_ => panic!("unexpected MGT interface"),
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, state);
}
fn health(&self) -> Option<HealthState> {
self.health_table.health(ComponentId::AcsMgt.into())
}
fn drain_events(&self) -> Vec<mgt::Event> {
self.event_rx.try_iter().collect()
}
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
/// reached normal mode already registered the first fault.
fn exceed_reply_fault_threshold(&mut self) {
for _ in 0..REPLY_FAULT_THRESHOLD {
self.handler.periodic_operation();
}
}
/// Drives a started power cycle recovery to completion, completing both power switch
/// handshakes.
fn complete_power_cycle(&mut self) {
self.handler.periodic_operation();
self.set_switch_state(SwitchState::Off);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Off);
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
}
}
#[test]
fn test_initial_state_no_polling() {
let mut testbench = MgtTestbench::new();
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_switch_to_normal() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
testbench.handler.periodic_operation();
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.event_rx.try_recv(),
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
}
#[test]
fn test_mode_command_from_parent() {
let mut testbench = MgtTestbench::new();
testbench
.parent_request_tx
.send(ModeRequest::SetMode(DeviceMode::Normal))
.unwrap();
testbench.handler.periodic_operation();
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
// Commanded by the parent, so no TC response is expected.
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_torque_command_rejected_when_off() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_torque_command_forwarded_in_normal_mode() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.test_interface().sent_frames.clear();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(
testbench.test_interface().sent_requests().first(),
Some(&sim_mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
})
);
}
#[test]
fn test_torque_command_without_ack() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
}
#[test]
fn test_hk_polling_updates_hk_set() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert!(
testbench
.test_interface()
.sent_requests()
.contains(&sim_mgt::Request::RequestHk)
);
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(
testbench.next_response(),
Response::Hk(HkSet {
valid: true,
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
})
);
}
#[test]
fn test_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.handler.periodic_operation();
let switch_request = testbench
.switch_rx
.try_iter()
.last()
.expect("no switch request");
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
testbench.set_switch_state(SwitchState::Off);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Off)]
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Off))
);
}
#[test]
fn test_hk_set_invalid_after_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
}
#[test]
fn test_periodic_hk() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
Duration::ZERO,
))));
testbench.handler.periodic_operation();
assert!(matches!(testbench.next_response(), Response::Hk(_)));
let tm = testbench.tm_rx.try_recv();
assert!(tm.is_err(), "only one HK packet per cycle expected");
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
testbench.handler.periodic_operation();
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_valid_reply_no_fault() {
let mut testbench = MgtTestbench::new();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 0);
assert!(testbench.handler.hk_set.valid);
}
#[test]
fn test_missing_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(!testbench.handler.hk_set.valid);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_unexpected_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_invalid_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().replies.push_back(vec![0xff]);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_reply_fault_threshold_starts_recovery() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::Started)
]
));
testbench.complete_power_cycle();
assert_eq!(testbench.health(), Some(HealthState::Healthy));
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::Recovery(RecoveryEvent::Done)]
));
// The power cycle is hidden from the parent.
assert!(testbench.parent_report_rx.try_recv().is_err());
}
#[test]
fn test_unresponsive_device_marked_faulty() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
for _ in 0..RECOVERY_THRESHOLD {
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
testbench.complete_power_cycle();
}
testbench.drain_events();
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::Faulty));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
]
));
assert_eq!(
testbench.handler.switch_and_mode_helper.target(),
Some(DeviceMode::Off)
);
}
#[test]
fn test_set_health() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
HealthState::Faulty,
)));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(testbench.health(), Some(HealthState::Faulty));
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod ctrl;
pub mod mgm;
pub mod mgm_assembly;
pub mod mgt;
pub mod subsystem;
+406
View File
@@ -0,0 +1,406 @@
#![allow(dead_code)]
use std::{
sync::mpsc::{self, Receiver, SyncSender},
time::Duration,
};
use example_std::{ModeHelper, TmtcQueues};
use satrs::{
mode_tree::{
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
SequenceTableMapTable, SequenceTablesMapValue, TargetModeTables,
},
spacepackets::CcsdsPacketIdAndPsc,
subsystem::{
ModeCommandingResult, ModeRaw, ModeResponse, ModeSetRequest, ModeTreeHelperError,
SubsystemCommandingHelper, SubsystemHelperResult,
},
};
use types::{
ComponentId,
acs::subsystem::{Mode, response},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
fn build_sequence_tables() -> SequenceModeTables {
let mut seq_tables = SequenceModeTables::default();
let mut off_table = SequenceTablesMapValue::new("OFF");
let mut off_step_0 = SequenceTableMapTable::new("OFF_STEP_0");
off_step_0.add_entry(SequenceTableEntry::new(
"OFF_CTRL_PASSIVE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
true,
));
off_table.add_sequence_table(off_step_0);
let mut off_step_1 = SequenceTableMapTable::new("OFF_STEP_1");
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGM_ASSY_OFF",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off).into(),
false,
));
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGT_OFF",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
false,
));
off_table.add_sequence_table(off_step_1);
seq_tables.0.insert(Mode::Off as ModeRaw, off_table);
let mut safe_table = SequenceTablesMapValue::new("SAFE");
let mut safe_step_0 = SequenceTableMapTable::new("SAFE_STEP_0");
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGM_ASSY_NORMAL",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal).into(),
false,
));
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGT_NORMAL",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Normal.into(),
false,
));
safe_table.add_sequence_table(safe_step_0);
let mut safe_step_1 = SequenceTableMapTable::new("SAFE_STEP_1");
safe_step_1.add_entry(SequenceTableEntry::new(
"SAFE_CTRL_SAFE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Safe.into(),
false,
));
safe_table.add_sequence_table(safe_step_1);
seq_tables.0.insert(Mode::Safe as ModeRaw, safe_table);
seq_tables
}
fn ctrl_response_to_mode_response(
response: types::acs::ctrl::response::ModeReport,
) -> ModeResponse {
let sender_id = ComponentId::AcsController as satrs::ComponentId;
match response {
types::acs::ctrl::response::ModeReport::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::ctrl::response::ModeReport::WrongMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgm_assy_response_to_mode_response(
response: types::acs::mgm_assembly::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgmAssembly as satrs::ComponentId;
match response {
types::acs::mgm_assembly::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgm_assembly::response::ModeResponse::SetModeTimeout(_)
| types::acs::mgm_assembly::response::ModeResponse::WrongMode(_)
| types::acs::mgm_assembly::response::ModeResponse::CanNotKeepMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgt_response_to_mode_response(
response: types::acs::mgt::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
match response {
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
#[derive(Debug)]
pub struct ModeRequestSenders {
pub mode_request_ctrl: SyncSender<types::acs::ctrl::request::ModeRequest>,
pub mode_request_mgm_assy: SyncSender<types::acs::mgm_assembly::request::ModeRequest>,
pub mode_request_mgt: SyncSender<types::acs::mgt::request::ModeRequest>,
}
#[derive(Debug)]
pub struct ModeReportReceivers {
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
}
#[derive(Debug)]
pub struct Subsystem {
mode_helper: ModeHelper<types::acs::subsystem::Mode, TransitionState>,
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
subsystem_helper: SubsystemCommandingHelper,
}
impl Subsystem {
pub const ID: ComponentId = ComponentId::AcsSubsystem;
pub fn new(
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
) -> Self {
let mut mode_store_vec = ModeStoreVec::default();
mode_store_vec
.add_component(
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::NoModeKeeping.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
)
.unwrap();
let target_tables = TargetModeTables::default();
let sequence_tables = build_sequence_tables();
Self {
mode_helper: ModeHelper::new(
types::acs::subsystem::Mode::Off,
Duration::from_millis(2000),
),
mode_request_senders,
mode_report_receivers,
tmtc_queues,
subsystem_helper: SubsystemCommandingHelper::new(
mode_store_vec,
target_tables,
sequence_tables,
),
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
let mut mode_requests = Vec::new();
while let Ok(response) = self.mode_report_receivers.mode_response_ctrl.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(ctrl_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgm_assy.try_recv() {
let result = self.subsystem_helper.state_machine(
Some(mgm_assy_response_to_mode_response(response)),
|request| mode_requests.push(request),
);
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgt.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(mgt_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
let result = self
.subsystem_helper
.state_machine(None, |request| mode_requests.push(request));
self.handle_state_machine_result(result);
for request in mode_requests {
self.handle_mode_set_request(request);
}
}
fn handle_state_machine_result(
&mut self,
result: Result<SubsystemHelperResult, ModeTreeHelperError>,
) {
match result {
Ok(result) => match result {
SubsystemHelperResult::Idle => (),
SubsystemHelperResult::TargetKeeping => (),
SubsystemHelperResult::ModeCommanding(mode_commanding_result) => {
match mode_commanding_result {
ModeCommandingResult::Done => {
let target_mode_typed =
self.subsystem_helper.seq_exec_helper.target_mode();
let target_mode = target_mode_typed.map(Mode::try_from);
log::info!(
"ACS SS: mode commanding for target mode {:?} done",
target_mode
);
self.finish_mode_transition();
}
ModeCommandingResult::StepDone => log::info!("mode commanding step done"),
ModeCommandingResult::AwaitingSuccessCheck => {
log::info!("ACS SS: mode commanding awaiting success check")
}
}
}
},
Err(e) => {
log::error!("mode tree helper error: {}", e);
self.mode_helper.finish(false);
}
}
}
fn finish_mode_transition(&mut self) {
let tc_commander = self.mode_helper.finish(true);
if let Some(requestor) = tc_commander {
self.send_telemetry(
Some(requestor),
response::Response::Mode(response::ModeResponse::Mode(self.mode_helper.current)),
);
}
}
fn handle_mode_set_request(&mut self, request: ModeSetRequest) {
let target_id = ComponentId::try_from(request.target_id);
if let Ok(target_id) = target_id {
match target_id {
ComponentId::AcsMgmAssembly => self
.mode_request_senders
.mode_request_mgm_assy
.send(types::acs::mgm_assembly::request::ModeRequest::SetMode(
types::acs::mgm_assembly::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsController => self
.mode_request_senders
.mode_request_ctrl
.send(types::acs::ctrl::request::ModeRequest::SetMode(
types::acs::ctrl::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsMgt => self
.mode_request_senders
.mode_request_mgt
.send(types::acs::mgt::request::ModeRequest::SetMode(
types::DeviceMode::try_from(request.mode).unwrap(),
))
.unwrap(),
_ => {
log::error!("invalid target ID {:?} for mode command", target_id);
}
}
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::subsystem::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::subsystem::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::subsystem::request::Request::Mode(mode_request) => {
self.handle_mode_request(Some(tc_id), mode_request);
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn handle_mode_request(
&mut self,
tc_id: Option<CcsdsPacketIdAndPsc>,
mode_request: types::acs::subsystem::request::ModeRequest,
) {
match mode_request {
types::acs::subsystem::request::ModeRequest::SetMode(target_mode) => {
self.mode_helper.start(target_mode);
self.mode_helper.tc_commander = tc_id;
if let Err(e) = self
.subsystem_helper
.start_command_sequence(target_mode as ModeRaw)
{
log::error!("error starting command sequence: {}", e);
}
}
types::acs::subsystem::request::ModeRequest::ReadMode => {
self.send_telemetry(
None,
response::Response::Mode(response::ModeResponse::Mode(
self.mode_helper.current,
)),
);
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::subsystem::response::Response,
) {
match crate::ccsds::pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketIdAndPsc, SpHeader,
time::{StdTimestampError, cds::CdsTime},
};
use serde::Serialize;
use types::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
#[derive(Debug, thiserror::Error)]
pub enum CcsdsTmCreationError {
#[error("postcard error: {0}")]
Postcard(#[from] postcard::Error),
#[error("timestamp error: {0}")]
Time(#[from] StdTimestampError),
}
pub fn pack_ccsds_tm_packet_for_now(
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl Serialize + Message),
) -> Result<CcsdsTmPacketOwned, CcsdsTmCreationError> {
let now = CdsTime::now_with_u16_days()?;
let sp_header = SpHeader::new_from_apid(u11::new(Apid::Tmtc as u16));
let tm_header = TmHeader::new(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
&now,
);
Ok(CcsdsTmPacketOwned::new_with_serde_payload(
sp_header, &tm_header, payload,
)?)
}
@@ -1,18 +1,16 @@
use arbitrary_int::u11;
use lazy_static::lazy_static;
use satrs::{
res_code::ResultU16,
legacy::res_code::ResultU16,
spacepackets::{PacketId, PacketType},
};
use satrs_mib::res_code::ResultU16Info;
use satrs_mib::resultcode;
use std::{collections::HashSet, net::Ipv4Addr};
use strum::IntoEnumIterator;
use strum::IntoEnumIterator as _;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::{
events::{EventU32TypedSev, SeverityInfo},
pool::{StaticMemoryPool, StaticPoolConfig},
};
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
@@ -38,20 +36,17 @@ pub enum GroupId {
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
pub const SERVER_PORT: u16 = 7301;
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
lazy_static! {
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
let mut set = HashSet::new();
for id in components::Apid::iter() {
set.insert(PacketId::new(PacketType::Tc, true, id as u16));
for id in types::Apid::iter() {
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
}
set
};
pub static ref APID_VALIDATOR: HashSet<u16> = {
let mut set = HashSet::new();
for id in components::Apid::iter() {
for id in types::Apid::iter() {
set.insert(id as u16);
}
set
@@ -123,67 +118,16 @@ pub mod mode_err {
}
pub mod components {
use satrs::request::UniqueApidTargetId;
use strum::EnumIter;
use satrs::ComponentId;
#[derive(Copy, Clone, PartialEq, Eq, EnumIter)]
pub enum Apid {
Sched = 1,
GenericPus = 2,
Acs = 3,
Cfdp = 4,
Tmtc = 5,
}
// Component IDs for components with the PUS APID.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum PusId {
PusEventManagement = 0,
PusRouting = 1,
PusTest = 2,
PusAction = 3,
PusMode = 4,
PusHk = 5,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum AcsId {
Mgm0 = 0,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TmtcId {
UdpServer = 0,
TcpServer = 1,
}
pub const PUS_ACTION_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusAction as u32);
pub const PUS_EVENT_MANAGEMENT: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, 0);
pub const PUS_ROUTING_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusRouting as u32);
pub const PUS_TEST_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusTest as u32);
pub const PUS_MODE_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusMode as u32);
pub const PUS_HK_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::GenericPus as u16, PusId::PusHk as u32);
pub const PUS_SCHED_SERVICE: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Sched as u16, 0);
pub const MGM_HANDLER_0: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Acs as u16, AcsId::Mgm0 as u32);
pub const UDP_SERVER: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::UdpServer as u32);
pub const TCP_SERVER: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::TcpServer as u32);
pub const NO_SENDER: ComponentId = ComponentId::MAX;
}
pub mod pool {
use super::*;
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
(
StaticMemoryPool::new(StaticPoolConfig::new(
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
@@ -194,7 +138,7 @@ pub mod pool {
],
true,
)),
StaticMemoryPool::new(StaticPoolConfig::new(
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
@@ -209,7 +153,7 @@ pub mod pool {
}
pub fn create_sched_tc_pool() -> StaticMemoryPool {
StaticMemoryPool::new(StaticPoolConfig::new(
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
@@ -225,7 +169,7 @@ pub mod pool {
pub mod tasks {
pub const FREQ_MS_UDP_TMTC: u64 = 200;
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
pub const FREQ_MS_AOCS: u64 = 500;
pub const FREQ_MS_PUS_STACK: u64 = 200;
pub const FREQ_MS_AOCS: u64 = 200;
pub const FREQ_MS_CONTROLLER: u64 = 200;
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
}
+89
View File
@@ -0,0 +1,89 @@
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct Controller {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
}
impl Controller {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
) -> Self {
Self {
tc_rx,
tm_tx,
event_ctrl_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
control::request::Request::Ping => (),
control::request::Request::TestEvent => {
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
}
control::request::Request::SimConnect(_ipv4_addr) => {
// TODO: Does this make sense here? I guess it does to try
// a connect when the minisim is started after the OBSW?
log::warn!("sim connect request not supported yet");
}
}
self.send_telemetry(Some(tc_id), control::response::Response::Ok);
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: control::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
+200
View File
@@ -0,0 +1,200 @@
use std::collections::VecDeque;
use std::time::Duration;
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use satrs::health::{HealthState, HealthTableMapSync};
use types::{ComponentId, DeviceMode};
use crate::device_mode::SwitchAndModeHelper;
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
// before the counter is decremented again.
pub const RECOVERY_THRESHOLD: u32 = 2;
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
/// Generic FDIR events. The device handler maps them to its own event type.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FdirEvent {
FaultThresholdExceeded,
Recovery(RecoveryEvent),
}
/// Fault counting and power cycle recovery for device handlers which own the power switch of
/// their device.
///
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
/// events with [Self::next_event].
pub struct DeviceFdir {
name: &'static str,
fault_counter: FaultCounterStd,
recovery: RecoveryFdir<HealthTableMapSync>,
pub recovery_off_duration: Duration,
events: VecDeque<FdirEvent>,
}
impl DeviceFdir {
pub fn new(
name: &'static str,
component_id: ComponentId,
health_table: HealthTableMapSync,
fault_counter: FaultCounterStd,
) -> Self {
Self {
name,
fault_counter,
recovery: RecoveryFdir::new(
component_id.into(),
health_table,
RECOVERY_THRESHOLD,
RECOVERY_DECREMENT_AFTER,
),
recovery_off_duration: RECOVERY_OFF_DURATION,
events: VecDeque::new(),
}
}
#[cfg(test)]
pub fn fault_count(&self) -> u32 {
self.fault_counter.fault_count()
}
pub fn set_health(&mut self, health: HealthState) {
self.recovery.set_health(health);
}
pub fn next_event(&mut self) -> Option<FdirEvent> {
self.events.pop_front()
}
/// Should be called once per cycle, before the mode transition is handled. Starts a power
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
self.recovery.periodic_operation();
self.check_needs_recovery(modes);
}
pub fn register_success(&mut self) {
self.fault_counter.try_decrement();
}
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
/// too often, the component is marked faulty and commanded off instead.
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if !self.fault_counter.increment_and_check() {
return;
}
match self.recovery.handle_fault() {
FaultResponse::Ignored => {
log::info!(
"{}: fault threshold exceeded, but component is already faulty, \
recovering or externally controlled",
self.name
);
}
FaultResponse::Recover => {
log::warn!(
"{}: fault threshold exceeded, power cycling device",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.check_needs_recovery(modes);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: fault threshold exceeded after too many recoveries, marking \
component faulty",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
}
}
/// Mode commands from ground or the parent abort a running recovery. Must be called before
/// the commanded transition is started.
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() {
log::warn!("{}: mode command aborts power cycle recovery", self.name);
// Otherwise, the recovery would restart right away.
self.recovery.recovery_done();
}
}
pub fn handle_power_cycle_done(&mut self) {
log::info!("{}: power cycle recovery done", self.name);
// Faults registered while the device was switched off do not count anymore.
self.fault_counter.clear();
self.recovery.recovery_done();
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
}
/// A failed power cycle costs a recovery attempt like any other fault.
pub fn handle_power_cycle_failed(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
match self.recovery.recovery_failed() {
FaultResponse::Recover => {
log::warn!("{}: power cycle recovery failed, retrying", self.name);
self.start_recovery(modes, restore_mode);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: power cycle recovery failed too often, marking component faulty",
self.name
);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
// Ground changed the health during the recovery and is in charge now.
FaultResponse::Ignored => (),
}
}
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
{
return;
}
if modes.mode() == DeviceMode::Off {
// Nothing to power cycle, the next switch-on is a fresh start anyway.
log::info!("{}: device is off, no recovery required", self.name);
self.recovery.recovery_done();
return;
}
let restore_mode = modes.mode();
self.start_recovery(modes, restore_mode);
}
fn start_recovery(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
log::warn!("{}: starting power cycle recovery", self.name);
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
}
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
// Do not restart an already pending Off transition, which would reset the transition
// state machine before it can finish.
if modes.target() != Some(DeviceMode::Off) {
log::warn!("{}: commanding device off due to fault", self.name);
modes.start_transition(DeviceMode::Off, None);
}
}
}
+504
View File
@@ -0,0 +1,504 @@
use std::time::{Duration, Instant};
use types::pcdu::SwitchId;
use crate::eps::PowerSwitchHelper;
/// This is a helper trait required to make [SwitchAndModeHelper] generic.
///
/// It allows distinguish a powered-off state from one or more powered-on states, so
/// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode.
pub trait PowerSwitchedMode: Copy + PartialEq {
const OFF: Self;
fn requires_power(&self) -> bool;
}
impl PowerSwitchedMode for types::DeviceMode {
const OFF: Self = types::DeviceMode::Off;
fn requires_power(&self) -> bool {
*self != types::DeviceMode::Off
}
}
#[derive(Default, Debug, PartialEq, Eq)]
enum SwitchTransitionState {
#[default]
Idle,
PowerSwitching,
Done,
}
/// Outcome of a single power switch transition.
enum SwitchOutcome {
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
}
/// Dedicated states for power cycling a device.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PowerCycleState<Mode> {
Idle,
SwitchingOff { restore_mode: Mode },
WaitingOff { restore_mode: Mode, since: Instant },
SwitchingOn { restore_mode: Mode },
}
/// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`]
/// has driven it to completion. Carries back whichever TC commanded the transition, if any, so
/// the caller can reply to it -- what that reply looks like is handler-specific, so this stays
/// out of the helper.
pub enum ModeTransitionEvent<Mode> {
/// The target mode was reached.
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The target mode could not be reached.
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The power cycle completed and the mode before the power cycle was restored.
PowerCycleDone,
/// Power switching failed during the power cycle. The power cycle is not hidden anymore,
/// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. `restore_mode` is
/// the mode the power cycle should have restored, which can be used to retry it.
PowerCycleFailed { restore_mode: Mode },
}
/// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done)
/// shared by every device handler that owns a single power switch of its own.
///
/// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
/// transition finishes (or fails) and leaves what to do about it to the caller.
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
power_cycle_state: PowerCycleState<Mode>,
power_cycle_off_duration: Duration,
}
impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
pub fn new(
init_mode: Mode,
timeout: Duration,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
) -> Self {
Self {
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
switch_helper,
switch_id,
power_cycle_state: PowerCycleState::Idle,
power_cycle_off_duration: Duration::ZERO,
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
pub fn target(&self) -> Option<Mode> {
self.mode_helper.target
}
/// Mode which should be reported to other components. A power cycle is hidden from them,
/// so this is the mode which is restored after the power cycle while one is active.
pub fn reported_mode(&self) -> Mode {
match self.power_cycle_state {
PowerCycleState::SwitchingOff { restore_mode }
| PowerCycleState::WaitingOff { restore_mode, .. }
| PowerCycleState::SwitchingOn { restore_mode } => restore_mode,
PowerCycleState::Idle => self.mode(),
}
}
#[inline]
pub fn power_cycle_active(&self) -> bool {
self.power_cycle_state != PowerCycleState::Idle
}
/// Starts a new transition, aborting a running power cycle.
pub fn start_transition(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.power_cycle_state = PowerCycleState::Idle;
self.start_transition_internal(target_mode, tc_commander);
}
/// Switches the device off, keeps it off for `off_duration` and then switches it to
/// `restore_mode`. Reaching the intermediate off mode does not generate an event.
pub fn start_power_cycle(&mut self, restore_mode: Mode, off_duration: Duration) {
self.power_cycle_state = PowerCycleState::SwitchingOff { restore_mode };
self.power_cycle_off_duration = off_duration;
self.start_transition_internal(Mode::OFF, None);
}
fn start_transition_internal(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.mode_helper.tc_commander = tc_commander;
self.mode_helper.start(target_mode);
}
/// This is the main API that the periodic handler of a device handler should call.
///
/// It handles the switch commanding and returns relevant events.
pub fn handle_mode_transition(&mut self) -> Option<ModeTransitionEvent<Mode>> {
// The most probable case: Nothing to do.
if self.target().is_none() && !self.power_cycle_active() {
return None;
}
// Handle this as an extra step so the switch transition after this can proceed.
self.handle_waiting_for_off_when_power_cycling();
// Core logic: Command the switches, check whether target switch state was reached.
// Note the ?: if a switch transition is on-going, we might do an early return.
let outcome = self.handle_switch_transition()?;
// Regular mode transition without power cycling.
if self.power_cycle_state == PowerCycleState::Idle {
return Some(match outcome {
SwitchOutcome::Reached(tc_commander) => ModeTransitionEvent::Reached(tc_commander),
SwitchOutcome::Failed(tc_commander) => ModeTransitionEvent::Failed(tc_commander),
});
}
// Power cycling, where a bit more logic is required.
// Handle the error case first.
if let SwitchOutcome::Failed(_) = outcome {
let restore_mode = self.reported_mode();
self.power_cycle_state = PowerCycleState::Idle;
return Some(ModeTransitionEvent::PowerCycleFailed { restore_mode });
}
// At this point: The switching was succesfull, so we only match on the
// power cycle state.
match self.power_cycle_state {
// No switching going on for thse cases.
PowerCycleState::Idle | PowerCycleState::WaitingOff { .. } => None,
PowerCycleState::SwitchingOff { restore_mode } => {
self.power_cycle_state = PowerCycleState::WaitingOff {
restore_mode,
since: Instant::now(),
};
None
}
PowerCycleState::SwitchingOn { .. } => {
// Power is back and we are done.
self.power_cycle_state = PowerCycleState::Idle;
Some(ModeTransitionEvent::PowerCycleDone)
}
}
}
fn handle_waiting_for_off_when_power_cycling(&mut self) {
if let PowerCycleState::WaitingOff {
restore_mode,
since,
} = self.power_cycle_state
&& since.elapsed() >= self.power_cycle_off_duration
{
self.power_cycle_state = PowerCycleState::SwitchingOn { restore_mode };
self.start_transition_internal(restore_mode, None);
}
}
fn handle_switch_transition(&mut self) -> Option<SwitchOutcome> {
let target_mode = self.mode_helper.target?;
let switch_target_on = target_mode.requires_power();
if self.mode_helper.transition_state == SwitchTransitionState::Idle {
let result = if switch_target_on {
self.switch_helper.send_switch_on_cmd(self.switch_id)
} else {
self.switch_helper.send_switch_off_cmd(self.switch_id)
};
if result.is_err() {
// Could not send switch command.. still continue with transition.
log::error!(
"failed to send switch {} command",
if switch_target_on { "on" } else { "off" }
);
}
self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching;
}
if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching {
if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on {
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
self.mode_helper.transition_state = SwitchTransitionState::Done;
} else if self.mode_helper.timed_out() {
return Some(SwitchOutcome::Failed(self.mode_helper.finish(false)));
}
}
if self.mode_helper.transition_state == SwitchTransitionState::Done {
return Some(SwitchOutcome::Reached(self.mode_helper.finish(true)));
}
None
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex, mpsc};
use arbitrary_int::u11;
use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use types::{
DeviceMode,
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
const TIMEOUT: Duration = Duration::from_millis(50);
struct Testbench {
helper: SwitchAndModeHelper<DeviceMode>,
switch_rx: mpsc::Receiver<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
impl Testbench {
fn new() -> Self {
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgm0, SwitchState::Off);
let shared_switch_set: SharedSwitchSet =
Arc::new(Mutex::new(SwitchSet::new(switch_map)));
Self {
helper: SwitchAndModeHelper::new(
DeviceMode::Off,
TIMEOUT,
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
SwitchId::Mgm0,
),
switch_rx,
shared_switch_set,
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgm0, state);
}
fn switch_requests(&self) -> Vec<SwitchStateBinary> {
self.switch_rx
.try_iter()
.map(|req| req.target_state)
.collect()
}
/// Drives a transition to `Normal` to completion.
fn switch_to_normal(&mut self) {
self.helper.start_transition(DeviceMode::Normal, None);
self.set_switch_state(SwitchState::On);
assert!(matches!(
self.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
self.switch_requests();
}
/// Starts a power cycle from `Normal` and drives it until the device is off.
fn power_cycle_until_off(&mut self, off_duration: Duration) {
self.switch_to_normal();
self.helper
.start_power_cycle(DeviceMode::Normal, off_duration);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]);
self.set_switch_state(SwitchState::Off);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.helper.mode(), DeviceMode::Off);
}
}
fn tc_id() -> CcsdsPacketIdAndPsc {
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1)))
}
#[test]
fn test_no_transition() {
let mut tb = Testbench::new();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.handle_mode_transition().is_none());
assert!(tb.switch_requests().is_empty());
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_switch_on() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode reached event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert_eq!(tb.helper.target(), None);
assert!(tb.switch_requests().is_empty());
}
#[test]
fn test_switch_off() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper.start_transition(DeviceMode::Off, None);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]);
tb.set_switch_state(SwitchState::Off);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
}
#[test]
fn test_switch_already_in_target_state() {
let mut tb = Testbench::new();
tb.set_switch_state(SwitchState::On);
tb.helper.start_transition(DeviceMode::On, None);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
// The switch command is still sent.
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
#[test]
fn test_switch_timeout() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode failed event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
}
#[test]
fn test_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.power_cycle_active());
// The off duration elapsed, so switching on starts right away.
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleDone)
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_reports_restored_mode() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::from_secs(60));
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
tb.helper.handle_mode_transition();
tb.set_switch_state(SwitchState::Off);
tb.helper.handle_mode_transition();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_reports_restored_mode_while_switching_on() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_waits_off_duration() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
for _ in 0..3 {
assert!(tb.helper.handle_mode_transition().is_none());
}
assert!(tb.switch_requests().is_empty());
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_off_timeout() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_on_timeout() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert!(!tb.helper.power_cycle_active());
// The failed power cycle is not hidden anymore.
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_transition_aborts_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
tb.helper.start_transition(DeviceMode::On, Some(tc_id()));
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
tb.set_switch_state(SwitchState::On);
// A regular transition event instead of a power cycle event.
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(Some(_)))
));
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
}
+119
View File
@@ -0,0 +1,119 @@
use derive_new::new;
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
use types::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
use satrs::{queue::GenericSendError, request::MessageMetadata};
use thiserror::Error;
use crate::eps::pcdu::SwitchMapWrapper;
use self::pcdu::SharedSwitchSet;
pub mod pcdu;
#[derive(new, Clone)]
pub struct PowerSwitchHelper {
switcher_tx: mpsc::SyncSender<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum SwitchCommandingError {
#[error("send error: {0}")]
Send(#[from] GenericSendError),
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
pub enum SwitchInfoError {
/// This is a configuration error which should not occur.
#[error("switch ID not in map")]
SwitchIdNotInMap(SwitchId),
#[error("switch set invalid")]
SwitchSetInvalid,
}
impl PowerSwitchHelper {
pub fn send_switch_on_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::On))?;
Ok(())
}
pub fn send_switch_off_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::Off))?;
Ok(())
}
pub fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, SwitchInfoError> {
let switch_set = self
.shared_switch_set
.lock()
.expect("failed to lock switch set");
if !switch_set.valid {
return Err(SwitchInfoError::SwitchSetInvalid);
}
if let Some(state) = switch_set.switch_map.get(&switch_id) {
return Ok(*state);
}
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
}
#[allow(dead_code)]
fn switch_delay_ms(&self) -> Duration {
// Here, we could set device specific switch delays theoretically. Set it to this value
// for now.
Duration::from_millis(1000)
}
pub fn is_switch_on(&self, switch_id: SwitchId) -> bool {
if let Ok(state) = self.switch_state(switch_id) {
state == SwitchState::On
} else {
false
}
}
}
#[allow(dead_code)]
#[derive(new)]
pub struct SwitchRequestInfo {
pub requestor_info: MessageMetadata,
pub switch_id: SwitchId,
pub target_state: SwitchStateBinary,
}
// Test switch helper which can be used for unittests.
#[allow(dead_code)]
pub struct TestSwitchHelper {
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
#[allow(dead_code)]
pub switch_delay_request_count: u32,
pub next_switch_delay: Duration,
pub switch_map: RefCell<SwitchMapWrapper>,
pub switch_map_valid: bool,
}
impl Default for TestSwitchHelper {
fn default() -> Self {
Self {
switch_requests: Default::default(),
switch_info_requests: Default::default(),
switch_delay_request_count: Default::default(),
next_switch_delay: Duration::from_millis(1000),
switch_map: Default::default(),
switch_map_valid: true,
}
}
}
#[allow(dead_code)]
impl TestSwitchHelper {
// Helper function which can be used to force a switch to another state for test purposes.
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
self.switch_map.get_mut().0.insert(switch, state);
}
}
+773
View File
@@ -0,0 +1,773 @@
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
sync::{Arc, Mutex, mpsc},
};
use derive_new::new;
use example_std::TimestampHelper;
use minisim_types::{
SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator as _;
use types::{
ComponentId, DeviceMode,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
SwitchStateBinary, SwitchesBitfield,
},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwitchSet {
pub valid: bool,
pub switch_map: SwitchMap,
}
impl SwitchSet {
pub fn new(switch_map: SwitchMap) -> Self {
Self {
valid: true,
switch_map,
}
}
pub fn new_with_init_switches_unknown() -> Self {
let wrapper = SwitchMapWrapper::default();
Self::new(wrapper.0)
}
pub fn as_bitfield(&self) -> Option<SwitchesBitfield> {
for entry in SwitchId::iter() {
if !self.switch_map.contains_key(&entry) {
return None;
}
}
Some(
SwitchesBitfield::builder()
.with_magnetorquer(*self.switch_map.get(&SwitchId::Mgt).unwrap() == SwitchState::On)
.with_mgm1(*self.switch_map.get(&SwitchId::Mgm1).unwrap() == SwitchState::On)
.with_mgm0(*self.switch_map.get(&SwitchId::Mgm0).unwrap() == SwitchState::On)
.build(),
)
}
#[allow(dead_code)]
pub fn set_switch_state(&mut self, switch_id: SwitchId, state: SwitchState) -> bool {
if !self.switch_map.contains_key(&switch_id) {
return false;
}
*self.switch_map.get_mut(&switch_id).unwrap() = state;
true
}
}
pub type SwitchMap = HashMap<SwitchId, SwitchState>;
pub struct SwitchMapWrapper(pub SwitchMap);
impl Default for SwitchMapWrapper {
fn default() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Unknown);
}
Self(switch_map)
}
}
impl SwitchMapWrapper {
#[allow(dead_code)]
pub fn new_with_init_switches_off() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Off);
}
Self(switch_map)
}
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
Self(
switch_map
.iter()
.map(|(key, value)| (*key, SwitchState::from(*value)))
.collect(),
)
}
}
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
pub trait SerialInterface {
type Error: core::fmt::Debug;
/// Send some data via the serial interface.
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
/// Receive all replies received on the serial interface so far. This function takes a closure
/// and call its for each received packet, passing the received packet into it.
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error>;
}
#[derive(new)]
pub struct SerialInterfaceToSim {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum SetId {
SwitcherSet = 0,
}
impl SerialInterface for SerialInterfaceToSim {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
self.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(request))
.expect("failed to send request to simulation");
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
loop {
match self.sim_reply_rx.try_recv() {
Ok(reply) => {
let reply = postcard::to_allocvec(&reply).unwrap();
f(&reply);
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("sim reply sender has disconnected");
break;
}
},
}
}
Ok(())
}
}
#[derive(Default)]
pub struct SerialInterfaceDummy {
// Need interior mutability here for both fields.
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
pub reply_deque: RefCell<VecDeque<SimReply>>,
}
impl SerialInterface for SerialInterfaceDummy {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
match pcdu_req {
PcduRequest::SwitchDevice { switch, state } => {
switch_map_mut
.insert(switch, state)
.expect("switch map capacity exceeded");
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
switch_map_mut.clone(),
)));
}
};
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.next_reply_serialized();
f(&reply);
if self.reply_queue_empty() {
break;
}
}
Ok(())
}
}
impl SerialInterfaceDummy {
fn next_reply_serialized(&self) -> Vec<u8> {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
let next_reply = reply_deque_mut.pop_front().unwrap();
postcard::to_allocvec(&next_reply).unwrap()
}
fn reply_queue_empty(&self) -> bool {
self.reply_deque.borrow().is_empty()
}
}
pub enum SerialSimInterfaceWrapper {
Dummy(SerialInterfaceDummy),
Sim(SerialInterfaceToSim),
}
impl SerialInterface for SerialSimInterfaceWrapper {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
}
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpCode {
RegularOp = 0,
PollAndRecvReplies = 1,
}
/// Example PCDU device handler.
#[allow(clippy::too_many_arguments)]
pub struct PcduHandler<ComInterface: SerialInterface> {
dev_str: &'static str,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
pub com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
mode: DeviceMode,
stamp_helper: TimestampHelper,
event_tx: mpsc::SyncSender<pcdu::Event>,
}
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
init_mode: DeviceMode,
event_tx: mpsc::SyncSender<pcdu::Event>,
) -> Self {
Self {
dev_str: "PCDU",
tc_rx,
switch_request_rx,
tm_tx,
com_interface,
shared_switch_map,
stamp_helper: TimestampHelper::default(),
// Start in normal mode by default. Assume that the PCDU itself is on by default.
mode: init_mode,
event_tx,
}
}
pub fn periodic_operation(&mut self, op_code: OpCode) {
match op_code {
OpCode::RegularOp => {
self.stamp_helper.update_from_now();
// Handle requests.
self.handle_telecommands();
self.handle_switch_requests();
// Poll the switch states and/or telemetry regularly here.
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
self.handle_periodic_commands();
}
}
OpCode::PollAndRecvReplies => {
self.poll_and_handle_replies();
}
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.mode
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<pcdu::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
pcdu::request::Request::Ping => {
self.send_tm(Some(tc_id), pcdu::response::Response::Ok)
}
pcdu::request::Request::GetSwitches => self.send_tm(
Some(tc_id),
pcdu::response::Response::Switches(
self.shared_switch_map
.lock()
.unwrap()
.as_bitfield()
.expect("could not build switches response"),
),
),
pcdu::request::Request::EnableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::On,
);
}
pcdu::request::Request::DisableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::Off,
);
}
pcdu::request::Request::Mode(device_mode) => {
self.switch_mode(tc_id, device_mode)
}
}
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_switches_bitfield_request(
&mut self,
switches: SwitchesBitfield,
state: SwitchStateBinary,
) {
if switches.mgm0() {
self.handle_device_switching(SwitchId::Mgm0, state);
}
if switches.mgm1() {
self.handle_device_switching(SwitchId::Mgm1, state);
}
if switches.magnetorquer() {
self.handle_device_switching(SwitchId::Mgt, state);
}
}
pub fn send_tm(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: pcdu::response::Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
fn switch_mode(&mut self, requestor: CcsdsPacketIdAndPsc, mode: DeviceMode) {
log::info!("{}: transitioning to mode {:?}", self.dev_str, mode);
self.mode = mode;
if self.mode() == DeviceMode::Off {
self.shared_switch_map.lock().unwrap().valid = false;
}
log::info!("{} announcing mode: {:?}", self.dev_str, self.mode);
self.send_telemetry(Some(requestor), pcdu::response::Response::Ok);
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: pcdu::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_periodic_commands(&self) {
let pcdu_req = PcduRequest::RequestSwitchInfo;
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
log::warn!("polling PCDU switch info failed");
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
log::warn!("failed to send comm error event: {}", e);
}
}
}
/*
pub fn handle_mode_requests(&mut self) {
loop {
// TODO: Only allow one set mode request per cycle?
match self.mode_node.try_recv_mode_request() {
Ok(opt_msg) => {
if let Some(msg) = opt_msg {
let result = self.handle_mode_request(msg);
// TODO: Trigger event?
if result.is_err() {
log::warn!(
"{}: mode request failed with error {:?}",
self.dev_str,
result.err().unwrap()
);
}
} else {
break;
}
}
Err(e) => match e {
satrs::queue::GenericReceiveError::Empty => {
break;
}
satrs::queue::GenericReceiveError::TxDisconnected(_) => {
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
}
},
}
}
}
*/
pub fn handle_device_switching(&mut self, switch_id: SwitchId, state: SwitchStateBinary) {
let pcdu_req = PcduRequest::SwitchDevice {
switch: switch_id,
state,
};
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
self.com_interface
.send(&pcdu_req_ser)
.expect("failed to send switch request to PCDU");
}
pub fn handle_switch_requests(&mut self) {
loop {
match self.switch_request_rx.try_recv() {
Ok(switch_req) => {
self.handle_device_switching(switch_req.switch_id(), switch_req.target_state());
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("switch request receiver has disconnected");
break;
}
},
};
}
}
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
return;
};
match pcdu_reply {
PcduReply::SwitchInfo(switch_info) => {
let switch_map_wrapper =
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
let mut shared_switch_map = self
.shared_switch_map
.lock()
.expect("failed to lock switch map");
shared_switch_map.switch_map = switch_map_wrapper.0;
shared_switch_map.valid = true;
}
}
}) {
log::warn!("receiving PCDU replies failed: {e:?}");
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, TcHeader,
pcdu::{SwitchMapBinary, SwitchStateBinary},
};
use super::*;
pub fn create_request_tc(
request: types::pcdu::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Eps as u16)),
TcHeader::new(ComponentId::EpsPcdu, request.message_type()),
request,
)
}
#[derive(Default)]
pub struct SerialInterfaceTest {
pub inner: SerialInterfaceDummy,
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
/// Makes the next `send` call fail, to exercise comm-error handling.
pub fail_next_send: RefCell<bool>,
}
impl SerialInterface for SerialInterfaceTest {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
if self.fail_next_send.replace(false) {
return Err(());
}
let mut send_queue_mut = self.send_queue.borrow_mut();
send_queue_mut.push_back(data.to_vec());
self.inner.send(data)
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.inner.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.inner.next_reply_serialized();
self.reply_queue.borrow_mut().push_back(reply.clone());
f(&reply);
if self.inner.reply_queue_empty() {
break;
}
}
Ok(())
}
}
#[allow(dead_code)]
pub struct PcduTestbench {
pub mode_request_tx: mpsc::SyncSender<types::pcdu::request::Request>,
pub mode_reply_rx_to_parent: mpsc::Receiver<types::pcdu::response::Response>,
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
pub switch_request_tx: mpsc::Sender<SwitchRequest>,
pub event_rx: mpsc::Receiver<pcdu::Event>,
pub handler: PcduHandler<SerialInterfaceTest>,
}
impl PcduTestbench {
pub fn new() -> Self {
let (mode_request_tx, _mode_request_rx) = mpsc::sync_channel(5);
let (_mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
let (event_tx, event_rx) = mpsc::sync_channel(5);
let shared_switch_map =
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let handler = PcduHandler::new(
tc_rx,
tm_tx.clone(),
switch_reqest_rx,
SerialInterfaceTest::default(),
shared_switch_map,
DeviceMode::Off,
event_tx,
);
Self {
mode_request_tx,
mode_reply_rx_to_parent,
tc_tx,
tm_rx,
switch_request_tx,
event_rx,
handler,
}
}
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
}
pub fn verify_switch_req_was_sent(
&self,
expected_queue_len: usize,
switch_id: SwitchId,
target_state: SwitchStateBinary,
) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(
pcdu_req,
PcduRequest::SwitchDevice {
switch: switch_id,
state: target_state
}
)
}
pub fn verify_switch_reply_received(
&self,
expected_queue_len: usize,
expected_map: SwitchMapBinary,
) {
// Check that a switch reply was read back.
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
assert_eq!(reply_received_mut.len(), expected_queue_len);
let reply_received = reply_received_mut.pop_front().unwrap();
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
assert_eq!(
sim_reply,
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
);
}
}
#[test]
fn test_periodic_command_send_failure_sends_event() {
let testbench = PcduTestbench::new();
*testbench.handler.com_interface.fail_next_send.borrow_mut() = true;
testbench.handler.handle_periodic_commands();
let event = testbench
.event_rx
.try_recv()
.expect("expected comm error event");
assert!(matches!(event, pcdu::Event::SerialCommError));
}
#[test]
fn test_basic_handler() {
let mut testbench = PcduTestbench::new();
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Handler is OFF, no changes expected.
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
}
#[test]
fn test_normal_mode() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Check correctness of mode.
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
testbench.verify_switch_info_req_was_sent(1);
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
#[test]
fn test_switch_request_handling() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
testbench
.switch_request_tx
.send(SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On))
.expect("failed to send switch request");
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
testbench.verify_switch_info_req_was_sent(1);
let mut switch_map = SwitchMapBinaryWrapper::default().0;
*switch_map
.get_mut(&SwitchId::Mgm0)
.expect("switch state setting failed") = SwitchStateBinary::On;
testbench.verify_switch_reply_received(1, switch_map);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
}
+340
View File
@@ -0,0 +1,340 @@
use std::collections::HashSet;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, Event, EventId, Message,
acs::{mgm, mgm_assembly, mgt},
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
event_manager::{request::Request, response::Response},
pcdu, tmtc,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct EventManager {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
/// Shared by all MGM instances, which is why the sender ID is part of the message.
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
/// Shared by all TC sources, which is why the sender ID is part of the message.
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
/// Senders with all their event TM generation disabled.
disabled_components: HashSet<ComponentId>,
/// Individual (sender, event ID) pairs with their TM generation disabled.
disabled_events: HashSet<(ComponentId, u16)>,
}
impl EventManager {
#[allow(clippy::too_many_arguments)]
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
disabled_components: HashSet::new(),
disabled_events: HashSet::new(),
}
}
/// Silences all event TM generation for `sender_id`, regardless of the specific event.
fn disable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.insert(sender_id);
}
fn enable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.remove(&sender_id);
}
/// Silences TM generation for one specific event of `sender_id`, leaving its other events
/// unaffected.
fn disable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.insert((sender_id, event_id));
}
fn enable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.remove(&(sender_id, event_id));
}
fn tm_generation_enabled(&self, sender_id: ComponentId, event_id: u16) -> bool {
!self.disabled_components.contains(&sender_id)
&& !self.disabled_events.contains(&(sender_id, event_id))
}
pub fn periodic_operation(&mut self) {
// Telecommands first, so filter changes already apply to the events of this cycle.
self.handle_telecommands();
while let Ok(event) = self.ctrl_rx.try_recv() {
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
}
while let Ok((sender_id, event)) = self.mgm_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
}
while let Ok(event) = self.mgt_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgt, &event);
}
while let Ok(event) = self.pcdu_rx.try_recv() {
self.event_to_tm(ComponentId::EpsPcdu, &event);
}
while let Ok((sender_id, event)) = self.tc_source_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("failed to deserialize event manager request: {}", e);
continue;
}
};
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::EnableComponent(sender_id) => self.enable_component(sender_id),
Request::DisableComponent(sender_id) => self.disable_component(sender_id),
Request::EnableEvent {
sender_id,
event_id,
} => self.enable_event(sender_id, event_id),
Request::DisableEvent {
sender_id,
event_id,
} => self.disable_event(sender_id, event_id),
}
self.send_tm(ComponentId::EventManager, Some(tc_id), &Response::Ok);
}
}
pub fn event_to_tm(
&mut self,
sender_id: ComponentId,
event: &(impl serde::Serialize + Message + EventId + core::fmt::Debug),
) {
let tm_enabled = self.tm_generation_enabled(sender_id, event.event_id());
log::info!(
"event {:?} (ID {}) from {:?}{}",
event,
event.event_id(),
sender_id,
if tm_enabled { "" } else { ", TM disabled" }
);
if !tm_enabled {
return;
}
self.send_tm(sender_id, None, event);
}
fn send_tm(
&self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
match pack_ccsds_tm_packet_for_now(sender_id, tc_id, payload) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("error sending TM packet: {:?}", e);
}
}
Err(e) => {
log::warn!("error packing TM packet: {:?}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::{self, TryRecvError};
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{Apid, MessageType, TcHeader, acs::mgm};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
mgm_event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_manager: EventManager,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
Self {
tc_tx,
mgm_event_tx,
tm_rx,
event_manager: EventManager::new(
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
),
}
}
fn send_request(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(ComponentId::EventManager, MessageType::Event),
request,
))
.unwrap();
}
}
#[test]
fn test_events_enabled_by_default() {
let mut tb = Testbench::new();
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_component_silences_all_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disabled_component_does_not_affect_other_components() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm1);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_event_only_silences_that_specific_event() {
let mut tb = Testbench::new();
tb.event_manager.disable_event(
ComponentId::AcsMgm0,
mgm::Event::SpiFaultThresholdExceeded.event_id(),
);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected event TM");
let event: mgm::Event = postcard::from_bytes(&tm.payload).unwrap();
assert!(matches!(event, mgm::Event::ModeChanged(_)));
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_re_enabling_a_component_restores_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.event_manager.enable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disable_component_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableComponent(ComponentId::AcsMgm0));
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected request response TM");
assert_eq!(tm.tm_header.sender_id, ComponentId::EventManager);
assert!(tm.tm_header.tc_id.is_some());
let response: Response = postcard::from_bytes(&tm.payload).unwrap();
assert_eq!(response, Response::Ok);
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disable_event_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.send_request(Request::EnableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.tm_rx.try_recv().expect("expected event TM");
}
}
@@ -1,3 +1,4 @@
//! This module contains all component related to the direct interface of the example.
pub mod sim_client_udp;
pub mod tcp;
pub mod udp;
@@ -0,0 +1,402 @@
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
sync::mpsc,
time::Duration,
};
use minisim_types::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
use minisim_types::{SimCtrlReply, SimCtrlRequest};
use satrs::HandlingStatus;
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
pub fn create_sim_client(
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Option<SimClientUdp> {
match SimClientUdp::new(
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
sim_request_rx,
) {
Ok(sim_client) => {
log::info!("simulator client connection success");
return Some(sim_client);
}
Err(e) => {
log::warn!("sim client creation error: {e}");
}
}
None
}
#[derive(thiserror::Error, Debug)]
pub enum SimClientCreationError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("timeout when trying to connect to sim UDP server")]
Timeout,
#[error("invalid ping reply when trying connection to UDP sim server: {0}")]
InvalidReply(#[from] postcard::Error),
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
ReplyIsNotPong(Box<SimReply>),
}
pub struct SimClientUdp {
udp_client: UdpSocket,
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
reply_map: SimReplyMap,
reply_buf: [u8; 4096],
}
impl SimClientUdp {
pub fn new(
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Result<Self, SimClientCreationError> {
let mut reply_buf: [u8; 4096] = [0; 4096];
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
udp_client.set_read_timeout(Some(Duration::from_millis(100)))?;
Self::attempt_connection(&mut udp_client, simulator_addr, &mut reply_buf)?;
udp_client.set_nonblocking(true)?;
Ok(Self {
udp_client,
simulator_addr,
sim_request_rx,
reply_map: SimReplyMap(HashMap::new()),
reply_buf,
})
}
pub fn attempt_connection(
udp_client: &mut UdpSocket,
simulator_addr: SocketAddr,
reply_buf: &mut [u8],
) -> Result<(), SimClientCreationError> {
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req_raw = postcard::to_allocvec(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(&sim_req_raw, simulator_addr)?;
match udp_client.recv(reply_buf) {
Ok(reply_len) => {
let sim_reply: SimReply = postcard::from_bytes(&reply_buf[0..reply_len])?;
match sim_reply {
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
_ => Err(SimClientCreationError::ReplyIsNotPong(Box::new(sim_reply))),
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut
|| e.kind() == std::io::ErrorKind::WouldBlock
{
Err(SimClientCreationError::Timeout)
} else {
Err(SimClientCreationError::Io(e))
}
}
}
}
pub fn operation(&mut self) -> HandlingStatus {
let mut no_sim_requests_handled = true;
let mut no_data_from_udp_server_received = true;
loop {
match self.sim_request_rx.try_recv() {
Ok(request) => {
let request_raw =
postcard::to_allocvec(&request).expect("failed to serialize SimRequest");
if let Err(e) = self.udp_client.send_to(&request_raw, self.simulator_addr) {
log::error!("error sending data to UDP SIM server: {e}");
break;
} else {
no_sim_requests_handled = false;
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => {
break;
}
mpsc::TryRecvError::Disconnected => {
log::warn!("SIM request sender disconnected");
break;
}
},
}
}
loop {
match self.udp_client.recv(&mut self.reply_buf) {
Ok(recvd_bytes) => {
no_data_from_udp_server_received = false;
let sim_reply_result: postcard::Result<SimReply> =
postcard::from_bytes(&self.reply_buf[0..recvd_bytes]);
match sim_reply_result {
Ok(sim_reply) => {
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
sender.send(sim_reply).expect("failed to send SIM reply");
} else {
log::warn!(
"no recipient for SIM reply from component {:?}",
sim_reply.component()
);
}
}
Err(e) => {
log::warn!("failed to deserialize SIM reply: {e}");
}
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
break;
}
log::error!("error receiving data from UDP SIM server: {e}");
break;
}
}
}
if no_sim_requests_handled && no_data_from_udp_server_received {
return HandlingStatus::Empty;
}
HandlingStatus::HandledOne
}
pub fn add_reply_recipient(
&mut self,
component: ComponentId,
reply_sender: mpsc::Sender<SimReply>,
) {
self.reply_map.0.insert(component, reply_sender);
}
}
#[cfg(test)]
pub mod tests {
use std::{
net::{SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
},
time::Duration,
};
use minisim_types::{
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use types::pcdu::SwitchMapBinary;
use super::SimClientUdp;
struct UdpSimTestServer {
udp_server: UdpSocket,
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
last_sender: Option<SocketAddr>,
stop_signal: Arc<AtomicBool>,
recv_buf: [u8; 1024],
}
impl UdpSimTestServer {
pub fn new(
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
stop_signal: Arc<AtomicBool>,
) -> Self {
let udp_server = UdpSocket::bind("127.0.0.1:0").expect("creating UDP server failed");
udp_server
.set_nonblocking(true)
.expect("failed to set UDP server to non-blocking");
Self {
udp_server,
request_tx,
reply_rx,
last_sender: None,
stop_signal,
recv_buf: [0; 1024],
}
}
pub fn operation(&mut self) {
loop {
let mut no_sim_replies_handled = true;
let mut no_data_received = true;
if self.stop_signal.load(Ordering::Relaxed) {
break;
}
if let Some(last_sender) = self.last_sender {
loop {
match self.reply_rx.try_recv() {
Ok(sim_reply) => {
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(&sim_reply_raw, last_sender)
.expect("failed to send reply to client from UDP server");
no_sim_replies_handled = false;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
panic!("reply sender disconnected")
}
},
}
}
}
loop {
match self.udp_server.recv_from(&mut self.recv_buf) {
Ok((read_bytes, from)) => {
let sim_request: SimRequestWithTime =
postcard::from_bytes(&self.recv_buf[0..read_bytes])
.expect("failed to deserialize SimRequest");
// For a ping, we perform the reply handling here directly
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(&sim_reply_raw, from)
.expect("failed to send reply to client from UDP server");
}
// Forward each SIM request for testing purposes.
self.request_tx
.send(sim_request)
.expect("failed to send request");
}
Err(e) => {
if e.kind() != std::io::ErrorKind::WouldBlock
&& e.kind() != std::io::ErrorKind::TimedOut
{
panic!("UDP server error: {}", e);
}
break;
}
}
}
if no_sim_replies_handled && no_data_received {
std::thread::sleep(Duration::from_millis(5));
}
}
}
pub fn local_addr(&self) -> SocketAddr {
self.udp_server.local_addr().unwrap()
}
}
#[test]
fn basic_connection_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (_server_sim_reply_tx, server_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
server_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (_client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
#[test]
fn basic_request_reply_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (server_sim_reply_tx, sever_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
sever_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
let (client_pcdu_reply_tx, client_pcdu_reply_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
client.add_reply_recipient(ComponentId::Pcdu, client_pcdu_reply_tx);
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
let pcdu_req = PcduRequest::RequestSwitchInfo;
client_sim_req_tx
.send(SimRequestWithTime::new_with_epoch_time(pcdu_req))
.expect("send failed");
client.operation();
// Check that the request arrives properly at the server.
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::Pcdu(PcduRequest::RequestSwitchInfo)
);
// We inject the reply ourselves.
let pcdu_reply = PcduReply::SwitchInfo(SwitchMapBinary::default());
server_sim_reply_tx
.send(SimReply::from(pcdu_reply.clone()))
.expect("sending PCDU reply failed");
// Now we verify that the reply is sent by the UDP server back to the client, and then
// forwarded by the clients internal map.
let mut pcdu_reply_received = false;
for _ in 0..3 {
client.operation();
match client_pcdu_reply_rx.try_recv() {
Ok(sim_reply) => {
assert_eq!(sim_reply.component(), ComponentId::Pcdu);
assert_eq!(sim_reply, SimReply::Pcdu(pcdu_reply.clone()));
pcdu_reply_received = true;
break;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => std::thread::sleep(Duration::from_millis(10)),
mpsc::TryRecvError::Disconnected => panic!("reply sender disconnected"),
},
}
}
if !pcdu_reply_received {
panic!("no reply received");
}
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
}
@@ -1,18 +1,20 @@
use std::time::Duration;
use std::{
collections::{HashSet, VecDeque},
fmt::Debug,
marker::PhantomData,
sync::{Arc, Mutex},
};
use log::{info, warn};
use satrs::hal::std::tcp_spacepackets_server::CcsdsPacketParser;
use satrs::{
encoding::ccsds::{SpValidity, SpacePacketValidator},
hal::std::tcp_server::{HandledConnectionHandler, ServerConfig, TcpSpacepacketsServer},
spacepackets::{CcsdsPacket, PacketId},
tmtc::{PacketSenderRaw, PacketSource},
tmtc::PacketSource,
};
use crate::tmtc::sender::TmTcSender;
#[derive(Default)]
pub struct ConnectionFinishedHandler {}
@@ -29,7 +31,7 @@ impl SpacePacketValidator for SimplePacketValidator {
if self.valid_ids.contains(&sp_header.packet_id()) {
return SpValidity::Valid;
}
log::warn!("ignoring space packet with header {:?}", sp_header);
log::warn!("ignoring space packet with header {sp_header:?}");
// We could perform a CRC check.. but lets keep this simple and assume that TCP ensures
// data integrity.
SpValidity::Skip
@@ -101,50 +103,39 @@ impl PacketSource for SyncTcpTmSource {
}
}
pub type TcpServer<ReceivesTc, SendError> = TcpSpacepacketsServer<
pub type TcpServer<ReceivesTc> = TcpSpacepacketsServer<
SyncTcpTmSource,
ReceivesTc,
SimplePacketValidator,
ConnectionFinishedHandler,
(),
SendError,
>;
pub struct TcpTask<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>(
pub TcpServer<TcSender, SendError>,
PhantomData<SendError>,
);
pub struct TcpTask(pub TcpServer<TmTcSender>);
impl<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>
TcpTask<TcSender, SendError>
{
impl TcpTask {
pub fn new(
cfg: ServerConfig,
tm_source: SyncTcpTmSource,
tc_sender: TcSender,
tc_sender: TmTcSender,
valid_ids: HashSet<PacketId>,
) -> Result<Self, std::io::Error> {
Ok(Self(
TcpSpacepacketsServer::new(
cfg,
tm_source,
tc_sender,
SimplePacketValidator { valid_ids },
ConnectionFinishedHandler::default(),
None,
)?,
PhantomData,
))
Ok(Self(TcpSpacepacketsServer::new(
cfg,
tm_source,
CcsdsPacketParser::new(cfg.id, 2048, tc_sender, SimplePacketValidator { valid_ids }),
ConnectionFinishedHandler::default(),
None,
)?))
}
pub fn periodic_operation(&mut self) {
loop {
let result = self.0.handle_all_connections(None);
match result {
Ok(_conn_result) => (),
Err(e) => {
warn!("TCP server error: {e:?}");
}
let result = self
.0
.handle_all_connections(Some(Duration::from_millis(400)));
match result {
Ok(_conn_result) => (),
Err(e) => {
warn!("TCP server error: {e:?}");
}
}
}
+223
View File
@@ -0,0 +1,223 @@
#![allow(dead_code)]
use std::collections::VecDeque;
use std::net::{SocketAddr, UdpSocket};
use std::sync::{Arc, Mutex, mpsc};
use log::warn;
use satrs::HandlingStatus;
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use satrs::queue::GenericSendError;
use types::ccsds::CcsdsTmPacketOwned;
use crate::tmtc::sender::TmTcSender;
pub trait UdpTmHandlerProvider {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
pub struct UdpTmHandlerWithChannel {
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
}
impl UdpTmHandlerProvider for UdpTmHandlerWithChannel {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
while let Ok(tm) = self.tm_rx.try_recv() {
log::debug!("sending TM from sender {:?}", tm.tm_header.sender_id);
let result = socket.send_to(&tm.to_vec(), recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
#[derive(Default, Debug, Clone)]
pub struct TestTmHandler {
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
}
impl UdpTmHandlerProvider for TestTmHandler {
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
}
}
pub enum UdpTmHandler {
Normal(UdpTmHandlerWithChannel),
Test(TestTmHandler),
}
impl From<UdpTmHandlerWithChannel> for UdpTmHandler {
fn from(handler: UdpTmHandlerWithChannel) -> Self {
UdpTmHandler::Normal(handler)
}
}
impl From<TestTmHandler> for UdpTmHandler {
fn from(handler: TestTmHandler) -> Self {
UdpTmHandler::Test(handler)
}
}
impl UdpTmHandlerProvider for UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
match self {
UdpTmHandler::Normal(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
UdpTmHandler::Test(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
}
}
}
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<TmTcSender, GenericSendError>,
pub tm_handler: UdpTmHandler,
}
impl UdpTmtcServer {
pub fn periodic_operation(&mut self) {
loop {
if self.poll_tc_server() == HandlingStatus::Empty {
break;
}
}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
}
}
fn poll_tc_server(&mut self) -> HandlingStatus {
match self.udp_tc_server.try_recv_tc() {
Ok(_) => HandlingStatus::HandledOne,
Err(e) => {
match e {
ReceiveResult::NothingReceived => (),
ReceiveResult::Io(e) => {
warn!("IO error {e}");
}
ReceiveResult::Send(send_error) => {
warn!("send error {send_error:?}");
}
}
HandlingStatus::Empty
}
}
}
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use std::net::Ipv4Addr;
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u14;
use example_std::config::OBSW_SERVER_ADDR;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
ComponentId,
spacepackets::{
SpHeader,
ecss::{WritablePusPacket, tc::PusTcCreator},
},
};
use types::Apid;
use crate::tmtc::sender::MockSender;
use super::*;
const UDP_SERVER_ID: ComponentId = 0x05;
#[test]
fn test_basic() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TmTcSender::Mock(MockSender::default());
let udp_tc_server =
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler: tm_handler.into(),
};
udp_dyn_server.periodic_operation();
let queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow();
assert!(queue.is_empty());
assert!(tm_handler_calls.lock().unwrap().is_empty());
}
#[test]
fn test_transactions() {
let sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let test_receiver = TmTcSender::Mock(MockSender::default());
let udp_tc_server =
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
let server_addr = udp_tc_server.socket.local_addr().unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler: tm_handler.into(),
};
let sph = SpHeader::new_for_unseg_tc(Apid::Tmtc.raw_value(), u14::ZERO, 0);
let ping_tc = PusTcCreator::new_simple(
sph,
MessageTypeId::new(17, 1),
&[],
CreatorConfig::default(),
)
.to_vec()
.unwrap();
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
let client_addr = client.local_addr().unwrap();
println!("{}", server_addr);
client.send_to(&ping_tc, server_addr).unwrap();
udp_dyn_server.periodic_operation();
{
let mut queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow_mut();
assert!(!queue.is_empty());
let packet_with_sender = queue.pop_front().unwrap();
assert_eq!(packet_with_sender.packet, ping_tc);
assert_eq!(packet_with_sender.sender_id, UDP_SERVER_ID);
}
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
udp_dyn_server.periodic_operation();
let queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow();
assert!(queue.is_empty());
drop(queue);
// Still tries to send to the same client.
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
}
}
+293
View File
@@ -0,0 +1,293 @@
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u11;
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
use satrs::pus::event::EventTmHook;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::EcssTmSender;
use satrs::request::UniqueApidTargetId;
use satrs::{
event_man_legacy::{EventManagerWithBoundedMpsc, EventSendProvider, EventU32SenderMpscBounded},
pus::{
event_man::{
DefaultPusEventU32TmCreator, EventReporter, EventRequest, EventRequestWithToken,
},
verification::{TcStateStarted, VerificationReportingProvider, VerificationToken},
},
spacepackets::time::cds::CdsTime,
};
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use crate::update_time;
// This helper sets the APID of the event sender for the PUS telemetry.
#[derive(Default)]
pub struct EventApidSetter {
pub next_apid: u11,
}
impl EventTmHook for EventApidSetter {
fn modify_tm(&self, tm: &mut satrs::spacepackets::ecss::tm::PusTmCreator) {
tm.set_apid(self.next_apid);
}
}
/*
/// The PUS event handler subscribes for all events and converts them into ECSS PUS 5 event
/// packets. It also handles the verification completion of PUS event service requests.
pub struct PusEventHandler<TmSender: EcssTmSender> {
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
pus_event_tm_creator: DefaultPusEventU32TmCreator<EventApidSetter>,
pus_event_man_rx: mpsc::Receiver<EventMessageU32>,
tm_sender: TmSender,
time_provider: CdsTime,
timestamp: [u8; 7],
small_data_buf: [u8; 64],
verif_handler: VerificationReporter,
}
impl<TmSender: EcssTmSender> PusEventHandler<TmSender> {
pub fn new(
tm_sender: TmSender,
verif_handler: VerificationReporter,
event_manager: &mut EventManagerWithBoundedMpsc,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
) -> Self {
let event_queue_cap = 30;
let (pus_event_man_tx, pus_event_man_rx) = mpsc::sync_channel(event_queue_cap);
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
// telemetry for each event.
let event_reporter = EventReporter::new_with_hook(
PUS_EVENT_MANAGEMENT.raw(),
u11::ZERO,
0,
128,
EventApidSetter::default(),
);
let pus_event_dispatcher =
DefaultPusEventU32TmCreator::new_with_default_backend(event_reporter);
let pus_event_man_send_provider = EventU32SenderMpscBounded::new(
PUS_EVENT_MANAGEMENT.raw(),
pus_event_man_tx,
event_queue_cap,
);
event_manager.subscribe_all(pus_event_man_send_provider.target_id());
event_manager.add_sender(pus_event_man_send_provider);
Self {
event_request_rx,
pus_event_tm_creator: pus_event_dispatcher,
pus_event_man_rx,
time_provider: CdsTime::new_with_u16_days(0, 0),
timestamp: [0; 7],
small_data_buf: [0; 64],
verif_handler,
tm_sender,
}
}
pub fn handle_event_requests(&mut self) {
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
let started_token: VerificationToken<TcStateStarted> = event_req
.token
.try_into()
.expect("expected start verification token");
self.verif_handler
.completion_success(&self.tm_sender, started_token, timestamp)
.expect("Sending completion success failed");
};
loop {
// handle event requests
match self.event_request_rx.try_recv() {
Ok(event_req) => match event_req.request {
EventRequest::Enable(event) => {
self.pus_event_tm_creator
.enable_tm_for_event(&event)
.expect("Enabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
EventRequest::Disable(event) => {
self.pus_event_tm_creator
.disable_tm_for_event(&event)
.expect("Disabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("all event request senders have disconnected");
break;
}
},
}
}
}
pub fn generate_pus_event_tm(&mut self) {
loop {
// Perform the generation of PUS event packets
match self.pus_event_man_rx.try_recv() {
Ok(event_msg) => {
// We use the TM modification hook to set the sender APID for each event.
self.pus_event_tm_creator.reporter.tm_hook.next_apid =
UniqueApidTargetId::from(event_msg.sender_id()).apid;
update_time(&mut self.time_provider, &mut self.timestamp);
let generation_result = self
.pus_event_tm_creator
.generate_pus_event_tm_generic_with_generic_params(
&self.tm_sender,
&self.timestamp,
event_msg.event(),
&mut self.small_data_buf,
event_msg.params(),
)
.expect("Sending TM as event failed");
if !generation_result.params_were_propagated {
log::warn!(
"Event TM parameters were not propagated: {:?}",
event_msg.params()
);
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("All event senders have disconnected");
break;
}
},
}
}
}
}
pub struct EventHandler<TmSender: EcssTmSender> {
pub pus_event_handler: PusEventHandler<TmSender>,
event_manager: EventManagerWithBoundedMpsc,
}
impl<TmSender: EcssTmSender> EventHandler<TmSender> {
pub fn new(
tm_sender: TmSender,
event_rx: mpsc::Receiver<EventMessageU32>,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
) -> Self {
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
let pus_event_handler = PusEventHandler::new(
tm_sender,
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
&mut event_manager,
event_request_rx,
);
Self {
pus_event_handler,
event_manager,
}
}
#[allow(dead_code)]
pub fn event_manager(&mut self) -> &mut EventManagerWithBoundedMpsc {
&mut self.event_manager
}
pub fn periodic_operation(&mut self) {
self.pus_event_handler.handle_event_requests();
self.try_event_routing();
self.pus_event_handler.generate_pus_event_tm();
}
pub fn try_event_routing(&mut self) {
let error_handler = |event_msg: &EventMessageU32, error: EventRoutingError| {
self.routing_error_handler(event_msg, error)
};
// Perform the event routing.
self.event_manager.try_event_handling(error_handler);
}
pub fn routing_error_handler(&self, event_msg: &EventMessageU32, error: EventRoutingError) {
log::warn!("event routing error for event {event_msg:?}: {error:?}");
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::u21;
use satrs::{
events_legacy::EventU32,
pus::verification::VerificationReporterConfig,
spacepackets::ecss::{tm::PusTmReader, PusPacket},
tmtc::PacketAsVec,
};
use super::*;
const TEST_CREATOR_ID: UniqueApidTargetId = UniqueApidTargetId::new(u11::new(1), u21::new(2));
const TEST_EVENT: EventU32 = EventU32::new(satrs::events_legacy::Severity::Info, 1, 1);
pub struct EventManagementTestbench {
pub event_tx: mpsc::SyncSender<EventMessageU32>,
pub event_manager: EventManagerWithBoundedMpsc,
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
pub pus_event_handler: PusEventHandler<mpsc::Sender<PacketAsVec>>,
}
impl EventManagementTestbench {
pub fn new() -> Self {
let (event_tx, event_rx) = mpsc::sync_channel(10);
let (_event_req_tx, event_req_rx) = mpsc::sync_channel(10);
let (tm_sender, tm_receiver) = mpsc::channel();
let verif_reporter_cfg = VerificationReporterConfig::new(u11::new(0x05), 2, 2, 128);
let verif_reporter =
VerificationReporter::new(PUS_EVENT_MANAGEMENT.id(), &verif_reporter_cfg);
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
let pus_event_handler = PusEventHandler::<mpsc::Sender<PacketAsVec>>::new(
tm_sender,
verif_reporter,
&mut event_manager,
event_req_rx,
);
Self {
event_tx,
tm_receiver,
event_manager,
pus_event_handler,
}
}
}
#[test]
fn test_basic_event_generation() {
let mut testbench = EventManagementTestbench::new();
testbench
.event_tx
.send(EventMessageU32::new(
TEST_CREATOR_ID.id(),
EventU32::new(satrs::events_legacy::Severity::Info, 1, 1),
))
.expect("failed to send event");
testbench.pus_event_handler.handle_event_requests();
testbench.event_manager.try_event_handling(|_, _| {});
testbench.pus_event_handler.generate_pus_event_tm();
let tm_packet = testbench
.tm_receiver
.try_recv()
.expect("failed to receive TM packet");
assert_eq!(tm_packet.sender_id, PUS_EVENT_MANAGEMENT.id());
let tm_reader = PusTmReader::new(&tm_packet.packet, 7).expect("failed to create TM reader");
assert_eq!(tm_reader.apid(), TEST_CREATOR_ID.apid);
assert_eq!(tm_reader.user_data().len(), 4);
let event_read_back = EventU32::from_be_bytes(tm_reader.user_data().try_into().unwrap());
assert_eq!(event_read_back, TEST_EVENT);
}
#[test]
fn test_basic_event_disabled() {
// TODO: Add test.
}
}
*/
+74
View File
@@ -0,0 +1,74 @@
use arbitrary_int::traits::Integer as _;
use derive_new::new;
use satrs::hk::UniqueId;
use satrs::request::UniqueApidTargetId;
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use satrs::spacepackets::ecss::{hk, CreatorConfig, MessageTypeId};
use satrs::spacepackets::{ByteConversionError, SpHeader};
#[derive(Debug, new, Copy, Clone)]
pub struct HkUniqueId {
target_id: UniqueApidTargetId,
set_id: UniqueId,
}
impl HkUniqueId {
#[allow(dead_code)]
pub fn target_id(&self) -> UniqueApidTargetId {
self.target_id
}
#[allow(dead_code)]
pub fn set_id(&self) -> UniqueId {
self.set_id
}
#[allow(dead_code)]
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < 8 {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: 8,
});
}
buf[0..4].copy_from_slice(&self.target_id.unique_id.as_u32().to_be_bytes());
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
Ok(8)
}
}
#[derive(new)]
pub struct PusHkHelper {
component_id: UniqueApidTargetId,
}
impl PusHkHelper {
pub fn generate_hk_report_packet<
'a,
'b,
HkWriter: FnMut(&mut [u8]) -> Result<usize, ByteConversionError>,
>(
&self,
timestamp: &'a [u8],
set_id: u32,
hk_data_writer: &mut HkWriter,
buf: &'b mut [u8],
) -> Result<PusTmCreator<'a, 'b>, ByteConversionError> {
let sec_header = PusTmSecondaryHeader::new(
MessageTypeId::new(3, hk::MessageSubtypeId::TmHkPacket as u8),
0,
0,
timestamp,
);
buf[0..4].copy_from_slice(&self.component_id.unique_id.as_u32().to_be_bytes());
buf[4..8].copy_from_slice(&set_id.to_be_bytes());
let (_, second_half) = buf.split_at_mut(8);
let hk_data_len = hk_data_writer(second_half)?;
Ok(PusTmCreator::new(
SpHeader::new_from_apid(self.component_id.apid),
sec_header,
&buf[0..8 + hk_data_len],
CreatorConfig::default(),
))
}
}
@@ -1,30 +1,29 @@
use log::{error, warn};
use log::warn;
use satrs::action::{ActionRequest, ActionRequestVariant};
use satrs::params::WritableToBeBytes;
use satrs::pool::SharedStaticMemoryPool;
use satrs::pus::action::{
ActionReplyPus, ActionReplyVariant, ActivePusActionRequestStd, DefaultActiveActionRequestMap,
};
use satrs::pus::verification::{
FailParams, FailParamsWithStep, TcStateAccepted, TcStateStarted, VerificationReporter,
handle_completion_failure_with_generic_params, handle_step_failure_with_generic_params,
FailParamHelper, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActiveRequestProvider, EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter,
EcssTcInVecConverter, EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver,
MpscTmAsVecSender, PusPacketHandlerResult, PusReplyHandler, PusServiceHelper,
PusTcToRequestConverter,
ActiveRequest, EcssTcAndToken, EcssTcCacher, EcssTmSender, EcssTmtcError,
GenericConversionError, MpscTcReceiver, PusPacketHandlingError, PusReplyHandler,
PusServiceHelper, PusTcToRequestConverter,
};
use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket};
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
use satrs_example::config::components::PUS_ACTION_SERVICE;
use satrs_example::config::tmtc_err;
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket, PusServiceId};
use example_std::config::tmtc_err;
use example_std::ids;
use example_std::ids::generic_pus::PUS_ACTION;
use std::sync::mpsc;
use std::time::Duration;
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use super::{
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
@@ -61,7 +60,7 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
active_request: &ActivePusActionRequestStd,
tm_sender: &(impl EcssTmSender + ?Sized),
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
timestamp: &[u8],
) -> Result<bool, Self::Error> {
let verif_token: VerificationToken<TcStateStarted> = active_request
.token()
@@ -69,15 +68,23 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
.expect("invalid token state");
let remove_entry = match &reply.message.variant {
ActionReplyVariant::CompletionFailed { error_code, params } => {
let mut fail_data_len = 0;
if let Some(params) = params {
fail_data_len = params.write_to_be_bytes(&mut self.fail_data_buf)?;
}
verification_handler.completion_failure(
let error_propagated = handle_completion_failure_with_generic_params(
tm_sender,
verif_token,
FailParams::new(time_stamp, error_code, &self.fail_data_buf[..fail_data_len]),
verification_handler,
FailParamHelper {
error_code,
params: params.as_ref(),
timestamp,
small_data_buf: &mut self.fail_data_buf,
},
)?;
if !error_propagated {
log::warn!(
"error params for completion failure were not propated: {:?}",
params.as_ref()
);
}
true
}
ActionReplyVariant::StepFailed {
@@ -85,31 +92,35 @@ impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyH
step,
params,
} => {
let mut fail_data_len = 0;
if let Some(params) = params {
fail_data_len = params.write_to_be_bytes(&mut self.fail_data_buf)?;
}
verification_handler.step_failure(
let error_propagated = handle_step_failure_with_generic_params(
tm_sender,
verif_token,
FailParamsWithStep::new(
time_stamp,
&EcssEnumU16::new(*step),
verification_handler,
FailParamHelper {
error_code,
&self.fail_data_buf[..fail_data_len],
),
params: params.as_ref(),
timestamp,
small_data_buf: &mut self.fail_data_buf,
},
&EcssEnumU16::new(*step),
)?;
if !error_propagated {
log::warn!(
"error params for completion failure were not propated: {:?}",
params.as_ref()
);
}
true
}
ActionReplyVariant::Completed => {
verification_handler.completion_success(tm_sender, verif_token, time_stamp)?;
verification_handler.completion_success(tm_sender, verif_token, timestamp)?;
true
}
ActionReplyVariant::StepSuccess { step } => {
verification_handler.step_success(
tm_sender,
&verif_token,
time_stamp,
timestamp,
EcssEnumU16::new(*step),
)?;
false
@@ -150,7 +161,7 @@ impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for Actio
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(ActivePusActionRequestStd, ActionRequest), Self::Error> {
let subservice = tc.subservice();
let subservice = tc.message_subtype_id();
let user_data = tc.user_data();
if user_data.len() < 8 {
verif_reporter
@@ -195,20 +206,20 @@ impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for Actio
}
}
pub fn create_action_service_static(
tm_sender: PacketSenderWithSharedPool,
tc_pool: SharedStaticMemoryPool,
pub fn create_action_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
action_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
) -> ActionServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
) -> ActionServiceWrapper {
let action_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_ACTION_SERVICE.id(),
ids::generic_pus::PUS_ACTION.id(),
pus_action_rx,
tm_sender,
create_verification_reporter(PUS_ACTION_SERVICE.id(), PUS_ACTION_SERVICE.apid),
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
create_verification_reporter(PUS_ACTION.id(), PUS_ACTION.apid),
tc_in_mem_converter,
),
ActionRequestConverter::default(),
// TODO: Implementation which does not use run-time allocation? Maybe something like
@@ -223,36 +234,9 @@ pub fn create_action_service_static(
}
}
pub fn create_action_service_dynamic(
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
action_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
) -> ActionServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
let action_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_ACTION_SERVICE.id(),
pus_action_rx,
tm_funnel_tx,
create_verification_reporter(PUS_ACTION_SERVICE.id(), PUS_ACTION_SERVICE.apid),
EcssTcInVecConverter::default(),
),
ActionRequestConverter::default(),
DefaultActiveActionRequestMap::default(),
ActionReplyHandler::default(),
action_router,
reply_receiver,
);
ActionServiceWrapper {
service: action_request_handler,
}
}
pub struct ActionServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
pub struct ActionServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
TmSender,
TcInMemConverter,
VerificationReporter,
ActionRequestConverter,
ActionReplyHandler,
@@ -263,57 +247,38 @@ pub struct ActionServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTc
>,
}
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> TargetedPusService
for ActionServiceWrapper<TmSender, TcInMemConverter>
{
/// Returns [true] if the packet handling is finished.
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
match self.service.poll_and_handle_next_tc(time_stamp) {
Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
warn!("PUS 8 partial packet handling success: {e:?}")
}
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
warn!("PUS 8 invalid subservice {invalid}");
}
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
warn!("PUS 8 subservice {subservice} not implemented");
}
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
},
Err(error) => {
error!("PUS packet handling error: {error:?}");
// To avoid permanent loops on error cases.
return HandlingStatus::Empty;
}
impl TargetedPusService for ActionServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Action as u8;
const SERVICE_STR: &'static str = "action";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
HandlingStatus::HandledOne
}
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
// This only fails if all senders disconnected. Treat it like an empty queue.
self.service
.poll_and_check_next_reply(time_stamp)
.unwrap_or_else(|e| {
warn!("PUS 8: Handling reply failed with error {e:?}");
HandlingStatus::Empty
})
}
fn check_for_request_timeouts(&mut self) {
self.service.check_for_request_timeouts();
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer as _;
use satrs::pus::test_util::{
TEST_APID, TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
};
use satrs::pus::verification;
use satrs::pus::verification::test_util::TestVerificationReporter;
use satrs::pus::{verification, EcssTcVecCacher};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::tmtc::PacketAsVec;
use satrs::ComponentId;
use satrs::{
res_code::ResultU16,
@@ -346,10 +311,10 @@ mod tests {
{
pub fn new_for_action(owner_id: ComponentId, target_id: ComponentId) -> Self {
let _ = env_logger::builder().is_test(true).try_init();
let (tm_funnel_tx, tm_funnel_rx) = mpsc::channel();
let (tm_funnel_tx, tm_funnel_rx) = mpsc::sync_channel(5);
let (pus_action_tx, pus_action_rx) = mpsc::channel();
let (action_reply_tx, action_reply_rx) = mpsc::channel();
let (action_req_tx, action_req_rx) = mpsc::channel();
let (action_req_tx, action_req_rx) = mpsc::sync_channel(10);
let verif_reporter = TestVerificationReporter::new(owner_id);
let mut generic_req_router = GenericRequestRouter::default();
generic_req_router
@@ -360,9 +325,9 @@ mod tests {
PusServiceHelper::new(
owner_id,
pus_action_rx,
tm_funnel_tx.clone(),
TmTcSender::Heap(tm_funnel_tx.clone()),
verif_reporter,
EcssTcInVecConverter::default(),
EcssTcCacher::Heap(EcssTcVecCacher::default()),
),
ActionRequestConverter::default(),
DefaultActiveActionRequestMap::default(),
@@ -405,7 +370,7 @@ mod tests {
if let Err(mpsc::TryRecvError::Empty) = packet {
} else {
let tm = packet.unwrap();
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap().0;
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap();
panic!("unexpected TM packet {unexpected_tm:?}");
}
}
@@ -417,7 +382,7 @@ mod tests {
}
let result = result.unwrap();
match result {
PusPacketHandlerResult::RequestHandled => (),
HandlingStatus::HandledOne => (),
_ => panic!("unexpected result {result:?}"),
}
}
@@ -429,26 +394,30 @@ mod tests {
}
let result = result.unwrap();
match result {
PusPacketHandlerResult::Empty => (),
HandlingStatus::Empty => (),
_ => panic!("unexpected result {result:?}"),
}
}
pub fn verify_next_reply_is_handled_properly(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_check_next_reply(time_stamp);
let result = self.service.poll_and_handle_next_reply(time_stamp);
assert!(result.is_ok());
assert_eq!(result.unwrap(), HandlingStatus::HandledOne);
}
pub fn verify_all_replies_handled(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_check_next_reply(time_stamp);
let result = self.service.poll_and_handle_next_reply(time_stamp);
assert!(result.is_ok());
assert_eq!(result.unwrap(), HandlingStatus::Empty);
}
pub fn add_tc(&mut self, tc: &PusTcCreator) {
self.request_id = Some(verification::RequestId::new(tc).into());
let token = self.service.service_helper.verif_reporter_mut().add_tc(tc);
let token = self
.service
.service_helper
.verif_reporter_mut()
.start_verification(tc);
let accepted_token = self
.service
.service_helper
@@ -481,12 +450,13 @@ mod tests {
);
// Create a basic action request and verify forwarding.
let sp_header = SpHeader::new_from_apid(TEST_APID);
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.to_be_bytes());
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
let pus8_packet = PusTcCreator::new(sp_header, sec_header, &app_data, true);
let pus8_packet =
PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
testbench.add_tc(&pus8_packet);
let time_stamp: [u8; 7] = [0; 7];
testbench.verify_next_tc_is_handled_properly(&time_stamp);
@@ -522,7 +492,7 @@ mod tests {
TEST_COMPONENT_ID_1.id(),
);
// Create a basic action request and verify forwarding.
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
// Invalid ID, routing should fail.
@@ -532,7 +502,7 @@ mod tests {
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
true,
CreatorConfig::default(),
);
testbench.add_tc(&pus8_packet);
let time_stamp: [u8; 7] = [0; 7];
@@ -548,17 +518,17 @@ mod tests {
TEST_COMPONENT_ID_0.raw(),
ActionRequestConverter::default(),
);
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
// Invalid ID, routing should fail.
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
let pus8_packet = PusTcCreator::new(
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
true,
CreatorConfig::default(),
);
let token = testbench.add_tc(&pus8_packet);
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
@@ -584,11 +554,11 @@ mod tests {
fn converter_action_req_with_data() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ActionRequestConverter::default());
let sec_header = PusTcSecondaryHeader::new_simple(8, 128);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 16] = [0; 16];
// Invalid ID, routing should fail.
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
for i in 0..8 {
app_data[i + 8] = i as u8;
@@ -597,7 +567,7 @@ mod tests {
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
true,
CreatorConfig::default(),
);
let token = testbench.add_tc(&pus8_packet);
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
@@ -727,7 +697,7 @@ mod tests {
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_reply = ActionReplyPus::new(5_u32, ActionReplyVariant::Completed);
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), action_reply);
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
@@ -0,0 +1,91 @@
use std::sync::mpsc;
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use satrs::pus::event_man::EventRequestWithToken;
use satrs::pus::event_srv::PusEventServiceHandler;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
PartialPusHandlingError, PusServiceHelper,
};
use satrs::spacepackets::ecss::PusServiceId;
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use super::{DirectPusService, HandlingStatus};
pub fn create_event_service(
tm_sender: TmTcSender,
tm_in_pool_converter: EcssTcCacher,
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
event_request_tx: mpsc::Sender<EventRequestWithToken>,
) -> EventServiceWrapper {
let pus_5_handler = PusEventServiceHandler::new(
PusServiceHelper::new(
PUS_EVENT_MANAGEMENT.id(),
pus_event_rx,
tm_sender,
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
tm_in_pool_converter,
),
event_request_tx,
);
EventServiceWrapper {
handler: pus_5_handler,
}
}
pub struct EventServiceWrapper {
pub handler:
PusEventServiceHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
}
impl DirectPusService for EventServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Event as u8;
const SERVICE_STR: &'static str = "events";
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let result = self
.handler
.poll_and_handle_next_tc(error_handler, time_stamp);
if let Err(e) = result {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match result.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
}
HandlingStatus::HandledOne
}
}
@@ -1,30 +1,28 @@
use derive_new::new;
use log::{error, warn};
use satrs::hk::{CollectionIntervalFactor, HkRequest, HkRequestVariant, UniqueId};
use satrs::pool::SharedStaticMemoryPool;
use satrs::pus::verification::{
FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActivePusRequestStd, ActiveRequestProvider, DefaultActiveRequestMap, EcssTcAndToken,
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter, EcssTmSender,
EcssTmtcError, GenericConversionError, MpscTcReceiver, MpscTmAsVecSender,
PusPacketHandlerResult, PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
ActivePusRequestStd, ActiveRequest, DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher,
EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver, PusPacketHandlingError,
PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
};
use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::res_code::ResultU16;
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{hk, PusPacket};
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
use satrs_example::config::components::PUS_HK_SERVICE;
use satrs_example::config::{hk_err, tmtc_err};
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
use example_std::config::{hk_err, tmtc_err};
use example_std::ids::generic_pus::PUS_HK;
use std::sync::mpsc;
use std::time::Duration;
use crate::pus::{create_verification_reporter, generic_pus_request_timeout_handler};
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use super::{HandlingStatus, PusTargetedRequestService};
use super::{HandlingStatus, PusTargetedRequestService, TargetedPusService};
#[derive(Clone, PartialEq, Debug, new)]
pub struct HkReply {
@@ -33,8 +31,10 @@ pub struct HkReply {
}
#[derive(Clone, PartialEq, Debug)]
#[allow(dead_code)]
pub enum HkReplyVariant {
Ack,
Failed(ResultU16),
}
#[derive(Default)]
@@ -70,6 +70,15 @@ impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
.completion_success(tm_sender, started_token, time_stamp)
.expect("sending completion success verification failed");
}
HkReplyVariant::Failed(failure_code) => {
verification_handler
.completion_failure(
tm_sender,
started_token,
FailParams::new(time_stamp, &failure_code, &[]),
)
.expect("sending completion success verification failed");
}
};
Ok(true)
}
@@ -155,11 +164,11 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
found: 4,
});
}
let subservice = tc.subservice();
let subservice = tc.message_subtype_id();
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).expect("invalid tc format");
let unique_id = u32::from_be_bytes(tc.user_data()[4..8].try_into().unwrap());
let standard_subservice = hk::Subservice::try_from(subservice);
let standard_subservice = hk::MessageSubtypeId::try_from(subservice);
if standard_subservice.is_err() {
verif_reporter
.start_failure(
@@ -171,19 +180,22 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
return Err(GenericConversionError::InvalidSubservice(subservice));
}
let request = match standard_subservice.unwrap() {
hk::Subservice::TcEnableHkGeneration | hk::Subservice::TcEnableDiagGeneration => {
hk::MessageSubtypeId::TcEnableHkGeneration
| hk::MessageSubtypeId::TcEnableDiagGeneration => {
HkRequest::new(unique_id, HkRequestVariant::EnablePeriodic)
}
hk::Subservice::TcDisableHkGeneration | hk::Subservice::TcDisableDiagGeneration => {
hk::MessageSubtypeId::TcDisableHkGeneration
| hk::MessageSubtypeId::TcDisableDiagGeneration => {
HkRequest::new(unique_id, HkRequestVariant::DisablePeriodic)
}
hk::Subservice::TcReportHkReportStructures => todo!(),
hk::Subservice::TmHkPacket => todo!(),
hk::Subservice::TcGenerateOneShotHk | hk::Subservice::TcGenerateOneShotDiag => {
hk::MessageSubtypeId::TcReportHkReportStructures => todo!(),
hk::MessageSubtypeId::TmHkPacket => todo!(),
hk::MessageSubtypeId::TcGenerateOneShotHk
| hk::MessageSubtypeId::TcGenerateOneShotDiag => {
HkRequest::new(unique_id, HkRequestVariant::OneShot)
}
hk::Subservice::TcModifyDiagCollectionInterval
| hk::Subservice::TcModifyHkCollectionInterval => {
hk::MessageSubtypeId::TcModifyDiagCollectionInterval
| hk::MessageSubtypeId::TcModifyHkCollectionInterval => {
if user_data.len() < 12 {
verif_reporter
.start_failure(
@@ -231,20 +243,20 @@ impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConver
}
}
pub fn create_hk_service_static(
tm_sender: PacketSenderWithSharedPool,
tc_pool: SharedStaticMemoryPool,
pub fn create_hk_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
request_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
) -> HkServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
) -> HkServiceWrapper {
let pus_3_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_HK_SERVICE.id(),
PUS_HK.id(),
pus_hk_rx,
tm_sender,
create_verification_reporter(PUS_HK_SERVICE.id(), PUS_HK_SERVICE.apid),
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
create_verification_reporter(PUS_HK.id(), PUS_HK.apid),
tc_in_mem_converter,
),
HkRequestConverter::default(),
DefaultActiveRequestMap::default(),
@@ -257,36 +269,9 @@ pub fn create_hk_service_static(
}
}
pub fn create_hk_service_dynamic(
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
request_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
) -> HkServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
let pus_3_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_HK_SERVICE.id(),
pus_hk_rx,
tm_funnel_tx,
create_verification_reporter(PUS_HK_SERVICE.id(), PUS_HK_SERVICE.apid),
EcssTcInVecConverter::default(),
),
HkRequestConverter::default(),
DefaultActiveRequestMap::default(),
HkReplyHandler::default(),
request_router,
reply_receiver,
);
HkServiceWrapper {
service: pus_3_handler,
}
}
pub struct HkServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
pub struct HkServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
TmSender,
TcInMemConverter,
VerificationReporter,
HkRequestConverter,
HkReplyHandler,
@@ -297,64 +282,46 @@ pub struct HkServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMe
>,
}
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter>
HkServiceWrapper<TmSender, TcInMemConverter>
{
pub fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
match self.service.poll_and_handle_next_tc(time_stamp) {
Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
warn!("PUS 3 partial packet handling success: {e:?}")
}
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
warn!("PUS 3 invalid subservice {invalid}");
}
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
warn!("PUS 3 subservice {subservice} not implemented");
}
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
},
Err(error) => {
error!("PUS packet handling error: {error:?}");
// To avoid permanent loops on error cases.
return HandlingStatus::Empty;
}
impl TargetedPusService for HkServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Housekeeping as u8;
const SERVICE_STR: &'static str = "housekeeping";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
HandlingStatus::HandledOne
}
pub fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
// This only fails if all senders disconnected. Treat it like an empty queue.
self.service
.poll_and_check_next_reply(time_stamp)
.unwrap_or_else(|e| {
warn!("PUS 3: Handling reply failed with error {e:?}");
HandlingStatus::Empty
})
}
pub fn check_for_request_timeouts(&mut self) {
self.service.check_for_request_timeouts();
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer as _;
use arbitrary_int::{u14, u21};
use satrs::pus::test_util::{
TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
hk::HkRequestVariant,
pus::test_util::TEST_APID,
request::GenericMessage,
spacepackets::{
ecss::{hk::Subservice, tc::PusTcCreator},
ecss::{hk::MessageSubtypeId, tc::PusTcCreator},
SpHeader,
},
};
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
use crate::pus::{
hk::HkReplyVariant,
@@ -367,19 +334,18 @@ mod tests {
fn hk_converter_one_shot_req() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let hk_req = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcGenerateOneShotHk as u8,
MessageTypeId::new(3, MessageSubtypeId::TcGenerateOneShotHk as u8),
&app_data,
true,
CreatorConfig::default(),
);
let accepted_token = hk_bench.add_tc(&hk_req);
let (_active_req, req) = hk_bench
@@ -397,11 +363,11 @@ mod tests {
fn hk_converter_enable_periodic_generation() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let mut generic_check = |tc: &PusTcCreator| {
let accepted_token = hk_bench.add_tc(tc);
@@ -416,18 +382,16 @@ mod tests {
};
let tc0 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcEnableHkGeneration as u8,
MessageTypeId::new(3, MessageSubtypeId::TcEnableHkGeneration as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcEnableDiagGeneration as u8,
MessageTypeId::new(3, MessageSubtypeId::TcEnableDiagGeneration as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc1);
}
@@ -436,11 +400,11 @@ mod tests {
fn hk_conversion_disable_periodic_generation() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let mut generic_check = |tc: &PusTcCreator| {
let accepted_token = hk_bench.add_tc(tc);
@@ -455,18 +419,16 @@ mod tests {
};
let tc0 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcDisableHkGeneration as u8,
MessageTypeId::new(3, MessageSubtypeId::TcDisableHkGeneration as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcDisableDiagGeneration as u8,
MessageTypeId::new(3, MessageSubtypeId::TcDisableDiagGeneration as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc1);
}
@@ -475,12 +437,12 @@ mod tests {
fn hk_conversion_modify_interval() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 12] = [0; 12];
let collection_interval_factor = 5_u32;
app_data[0..4].copy_from_slice(&target_id.to_be_bytes());
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
app_data[8..12].copy_from_slice(&collection_interval_factor.to_be_bytes());
@@ -498,18 +460,16 @@ mod tests {
};
let tc0 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcModifyHkCollectionInterval as u8,
MessageTypeId::new(3, MessageSubtypeId::TcModifyHkCollectionInterval as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
3,
Subservice::TcModifyDiagCollectionInterval as u8,
MessageTypeId::new(3, MessageSubtypeId::TcModifyDiagCollectionInterval as u8),
&app_data,
true,
CreatorConfig::default(),
);
generic_check(&tc1);
}
@@ -518,8 +478,8 @@ mod tests {
fn hk_reply_handler() {
let mut reply_testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), HkReplyHandler::default());
let sender_id = 2_u64;
let apid_target_id = 3_u32;
let sender_id = 2_u32;
let apid_target_id = u21::new(3);
let unique_id = 5_u32;
let (req_id, active_req) = reply_testbench.add_tc(TEST_APID, apid_target_id, &[]);
let reply = GenericMessage::new(
@@ -540,7 +500,7 @@ mod tests {
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
let action_reply = HkReply::new(5_u32, HkReplyVariant::Ack);
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), action_reply);
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
@@ -1,15 +1,16 @@
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use log::warn;
use satrs::pool::PoolAddr;
use satrs::pus::verification::{
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReporterCfg, VerificationReportingProvider, VerificationToken,
VerificationReporterConfig, VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActiveRequestMapProvider, ActiveRequestProvider, EcssTcAndToken, EcssTcInMemConverter,
ActiveRequest, ActiveRequestStore, CacheAndReadRawEcssTc, EcssTcAndToken, EcssTcCacher,
EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericConversionError, GenericRoutingError,
PusPacketHandlerResult, PusPacketHandlingError, PusReplyHandler, PusRequestRouter,
PusServiceHelper, PusTcToRequestConverter, TcInMemory,
HandlingStatus, PusPacketHandlingError, PusReplyHandler, PusRequestRouter, PusServiceHelper,
PusTcToRequestConverter, TcInMemory,
};
use satrs::queue::{GenericReceiveError, GenericSendError};
use satrs::request::{Apid, GenericMessage, MessageMetadata};
@@ -17,11 +18,11 @@ use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use satrs::tmtc::{PacketAsVec, PacketInPool};
use satrs::ComponentId;
use satrs_example::config::components::PUS_ROUTING_SERVICE;
use satrs_example::config::{tmtc_err, CustomPusServiceId};
use satrs_example::TimeStampHelper;
use example_std::config::{tmtc_err, CustomPusServiceId};
use example_std::ids::generic_pus::PUS_ROUTING;
use example_std::TimestampHelper;
use std::fmt::Debug;
use std::sync::mpsc::{self, Sender};
use std::sync::mpsc;
pub mod action;
pub mod event;
@@ -31,55 +32,49 @@ pub mod scheduler;
pub mod stack;
pub mod test;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum HandlingStatus {
Empty,
HandledOne,
}
pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> VerificationReporter {
let verif_cfg = VerificationReporterCfg::new(apid, 1, 2, 8).unwrap();
let verif_cfg = VerificationReporterConfig::new(apid, 1, 2, 8);
// Every software component which needs to generate verification telemetry, gets a cloned
// verification reporter.
VerificationReporter::new(owner_id, &verif_cfg)
}
/*
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
pub struct PusTcMpscRouter {
pub test_tc_sender: Sender<EcssTcAndToken>,
pub event_tc_sender: Sender<EcssTcAndToken>,
pub sched_tc_sender: Sender<EcssTcAndToken>,
pub hk_tc_sender: Sender<EcssTcAndToken>,
pub action_tc_sender: Sender<EcssTcAndToken>,
pub mode_tc_sender: Sender<EcssTcAndToken>,
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub event_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub sched_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub hk_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
#[allow(dead_code)]
pub action_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub mode_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
}
pub struct PusTcDistributor<TmSender: EcssTmSender> {
pub struct PusTcDistributor {
#[allow(dead_code)]
pub id: ComponentId,
pub tm_sender: TmSender,
pub tm_sender: TmTcSender,
pub verif_reporter: VerificationReporter,
pub pus_router: PusTcMpscRouter,
stamp_helper: TimeStampHelper,
stamp_helper: TimestampHelper,
}
impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
pub fn new(tm_sender: TmSender, pus_router: PusTcMpscRouter) -> Self {
impl PusTcDistributor {
pub fn new(tm_sender: TmTcSender, pus_router: PusTcMpscRouter) -> Self {
Self {
id: PUS_ROUTING_SERVICE.raw(),
id: PUS_ROUTING.raw(),
tm_sender,
verif_reporter: create_verification_reporter(
PUS_ROUTING_SERVICE.id(),
PUS_ROUTING_SERVICE.apid,
),
verif_reporter: create_verification_reporter(PUS_ROUTING.id(), PUS_ROUTING.apid),
pus_router,
stamp_helper: TimeStampHelper::default(),
stamp_helper: TimestampHelper::default(),
}
}
pub fn handle_tc_packet_vec(
&mut self,
packet_as_vec: PacketAsVec,
) -> Result<PusPacketHandlerResult, GenericSendError> {
) -> Result<HandlingStatus, GenericSendError> {
self.handle_tc_generic(packet_as_vec.sender_id, None, &packet_as_vec.packet)
}
@@ -87,7 +82,7 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
&mut self,
packet_in_pool: PacketInPool,
pus_tc_copy: &[u8],
) -> Result<PusPacketHandlerResult, GenericSendError> {
) -> Result<HandlingStatus, GenericSendError> {
self.handle_tc_generic(
packet_in_pool.sender_id,
Some(packet_in_pool.store_addr),
@@ -100,7 +95,7 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
sender_id: ComponentId,
addr_opt: Option<PoolAddr>,
raw_tc: &[u8],
) -> Result<PusPacketHandlerResult, GenericSendError> {
) -> Result<HandlingStatus, GenericSendError> {
let pus_tc_result = PusTcReader::new(raw_tc);
if pus_tc_result.is_err() {
log::warn!(
@@ -108,17 +103,18 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
sender_id,
pus_tc_result.unwrap_err()
);
log::warn!("raw data: {:x?}", raw_tc);
return Ok(PusPacketHandlerResult::RequestHandled);
log::warn!("raw data: {raw_tc:x?}");
// TODO: Shouldn't this be an error?
return Ok(HandlingStatus::HandledOne);
}
let pus_tc = pus_tc_result.unwrap().0;
let init_token = self.verif_reporter.add_tc(&pus_tc);
let pus_tc = pus_tc_result.unwrap();
let init_token = self.verif_reporter.start_verification(&pus_tc);
self.stamp_helper.update_from_now();
let accepted_token = self
.verif_reporter
.acceptance_success(&self.tm_sender, init_token, self.stamp_helper.stamp())
.expect("Acceptance success failure");
let service = PusServiceId::try_from(pus_tc.service());
let service = PusServiceId::try_from(pus_tc.service_type_id());
let tc_in_memory: TcInMemory = if let Some(store_addr) = addr_opt {
PacketInPool::new(sender_id, store_addr).into()
} else {
@@ -189,17 +185,66 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
}
}
}
Ok(PusPacketHandlerResult::RequestHandled)
Ok(HandlingStatus::HandledOne)
}
}
*/
pub trait TargetedPusService {
/// Returns [true] if the packet handling is finished.
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus;
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus;
const SERVICE_ID: u8;
const SERVICE_STR: &'static str;
fn poll_and_handle_next_tc_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let result = self.poll_and_handle_next_tc(time_stamp);
if let Err(e) = result {
log::error!(
"PUS service {}({})packet handling error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on error cases.
return HandlingStatus::Empty;
}
result.unwrap()
}
fn poll_and_handle_next_reply_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
// This only fails if all senders disconnected. Treat it like an empty queue.
self.poll_and_handle_next_reply(time_stamp)
.unwrap_or_else(|e| {
warn!(
"PUS servce {}({}): Handling reply failed with error {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
HandlingStatus::Empty
})
}
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
/// Generic trait for services which handle packets directly. Kept minimal right now because
/// of the difficulty to allow flexible user code for these services..
pub trait DirectPusService {
const SERVICE_ID: u8;
const SERVICE_STR: &'static str;
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus;
}
/// This is a generic handler class for all PUS services where a PUS telecommand is converted
/// to a targeted request.
///
@@ -223,21 +268,19 @@ pub trait TargetedPusService {
/// 3. [Self::check_for_request_timeouts] which checks for request timeouts, covering step 7.
pub struct PusTargetedRequestService<
TcReceiver: EcssTcReceiver,
TmSender: EcssTmSender,
TcInMemConverter: EcssTcInMemConverter,
VerificationReporter: VerificationReportingProvider,
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
> {
pub service_helper:
PusServiceHelper<TcReceiver, TmSender, TcInMemConverter, VerificationReporter>,
PusServiceHelper<TcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
pub request_router: GenericRequestRouter,
pub request_converter: RequestConverter,
pub active_request_map: ActiveRequestMap,
pub active_request_map: ActiveRequestMapInstance,
pub reply_handler: ReplyHandler,
pub reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
phantom: std::marker::PhantomData<(RequestType, ActiveRequestInfo, ReplyType)>,
@@ -245,24 +288,20 @@ pub struct PusTargetedRequestService<
impl<
TcReceiver: EcssTcReceiver,
TmSender: EcssTmSender,
TcInMemConverter: EcssTcInMemConverter,
VerificationReporter: VerificationReportingProvider,
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
>
PusTargetedRequestService<
TcReceiver,
TmSender,
TcInMemConverter,
VerificationReporter,
RequestConverter,
ReplyHandler,
ActiveRequestMap,
ActiveRequestMapInstance,
ActiveRequestInfo,
RequestType,
ReplyType,
@@ -273,12 +312,12 @@ where
pub fn new(
service_helper: PusServiceHelper<
TcReceiver,
TmSender,
TcInMemConverter,
TmTcSender,
EcssTcCacher,
VerificationReporter,
>,
request_converter: RequestConverter,
active_request_map: ActiveRequestMap,
active_request_map: ActiveRequestMapInstance,
reply_hook: ReplyHandler,
request_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
@@ -297,10 +336,10 @@ where
pub fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
) -> Result<HandlingStatus, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
return Ok(HandlingStatus::Empty);
}
let ecss_tc_and_token = possible_packet.unwrap();
self.service_helper
@@ -356,7 +395,7 @@ where
return Err(e.into());
}
}
Ok(PusPacketHandlerResult::RequestHandled)
Ok(HandlingStatus::HandledOne)
}
fn handle_conversion_to_request_error(
@@ -409,7 +448,7 @@ where
}
}
pub fn poll_and_check_next_reply(
pub fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError> {
@@ -439,20 +478,17 @@ where
return Ok(());
}
let active_request = active_req_opt.unwrap();
let request_finished = self
.reply_handler
.handle_reply(
reply,
active_request,
&self.service_helper.common.tm_sender,
&self.service_helper.common.verif_reporter,
time_stamp,
)
.unwrap_or(false);
if request_finished {
let result = self.reply_handler.handle_reply(
reply,
active_request,
&self.service_helper.common.tm_sender,
&self.service_helper.common.verif_reporter,
time_stamp,
);
if result.is_err() || (result.is_ok() && *result.as_ref().unwrap()) {
self.active_request_map.remove(reply.request_id());
}
Ok(())
result.map(|_| ())
}
pub fn check_for_request_timeouts(&mut self) {
@@ -475,7 +511,7 @@ where
/// and also log the error.
pub fn generic_pus_request_timeout_handler(
sender: &(impl EcssTmSender + ?Sized),
active_request: &(impl ActiveRequestProvider + Debug),
active_request: &(impl ActiveRequest + Debug),
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
service_str: &'static str,
@@ -497,13 +533,15 @@ pub fn generic_pus_request_timeout_handler(
pub(crate) mod tests {
use std::time::Duration;
use arbitrary_int::{u11, u21};
use satrs::pus::test_util::TEST_COMPONENT_ID_0;
use satrs::pus::{MpscTmAsVecSender, PusTmVariant};
use satrs::request::RequestId;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
pus::{
verification::test_util::TestVerificationReporter, ActivePusRequestStd,
ActiveRequestMapProvider, EcssTcInVecConverter, MpscTcReceiver,
ActiveRequestStore, MpscTcReceiver,
},
request::UniqueApidTargetId,
spacepackets::{
@@ -522,7 +560,7 @@ pub(crate) mod tests {
// Testbench dedicated to the testing of [PusReplyHandler]s
pub struct ReplyHandlerTestbench<
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestInfo: ActiveRequest,
Reply,
> {
pub id: ComponentId,
@@ -536,7 +574,7 @@ pub(crate) mod tests {
impl<
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestInfo: ActiveRequest,
Reply,
> ReplyHandlerTestbench<ReplyHandler, ActiveRequestInfo, Reply>
{
@@ -556,17 +594,17 @@ pub(crate) mod tests {
pub fn add_tc(
&mut self,
apid: u16,
apid_target: u32,
apid: u11,
apid_target: u21,
time_stamp: &[u8],
) -> (verification::RequestId, ActivePusRequestStd) {
let sp_header = SpHeader::new_from_apid(apid);
let sec_header_dummy = PusTcSecondaryHeader::new_simple(0, 0);
let init = self.verif_reporter.add_tc(&PusTcCreator::new(
let sec_header_dummy = PusTcSecondaryHeader::new_simple(MessageTypeId::new(0, 0));
let init = self.verif_reporter.start_verification(&PusTcCreator::new(
sp_header,
sec_header_dummy,
&[],
true,
CreatorConfig::default(),
));
let accepted = self
.verif_reporter
@@ -637,7 +675,7 @@ pub(crate) mod tests {
// Testbench dedicated to the testing of [PusTcToRequestConverter]s
pub struct PusConverterTestbench<
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestInfo: ActiveRequest,
Request,
> {
pub id: ComponentId,
@@ -651,7 +689,7 @@ pub(crate) mod tests {
impl<
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestInfo: ActiveRequest,
Request,
> PusConverterTestbench<Converter, ActiveRequestInfo, Request>
{
@@ -669,7 +707,7 @@ pub(crate) mod tests {
}
pub fn add_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
let token = self.verif_reporter.add_tc(tc);
let token = self.verif_reporter.start_verification(tc);
self.current_request_id = Some(verification::RequestId::new(tc));
self.current_packet = Some(tc.to_vec().unwrap());
self.verif_reporter
@@ -685,8 +723,8 @@ pub(crate) mod tests {
&mut self,
token: VerificationToken<TcStateAccepted>,
time_stamp: &[u8],
expected_apid: u16,
expected_apid_target: u32,
expected_apid: u11,
expected_apid_target: u21,
) -> Result<(ActiveRequestInfo, Request), Converter::Error> {
if self.current_packet.is_none() {
return Err(GenericConversionError::InvalidAppData(
@@ -697,7 +735,7 @@ pub(crate) mod tests {
let tc_reader = PusTcReader::new(&current_packet).unwrap();
let (active_info, request) = self.converter.convert(
token,
&tc_reader.0,
&tc_reader,
&self.dummy_sender,
&self.verif_reporter,
time_stamp,
@@ -717,19 +755,17 @@ pub(crate) mod tests {
pub struct TargetedPusRequestTestbench<
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMap: ActiveRequestMapProvider<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequestProvider,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
> {
pub service: PusTargetedRequestService<
MpscTcReceiver,
MpscTmAsVecSender,
EcssTcInVecConverter,
TestVerificationReporter,
RequestConverter,
ReplyHandler,
ActiveRequestMap,
ActiveRequestMapInstance,
ActiveRequestInfo,
RequestType,
ReplyType,
@@ -1,15 +1,17 @@
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u14;
use derive_new::new;
use log::{error, warn};
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
use satrs::mode_tree::{ModeNode, ModeParent};
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use example_std::ids;
use std::sync::mpsc;
use std::time::Duration;
use crate::requests::GenericRequestRouter;
use satrs::pool::SharedStaticMemoryPool;
use crate::tmtc::sender::TmTcSender;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DefaultActiveRequestMap, EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter,
EcssTcInVecConverter, MpscTcReceiver, MpscTmAsVecSender, PusPacketHandlerResult,
DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher, MpscTcReceiver, PusPacketHandlingError,
PusServiceHelper,
};
use satrs::request::GenericMessage;
@@ -21,8 +23,8 @@ use satrs::{
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReportingProvider,
VerificationToken,
},
ActivePusRequestStd, ActiveRequestProvider, EcssTmSender, EcssTmtcError,
GenericConversionError, PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
ActivePusRequestStd, ActiveRequest, EcssTmSender, EcssTmtcError, GenericConversionError,
PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
},
request::UniqueApidTargetId,
spacepackets::{
@@ -35,8 +37,7 @@ use satrs::{
},
ComponentId,
};
use satrs_example::config::components::PUS_MODE_SERVICE;
use satrs_example::config::{mode_err, tmtc_err};
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
use super::{
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
@@ -79,10 +80,19 @@ impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
.write_to_be_bytes(&mut source_data)
.expect("writing mode reply failed");
let req_id = verification::RequestId::from(reply.request_id());
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), 0, 0);
let sec_header =
PusTmSecondaryHeader::new(200, Subservice::TmModeReply as u8, 0, 0, time_stamp);
let pus_tm = PusTmCreator::new(sp_header, sec_header, &source_data, true);
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), u14::ZERO, 0);
let sec_header = PusTmSecondaryHeader::new(
MessageTypeId::new(200, Subservice::TmModeReply as u8),
0,
0,
time_stamp,
);
let pus_tm = PusTmCreator::new(
sp_header,
sec_header,
&source_data,
CreatorConfig::default(),
);
tm_sender.send_tm(self.owner_id, PusTmVariant::Direct(pus_tm))?;
verification_handler.completion_success(tm_sender, started_token, time_stamp)?;
}
@@ -111,6 +121,7 @@ impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
),
)?;
}
ModeReply::ModeInfo(_mode_and_submode) => (),
};
Ok(true)
}
@@ -147,7 +158,7 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(ActivePusRequestStd, ModeRequest), Self::Error> {
let subservice = tc.subservice();
let subservice = tc.message_subtype_id();
let user_data = tc.user_data();
let not_enough_app_data = |expected: usize| {
verif_reporter
@@ -191,7 +202,13 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
}
let mode_and_submode = ModeAndSubmode::from_be_bytes(&tc.user_data()[4..])
.expect("mode and submode extraction failed");
Ok((active_request, ModeRequest::SetMode(mode_and_submode)))
Ok((
active_request,
ModeRequest::SetMode {
mode_and_submode,
forced: false,
},
))
}
Subservice::TcReadMode => Ok((active_request, ModeRequest::ReadMode)),
Subservice::TcAnnounceMode => Ok((active_request, ModeRequest::AnnounceMode)),
@@ -203,24 +220,27 @@ impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestCo
}
}
pub fn create_mode_service_static(
tm_sender: PacketSenderWithSharedPool,
tc_pool: SharedStaticMemoryPool,
pub fn create_mode_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
mode_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
) -> ModeServiceWrapper<PacketSenderWithSharedPool, EcssTcInSharedStoreConverter> {
) -> ModeServiceWrapper {
let mode_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_MODE_SERVICE.id(),
ids::generic_pus::PUS_MODE.id(),
pus_action_rx,
tm_sender,
create_verification_reporter(PUS_MODE_SERVICE.id(), PUS_MODE_SERVICE.apid),
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
create_verification_reporter(
ids::generic_pus::PUS_MODE.id(),
ids::generic_pus::PUS_MODE.apid,
),
tc_in_mem_converter,
),
ModeRequestConverter::default(),
DefaultActiveRequestMap::default(),
ModeReplyHandler::new(PUS_MODE_SERVICE.id()),
ModeReplyHandler::new(ids::generic_pus::PUS_MODE.id()),
mode_router,
reply_receiver,
);
@@ -229,36 +249,9 @@ pub fn create_mode_service_static(
}
}
pub fn create_mode_service_dynamic(
tm_funnel_tx: mpsc::Sender<PacketAsVec>,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
mode_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
) -> ModeServiceWrapper<MpscTmAsVecSender, EcssTcInVecConverter> {
let mode_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_MODE_SERVICE.id(),
pus_action_rx,
tm_funnel_tx,
create_verification_reporter(PUS_MODE_SERVICE.id(), PUS_MODE_SERVICE.apid),
EcssTcInVecConverter::default(),
),
ModeRequestConverter::default(),
DefaultActiveRequestMap::default(),
ModeReplyHandler::new(PUS_MODE_SERVICE.id()),
mode_router,
reply_receiver,
);
ModeServiceWrapper {
service: mode_request_handler,
}
}
pub struct ModeServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> {
pub struct ModeServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
TmSender,
TcInMemConverter,
VerificationReporter,
ModeRequestConverter,
ModeReplyHandler,
@@ -269,51 +262,51 @@ pub struct ModeServiceWrapper<TmSender: EcssTmSender, TcInMemConverter: EcssTcIn
>,
}
impl<TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter> TargetedPusService
for ModeServiceWrapper<TmSender, TcInMemConverter>
{
/// Returns [true] if the packet handling is finished.
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
match self.service.poll_and_handle_next_tc(time_stamp) {
Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
warn!("PUS mode service: partial packet handling success: {e:?}")
}
PusPacketHandlerResult::CustomSubservice(invalid, _) => {
warn!("PUS mode service: invalid subservice {invalid}");
}
PusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
warn!("PUS mode service: {subservice} not implemented");
}
PusPacketHandlerResult::Empty => return HandlingStatus::Empty,
},
Err(error) => {
error!("PUS mode service: packet handling error: {error:?}");
// To avoid permanent loops on error cases.
return HandlingStatus::Empty;
}
}
HandlingStatus::HandledOne
}
fn poll_and_handle_next_reply(&mut self, time_stamp: &[u8]) -> HandlingStatus {
self.service
.poll_and_check_next_reply(time_stamp)
.unwrap_or_else(|e| {
warn!("PUS action service: Handling reply failed with error {e:?}");
HandlingStatus::HandledOne
})
}
fn check_for_request_timeouts(&mut self) {
self.service.check_for_request_timeouts();
impl ModeNode for ModeServiceWrapper {
fn id(&self) -> ComponentId {
self.service.service_helper.id()
}
}
impl ModeParent for ModeServiceWrapper {
type Sender = mpsc::SyncSender<GenericMessage<ModeRequest>>;
fn add_mode_child(&mut self, id: ComponentId, request_sender: Self::Sender) {
self.service
.request_router
.mode_router_map
.insert(id, request_sender);
}
}
impl TargetedPusService for ModeServiceWrapper {
const SERVICE_ID: u8 = CustomPusServiceId::Mode as u8;
const SERVICE_STR: &'static str = "mode";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer;
use arbitrary_int::u14;
use satrs::pus::test_util::{TEST_APID, TEST_COMPONENT_ID_0, TEST_UNIQUE_ID_0};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
mode::{ModeAndSubmode, ModeReply, ModeRequest},
pus::mode::Subservice,
@@ -323,7 +316,7 @@ mod tests {
SpHeader,
},
};
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
use crate::pus::{
mode::ModeReplyHandler,
@@ -336,11 +329,12 @@ mod tests {
fn mode_converter_read_mode_request() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcReadMode as u8);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header =
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcReadMode as u8));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
@@ -352,31 +346,41 @@ mod tests {
fn mode_converter_set_mode_request() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcSetMode as u8);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header =
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcSetMode as u8));
let mut app_data: [u8; 4 + ModeAndSubmode::RAW_LEN] = [0; 4 + ModeAndSubmode::RAW_LEN];
let mode_and_submode = ModeAndSubmode::new(2, 1);
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
mode_and_submode
.write_to_be_bytes(&mut app_data[4..])
.unwrap();
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion has failed");
assert_eq!(req, ModeRequest::SetMode(mode_and_submode));
assert_eq!(
req,
ModeRequest::SetMode {
mode_and_submode,
forced: false
}
);
}
#[test]
fn mode_converter_announce_mode() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sec_header = PusTcSecondaryHeader::new_simple(200, Subservice::TcAnnounceMode as u8);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
200,
Subservice::TcAnnounceMode as u8,
));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
@@ -388,12 +392,14 @@ mod tests {
fn mode_converter_announce_mode_recursively() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, 0, 0);
let sec_header =
PusTcSecondaryHeader::new_simple(200, Subservice::TcAnnounceModeRecursive as u8);
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
200,
Subservice::TcAnnounceModeRecursive as u8,
));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, true);
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
@@ -409,7 +415,7 @@ mod tests {
);
let mode_reply = ModeReply::ModeReply(ModeAndSubmode::new(5, 1));
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u64), mode_reply);
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), mode_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
@@ -0,0 +1,200 @@
use std::sync::mpsc;
use std::time::Duration;
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use log::info;
use satrs::pool::{PoolProvider, StaticMemoryPool};
use satrs::pus::scheduler::{PusSchedulerAlloc, TcInfo};
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
PartialPusHandlingError, PusServiceHelper,
};
use satrs::spacepackets::ecss::PusServiceId;
use satrs::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool};
use satrs::ComponentId;
use example_std::ids::sched::PUS_SCHED;
use super::{DirectPusService, HandlingStatus};
pub trait TcReleaseProvider {
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
}
impl TcReleaseProvider for PacketSenderWithSharedPool {
fn release(
&mut self,
sender_id: ComponentId,
enabled: bool,
_info: &TcInfo,
tc: &[u8],
) -> bool {
if enabled {
let shared_pool = self.shared_pool.get_mut();
// Transfer TC from scheduler TC pool to shared TC pool.
let released_tc_addr = shared_pool
.0
.write()
.expect("locking pool failed")
.add(tc)
.expect("adding TC to shared pool failed");
self.sender
.send(PacketInPool::new(sender_id, released_tc_addr))
.expect("sending TC to TC source failed");
}
true
}
}
impl TcReleaseProvider for mpsc::SyncSender<PacketAsVec> {
fn release(
&mut self,
sender_id: ComponentId,
enabled: bool,
_info: &TcInfo,
tc: &[u8],
) -> bool {
if enabled {
// Send released TC to centralized TC source.
self.send(PacketAsVec::new(sender_id, tc.to_vec()))
.expect("sending TC to TC source failed");
}
true
}
}
#[allow(dead_code)]
pub enum TcReleaser {
Static(PacketSenderWithSharedPool),
Heap(mpsc::SyncSender<PacketAsVec>),
}
impl TcReleaseProvider for TcReleaser {
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool {
match self {
TcReleaser::Static(sender) => sender.release(sender_id, enabled, info, tc),
TcReleaser::Heap(sender) => sender.release(sender_id, enabled, info, tc),
}
}
}
pub struct SchedulingServiceWrapper {
pub pus_11_handler: PusSchedServiceHandler<
MpscTcReceiver,
TmTcSender,
EcssTcCacher,
VerificationReporter,
PusSchedulerAlloc,
>,
pub sched_tc_pool: StaticMemoryPool,
pub releaser_buf: [u8; 4096],
pub tc_releaser: TcReleaser,
}
impl DirectPusService for SchedulingServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Verification as u8;
const SERVICE_STR: &'static str = "verification";
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let result = self.pus_11_handler.poll_and_handle_next_tc(
error_handler,
time_stamp,
&mut self.sched_tc_pool,
);
if let Err(e) = result {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match result.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
}
HandlingStatus::HandledOne
}
}
impl SchedulingServiceWrapper {
pub fn release_tcs(&mut self) {
let id = self.pus_11_handler.service_helper.id();
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
self.tc_releaser.release(id, enabled, info, tc)
};
self.pus_11_handler
.scheduler_mut()
.update_time_from_now()
.unwrap();
let released_tcs = self
.pus_11_handler
.scheduler_mut()
.release_telecommands_with_buffer(
releaser,
&mut self.sched_tc_pool,
&mut self.releaser_buf,
)
.expect("releasing TCs failed");
if released_tcs > 0 {
info!("{released_tcs} TC(s) released from scheduler");
}
}
}
pub fn create_scheduler_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
tc_releaser: TcReleaser,
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
sched_tc_pool: StaticMemoryPool,
) -> SchedulingServiceWrapper {
let scheduler = PusSchedulerAlloc::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed");
let pus_11_handler = PusSchedServiceHandler::new(
PusServiceHelper::new(
PUS_SCHED.id(),
pus_sched_rx,
tm_sender,
create_verification_reporter(PUS_SCHED.id(), PUS_SCHED.apid),
tc_in_mem_converter,
),
scheduler,
);
SchedulingServiceWrapper {
pus_11_handler,
sched_tc_pool,
releaser_buf: [0; 4096],
tc_releaser,
}
}
@@ -0,0 +1,88 @@
use crate::pus::mode::ModeServiceWrapper;
use derive_new::new;
use satrs::spacepackets::time::{cds, TimeWriter};
use super::{
action::ActionServiceWrapper, event::EventServiceWrapper, hk::HkServiceWrapper,
scheduler::SchedulingServiceWrapper, test::TestCustomServiceWrapper, DirectPusService,
HandlingStatus, TargetedPusService,
};
#[derive(new)]
pub struct PusStack {
pub test_srv: TestCustomServiceWrapper,
pub hk_srv_wrapper: HkServiceWrapper,
pub event_srv: EventServiceWrapper,
pub action_srv_wrapper: ActionServiceWrapper,
pub schedule_srv: SchedulingServiceWrapper,
pub mode_srv: ModeServiceWrapper,
}
impl PusStack {
pub fn periodic_operation(&mut self) {
// Release all telecommands which reached their release time before calling the service
// handlers.
self.schedule_srv.release_tcs();
let timestamp = cds::CdsTime::now_with_u16_days()
.expect("time stamp generation error")
.to_vec()
.unwrap();
let mut loop_count = 0_u32;
// Hot loop which will run continuously until all request and reply handling is done.
loop {
let mut nothing_to_do = true;
Self::direct_service_checker(&mut self.test_srv, &timestamp, &mut nothing_to_do);
Self::direct_service_checker(&mut self.schedule_srv, &timestamp, &mut nothing_to_do);
Self::direct_service_checker(&mut self.event_srv, &timestamp, &mut nothing_to_do);
Self::targeted_service_checker(
&mut self.action_srv_wrapper,
&timestamp,
&mut nothing_to_do,
);
Self::targeted_service_checker(
&mut self.hk_srv_wrapper,
&timestamp,
&mut nothing_to_do,
);
Self::targeted_service_checker(&mut self.mode_srv, &timestamp, &mut nothing_to_do);
if nothing_to_do {
// Timeout checking is only done once.
self.action_srv_wrapper.check_for_request_timeouts();
self.hk_srv_wrapper.check_for_request_timeouts();
self.mode_srv.check_for_request_timeouts();
break;
}
// Safety mechanism to avoid infinite loops.
loop_count += 1;
if loop_count >= 500 {
log::warn!("reached PUS stack loop count 500, breaking");
break;
}
}
}
pub fn direct_service_checker<S: DirectPusService>(
service: &mut S,
timestamp: &[u8],
nothing_to_do: &mut bool,
) {
let handling_status = service.poll_and_handle_next_tc(timestamp);
if handling_status == HandlingStatus::HandledOne {
*nothing_to_do = false;
}
}
pub fn targeted_service_checker<S: TargetedPusService>(
service: &mut S,
timestamp: &[u8],
nothing_to_do: &mut bool,
) {
let request_handling = service.poll_and_handle_next_tc_default_handler(timestamp);
let reply_handling = service.poll_and_handle_next_reply_default_handler(timestamp);
if request_handling == HandlingStatus::HandledOne
|| reply_handling == HandlingStatus::HandledOne
{
*nothing_to_do = false;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use log::info;
use satrs::event_man_legacy::{EventMessage, EventMessageU32};
use satrs::pus::test::PusService17TestHandler;
use satrs::pus::verification::{FailParams, VerificationReporter, VerificationReportingProvider};
use satrs::pus::PartialPusHandlingError;
use satrs::pus::{
CacheAndReadRawEcssTc, DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher,
MpscTcReceiver, PusServiceHelper,
};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use example_std::config::{tmtc_err, TEST_EVENT};
use example_std::ids::generic_pus::PUS_TEST;
use std::sync::mpsc;
use super::{DirectPusService, HandlingStatus};
pub fn create_test_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
event_sender: mpsc::SyncSender<EventMessageU32>,
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
) -> TestCustomServiceWrapper {
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
PUS_TEST.id(),
pus_test_rx,
tm_sender,
create_verification_reporter(PUS_TEST.id(), PUS_TEST.apid),
tc_in_mem_converter,
));
TestCustomServiceWrapper {
handler: pus17_handler,
event_tx: event_sender,
}
}
pub struct TestCustomServiceWrapper {
pub handler:
PusService17TestHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
pub event_tx: mpsc::SyncSender<EventMessageU32>,
}
impl DirectPusService for TestCustomServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Test as u8;
const SERVICE_STR: &'static str = "test";
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let res = self
.handler
.poll_and_handle_next_tc(error_handler, timestamp);
if let Err(e) = res {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match res.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => {
if handling_status == HandlingStatus::HandledOne {
info!("Received PUS ping command TC[17,1]");
info!("Sent ping reply PUS TM[17,2]");
}
return handling_status;
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::CustomSubservice(subservice, token) => {
let tc = PusTcReader::new(
self.handler
.service_helper
.tc_in_mem_converter
.tc_slice_raw(),
)
.unwrap();
if subservice == 128 {
info!("generating test event");
self.event_tx
.send(EventMessage::new(PUS_TEST.id(), TEST_EVENT.into()))
.expect("Sending test event failed");
match self.handler.service_helper.verif_reporter().start_success(
self.handler.service_helper.tm_sender(),
token,
timestamp,
) {
Ok(started_token) => {
if let Err(e) = self
.handler
.service_helper
.verif_reporter()
.completion_success(
self.handler.service_helper.tm_sender(),
started_token,
timestamp,
)
{
error_handler(&PartialPusHandlingError::Verification(e));
}
}
Err(e) => {
error_handler(&PartialPusHandlingError::Verification(e));
}
}
} else {
let fail_data = [tc.message_subtype_id()];
self.handler
.service_helper
.verif_reporter()
.start_failure(
self.handler.service_helper.tm_sender(),
token,
FailParams::new(
timestamp,
&tmtc_err::INVALID_PUS_SUBSERVICE,
&fail_data,
),
)
.expect("Sending start failure verification failed");
}
}
}
HandlingStatus::HandledOne
}
}
@@ -8,15 +8,15 @@ use satrs::mode::ModeRequest;
use satrs::pus::verification::{
FailParams, TcStateAccepted, VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{ActiveRequestProvider, EcssTmSender, GenericRoutingError, PusRequestRouter};
use satrs::pus::{ActiveRequest, EcssTmSender, GenericRoutingError, PusRequestRouter};
use satrs::queue::GenericSendError;
use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::PusPacket;
use satrs::ComponentId;
use satrs_example::config::components::PUS_ROUTING_SERVICE;
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
/*
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum CompositeRequest {
@@ -26,16 +26,18 @@ pub enum CompositeRequest {
#[derive(Clone)]
pub struct GenericRequestRouter {
#[allow(dead_code)]
pub id: ComponentId,
// All messages which do not have a dedicated queue.
pub composite_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<CompositeRequest>>>,
pub mode_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<ModeRequest>>>,
pub composite_router_map:
HashMap<ComponentId, mpsc::SyncSender<GenericMessage<CompositeRequest>>>,
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
}
impl Default for GenericRequestRouter {
fn default() -> Self {
Self {
id: PUS_ROUTING_SERVICE.raw(),
id: ids::generic_pus::PUS_ROUTING.raw(),
composite_router_map: Default::default(),
mode_router_map: Default::default(),
}
@@ -44,7 +46,7 @@ impl Default for GenericRequestRouter {
impl GenericRequestRouter {
pub(crate) fn handle_error_generic(
&self,
active_request: &impl ActiveRequestProvider,
active_request: &impl ActiveRequest,
tc: &PusTcReader,
error: GenericRoutingError,
tm_sender: &(impl EcssTmSender + ?Sized),
@@ -53,7 +55,7 @@ impl GenericRequestRouter {
) {
warn!(
"Routing request for service {} failed: {error:?}",
tc.service()
tc.service_type_id()
);
let accepted_token: VerificationToken<TcStateAccepted> = active_request
.token()
@@ -64,7 +66,8 @@ impl GenericRequestRouter {
let apid_target_id = UniqueApidTargetId::from(id);
warn!("Target APID for request: {}", apid_target_id.apid);
warn!("Target Unique ID for request: {}", apid_target_id.unique_id);
let mut fail_data: [u8; 8] = [0; 8];
let mut fail_data: [u8; core::mem::size_of::<ComponentId>()] =
[0; core::mem::size_of::<ComponentId>()];
fail_data.copy_from_slice(&id.to_be_bytes());
verif_reporter
.completion_failure(
@@ -150,3 +153,4 @@ impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
Err(GenericRoutingError::UnknownTargetId(target_id))
}
}
*/
+158
View File
@@ -0,0 +1,158 @@
extern crate alloc;
use std::{
sync::mpsc,
time::{Duration, Instant},
};
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
pub use types::ComponentId;
use types::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
pub mod config;
/// Simple type modelling packet stored in the heap. This structure is intended to
/// be used when sending a packet via a message queue, so it also contains the sender ID.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PacketAsVec {
pub sender_id: ComponentId,
pub packet: Vec<u8>,
}
impl PacketAsVec {
pub fn new(sender_id: ComponentId, packet: Vec<u8>) -> Self {
Self { sender_id, packet }
}
}
pub struct TimestampHelper {
stamper: CdsTime,
time_stamp: [u8; 7],
}
impl TimestampHelper {
pub fn stamp(&self) -> &[u8] {
&self.time_stamp
}
pub fn update_from_now(&mut self) {
self.stamper
.update_from_now()
.expect("Updating timestamp failed");
self.stamper
.write_to_bytes(&mut self.time_stamp)
.expect("Writing timestamp failed");
}
}
impl Default for TimestampHelper {
fn default() -> Self {
Self {
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),
time_stamp: Default::default(),
}
}
}
/// Helper structure for periodic HK generation of a single set.
#[derive(Debug)]
pub struct HkHelperSingleSet {
pub enabled: bool,
pub frequency: Duration,
pub last_generated: Option<Instant>,
}
impl HkHelperSingleSet {
#[inline]
pub const fn new(enabled: bool, init_frequency: Duration) -> Self {
Self {
enabled,
frequency: init_frequency,
last_generated: None,
}
}
#[inline]
pub const fn enabled(&self) -> bool {
self.enabled
}
/// Check whether a new HK packet needs to be generated.
pub fn needs_generation(&mut self) -> bool {
if !self.enabled {
return false;
}
if self.last_generated.is_none() {
self.last_generated = Some(Instant::now());
return true;
}
let last_generated = self.last_generated.unwrap();
if Instant::now() - last_generated >= self.frequency {
self.last_generated = Some(Instant::now());
return true;
}
false
}
}
#[derive(Debug)]
pub struct TmtcQueues {
pub tc_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
#[derive(Debug)]
pub struct ModeHelper<Mode, TransitionState> {
pub current: Mode,
pub target: Option<Mode>,
pub tc_commander: Option<CcsdsPacketIdAndPsc>,
pub transition_start: Option<Instant>,
pub timeout: Duration,
pub transition_state: TransitionState,
}
impl<Mode: Copy + Clone, TransitionState: Default> ModeHelper<Mode, TransitionState> {
pub fn new(init_mode: Mode, timeout: Duration) -> Self {
Self {
current: init_mode,
target: Default::default(),
tc_commander: Default::default(),
transition_start: None,
timeout,
transition_state: Default::default(),
}
}
pub fn start(&mut self, target: Mode) {
self.target = Some(target);
self.transition_start = Some(Instant::now());
self.transition_state = TransitionState::default();
}
#[inline]
pub fn transition_active(&self) -> bool {
self.target.is_some()
}
pub fn timed_out(&self) -> bool {
if self.target.is_none() {
return false;
}
if let Some(transition_start) = self.transition_start {
return Instant::now() - transition_start >= self.timeout;
}
false
}
pub fn finish(&mut self, success: bool) -> Option<CcsdsPacketIdAndPsc> {
self.target?;
if success {
self.current = self.target.take().unwrap();
} else {
self.target = None;
}
self.transition_state = Default::default();
self.transition_start = None;
self.tc_commander.take()
}
}
@@ -1,4 +1,13 @@
use std::str::FromStr as _;
pub fn setup_logger() -> Result<(), fern::InitError> {
// Read the RUST_LOG environment variable and parse it.
// If the variable is not set or is invalid, default to Debug.
let mut log_level = log::LevelFilter::Info;
if let Ok(log_level_str) = std::env::var("RUST_LOG") {
log_level = log::LevelFilter::from_str(&log_level_str).unwrap_or(log::LevelFilter::Info);
}
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@@ -9,7 +18,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
message
))
})
.level(log::LevelFilter::Debug)
.level(log_level)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
+475
View File
@@ -0,0 +1,475 @@
use std::{
net::{IpAddr, SocketAddr},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
mpsc,
},
thread,
time::Duration,
};
use eps::{
PowerSwitchHelper,
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
};
use example_std::{
TmtcQueues,
config::{
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
},
};
use interface::{
sim_client_udp::create_sim_client,
tcp::{SyncTcpTmSource, TcpTask},
udp::UdpTmtcServer,
};
use log::info;
use logger::setup_logger;
use satrs::{
HandlingStatus,
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
spacepackets::time::cds::CdsTime,
};
use tmtc::sender::TmTcSender;
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
use types::{ComponentId, DeviceMode};
use crate::{
acs::{ctrl, mgm, mgm_assembly, mgt, subsystem},
controller::Controller,
eps::pcdu::SwitchSet,
event_manager::EventManager,
interface::udp::UdpTmHandlerWithChannel,
tmtc::tc_source::CcsdsDistributor,
};
mod acs;
mod ccsds;
mod controller;
mod device_fdir;
mod device_mode;
mod eps;
mod event_manager;
mod interface;
mod logger;
mod tmtc;
fn main() {
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
setup_logger().expect("setting up logging with fern failed");
println!("Runng OBSW example");
ctrlc::set_handler(move || {
log::info!("Received Ctrl-C, shutting down");
KILL_SIGNAL.store(true, Ordering::Relaxed);
})
.expect("Error setting Ctrl-C handler");
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
let (sim_request_tx, sim_request_rx) = mpsc::channel();
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
let (mgt_sim_reply_tx, mgt_sim_reply_rx) = mpsc::channel();
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
let mut opt_sim_client = create_sim_client(sim_request_rx);
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
let (event_manager_tc_tx, event_manager_tc_rx) = mpsc::sync_channel(10);
let (mgt_request_tx, mgt_request_rx) = mpsc::sync_channel(5);
let (mgt_report_tx, mgt_report_rx) = mpsc::sync_channel(5);
let (acs_ctrl_request_tx, acs_ctrl_request_rx) = mpsc::sync_channel(5);
let (acs_ctrl_response_tx, acs_ctrl_response_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and ACS subsystem.
let (mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
let (mgm_assembly_report_tx, mgm_assembly_report_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and MGM devices.
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
let mut event_manager = EventManager::new(
event_manager_tc_rx,
event_ctrl_rx,
mgm_event_rx,
mgm_assembly_event_rx,
mgt_event_rx,
pcdu_event_rx,
tc_source_event_rx,
tm_sink_tx.clone(),
);
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
let ccsds_distributor = CcsdsDistributor::default();
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor, tc_source_event_tx);
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
let udp_tm_handler = UdpTmHandlerWithChannel {
tm_rx: tm_server_rx,
};
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
let udp_tc_server = UdpTcServer::new(
ComponentId::UdpServer as u32,
sock_addr,
2048,
tc_sender.clone(),
)
.expect("creating UDP TMTC server failed");
let mut udp_tmtc_server = UdpTmtcServer {
udp_tc_server,
tm_handler: udp_tm_handler.into(),
};
let tcp_server_cfg = ServerConfig::new(
ComponentId::TcpServer as u32,
sock_addr,
Duration::from_millis(400),
4096,
8192,
);
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
let mut tcp_server = TcpTask::new(
tcp_server_cfg,
sync_tm_tcp_source.clone(),
tc_sender,
PACKET_ID_VALIDATOR.clone(),
)
.expect("tcp server creation failed");
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
let shared_switch_set = Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
// Global FDIR health table, shared by all software objects.
let health_table = satrs::health::HealthTableMapSync::default();
let shared_mgm_0_set = Arc::default();
let shared_mgm_1_set = Arc::default();
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
opt_sim_client.as_mut()
{
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_0_sim_reply_rx,
}),
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_1,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_1_sim_reply_rx,
}),
)
} else {
(
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
)
};
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_0,
TmtcQueues {
tc_rx: mgm_0_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_0_spi_interface,
shared_mgm_0_set,
mgm::ModeLeafHelper {
request_rx: mgm_0_mode_request_rx,
report_tx: mgm_0_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx.clone(),
);
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_1,
TmtcQueues {
tc_rx: mgm_1_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_1_spi_interface,
shared_mgm_1_set,
mgm::ModeLeafHelper {
request_rx: mgm_1_mode_request_rx,
report_tx: mgm_1_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx,
);
let mut mgm_assembly = mgm_assembly::Assembly::new(
mgm_assembly::ParentQueueHelper {
request_rx: mgm_assembly_request_rx,
report_tx: mgm_assembly_report_tx,
},
mgm_assembly::ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues {
tc_rx: mgm_assembly_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
Duration::from_millis(2000),
mgm_assembly_event_tx,
);
let mut acs_controller = ctrl::Controller::new(ctrl::ModeLeafHelper {
request_rx: acs_ctrl_request_rx,
report_tx: acs_ctrl_response_tx,
});
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
mgt::MgtCommunication::Sim(mgt::SimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgt_sim_reply_rx,
})
} else {
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
};
let mut mgt_handler = mgt::MgtHandler::new(
TmtcQueues {
tc_rx: mgt_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgt_com,
mgt::ModeLeafHelper {
request_rx: mgt_request_rx,
report_tx: mgt_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgt_event_tx,
);
let mut acs_subsystem = subsystem::Subsystem::new(
subsystem::ModeRequestSenders {
mode_request_ctrl: acs_ctrl_request_tx,
mode_request_mgm_assy: mgm_assembly_request_tx,
mode_request_mgt: mgt_request_tx,
},
subsystem::ModeReportReceivers {
mode_response_ctrl: acs_ctrl_response_rx,
mode_response_mgm_assy: mgm_assembly_report_rx,
mode_response_mgt: mgt_report_rx,
},
TmtcQueues {
tc_rx: acs_subsystem_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
);
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
))
} else {
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
};
let mut pcdu_handler = PcduHandler::new(
pcdu_handler_tc_rx,
tm_sink_tx.clone(),
switch_request_rx,
pcdu_serial_interface,
shared_switch_set,
DeviceMode::Normal,
pcdu_event_tx,
);
// The PCDU is a critical component which should be in normal mode immediately.
pcdu_handler_mode_tx
.send(types::pcdu::request::Request::Mode(DeviceMode::Normal))
.expect("sending initial mode request failed");
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC & UDP".to_string())
.spawn(move || {
info!("Running UDP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
udp_tmtc_server.periodic_operation();
tc_source.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
.spawn(move || {
info!("Running TCP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tcp_server.periodic_operation();
}
})
.unwrap();
info!("Starting TM funnel task");
let jh_tm_funnel = thread::Builder::new()
.name("TM SINK".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tm_sink.operation();
}
})
.unwrap();
let mut opt_jh_sim_client = None;
if let Some(mut sim_client) = opt_sim_client {
info!("Starting UDP sim client task");
opt_jh_sim_client = Some(
thread::Builder::new()
.name("SIM ADAPTER".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
if sim_client.operation() == HandlingStatus::Empty {
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
}
}
})
.unwrap(),
);
}
info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new()
.name("AOCS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
mgm_0_handler.periodic_operation();
mgm_1_handler.periodic_operation();
mgm_assembly.periodic_operation();
acs_controller.periodic_operation();
mgt_handler.periodic_operation();
acs_subsystem.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
}
})
.unwrap();
info!("Starting EPS thread");
let jh_eps = thread::Builder::new()
.name("EPS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
// TODO: We should introduce something like a fixed timeslot helper to allow a more
// declarative API. It would also be very useful for the AOCS task.
//
// TODO: The fixed timeslot handler exists.. use it.
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(300));
}
})
.unwrap();
info!("Starting controller thread");
let jh_controller_thread = thread::Builder::new()
.name("CTRL".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
controller.periodic_operation();
event_manager.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
}
})
.unwrap();
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh_tm_funnel
.join()
.expect("Joining TM Funnel thread failed");
if let Some(jh_sim_client) = opt_jh_sim_client {
jh_sim_client
.join()
.expect("Joining SIM client thread failed");
}
jh_aocs.join().expect("Joining AOCS thread failed");
jh_eps.join().expect("Joining EPS thread failed");
jh_controller_thread
.join()
.expect("Joining PUS handler thread failed");
}
pub fn update_time(time_provider: &mut CdsTime, timestamp: &mut [u8]) {
time_provider
.update_from_now()
.expect("Could not get current time");
time_provider
.write_to_bytes(timestamp)
.expect("Writing timestamp failed");
}
@@ -1,2 +1,3 @@
pub mod sender;
pub mod tc_source;
pub mod tm_sink;
+55
View File
@@ -0,0 +1,55 @@
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
use satrs::{
ComponentId,
queue::GenericSendError,
tmtc::{PacketAsVec, PacketHandler},
};
#[derive(Default, Debug, Clone)]
pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum TmTcSender {
Normal(mpsc::SyncSender<PacketAsVec>),
Mock(MockSender),
}
impl TmTcSender {
#[allow(dead_code)]
pub fn get_mock_sender(&mut self) -> Option<&mut MockSender> {
match self {
TmTcSender::Mock(sender) => Some(sender),
_ => None,
}
}
}
impl PacketHandler for TmTcSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
match self {
TmTcSender::Normal(sync_sender) => {
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
}
}
TmTcSender::Mock(sender) => {
sender.handle_packet(sender_id, packet).unwrap();
}
}
Ok(())
}
}
impl PacketHandler for MockSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut mut_queue = self.0.borrow_mut();
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
Ok(())
}
}
+240
View File
@@ -0,0 +1,240 @@
use satrs::{
ComponentId as RawComponentId, HandlingStatus,
spacepackets::{CcsdsPacketReader, ChecksumType},
tmtc::PacketAsVec,
};
use std::{
collections::HashMap,
sync::mpsc::{self, TryRecvError},
};
use types::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned, tmtc};
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
// TC source components where the heap is the backing memory of the received telecommands.
pub struct TcSourceTask {
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
}
impl TcSourceTask {
pub fn new(
tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
) -> Self {
Self {
tc_receiver,
ccsds_distributor,
event_tx,
}
}
pub fn add_target(
&mut self,
target_id: ComponentId,
sender: mpsc::SyncSender<CcsdsTcPacketOwned>,
) {
self.ccsds_distributor.insert(target_id, sender);
}
pub fn periodic_operation(&mut self) {
loop {
if self.poll_tc() == HandlingStatus::Empty {
break;
}
}
}
pub fn poll_tc(&mut self) -> HandlingStatus {
match self.tc_receiver.try_recv() {
Ok(packet) => {
log::debug!("received raw packet: {:?}", packet);
let ccsds_tc_reader_result =
CcsdsPacketReader::new(&packet.packet, Some(ChecksumType::WithCrc16));
if ccsds_tc_reader_result.is_err() {
log::warn!(
"received invalid CCSDS TC packet: {:?}",
ccsds_tc_reader_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcPacket);
return HandlingStatus::HandledOne;
}
let ccsds_tc_reader = ccsds_tc_reader_result.unwrap();
let tc_header_result =
postcard::take_from_bytes::<TcHeader>(ccsds_tc_reader.user_data());
if tc_header_result.is_err() {
log::warn!(
"received CCSDS TC packet with invalid TC header: {:?}",
tc_header_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcHeader);
return HandlingStatus::HandledOne;
}
let (tc_header, payload) = tc_header_result.unwrap();
if let Some(sender) = self.ccsds_distributor.get(&tc_header.target_id) {
log::debug!("sending TC packet to target ID: {:?}", tc_header.target_id);
sender
.send(CcsdsTcPacketOwned {
sp_header: *ccsds_tc_reader.sp_header(),
tc_header,
payload: payload.to_vec(),
})
.ok();
} else {
log::warn!("no TC handler for target ID {:?}", tc_header.target_id);
self.send_event(
packet.sender_id,
tmtc::Event::UnknownTargetId(tc_header.target_id),
);
}
HandlingStatus::HandledOne
}
Err(e) => match e {
TryRecvError::Empty => HandlingStatus::Empty,
TryRecvError::Disconnected => {
log::warn!("tmtc thread: sender disconnected");
HandlingStatus::Empty
}
},
}
}
/// `sender_id` is the raw ID tagged on the received packet, which is not necessarily a
/// known [ComponentId] (e.g. a spoofed or garbled packet). Falls back to [ComponentId::Ground]
/// as the event sender in that case.
fn send_event(&self, sender_id: RawComponentId, event: tmtc::Event) {
let sender_id = ComponentId::try_from(sender_id).unwrap_or_else(|_| {
log::warn!("TC source event for unknown raw sender ID {}", sender_id);
ComponentId::Ground
});
if let Err(e) = self.event_tx.send((sender_id, event)) {
log::warn!("failed to send TC source event: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketCreatorOwned, ChecksumType, PacketType, SpacePacketHeader,
};
use types::{Apid, MessageType, ccsds::CcsdsTcPacketOwned};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<PacketAsVec>,
target_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
event_rx: mpsc::Receiver<(ComponentId, tmtc::Event)>,
tc_source: TcSourceTask,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_source_rx) = mpsc::sync_channel(5);
let (target_tx, target_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
let mut tc_source =
TcSourceTask::new(tc_source_rx, CcsdsDistributor::default(), event_tx);
tc_source.add_target(ComponentId::EpsPcdu, target_tx);
Self {
tc_tx,
target_rx,
event_rx,
tc_source,
}
}
}
fn valid_tc_raw(target_id: ComponentId) -> Vec<u8> {
CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, MessageType::Ping),
(),
)
.to_vec()
}
/// A structurally valid CCSDS packet (correct length and CRC), but with arbitrary user data
/// instead of a postcard-encoded [TcHeader].
fn raw_ccsds_tc_with_user_data(user_data: &[u8]) -> Vec<u8> {
CcsdsPacketCreatorOwned::new(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
PacketType::Tc,
user_data,
Some(ChecksumType::WithCrc16),
)
.unwrap()
.to_vec()
}
#[test]
fn test_valid_tc_is_routed_without_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::EpsPcdu),
))
.unwrap();
tb.tc_source.periodic_operation();
tb.target_rx.try_recv().expect("TC was not routed");
assert!(matches!(tb.event_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_invalid_ccsds_packet_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
vec![1, 2, 3],
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcPacket));
}
#[test]
fn test_invalid_tc_header_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
// A single byte is enough to decode the `ComponentId` discriminant, but not
// enough for the trailing `MessageType`, so `TcHeader` deserialization fails
// while the CCSDS packet itself stays valid.
raw_ccsds_tc_with_user_data(&[0]),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcHeader));
}
#[test]
fn test_unknown_target_id_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::Ground),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(
event,
tmtc::Event::UnknownTargetId(ComponentId::Ground)
));
}
}
+58
View File
@@ -0,0 +1,58 @@
use std::{
collections::HashMap,
sync::mpsc::{self},
};
use arbitrary_int::{u11, u14};
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
use types::ccsds::CcsdsTmPacketOwned;
use crate::interface::tcp::SyncTcpTmSource;
#[derive(Default)]
pub struct CcsdsSeqCounterMap {
apid_seq_counter_map: HashMap<u11, SequenceCounterCcsdsSimple>,
}
impl CcsdsSeqCounterMap {
pub fn get_and_increment(&mut self, apid: u11) -> u14 {
u14::new(
self.apid_seq_counter_map
.entry(apid)
.or_default()
.get_and_increment(),
)
}
}
pub struct TmSink {
seq_counter_map: CcsdsSeqCounterMap,
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
impl TmSink {
pub fn new(
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
seq_counter_map: Default::default(),
sync_tm_tcp_source,
tm_funnel_rx,
tm_server_tx,
}
}
pub fn operation(&mut self) {
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
tm.sp_header
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
self.tm_server_tx
.send(tm)
.expect("Sending TM to server failed");
}
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "minisim-types"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { version = "0.7", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
tai-time = { version = "1", default-features = false, features = ["serde"] }
thiserror = { version = "2", default-features = false }
types = { path = "../types" }
[dev-dependencies]
postcard = { version = "1", features = ["alloc"] }
+472
View File
@@ -0,0 +1,472 @@
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use tai_time::MonotonicTime;
use crate::{
acs::{mgm, mgt},
eps::{PcduReply, PcduRequest},
};
/// Used by clients to route replies to the component handling them.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum ComponentId {
SimCtrl,
Mgm0Lis3Mdl,
Mgm1Lis3Mdl,
Mgt,
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimRequest {
SimCtrl(SimCtrlRequest),
Mgm {
id: mgm::Id,
request: mgm::Request,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduRequest),
}
impl From<SimCtrlRequest> for SimRequest {
fn from(request: SimCtrlRequest) -> Self {
Self::SimCtrl(request)
}
}
impl From<mgt::Request> for SimRequest {
fn from(request: mgt::Request) -> Self {
Self::Mgt(request.to_frame())
}
}
impl From<PcduRequest> for SimRequest {
fn from(request: PcduRequest) -> Self {
Self::Pcdu(request)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimRequestWithTime {
pub request: SimRequest,
pub timestamp: MonotonicTime,
}
impl SimRequestWithTime {
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
Self {
request: request.into(),
timestamp,
}
}
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
Self::new(request, MonotonicTime::EPOCH)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimReply {
SimCtrl(SimCtrlReply),
Mgm {
id: mgm::Id,
reply: mgm::Reply,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduReply),
}
impl SimReply {
pub fn component(&self) -> ComponentId {
match self {
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
SimReply::Mgm { id, .. } => id.sim_component(),
SimReply::Mgt(_) => ComponentId::Mgt,
SimReply::Pcdu(_) => ComponentId::Pcdu,
}
}
}
impl From<SimCtrlReply> for SimReply {
fn from(reply: SimCtrlReply) -> Self {
Self::SimCtrl(reply)
}
}
impl From<mgt::Reply> for SimReply {
fn from(reply: mgt::Reply) -> Self {
Self::Mgt(reply.to_frame())
}
}
impl From<PcduReply> for SimReply {
fn from(reply: PcduReply) -> Self {
Self::Pcdu(reply)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlRequest {
Ping,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlReply {
Pong,
}
pub mod eps {
use super::*;
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
switch: SwitchId,
state: SwitchStateBinary,
},
RequestSwitchInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
SwitchInfo(SwitchMapBinary),
}
}
pub mod acs {
/// MGM module strongly based on the LIS3MDL device.
pub mod mgm {
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::ComponentId;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
// similar which needs to be converted to a signed float value with physical units. We will
// simplify this now and generate the signed float values directly. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reply {
pub switch_state: SwitchStateBinary,
pub sensor_values: SensorValuesMicroTesla,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: RawValues,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum Id {
Mgm0,
Mgm1,
}
impl Id {
pub const fn sim_component(&self) -> ComponentId {
match self {
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
}
}
}
impl RawValues {
pub const fn splat(value: i16) -> Self {
Self {
x: value,
y: value,
z: value,
}
}
}
}
/// Simple serial protocol of the magnetorquer.
///
/// The first byte of each frame is the packet ID. The high bit of the ID is set for replies.
/// All fields are big endian. Every command is answered with exactly one reply, but only
/// if the device is powered. The device drops invalid frames.
///
/// A data link layer is deliberately skipped for simplicity. A real serial link would need
/// framing and error detection, for example COBS encoding and a CRC. Here, the transport
/// always delivers complete and intact frames.
pub mod mgt {
use alloc::{vec, vec::Vec};
use core::time::Duration;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use types::acs::mgt::Dipole;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum RequestId {
RequestHk = 0x01,
/// Payload: dipole (3 x i16), duration in milliseconds (u32).
ApplyTorque = 0x02,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum ReplyId {
/// Payload: dipole (3 x i16), torquing flag (u8).
Hk = 0x81,
/// Reply to [RequestId::ApplyTorque].
Ack = 0x82,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FrameError {
#[error("empty frame")]
Empty,
#[error("unknown packet ID {0:#04x}")]
UnknownPacketId(u8),
#[error("invalid length {len} for packet ID {id:#04x}")]
InvalidLength { id: u8, len: usize },
}
fn split_packet_id(frame: &[u8]) -> Result<(u8, &[u8]), FrameError> {
let (&id, payload) = frame.split_first().ok_or(FrameError::Empty)?;
Ok((id, payload))
}
fn payload_array<const N: usize>(id: u8, payload: &[u8]) -> Result<&[u8; N], FrameError> {
payload.try_into().map_err(|_| FrameError::InvalidLength {
id,
len: payload.len() + 1,
})
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Request {
/// The duration has millisecond resolution on the wire.
ApplyTorque {
duration: Duration,
dipole: Dipole,
},
RequestHk,
}
impl Request {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Request::RequestHk => vec![RequestId::RequestHk.into()],
Request::ApplyTorque { duration, dipole } => {
let mut frame = vec![RequestId::ApplyTorque.into()];
frame.extend_from_slice(&dipole.to_be_bytes());
let duration_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX);
frame.extend_from_slice(&duration_ms.to_be_bytes());
frame
}
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match RequestId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
RequestId::RequestHk => {
payload_array::<0>(id, payload)?;
Ok(Request::RequestHk)
}
RequestId::ApplyTorque => {
let payload: &[u8; Dipole::LEN + 4] = payload_array(id, payload)?;
let [dipole @ .., d0, d1, d2, d3] = *payload;
Ok(Request::ApplyTorque {
duration: Duration::from_millis(
u32::from_be_bytes([d0, d1, d2, d3]).into(),
),
dipole: Dipole::from_be_bytes(&dipole),
})
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HkSet {
pub dipole: Dipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Reply {
Hk(HkSet),
Ack,
}
impl Reply {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Reply::Hk(hk) => {
let mut frame = vec![ReplyId::Hk.into()];
frame.extend_from_slice(&hk.dipole.to_be_bytes());
frame.push(hk.torquing as u8);
frame
}
Reply::Ack => vec![ReplyId::Ack.into()],
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match ReplyId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
ReplyId::Hk => {
let payload: &[u8; Dipole::LEN + 1] = payload_array(id, payload)?;
let [dipole @ .., torquing] = *payload;
Ok(Reply::Hk(HkSet {
dipole: Dipole::from_be_bytes(&dipole),
torquing: torquing != 0,
}))
}
ReplyId::Ack => {
payload_array::<0>(id, payload)?;
Ok(Reply::Ack)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_torque_frame() {
let request = Request::ApplyTorque {
duration: Duration::from_millis(0x0102_0304),
dipole: Dipole {
x: -2,
y: 0x0506,
z: 0x0708,
},
};
let frame = request.to_frame();
assert_eq!(
frame,
[
0x02, 0xff, 0xfe, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04
]
);
assert_eq!(Request::from_frame(&frame), Ok(request));
}
#[test]
fn test_request_hk_frame() {
assert_eq!(Request::RequestHk.to_frame(), [0x01]);
assert_eq!(Request::from_frame(&[0x01]), Ok(Request::RequestHk));
}
#[test]
fn test_reply_frames() {
let hk = Reply::Hk(HkSet {
dipole: Dipole { x: 1, y: 2, z: 3 },
torquing: true,
});
let frame = hk.to_frame();
assert_eq!(frame, [0x81, 0, 1, 0, 2, 0, 3, 1]);
assert_eq!(Reply::from_frame(&frame), Ok(hk));
assert_eq!(Reply::Ack.to_frame(), [0x82]);
assert_eq!(Reply::from_frame(&[0x82]), Ok(Reply::Ack));
}
#[test]
fn test_invalid_frames() {
assert_eq!(Request::from_frame(&[]), Err(FrameError::Empty));
assert_eq!(
Request::from_frame(&[0x81]),
Err(FrameError::UnknownPacketId(0x81))
);
assert_eq!(
Request::from_frame(&[0x01, 0x00]),
Err(FrameError::InvalidLength { id: 0x01, len: 2 })
);
assert_eq!(
Reply::from_frame(&[0x81, 0, 1]),
Err(FrameError::InvalidLength { id: 0x81, len: 3 })
);
}
}
}
}
pub mod udp {
pub const SIM_CTRL_PORT: u16 = 7303;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_serde_roundtrip() {
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let bytes = postcard::to_allocvec(&sim_request).unwrap();
let deserialized: SimRequestWithTime = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_request);
}
#[test]
fn test_reply_serde_roundtrip() {
let sim_reply = SimReply::from(SimCtrlReply::Pong);
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
let bytes = postcard::to_allocvec(&sim_reply).unwrap();
let deserialized: SimReply = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_reply);
}
}
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "minisim"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }
postcard = { version = "1", features = ["alloc"] }
log = "0.4"
thiserror = "2"
fern = "0.7"
strum = { version = "0.28", features = ["derive"] }
num_enum = "0.7"
humantime = "2"
nexosim = "1"
satrs = { path = "../../satrs" }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
[dev-dependencies]
delegate = "0.13"
+32
View File
@@ -0,0 +1,32 @@
sat-rs minisim
======
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
[nexosim](https://github.com/asynchronics/nexosim).
Right now, this crate is primarily used together with the
[`example-std` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std)
to simulate the devices connected to the example application.
You can simply run this application using
```sh
cargo run
```
or
```sh
cargo run -p minisim
```
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
simulation replies with any other application.
The simulator was designed in a modular way to be scalable and adaptable to other communication
schemes. This might allow it to serve a mini-simulator for other example applications which
still have similar device handlers.
The following graph shows the high-level architecture of the mini-simulator.
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
Loaded 100 of 300 files, more files were not shown because too many files have changed in this diff. Show more