96 Commits

Author SHA1 Message Date
a891b947c7 Finish PUS service optimizations
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
Rust/sat-rs/pipeline/head This commit looks good
- Better naming for pool abstractions
- Added last unittests for PUS helper services
- Introduce new abstraction for PUS schedulers
- `StoreAddr` is now a generic u64
- `spacepackets` points to 0.7.0 release
2024-02-03 13:43:46 +01:00
6152c834d4 Merge pull request 'Tricky PUS handler changes' (#95) from tricky-pus-abstraction-changes into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #95
2024-01-31 14:35:09 +01:00
e2941d34ca need a copy of that image here
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build started...
2024-01-31 14:34:50 +01:00
4ace46e141 small bugfix for python tester
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-31 12:31:09 +01:00
134feeb1b4 bump tmtccmd
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-31 12:28:45 +01:00
e2086391bc Merge remote-tracking branch 'origin/main' into tricky-pus-abstraction-changes
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2024-01-31 12:09:55 +01:00
8bb13efe80 Merge remote-tracking branch 'origin/main' into tricky-pus-abstraction-changes
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-01-31 11:40:29 +01:00
0109c6855d this might do the job
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-01-31 11:40:01 +01:00
a09af65396 Merge pull request 'TargetIdWithApid' (#63) from TargetIdWithApid into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #63
Reviewed-by: Robin Müller <muellerr@irs.uni-stuttgart.de>
2024-01-31 11:06:33 +01:00
7cbe4f1170 some progress 2024-01-31 01:32:03 +01:00
93fb38a9b7 ignore log file
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-31 00:02:27 +01:00
21961daba4 these 2 deps are not required
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-31 00:00:56 +01:00
b79b5d2009 done
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-30 23:59:29 +01:00
b27842c2bb all clippy fixes
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-30 23:48:46 +01:00
21edd1dcff Merge branch 'main' into TargetIdWithApid
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-01-30 23:31:49 +01:00
7ca4825bba Merge pull request 'Graphs for sat-rs' (#93) from improve-graphs into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #93
2024-01-30 23:31:02 +01:00
0437e2b095 add satrs-book README
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build started...
2024-01-30 23:30:53 +01:00
4e43fb8fd7 single source event manager graph, fix doc build
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-30 23:27:22 +01:00
62c9d13cec this is tricky
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2024-01-30 09:59:45 +01:00
5f227d1a20 refactored PUS handlers
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2024-01-30 01:18:48 +01:00
f3baa5247e Merge remote-tracking branch 'origin/main' into improve-graphs
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-01-29 23:42:52 +01:00
0681f5847e Merge pull request 'Continue CFDP handlers' (#90) from continue_cfdp_handlers into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #90
2024-01-29 23:42:03 +01:00
aade7c51f2 Merge remote-tracking branch 'origin/main' into TargetIdWithApid
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-01-29 23:38:32 +01:00
bf97a03730 let's finish this PR for now
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-29 23:36:34 +01:00
602aea3ec5 add new graphs
Some checks failed
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-01-29 12:00:46 +01:00
c9a7f75ca4 move images 2024-01-29 11:44:32 +01:00
dada8a775d improve some graphs 2024-01-29 11:44:02 +01:00
48b8c6891a verified first finished PDU success
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-23 19:43:52 +01:00
d8acaaf580 add test for requested closure
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2024-01-23 18:43:09 +01:00
1d19530349 Merge branch 'main' into continue_cfdp_handlers
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-01-23 16:54:38 +01:00
c2bd862ba4 typo
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-22 19:42:18 +01:00
c5054c323e add some documentation and other improvements
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-22 19:40:58 +01:00
7776847364 rework timer and packet send handling
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-22 19:24:48 +01:00
4cf96ce0d5 extend remote cfg fields
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-22 14:08:25 +01:00
303a9ab581 more docs
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-18 17:08:23 +01:00
71ce43eca6 added some docs
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-18 16:26:29 +01:00
42cb3f7e6b verify fault handling correctness
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-18 16:16:28 +01:00
620ffbb131 add test for reached check limit
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-18 15:28:15 +01:00
7f301a0771 only allow CFDP for alloc for now
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-12-15 17:21:23 +01:00
7e8be538e0 improve CFDP tests
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-15 17:18:28 +01:00
28a8b18329 the file store still required alloc
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-15 16:10:46 +01:00
2f07fdfe83 first test for check limit handling
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-15 16:08:39 +01:00
9605dbb13a mocking the check timer is tricky
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-15 15:30:41 +01:00
27c5e4d14e cleaned up tests
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-15 13:49:24 +01:00
37c2f72cbc cargo fmt
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-14 14:22:52 +01:00
4e81fd2e16 some improvements
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-14 12:27:37 +01:00
7615729af9 fix tests
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-14 12:17:16 +01:00
fbd05a4a25 add check limit handling
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-14 12:10:22 +01:00
0ecb718416 this works better apparently
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-14 00:29:02 +01:00
1b1bef2958 make it compile again
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-13 18:55:26 +01:00
bd6e1637e4 the check finally works again
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-13 15:49:26 +01:00
5a55993452 fix some tests 2023-12-13 15:35:21 +01:00
c766ab2d71 come on, neotest..
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-13 15:15:22 +01:00
a4346fd182 Merge pull request 'remove those trailing slashes' (#91) from deplyoment-remove-trailing-slashes into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #91
2023-12-11 15:22:59 +01:00
094a9f0956 remove those trailing slashes
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2023-12-11 15:12:50 +01:00
51d3c9b6e8 another spacepackets update
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-07 14:05:50 +01:00
774d9b5961 bumped spacepackets
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-07 13:29:26 +01:00
0b81198c03 that will be a lot of API changes
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-06 19:59:27 +01:00
f6d2cfa042 let's try the new spacepackets version
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-12-06 18:20:29 +01:00
4a6c28724f Merge pull request 'add coverage' (#89) from add-cov into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #89
2023-12-06 18:15:26 +01:00
8142ae9c38 add coverage
Some checks are pending
Rust/sat-rs/pipeline/head This commit looks good
Rust/sat-rs/pipeline/pr-main Build queued...
2023-12-01 22:21:12 +01:00
da8858eae0 well this is annoying..
Some checks failed
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
2023-11-24 15:47:03 +01:00
274ae654cd add new filesystem abstractions 2023-11-24 15:02:22 +01:00
f7f1017fed continue CFDP handlers
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2023-11-11 19:05:46 +01:00
2afb3de227 Merge pull request 'bump_tmtccmd' (#88) from bump_tmtccmd into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #88
2023-11-11 16:37:47 +01:00
f9b94b29dc Merge branch 'main' into bump_tmtccmd
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build queued...
2023-11-11 16:36:27 +01:00
ca360d2d8d ruff fix 2023-11-11 16:36:13 +01:00
80305466e5 bump tmtccmd dependency
Some checks are pending
Rust/sat-rs/pipeline/head Build queued...
2023-11-11 16:33:43 +01:00
bb3fd8fe74 Merge pull request 'made set_reuse_port unix only' (#87) from tcp_windows_fix into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #87
2023-10-26 13:40:39 +02:00
lkoester
e75a145b0e made set_reuse_port unix only
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2023-10-26 13:34:37 +02:00
8cab8ab011 Merge pull request 'this intermediate struct is not necessary' (#86) from simplify-some-tcp-components into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #86
2023-10-01 15:00:13 +02:00
6a300f5b65 Merge branch 'simplify-some-tcp-components' of egit.irs.uni-stuttgart.de:rust/sat-rs into simplify-some-tcp-components
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
Rust/sat-rs/pipeline/head This commit looks good
2023-10-01 14:53:33 +02:00
922631022c some tiny tweaks 2023-10-01 14:48:41 +02:00
157d904794 no box necessary.. 2023-10-01 14:32:15 +02:00
62a9f58462 Merge branch 'main' into simplify-some-tcp-components
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-10-01 13:57:16 +02:00
7654670967 this intermediate struct is not necessary
Some checks are pending
Rust/sat-rs/pipeline/head Build started...
2023-10-01 13:55:42 +02:00
ef8417d9db Merge pull request 'Example: Add TCP server' (#85) from example-add-tcp-server into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #85
2023-09-29 14:18:25 +02:00
40bf53d261 extend introduction
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-29 14:17:25 +02:00
7cfa4f9785 extend README
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-29 14:13:22 +02:00
183aca3219 TCP support working
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-29 14:11:03 +02:00
47b794e12f smaller modules
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-09-29 12:38:57 +02:00
77c06718c9 README update
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-27 14:33:24 +02:00
6bee0f35ff add structure overview
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-27 14:28:42 +02:00
8f325138ff found the bug
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2023-09-27 00:25:50 +02:00
5a3b9fb46b why is this so problematic.. 2023-09-27 00:21:03 +02:00
7ca8d52368 use explicit versions for sat-rs dependencies
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2023-09-26 23:06:52 +02:00
b458c2cb83 Merge remote-tracking branch 'origin/main' into example-add-tcp-server
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2023-09-26 23:04:51 +02:00
d21e98d2e5 start adding tcp server 2023-09-26 23:00:47 +02:00
92ac91e194 Merge branch 'main' into TargetIdWithApid
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
2023-09-26 16:00:16 +02:00
c01c6d1504 Merge branch 'main' into TargetIdWithApid
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-16 16:15:25 +02:00
a8b4519748 Merge branch 'main' into TargetIdWithApid
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-07 17:33:02 +02:00
f8df716865 Merge remote-tracking branch 'origin/main' into TargetIdWithApid
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-08-29 10:22:23 +02:00
lkoester
a3311f102e added HkUniqueId
Some checks failed
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
2023-08-15 21:07:15 +02:00
26404cdfe1 remove unused imports
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2023-08-15 20:41:51 +02:00
lkoester
5aa2ec74ba still broken
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2023-08-15 08:50:08 +02:00
lkoester
075dc38434 first implementation of apid with target id, currently main broken 2023-08-15 08:49:54 +02:00
72 changed files with 7538 additions and 2306 deletions

View File

@@ -43,3 +43,16 @@ Each project has its own `CHANGELOG.md`.
packet protocol implementations. This repository is re-exported in the packet protocol implementations. This repository is re-exported in the
[`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core) [`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core)
crate. crate.
# Coverage
Coverage was generated using [`grcov`](https://github.com/mozilla/grcov). If you have not done so
already, install the `llvm-tools-preview`:
```sh
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
optionally supply the `--open` flag to open the coverage report in your webbrowser.

View File

@@ -67,7 +67,7 @@ pipeline {
sh 'mdbook build' sh 'mdbook build'
sshagent(credentials: ['documentation-buildfix']) { sshagent(credentials: ['documentation-buildfix']) {
// Deploy to Apache webserver // Deploy to Apache webserver
sh 'rsync -r --delete book/ buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs' sh 'rsync -r --delete book buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs'
} }
} }
} }

61
coverage.py Executable file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
import os
import logging
import argparse
import webbrowser
_LOGGER = logging.getLogger()
def generate_cov_report(open_report: bool, format: str, package: str):
logging.basicConfig(level=logging.INFO)
os.environ["RUSTFLAGS"] = "-Cinstrument-coverage"
os.environ["LLVM_PROFILE_FILE"] = "target/coverage/%p-%m.profraw"
_LOGGER.info("Executing tests with coverage")
os.system(f"cargo test -p {package}")
out_path = "./target/debug/coverage"
if format == "lcov":
out_path = "./target/debug/lcov.info"
os.system(
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
f"-o {out_path}"
)
if format == "lcov":
os.system(
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
"--legend ./target/debug/lcov.info"
)
if open_report:
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
webbrowser.open_new_tab(coverage_report_path)
_LOGGER.info("Done")
def main():
parser = argparse.ArgumentParser(
description="Generate coverage report and optionally open it in a browser"
)
parser.add_argument(
"--open", action="store_true", help="Open the coverage report in a browser"
)
parser.add_argument(
"-p",
"--package",
choices=["satrs-core"],
default="satrs-core",
help="Choose project to generate coverage for",
)
parser.add_argument(
"--format",
choices=["html", "lcov"],
default="html",
help="Choose report format (html or lcov)",
)
args = parser.parse_args()
generate_cov_report(args.open, args.format, args.package)
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"> <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.22--> <!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/> <key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/> <key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/> <key for="port" id="d2" yfiles.type="portgeometry"/>
@@ -20,7 +20,7 @@
<y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/> <y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
<y:Fill hasColor="false" transparent="false"/> <y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.936037063598633" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="150.1282958984375" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="24.177873611450195" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="168.3929901123047" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@@ -31,7 +31,7 @@
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/> <y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
<y:Fill color="#FFFF00" transparent="false"/> <y:Fill color="#FFFF00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.21258544921875" x="58.393707275390625" xml:space="preserve" y="21.27395296096796">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="24.177873611450195" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="127.31723022460938" x="37.84138488769531" xml:space="preserve" y="17.911063194274846">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@@ -39,10 +39,10 @@
<node id="n2"> <node id="n2">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/> <y:Geometry height="60.0" width="92.24000000000001" x="607.36" y="413.23"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="13.4398193359375" xml:space="preserve" y="14.547905921936035">Event <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="10.039937133789067" xml:space="preserve" y="10.063962936401367">Event
Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -54,7 +54,7 @@ Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/> <y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="10.719819335937473" xml:space="preserve" y="14.547905921936035">Event <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="2.199937133789035" xml:space="preserve" y="10.063962936401367">Event
Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -63,10 +63,10 @@ Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n4"> <node id="n4">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/> <y:Geometry height="60.0" width="87.27999999999997" x="845.9410837438425" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="8.719819335937359" xml:space="preserve" y="14.547905921936035">Event <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="7.559937133789049" xml:space="preserve" y="10.063962936401367">Event
Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -78,7 +78,7 @@ Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/> <y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="16.079819335937373" xml:space="preserve" y="14.547905921936035">Event <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="7.559937133788935" xml:space="preserve" y="10.063962936401367">Event
Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -87,10 +87,10 @@ Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n6"> <node id="n6">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/> <y:Geometry height="60.0" width="145.19999999999993" x="734.2060377358491" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.78865051269531" x="16.605674743652344" xml:space="preserve" y="14.547905921936035">PUS Service 5 <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="122.38423156738281" x="11.407884216308503" xml:space="preserve" y="10.063962936401367">PUS Service 5
Event Reporting Event Reporting
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
@@ -100,10 +100,10 @@ Event Reporting
<node id="n7"> <node id="n7">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/> <y:Geometry height="60.0" width="136.8599999999999" x="901.8" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.08859252929688" x="17.2757037353515" xml:space="preserve" y="14.547905921936035">PUS Service 19 <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.78424072265625" x="13.037879638671825" xml:space="preserve" y="10.063962936401367">PUS Service 19
Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -112,10 +112,10 @@ Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel>
<node id="n8"> <node id="n8">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/> <y:Geometry height="60.0" width="97.27999999999997" x="787.1260377358491" y="733.8400000000001"/>
<y:Fill color="#FFCC99" transparent="false"/> <y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.932403564453125" x="13.673798217773424" xml:space="preserve" y="14.547905921936035">Telemetry <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.57614135742188" x="9.351929321289049" xml:space="preserve" y="10.063962936401367">Telemetry
Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@@ -124,10 +124,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
<node id="n9"> <node id="n9">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/> <y:Geometry height="218.79999999999995" width="256.8800000000001" x="1068.6599999999999" y="575.0400000000002"/>
<y:Fill hasColor="false" transparent="false"/> <y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="143.6875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="181.591796875" x="8.373079774614325" xml:space="preserve" y="7.444138124199753">Subscriptions <y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="190.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="240.7890625" x="10.203400059311889" xml:space="preserve" y="9.536167573623516">Subscriptions
1. Event Creator 0 subscribes 1. Event Creator 0 subscribes
for event 0 for event 0
@@ -144,10 +144,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
<edge id="e0" source="n4" target="n1"> <edge id="e0" source="n4" target="n1">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/> <y:Path sx="15.418916256157559" sy="0.0" tx="-10.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="8.639817810058275" xml:space="preserve" y="29.00100609374465">event 1 <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="4.48011932373015" xml:space="preserve" y="27.465240361497138">event 1
(group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> (group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@@ -161,7 +161,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="25.334655000000453" xml:space="preserve" y="-40.972107505798476">event 0 <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="24.477808328186484" xml:space="preserve" y="-43.213945007324355">event 0
(group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> (group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@@ -173,7 +173,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
<y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/> <y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="5.6761352539062955" xml:space="preserve" y="27.551854405966765">event 2 <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="5.6761352539062955" xml:space="preserve" y="26.10821814399378">event 2
(group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> (group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@@ -187,8 +187,8 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.38468933105469" x="26.667665869801795" xml:space="preserve" y="43.287014528669715">event 3 (group 2) <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.94757080078125" x="8.646225134939186" xml:space="preserve" y="27.90167113105076">event 3 (group 2)
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="75.3599999999999" distanceToCenter="true" position="left" ratio="0.2967848459873102" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.12000000000057" distanceToCenter="true" position="left" ratio="0.18074782715730137" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -196,10 +196,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<edge id="e4" source="n1" target="n6"> <edge id="e4" source="n1" target="n6">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/> <y:Path sx="-80.36000000000001" sy="0.0" tx="28.333962264150955" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="-105.378832397461" xml:space="preserve" y="15.634602566150534">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -207,10 +207,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<edge id="e5" source="n1" target="n7"> <edge id="e5" source="n1" target="n7">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/> <y:Path sx="32.38107215104537" sy="0.0" tx="-22.34892784895453" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" underlinedText="true" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="13.821211921553186" xml:space="preserve" y="16.782337120427428">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -219,11 +219,11 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="658.6" y="536.6799999999998"/> <y:Point x="653.48" y="536.6799999999998"/>
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.69230651855469" x="-131.99129340961497" xml:space="preserve" y="-45.45208675384538">event 1 <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="51.47381591796875" x="-139.88417228306275" xml:space="preserve" y="-47.69392425537126">event 1
event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@@ -232,10 +232,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<edge id="e7" source="n1" target="n4"> <edge id="e7" source="n1" target="n4">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/> <y:Path sx="-35.69940886699487" sy="0.0" tx="-9.780492610837314" ty="1.5"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="46.14430236816406" x="-54.352158195608126" xml:space="preserve" y="-79.29459128622307">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.16780090332031" x="-57.86390746318625" xml:space="preserve" y="-80.0118020361142">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -243,10 +243,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<edge id="e8" source="n6" target="n8"> <edge id="e8" source="n6" target="n8">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/> <y:Path sx="28.960000000000036" sy="0.0" tx="0.0" ty="-21.423522388059723"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="87.40060424804688" x="-100.50030212402339" xml:space="preserve" y="11.337896156311103">enabled Events <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="101.29963684082031" x="-107.4498329306548" xml:space="preserve" y="9.082203674316474">enabled Events
as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,274 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="115.8239999999999" width="93.73760000000004" x="680.0096000000001" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.060546875" x="4.0" xml:space="preserve" y="7.633644259571071">spacepackets<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.5" nodeRatioY="-0.4340927246548981" offsetX="4.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="38.232" width="93.73760000000004" x="680.0096000000001" y="253.37632000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="30.056640625" x="31.840479687499965" xml:space="preserve" y="10.131624999999985">cfdp<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="163.40159999999997" width="284.4319999999997" x="783.8771200000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.896484375" x="12.842478099131654" xml:space="preserve" y="10.580257051368562">sat-rs core<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.45484868756282143" nodeRatioY="-0.4352499788780002" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="75.46560000000002" width="82.73087999999973" x="985.5782400000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.91796875" x="5.868388937197551" xml:space="preserve" y="7.831944999999905">HAL<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.42906652344085205" nodeRatioY="-0.39621834319213123" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="57.03359999999975" x="998.4268800000001" y="165.02472000000006"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="41.763671875" x="7.634964062499762" xml:space="preserve" y="1.015625">TCP/IP<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000003" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.63671875" x="17.56564062500013" xml:space="preserve" y="1.015625">PUS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.62890625" x="9.069546875000015" xml:space="preserve" y="1.015625">Events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999995" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="40.708984375" x="19.796867812500068" xml:space="preserve" y="1.015625">Power<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000003" y="163.78943999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="42.947265625" x="9.910367187500128" xml:space="preserve" y="1.015625">Modes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4944" y="164.06207999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="45.232421875" x="8.767789062500015" xml:space="preserve" y="1.7148249999999905">CCSDS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n10">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4943999999999" y="198.76255999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="35.1953125" x="13.786343750000128" xml:space="preserve" y="1.7148249999999905">ECSS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n11">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.20384" y="163.78943999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.62890625" x="20.05434687500008" xml:space="preserve" y="1.015625">Thermal<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n12">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="188.43504"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="89.494140625" x="2.1217296874999647" xml:space="preserve" y="1.015625">Housekeeping<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n13">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="63.765625" x="22.802665047002165" xml:space="preserve" y="16.015624999999986">sat-rs MIB<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n14">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="73.22265625" x="10.257471875000078" xml:space="preserve" y="1.015625">Parameters<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n15">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000025" x="969.0342399999994" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.25390625" x="25.52440687500018" xml:space="preserve" y="1.015625">Pool<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n16">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.392578125" x="28.172510937499965" xml:space="preserve" y="1.015625">TMTC<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n17">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000002" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="32.01953125" x="15.374234375000242" xml:space="preserve" y="1.015625">FDIR<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n18">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="184.90752000000006"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.505859375" x="18.932547859502165" xml:space="preserve" y="16.015625">sat-rs Book<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n19">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999997" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.22265625" x="5.040031875000068" xml:space="preserve" y="1.015625">Subsystem<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n20">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.044921875" x="22.84633906250008" xml:space="preserve" y="1.015625">Actions<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n21">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="188.0"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="43.404296875" x="9.681851562500015" xml:space="preserve" y="1.015625">Health<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n22">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="241.60832000000005"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="93.701171875" x="7.834891609502165" xml:space="preserve" y="16.015625">sat-rs Example<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

@@ -0,0 +1,607 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD>
1 0 obj
<<
/Title ()
/Author ()
/Subject ()
/Keywords ()
/Creator (yExport 1.5)
/Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5)
/CreationDate (D:20240129115539+01'00')
/ModDate (D:20240129115539+01'00')
/Trapped /False
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 3 0 R
/ViewerPreferences 4 0 R
/OpenAction [5 0 R /Fit]
>>
endobj
4 0 obj
<<
/FitWindow true
/CenterWindow false
>>
endobj
5 0 obj
<<
/Parent 3 0 R
/Type /Page
/Contents 6 0 R
>>
endobj
6 0 obj
<<
/Length 7 0 R
/Filter [/ASCII85Decode /FlateDecode]
>>
stream
GauF[bH</%%P0O,YLl(tZJLY97g9BH@!gh:YlW`<fOajJs+$:F5t\]Qi,fnlDGR9B.l3M[OVCAa<;r/S
s1ZqITDp7Q@,C;HT<I_@J,N`(]c[1N=0M5I_*<tQb0OG`:]1.`YJ:(g^]';W49+\tp[`n6l[R)bs5j:V
pn[ob^]2Kls82iohuAuAh;A8"+.jVS:]D/Zl]X6!A,i@L-gaHos1]u[s/G>al/DYSN;a2iOfS98U1f!=
RtII8p?rM\oeCm\q;md^YJ<A_CRW1frY*?Ds8)&2s0">ss1^_p*lS)&qPUOsiH_9m#Q9^5NV,E+^N*Jl
8q&0LBDj^"iuc=<4Ub5JJpdP2a7K?-l[S`2fFOVocO[[<D<G96I-"CX%@b,Jp"*mN[0J)=[_BaV^LdPI
"/ntfZG4-R17UCFrYfSPRMdMLT\:DLc`ZqVQ.*<n258NM17s!oB[=t2CB8X+G5dp>kkRK:B!:],iKhj%
Ldo7**tfH;*ap-_!Id4NBNTbrOjEl.&l@_sqc=lT"]Vq8=5^YUM)BjU:Dh@KmRI3uh!K<?rLBRdJ0#%&
R\J!AF.p!Es.><Hr6"FmUPRcD!cLt]=oYs._FCke,E?oZ>"^*_Uf%Fi9f-[Ehd'ni7%huWD>pDG-p4V-
5Bo*ri)T;blTk<pib6cSl2OHO54h@sd/WKhoPaW6DK<\e-M]@]=.Be*AKM7_4+#WCNVHf"_ak^\*PZ1i
.\*J%S\L"c5p&SVXHJ2pgN%:[-fEKBQef^rl-Q9]A/i\IH&4@`jL)?\EQD!5dC[cC@XM3S#2+6U;1\6!
UjSJ8F1<3$i)V:J'rVQ3HQ^a`P\@Ie-R*RmA@^>WUs%83ao.''CA[>!?bRV!1nKt,Zh)Dln%_(*e!X`4
\bq$GHf-T-rtF":`ukh-p)@S?E(7GK^ej4\GKq@$1A7C?d[6Y@^F?H9-!m^;oDO'YjE+7.T_[qWEC2U<
+'Z*h-7XSXc)3]T/=t=[e'q*>GA4Ma<u7[UHUO:qrjp&FB[j&EDA1?h__SXmrDug0Z%o(3qGl/!HbMIl
bLD89nGD/,,FOdic3:CF8i8$5&JS+IH?2n-E#!VZg8#+gY;U(][Q)3pc'7Peh=do<o;h/_H6a1=(EVX6
]fDborO^Ni?GF(m:$N=uOS@@LP`J;9dp)@>kRcVdQ1V,OTR(_/f-ig]V)F"@p^NaOj8#'ESS/Yt78:+[
[qEkMeZ_7Wdd$G:EZrYDi+Y!.2\U?2`cmWRCr*H=cqn(6/#8ldII,P"FA^'IfVc.1KRtsI^%_qDcSk(^
Brf*ENtS6X*p''_I0_r/=1mei"3M)=T%<;d#=H^ac5/\7FPLQ-/5rD/CN__S380K=qt7L)`XEmP2#;`g
P"s5!3N\]3Ldg&p-L.+d`-7mY$k:M)I%`CiXO`s*a73b'S<;W>BurAf3)t"HKZl!m@.0+.8d#+KGh+f+
'pRCnV%Zgt^.**s$`1Qar6j@Qjb?B0NMP%'VtEp`_t>Qo-u1YT4L,B"_IslmHNQmi/^p=deDCp?Fir4@
`Ukc2UG[h>?+>Vhn'g30M#J2o>$)Rpl`k>BXB00n;[pL)_9It?O//tK\rs09S19EQHjlR-Z,gEdS]9?C
X(3@iEb!(,4fE?1N/o'F0?\e+9`()sI8D"Oo*:Q(EhAC;dG((O9o4L'2KW_]_m6Mi6EBJASIUgK$iFZb
[MEV3>g@PkF3o*r0NinUUBO88IhsOKUP[PjGrP-MFBHORhgYfpTX09&'E*(M:?2-<6gl<-Yc"n+%anQ>
[-2p@f]\$2"6g9LQFc!?7l-7`q9gMYUD>.kR;$lbqV]R=&#8)nmqkmCNe;r?_T`LAGdEj=M=4LB,O$d!
XA@2)cf"%=DoXk%bM.m)eO!<,%PC=J-!6s/_.&/CS%lf0-(2"Ar<6JUn70,`%!;7%%NZV)rH@@KVqLAl
C[I_%CfsPgY>DYSja$BXU&EuL7Cua*&$<c;&!,mj/kPgtL3n/1L:$51dr52goih/20C.'thVH+#j5!J;
.t<=ZOg=g?`AIsMWo<>Js&\\+a1D+]*VAifrR%Rn_@*\_)s?;HU]"pPV[d[&UJCh5E.bi3Dh\^;hlWT_
^G.[:I*ic,0Z1?-rlu<M/Zt+Kh-n3FTHtO3BNSLI#)>$V\).$M*P+tu^roO#U"`(Ff$T+"[9)?@-2nP=
kNUNY-/1]TUEqXHLi1kOP;98k@Dn*`iBXE`U("/<>Ep9UIh,^f*8NL+=8u^tP\R("GZTp]S!o%3gF%/_
.c13DojDKYduY*oS>g6#XE@h*Q!qpA2<sr*+U=)ST6"6_O(sq%9C=4W4Zr;\KB;25B,,,5GZTH$b;gX1
U#(HH(#P:#'u.=.Sa$f``;AjUjJ,SQ7S:kMl/6`Y;i+PM;\7g9HbQH?'L@k*'c=ZDokK2Q@n8+-f3@Q^
NRdggj(2AYmVV%,&q2T>)n!34XDSR4]3u/7IJU#d#mJ;%iQK0/n8mU$^5i)oEkO'`GFFlVrH1iqUJ^@H
<kKkB`-7"&HMjckW#jde"iY9IfF5Ho#9U-YekDQ`2Y;VWXVQ4a26g6TXr]E1_iu+f^2m!:b2+CT9O$(A
IWFoB_1f6Mn*31d2MV4eP^2b3VcCGS=0`(=LsOb\Q0U0<X4jNLl!6;in*4:aPV5'`gXJt+gq>5YmS3Kn
5-]H9:N*U`RT4*N*?cdgNc[ii/R0MK@h6/Ho:aKh'og_\%&u2O6h@H%,d33a<S;bdhGoF*HG0s(/'%:b
m*0r<6d\6pY&&QD9oi@Y7J*aShEu4nGNEpdn*4<7Q3Wnu@<:'gs#m<%D'A?eh.2Wqr6d^YqL`h04R"]S
C!Bo[klhn5'lV4>^&5$Y<tE`*DVior&p;CiB,(NEG?'jIloJdpj,i>==d.D5e7BjO[-eq"K$AUDSfDN+
[cW3#OfalZbpld:8RJ5g8+4S\<><.B(cpW$/TEA;GNIJjX-UkeoWB72r<qZGQ@60=GL_lFFVnC+eAK0]
<73oSZ9U5kM\8O^r1KCbIrL_"D'MqpOPZ6If?0-<?29u^Y5Go74oJIH?.BU6YG]-Q$i=V3F[\\_kht<_
;i*Qi.7ipMl0I2kA,!-pI!^0@p*JD/U,T2[H<2\D*%$m+XbABPd*W8tGImFf18]-]lClnTf2s,bAZm$^
LW@rh]5eVX*jP=T04e9;l$V*rX1I'j1[jVn;*I+uP']m`GT4o=>NIT-X>h5*P+5/:Y+1+9Y$V$#Np#-o
r;<<pN8kch-N4<f1'/KHQHLj3_mWigKnlk<Zo"EKH*7Z/BIt4]T._2OeNNXCpAR3"k_$6^s6sFmK9+F7
o*7JYiIg:_%snUY)LV,6If.)i3@j1e#Hc_gmu:LCXj)(6bX1Na<,qs-_.SPIcoV/7Q]:$@G7U0urh2K'
r57AY-7XN.p(+8Aa$6pP*riGp!p;?/N:j9L7[JQ$_["`H5O;PgkT<!e*rjTLqhL3r+534)5O>\"grAdp
2_Yd#l-VKi@@%/b1=>3-WjQpHUOUqB*^\6CpkR-<oFG=,".AAD0gtaHHhS,>IFl+"l^31S,N_Q,nDJcn
_qoc:g[r(:i:[\.oj.8bhWr3Hlk-:nIqY',raD.)8H7$sgASojs!]2`Bp1^Tqd>cEOaYjINN:J#k</d$
LFN31:\]<l'JiBI?2LqgZV:p3B)3FohU\Hpk=EnC[(O$n^cscT2E9"rgM^rLL(_%?ft8ntolTcYU;oS.
YV2&"Rc\dHnaY<')ZSm`nqiodrqn,JrI7#n"#Dd@<[]1;YVIur(`j8J46Ai7-6@6POJ7!,4@XNfd4C=`
ott>OHJ*nFGuOjIc+V*ebi>X\E@W9OY\ZU0Z%dWgA7Pg@7`L/,DR;?Z#1:";1@/'W+^PP)B_4M%H_77M
F48bkrA%no@-h-LbuT/;TI^V#GP\rDCkd+.TaV>YWb"!SBKBE#S!?35A8pO0'Rus6*jHs"8R^5]*M]%3
jl)tm<o:%(U_:!mB[Me(qmFGgUX6VYUf,.47Dj.kHp>F;\SY%YQ$Sb^NR*?_7S\HeDr*kXPn3FkW1\W$
XkdP<&"p%3fhN?cCB[[9bbNK'l+=jn#J5Hh72`jc:qAD;^u(Zm<q*/LhH;!,]4YH?hV;U8PU#<j0d@q,
6D&a@:Ic9?L'MuUD7%HP1Tbg3UI^Ck)rZ;*bt@e27L&'0m8/kfE?-iaDq?HtqN\78O_YCO[qE_DCDREp
K5VX0>"^IHn^9)JaLM*Xq-hDulNlcD2Oa-W7b_%":,^fRp?2Hs2O_T@+3V/f"N#X1S8DMiLd+MM0]K%c
Lh\=m!]r?th&\Fk`$QcJjqeRRqN";U.6]ggl`e[p]7]Y,$Ih4ZrD:u4AmG4qTJGcb3i]p/+l(^W]a;];
mrjc!lW;2ZhBR'=pD8Wq_e@Q"O`j4`7>GdGL,%[UN2XB;f\'WZf4,SVLLJT"\CX)dl:LDj(n//@d5nPr
br8AV1U?S)37cbY/$oJ/Ke`#,m]f+IJ5-ci$9a#rXWA2K(=mAehTQa9MXMlp*uQ4a_hCs4UY=AU?%J^_
'#!%6?1pRWETiL3Dbj>)gkcUHiASIHS'bPENTCbp7ppfgN8uP0gMQ(kr%.`^L7X+T:hOl-%A:P)HEl+Z
@AECG(<9h]3f%#m4H\Xs_ru(8[s2!A)sI)V*qE#t2APs6NouTToRDYhHh%"A5Q)Vq`X<FKT]\+c/H/P?
+q8N]$<3SDGVC??/@_E6kdqj`J'8.-SCi=F2ILWofUkF)2Z8JBjSo-+``C@q@Z$Z$Rhnp[[D7=[s.D[r
duMR+!sUM]Yh.9`N"q"GNf96;`_EfMo1&5G"I(<Eg7d_HKe6l7<m"N*aCWS8@16+RAoaQY$_>I#I$]fb
0CP%!8Gru8dqDMB(B9XeLZCN^U/GG27DZo5qE82MYsHWf&c=i&_,`";S;lo.liFLe.pJW9B[8G>?Xi2.
R;F.d&ZgFb[je"q'9%9dG>mZ^NudC%4`]TtGKfiD^R8LPq0.70nmCM4jZVV7k5FsQcUXa2NDshR%e3h%
8\Z_^W^f!r\YITG+j&cZ%u.40\3G**-['4uk<ErMGjI6Hbr%L*e(m-d_^cImE)*Fo:iTXW^<toU3mEYA
<Wk[YH`NseH+Rt&P4Kaul;9#N`=*)PUAR_XRt'%$d=5/Y>4tKbXT!s5CGanJO]"8#*F*eJQ)IiUK%)ib
q;MgcM[+_RX;L%U#KOBKc8Q8;T9&q]OjL>?%+X8rcAqPr@N<+`o-PZ3f6>uoKAL86815l=/;\,NN;Ig)
(B%;`WSSjlZ1lj]gde-jl)f2O^1H*!UgIC\).,$7s3Fa@>ST8]IO6<f>NsEN&)jb(p&$3:LM&qSC7^jD
/J9$?b5CG=Q'(rP2M`OP<=^OEB7:?=@&61#H'kBHC5f%N%&A=)SFN[i3bm2?l`fJm=XGAcXTQlr`cpA)
(m=(Vr)fW6I>83^N`PQ3FeVIRFDO+@%"VF+#=Yum!8<AU@:c_CQ\b!k0&!Y1Ue>>OTFW0Y]@_W/HJ:Oa
6^889k7pKM3F)^4MH5W\>/6Bp*mBq*7r1.@hd[I!f_iOF6qQ^ZH*Kc5GJ.1*\tih65UCl'4'aD;g0n[>
c(31@1c@_D0`_^cj(QCBYXLaWrtA`t?aYO54rZZ6<ce+%pbB:RLId4YA?fA`d6F1&7d,2BhHKc@k&b>(
")"C-L\XF9DY:^)S\CVg%Db[6mab&HkAT?@crq<r.HKE\XH;]=-4`52pq^'&jRP<:(eZWQBhnc$K&M0!
VB=;_N:J)\`Z-cB@E1Bn`*Z*i(X+L%N3Uo)R8]D#qnE5QXNDV;V!qngXYe26+sol<<9t)M=".]OG^\lo
dsW``cPIZ<)CJ8a%HET8faj9;;MR,VV86JIgWYbRCi.ru>VMZ]+ed*ml,eED*$P][Ln/bB*/8&i]Z.Xg
_hea_No#ZD*k\t4XS7s6DiAgsgE'O/brV^\ODB]*iUskJA9rW+p18:coK+bgoI%BB#F=8$hCp%Q(A=#'
*^okU.N7ECr^[>PBUAF#NoAA;Yk=cVdF\p4C8_\dc3g=k7QocH7M9Q=g5T"XZ]_;nd&NZ%agkKc&lC?%
L:$\#(GR6s3J4(e\gS$cF(=S2?T<+%[k__Zho[a?:=s/f@HHeLV7"UKVHqMMo-X@5f'2uVH&s8[Iu!(3
N\b3\0S]e#<edp*8IRbOQ,>F4c\)(hgIn25S2dk4WKPf+5)jt9HSI9-WF&ot6RO9<R&rAok1$`q)X>J1
_E;Jm372V_c6YndHjlQRYoI=W,]IquY>pO;0r.&(TT'W2Ipsru#Fd>j.$>N0T3Q$>X.?g%gh`)uEoFHH
F^GLuShr9mU[RCOn^!1@JapB,j6FR;$J.LH.l/sd4!If#m2BbF6Yk]fb\@^Kq"SZPNF+&T5&nrQqC/C&
#5s$8k(C6X14"qIs"d#2Go+7\#@gQik</d$p&+F#a-2a0<.6R3g?,=Bg/jN>'!_H<\sFJ(k!H,o_3cKN
p1A;Tcg"Jj=rVPJnZ.D/'/q(WV>:\2Bd_):UjZnpa3h1E(I$U>27KEto)-El@t4Ll%n=_O:J-tU,h=d=
;/@AWYWZ\ob!n0Kgp8@WQU[I+@FAnU]61"fP4Xs@__$W+K:!7E%]#W(1A1"%5Kp'=0"N^pfR8S]f:s!o
EkQ3tW$s\AqP,Cp[DP[k%Q;R7_TJQY/_a(NTlna`TeB9/9*Tg$ZDcOB99ePpN`h7"gm:"QFKiHXdt0uQ
Vc;kQ0V[[C6s6<@I>OkO3Zg[qik[/;h[d`l=i*XplumpT&iKQuZk@,6ZXjfPRl6=A"ekE3%\EUd/E'6(
jK`++KJHNg93=qrXROfud]`?#,#)/)Ek`i]L:LU(KZ0M!-8AJtq\$n7],tB(Y^?a>h8>R-4RFe/6F.9<
K]k(+krki%<jpa*[,<L%o2orhXJSZTPb-^VLS,Q6M435KkkR^GY-<;1ROHMS,q=>En0L2pP.j?3kn!V'
Kf3N;&fOL>&g])@o-Hd@q@`,T\\jafX:3b"B9da)?77`qX#7N\9?N"9Rp&,A99dC/47jh(k.:<%hf_A\
h5Sth4)5U%9pB5`-!VtIQ`0JLkY&S\$g`VFdsB[Bs*aBu%Gg*a,A:f4cV##rBd7A_()DgBS>KO=lBDP(
Yj8BWRsW(iodi9pgK%UMO!/;F0!7/VV]j`ZWuV<FQ-[T?Yl<q4%R\FSgQQ[iJ$ibse9b_2]V<//2E$T8
><1,lTSq9m,6aHVOEG*"ZmaRH\CBcdFKY)*GL*Rb)tV+;[Y-<qcm]Kg'N-_DFPQPDp$h_f/\O#:Q']J@
MBt5<M/Uo^ZAq+':'Wh&ZmOKM1ER>'St;\erQ.!8k$!/ak.#/DF98I`cKeV'>IqIYdC\;<HNqNK%Ujpt
i9JIqg#QhQoO%MjNZW_q+Htj+'j"Tk/0X&lVXs.AQ>MQOm^A^rk%>=X363Q/GXM&oVK(UOmi$oUh+gV.
(3<t;!p;H7pMf,N30(Dp:\-N-6?7Jj8drILXc#BiL@sIQqC@NLF+:N)S&<!fkV1hJ"-qL`Vr1;,(WAJr
Z&>S40t5@%TG+GRnVjIAW%hap-lP)VM@g,4=IXWCA(gA?*\&e/=d&jYaCRjUGM-^$Wn#gNc,H0rP<%N3
m+?fqH6!mq.*aTI^\'XclYlVj,QC5TLj\)40&;;72gn!^hWZ,nB4+l6ME3sHkfpHgNJemc,.m1:V;@RA
9$b<D11_mETK4(GZSfB$CK94+4bKo=+SQ:,/!Z\lO1[!<9TQ8+H3V7F$!=uSs,"W,nFo/PHNoe2Wu=>5
rCfWA)>*2X3G#'<?;Jn?2ie@]U-j.PC>>qlh+2i_FR=N@a?5+3]d<pFTY$MaBCM?SGQoC>^*b[o0"bT>
oGKZYJSE`)SarF?]mu^!L`(3NZpYUZlU[CdrR/1,QQ^j&7Ang"P/kmc*?5XpVaWVQ51/4,q6i]scSItZ
cS:a2+5faApu*9A8[5\FhiZeUoK-4LF%;;9P*31qXth0DI9l05;*rfTQD&j\Z*89GTVc;=TQ`U:4SqE;
em+:-dM<kj,L0/(T_X`h>3-2"VUhMiI*]O?RYa"?LOY^HEq/%nO;a+SG>ljBCq&=6"D6Qh"!(f,ec#u)
bm/"eNAT43'8p1#^+f6#n<rrtX,T<(Z\%nTj_3oOjn6:5\frW/abeJn/B[HbZ^gk;/PuEQ*3HOIe%%"*
s780rJpS[4ic"PoD1fnl-U%Kds5BR-hE!l_Gd_("jW>i3,mA1FHKA\l)?0p,q$1Qum2pT%s,H2+0p7'j
htt?<55P/5om?_E00XILr$=/FqSE,s1hu&(5*U?OlZmp;Bfe.sn9g;]U#gNY@=O7+@3kDuB[D3f(Od.H
fCS+aHe7_m7le>2A472=4X+53L!1TfiqN\*mh<_pr^RISN3`pD'@rXCV0'aBJX<T*s,$^Ms3.10jtjGk
`r>09a$0gl^r==i#JS6Irsb"Oom<>+RLMnI_\8FX-<uq_qVYuu8%U3*6gTjcm&=NbqjH^%!OMO/Lnp2M
*[ZO%L70?6ergM=Wtd,BQT>`U!6pK@+2AZkd/6[W.Q_D#rs@K>](haG-K3jQN)[j$6FTCEGgO>)/'T*0
n^adB>Mu%^3^X``RS]8E6,YC-^6:HF@"a(`H6Y^6^;R;er]A9J#A.aSLYaSr@]2=#^!g]?j9$Ht1b'76
^[g'/d;`;CPX33-TgNa--<U&4aDHq*U3)@X:$rtd.sdGa<U0NW5o5R$>I3CD@j<"WIofV/d<2?0Z?>%%
rCr:o/gKA#+48`2!V4INd':MSC^$_?^_Fk:^%#p(Zts_sT/TBeT<4cO/_'bH95aQL1JcE<Y@9FGHGnf?
hVZQTkkFitj%ZnFf$PG1FnBg2&A;EZ"W1EuIQ>q(%=k&",fqm\b>F3PYQ^<jbm&7>^Y=&-07K2JVS#DM
-0"l'k#[L<qhBYh$Ym>HpsOJriJjLd*Nm+*;GeT^-'N9*r.3W$4MEd/-g-EZXkAE':O<!R?XIWP5K0P;
_3]=Gie=XT3hP+dW5GNVipZ#.Z[!%Qg:<<_Vh$[__W"M-.6c:+K2;0@iXYWPI&d/MTAJTA%CaM<BrEtB
LAJLV<;dH%>2\T+;mY;`Soj3Cn>XbPTk!Skh@uUI]>:EA<Um&7,X,4XK24AAi7""KI&ZXbidYLLb`+;Q
WCSH4rKhmKiLdnkfUr2^rqt[7gLV?O.h/IG^CMCJ`7dE>B#SE'"^BQp9:>B6e[u6hY8S>7-\0$0'M0iX
0!&*\Gp<R%Wb6mFWYTNI_EiRYnr.Yf.S>:\O"c1LDt2V?H[NPKCjE&jHTo3j#_BC)g`/i!kRA=2M[aCX
ZZn"oJ-t1%nm.n*TlB1DZJOs'OjeLZALN"ME(<>[K$+r."hI//r"GHHp.pYA'Dm%SL#(Lr08^I8<]Lj+
`kKZNZ%$GcGG(q+EGO;k_2)CN!XtOUJ<$>6Y_U\s,OXMu-In"@Uj*QiFF=rE=7N=1)9(GWU"DjJp@6YN
5(;M5<?Uf%IGr"K(%0?np1ek;)QDQ&gGg)_HH#CdOVZ7JUD:'(GdDmI\)Fsq<M(H.aO0'?g\:Vck8QK?
`g;Y,44Zu3gInbL$`:tT-Xl1aN/JAD,XPsVT)hs#%:e9,T`uXo4mG$(6=eCtS(V!bB,aA;%5gatiZ:]:
+6&ORWY#*F+E!jL*U89:<jbdH2#j`?[kji'mn0l@>B^OUX,KEQcEkGDRdM$o3-]5kG?bJt*V[QGUPqt8
^C4l"[EpBEdS?2i.!Ig)WCRo/I00pBCM-Lji]1,b`h9tFd!Hb(W<dMbNiIn^7Vf=Z!_T7]"r^W$SE.[b
$5`]!m?^ja8fQ3M'f"#Is5;^"m_g.qap]!O`De+J928'.l`ontFj;aR>5\tgYD<KS.h3'm?)GA>Dn7@K
Z=<jCnDs+nR'#Kf'55H^3A__Q2H6VLp$BKI"NBNiPQTOL-3!%scfG"0JNMtlELfF,4QrMFfLg#3=]NIT
C8Zq+<F;gU<EOD.6I]n5;1$?tPdJ<t=.hF/rU_C+f`I1g<WsBTrL_il7;oU)>`BJ2d9\*M]t&$,BCiAq
@q>#qhYQ>P:CtSka[J9lZ=&KL78noXK)*4]r84%f=HN&8[0N<CA;'a@i$Fm)KDu%?m($jG4R`S.VKB$)
5B5bi+fk$,XPF0NmjIi.rX2kU7W"H\r/IdAdr<61nVnb#F;ukL>?R]6T_ct,49>oR(Gfgt?hjU`=pR76
AHseqP<)1$C$(;@I_2k!h9reSj4&T&PNr;%V/X92l=FVNQmN&R+p6LZ+R>_7^Y/*iVj/6;Y:q&rnQFtk
;1"YN#V#[h@8<+cW6"fGXRl74RMbi]'5)Ru1W(aOBEQ@W6]!taN?2jnHXk(LE5^Njj#q<L8]/DcK#mU0
h_9A['=@uU86CqK+aPD5[D"UinNa`9Nb+fTl/Ue#;haO(hR#BaHCq0X$c6[G*8;C0'rbf!V.#L+1[s5g
1pg8bAG/;/P%hbhr.Fq\b7^Q!2#&*8bHg:iBrl51k5#caV"3&tQW<5RJDu:]kak'goGtB?\+52h8^*hq
<iLBr4)4:e1[)9(9Wo5]GI.9`[!2OaF(+)7It<hFG,_Ldq;dN4M=/6K.o4&cYEG;t`%trCF(/tAiM\))
i$8Z"omo["c8_ru3st]kCS>9N[o5%n-IYH*=@J4Mb)M-grW1&aYke&!%]uMh5B08rhQp=$c`lX7.Xsgo
(Ks\qTO^8[;0faCXq4Xf&mSU;$30L6jXba/b%RFaZpu&!=P]PMePRSGAh_\O\E=Yj>[a.lVLEjaqd:LS
!u_-S-1Nh#5Q:t+>%nJd/7\,cqHs=q:F\SOPFB^qJ']NWb[kP2dJ)l1nC91a3Ia5)'))nCgY+;E0!o0p
HaV=(A="#Pbsq]b%NX]1BfFU_N.7R._'_Dd_rq'8PIkAoq1)-oFV]A2Pq-@SG^_bRE%p/OR31FCah4<@
r`!bQ.\8qd2KD8jkSZau?<W+1L8)(q2.:r`e(X$]rN267baWC5g'jIu([@UP,)g?+l<;"Z%4pNcdgkmf
(UG"f%LO,SgUS-r08nPcBP`ql%;d5VY_GkFZpEQ#)/<i#XnV0.ZZBuO8]^ibX-&8*(D:#,40jR.Jqt/&
oU`FJ\0a#*`jq*i?+t?bB[^*cI<<l*[*"PdKAU5[L/)]8(MEEVbnI&G2ap)BmI]E&(01P?;Joq<Cq0H&
oVWsZ&$`^@s7]^C<0_.a/lUc3X^>4+(J-f'UXCbkY+(Ne)]4uSU1j/B19BA5Got&``0S)&(E'QZgcW#b
okR`\.O1B?p;s5U3cq@Fc7A72dFjU^BYtS2-u@tfFJ5eDX2Sh]q5(&*K"iN*jP&.'4pBX`;s&+6I;[T-
25-`-bgku/"u?B<f8J<M6Wr0*3FoPmqXMueJ*.((JQjJD83CURPPMYIIf_93J,U,Yo7*)aJQf]e)[*Vp
3hD17!jWI$NTo11)]Gg7n//Ba2JWu!rkPtsLPR%8)euIGnX*+'(283Ua7P?MOf9,R7H(iH^\h.<ZJ)r]
*.djNkkek;7ZG*$33Wfnn\8Re6WQ@a4rd)-clkcG#T60[F7&Lh9YQ>sH@]OY_H<!"c&%?.r4>m))#r+m
9?qlp]B(h<89@<lr,Zge/DJ5BM1*&:@V,uuq!?*ROlor/Ok2A9DbGI5`2YrRjfmdV*/BL\B7^S$hLmZE
i.8:#X>^a\nF+ZF'/0?c'?c[8kS]"!19&#Vhon-\=6lW1Y;^;'q#50MmhG38>HSPLG4L@:hnVq/ni*!3
VXIi5^oms-d%d8l7GmPm`KM]V%ea7=XWX:]\%1C5QPI&1!NE@q4m7iT(c=qac,kUaqX-d@Nj!FmNBN_L
].]'1R%8B-VI/$6ZruhYI+NCLbS'!?)R;5N4UMlC,)jqES$#!Nh2;d<eY!P$'GS4DqgC5nln>2[-<,#e
g_a&'@-3@NB^U%*)Z%>4rHngQ>j^q=rVGiC6@<^cE!"(Q(mk**mrge`qSLj/[*N*.Z*$i9guilDe\h1f
AgOaS4Hme)PE\'pkrZF2PbJog3[&Ee+6c_,aZ?QMb$4AthF+GrZB_PRIZZRd125AGAo_GN"7oEJ;lj]j
5O3M`re`?db,X.E5g!"ScCp?r]PlYerpL\>\QYn(`F+FY`hpD;U![:53+G]q@?6(`<\6#r<rC4YoQbl:
QL68a$,!r6F")@g0%N%qNNo>/>*A[+F(HUQ2[p!"VE9PY*)h.QO)efpHG#Z\)7\8h9.U1]N%)h_\gTb(
Xb8m!UCUi;HTK.Ce[dFUIrh2&?^06fmh$'X*.^,k7sjo91,>[fV$5"@YHouYq!FT.3.dqGYc3)6YHJfG
ReQ_'-fq`/?c.@.Lqb^0qpO!;KO8kM%ZOb!T@`ASY$2:dn_6:ud5l[S&%l$g*IT+"5d&YDV<k)IS.p6-
Ho=c\\6iK"%hFOuq:5=2jN0bjrbC.o0dA42JA7i6[!VfO@?*_s<ET^Po-TJ<KtN*;q+pA's4r^PAhN:*
o8@`DljQ)J3NYJ,.uN;MrOU`dQh_:jO'Cg3YWm*TDJeO8n2VK[?G_?:;Wi=6s.9)eM7l.R6Mq\8LtKO^
mMKKTL+LMt(7kt08>hl5jVkT"U.8.N#@.m&11^BHUSaEPA(G>_2[:SIqQ<o*-qkeT_t-;J20DZTIu@bs
6!KEVlE(=d`1&/]I=7I(;025S0In%rmjCs(=1C^.G]BPFHoL8^?PSjU,>ms-n+TQH9RUJjQf8jse=#F:
rVi\'4f4d;\YVCi1N=eth`rmt@I@h+RtE^9qJK,,^P_EOkRma$kjMtL@qFjl.rYFZ2fUD;HrmetkuT[e
:9$gE]YBIe)Lk[1eTZWUqGgBa4V@p:qL5I`5h#W^<<Fr!$DftC.2t-.ECc)(s+uM6'?$=7^m4)=mJI3q
K9pHVRZj$"E9<O5]%Gcrq/</Wn!H2$k".mXS^-I8V9t.l3PDj(';AVc(V=7C[]p>Mc4G_)(2oMjGDl"m
r)Y]KXjeRm>5c]PZU(?pX,iK&I[gnJWY\XOM>NqgP+gb('miF#8+?C`BbR)!SV)9Lf^I!>G2=!a)4PcI
LVVU5oARS!H^/BR<bYLH=c\k`g'*V)9'c,;4`]&tNf!'kek.@7:.5*)dAhZ`]W][/hBQ_Jp"(G%4rUhD
Us_W)K7DLP`*[WoZ:?SEnuO9Y1h?!ph`'EIU$1R$$Z#ZM@!?0qh-q^hFO0I3)m)`T]@`g8OMA%R%8pms
GUmP[9V-[6b9]l0A)^!p&n54$Z\R`bCBRKJIK($ap$2M*qOH=]6fhCj)Llml9=F$`ZoDWI/=mn\b%cl*
L+TH:T2o!NOg_@$<8ccM(>=rI*//[;k`J[tB:@-^k4B/O')%^YV0f%*L+BA=afnWl*%@.Ai\Fba?J/Os
m*=rFc)$V&d#d$B=Jqgt8%+?Mih+jZA,\7C,J`BU`dOn`9_/l3#%m3]ZMr<(XO&_@Y%P2cObl-ep"([-
c7b_q!%0,^YpdM&+lTN`5B1rh31T<61=>Ohs6)Rg;%EWU$2>]jE7^0F&^8+UleS[tb".$DLrrP!h8dGs
RrQH`$P:Vi)c$ig/'m%si]@ff+5Md%$fu]@C;QW]RHl!O;KLg>*K`qC(!Ge<n=6Api/&7o[Q5kHg&"H8
AO#S>W=<PFF3haCAIO%XZpn+*T'n;ho4YKKr!Yf$@\eWccR,l>nLF3(+ha^&]'nl@C6IDIk('870l9$`
fLHa[R;98)/LKR`HWR:+lJF1X+4f(@4c;=kL;I8^W"j?N1fbe";4IAt\?9oLA,VH0:?R0KWu1*YcDoGt
KkaA':FEo9ZPQT?-c$ssHA,D=62Ia'B2oC<PVraWg)AC'Sa9/&QJa>`X@8*IZ7Yn*s3@_CZhLoL'B@H9
EDO!Hs%WCcNE(gIiso%c*+>W86J6S#-"WXRZ`#=[G?&qNB"DePg-M#$YB>ScAFeU)ToB9d:@"4#<Q,=!
4k9:6LV>j\j2Wf1Eq%I0IsYhJ`kHO9)#8+,q(j#j"CBgZpZFcG5Q>qOAi0'.Wj2Ic\F?l@<6_oZGLB==
M5J93[u42G)u"=gN]Q^mpW9a)\@Xf68'IGuF1SU3&DfE?Frs:ZZm;sFE(Go`>kd+H:>X*^,TU<n^>f^p
M2T"*9h8lDGNu=Gs4,41ZI@nW7=UZ]Q(2,EADZ][GS\YEH4TE4rDuf%.BGg$'WMN::;;'YGj/PIbdtQ1
nIB9aSXikPm<8baPr6LJ:5Lc_m.&S^`6J8t7i^hud@(nOR^e%)ODH>r&5TB21N=IE-JiN#EC(pIcs+$N
Iei6/@;=u;q:mX<F@rKZ7?8!aMuG'$$h#&,ej:;Xa6Z(s78'Z0]*$e><_i,%gZEGRAj-12ou&lIZ#b<7
0"]-n9\aTI\$V?IEBFn?SDsuq[WdaE'nF3llb\+O\i;lLY5QZ9RC@9pj"p-O(<8N-)/r90pG^4ara"]3
s&F'OH6f*=(ZO*<N)h%"DG1fdSE$/4jERT\o_UJPIODl\UooX99;Y-EN\A'0<ZKG-\\mr2h/SH.M>"26
IRi-.]qKT!3[lT?pcPJkSE"[7D^FYNQQ."6.-N6VNqYKSnbuhWGlLca]_=rZ]q%qf(N*QZeKZ="pl]>k
43&/g3Z#gu]ufdtr[,[(q?A?Yrqg*R,+.eHB&;CX(t6[@.=8VFcZ/7Q0kOhiDf+?eSr+cLqG??hm9H!o
3k5!<CIe'p\[-[Q*Oi`ZI1b;e,i0VenDXm%=f3J>Au0@[47&?I,OYaoN6'c5/`<puH(UC`#%e;.U<",4
@*uQ"]QnqS^M5)e]K'J"$4CblffY``pHaH/,_:6a+jW'_dc@a&I+mRd'1p"#%3hLrdJnT64;7Rc'b2T&
mmF9>=3_cK=lj>f?;B$)SVoOi2agRg^]+WLjj)V..d^']0ng?U-Z?LFXQ28;-?ADUjZ2knO*oM\msf#@
H,\#:XaK?,Fkq!2Fh5A&1c21]1fI?CiiQjI1S7Z@9t-QjY&<cGilp?c*L6nsSSo]V*VO;jr`1S(El:&j
khAm5<1UZBZo!gh-,7&U;2aA0n4Hn+/kM_q\#0LAJ]F7EeuW*EFXDLH*)5$!LA%HB`jOf`hpZ<$]tuRF
VHAP-)&8l)2,NFOD!J1G)g%*hiVakKjYudCpZbcin'8&oaU$(fT"LPFpWlQ5c^m8D-9m:[@eahkm]BN3
,p1XT\W1sMV8$12DA65$e5lRLa;p<_@0Ftbf7L3rnX_,bX3-qUhH<dg@ubLea10N"2G/h\[U[jdMiu9<
''eV<<o-(d'I+;]`_a=)F-F1$"KPcD270+<*q`:O%;IKk]-OjLSg@\;)BVGmib<%*EU9_ulQ1&S<JB+^
^8e[Zl1AenYG5+:iZp,,%Fp#f<J@u?d9:5VE`HgWqg;BuYHl0J@\2P7<Q1*(.L.W`L97!jICJ9Q<T04$
^8e\Mb>kCD$?N9u4rRd.Ras!l.SX*8X%F>l$XZn/SA\Hn-JOQ]8de:QX,:Di.W)[a[RpE\Y>rPiI'+se
`H0jK#b.AbV=AF-De7H[XX^I"3q-.Gn]hKD.P89W20kD^bh&lSfN4?(?,N.:]2-aQKu_]gWks[bI9!Nn
7]\NEn#K(0%\R!pa+m*M<3K<`7$DFe;=.#Y.ZSmV3B6V5`OBY`BrphGX%I0q6eSL!EAXgJXjSQj<J=N<
SlLK2FS`#sFQ'7J['/erb!XW8@o]e&Tef^a8%tu_k,QjmmoO-#9&%stic;t;`FE"T2d3mW[>WmVD6tY#
Wd&X8<u%Vl<u'=(>ZG,,je#":/4o5hRLi!="M)NRPiNLuo7%Q6@9.Zj&YIsbo.?`/pugT'(olOj]GRuC
3ktCohE4pTc[XkXFA$OY4;7=CX(1kt'754SbFP2J8;Qm95'SL5Jd:NKgZljpq1c1kbCXqLp[dHEJnfqG
\:KBNS)m1"8$:;J3QIIEIDLVE_7;LMK8#YE9\p:f1rG<;6Ml#ia9^B<IsdGG*WL##Dd0uFl#!Ik\a\b!
H6J<NmaK=@k!I7MFNDGi>9oJSTq+sdk/(4$l'o$I5BU5@2_@\UAk[@fc@8ka+aDRgTd%lG"3^,>)]!Lk
mfD0?Lgt;fZm)S[#X/^9*W>`E?ZAOCQXrq[j;UN"eKP?H4.a)RQ>`Wd0:q&qh=#+>YGP&&Q9BP=d=%)8
8G$etQ"Qjj$X\O)lFXR(^qQj2#W):Z*-s!OZ"]t:k&s):cgQJkZfqqRfbWUfZPKf1:j?Q1Tdol@ZBdCQ
EH`Jh08$/-:hbr7+/=-;FB]uP4@Pb&1u-",ICfeS%WIJ?V*+C&VH`884)K#bD>JJD,[^*][0mIP6$@M9
AYD";534u:F:lPOLoEIXrfEH^)3ls7;^/O=V9/1[XZV/K(Z*NYKENF@16AMo"tmPeOS'ekUMZYr5*c(P
GiG4%A67PCjZHf\pc'8>qYS,!S#9gJ3Kna3\T/e>E2&rjh'4))3o8JVVFtHEMWetZZiF`S$ZukE</);g
>HXS5!MKF`Uj,P+;nW.5EN7;,WYI%(d&$N,m7!AY>69(f1hY*5+PO&[EH9@OU3,-qc$!9s"3%U#1C#Q8
QEnd#&q:Sb#'jg:'OD!_)eoV'U+nEd\?][DfM5\JV;%k!i_mmFM%h<JYB?W"pSB4Z8s:>mRF?#]Mjg:k
d2QjtUIUTjZd^E];Z^7b<(%k4eh)m1.JI")Mpknr5.S%>*DG_`X^@`@=Ju+2*654%>,$28`bDA+lPdh;
kgQYR[VM!sY>DtrGWXt)\&RCKH#MfXXjT2?+Z)"cldrbgW@2-7)TRtUS:fcrA*_N"%R&UBE@T<.]>g9*
Tet%=`C&[!1p1heF<i_=9@WO^3"6`q8)lF?.r(T?\M@(C3-(#=Ql;l6Y&BOUg;?G@=<>CE)MdKi?/mKF
Y,$''CCsEQjNok$Y5-I)Wm)cJ;e73'LdCB#j#D4A;fTR7CE1lmOiFYOgm?$c$7b(LSE8H23^ruXNO*nZ
:9Bc<6Pn?i0=k!?[,dO<AkGU#ZS47snTPA#dh9\6)Fq5k1_02VCRdij1,KJMe48Ou<Z@.?&RD2Pl>R2"
NDhnr`U_c5`s@ZGd-E;lBcL,JP/aIL.WdLBS^_`/338+t3e\qqNlE6U]leB*Q,Z+kph1Y<m$>Y2NBrQq
dP$+50s/8r$9SE;X/<C><'0f4COc<ss2,93`@R_(^tt#d6q4R<4[HuNdC<!=\XU2b=7d1'oD6-rHnqS.
DWYK4bO:)V`LR?[QThZh;?t4EgeN_CiM`iOao+;ki\fMUbu<!S\U`^+.<:(GSlJ6!nCkNRO]?l?.5Qh6
%"%g<X4?d9oBMaQNgf5@3"Cg?mXBQAaI^rRT6M,PKs\QBLY9!5m+,4VW&&eFfE,MF(mB,i$:`ep2pIHQ
KGMsM[*r2@qm6eTAKu$>[5c\Y-$7Dq6brHoRPaA)NqQ`!$R>Cc-Ul,Y%n8])p4`oq#tb@>7"`1Ego9@+
=5>mZCr^D9>]F<#`bkr`[q?6*3RtL-c]K<V\WA7eaEh*Z\RVO>1TTD[/nAAO6K2_HbY3D0/&Rjkg9g0&
ofG_W[>>rcLnP\9f?Wk\Z'U@!_)rcl/9^;"JVMH,S;0VZ[](k@lD$j9b(U7_[1R=,4$W8IWks\!VVue`
O0K#V#7p2Pn">KnMJ]&0qjtKlp!q<WSdPiCP+r6G]c"Gio8t^*!Cs!2Gai^s2]!m]L%.g[e4ojHAMB/'
'NZ>a-H.jVaq7mV%;^E/S_!'&T6ALAn[9+WL"s6]Q+<"7@E&<%,`:7GpIG7W'ePn?)4CpgU=lC*b/;Pf
fP0M2(mQ]&0KVpV+L6`&bh14'29G[;MS,rh;*t3Geh*1do`fU%mFN#&;+$OPnd&*c`[H(>f@lfPo8,8A
6AakiZ*YL"Hb#G0cQY<dO>I][>`N**d&KSg!7AN+1@mCrD]RUH^^^pSXR,k^B=5'DqH(m3p,VV9riKhZ
r*`&t[8d(ob\3uY[r(dee^%KfPa0CY]JA%2fkYsVg-p;3lnch7SIq.T4#(C`Vbh;uiVP^8rj,aI97&>(
9\7cSh2%NAZ-Qeqmrd<gYKMr'H#N"48tKSN]&e"U.Q6(C%'h!G:o68/\1Y+re3u=(53oMJ<mJ8b)K/uL
D$I@>pBt\@%NONN^>&Y,0u!!X''%0l%u`1=lt_"%1QF1R`V;<B'KQEX@o4pubr[JB%[?-6^8t!\N\<'3
UZ`NbnMq'cSA)&p`Z]oVZeXFt,]\/p+d')%,a$i&=!Wo34XlL>\W'LCk&#=,m'9nfaUpUk2Fg>ZYr$)G
2$L+1LG!qq)chf!4G/G&=>T]pGp]0R*isr]b-@h>kUsu6`R%H0l'-<8`8(OUBI)HaBaoMmq7s*3;-B'3
UATikc]!7MGg8m#%Bg\`>)QSp7fQZEHac9l"qnOWL2-(-"_unL^o/%AVA"&QD;<<MoGRcmn+[\_nMjXZ
'dut-LNF*l>DT3pc>fRofBi<NR4-*I3`<0#ft"6>.'4:#86MdfQt/2"qKqkBB+a+"m0g0?K4?1mi([DE
[aLH.4+DB6B.-i2PDcAZ9TRflqg^X^'Qg=U]6.O]W5!<64;DV;`VEj$mbU>ZSR&PoV\Z`BbVH0N:R9@F
WL806.rTNqSCHU\ZhSce$DgLV<ZY3f-$5;d/CCgRf)Rj`nSH<1-pu[)R1c@.:TAkV8(P!=n=28PXP[es
%=\J^eT8ILQE&Iki?Wh[[5=(+EN1F5o.bC)\@aERm.p4\*$<HID_:g3s6i]eL[Vs9D3(kr*54V2+RSst
T/`GB)df5$Q?Qnpm4pS(NdhP1W]`5-s7-DTkILG:i'M'1T)Za;Qcj^Prd/R24cfSkiqU3:rO!EeDf;/C
GXf%iCcgXa?trpGXZ#h2c0/AUfbcWL[W1Jh+d7_[06RuGdC)R?XB!22#^GW/6WWN9h^p:X_XK#pF[i4q
<Oo$IC7X^pco:f>Q*n94F!"D#ZG,JsDJ+%uko#5[)hcqr6<AYjEF;X^@\iPMM[oO4TFuPb'YN]-I8de"
LY]1G#nu(&#2cl<7#Ml>Cqh(d_<-AfhXn7D[C;9:*%4:l$FC,kgZhM.Kq8"sG:tF$rgFtglB*7%a4VnJ
R.Xt\Ok+4ugl9dPRCdD-h9:,?IPip%W#btFr>!msBX@J)H"Q3cJQ@Y,#sAV&hXi,5(\\(GX,RG?gSlZS
B%m+m]QJOhf\X+/?YbjDE;/>Fg2Y4qh6RtV:97p<';M>(S%?"7RWU*0Z_FG.(</>-W"!15'h\BC;*`>3
[P0:FLq"p`9Z[(Rb3P(94<*tU54R#-rj>OlFgq/;gP`CO[2aDLN[q]!?B[J)nhH7g0`K;[2L/ls2^e"2
rAATC%c(-9$5%3>=Nd[]&\p2t7ZH$t@`C>Z+$27O_MjWJ*r.s$pH(&YYQ"Sq[ital],<Q.2Jes40SIo-
Ri0\EFkKn^b2n-5eqK38_`=@?l'uGM`\pW(g2GuXi+?$ML7>^JB<<Hr&hSfWaq&D+aq@RJA;NfREuqC%
7]SB%@.,%9'_)h7+o]C3kb-EL)FtfrDi]*#b"]'sN@mE,.U!GQa*#oKRS1tEnegAWM4,@,A>kSSH,g70
*LsDh)egB86^dn2a&j;;>:4JPi9c(fe6P1P];6_[UWggL#A5!mOU4CLTePIEN:L%Z4%[$hZQ7,hS..ie
e5,pNFQk<%aBJ;gs5ceVm<)TD+(UnOhV2,OiI!&RJO<Q60(JpA8'/Lb\2+mbM6&nZjrRq$)](E5qjMo_
09==q<gha$H@"m(J^&6qH`Kp0Sm9+i3f#EH(K.b#k9\P`(51KUZ$?t.A,A@7AH(eH0tO\dnkI4Rr/!ic
LAksYk."T2hHB-E"\*^Do"=tie@M-T/?e]o$AmPFfnW8C3;S=@<Sg*V[jjWaN;FMEWaG&sk,O%^C?9c'
-kWQ<RWM`VE$Ijr^9$uHX0BR0a#n,m,,P1DZ@Zc,+[AD1*DT$cHo`>op-5(f&]kIg?n=?9643V9HS,pU
@;3;XG5gbNdN6:K-_lKtFseQAk^Pc/D55SVK3$hK@_Y7*Q"tO0U.E^)o,SXBTm9GfJ(\%4VrPbKZRf\C
R:>&Jb;K2Vk!g>V)>e?sH,f^8/roLI^:3\,k05<+,oDGVK8*V3MKW(RUGT^Ar'QL-Cgm/:*P3,774b"e
dN4H#fUAAj[&>m,,V4\n<='n[eh$/oeC2PR2?%31%tRU3`%lY.Obgs@;d9,JQ$2cidGFj%d68=^2cKda
(d0WVKRXJ$&rbf31'c"B@aR!fR+^k3,1nN^lW`he(Y8>C<"o%A:N"#(3;U[_oVe+@+&&ZZPFQ]&fe+W5
/._/HSoKAF89nOUWXM?LCY7>Vo1SnBEgE`?>%MDC7TN0rUqA+^KMnuM^*Mg:\.;"WGO/5Q!oGWlT%?R`
03EO\Pd@U/96j*6<@Gm=T7<Y!YA;rPP\*I3Au4pWnuQXJ8pMK%d\;s>*@Z2+.G'tc8U3dJg`tp+\phta
rm(!gXM6R(2NDVo*M@^SP&eOL6coc<5q_9E+dDO6s$4^70;o=/f83HuH-,K4%fVVR;+`jPTl=/rD0.DN
RJ4=KPUdC7Xc-!?c-1UmOE.,?#V")YkX`k&elS4/=8s089^_^R7Ll:hB`I263JEE::,Z[ZFLQND,A"Be
AMV,DO<FeVI5B!d]Y)ds>?G-"QH'^c,Klnb19t18)WR1Tng#Hs+X9'"ahl-_4>O3UCP@]u`^qF#doSJl
!pb?LA>BY.OjMf2'pOj/>UCjDKfAm"7W`-rA;'0fLh"RuOnVBadFl9rOePq`AV>9MKG;59^1:b[T"1Ot
Nf7qdj2_LBI$YH.&B`(PIjcPI8OGt5NrUrPr!o&*hO].+,Wmj'aE5-T.4CAqWQRgMjCP`>#MS(bg>ar_
o=C*I_EECj87>5V,9qF4J$cO+NK%/p"*^kXQ1lVRi3W"Eq*4Ck>cB0V`Ju?,WK5kgl81c7eZKK]fn0mo
n>-R7HeG"XQZ[/Mgj8=_$9._K%Por4Z^=ISkl:lbF@]LTb4aUc7/$7Yn2d0^^9X,JZ:?Mnm-g&[h>DJ#
[D%^dFGSS(W2.Li.#=hCrd)nWa2-O(&c=h;0p4b+U[Ee!b)0q><#f-d&mJ#I&?V#!4irnq\T]j_X3o4^
XS[7V><Et?doZ$lH6a1=f8((6d1hdUNHO&!8)KJo)6us$Q[0q5U])</`6gtgGUIaB]9^ukL[4e.R_`'&
O)FfWi\9-*o+/elUMsNp5^]ZBWnchtNp!T!\io=cFF2MW@rrufs2;rACe,4?[,ndWdg*Lm+od,`r-W"d
-_Lq3)lmc2jBYR$$k7F6p]IXd)T)3^G`RWA4kmllZ.A'b0]SnY4;Q-5U#D:t`1F%@Q$*;i[MN+Ocd&$B
Xa$Z;`n)21[n>M%lCN[[)!Bo8Del&ULg4jm*tpN^Zeub5]JNXC$GG!0D83LC&Q@_s`dRai0mBG<fG/[V
en5\/c[1,WQ+a`I?bhqeOd^*bnSPTuHLlq7:@@OVh/Y=me#W@^-L&2t]&n=3,?ZI!Z`hi&1QtVp]A1*l
?VRf+e.:9=Ar%&ta^9ACcndk6=0Z^nGKP,s?g85648LJGZ*e[W.m5]%ff[QVml$M[r2MT2H?@W&'M#kI
Cc>mN3Q?6G+iL0rVTdoJq*s)!r_#4qB"aY`='6KY9TD[6HF7A=FZ?.-BZ,`6;PrZJL+'4[k5dE(6et1N
@%;B<q(&%+,tr]CrR]8:IM_0Xd0jQ!RoRuRS]<$PL3e!\)(=mZ8lD-W'm;DJ]BI+=/b@0dXjeC1IQJ!%
:';I&>%<t!d[Na41F^!#kAf&:2*>pQ.5f]mk^RjCijPmFHQ:[=kFUIX+&b#@g9iY0&^aD</EShpa#cch
EkRldFc*mP4&PiSm6mpDY!q"#ULQ9PB[oj.[1heDF;Lq;R]XcqJs$J$f02K.f9t^o:Fped],gR7D#<lU
diJ;`=3bS$r"F<9f(5NdTE!5!l8dmZMJOiXnS?!q3FNH,)aSLW$%n360+=7i?ml%aNZXeaXA+hN7b!9X
,<Tac24R^Z2ZR325+aEPPeW&Z"Ut!*9/8<QcpJ0W*eZ\;$X9jNhk\qjEP"a/3dCh?J]hFC.T"qh$OHQ7
k^NUSg$+ZMD0[sK^Vc4BL)*Tss6Z<cn3Cd.n$#X-5u&5*3c]s1ck*m_5Bbk'"It&/+GDn=--B?iLEk3Y
SlUgjkGs2,>+L2ImoiUL:_D]2S;']8`D'"q1^aN`QWUE4In-tY7k_*L7h;#0m/8"gkCCtAX#939(5aiR
pab[-$^4j*'i9/L)u4[ufD65#Fr5Muh2Q^7rT8$a,r/F[X8V\"kp?hVSF]31ZBT4aZ4:/:8J*d`c:amG
#B#I%%4GM\0Inc[bJqE&AA,N"F1CO"kg+*3QT58q$uJKQCV<n-^<=A'cMNHrUjsESrX+Y$Je9[7K)7tl
DYh`4TlHqX"^5#Y&1@pkNOMbqgnk-&UU=l$FulQ,(HA"4Q9:<W^FR%JC^oo@05R0qYKat1hsBgf]$S?G
9o#7*psLI1TtakQ^;XA,+*=m.`=kHXFM^0f\YQ!QINL[t(MA-S'9Bq%1a#5QUd"H-%c)Mm;(h-e7N/X,
[g][TDAdbeX^.BnFs>@ICQ3#mK9]Ro6n(nJlL^AEitMaF^,Vl\.d"<&"6UWNUI!.2f_[oMV5s#S3B61@
o0sgo6PbR'fd4jb_U=<$elP\m=#G$l=nAk<8*eO%``6IB$FkLHr"@R8NA`#_Ar-E/ae.\,R6(,ZPk3_F
Jhku1#Yn1&Fb<R@qO]C>T19,kA+eETB@eKiBO(DB`X4:?:)-395,M)ZgEXK(j1O?Z>fSu^X7X6aFik(:
=6Ja/s+uk]s2&@h3YL''p)L.L*2OC-nC?Sc9ep8!U7pQ0qt(!cl;pNLSj0`VFs+nsEBkuLh*hf%`RhW=
f#Bn?K/u/P^h<RJ>l)HqI]fq_R.UFSQk/F&QRKd>+7-Gcckj-4E>KpUYD4d4kO"2890*j'=Ik@)7s5cV
bJSt$Uk/:.):n^%[U,,-+[nnG[edULX_tsm,$MUH1p=9j=9QbXf3r?_5WekLrLVc4V'FkWBWUA]0qtP^
*"u?coo',:%5jJcNG84rN"MC$Q_I2FX_;o^KfY>sIF5I7r6MR6&a9ME=5o7aHi?""/\0`30-M=L6qdL`
kiiue^-pf&Z!8Y-FS?Na6s[:aXKI2P=6ho]`Ik%;W=h)W-(^[eZE7b"s,b#]>1WgC#O>+[&p6g%j`m*W
&Zk%SA^ONNSo+$EWnVmk-0c7Y_3\iWm=7US^+LqPMA1Gl0@>sL$YY5ZU5Q7'$,%cpU:KY69;V&\1KE^`
Xlsnuoj)'5=6jtC,987f02]dU]0kEWWRU$`3"<Lr.po$^<<cg)\lArAU:O>O?UFCO0;0H07]UF%ab$SZ
[c"/i8an%kep"K]M(\?F.Qp`+&qTtmNiTleq\-\sju@_pn[ElPn[ElPof%7)N@'%0l4u1'$.238Z\@MU
/mk=_ff![fMT_=t$=?-PARe87Rsp&h-"!CXV#Xjh1?U]18R&_0=t^>l?!GPupYeb9/'+%gQcbZ)oaC/A
2RH6$,&X%nYu.MK;8#mUGNd\[]q+m.5!$>Xn`pRlc/!mYHB85bgQht'#$oOG`92L6s!RO>fEaQTm5)Th
?$Gsd8Q*6WDO\E1rPanI<MPj$mRnef-<o;[2g,Q7\nnk[":6.:8?C(mkjkT&!+q%l%GPW=eD+LD>kBT?
l)NpYeN$RC>4n`Sp,[M!cZm/g`cmL=R>&BC&JqqV#A<kPgG+/:XcR&c,eK>=Sn<$7E2s/Vcg]Ia(.7#'
d>lg;l+:fQZctL)A)'6Lmk<)_a7qLN,^W9k1[;e@kNe'"3;`@kkb?Jt[7h$=G=M2i(t4R76A#E0nLSfP
c"t'E\j;BH)<JZ@[:!W_rX8DnTFFh)K8<S9=%Iar`B!luWinD"2'DgfiHD0oLrcPXjDg@*Z/4j=.t]3J
UCU$iYTAP["qOIi;ipuK$LdfcLM*UoPNUq!Q1-e4<JCDI:[HF-?2YJ/K!8ng:EU5cjF7-e]SW0o]!96B
M=%[*m=t,7Yb)+Lfh^b'R#L"JZSf1\:_M(n"QGt#_Nkt&^AL\Pg%fiuDHc^?c,ef3*cV>>?_["MnaXh'
@QgJXl%h8FB*Vf$Nh]t1:H2L#Rmg!J6r5)!3nL-5cTl,r_O2dCph#*Ulea;b7"RHj8+RelilILLU0M`2
Z,3Td^l&$aGX&HopKold@XEa(lK54fCd;GsPmF_;9WXu#eIC8N;rK<)CU*_MoogOa9m9;P4S^;XUU+JN
,!4o'Eq8,OKB3`PSA1c;ektH9:m'jNrU5:Hfq)Y=,-[V$61;GR?eS%"ZTd=>67.IGT%agVlGLGp-ZO#b
O`EQ#</PqBZ.%FAn;7%YK>#_?S][A+)hrP,D\YL?@sOe[n;IHEko.DF\P69(h1KK_#1\UcdEmd]A@\_:
laFCDgn)r>[YOP*$5*sQ24&"88q*BrpR-)`*Ygmc[ZH&XZ%Yk(+qNF2[#J#E65QK+#%&3%\\KVDM)hAl
ZJ2'rk*M:($c&;KL@(S3SN+2kl^&d<D'pNoToX$?W@)#7!?)aR%`d&oh8qHLKrpi/!(UYWB]_9/->-+Y
`X^I66(%n`aN^m`LAGF;c8^pIVr'6jlK?uZ1tJ^mqu?BGc*LjFDtPo&P@MYd$?<'oHLil;E(S=gj"iZO
UsV-PJ?"CE:p*'R!S2ZLaj2e8%iSGn!__aI`pqXEX2<jfP0(+e>&hG31TV6VKmF:&X9*6'Ti,b[\<-ge
#L8h/$\/-h1Nf0h@tIat'lo[KSd%QQhp(p):6KTt9XjWGC<Y(7Fh3sV`@@D*Wg-=MHkLNi(!b(QIFOX0
q]'DrhAu?Z5Hd_G6C[r9M2UYD1BgHUIe2K-BR-*Yl`T>.O,Z`S#EkCf0PW>Yr0<R<KsG^*4R/7JfCGkX
2I)<<%ba*MhXK<u'r_bmniHLV>$gA9Uuh!fK]TO:b6P'(LVV:%D`X#$\9fL.HslJBI,P`_a3=:SQM7^R
eFFGH5?`Ll9h&<81Po"U*4=_qdINbUcd<).Pa9@8W,ubL*G]9MM9;5Qb<'_rn!I;8hX%E,EQYL+/W]^E
Y2qUkM581opL>0p+*<X2^MqIWL/-GrKoqZ+e`P9M36flW"/0%*E]q.)c4X;%FDIch#A^/U5f7]Nr8nSS
BmE(o:7Gn?nig5@YdF='W^@(U!6P`%o(G/DT"Y(B($uQIH0]I=0)(j7lCfFVeuQ3hc<i^4eGIYa`g`s;
Q4H_*7;kA%jZ8t[/m14YUZr17[86rUP>\>*<GD>J`a;QYWq_Bh(2-)XEspgB1ErAi6Ie-b&`h<?e2;Nu
IRX&N43o$%kiJJS8nii^nhOunrdg!m;)h%p?'W>8O,KZlZHp5L=QSaZrDHq+TM0DJ;M@7'C=#\WPBFhO
/Ucc&k<a$Y3cTj7%oMm*Z'Lm>(m!bD'?Bm$288F`Mt+n%Nq&h:9T2bN>9cZ^6!hl3lQ,UHC,5Y\P#`JC
+)CfMp_)1;NUJ"?WY5leM<!-9?sf2tq+p@X1b'UX?_pU+DtkfAb(T#\d[OfS[7[3#?U#M;e)fRrSj`2$
%"En"GRR/U\G"tr;U[@qXb=-.aW&Q$1t'(9?Ai;=]DpeoOF3(@#M69#F(\b@7eb&0V0&p;PMFp>l**Wa
_fS1Q71^Z7MX'eNh*c[sQ5eH;jsO+ThaKsSAeZ"ckc.g(B-h^`PX0+]'B@"<1H`k=fqM*Q)NsM48O9@:
H5N\).VJ-a/_!K'hO<6r6f#BA*Y'cY/<9+^3EfR^q)PcIqFb"Y9:4BcH-s*#FhR/`_+:<0Cs(!`BZKPY
8\K4c'Go,07:F6R;JE(iVq!>3=Ia/Y6Zd52jr\VCjm%hho;p+o.%ctcf!cPn+r0r+am5"i'fr$&f?@MF
.+bJ&o!QVlf;Kc=CS8(ncTrF`n]#<Y>;;uQ'c^M,[!2OqW`,1c-h@EA]+ufa2<I+WYEo*CkPr=l&[d6=
c`)ok[Ll96Oe7Q(>#TO:nsil\XC:ml.\Z=[&)EltfX725+*_,>!h;1o^I,/YoB[g-nSB5L+iML$&fg;;
WQpsm(dDs;pr%r:I/XWrPlpLNpsEfF2(pQDfX4a+pV1YX/QReakGTdFROEGeY^J)$>$\n1SuRccX(sG\
G4F-YrD,"G]Bd5oB;JW:/Z/C#dUI>eCs('\.DCtX8s])W:0.%D\FEW8D4q`Gh//CrMHrC;QD(rD/Z)`*
]X7kIYQigkrLlj@?ZTP!\lW(ZO_Z\le7)DiP#IjO?WsPZ09d6Y7"k&*%IJRXZ.aG\NESF4[@e;9[.6PA
eNkXZ(39tZ(45Q^:D7ksH`k<rYL$adqmU[ebTj0;->S[h@RfltrRYPYF]#dlc_2-+EKb%@HG?gh/ZpZE
ldqsI>"j-oKGJq-T4a5Gb:00k>BhSVa&C%coOiD%<^]o1;uDP$0Nnt^9gGm^W$NNK>*Ii6[>6S.`/;rB
)N!Wll_`d`8t?4SXCoT@jjrO`r5,-bo_^'H>HMf;fhAPcX&fn>$7(+P[[N2TL99%tm15%3_36I_hhBY@
d5@V&:.3(*U-<b[YSH$$[!`GFL!?]3chqcPVs@&f]JqNH'ij"I2[7P^\\6gE>"ga;Vo>)eMTsUZ&Z<"b
;JqtFKUl:BXj-6/4.Xa81LZ!WeeeW_4G=OlG"FWep,UcQhprW/7#:q3NP6Vf0Xg,kh88bgg97`Loldp+
YrGcW[9E]HUL;+C@ooInVnFS3L8<:)N%d%Ke#)S&J=CO-(Y4Rd,LJps=-_L/95?iVYE4+1dG)1_g,A@i
9'_Ea]2ih?rR@i)#06U+-#2p=\l<^X^0A-(0hA/+2J`#!e+QdrVsS$0gD\RVm"ikA)npVYrOMoS%iH>H
SX=&dB\h]EBH8-3lJt=7[ak1mn[m_@iGl6\Dkuh+Xu[$6fs$Z/D0QJGnJ\!R1q`V>`:[i+^DkpR^?JpG
JuLK5@)hImFM[X4T^[&0,#*tX@Qp:'Qg$hCD:c%',9fa+;tA$FZo)2*F[C-iDm=HAA.feon=CE'N[p\3
\gTJl`8=^dD1bJ#hmDYU0E(HcpMT?&6+)TBqu&Y-/B:<V-ZBmqk<H49s.B%FpZCm7/%Xi[n_f9!S1IM3
_KGuO(s#mK8S)Q:1&EV,3"4G[!t7?YUg<LMDZk=nbYmm.-9*HgX*\aD=)f$_oEZ[trd%i@2*P^NNQXGB
/N4F:eJl:gkNfq20AHR21k:tBQ)mptH\,l\hF,(NTJOG7>1m>)IhU?4ik)KRj&]*@-$k7!kG,eU)&P*F
aV+l=Hf@f2O7s=bGlM?iB"<EB;r!B.@*<L*#3%W9-=X`X4.Y_e)ib#ql5BojNC^Gs!\a'RhddTrs6cbS
`inrZ'CX?"S?TbtQ'i`,^]E<'eB0]dIcYt/o%*<2BS@up?Dl`FoTts.8c6gc%UC!^dp\)fU_8%8'YNZ,
I8_+7&&7p`F,XJ#Ch06][ui&&E$d$0$7G>EO-\ng-q5>lODX6#DId5QG7PGYX.?)feK^Y>nG^FQ".P@#
[:1KCc)tFDml(+SJ$n)q&ip_FpLs2rlOSG'kTKkE.ZHgC2gn8;S63=HgobBO1R+qD0mhS"<HFWs<fL`C
n9]CbkYRdO4K^W^j8*Z_U4-+4WZI"Te_d#DcAnFGrd&YL:&jVmNMftCiGkR??XiQ8h@u%:]8$mp7AVWM
5RLiiI8N+O*%/:+L@e--4e,WbH$hqW30-j_aa'E?N*o8=ON70o\4tjD<-5hXVm8h<`U*'PESD>@>3da\
\`d,m]1(G3^RALmargT,YL+'S(.ZO9*]heT'<jtm_2H%%pr;'V2m2=S5)SBm^qLHVo:O[-s(D.E")1:R
^Q&$L[k2$<rY,2#J!8cuE=f72iH64Sj:hMQfIL42!:V(T2Vh]*E,G4hfUn5^qo&=omNu/^jRd-V(SaJS
rlRW?)ck?B$Sg?kme-P]7uoFnM>.16WEmZjfIUr0iU<2RHnZ&=TBCpSLtVf(I#UsLAMM%g:HEggfP-jQ
_=nIf7^-9uJ)^'IpE%*>kn3BcYeRD/Q/ft)e#7IZN+a^+NZ,d;"H)Z"*aUG@]Tn&[+:o(GQP\sh=mV3*
Ok54;c77\nX;GP(nM0.90Nk*.KtE3L;HCtNPa<8$-@)#>1\YPWVJZ[dGBmEGpL!p#/5GXsdd7P009N!`
jg9'(PBe4D_lS`NLe8="$;SEobtb2*/E^%Fi23(EJtP`pNE>o'"<+"YibWhd]5mT]Hb:bV>;>^2M"FI<
Zn`+N)g<],BVY'G-gicmF@J4Td@Dh;o86AkRs8bhfD4Qrgem$S.5.21qpdE'?][DhSb;R+?R!qQ@MI\1
W%0nZRP!n(NT&ckS.8#@SR\'8o&B,Qo+WP[NJ.RCI?P=M_[=#qp#eR05LdKZ?2Ej^2;Xl;dM^[pVQp='
^g$WFRJ0g4igCeXq*2*jL\KsF+)S7iX)3iqMY4im.Mo2E.*dMamil'A"s2j"5A64DWi@Td`j[@sL+HrR
qgSYnH?5)O.\ld."4i$QGNb>1s4)\c;r/aZ_#$3jpYd/88'K2nQm:-3N&`B3Rio/s[Q*6]R1T"2*<'<H
pP5?nDo9)l?LNNl#_`l@a.&X,.f:n2n"_L2S'W#f,M^7NdJHT/*TMu:IZ4j_[QT^fNEVU0i#&_<HpQ).
iqnYXM4BCcrW_6?6d'Z(h89o2-:<dHL3I<3AmFkt)+49t7?R,>S<qb][BfP&Kl0o:GS<"(ebNXg$GZ:R
>\3mX=,s*rDPtcsYK@m,af=G;ooT\0,&,-:B9CIdr,ZiLK9>@nHmh)h`BJNP(t@T)fmORGVfrP\65kQr
_2B#eqEOR;90'_H2srlWS^.CBp\#W/m@3j!)R_efM;*Wc3U?%ZR:#$rC;cenS,u8bb8T?!=)j2h;NKME
%M`'9(a?HDTfMhXERm$jTOij>nduooat6M$N:YjSmdXY<2<-i_T(!V;4n0HJ5HS1tE4Y12/m>jZ<4GoJ
I/Xf"o7'iPLf&q\c2-<&K@s7]Ac[,+=OD!0Wft;4X"(8G4c>LgMub18$7J!QZZ]sE[)19dMe>5UO>hYf
Q&FQJ-6/-Jice8'6qC=]0$N$$Lj5c@Ze(.<,ki5E=*5>T?lp;*ZrZW/n%129[L-9GdmZ3^cS&:W0f2l:
.,eM"'U>"7E/cnE\:_C5<.Z`_EOqVoY?Z>c%TS(!;n9WSd-C:Bi-FX/:)hH"Up7@0CEcA:V;-<B)K<C(
CFAK8=1;!.EZ,Q,<d=dR^cV/2lp'dSk]a&!M,Q(-Wqd'O\Z1G.d7&P'SYCcP$T,dCVKL@aLdT`Hh&G6i
jPP*Mg/kqTGog%.*;'s:4'D6J-MjcAMX#-R=qV__Oop2cLN??&/^brML5[ONU7tR"2s&"ZEqFaQZM`C)
a[VJiAGljHHSqEshO!*jOZAfWcL4.>CO<HAU3Yf@gTmP^-f/4(0db3hhYK[+Mjbo(%^2-u%kUWP89`F]
;ApfPQ&CMFX/K&8Bg0s2`OR+1e!G<JcRY!30;A_R-U3kL<?;NS6f##*',`ZdegA,TWie-`)H9X)B*_Le
L>4ZXND_kX4Rr#/-_<gseJ[VEhZocW8,_Y($eGbbhFNdkg8*Wdn;9>aF_)?X1XqKZ5W,Q7[qM)?2,!Nd
<NP5):N\b@I+3oR^"/1KAij^U=VWDZ,Hn&p#+8UQB-c"I!uL3BDX2566YYPP)ln]ai2V(^b-c"]NUlef
=__3PesB=[=#J!ec^IA87;L9X=As1MU$Zs]Q`uLDE`g^8e/;9Y%&k/#nFW.tH?uU.:m#:4eRkR]HZc:H
P-+`H,P@T"UD#IbDftHa3e^j&FQja=>$%Q)H;X=oA*F+a4qg76^CNdk$ru.errs^DnT1<%^cB$O,[=<(
CV,f`,-=tI>a31#XD=&*^'E&-oH!lQ55dMTW9940DFkSDZu)-i<ANV'G&`&9p=(*UG/5^7ANta:h(J<h
:N>V<#Q:rDlo4?=l0+]L<A&=*>ZJ<?[M88X_3m6C`Uf^(c_GMXQ?74K/on,7*8/l</e:43*1[MAalaaG
IZfaM&TpU2UFf&(X'JHB?eIjF'sAC\A0F9$PpP`.)Q1*)2mPY0??89+,@C6*g%E2190Ct8@u^jG8b$p:
PoRrt[rdd?@o?UC$6F?#'Q$0Om6]@(2>]Gl&!c1nrPA(UAjj5(B)P?FrbTfNAorjPj@]c607g,QiCd/%
&F_!Gg\C=cC3SLE^JmQL1hUqO/A6q`9rGB[DlDes^O:rBodoI+>$\V8K>Nr\bh=XKX25E_[kD1/j,Yj%
l&j#Q5\7(@Q`+fjad8rD)o@q,`L--@lB%fgKqTY/Zpq0on0Nb)QO(5lmra#73i%*t*NF6=;ft_7.sc&F
I96HteQUet83s*49@WP/DNTpnL!o?]e?I>p[T>Q)7!5O=^-m+E9YQ1++`42"b!rH!p^ZThA\hBUP):`d
p"PfE?0"YO[SQZ2Jc*-?PJ4Da02]>^Uf/M>*FGOac"$.=^Yqh!1uV),\!ZnNFHQN,Mq^\LBj-NM;2[D[
^6!^K@@%G48Gp_Xq\#%rq%AiNf.g!gc8AE_^'.?HFA1;1j\&,paHr".O=tV>(KE)fma8(9RbQuKQHH=F
%<_#B&%QMok8'>r&>;(lH^j"Y.pC2d6^ZU9A[ajW'5^.NR:pU)]?.%R?VROYk*?VE5I/$lh]LnU\OVTO
F3]TjEa,t@FJS\fZXY,\3T*L&,V?Jbg-"mF+O34<GbOB&?f6<FQZs+-Is8i;r)M92;ND#.<W*q(SSNTk
EKMgsYG_&GUY:ncH;]89=,hBaC6u!,'u#\_+ibo=8qYK4aUqj\1Y\4oJJGY>jD,7'#3Q:NA"2n]MiN`O
8K,Z#BiEcqV,l)liihG(g9WB5%&g,ZQ8FKn8D\:YB?ADone/_**P[c4\ds.TGqjqK0W`ccRH?[ll$r&g
HTkD7X,8g3Zas3-31t]5],nd<g5@LB=kc)t)R(s-HZc:@(J(s:KJU":hopfIX1GJTZB4j^0'U&?%tI1&
?_VK4Ip]<!iSp?pdJJ.DUPDn:m_cr:l/BH3J!LrC`A`'id_C.V_>Qo&l/bFlo_lKQ%ZU=SDdO8fGC>BL
Wk1_KSBpUA!S-,>lQ=%j\pJ$QJt\G8I)7hA+oT"h'g9,.*S<$7+XeBI6n`a@Z=hG-(<WiL-I">r28+=#
55LaTUIhWUApj?I6h]%u>^H4S2grHS(>-9FI&,L@mI#IEp9s,,kB=HA5jNIhT5N^7E2b1FDs+PLhn=8S
bN8i'Z:E3Z,9;fRB1MYD(0^<4(E`8m42^G<nG"CaFlT.YDd`&ohfKn]+0s1!FXiAu>O=@t?PF"l+8K"R
pi\g,2i>e:.Yi<1"Z&A*rWB;G\jfNXWIgo?VK3]kHVr1u$94;6E\8F$lT:0rK4?7!a4H+59NdD:nTjk"
k]"VU8&I9<a4H*Jn!>BRLSA_`MW49=Rb*TA>ZmQJ1F>#pV%W\"jO$`s*efAR7B.:eo/g8=Y%YoBqs/r3
H^E<.@IN)<bNnnKE1Y,=p"^h<5O--8O8f=^Z[-S;'qT84lG;qA[olWo^c:)!A>$V\c]0$/hEZ7>UXri,
.s^M>]2`OLE_&YS.RU.ZZ07JA\TuVqX`O$F(H*oUF[""(I(S`uB-n6q'@2#.0$uu+DKMu/p^bg+I!o(/
s-ODji5&>nQJ(s'.-V03#+@Y'a"t+RYu3m=l*k<l<E1%h.'[p+d85\R=6S,m>tftHgo]k>>qE2B+J*t%
N(UAKk0t:"#,F:9bU-NH/'C%\Yo_1.Vj:qbhW($$#FBF6Q_.Zn1TGNd]T&OkS`a7`;0\7=]K(!BY:G5d
1P$L?s7UQb.Fj<"rH(oun*jS2ospMPe""cpS]J2Y2t)7J/RZogC6-r2r2`jB=6e'.U'l.1%8G@WW]/pO
A&g"GK/_,uUSURmofS.2H8m-YI2JJc4/oYfR/uJ88T.TkX,s*U>'QFOTF6cOK>L#=5<#L4f?G<M?fYWW
bpC<W\tm*Fh2!m8.hXD]E3c(b;C80^&cN3,><m@tI;V[8e6-tB>HQne%#kQSn4h/%p@I6q>akGbSU5bK
(S(,'NE*\hZhSb&3&eEp<&M:-5p"p;EA=nqc+18F^,<P]"\630B2Et2U>D,o@`DPkE;a6`KX[P:rWPh-
,nb54X]?FBEYp+9Fs2lE##bD<a"a0VW&6eYS?)>W5B.JeS]ZdiLM'IcWk+ErWUoCQ2C=tqC`%/Me)uH`
\MV5C[t].klBPup:a$hql.TAMfVqB5;,QAF?d<mqFVu2UIUbTXQ$MXNi=7lg"Z_ip\99b.a_SG/Rbc7H
NMoVDr;,hfdDBohGCN\Kg<EDkUU>BD3kSTuUEI:O)IrWob4E40OcZ;U(LI$f+?u7#piel6eUJ<F"/8:J
X6KTUPFOfT[;)L`;fmiaMG!`qTI>,.dhd)b4nd@h>?*Y-8$V`T<6X^(C0Br(&a6%"SRgXXDe7P'ABCiV
rV;?MJe>3G@EOa]jiCK0EFnbjo_AFl2=$C/pIrN9Z=[.TY(?e>9T!*N>b$Hd*Te`/cABhQS9*[.=<i&?
YQEG;N$M38jq/;__/iFD>aWp:r[$T*rS0ndablLHkB?<;:Q+qd.St%gg:W?P(355]ALVWd=b70YS1S*g
fc#3;YMtr0(S%1`^7tTg51/SMHBKfqA&Eg;NK2(G0`cC9&W.gl+en)I'dJDc^5PO*PTa*7:rK5\rc"*5
P\*-O&W$6-U<sR*aEb-,OXM9.F<a?U_N)t'RU;7L]4XNTd\84DDe4<A9+d$FBQW0?6'GOjXZcq80g?A3
%N1.P8aXQ4#G][t`5P\nA@W6r)BD?\g6+Bb)q!l0n`8o/Kjh$U2chh^o^7s$'hL0nU<t"QWG(q3(pD1t
oUXp2DrKPdltH#sS!`_iI+"eb4-#&idA%G;,$<MbBW5[[o-E\Z[At!SDN%&/FK8J7bf`L4"Q./-W\<Gr
)V5a%[+#=8`em6Hq9?:-Se4t4_tGPu4aNl:oT1fBfN9a`@VkYVM+1jqHu;$CHnFq6U^K1$h3cURCRN_H
kq-.?N^fkkNgh+sF4@1\n(,#:h,Pr5N#Cs<dHK-gN(3OmAMj7aIShpO]_$f87a\/]<X_F#iZ,O'X\#$F
QPi]?aYU,J479DuLr)hTT0F=U9Diq:U3D..$g-o5Zj]_-8Zi3GfZ*&sFC;7b[VBJlVtr@%%\1C7hg[AD
oJr5,gMpZEU[<L#Mc$nR0'?N.X8XD@l?#KA*h?&RFF#!s(\:$^>,NUSf;6rAHc8$/GAHu/&WNLC58eO1
YL4a,&"V##1e4J8^6`tg`]$-JoK^i1JMFq39BgQTj>o=b\"CJj!jmt_7ujoKV_c3^k]HK'3ZU6Y6Ds"?
odlZZ3)Jr5N,pH6oK\_cO=n_&mPm6Jq;*`pc3RS=FP\ORf&f]/UXRaTQSrXSFJKU3S[ZJq\^Z^H;CXma
L69:"kttYG>u-VYf6]i^Wtj(IC8&"XI_;0#>Q-8Bh@2:PgTApN([6$P'+CEQaNl_u:=h.qdi"?rFLpVD
F?9!LKhduI6`6tDU!jlHD4N;i8>l&;S9;DQCT5d6P')'nl1Dbt)r--Fr3G3@?2#\BdriiR7Ji]]VmCSE
g9(BfXS\dAW>2M`WA1L'DuE"nY+&eXg'\l]Wg$SU7;OS:fDc:=T2l0Q=[=sol-L^8*+LV8<,>S$07t5.
m&LdoUX.C!EU<pQp.O#PEU`VYc*W\=/kXQ=QScXdN<5M"flbdU93,EAY1rFE,O=K1pt>h97c5#>>OQ\s
\0oe%))o\/+tRWFmpA2l@=$su0iouK9l2/F:D3EEh)ACodeO0E1;)c&dk\Su;9HS7\P<h,L`Kc!VFCfZ
VQ<u0+a7=5m0-Nq:t,^$?9AO!HU-)Fi]em@\oP'pQQ<4%LX%LMk[6qJ1LP"&dZ-LYh59%;bY-Kh^_Tr7
'!5YPE:(Ut1Sa[:>^<h#%Jmi*^/oFpYBF0=5LWUn[4]9<;jK8`o-2)r2J@hUZGPDg7r*R"Dl&^BSB9p/
2j%kd8/u:6Q8lDJd,rMMA.O]#;5ga*C("R:1D@a8(\8>GHSL:3D)e4%]tN.'28Xc/][kt">-caF*O9',
-HNA+3q+(aft"=rlhoFse<)?N)O%a]c;TU:IH%X=U*eqA`RMa@hK!F8r<%P(;"6X`%W-nnBYS'I4pU5@
XjiN0Kj>?PeUmN5\=XA6b?!S4Dl8_.;.&D(;HQqaG#\1JY=!.,^BsAuOn\Z<]L?EPrMfaV3;P#(rpb_H
o^B%ZXSt@$?2!`lSVnOGcVO5;]p-O)a<7K9r@=$Uqc@<JZ@>@3?[:IEC-FQ1_+3hW0&.C(=!H?2>`$i`
jpk%l=qe9D:ZP+NC=iB#a)3lBMUG"s@/mk$X7S19[.-d-d'+jD?#P3-Ii]K.`)IKiXb)CgfZ[m/s,R+6
VQubnA&l*fW[Q+\`r%`>a*][blu*aHht<E!kJ=VG1Ond142TE>=NCS`nY%*"7X`8Q`4et6aL_V$h&(u8
YPJF#ZNoCV-\k5_bV)NgIi\IVp),hMa:=CIGlJkkj=KBq+I^N!_"dZD08tbc[,g9#BV2;nSS)RZGfD1$
TXK)nOR8a)ouHkh$9<[!M#`Ci^f7`P'":TnanG"d\:5OF<CXQElXETk[7FF9e6^sKNo!1g,hhGYQG_^q
acI-q@7#R(m?N&8Dp0+a\l""3`;AkfT!ZP!o%m`oS.?:8i+s:TEq*\H"X*uf`L!uZVjTE,f4(+?#T4o=
h,f&RhcrLlb-K)i<r50+mprl@_V>ho>XIa=='S(:lSOq>ns(K,#=75>E!\ND`@9VMVHRQ.$Hcd5oo324
%:=i9_:H]XJ'W%\T+I?=")\"?K:>Ud1GdV[!Dd;?M#+<"9'72,^DJ5F@r$mu0LJT>N'0KSJRTBHj:MqA
bTeg#5#48)"4T;4oi+.0Y?M>qT>@:M"r#n9*di\;7(*3B9&qH>33R1W@k'<\F#%_,IrI5na->J\s,&!"
YIF&s;D+*Gr/4q?,oZLkKMTosruJ`NogJ=J`=p3H't?dF]Dal%?E1sTiPFZbo7sEBmIn@m0G\O//M._o
j<a%4@WU;&#Fep?Y:jCi\]$5<jqc9Kc7jbK2p\?pIX,WY\(>DCJ(AP9FBQ67Qi(;7ceTNtB>@&KjDQ,L
Esq(&)L$5tka2(/*mY9U/FO..m/=I8$/Y7O]Ho0g2uG^lL=gEZTj)2T*R1>f"6;FG4B)pm=eOu0n759$
rGn#^?dH+ooiC<S[7E>Zm0sN;%nPt7-#9`7M,[."kX^L!XneYCA;cCUL'Y+ErZupVk?8@AX/)O,8V/Vd
Ac;r*5I@S0<Pl#d$8,fcL0Hmq&VRW"0#P:^R&5+tBA4<b>8eQ20G]f*[XmsYnZ%*NH)O1mV9DB8In@)L
V30n]a_93GAs6n9WR>[,cnoZAbBJGTnF7G-4lkM8A)'m)m[T!%Dj)6t*6/6)N%ks+qU?sBn4f$ZaE+t7
?$gMN7tiAQNM##6jcQ2U+4d!K3r#R/U(:>r4hg($ceR8I&m'OZVCKfu5do[lN#(EjDrf!>mF<,V\0,0p
YZB?rbQX(q>CM/7^\:Z14JJ>2XCgb&aIn(AEEK$\5bRWNPC=XW\JcuH1P44P)']C`7[&"+F`n&FQ-:N$
*m4bu$RhuOe[J/A4Y<#Eh_*%T/KDt75n:F`%\J74qVCn'.MC>"[>iB=JKu%?gm^cV!bib!HZ9#JXcc;*
G1hc-!=ebPh4'fb>u\p2InJ/q4&X`T_TJt0J?gD71:M0HP'net-<n4.bI21"2s,lDgh#t#NQ)TL^#`$/
6nN55=WIi^l=0)pjBk\sV7`/XQM/")8M600A"Jb$]"&n>rS532n5+rkZThtEH4RD]di^[UfcR_^9[;S<
))02Zmku"WN&aR-Z)o7;JZn='=_Ba]-r\e(SbY7Hm4\*&1EN&VjR1[s7rRA_Uh6^hUMOJVYKZMcSPmR-
(E03'0]=LPdRVMKP0"$ASB4,K*G^[!MR:ijIea6%jml/&Rh!'#n6qYV_If!Y_bpFe]__hU-SM:cf^*WL
KI?\?YW.S"gKX[kNr!]rhf25nF&i5\n)phKqS!o@qJ>U^Nq[BC2L`GFb2+1a0oC!`kl4^HJTkGP/Et#<
!B4<d,"l0Qjs<'9Z]GJMJP^/<9t(Zoe@(ol8f,nL4PU?ckZ(Z$@X]:I9^b!0.WQc<c[c@?[mAec_E,r/
4?25Co3gr54d8b%H\BW/[SMWU+j1lY>AmIkgRjIK?/@8:l$(G]J%<,O1A=kI+8pj$';bE)!9X9^98W*Q
)(?IL0Q[MJ8[L9EoBL[/SST,[NW+pBZV;LgD7%?\5UQrEDu4M&V)[8M0+2=#Nu;##hXQ2GH/hJA:UX]p
^ODS8TAhZ^8*k"Hp(B?[rh,5?X?i.<nD<-G_\,b\S!e.9`t.l>4_R<Lp^Ttuks!==pra)Q5KU.KP"atA
'BHTXG0XUDBP1ItBg:4jA=Fs[$?.4b@rNY&N`ZN.j3W)Q>?YGpk;V8NYk>hF0oI[3Sd0P>Y40=V<R0Ys
[.A8b`!<O7jK,O&D(+'nCs%Wqq'LI7V.Af]Waso$=YC/p:eKes`:4OBHb*'ph-MXfY;\J(V(d0Q;dU.Z
IDdN2Nq:)P.imuB49\93pd%.>XOPbCb4R4_auRd_*F@FLr0@41jh:&7K@$UqA5!O3#r]puND[f?R5HJS
7cdM'r'3Sq(@"ih_C<fAi'^n/=2FZHhBfI4;O30pPW9Vp8/bf&@,,OI_2[K^aWc!"'WdX+A)i]A-J)@h
GZ/HlNU\dS=^r_b32J]aD2F)_/tAN.rb1[s:5_mdPXO\<SP*u)IMf?'J")6!a.lKNFY]KT%g;d0XP3jq
j(n*Qc_\YdpX@K0Lbst:L!O*6Qch<ke.Gr6(!!9mK?5fMmr!urb[[q5P&=^Ekhe2.nnFA<Sa`'#XT+<U
rdTI%ERV9G,cU]a0G;?fC%H%g^n"i7UAc\"F-F&Q_%4'n[s+Ql>^7*M5Vl#EcHcAiba_?'Y;e\GdAP_B
r,luN<:p>(C,02dHr:&s'9[Wg;C6V(`nL6t2QEGmpV(p7p`lV>U]1j+_)i/&WW"u6r4h+."!bb>3dVY'
rT.5XoP>Tt[,3Q<GeGia!B)F_(UqthK8^OsRCh9[NohCbD3/N\r0@GEC8"qBHKCh7=dAO#%eQuap-^F<
&Wd34,M<`7U.1>ZDo,S*kh!N,1["`6GML/sgu(E4Y2J%U=tAJ@fa'\>6go<pWN[*8Y9;TH3c9oqc)\Lb
CXm@62Qi77\%Y#T!)W3X(c,;7IJ7(n8fm#Znbjua_i1<AdrIA,..L^g?,ioENa;Z<(eE,mI>m%VZMeq8
j*XK-[nf'1b(.S2]iF/`XK,P>$XZiE11Vq`C$iJF\%:E`L:ag3(G&0u(S]s"iZ$j*8Un[oZ>g,h/Z7.K
#(3`d[&WGTIs4CYDoB9l<G2@q;0SFh(N$*]+(%De,AZ+Q^Goi3jW_G25tQ1idfM*Vq5XD'MpZ6LDq'=,
'g.T>64@I5TFA!ZoYatX3t9D3h!nLVQSQX6ZJ48r,k$&EU?<aUV[K=XP4NrMh"HTf,*lU^8ZCu8Q6C3S
o_a5pH-PlT9KqBS7Uq8c`:@9J3?'PDCB#tKnrH](l0<@Z2O]$:]]@M++(jJ$7u*pg+[:0aCiGRk&8k!O
N6Q_3pIsH>>e$uiVoAr.mDHF#1n06kC2"1,`4&tUDZNbQS4f"oB.\![Y,WAq0(eXdk/(+ol<%%bDrI/6
>qsI9n]%$K*+DBI^,Oq6;=@cTQ&XOBP"VS<$a$(kAC;<SGBn-*_Dla//T`\[JFg9aAWb.p929"[c70LV
$R98NnH*BCI+!ugW@ps.e\>AZ(<oMB`Fc55nFSCM896@=&g:jrAm@un<m@OFLq6[*-i:Kb]#>_[\ao>X
,ME]M@R/8sE[&s?H#'C3IK^e/=ir,)j1YRrXa3U8>+>0Ja%3d?P8Vb'Gg@lrg-s^?1U\QNMaf-fPF`.o
;oX3D,\FToQIe7]b0h,HDD.!B+smBSne&Y/bePg<4r>:\j4hNnbZWH;fZD2fU.`qM?T/`:@,Ri#i\&jK
)<&W+I>g>B44iJ]<pXRh0kR]R8T%C_Ln+FeFCtA:*uOt8*_7pBUq0r=A:..%JtY8?-&_Rb[@>g$PMTJ"
G&1*;2P$,(R<_W0?DAQ&_0pI*[0ufMX]rI7*_EJiHb*Gp^#lSA!cGekILgI+#ii!%N0q^JjTRl[?0h@U
GhQaB?KBQ]?[+M&q=K[^I*0XEM)iBK$:aN&oQf/qbaCbEXha&+d+s^"40/.ZRH)3_en"/<4,3L>q-cbV
^<";E1sJ4LCmV^4?*j/<L]!;mp+%b-/A9(km)R]#M0u#0fl'pi$SO8q7f@P$bZ@AR='BmaLcMLj7c]-n
\fu32ItEN'H2Pj@q,4nho!0gbcTAOt=??+UE;F*L]4QK![0thD`n9oYN?^:()FJnlQ4Y>0pmSPLD,(Vd
fi!bha(ICaCZ5epHBu*lfdHAYF,k!@8iEqhH11b_eNX!Q.0nV[_;cb0Ui?f8dT^a8h&&A9kbD9M?c(I0
7-8?"q%C5?6Y*RB2SPP#9=+%k/.M(,)qiAr?7f1EA81ShW'nn47,=S?<]`sNgXD.hY6i[En1X$!Cd`4#
M7T3-?-hVM1`"h=$Vh,_`V*UNpT/5L4=D$!!K!c2<W\\<j\'DXqZ9qEOQH%(P<G*'#[_&9Eqp?[%HD-!
@S6baHDtP,='sQZ>3pI/OAbidS!3YsDO*0,`TOld?U?`"$.Y=pTdRsKn7^I@Yl(=LUP7s/rNb?R]e-a'
.qpl8p2PHda[Np3B&nSlM1b`]VQh"b'!2shFI&dE8ToaK2gpNfCaZ)-g)IbZB9Rb#NEH`D>fh]66tUgD
p_'"B8+Ir]j+ZPe(tk68\EZXjn,=Wbgsb\66smfH:-,7OBf]rNp!0C?fjskqH>[VlZgo^!7q`>-&hj+C
dk')*VdpD27R$@o_)3q$q2QQO$0:jZ%<nE*7K%EM(3U_%(3^e&(-1.HNnn%2D76cDJ8_bb)cUgU[TAB]
fcN/uZ4-<b)qNa_k?Im1>pNIUqIY$'okJc[lohWLrT0lgh7lI@f(!P&UFFO!M4^!<pCWT<b7:j+I3<RK
gAcoB(MMUJY4hTn^\tiXYQ';^f)?`d6`gmQI+9!L^6C87,j`r2r"hQk8NJSV(4)-='3[RJ1jNL)HafN`
m$T./c0csoNZFb]NmI'-lh:),])=ecj7mt<WpUS=l8_Mknp0d"3(d'%Xl762HM`&Ych.!Z@Q1^V/S'A"
i%2=lFNoaslRU;.B#rlY/<[b<hU^`U8M8uj"dZn>3,+m8J$7PRPhp:d?&I>.o&qp:__,Zp[T^a7CHGKO
1Angtbl_0;]p6@a]'@Gq'+TMqft)sRf6fVX%NG!/pR85.,hCdm"shLMUUScTPs2V\iU;oboD!Z.^AU4>
>YF^Xa'ql9[LB,KSrf]a4P;rX-@%MOomp6^Zb!'HNBNi'B%QG,[?=WBeUtc@fpd+Y00XMJh<M\Y*7XdA
g\@j4Ec&PNNit&g\;:0KDh=OQ2Cd\h=D-u0\]dQ!h,;jbh/0?"DL>=tHRZ(6mF[RVZWi@P[!1c3BoB@1
mX3D5j41>]]\_/Q^dY>lrUfQ`DlWe"_gWZ:34lg:p;!Sd.l_qF;Yq!kTD]#hYM"+5O775`Y=X)NJ*hA[
I-R?ng!u\f7i]7te'q.Z$BIt`iO_8!X7$gPK(&F*gNS#@RBMsY>\Xc^1S<Ht]uAha]kSHU1q_buod7,p
m_*(uD6tE]AI3P+r&[(Tkl#Y]n((>@Z`Om7Y!,()/_o/41&(Fh;2qE=^Y7j2F$e]*9;L=OKuH+>Oim8;
5!srD).ED+#7Yaq$d2`*04j>4\LU_:ne[GZi>-\tm.9S'lJ&sDLX$0;$TX_?\9fUB%c.(H-O[n<B`ri*
g,+E)?aJ==fjV>o*^n89\>6'geE?A"J\mmW&"]iOhu*8mDYpuS#OQhQpV+6L$Af.S=i\9V/eC0iH.USN
QY)FW?;cdV*d`m[M:,s"0c`9EkPBjWWH;Dt]&ZM@&+1"^H'/C1Rd@[M_rp@6,,;ecdJN[Fn7J!C/VW).
N<cTUdJo$AiB6ID^hXoW*jG;'(p^fIPK)+r_i?iAB,MGrnafX.Rr->WHM5P2T'>sdVn]%C8"\IOi!Q"J
GK<UTg4S`THI>@F;P*kcTKRJNH:;_+BHbsS\CMYb:@5eI*BmO.l=W;.;ML]7fGTE\::+^0/"ZGKiD)$m
^U-20NFm#@>-3:-oVJJ*I'lVebF..EoD2&sC/;TU8DP:BUshrHk7?aS7]/<q7pu*ce)(b7hFjDV*U$GQ
//b)KpSS8E$KJF7d9D%$-q*Ih2BeKD[ed4(%6IHD\jp\BpA]\DqJSUlIGF;kVO)f3prC]fhpqc(a$9Oo
f73b>^ZUZms6l$@VeZ8'pHS]V?iT6fpTE+4"O=(<bknC"k9$q.mCrSSO8o,qn_\fn*uFoMFo~>
endstream
endobj
7 0 obj
40412
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 537.00 194.00]
/Resources 8 0 R
/Kids [5 0 R]
/Count 1
>>
endobj
9 0 obj
[/PDF /Text /ImageC]
endobj
10 0 obj
<<
/S /Transparency
/CS /DeviceRGB
/I true
/K false
>>
endobj
11 0 obj
<<
/Alpha1
<<
/ca 1.0000
/CA 1.0000
/BM /Normal
/AIS false
>>
>>
endobj
8 0 obj
<<
/ProcSet 9 0 R
/ExtGState 11 0 R
>>
endobj
xref
0 12
0000000000 65535 f
0000000015 00000 n
0000000315 00000 n
0000041155 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000041131 00000 n
0000041609 00000 n
0000041325 00000 n
0000041364 00000 n
0000041466 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
41682
%%EOF

12
satrs-book/README.md Normal file
View File

@@ -0,0 +1,12 @@
sat-rs book
=========
High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
## Building
If you have not done so, install `mdbook` using `cargo install mdbook --locked`.
```sh
mdbook build
```

View File

@@ -13,4 +13,4 @@ event components recommended by this framework do not really need this service.
The following images shows how the flow of events could look like in a system where components The following images shows how the flow of events could look like in a system where components
can generate events, and where other system components might be interested in those events: can generate events, and where other system components might be interested in those events:
![Event flow](images/event_man_arch.png) ![Event flow](images/events/event_man_arch.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -21,3 +21,9 @@ A lot of the architecture and general design considerations are based on the
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/) through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/). and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
# Getting started with the example
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
the practical application of `sat-rs` inside an application, it is recommended to have a look at
the example application.

View File

@@ -73,11 +73,10 @@ features = ["all"]
optional = true optional = true
[dependencies.spacepackets] [dependencies.spacepackets]
version = "0.7.0-beta.2" version = "0.7.0"
default-features = false default-features = false
# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" # git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git"
# rev = "79d26e1a6" # rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7"
# branch = ""
[dependencies.cobs] [dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git" git = "https://github.com/robamu/cobs.rs.git"
@@ -91,6 +90,7 @@ zerocopy = "0.7"
once_cell = "1.13" once_cell = "1.13"
serde_json = "1" serde_json = "1"
rand = "0.8" rand = "0.8"
tempfile = "3"
[dev-dependencies.postcard] [dev-dependencies.postcard]
version = "1" version = "1"
@@ -123,4 +123,4 @@ doc-images = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "doc_cfg"] rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"]

View File

@@ -1,259 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.22-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.936037063598633" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="150.1282958984375" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
<y:Fill color="#FFFF00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.21258544921875" x="58.393707275390625" xml:space="preserve" y="21.27395296096796">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="13.4398193359375" xml:space="preserve" y="14.547905921936035">Event
Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="10.719819335937473" xml:space="preserve" y="14.547905921936035">Event
Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="8.719819335937359" xml:space="preserve" y="14.547905921936035">Event
Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="16.079819335937373" xml:space="preserve" y="14.547905921936035">Event
Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.78865051269531" x="16.605674743652344" xml:space="preserve" y="14.547905921936035">PUS Service 5
Event Reporting
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.08859252929688" x="17.2757037353515" xml:space="preserve" y="14.547905921936035">PUS Service 19
Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/>
<y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.932403564453125" x="13.673798217773424" xml:space="preserve" y="14.547905921936035">Telemetry
Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="143.6875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="181.591796875" x="8.373079774614325" xml:space="preserve" y="7.444138124199753">Subscriptions
1. Event Creator 0 subscribes
for event 0
2. Event Creator 1 subscribes
for event group 2
3. PUS Service 5 handler
subscribes for all events
4. PUS Service 19 handler
subscribes for all events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.4602795077105583" nodeRatioY="-0.45641605313700395" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<edge id="e0" source="n4" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="8.639817810058275" xml:space="preserve" y="29.00100609374465">event 1
(group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e1" source="n2" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="11.93999999999994" tx="-83.5" ty="0.0">
<y:Point x="832.0" y="455.16999999999996"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="25.334655000000453" xml:space="preserve" y="-40.972107505798476">event 0
(group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n3" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="5.6761352539062955" xml:space="preserve" y="27.551854405966765">event 2
(group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e3" source="n5" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-6.275467980295616" sy="0.0" tx="57.5" ty="8.5">
<y:Point x="1149.8845320197042" y="545.1799999999998"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.38468933105469" x="26.667665869801795" xml:space="preserve" y="43.287014528669715">event 3 (group 2)
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="75.3599999999999" distanceToCenter="true" position="left" ratio="0.2967848459873102" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e4" source="n1" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n1" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n1" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="658.6" y="536.6799999999998"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.69230651855469" x="-131.99129340961497" xml:space="preserve" y="-45.45208675384538">event 1
event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n1" target="n4">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="46.14430236816406" x="-54.352158195608126" xml:space="preserve" y="-79.29459128622307">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n6" target="n8">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="87.40060424804688" x="-100.50030212402339" xml:space="preserve" y="11.337896156311103">enabled Events
as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
use alloc::string::{String, ToString};
use core::fmt::Display;
use crc::{Crc, CRC_32_CKSUM};
use spacepackets::cfdp::ChecksumType;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "std")]
pub use stdmod::*;
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
#[derive(Debug, Clone)]
pub enum FilestoreError {
FileDoesNotExist,
FileAlreadyExists,
DirDoesNotExist,
Permission,
IsNotFile,
IsNotDirectory,
ByteConversion(ByteConversionError),
Io {
raw_errno: Option<i32>,
string: String,
},
ChecksumTypeNotImplemented(ChecksumType),
}
impl From<ByteConversionError> for FilestoreError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversion(value)
}
}
impl Display for FilestoreError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FilestoreError::FileDoesNotExist => {
write!(f, "file does not exist")
}
FilestoreError::FileAlreadyExists => {
write!(f, "file already exists")
}
FilestoreError::DirDoesNotExist => {
write!(f, "directory does not exist")
}
FilestoreError::Permission => {
write!(f, "permission error")
}
FilestoreError::IsNotFile => {
write!(f, "is not a file")
}
FilestoreError::IsNotDirectory => {
write!(f, "is not a directory")
}
FilestoreError::ByteConversion(e) => {
write!(f, "filestore error: {e}")
}
FilestoreError::Io { raw_errno, string } => {
write!(
f,
"filestore generic IO error with raw errno {:?}: {}",
raw_errno, string
)
}
FilestoreError::ChecksumTypeNotImplemented(checksum_type) => {
write!(f, "checksum {:?} not implemented", checksum_type)
}
}
}
}
impl Error for FilestoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
FilestoreError::ByteConversion(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for FilestoreError {
fn from(value: std::io::Error) -> Self {
Self::Io {
raw_errno: value.raw_os_error(),
string: value.to_string(),
}
}
}
pub trait VirtualFilestore {
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError>;
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError>;
/// Truncating a file means deleting all its data so the resulting file is empty.
/// This can be more efficient than removing and re-creating a file.
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>;
fn remove_dir(&self, file_path: &str, all: bool) -> Result<(), FilestoreError>;
fn read_data(
&self,
file_path: &str,
offset: u64,
read_len: u64,
buf: &mut [u8],
) -> Result<(), FilestoreError>;
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>;
fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str>;
fn is_file(&self, path: &str) -> bool;
fn is_dir(&self, path: &str) -> bool {
!self.is_file(path)
}
fn exists(&self, path: &str) -> bool;
/// This special function is the CFDP specific abstraction to verify the checksum of a file.
/// This allows to keep OS specific details like reading the whole file in the most efficient
/// manner inside the file system abstraction.
fn checksum_verify(
&self,
file_path: &str,
checksum_type: ChecksumType,
expected_checksum: u32,
verification_buf: &mut [u8],
) -> Result<bool, FilestoreError>;
}
#[cfg(feature = "std")]
pub mod stdmod {
use super::*;
use std::{
fs::{self, File, OpenOptions},
io::{BufReader, Read, Seek, SeekFrom, Write},
path::Path,
};
#[derive(Default)]
pub struct NativeFilestore {}
impl VirtualFilestore for NativeFilestore {
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if self.exists(file_path) {
return Err(FilestoreError::FileAlreadyExists);
}
File::create(file_path)?;
Ok(())
}
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Ok(());
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
}
fs::remove_file(file_path)?;
Ok(())
}
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Ok(());
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
}
OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)?;
Ok(())
}
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError> {
if !self.exists(dir_path) {
return Err(FilestoreError::DirDoesNotExist);
}
if !self.is_dir(dir_path) {
return Err(FilestoreError::IsNotDirectory);
}
if !all {
fs::remove_dir(dir_path)?;
return Ok(());
}
fs::remove_dir_all(dir_path)?;
Ok(())
}
fn read_data(
&self,
file_name: &str,
offset: u64,
read_len: u64,
buf: &mut [u8],
) -> Result<(), FilestoreError> {
if buf.len() < read_len as usize {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: read_len as usize,
}
.into());
}
if !self.exists(file_name) {
return Err(FilestoreError::FileDoesNotExist);
}
if !self.is_file(file_name) {
return Err(FilestoreError::IsNotFile);
}
let mut file = File::open(file_name)?;
file.seek(SeekFrom::Start(offset))?;
file.read_exact(&mut buf[0..read_len as usize])?;
Ok(())
}
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError> {
if !self.exists(file) {
return Err(FilestoreError::FileDoesNotExist);
}
if !self.is_file(file) {
return Err(FilestoreError::IsNotFile);
}
let mut file = OpenOptions::new().write(true).open(file)?;
file.seek(SeekFrom::Start(offset))?;
file.write_all(buf)?;
Ok(())
}
fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str> {
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
fn is_file(&self, path: &str) -> bool {
let path = Path::new(path);
path.is_file()
}
fn is_dir(&self, path: &str) -> bool {
let path = Path::new(path);
path.is_dir()
}
fn exists(&self, path: &str) -> bool {
let path = Path::new(path);
if !path.exists() {
return false;
}
true
}
fn checksum_verify(
&self,
file_path: &str,
checksum_type: ChecksumType,
expected_checksum: u32,
verification_buf: &mut [u8],
) -> Result<bool, FilestoreError> {
match checksum_type {
ChecksumType::Modular => {
todo!();
}
ChecksumType::Crc32 => {
let mut digest = CRC_32.digest();
let file_to_check = File::open(file_path)?;
let mut buf_reader = BufReader::new(file_to_check);
loop {
let bytes_read = buf_reader.read(verification_buf)?;
if bytes_read == 0 {
break;
}
digest.update(&verification_buf[0..bytes_read]);
}
if digest.finalize() == expected_checksum {
return Ok(true);
}
Ok(false)
}
ChecksumType::NullChecksum => Ok(true),
_ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)),
}
}
}
}
#[cfg(test)]
mod tests {
use std::{fs, path::Path, println};
use super::*;
use tempfile::tempdir;
const NATIVE_FS: NativeFilestore = NativeFilestore {};
#[test]
fn test_basic_native_filestore_create() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
let result =
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
assert!(result.is_ok());
let path = Path::new(&file_path);
assert!(path.exists());
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_basic_native_fs_exists() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_basic_native_fs_write() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
println!("{}", file_path.to_str().unwrap());
let write_data = "hello world\n";
NATIVE_FS
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
.expect("writing to file failed");
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
assert_eq!(read_back, write_data);
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_basic_native_fs_read() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
println!("{}", file_path.to_str().unwrap());
let write_data = "hello world\n";
NATIVE_FS
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
.expect("writing to file failed");
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
assert_eq!(read_back, write_data);
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
}

View File

@@ -1,8 +1,13 @@
//! This module contains the implementation of the CFDP high level classes as specified in the
//! CCSDS 727.0-B-5.
use core::{cell::RefCell, fmt::Debug, hash::Hash};
use crc::{Crc, CRC_32_CKSUM}; use crc::{Crc, CRC_32_CKSUM};
use hashbrown::HashMap;
use spacepackets::{ use spacepackets::{
cfdp::{ cfdp::{
pdu::{FileDirectiveType, PduError, PduHeader}, pdu::{FileDirectiveType, PduError, PduHeader},
ChecksumType, PduType, TransmissionMode, ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode,
}, },
util::UnsignedByteField, util::UnsignedByteField,
}; };
@@ -14,6 +19,8 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub mod dest; pub mod dest;
#[cfg(feature = "alloc")]
pub mod filestore;
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub mod source; pub mod source;
pub mod user; pub mod user;
@@ -24,7 +31,27 @@ pub enum EntityType {
Receiving, Receiving,
} }
/// Generic abstraction for a check timer which has different functionality depending on whether pub enum TimerContext {
CheckLimit {
local_id: UnsignedByteField,
remote_id: UnsignedByteField,
entity_type: EntityType,
},
NakActivity {
expiry_time_seconds: f32,
},
PositiveAck {
expiry_time_seconds: f32,
},
}
/// Generic abstraction for a check timer which is used by 3 mechanisms of the CFDP protocol.
///
/// ## 1. Check limit handling
///
/// The first mechanism is the check limit handling for unacknowledged transfers as specified
/// in 4.6.3.2 and 4.6.3.3 of the CFDP standard.
/// For this mechanism, the timer has different functionality depending on whether
/// the using entity is the sending entity or the receiving entity for the unacknowledged /// the using entity is the sending entity or the receiving entity for the unacknowledged
/// transmission mode. /// transmission mode.
/// ///
@@ -35,30 +62,40 @@ pub enum EntityType {
/// For the receiving entity, this timer determines the expiry period for incrementing a check /// For the receiving entity, this timer determines the expiry period for incrementing a check
/// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order /// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. /// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard.
pub trait CheckTimerProvider { ///
/// ## 2. NAK activity limit
///
/// The timer will be used to perform the NAK activity check as specified in 4.6.4.7 of the CFDP
/// standard. The expiration period will be provided by the NAK timer expiration limit of the
/// remote entity configuration.
///
/// ## 3. Positive ACK procedures
///
/// The timer will be used to perform the Positive Acknowledgement Procedures as specified in
/// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer
/// interval of the remote entity configuration.
pub trait CheckTimer: Debug {
fn has_expired(&self) -> bool; fn has_expired(&self) -> bool;
fn reset(&mut self);
} }
/// A generic trait which allows CFDP entities to create check timers which are required to /// A generic trait which allows CFDP entities to create check timers which are required to
/// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2 /// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2
/// and 4.6.3.3. The [CheckTimerProvider] provides more information about the purpose of the /// and 4.6.3.3. The [CheckTimer] documentation provides more information about the purpose of the
/// check timer. /// check timer in the context of CFDP.
/// ///
/// This trait also allows the creation of different check timers depending on /// This trait also allows the creation of different check timers depending on context and purpose
/// the ID of the local entity, the ID of the remote entity for a given transaction, and the /// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or
/// type of entity. /// other factors.
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
pub trait CheckTimerCreator { pub trait CheckTimerCreator {
fn get_check_timer_provider( fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box<dyn CheckTimer>;
local_id: &UnsignedByteField,
remote_id: &UnsignedByteField,
entity_type: EntityType,
) -> Box<dyn CheckTimerProvider>;
} }
/// Simple implementation of the [CheckTimerProvider] trait assuming a standard runtime. /// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime.
/// It also assumes that a second accuracy of the check timer period is sufficient. /// It also assumes that a second accuracy of the check timer period is sufficient.
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[derive(Debug)]
pub struct StdCheckTimer { pub struct StdCheckTimer {
expiry_time_seconds: u64, expiry_time_seconds: u64,
start_time: std::time::Instant, start_time: std::time::Instant,
@@ -75,7 +112,7 @@ impl StdCheckTimer {
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl CheckTimerProvider for StdCheckTimer { impl CheckTimer for StdCheckTimer {
fn has_expired(&self) -> bool { fn has_expired(&self) -> bool {
let elapsed_time = self.start_time.elapsed(); let elapsed_time = self.start_time.elapsed();
if elapsed_time.as_secs() > self.expiry_time_seconds { if elapsed_time.as_secs() > self.expiry_time_seconds {
@@ -83,24 +120,322 @@ impl CheckTimerProvider for StdCheckTimer {
} }
false false
} }
fn reset(&mut self) {
self.start_time = std::time::Instant::now();
}
} }
#[derive(Debug)] /// This structure models the remote entity configuration information as specified in chapter 8.3
/// of the CFDP standard.
/// Some of the fields which were not considered necessary for the Rust implementation
/// were omitted. Some other fields which are not contained inside the standard but are considered
/// necessary for the Rust implementation are included.
///
/// ## Notes on Positive Acknowledgment Procedures
///
/// The `positive_ack_timer_interval_seconds` and `positive_ack_timer_expiration_limit` will
/// be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending
/// entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU).
/// Once the expected ACK response has not been received for that interval, as counter will be
/// incremented and the timer will be reset. Once the counter exceeds the
/// `positive_ack_timer_expiration_limit`, a Positive ACK Limit Reached fault will be declared.
///
/// ## Notes on Deferred Lost Segment Procedures
///
/// This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After
/// issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is
/// reset when missing segments or missing metadata is received. The timer will be deactivated if
/// all missing data is received. If the timer expires, a new NAK sequence will be issued and a
/// counter will be incremented, which can lead to a NAK Limit Reached fault being declared.
///
/// ## Fields
///
/// * `entity_id` - The ID of the remote entity.
/// * `max_packet_len` - This determines of all PDUs generated for that remote entity in addition
/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs.
/// * `max_file_segment_len` The maximum file segment length which determines the maximum size
/// of file data PDUs in addition to the `max_packet_len` attribute. If this field is set
/// to None, the maximum file segment length will be derived from the maximum packet length.
/// If this has some value which is smaller than the segment value derived from
/// `max_packet_len`, this value will be picked.
/// * `closure_requested_by_default` - If the closure requested field is not supplied as part of
/// the Put Request, it will be determined from this field in the remote configuration.
/// * `crc_on_transmission_by_default` - If the CRC option is not supplied as part of the Put
/// Request, it will be determined from this field in the remote configuration.
/// * `default_transmission_mode` - If the transmission mode is not supplied as part of the
/// Put Request, it will be determined from this field in the remote configuration.
/// * `disposition_on_cancellation` - Determines whether an incomplete received file is discard on
/// transaction cancellation. Defaults to False.
/// * `default_crc_type` - Default checksum type used to calculate for all file transmissions to
/// this remote entity.
/// * `check_limit` - This timer determines the expiry period for incrementing a check counter
/// after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to
/// 2, so the check limit timer may expire twice.
/// * `positive_ack_timer_interval_seconds`- See the notes on the Positive Acknowledgment
/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to
/// 10 seconds.
/// * `positive_ack_timer_expiration_limit` - See the notes on the Positive Acknowledgment
/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice.
/// * `immediate_nak_mode` - Specifies whether a NAK sequence should be issued immediately when a
/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True.
/// * `nak_timer_interval_seconds` - See the notes on the Deferred Lost Segment Procedure inside
/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds.
/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside
/// the class documentation. Defaults to 2, so the timer may expire two times.
#[derive(Debug, Copy, Clone)]
pub struct RemoteEntityConfig { pub struct RemoteEntityConfig {
pub entity_id: UnsignedByteField, pub entity_id: UnsignedByteField,
pub max_packet_len: usize,
pub max_file_segment_len: usize, pub max_file_segment_len: usize,
pub closure_requeted_by_default: bool, pub closure_requested_by_default: bool,
pub crc_on_transmission_by_default: bool, pub crc_on_transmission_by_default: bool,
pub default_transmission_mode: TransmissionMode, pub default_transmission_mode: TransmissionMode,
pub default_crc_type: ChecksumType, pub default_crc_type: ChecksumType,
pub positive_ack_timer_interval_seconds: f32,
pub positive_ack_timer_expiration_limit: u32,
pub check_limit: u32, pub check_limit: u32,
pub disposition_on_cancellation: bool,
pub immediate_nak_mode: bool,
pub nak_timer_interval_seconds: f32,
pub nak_timer_expiration_limit: u32,
}
impl RemoteEntityConfig {
pub fn new_with_default_values(
entity_id: UnsignedByteField,
max_file_segment_len: usize,
max_packet_len: usize,
closure_requested_by_default: bool,
crc_on_transmission_by_default: bool,
default_transmission_mode: TransmissionMode,
default_crc_type: ChecksumType,
) -> Self {
Self {
entity_id,
max_file_segment_len,
max_packet_len,
closure_requested_by_default,
crc_on_transmission_by_default,
default_transmission_mode,
default_crc_type,
check_limit: 2,
positive_ack_timer_interval_seconds: 10.0,
positive_ack_timer_expiration_limit: 2,
disposition_on_cancellation: false,
immediate_nak_mode: true,
nak_timer_interval_seconds: 10.0,
nak_timer_expiration_limit: 2,
}
}
} }
pub trait RemoteEntityConfigProvider { pub trait RemoteEntityConfigProvider {
fn get_remote_config(&self, remote_id: &UnsignedByteField) -> Option<&RemoteEntityConfig>; /// Retrieve the remote entity configuration for the given remote ID.
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig>;
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig>;
/// Add a new remote configuration. Return [true] if the configuration was
/// inserted successfully, and [false] if a configuration already exists.
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool;
/// Remote a configuration. Returns [true] if the configuration was removed successfully,
/// and [false] if no configuration exists for the given remote ID.
fn remove_config(&mut self, remote_id: u64) -> bool;
} }
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg(feature = "std")]
#[derive(Default)]
pub struct StdRemoteEntityConfigProvider {
remote_cfg_table: HashMap<u64, RemoteEntityConfig>,
}
#[cfg(feature = "std")]
impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig> {
self.remote_cfg_table.get(&remote_id)
}
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> {
self.remote_cfg_table.get_mut(&remote_id)
}
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool {
self.remote_cfg_table
.insert(cfg.entity_id.value(), *cfg)
.is_some()
}
fn remove_config(&mut self, remote_id: u64) -> bool {
self.remote_cfg_table.remove(&remote_id).is_some()
}
}
/// This trait introduces some callbacks which will be called when a particular CFDP fault
/// handler is called.
///
/// It is passed into the CFDP handlers as part of the [DefaultFaultHandler] and the local entity
/// configuration and provides a way to specify custom user error handlers. This allows to
/// implement some CFDP features like fault handler logging, which would not be possible
/// generically otherwise.
///
/// For each error reported by the [DefaultFaultHandler], the appropriate fault handler callback
/// will be called depending on the [FaultHandlerCode].
pub trait UserFaultHandler {
fn notice_of_suspension_cb(
&mut self,
transaction_id: TransactionId,
cond: ConditionCode,
progress: u64,
);
fn notice_of_cancellation_cb(
&mut self,
transaction_id: TransactionId,
cond: ConditionCode,
progress: u64,
);
fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
}
/// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP
/// standard.
///
/// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler
/// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used
/// to select the error handling inside the CFDP handler itself in addition to dispatching to a
/// user-provided callback function provided by the [UserFaultHandler].
///
/// Some note on the provided default settings:
///
/// - Checksum failures will be ignored by default. This is because for unacknowledged transfers,
/// cancelling the transfer immediately would interfere with the check limit mechanism specified
/// in chapter 4.6.3.3.
/// - Unsupported checksum types will also be ignored by default. Even if the checksum type is
/// not supported the file transfer might still have worked properly.
///
/// For all other faults, the default fault handling operation will be to cancel the transaction.
/// These defaults can be overriden by using the [Self::set_fault_handler] method.
/// Please note that in any case, fault handler overrides can be specified by the sending CFDP
/// entity.
pub struct DefaultFaultHandler {
handler_array: [FaultHandlerCode; 10],
// Could also change the user fault handler trait to have non mutable methods, but that limits
// flexbility on the user side..
user_fault_handler: RefCell<Box<dyn UserFaultHandler + Send>>,
}
impl DefaultFaultHandler {
fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option<usize> {
Some(match conditon_code {
ConditionCode::PositiveAckLimitReached => 0,
ConditionCode::KeepAliveLimitReached => 1,
ConditionCode::InvalidTransmissionMode => 2,
ConditionCode::FilestoreRejection => 3,
ConditionCode::FileChecksumFailure => 4,
ConditionCode::FileSizeError => 5,
ConditionCode::NakLimitReached => 6,
ConditionCode::InactivityDetected => 7,
ConditionCode::CheckLimitReached => 8,
ConditionCode::UnsupportedChecksumType => 9,
_ => return None,
})
}
pub fn set_fault_handler(
&mut self,
condition_code: ConditionCode,
fault_handler: FaultHandlerCode,
) {
let array_idx = Self::condition_code_to_array_index(condition_code);
if array_idx.is_none() {
return;
}
self.handler_array[array_idx.unwrap()] = fault_handler;
}
pub fn new(user_fault_handler: Box<dyn UserFaultHandler + Send>) -> Self {
let mut init_array = [FaultHandlerCode::NoticeOfCancellation; 10];
init_array
[Self::condition_code_to_array_index(ConditionCode::FileChecksumFailure).unwrap()] =
FaultHandlerCode::IgnoreError;
init_array[Self::condition_code_to_array_index(ConditionCode::UnsupportedChecksumType)
.unwrap()] = FaultHandlerCode::IgnoreError;
Self {
handler_array: init_array,
user_fault_handler: RefCell::new(user_fault_handler),
}
}
pub fn get_fault_handler(&self, condition_code: ConditionCode) -> FaultHandlerCode {
let array_idx = Self::condition_code_to_array_index(condition_code);
if array_idx.is_none() {
return FaultHandlerCode::IgnoreError;
}
self.handler_array[array_idx.unwrap()]
}
pub fn report_fault(
&self,
transaction_id: TransactionId,
condition: ConditionCode,
progress: u64,
) -> FaultHandlerCode {
let array_idx = Self::condition_code_to_array_index(condition);
if array_idx.is_none() {
return FaultHandlerCode::IgnoreError;
}
let fh_code = self.handler_array[array_idx.unwrap()];
let mut handler_mut = self.user_fault_handler.borrow_mut();
match fh_code {
FaultHandlerCode::NoticeOfCancellation => {
handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress);
}
FaultHandlerCode::NoticeOfSuspension => {
handler_mut.notice_of_suspension_cb(transaction_id, condition, progress);
}
FaultHandlerCode::IgnoreError => {
handler_mut.ignore_cb(transaction_id, condition, progress);
}
FaultHandlerCode::AbandonTransaction => {
handler_mut.abandoned_cb(transaction_id, condition, progress);
}
}
fh_code
}
}
pub struct IndicationConfig {
pub eof_sent: bool,
pub eof_recv: bool,
pub file_segment_recv: bool,
pub transaction_finished: bool,
pub suspended: bool,
pub resumed: bool,
}
impl Default for IndicationConfig {
fn default() -> Self {
Self {
eof_sent: true,
eof_recv: true,
file_segment_recv: true,
transaction_finished: true,
suspended: true,
resumed: true,
}
}
}
pub struct LocalEntityConfig {
pub id: UnsignedByteField,
pub indication_cfg: IndicationConfig,
pub default_fault_handler: DefaultFaultHandler,
}
/// The CFDP transaction ID of a CFDP transaction consists of the source entity ID and the sequence
/// number of that transfer which is also determined by the CFDP source entity.
#[derive(Debug, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TransactionId { pub struct TransactionId {
source_id: UnsignedByteField, source_id: UnsignedByteField,
@@ -121,23 +456,38 @@ impl TransactionId {
} }
} }
impl Hash for TransactionId {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.source_id.value().hash(state);
self.seq_num.value().hash(state);
}
}
impl PartialEq for TransactionId {
fn eq(&self, other: &Self) -> bool {
self.source_id.value() == other.source_id.value()
&& self.seq_num.value() == other.seq_num.value()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TransactionStep { pub enum TransactionStep {
Idle = 0, Idle = 0,
TransactionStart = 1, TransactionStart = 1,
ReceivingFileDataPdus = 2, ReceivingFileDataPdus = 2,
SendingAckPdu = 3, ReceivingFileDataPdusWithCheckLimitHandling = 3,
TransferCompletion = 4, SendingAckPdu = 4,
SendingFinishedPdu = 5, TransferCompletion = 5,
SendingFinishedPdu = 6,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum State { pub enum State {
Idle = 0, Idle = 0,
BusyClass1Nacked = 2, Busy = 1,
BusyClass2Acked = 3, Suspended = 2,
} }
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM); pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
@@ -248,8 +598,8 @@ mod tests {
pdu::{ pdu::{
eof::EofPdu, eof::EofPdu,
file_data::FileDataPdu, file_data::FileDataPdu,
metadata::{MetadataGenericParams, MetadataPdu}, metadata::{MetadataGenericParams, MetadataPduCreator},
CommonPduConfig, FileDirectiveType, PduHeader, CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket,
}, },
PduType, PduType,
}; };
@@ -272,7 +622,8 @@ mod tests {
let dest_file_name = "hello-dest.txt"; let dest_file_name = "hello-dest.txt";
let src_lv = Lv::new_from_str(src_file_name).unwrap(); let src_lv = Lv::new_from_str(src_file_name).unwrap();
let dest_lv = Lv::new_from_str(dest_file_name).unwrap(); let dest_lv = Lv::new_from_str(dest_file_name).unwrap();
let metadata_pdu = MetadataPdu::new(pdu_header, metadata_params, src_lv, dest_lv, None); let metadata_pdu =
MetadataPduCreator::new_no_opts(pdu_header, metadata_params, src_lv, dest_lv);
metadata_pdu metadata_pdu
.write_to_bytes(&mut buf) .write_to_bytes(&mut buf)
.expect("writing metadata PDU failed"); .expect("writing metadata PDU failed");

View File

@@ -1,10 +1,10 @@
use spacepackets::{ use spacepackets::{
cfdp::{ cfdp::{
pdu::{ pdu::{
file_data::RecordContinuationState, file_data::SegmentMetadata,
finished::{DeliveryCode, FileStatus}, finished::{DeliveryCode, FileStatus},
}, },
tlv::msg_to_user::MsgToUserTlv, tlv::{msg_to_user::MsgToUserTlv, WritableTlv},
ConditionCode, ConditionCode,
}, },
util::UnsignedByteField, util::UnsignedByteField,
@@ -30,13 +30,44 @@ pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> {
pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>], pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>],
} }
#[cfg(feature = "alloc")]
#[derive(Debug)]
pub struct OwnedMetadataRecvdParams {
pub id: TransactionId,
pub source_id: UnsignedByteField,
pub file_size: u64,
pub src_file_name: alloc::string::String,
pub dest_file_name: alloc::string::String,
pub msgs_to_user: alloc::vec::Vec<alloc::vec::Vec<u8>>,
}
#[cfg(feature = "alloc")]
impl From<MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
fn from(value: MetadataReceivedParams) -> Self {
Self::from(&value)
}
}
#[cfg(feature = "alloc")]
impl From<&MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
fn from(value: &MetadataReceivedParams) -> Self {
Self {
id: value.id,
source_id: value.source_id,
file_size: value.file_size,
src_file_name: value.src_file_name.into(),
dest_file_name: value.dest_file_name.into(),
msgs_to_user: value.msgs_to_user.iter().map(|tlv| tlv.to_vec()).collect(),
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct FileSegmentRecvdParams<'seg_meta> { pub struct FileSegmentRecvdParams<'seg_meta> {
pub id: TransactionId, pub id: TransactionId,
pub offset: u64, pub offset: u64,
pub length: usize, pub length: usize,
pub rec_cont_state: Option<RecordContinuationState>, pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>,
pub segment_metadata: Option<&'seg_meta [u8]>,
} }
pub trait CfdpUser { pub trait CfdpUser {

View File

@@ -30,6 +30,12 @@ impl PacketIdLookup for [u16] {
} }
} }
impl PacketIdLookup for &[u16] {
fn validate(&self, packet_id: u16) -> bool {
self.binary_search(&packet_id).is_ok()
}
}
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
impl PacketIdLookup for Vec<PacketId> { impl PacketIdLookup for Vec<PacketId> {
fn validate(&self, packet_id: u16) -> bool { fn validate(&self, packet_id: u16) -> bool {
@@ -49,6 +55,12 @@ impl PacketIdLookup for [PacketId] {
} }
} }
impl PacketIdLookup for &[PacketId] {
fn validate(&self, packet_id: u16) -> bool {
self.binary_search(&PacketId::from(packet_id)).is_ok()
}
}
/// This function parses a given buffer for tightly packed CCSDS space packets. It uses the /// This function parses a given buffer for tightly packed CCSDS space packets. It uses the
/// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then /// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then
/// uses the length field of the packet to extract CCSDS packets. /// uses the length field of the packet to extract CCSDS packets.
@@ -101,7 +113,7 @@ pub fn parse_buffer_for_ccsds_space_packets<E>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use spacepackets::{ use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket}, ecss::{tc::PusTcCreator, WritablePusPacket},
PacketId, SpHeader, PacketId, SpHeader,
}; };

View File

@@ -1,49 +0,0 @@
pub enum FsrcGroupIds {
Tmtc = 0,
}
pub struct FsrcErrorRaw {
pub group_id: u8,
pub unique_id: u8,
pub group_name: &'static str,
pub info: &'static str,
}
pub trait FsrcErrorHandler {
fn error(&mut self, e: FsrcErrorRaw);
fn error_with_one_param(&mut self, e: FsrcErrorRaw, _p1: u32) {
self.error(e);
}
fn error_with_two_params(&mut self, e: FsrcErrorRaw, _p1: u32, _p2: u32) {
self.error(e);
}
}
impl FsrcErrorRaw {
pub const fn new(
group_id: u8,
unique_id: u8,
group_name: &'static str,
info: &'static str,
) -> Self {
FsrcErrorRaw {
group_id,
unique_id,
group_name,
info,
}
}
}
#[derive(Clone, Copy, Default)]
pub struct SimpleStdErrorHandler {}
#[cfg(feature = "use_std")]
impl FsrcErrorHandler for SimpleStdErrorHandler {
fn error(&mut self, e: FsrcErrorRaw) {
println!(
"Received error from group {} with ID ({},{}): {}",
e.group_name, e.group_id, e.unique_id, e.info
);
}
}

View File

@@ -2,23 +2,13 @@
//! //!
//! This module provides components to perform event routing. The most important component for this //! This module provides components to perform event routing. The most important component for this
//! task is the [EventManager]. It receives all events and then routes them to event subscribers //! task is the [EventManager]. It receives all events and then routes them to event subscribers
//! where appropriate. //! where appropriate. One common use case for satellite systems is to offer a light-weight
#![cfg_attr(feature = "doc-images", //! publish-subscribe mechanism and IPC mechanism for software and hardware events which are also
cfg_attr(all(), //! packaged as telemetry (TM) or can trigger a system response.
doc = ::embed_doc_image::embed_image!("event_man_arch", "images/event_man_arch.png"
)))]
#![cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \
to enable."
)]
//! One common use case for satellite systems is to offer a light-weight publish-subscribe mechanism
//! and IPC mechanism for software and hardware events which are also packaged as telemetry (TM) or
//! can trigger a system response.
//! //!
//! The following graph shows how the event flow for such a setup could look like: //! It is recommended to read the
//! //! [sat-rs book chapter](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/events.html)
//! ![Event flow][event_man_arch] //! about events first:
//! //!
//! The event manager has a listener table abstracted by the [ListenerTable], which maps //! The event manager has a listener table abstracted by the [ListenerTable], which maps
//! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId]. //! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId].

View File

@@ -1,4 +1,3 @@
use alloc::boxed::Box;
use alloc::vec; use alloc::vec;
use cobs::encode; use cobs::encode;
use delegate::delegate; use delegate::delegate;
@@ -29,7 +28,6 @@ impl<TmError, TcError: 'static> TcpTcParser<TmError, TcError> for CobsTcParser {
current_write_idx: usize, current_write_idx: usize,
next_write_idx: &mut usize, next_write_idx: &mut usize,
) -> Result<(), TcpTmtcError<TmError, TcError>> { ) -> Result<(), TcpTmtcError<TmError, TcError>> {
// Reader vec full, need to parse for packets.
conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets(
&mut tc_buffer[..current_write_idx], &mut tc_buffer[..current_write_idx],
tc_receiver.upcast_mut(), tc_receiver.upcast_mut(),
@@ -111,11 +109,23 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for CobsTmSender {
/// ///
/// The [TCP integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs) /// The [TCP integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs)
/// test also serves as the example application for this module. /// test also serves as the example application for this module.
pub struct TcpTmtcInCobsServer<TmError, TcError: 'static> { pub struct TcpTmtcInCobsServer<
generic_server: TcpTmtcGenericServer<TmError, TcError, CobsTmSender, CobsTcParser>, TmError,
TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
> {
generic_server:
TcpTmtcGenericServer<TmError, TcError, TmSource, TcReceiver, CobsTmSender, CobsTcParser>,
} }
impl<TmError: 'static, TcError: 'static> TcpTmtcInCobsServer<TmError, TcError> { impl<
TmError: 'static,
TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
> TcpTmtcInCobsServer<TmError, TcError, TmSource, TcReceiver>
{
/// Create a new TCP TMTC server which exchanges TMTC packets encoded with /// Create a new TCP TMTC server which exchanges TMTC packets encoded with
/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).
/// ///
@@ -128,9 +138,9 @@ impl<TmError: 'static, TcError: 'static> TcpTmtcInCobsServer<TmError, TcError> {
/// forwarded to this TC receiver. /// forwarded to this TC receiver.
pub fn new( pub fn new(
cfg: ServerConfig, cfg: ServerConfig,
tm_source: Box<dyn TmPacketSource<Error = TmError>>, tm_source: TmSource,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>, tc_receiver: TcReceiver,
) -> Result<Self, TcpTmtcError<TmError, TcError>> { ) -> Result<Self, std::io::Error> {
Ok(Self { Ok(Self {
generic_server: TcpTmtcGenericServer::new( generic_server: TcpTmtcGenericServer::new(
cfg, cfg,
@@ -177,7 +187,7 @@ mod tests {
ServerConfig, ServerConfig,
}, },
}; };
use alloc::{boxed::Box, sync::Arc}; use alloc::sync::Arc;
use cobs::encode; use cobs::encode;
use super::TcpTmtcInCobsServer; use super::TcpTmtcInCobsServer;
@@ -202,11 +212,11 @@ mod tests {
addr: &SocketAddr, addr: &SocketAddr,
tc_receiver: SyncTcCacher, tc_receiver: SyncTcCacher,
tm_source: SyncTmSource, tm_source: SyncTmSource,
) -> TcpTmtcInCobsServer<(), ()> { ) -> TcpTmtcInCobsServer<(), (), SyncTmSource, SyncTcCacher> {
TcpTmtcInCobsServer::new( TcpTmtcInCobsServer::new(
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024), ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source), tm_source,
Box::new(tc_receiver), tc_receiver,
) )
.expect("TCP server generation failed") .expect("TCP server generation failed")
} }

View File

@@ -1,6 +1,6 @@
//! Generic TCP TMTC servers with different TMTC format flavours. //! Generic TCP TMTC servers with different TMTC format flavours.
use alloc::vec; use alloc::vec;
use alloc::{boxed::Box, vec::Vec}; use alloc::vec::Vec;
use core::time::Duration; use core::time::Duration;
use socket2::{Domain, Socket, Type}; use socket2::{Domain, Socket, Type};
use std::io::Read; use std::io::Read;
@@ -134,20 +134,29 @@ pub trait TcpTmSender<TmError, TcError> {
pub struct TcpTmtcGenericServer< pub struct TcpTmtcGenericServer<
TmError, TmError,
TcError, TcError,
TmHandler: TcpTmSender<TmError, TcError>, TmSource: TmPacketSource<Error = TmError>,
TcHandler: TcpTcParser<TmError, TcError>, TcReceiver: ReceivesTc<Error = TcError>,
TmSender: TcpTmSender<TmError, TcError>,
TcParser: TcpTcParser<TmError, TcError>,
> { > {
base: TcpTmtcServerBase<TmError, TcError>, pub(crate) listener: TcpListener,
tc_handler: TcHandler, pub(crate) inner_loop_delay: Duration,
tm_handler: TmHandler, pub(crate) tm_source: TmSource,
pub(crate) tm_buffer: Vec<u8>,
pub(crate) tc_receiver: TcReceiver,
pub(crate) tc_buffer: Vec<u8>,
tc_handler: TcParser,
tm_handler: TmSender,
} }
impl< impl<
TmError: 'static, TmError: 'static,
TcError: 'static, TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
TmSender: TcpTmSender<TmError, TcError>, TmSender: TcpTmSender<TmError, TcError>,
TcParser: TcpTcParser<TmError, TcError>, TcParser: TcpTcParser<TmError, TcError>,
> TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser> > TcpTmtcGenericServer<TmError, TcError, TmSource, TcReceiver, TmSender, TcParser>
{ {
/// Create a new generic TMTC server instance. /// Create a new generic TMTC server instance.
/// ///
@@ -165,25 +174,38 @@ impl<
cfg: ServerConfig, cfg: ServerConfig,
tc_parser: TcParser, tc_parser: TcParser,
tm_sender: TmSender, tm_sender: TmSender,
tm_source: Box<dyn TmPacketSource<Error = TmError>>, tm_source: TmSource,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>, tc_receiver: TcReceiver,
) -> Result<TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>, std::io::Error> { ) -> Result<Self, std::io::Error> {
// Create a TCP listener bound to two addresses.
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
socket.set_reuse_address(cfg.reuse_addr)?;
#[cfg(unix)]
socket.set_reuse_port(cfg.reuse_port)?;
let addr = (cfg.addr).into();
socket.bind(&addr)?;
socket.listen(128)?;
Ok(Self { Ok(Self {
base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?,
tc_handler: tc_parser, tc_handler: tc_parser,
tm_handler: tm_sender, tm_handler: tm_sender,
listener: socket.into(),
inner_loop_delay: cfg.inner_loop_delay,
tm_source,
tm_buffer: vec![0; cfg.tm_buffer_size],
tc_receiver,
tc_buffer: vec![0; cfg.tc_buffer_size],
}) })
} }
/// Retrieve the internal [TcpListener] class. /// Retrieve the internal [TcpListener] class.
pub fn listener(&mut self) -> &mut TcpListener { pub fn listener(&mut self) -> &mut TcpListener {
self.base.listener() &mut self.listener
} }
/// Can be used to retrieve the local assigned address of the TCP server. This is especially /// Can be used to retrieve the local assigned address of the TCP server. This is especially
/// useful if using the port number 0 for OS auto-assignment. /// useful if using the port number 0 for OS auto-assignment.
pub fn local_addr(&self) -> std::io::Result<SocketAddr> { pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.base.local_addr() self.listener.local_addr()
} }
/// This call is used to handle the next connection to a client. Right now, it performs /// This call is used to handle the next connection to a client. Right now, it performs
@@ -205,20 +227,20 @@ impl<
let mut connection_result = ConnectionResult::default(); let mut connection_result = ConnectionResult::default();
let mut current_write_idx; let mut current_write_idx;
let mut next_write_idx = 0; let mut next_write_idx = 0;
let (mut stream, addr) = self.base.listener.accept()?; let (mut stream, addr) = self.listener.accept()?;
stream.set_nonblocking(true)?; stream.set_nonblocking(true)?;
connection_result.addr = Some(addr); connection_result.addr = Some(addr);
current_write_idx = next_write_idx; current_write_idx = next_write_idx;
loop { loop {
let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]); let read_result = stream.read(&mut self.tc_buffer[current_write_idx..]);
match read_result { match read_result {
Ok(0) => { Ok(0) => {
// Connection closed by client. If any TC was read, parse for complete packets. // Connection closed by client. If any TC was read, parse for complete packets.
// After that, break the outer loop. // After that, break the outer loop.
if current_write_idx > 0 { if current_write_idx > 0 {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.base.tc_buffer, &mut self.tc_buffer,
self.base.tc_receiver.as_mut(), &mut self.tc_receiver,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@@ -229,10 +251,10 @@ impl<
Ok(read_len) => { Ok(read_len) => {
current_write_idx += read_len; current_write_idx += read_len;
// TC buffer is full, we must parse for complete packets now. // TC buffer is full, we must parse for complete packets now.
if current_write_idx == self.base.tc_buffer.capacity() { if current_write_idx == self.tc_buffer.capacity() {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.base.tc_buffer, &mut self.tc_buffer,
self.base.tc_receiver.as_mut(), &mut self.tc_receiver,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@@ -245,8 +267,8 @@ impl<
// both UNIX and Windows. // both UNIX and Windows.
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.base.tc_buffer, &mut self.tc_buffer,
self.base.tc_receiver.as_mut(), &mut self.tc_receiver,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@@ -254,14 +276,14 @@ impl<
current_write_idx = next_write_idx; current_write_idx = next_write_idx;
if !self.tm_handler.handle_tm_sending( if !self.tm_handler.handle_tm_sending(
&mut self.base.tm_buffer, &mut self.tm_buffer,
self.base.tm_source.as_mut(), &mut self.tm_source,
&mut connection_result, &mut connection_result,
&mut stream, &mut stream,
)? { )? {
// No TC read, no TM was sent, but the client has not disconnected. // No TC read, no TM was sent, but the client has not disconnected.
// Perform an inner delay to avoid burning CPU time. // Perform an inner delay to avoid burning CPU time.
thread::sleep(self.base.inner_loop_delay); thread::sleep(self.inner_loop_delay);
} }
} }
_ => { _ => {
@@ -271,8 +293,8 @@ impl<
} }
} }
self.tm_handler.handle_tm_sending( self.tm_handler.handle_tm_sending(
&mut self.base.tm_buffer, &mut self.tm_buffer,
self.base.tm_source.as_mut(), &mut self.tm_source,
&mut connection_result, &mut connection_result,
&mut stream, &mut stream,
)?; )?;
@@ -280,47 +302,6 @@ impl<
} }
} }
pub(crate) struct TcpTmtcServerBase<TmError, TcError> {
pub(crate) listener: TcpListener,
pub(crate) inner_loop_delay: Duration,
pub(crate) tm_source: Box<dyn TmPacketSource<Error = TmError>>,
pub(crate) tm_buffer: Vec<u8>,
pub(crate) tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
pub(crate) tc_buffer: Vec<u8>,
}
impl<TmError, TcError> TcpTmtcServerBase<TmError, TcError> {
pub(crate) fn new(
cfg: ServerConfig,
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
) -> Result<Self, std::io::Error> {
// Create a TCP listener bound to two addresses.
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
socket.set_reuse_address(cfg.reuse_addr)?;
socket.set_reuse_port(cfg.reuse_port)?;
let addr = (cfg.addr).into();
socket.bind(&addr)?;
socket.listen(128)?;
Ok(Self {
listener: socket.into(),
inner_loop_delay: cfg.inner_loop_delay,
tm_source,
tm_buffer: vec![0; cfg.tm_buffer_size],
tc_receiver,
tc_buffer: vec![0; cfg.tc_buffer_size],
})
}
pub(crate) fn listener(&mut self) -> &mut TcpListener {
&mut self.listener
}
pub(crate) fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.listener.local_addr()
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use std::sync::Mutex; use std::sync::Mutex;

View File

@@ -88,16 +88,31 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for SpacepacketsTmSender {
/// [spacepackets::PacketId]s as part of the server configuration for that purpose. /// [spacepackets::PacketId]s as part of the server configuration for that purpose.
/// ///
/// ## Example /// ## Example
///
/// The [TCP server integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs) /// The [TCP server integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs)
/// also serves as the example application for this module. /// also serves as the example application for this module.
pub struct TcpSpacepacketsServer<TmError, TcError: 'static> { pub struct TcpSpacepacketsServer<
generic_server: TmError,
TcpTmtcGenericServer<TmError, TcError, SpacepacketsTmSender, SpacepacketsTcParser>, TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
> {
generic_server: TcpTmtcGenericServer<
TmError,
TcError,
TmSource,
TcReceiver,
SpacepacketsTmSender,
SpacepacketsTcParser,
>,
} }
impl<TmError: 'static, TcError: 'static> TcpSpacepacketsServer<TmError, TcError> { impl<
/// Create a new TCP TMTC server which exchanges CCSDS space packets. TmError: 'static,
TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
> TcpSpacepacketsServer<TmError, TcError, TmSource, TcReceiver>
{
/// ///
/// ## Parameter /// ## Parameter
/// ///
@@ -110,10 +125,10 @@ impl<TmError: 'static, TcError: 'static> TcpSpacepacketsServer<TmError, TcError>
/// parsing. This mechanism is used to have a start marker for finding CCSDS packets. /// parsing. This mechanism is used to have a start marker for finding CCSDS packets.
pub fn new( pub fn new(
cfg: ServerConfig, cfg: ServerConfig,
tm_source: Box<dyn TmPacketSource<Error = TmError>>, tm_source: TmSource,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>, tc_receiver: TcReceiver,
packet_id_lookup: Box<dyn PacketIdLookup + Send>, packet_id_lookup: Box<dyn PacketIdLookup + Send>,
) -> Result<Self, TcpTmtcError<TmError, TcError>> { ) -> Result<Self, std::io::Error> {
Ok(Self { Ok(Self {
generic_server: TcpTmtcGenericServer::new( generic_server: TcpTmtcGenericServer::new(
cfg, cfg,
@@ -158,7 +173,7 @@ mod tests {
use alloc::{boxed::Box, sync::Arc}; use alloc::{boxed::Box, sync::Arc};
use hashbrown::HashSet; use hashbrown::HashSet;
use spacepackets::{ use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket}, ecss::{tc::PusTcCreator, WritablePusPacket},
PacketId, SpHeader, PacketId, SpHeader,
}; };
@@ -179,11 +194,11 @@ mod tests {
tc_receiver: SyncTcCacher, tc_receiver: SyncTcCacher,
tm_source: SyncTmSource, tm_source: SyncTmSource,
packet_id_lookup: HashSet<PacketId>, packet_id_lookup: HashSet<PacketId>,
) -> TcpSpacepacketsServer<(), ()> { ) -> TcpSpacepacketsServer<(), (), SyncTmSource, SyncTcCacher> {
TcpSpacepacketsServer::new( TcpSpacepacketsServer::new(
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024), ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source), tm_source,
Box::new(tc_receiver), tc_receiver,
Box::new(packet_id_lookup), Box::new(packet_id_lookup),
) )
.expect("TCP server generation failed") .expect("TCP server generation failed")

View File

@@ -19,7 +19,7 @@ use std::vec::Vec;
/// ///
/// ``` /// ```
/// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; /// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
/// use spacepackets::ecss::SerializablePusPacket; /// use spacepackets::ecss::WritablePusPacket;
/// use satrs_core::hal::std::udp_server::UdpTcServer; /// use satrs_core::hal::std::udp_server::UdpTcServer;
/// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; /// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
/// use spacepackets::SpHeader; /// use spacepackets::SpHeader;
@@ -144,7 +144,7 @@ mod tests {
use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use crate::tmtc::ReceivesTcCore; use crate::tmtc::ReceivesTcCore;
use spacepackets::ecss::tc::PusTcCreator; use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::SerializablePusPacket; use spacepackets::ecss::WritablePusPacket;
use spacepackets::SpHeader; use spacepackets::SpHeader;
use std::boxed::Box; use std::boxed::Box;
use std::collections::VecDeque; use std::collections::VecDeque;

View File

@@ -20,9 +20,10 @@ extern crate downcast_rs;
#[cfg(any(feature = "std", test))] #[cfg(any(feature = "std", test))]
extern crate std; extern crate std;
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub mod cfdp; pub mod cfdp;
pub mod encoding; pub mod encoding;
pub mod error;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub mod event_man; pub mod event_man;

View File

@@ -1,23 +1,13 @@
//! # Pool implementation providing pre-allocated sub-pools with fixed size memory blocks //! # Pool implementation providing memory pools for packet storage.
//! //!
//! This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool //! # Example for the [StaticMemoryPool]
//! configuration. After the pre-allocation, no dynamic memory allocation will be performed
//! during run-time. This makes the implementation suitable for real-time applications and
//! embedded environments. The pool implementation will also track the size of the data stored
//! inside it.
//!
//! Transactions with the [pool][LocalPool] are done using a special [address][StoreAddr] type.
//! Adding any data to the pool will yield a store address. Modification and read operations are
//! done using a reference to a store address. Deletion will consume the store address.
//!
//! # Example
//! //!
//! ``` //! ```
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider}; //! use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig};
//! //!
//! // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes //! // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
//! let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]); //! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
//! let mut local_pool = LocalPool::new(pool_cfg); //! let mut local_pool = StaticMemoryPool::new(pool_cfg);
//! let mut addr; //! let mut addr;
//! { //! {
//! // Add new data to the pool //! // Add new data to the pool
@@ -77,22 +67,24 @@
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub use alloc_mod::*; pub use alloc_mod::*;
use core::fmt::{Display, Formatter}; use core::fmt::{Display, Formatter};
use delegate::delegate;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::error::Error; use std::error::Error;
type NumBlocks = u16; type NumBlocks = u16;
pub type StoreAddr = u64;
/// Simple address type used for transactions with the local pool. /// Simple address type used for transactions with the local pool.
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreAddr { pub struct StaticPoolAddr {
pub(crate) pool_idx: u16, pub(crate) pool_idx: u16,
pub(crate) packet_idx: NumBlocks, pub(crate) packet_idx: NumBlocks,
} }
impl StoreAddr { impl StaticPoolAddr {
pub const INVALID_ADDR: u32 = 0xFFFFFFFF; pub const INVALID_ADDR: u32 = 0xFFFFFFFF;
pub fn raw(&self) -> u32 { pub fn raw(&self) -> u32 {
@@ -100,7 +92,22 @@ impl StoreAddr {
} }
} }
impl Display for StoreAddr { impl From<StaticPoolAddr> for StoreAddr {
fn from(value: StaticPoolAddr) -> Self {
((value.pool_idx as u64) << 16) | value.packet_idx as u64
}
}
impl From<StoreAddr> for StaticPoolAddr {
fn from(value: StoreAddr) -> Self {
Self {
pool_idx: ((value >> 16) & 0xff) as u16,
packet_idx: (value & 0xff) as u16,
}
}
}
impl Display for StaticPoolAddr {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!( write!(
f, f,
@@ -180,39 +187,158 @@ impl Error for StoreError {
} }
} }
/// Generic trait for pool providers where the data can be modified and read in-place. This
/// generally means that a shared pool structure has to be wrapped inside a lock structure.
pub trait PoolProviderMemInPlace {
/// Add new data to the pool. The provider should attempt to reserve a memory block with the
/// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can
/// be used to access the data stored in the pool
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError>;
/// The provider should attempt to reserve a free memory block with the appropriate size and
/// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access
/// the data stored in the pool
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>;
/// Modify data added previously using a given [StoreAddr] by yielding a mutable reference
/// to it
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>;
/// Read data by yielding a read-only reference given a [StoreAddr]
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>;
/// Delete data inside the pool given a [StoreAddr]
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>;
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError>;
/// Retrieve the length of the data at the given store address.
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
if !self.has_element_at(addr)? {
return Err(StoreError::DataDoesNotExist(*addr));
}
Ok(self.read(addr)?.len())
}
}
pub trait PoolProviderMemInPlaceWithGuards: PoolProviderMemInPlace {
/// This function behaves like [PoolProviderMemInPlace::read], but consumes the provided address
/// and returns a RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
/// given address will be deleted automatically when the guard is dropped.
/// This can prevent memory leaks. Users can read the data and release the guard
/// if the data in the store is valid for further processing. If the data is faulty, no
/// manual deletion is necessary when returning from a processing function prematurely.
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard<Self>;
/// This function behaves like [PoolProviderMemInPlace::modify], but consumes the provided
/// address and returns a RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
/// given address will be deleted automatically when the guard is dropped.
/// This can prevent memory leaks. Users can read (and modify) the data and release the guard
/// if the data in the store is valid for further processing. If the data is faulty, no
/// manual deletion is necessary when returning from a processing function prematurely.
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self>;
}
pub struct PoolGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> {
pool: &'a mut MemProvider,
pub addr: StoreAddr,
no_deletion: bool,
deletion_failed_error: Option<StoreError>,
}
/// This helper object
impl<'a, MemProvider: PoolProviderMemInPlace> PoolGuard<'a, MemProvider> {
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
Self {
pool,
addr,
no_deletion: false,
deletion_failed_error: None,
}
}
pub fn read(&self) -> Result<&[u8], StoreError> {
self.pool.read(&self.addr)
}
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
/// is dropped.
pub fn release(&mut self) {
self.no_deletion = true;
}
}
impl<MemProvider: PoolProviderMemInPlace + ?Sized> Drop for PoolGuard<'_, MemProvider> {
fn drop(&mut self) {
if !self.no_deletion {
if let Err(e) = self.pool.delete(self.addr) {
self.deletion_failed_error = Some(e);
}
}
}
}
pub struct PoolRwGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> {
guard: PoolGuard<'a, MemProvider>,
}
impl<'a, MemProvider: PoolProviderMemInPlace> PoolRwGuard<'a, MemProvider> {
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
Self {
guard: PoolGuard::new(pool, addr),
}
}
pub fn modify(&mut self) -> Result<&mut [u8], StoreError> {
self.guard.pool.modify(&self.guard.addr)
}
delegate!(
to self.guard {
pub fn read(&self) -> Result<&[u8], StoreError>;
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
/// is dropped.
pub fn release(&mut self);
}
);
}
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
mod alloc_mod { mod alloc_mod {
use super::{
PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard,
StaticPoolAddr,
};
use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError}; use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError};
use alloc::boxed::Box;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use delegate::delegate;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub type ShareablePoolProvider = Box<dyn PoolProvider + Send + Sync>; pub type SharedStaticMemoryPool = Arc<RwLock<StaticMemoryPool>>;
#[cfg(feature = "std")]
pub type SharedPool = Arc<RwLock<ShareablePoolProvider>>;
type PoolSize = usize; type PoolSize = usize;
const STORE_FREE: PoolSize = PoolSize::MAX; const STORE_FREE: PoolSize = PoolSize::MAX;
pub const POOL_MAX_SIZE: PoolSize = STORE_FREE - 1; pub const POOL_MAX_SIZE: PoolSize = STORE_FREE - 1;
/// Configuration structure of the [local pool][LocalPool] /// Configuration structure of the [static memory pool][StaticMemoryPool]
/// ///
/// # Parameters /// # Parameters
/// ///
/// * `cfg`: Vector of tuples which represent a subpool. The first entry in the tuple specifies the /// * `cfg`: Vector of tuples which represent a subpool. The first entry in the tuple specifies the
/// number of memory blocks in the subpool, the second entry the size of the blocks /// number of memory blocks in the subpool, the second entry the size of the blocks
#[derive(Clone)] #[derive(Clone)]
pub struct PoolCfg { pub struct StaticPoolConfig {
cfg: Vec<(NumBlocks, usize)>, cfg: Vec<(NumBlocks, usize)>,
} }
impl PoolCfg { impl StaticPoolConfig {
pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self { pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self {
PoolCfg { cfg } StaticPoolConfig { cfg }
} }
pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> { pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> {
@@ -228,135 +354,30 @@ mod alloc_mod {
} }
} }
pub struct PoolGuard<'a> { /// Pool implementation providing sub-pools with fixed size memory blocks.
pool: &'a mut LocalPool, ///
pub addr: StoreAddr, /// This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool
no_deletion: bool, /// configuration. After the pre-allocation, no dynamic memory allocation will be performed
deletion_failed_error: Option<StoreError>, /// during run-time. This makes the implementation suitable for real-time applications and
} /// embedded environments. The pool implementation will also track the size of the data stored
/// inside it.
/// This helper object ///
impl<'a> PoolGuard<'a> { /// Transactions with the [pool][StaticMemoryPool] are done using a generic
pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self { /// [address][StoreAddr] type.
Self { /// Adding any data to the pool will yield a store address. Modification and read operations are
pool, /// done using a reference to a store address. Deletion will consume the store address.
addr, pub struct StaticMemoryPool {
no_deletion: false, pool_cfg: StaticPoolConfig,
deletion_failed_error: None,
}
}
pub fn read(&self) -> Result<&[u8], StoreError> {
self.pool.read(&self.addr)
}
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
/// is dropped.
pub fn release(&mut self) {
self.no_deletion = true;
}
}
impl Drop for PoolGuard<'_> {
fn drop(&mut self) {
if !self.no_deletion {
if let Err(e) = self.pool.delete(self.addr) {
self.deletion_failed_error = Some(e);
}
}
}
}
pub struct PoolRwGuard<'a> {
guard: PoolGuard<'a>,
}
impl<'a> PoolRwGuard<'a> {
pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self {
Self {
guard: PoolGuard::new(pool, addr),
}
}
pub fn modify(&mut self) -> Result<&mut [u8], StoreError> {
self.guard.pool.modify(&self.guard.addr)
}
delegate!(
to self.guard {
pub fn read(&self) -> Result<&[u8], StoreError>;
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
/// is dropped.
pub fn release(&mut self);
}
);
}
pub trait PoolProvider {
/// Add new data to the pool. The provider should attempt to reserve a memory block with the
/// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can
/// be used to access the data stored in the pool
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError>;
/// The provider should attempt to reserve a free memory block with the appropriate size and
/// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access
/// the data stored in the pool
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>;
/// Modify data added previously using a given [StoreAddr] by yielding a mutable reference
/// to it
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>;
/// This function behaves like [Self::modify], but consumes the provided address and returns a
/// RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
/// given address will be deleted automatically when the guard is dropped.
/// This can prevent memory leaks. Users can read (and modify) the data and release the guard
/// if the data in the store is valid for further processing. If the data is faulty, no
/// manual deletion is necessary when returning from a processing function prematurely.
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard;
/// Read data by yielding a read-only reference given a [StoreAddr]
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>;
/// This function behaves like [Self::read], but consumes the provided address and returns a
/// RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
/// given address will be deleted automatically when the guard is dropped.
/// This can prevent memory leaks. Users can read the data and release the guard
/// if the data in the store is valid for further processing. If the data is faulty, no
/// manual deletion is necessary when returning from a processing function prematurely.
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard;
/// Delete data inside the pool given a [StoreAddr]
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>;
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError>;
/// Retrieve the length of the data at the given store address.
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
if !self.has_element_at(addr)? {
return Err(StoreError::DataDoesNotExist(*addr));
}
Ok(self.read(addr)?.len())
}
}
/// Pool implementation providing sub-pools with fixed size memory blocks. More details in
/// the [module documentation][crate::pool]
pub struct LocalPool {
pool_cfg: PoolCfg,
pool: Vec<Vec<u8>>, pool: Vec<Vec<u8>>,
sizes_lists: Vec<Vec<PoolSize>>, sizes_lists: Vec<Vec<PoolSize>>,
} }
impl LocalPool { impl StaticMemoryPool {
/// Create a new local pool from the [given configuration][PoolCfg]. This function will sanitize /// Create a new local pool from the [given configuration][StaticPoolConfig]. This function
/// the given configuration as well. /// will sanitize the given configuration as well.
pub fn new(mut cfg: PoolCfg) -> LocalPool { pub fn new(mut cfg: StaticPoolConfig) -> StaticMemoryPool {
let subpools_num = cfg.sanitize(); let subpools_num = cfg.sanitize();
let mut local_pool = LocalPool { let mut local_pool = StaticMemoryPool {
pool_cfg: cfg, pool_cfg: cfg,
pool: Vec::with_capacity(subpools_num), pool: Vec::with_capacity(subpools_num),
sizes_lists: Vec::with_capacity(subpools_num), sizes_lists: Vec::with_capacity(subpools_num),
@@ -372,39 +393,39 @@ mod alloc_mod {
local_pool local_pool
} }
fn addr_check(&self, addr: &StoreAddr) -> Result<usize, StoreError> { fn addr_check(&self, addr: &StaticPoolAddr) -> Result<usize, StoreError> {
self.validate_addr(addr)?; self.validate_addr(addr)?;
let pool_idx = addr.pool_idx as usize; let pool_idx = addr.pool_idx as usize;
let size_list = self.sizes_lists.get(pool_idx).unwrap(); let size_list = self.sizes_lists.get(pool_idx).unwrap();
let curr_size = size_list[addr.packet_idx as usize]; let curr_size = size_list[addr.packet_idx as usize];
if curr_size == STORE_FREE { if curr_size == STORE_FREE {
return Err(StoreError::DataDoesNotExist(*addr)); return Err(StoreError::DataDoesNotExist(StoreAddr::from(*addr)));
} }
Ok(curr_size) Ok(curr_size)
} }
fn validate_addr(&self, addr: &StoreAddr) -> Result<(), StoreError> { fn validate_addr(&self, addr: &StaticPoolAddr) -> Result<(), StoreError> {
let pool_idx = addr.pool_idx as usize; let pool_idx = addr.pool_idx as usize;
if pool_idx >= self.pool_cfg.cfg.len() { if pool_idx >= self.pool_cfg.cfg.len() {
return Err(StoreError::InvalidStoreId( return Err(StoreError::InvalidStoreId(
StoreIdError::InvalidSubpool(addr.pool_idx), StoreIdError::InvalidSubpool(addr.pool_idx),
Some(*addr), Some(StoreAddr::from(*addr)),
)); ));
} }
if addr.packet_idx >= self.pool_cfg.cfg[addr.pool_idx as usize].0 { if addr.packet_idx >= self.pool_cfg.cfg[addr.pool_idx as usize].0 {
return Err(StoreError::InvalidStoreId( return Err(StoreError::InvalidStoreId(
StoreIdError::InvalidPacketIdx(addr.packet_idx), StoreIdError::InvalidPacketIdx(addr.packet_idx),
Some(*addr), Some(StoreAddr::from(*addr)),
)); ));
} }
Ok(()) Ok(())
} }
fn reserve(&mut self, data_len: usize) -> Result<StoreAddr, StoreError> { fn reserve(&mut self, data_len: usize) -> Result<StaticPoolAddr, StoreError> {
let subpool_idx = self.find_subpool(data_len, 0)?; let subpool_idx = self.find_subpool(data_len, 0)?;
let (slot, size_slot_ref) = self.find_empty(subpool_idx)?; let (slot, size_slot_ref) = self.find_empty(subpool_idx)?;
*size_slot_ref = data_len; *size_slot_ref = data_len;
Ok(StoreAddr { Ok(StaticPoolAddr {
pool_idx: subpool_idx, pool_idx: subpool_idx,
packet_idx: slot, packet_idx: slot,
}) })
@@ -422,7 +443,7 @@ mod alloc_mod {
Err(StoreError::DataTooLarge(req_size)) Err(StoreError::DataTooLarge(req_size))
} }
fn write(&mut self, addr: &StoreAddr, data: &[u8]) -> Result<(), StoreError> { fn write(&mut self, addr: &StaticPoolAddr, data: &[u8]) -> Result<(), StoreError> {
let packet_pos = self.raw_pos(addr).ok_or(StoreError::InternalError(0))?; let packet_pos = self.raw_pos(addr).ok_or(StoreError::InternalError(0))?;
let subpool = self let subpool = self
.pool .pool
@@ -449,13 +470,13 @@ mod alloc_mod {
Err(StoreError::StoreFull(subpool)) Err(StoreError::StoreFull(subpool))
} }
fn raw_pos(&self, addr: &StoreAddr) -> Option<usize> { fn raw_pos(&self, addr: &StaticPoolAddr) -> Option<usize> {
let (_, size) = self.pool_cfg.cfg.get(addr.pool_idx as usize)?; let (_, size) = self.pool_cfg.cfg.get(addr.pool_idx as usize)?;
Some(addr.packet_idx as usize * size) Some(addr.packet_idx as usize * size)
} }
} }
impl PoolProvider for LocalPool { impl PoolProviderMemInPlace for StaticMemoryPool {
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError> { fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError> {
let data_len = data.len(); let data_len = data.len();
if data_len > POOL_MAX_SIZE { if data_len > POOL_MAX_SIZE {
@@ -463,7 +484,7 @@ mod alloc_mod {
} }
let addr = self.reserve(data_len)?; let addr = self.reserve(data_len)?;
self.write(&addr, data)?; self.write(&addr, data)?;
Ok(addr) Ok(addr.into())
} }
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> { fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> {
@@ -474,34 +495,29 @@ mod alloc_mod {
let raw_pos = self.raw_pos(&addr).unwrap(); let raw_pos = self.raw_pos(&addr).unwrap();
let block = let block =
&mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len]; &mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len];
Ok((addr, block)) Ok((addr.into(), block))
} }
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> { fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> {
let curr_size = self.addr_check(addr)?; let addr = StaticPoolAddr::from(*addr);
let raw_pos = self.raw_pos(addr).unwrap(); let curr_size = self.addr_check(&addr)?;
let raw_pos = self.raw_pos(&addr).unwrap();
let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap() let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap()
[raw_pos..raw_pos + curr_size]; [raw_pos..raw_pos + curr_size];
Ok(block) Ok(block)
} }
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard {
PoolRwGuard::new(self, addr)
}
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> { fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> {
let curr_size = self.addr_check(addr)?; let addr = StaticPoolAddr::from(*addr);
let raw_pos = self.raw_pos(addr).unwrap(); let curr_size = self.addr_check(&addr)?;
let raw_pos = self.raw_pos(&addr).unwrap();
let block = let block =
&self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size]; &self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size];
Ok(block) Ok(block)
} }
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard {
PoolGuard::new(self, addr)
}
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> { fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> {
let addr = StaticPoolAddr::from(addr);
self.addr_check(&addr)?; self.addr_check(&addr)?;
let block_size = self.pool_cfg.cfg.get(addr.pool_idx as usize).unwrap().1; let block_size = self.pool_cfg.cfg.get(addr.pool_idx as usize).unwrap().1;
let raw_pos = self.raw_pos(&addr).unwrap(); let raw_pos = self.raw_pos(&addr).unwrap();
@@ -514,7 +530,8 @@ mod alloc_mod {
} }
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError> { fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError> {
self.validate_addr(addr)?; let addr = StaticPoolAddr::from(*addr);
self.validate_addr(&addr)?;
let pool_idx = addr.pool_idx as usize; let pool_idx = addr.pool_idx as usize;
let size_list = self.sizes_lists.get(pool_idx).unwrap(); let size_list = self.sizes_lists.get(pool_idx).unwrap();
let curr_size = size_list[addr.packet_idx as usize]; let curr_size = size_list[addr.packet_idx as usize];
@@ -524,34 +541,45 @@ mod alloc_mod {
Ok(true) Ok(true)
} }
} }
impl PoolProviderMemInPlaceWithGuards for StaticMemoryPool {
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self> {
PoolRwGuard::new(self, addr)
}
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard<Self> {
PoolGuard::new(self, addr)
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::pool::{ use crate::pool::{
LocalPool, PoolCfg, PoolGuard, PoolProvider, PoolRwGuard, StoreAddr, StoreError, PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard,
StoreIdError, POOL_MAX_SIZE, StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError,
POOL_MAX_SIZE,
}; };
use std::vec; use std::vec;
fn basic_small_pool() -> LocalPool { fn basic_small_pool() -> StaticMemoryPool {
// 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]); let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
LocalPool::new(pool_cfg) StaticMemoryPool::new(pool_cfg)
} }
#[test] #[test]
fn test_cfg() { fn test_cfg() {
// Values where number of buckets is 0 or size is too large should be removed // Values where number of buckets is 0 or size is too large should be removed
let mut pool_cfg = PoolCfg::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]); let mut pool_cfg = StaticPoolConfig::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]);
pool_cfg.sanitize(); pool_cfg.sanitize();
assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]); assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]);
// Entries should be ordered according to bucket size // Entries should be ordered according to bucket size
pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]); pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
pool_cfg.sanitize(); pool_cfg.sanitize();
assert_eq!(*pool_cfg.cfg(), vec![(32, 3), (16, 6), (8, 12)]); assert_eq!(*pool_cfg.cfg(), vec![(32, 3), (16, 6), (8, 12)]);
// Unstable sort is used, so order of entries with same block length should not matter // Unstable sort is used, so order of entries with same block length should not matter
pool_cfg = PoolCfg::new(vec![(12, 12), (14, 16), (10, 12)]); pool_cfg = StaticPoolConfig::new(vec![(12, 12), (14, 16), (10, 12)]);
pool_cfg.sanitize(); pool_cfg.sanitize();
assert!( assert!(
*pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)] *pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)]
@@ -600,10 +628,10 @@ mod tests {
let (addr, buf_ref) = res.unwrap(); let (addr, buf_ref) = res.unwrap();
assert_eq!( assert_eq!(
addr, addr,
StoreAddr { u64::from(StaticPoolAddr {
pool_idx: 2, pool_idx: 2,
packet_idx: 0 packet_idx: 0
} })
); );
assert_eq!(buf_ref.len(), 12); assert_eq!(buf_ref.len(), 12);
} }
@@ -655,10 +683,13 @@ mod tests {
fn test_read_does_not_exist() { fn test_read_does_not_exist() {
let local_pool = basic_small_pool(); let local_pool = basic_small_pool();
// Try to access data which does not exist // Try to access data which does not exist
let res = local_pool.read(&StoreAddr { let res = local_pool.read(
packet_idx: 0, &StaticPoolAddr {
pool_idx: 0, packet_idx: 0,
}); pool_idx: 0,
}
.into(),
);
assert!(res.is_err()); assert!(res.is_err());
assert!(matches!( assert!(matches!(
res.unwrap_err(), res.unwrap_err(),
@@ -684,10 +715,11 @@ mod tests {
#[test] #[test]
fn test_invalid_pool_idx() { fn test_invalid_pool_idx() {
let local_pool = basic_small_pool(); let local_pool = basic_small_pool();
let addr = StoreAddr { let addr = StaticPoolAddr {
pool_idx: 3, pool_idx: 3,
packet_idx: 0, packet_idx: 0,
}; }
.into();
let res = local_pool.read(&addr); let res = local_pool.read(&addr);
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
@@ -700,12 +732,12 @@ mod tests {
#[test] #[test]
fn test_invalid_packet_idx() { fn test_invalid_packet_idx() {
let local_pool = basic_small_pool(); let local_pool = basic_small_pool();
let addr = StoreAddr { let addr = StaticPoolAddr {
pool_idx: 2, pool_idx: 2,
packet_idx: 1, packet_idx: 1,
}; };
assert_eq!(addr.raw(), 0x00020001); assert_eq!(addr.raw(), 0x00020001);
let res = local_pool.read(&addr); let res = local_pool.read(&addr.into());
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
assert!(matches!( assert!(matches!(

View File

@@ -147,7 +147,7 @@ impl EventReporterBase {
Ok(PusTmCreator::new( Ok(PusTmCreator::new(
&mut sp_header, &mut sp_header,
sec_header, sec_header,
Some(&buf[0..current_idx]), &buf[0..current_idx],
true, true,
)) ))
} }

View File

@@ -82,7 +82,7 @@ pub mod heapless_mod {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub enum EventRequest<Event: GenericEvent = EventU32> { pub enum EventRequest<Event: GenericEvent = EventU32> {
Enable(Event), Enable(Event),
Disable(Event), Disable(Event),

View File

@@ -1,85 +1,64 @@
use crate::events::EventU32; use crate::events::EventU32;
use crate::pool::{SharedPool, StoreAddr};
use crate::pus::event_man::{EventRequest, EventRequestWithToken}; use crate::pus::event_man::{EventRequest, EventRequestWithToken};
use crate::pus::verification::{ use crate::pus::verification::TcStateToken;
StdVerifReporterWithSender, TcStateAccepted, TcStateToken, VerificationToken, use crate::pus::{PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError};
};
use crate::pus::{
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceBase, PusServiceHandler,
};
use alloc::boxed::Box;
use spacepackets::ecss::event::Subservice; use spacepackets::ecss::event::Subservice;
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::PusPacket; use spacepackets::ecss::PusPacket;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
pub struct PusService5EventHandler { use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
psb: PusServiceBase,
pub struct PusService5EventHandler<TcInMemConverter: EcssTcInMemConverter> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
event_request_tx: Sender<EventRequestWithToken>, event_request_tx: Sender<EventRequestWithToken>,
} }
impl PusService5EventHandler { impl<TcInMemConverter: EcssTcInMemConverter> PusService5EventHandler<TcInMemConverter> {
pub fn new( pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>, service_handler: PusServiceHelper<TcInMemConverter>,
shared_tc_store: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
event_request_tx: Sender<EventRequestWithToken>, event_request_tx: Sender<EventRequestWithToken>,
) -> Self { ) -> Self {
Self { Self {
psb: PusServiceBase::new( service_helper: service_handler,
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
event_request_tx, event_request_tx,
} }
} }
}
impl PusServiceHandler for PusService5EventHandler { pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
fn psb_mut(&mut self) -> &mut PusServiceBase { let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
&mut self.psb if possible_packet.is_none() {
} return Ok(PusPacketHandlerResult::Empty);
fn psb(&self) -> &PusServiceBase { }
&self.psb let ecss_tc_and_token = possible_packet.unwrap();
} let tc = self
.service_helper
fn handle_one_tc( .tc_in_mem_converter
&mut self, .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?;
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
let subservice = tc.subservice(); let subservice = tc.subservice();
let srv = Subservice::try_from(subservice); let srv = Subservice::try_from(subservice);
if srv.is_err() { if srv.is_err() {
return Ok(PusPacketHandlerResult::CustomSubservice( return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(), tc.subservice(),
token, ecss_tc_and_token.token,
)); ));
} }
let handle_enable_disable_request = |enable: bool, stamp: [u8; 7]| { let handle_enable_disable_request = |enable: bool, stamp: [u8; 7]| {
if tc.user_data().len() < 4 { if tc.user_data().len() < 4 {
return Err(PusPacketHandlingError::NotEnoughAppData( return Err(PusPacketHandlingError::NotEnoughAppData(
"At least 4 bytes event ID expected".into(), "at least 4 bytes event ID expected".into(),
)); ));
} }
let user_data = tc.user_data(); let user_data = tc.user_data();
let event_u32 = EventU32::from(u32::from_be_bytes(user_data[0..4].try_into().unwrap())); let event_u32 = EventU32::from(u32::from_be_bytes(user_data[0..4].try_into().unwrap()));
let start_token = self let start_token = self
.psb .service_helper
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_success(token, Some(&stamp)) .start_success(ecss_tc_and_token.token, Some(&stamp))
.map_err(|_| PartialPusHandlingError::Verification); .map_err(|_| PartialPusHandlingError::Verification);
let partial_error = start_token.clone().err(); let partial_error = start_token.clone().err();
let mut token: TcStateToken = token.into(); let mut token: TcStateToken = ecss_tc_and_token.token.into();
if let Ok(start_token) = start_token { if let Ok(start_token) = start_token {
token = start_token.into(); token = start_token.into();
} }
@@ -107,7 +86,7 @@ impl PusServiceHandler for PusService5EventHandler {
Ok(PusPacketHandlerResult::RequestHandled) Ok(PusPacketHandlerResult::RequestHandled)
}; };
let mut partial_error = None; let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error); let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
match srv.unwrap() { match srv.unwrap() {
Subservice::TmInfoReport Subservice::TmInfoReport
| Subservice::TmLowSeverityReport | Subservice::TmLowSeverityReport
@@ -123,7 +102,8 @@ impl PusServiceHandler for PusService5EventHandler {
} }
Subservice::TcReportDisabledList | Subservice::TmDisabledEventsReport => { Subservice::TcReportDisabledList | Subservice::TmDisabledEventsReport => {
return Ok(PusPacketHandlerResult::SubserviceNotImplemented( return Ok(PusPacketHandlerResult::SubserviceNotImplemented(
subservice, token, subservice,
ecss_tc_and_token.token,
)); ));
} }
} }
@@ -131,3 +111,170 @@ impl PusServiceHandler for PusService5EventHandler {
Ok(PusPacketHandlerResult::RequestHandled) Ok(PusPacketHandlerResult::RequestHandled)
} }
} }
#[cfg(test)]
mod tests {
use delegate::delegate;
use spacepackets::ecss::event::Subservice;
use spacepackets::util::UnsignedEnum;
use spacepackets::{
ecss::{
tc::{PusTcCreator, PusTcSecondaryHeader},
tm::PusTmReader,
},
SequenceFlags, SpHeader,
};
use std::sync::mpsc::{self, Sender};
use crate::pus::event_man::EventRequest;
use crate::pus::tests::SimplePusPacketHandler;
use crate::pus::verification::RequestId;
use crate::{
events::EventU32,
pus::{
event_man::EventRequestWithToken,
tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness, TEST_APID},
verification::{TcStateAccepted, VerificationToken},
EcssTcInSharedStoreConverter, PusPacketHandlerResult, PusPacketHandlingError,
},
};
use super::PusService5EventHandler;
const TEST_EVENT_0: EventU32 = EventU32::const_new(crate::events::Severity::INFO, 5, 25);
struct Pus5HandlerWithStoreTester {
common: PusServiceHandlerWithSharedStoreCommon,
handler: PusService5EventHandler<EcssTcInSharedStoreConverter>,
}
impl Pus5HandlerWithStoreTester {
pub fn new(event_request_tx: Sender<EventRequestWithToken>) -> Self {
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
Self {
common,
handler: PusService5EventHandler::new(srv_handler, event_request_tx),
}
}
}
impl PusTestHarness for Pus5HandlerWithStoreTester {
delegate! {
to self.common {
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
fn read_next_tm(&mut self) -> PusTmReader<'_>;
fn check_no_tm_available(&self) -> bool;
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
}
}
}
impl SimplePusPacketHandler for Pus5HandlerWithStoreTester {
delegate! {
to self.handler {
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
}
}
}
fn event_test(
test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler),
subservice: Subservice,
expected_event_req: EventRequest,
event_req_receiver: mpsc::Receiver<EventRequestWithToken>,
) {
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(5, subservice as u8);
let mut app_data = [0; 4];
TEST_EVENT_0
.write_to_be_bytes(&mut app_data)
.expect("writing test event failed");
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &app_data, true);
let token = test_harness.send_tc(&ping_tc);
let request_id = token.req_id();
test_harness.handle_one_tc().unwrap();
test_harness.check_next_verification_tm(1, request_id);
test_harness.check_next_verification_tm(3, request_id);
// Completion TM is not generated for us.
assert!(test_harness.check_no_tm_available());
let event_request = event_req_receiver
.try_recv()
.expect("no event request received");
assert_eq!(expected_event_req, event_request.request);
}
#[test]
fn test_enabling_event_reporting() {
let (event_request_tx, event_request_rx) = mpsc::channel();
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
event_test(
&mut test_harness,
Subservice::TcEnableEventGeneration,
EventRequest::Enable(TEST_EVENT_0),
event_request_rx,
);
}
#[test]
fn test_disabling_event_reporting() {
let (event_request_tx, event_request_rx) = mpsc::channel();
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
event_test(
&mut test_harness,
Subservice::TcDisableEventGeneration,
EventRequest::Disable(TEST_EVENT_0),
event_request_rx,
);
}
#[test]
fn test_empty_tc_queue() {
let (event_request_tx, _) = mpsc::channel();
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
let result = test_harness.handle_one_tc();
assert!(result.is_ok());
let result = result.unwrap();
if let PusPacketHandlerResult::Empty = result {
} else {
panic!("unexpected result type {result:?}")
}
}
#[test]
fn test_sending_custom_subservice() {
let (event_request_tx, _) = mpsc::channel();
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(5, 200);
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
test_harness.send_tc(&ping_tc);
let result = test_harness.handle_one_tc();
assert!(result.is_ok());
let result = result.unwrap();
if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result {
assert_eq!(subservice, 200);
} else {
panic!("unexpected result type {result:?}")
}
}
#[test]
fn test_sending_invalid_app_data() {
let (event_request_tx, _) = mpsc::channel();
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header =
PusTcSecondaryHeader::new_simple(5, Subservice::TcEnableEventGeneration as u8);
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &[0, 1, 2], true);
test_harness.send_tc(&ping_tc);
let result = test_harness.handle_one_tc();
assert!(result.is_err());
let result = result.unwrap_err();
if let PusPacketHandlingError::NotEnoughAppData(string) = result {
assert_eq!(string, "at least 4 bytes event ID expected");
} else {
panic!("unexpected result type {result:?}")
}
}
}

View File

@@ -55,12 +55,6 @@ impl<'tm> From<PusTmCreator<'tm>> for PusTmWrapper<'tm> {
} }
} }
pub type TcAddrWithToken = (StoreAddr, TcStateToken);
/// Generic abstraction for a telecommand being sent around after is has been accepted.
/// The actual telecommand is stored inside a pre-allocated pool structure.
pub type AcceptedTc = (StoreAddr, VerificationToken<TcStateAccepted>);
/// Generic error type for sending something via a message queue. /// Generic error type for sending something via a message queue.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum GenericSendError { pub enum GenericSendError {
@@ -200,11 +194,75 @@ pub trait EcssTcSenderCore: EcssChannel {
fn send_tc(&self, tc: PusTcCreator, token: Option<TcStateToken>) -> Result<(), EcssTmtcError>; fn send_tc(&self, tc: PusTcCreator, token: Option<TcStateToken>) -> Result<(), EcssTmtcError>;
} }
pub struct ReceivedTcWrapper { /// A PUS telecommand packet can be stored in memory using different methods. Right now,
pub store_addr: StoreAddr, /// storage inside a pool structure like [crate::pool::StaticMemoryPool], and storage inside a
/// `Vec<u8>` are supported.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TcInMemory {
StoreAddr(StoreAddr),
#[cfg(feature = "alloc")]
Vec(alloc::vec::Vec<u8>),
}
impl From<StoreAddr> for TcInMemory {
fn from(value: StoreAddr) -> Self {
Self::StoreAddr(value)
}
}
#[cfg(feature = "alloc")]
impl From<alloc::vec::Vec<u8>> for TcInMemory {
fn from(value: alloc::vec::Vec<u8>) -> Self {
Self::Vec(value)
}
}
/// Generic structure for an ECSS PUS Telecommand and its correspoding verification token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcssTcAndToken {
pub tc_in_memory: TcInMemory,
pub token: Option<TcStateToken>, pub token: Option<TcStateToken>,
} }
impl EcssTcAndToken {
pub fn new(tc_in_memory: impl Into<TcInMemory>, token: impl Into<TcStateToken>) -> Self {
Self {
tc_in_memory: tc_in_memory.into(),
token: Some(token.into()),
}
}
}
/// Generic abstraction for a telecommand being sent around after is has been accepted.
pub struct AcceptedEcssTcAndToken {
pub tc_in_memory: TcInMemory,
pub token: VerificationToken<TcStateAccepted>,
}
impl From<AcceptedEcssTcAndToken> for EcssTcAndToken {
fn from(value: AcceptedEcssTcAndToken) -> Self {
EcssTcAndToken {
tc_in_memory: value.tc_in_memory,
token: Some(value.token.into()),
}
}
}
impl TryFrom<EcssTcAndToken> for AcceptedEcssTcAndToken {
type Error = ();
fn try_from(value: EcssTcAndToken) -> Result<Self, Self::Error> {
if let Some(TcStateToken::Accepted(token)) = value.token {
return Ok(AcceptedEcssTcAndToken {
tc_in_memory: value.tc_in_memory,
token,
});
}
Err(())
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum TryRecvTmtcError { pub enum TryRecvTmtcError {
Error(EcssTmtcError), Error(EcssTmtcError),
@@ -231,7 +289,7 @@ impl From<StoreError> for TryRecvTmtcError {
/// Generic trait for a user supplied receiver object. /// Generic trait for a user supplied receiver object.
pub trait EcssTcReceiverCore: EcssChannel { pub trait EcssTcReceiverCore: EcssChannel {
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError>; fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError>;
} }
/// Generic trait for objects which can receive ECSS PUS telecommands. This trait is /// Generic trait for objects which can receive ECSS PUS telecommands. This trait is
@@ -309,21 +367,23 @@ mod alloc_mod {
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod std_mod { pub mod std_mod {
use crate::pool::{SharedPool, StoreAddr}; use crate::pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr};
use crate::pus::verification::{ use crate::pus::verification::{
StdVerifReporterWithSender, TcStateAccepted, VerificationToken, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
}; };
use crate::pus::{ use crate::pus::{
EcssChannel, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender, EcssTmSenderCore, EcssChannel, EcssTcAndToken, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender,
EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper, ReceivedTcWrapper, EcssTmSenderCore, EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper,
TcAddrWithToken, TryRecvTmtcError, TryRecvTmtcError,
}; };
use crate::tmtc::tm_helper::SharedTmStore; use crate::tmtc::tm_helper::SharedTmStore;
use crate::ChannelId; use crate::ChannelId;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::vec::Vec; use alloc::vec::Vec;
use crossbeam_channel as cb; use crossbeam_channel as cb;
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::tm::PusTmCreator; use spacepackets::ecss::tm::PusTmCreator;
use spacepackets::ecss::PusError; use spacepackets::ecss::PusError;
use spacepackets::time::cds::TimeProvider; use spacepackets::time::cds::TimeProvider;
@@ -335,6 +395,9 @@ pub mod std_mod {
use std::sync::mpsc::TryRecvError; use std::sync::mpsc::TryRecvError;
use thiserror::Error; use thiserror::Error;
use super::verification::VerificationReporterWithSender;
use super::{AcceptedEcssTcAndToken, TcInMemory};
impl From<mpsc::SendError<StoreAddr>> for EcssTmtcError { impl From<mpsc::SendError<StoreAddr>> for EcssTmtcError {
fn from(_: mpsc::SendError<StoreAddr>) -> Self { fn from(_: mpsc::SendError<StoreAddr>) -> Self {
Self::Send(GenericSendError::RxDisconnected) Self::Send(GenericSendError::RxDisconnected)
@@ -411,13 +474,13 @@ pub mod std_mod {
} }
} }
pub struct MpscTcInStoreReceiver { pub struct MpscTcReceiver {
id: ChannelId, id: ChannelId,
name: &'static str, name: &'static str,
receiver: mpsc::Receiver<TcAddrWithToken>, receiver: mpsc::Receiver<EcssTcAndToken>,
} }
impl EcssChannel for MpscTcInStoreReceiver { impl EcssChannel for MpscTcReceiver {
fn id(&self) -> ChannelId { fn id(&self) -> ChannelId {
self.id self.id
} }
@@ -427,26 +490,22 @@ pub mod std_mod {
} }
} }
impl EcssTcReceiverCore for MpscTcInStoreReceiver { impl EcssTcReceiverCore for MpscTcReceiver {
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> { fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e { self.receiver.try_recv().map_err(|e| match e {
TryRecvError::Empty => TryRecvTmtcError::Empty, TryRecvError::Empty => TryRecvTmtcError::Empty,
TryRecvError::Disconnected => { TryRecvError::Disconnected => {
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected)) TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
} }
})?;
Ok(ReceivedTcWrapper {
store_addr,
token: Some(token),
}) })
} }
} }
impl MpscTcInStoreReceiver { impl MpscTcReceiver {
pub fn new( pub fn new(
id: ChannelId, id: ChannelId,
name: &'static str, name: &'static str,
receiver: mpsc::Receiver<TcAddrWithToken>, receiver: mpsc::Receiver<EcssTcAndToken>,
) -> Self { ) -> Self {
Self { id, name, receiver } Self { id, name, receiver }
} }
@@ -459,8 +518,8 @@ pub mod std_mod {
#[derive(Clone)] #[derive(Clone)]
pub struct MpscTmAsVecSender { pub struct MpscTmAsVecSender {
id: ChannelId, id: ChannelId,
sender: mpsc::Sender<Vec<u8>>,
name: &'static str, name: &'static str,
sender: mpsc::Sender<Vec<u8>>,
} }
impl From<mpsc::SendError<Vec<u8>>> for EcssTmtcError { impl From<mpsc::SendError<Vec<u8>>> for EcssTmtcError {
@@ -545,23 +604,23 @@ pub mod std_mod {
} }
} }
pub struct CrossbeamTcInStoreReceiver { pub struct CrossbeamTcReceiver {
id: ChannelId, id: ChannelId,
name: &'static str, name: &'static str,
receiver: cb::Receiver<TcAddrWithToken>, receiver: cb::Receiver<EcssTcAndToken>,
} }
impl CrossbeamTcInStoreReceiver { impl CrossbeamTcReceiver {
pub fn new( pub fn new(
id: ChannelId, id: ChannelId,
name: &'static str, name: &'static str,
receiver: cb::Receiver<TcAddrWithToken>, receiver: cb::Receiver<EcssTcAndToken>,
) -> Self { ) -> Self {
Self { id, name, receiver } Self { id, name, receiver }
} }
} }
impl EcssChannel for CrossbeamTcInStoreReceiver { impl EcssChannel for CrossbeamTcReceiver {
fn id(&self) -> ChannelId { fn id(&self) -> ChannelId {
self.id self.id
} }
@@ -571,17 +630,13 @@ pub mod std_mod {
} }
} }
impl EcssTcReceiverCore for CrossbeamTcInStoreReceiver { impl EcssTcReceiverCore for CrossbeamTcReceiver {
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> { fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e { self.receiver.try_recv().map_err(|e| match e {
cb::TryRecvError::Empty => TryRecvTmtcError::Empty, cb::TryRecvError::Empty => TryRecvTmtcError::Empty,
cb::TryRecvError::Disconnected => { cb::TryRecvError::Disconnected => {
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected)) TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
} }
})?;
Ok(ReceivedTcWrapper {
store_addr,
token: Some(token),
}) })
} }
} }
@@ -596,8 +651,12 @@ pub mod std_mod {
InvalidSubservice(u8), InvalidSubservice(u8),
#[error("not enough application data available: {0}")] #[error("not enough application data available: {0}")]
NotEnoughAppData(String), NotEnoughAppData(String),
#[error("PUS packet too large, does not fit in buffer: {0}")]
PusPacketTooLarge(usize),
#[error("invalid application data")] #[error("invalid application data")]
InvalidAppData(String), InvalidAppData(String),
#[error("invalid format of TC in memory: {0:?}")]
InvalidTcInMemoryFormat(TcInMemory),
#[error("generic ECSS tmtc error: {0}")] #[error("generic ECSS tmtc error: {0}")]
EcssTmtc(#[from] EcssTmtcError), EcssTmtc(#[from] EcssTmtcError),
#[error("invalid verification token")] #[error("invalid verification token")]
@@ -634,42 +693,126 @@ pub mod std_mod {
} }
} }
/// Base class for handlers which can handle PUS TC packets. Right now, the verification pub trait EcssTcInMemConverter {
/// reporter is constrained to the [StdVerifReporterWithSender] and the service handler fn cache_ecss_tc_in_memory(
/// relies on TMTC packets being exchanged via a [SharedPool]. &mut self,
possible_packet: &TcInMemory,
) -> Result<(), PusPacketHandlingError>;
fn tc_slice_raw(&self) -> &[u8];
fn convert_ecss_tc_in_memory_to_reader(
&mut self,
possible_packet: &TcInMemory,
) -> Result<PusTcReader<'_>, PusPacketHandlingError> {
self.cache_ecss_tc_in_memory(possible_packet)?;
Ok(PusTcReader::new(self.tc_slice_raw())?.0)
}
}
/// Converter structure for PUS telecommands which are stored inside a `Vec<u8>` structure.
/// Please note that this structure is not able to convert TCs which are stored inside a
/// [SharedStaticMemoryPool].
#[derive(Default, Clone)]
pub struct EcssTcInVecConverter {
pub pus_tc_raw: Option<Vec<u8>>,
}
impl EcssTcInMemConverter for EcssTcInVecConverter {
fn cache_ecss_tc_in_memory(
&mut self,
tc_in_memory: &TcInMemory,
) -> Result<(), PusPacketHandlingError> {
self.pus_tc_raw = None;
match tc_in_memory {
super::TcInMemory::StoreAddr(_) => {
return Err(PusPacketHandlingError::InvalidTcInMemoryFormat(
tc_in_memory.clone(),
));
}
super::TcInMemory::Vec(vec) => {
self.pus_tc_raw = Some(vec.clone());
}
};
Ok(())
}
fn tc_slice_raw(&self) -> &[u8] {
if self.pus_tc_raw.is_none() {
return &[];
}
self.pus_tc_raw.as_ref().unwrap()
}
}
/// Converter structure for PUS telecommands which are stored inside
/// [SharedStaticMemoryPool] structure. This is useful if run-time allocation for these
/// packets should be avoided. Please note that this structure is not able to convert TCs which
/// are stored as a `Vec<u8>`.
pub struct EcssTcInSharedStoreConverter {
shared_tc_store: SharedStaticMemoryPool,
pus_buf: Vec<u8>,
}
impl EcssTcInSharedStoreConverter {
pub fn new(shared_tc_store: SharedStaticMemoryPool, max_expected_tc_size: usize) -> Self {
Self {
shared_tc_store,
pus_buf: alloc::vec![0; max_expected_tc_size],
}
}
pub fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> {
// Keep locked section as short as possible.
let mut tc_pool = self
.shared_tc_store
.write()
.map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?;
let tc_guard = tc_pool.read_with_guard(addr);
let tc_raw = tc_guard.read().unwrap();
if tc_raw.len() > self.pus_buf.len() {
return Err(PusPacketHandlingError::PusPacketTooLarge(tc_raw.len()));
}
self.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw);
Ok(())
}
}
impl EcssTcInMemConverter for EcssTcInSharedStoreConverter {
fn cache_ecss_tc_in_memory(
&mut self,
tc_in_memory: &TcInMemory,
) -> Result<(), PusPacketHandlingError> {
match tc_in_memory {
super::TcInMemory::StoreAddr(addr) => {
self.copy_tc_to_buf(*addr)?;
}
super::TcInMemory::Vec(_) => {
return Err(PusPacketHandlingError::InvalidTcInMemoryFormat(
tc_in_memory.clone(),
));
}
};
Ok(())
}
fn tc_slice_raw(&self) -> &[u8] {
self.pus_buf.as_ref()
}
}
pub struct PusServiceBase { pub struct PusServiceBase {
pub tc_receiver: Box<dyn EcssTcReceiver>, pub tc_receiver: Box<dyn EcssTcReceiver>,
pub shared_tc_store: SharedPool,
pub tm_sender: Box<dyn EcssTmSender>, pub tm_sender: Box<dyn EcssTmSender>,
pub tm_apid: u16, pub tm_apid: u16,
/// The verification handler is wrapped in a [RefCell] to allow the interior mutability /// The verification handler is wrapped in a [RefCell] to allow the interior mutability
/// pattern. This makes writing methods which are not mutable a lot easier. /// pattern. This makes writing methods which are not mutable a lot easier.
pub verification_handler: RefCell<StdVerifReporterWithSender>, pub verification_handler: RefCell<StdVerifReporterWithSender>,
pub pus_buf: [u8; 2048],
pub pus_size: usize,
} }
impl PusServiceBase { impl PusServiceBase {
pub fn new( #[cfg(feature = "std")]
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_store: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
) -> Self {
Self {
tc_receiver,
shared_tc_store,
tm_apid,
tm_sender,
verification_handler: RefCell::new(verification_handler),
pus_buf: [0; 2048],
pus_size: 0,
}
}
pub fn get_current_timestamp( pub fn get_current_timestamp(
&self,
partial_error: &mut Option<PartialPusHandlingError>, partial_error: &mut Option<PartialPusHandlingError>,
) -> [u8; 7] { ) -> [u8; 7] {
let mut time_stamp: [u8; 7] = [0; 7]; let mut time_stamp: [u8; 7] = [0; 7];
@@ -684,48 +827,73 @@ pub mod std_mod {
time_stamp time_stamp
} }
pub fn get_current_timestamp_ignore_error(&self) -> [u8; 7] { #[cfg(feature = "std")]
pub fn get_current_timestamp_ignore_error() -> [u8; 7] {
let mut dummy = None; let mut dummy = None;
self.get_current_timestamp(&mut dummy) Self::get_current_timestamp(&mut dummy)
} }
} }
pub trait PusServiceHandler { /// This is a high-level PUS packet handler helper.
fn psb_mut(&mut self) -> &mut PusServiceBase; ///
fn psb(&self) -> &PusServiceBase; /// It performs some of the boilerplate acitivities involved when handling PUS telecommands and
fn handle_one_tc( /// it can be used to implement the handling of PUS telecommands for certain PUS telecommands
&mut self, /// groups (for example individual services).
addr: StoreAddr, ///
token: VerificationToken<TcStateAccepted>, /// This base class can handle PUS telecommands backed by different memory storage machanisms
) -> Result<PusPacketHandlerResult, PusPacketHandlingError>; /// by using the [EcssTcInMemConverter] abstraction. This object provides some convenience
/// methods to make the generic parts of TC handling easier.
pub struct PusServiceHelper<TcInMemConverter: EcssTcInMemConverter> {
pub common: PusServiceBase,
pub tc_in_mem_converter: TcInMemConverter,
}
fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> { impl<TcInMemConverter: EcssTcInMemConverter> PusServiceHelper<TcInMemConverter> {
// Keep locked section as short as possible. pub fn new(
let psb_mut = self.psb_mut(); tc_receiver: Box<dyn EcssTcReceiver>,
let mut tc_pool = psb_mut tm_sender: Box<dyn EcssTmSender>,
.shared_tc_store tm_apid: u16,
.write() verification_handler: VerificationReporterWithSender,
.map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?; tc_in_mem_converter: TcInMemConverter,
let tc_guard = tc_pool.read_with_guard(addr); ) -> Self {
let tc_raw = tc_guard.read().unwrap(); Self {
psb_mut.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw); common: PusServiceBase {
Ok(()) tc_receiver,
tm_sender,
tm_apid,
verification_handler: RefCell::new(verification_handler),
},
tc_in_mem_converter,
}
} }
fn handle_next_packet(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> { /// This function can be used to poll the internal [EcssTcReceiver] object for the next
match self.psb().tc_receiver.recv_tc() { /// telecommand packet. It will return `Ok(None)` if there are not packets available.
Ok(ReceivedTcWrapper { store_addr, token }) => { /// In any other case, it will perform the acceptance of the ECSS TC packet using the
/// internal [VerificationReporterWithSender] object. It will then return the telecommand
/// and the according accepted token.
pub fn retrieve_and_accept_next_packet(
&mut self,
) -> Result<Option<AcceptedEcssTcAndToken>, PusPacketHandlingError> {
match self.common.tc_receiver.recv_tc() {
Ok(EcssTcAndToken {
tc_in_memory,
token,
}) => {
if token.is_none() { if token.is_none() {
return Err(PusPacketHandlingError::InvalidVerificationToken); return Err(PusPacketHandlingError::InvalidVerificationToken);
} }
let token = token.unwrap(); let token = token.unwrap();
let accepted_token = VerificationToken::<TcStateAccepted>::try_from(token) let accepted_token = VerificationToken::<TcStateAccepted>::try_from(token)
.map_err(|_| PusPacketHandlingError::InvalidVerificationToken)?; .map_err(|_| PusPacketHandlingError::InvalidVerificationToken)?;
self.handle_one_tc(store_addr, accepted_token) Ok(Some(AcceptedEcssTcAndToken {
tc_in_memory,
token: accepted_token,
}))
} }
Err(e) => match e { Err(e) => match e {
TryRecvTmtcError::Error(e) => Err(PusPacketHandlingError::EcssTmtc(e)), TryRecvTmtcError::Error(e) => Err(PusPacketHandlingError::EcssTmtc(e)),
TryRecvTmtcError::Empty => Ok(PusPacketHandlerResult::Empty), TryRecvTmtcError::Empty => Ok(None),
}, },
} }
} }
@@ -746,10 +914,35 @@ pub(crate) fn source_buffer_large_enough(cap: usize, len: usize) -> Result<(), E
} }
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub mod tests {
use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator}; use std::sync::mpsc::TryRecvError;
use std::sync::{mpsc, RwLock};
use alloc::boxed::Box;
use alloc::vec;
use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator, PusTmReader};
use spacepackets::ecss::{PusPacket, WritablePusPacket};
use spacepackets::CcsdsPacket; use spacepackets::CcsdsPacket;
use crate::pool::{
PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig,
StoreAddr,
};
use crate::pus::verification::RequestId;
use crate::tmtc::tm_helper::SharedTmStore;
use super::verification::{
TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
};
use super::{
EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver,
MpscTmAsVecSender, MpscTmInStoreSender, PusPacketHandlerResult, PusPacketHandlingError,
PusServiceHelper, TcInMemory,
};
pub const TEST_APID: u16 = 0x101;
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub(crate) struct CommonTmInfo { pub(crate) struct CommonTmInfo {
pub subservice: u8, pub subservice: u8,
@@ -759,12 +952,23 @@ pub(crate) mod tests {
pub time_stamp: [u8; 7], pub time_stamp: [u8; 7],
} }
pub trait PusTestHarness {
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
fn read_next_tm(&mut self) -> PusTmReader<'_>;
fn check_no_tm_available(&self) -> bool;
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
}
pub trait SimplePusPacketHandler {
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
}
impl CommonTmInfo { impl CommonTmInfo {
pub fn new_from_tm(tm: &PusTmCreator) -> Self { pub fn new_from_tm(tm: &PusTmCreator) -> Self {
let mut time_stamp = [0; 7]; let mut time_stamp = [0; 7];
time_stamp.clone_from_slice(&tm.timestamp()[0..7]); time_stamp.clone_from_slice(&tm.timestamp()[0..7]);
Self { Self {
subservice: tm.subservice(), subservice: PusPacket::subservice(tm),
apid: tm.apid(), apid: tm.apid(),
msg_counter: tm.msg_counter(), msg_counter: tm.msg_counter(),
dest_id: tm.dest_id(), dest_id: tm.dest_id(),
@@ -772,4 +976,192 @@ pub(crate) mod tests {
} }
} }
} }
/// Common fields for a PUS service test harness.
pub struct PusServiceHandlerWithSharedStoreCommon {
pus_buf: [u8; 2048],
tm_buf: [u8; 2048],
tc_pool: SharedStaticMemoryPool,
tm_pool: SharedTmStore,
tc_sender: mpsc::Sender<EcssTcAndToken>,
tm_receiver: mpsc::Receiver<StoreAddr>,
verification_handler: VerificationReporterWithSender,
}
impl PusServiceHandlerWithSharedStoreCommon {
/// This function generates the structure in addition to the PUS service handler
/// [PusServiceHandler] which might be required for a specific PUS service handler.
///
/// The PUS service handler is instantiated with a [EcssTcInStoreConverter].
pub fn new() -> (Self, PusServiceHelper<EcssTcInSharedStoreConverter>) {
let pool_cfg = StaticPoolConfig::new(vec![(16, 16), (8, 32), (4, 64)]);
let tc_pool = StaticMemoryPool::new(pool_cfg.clone());
let tm_pool = StaticMemoryPool::new(pool_cfg);
let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool));
let shared_tm_pool = SharedTmStore::new(tm_pool);
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
let (tm_tx, tm_rx) = mpsc::channel();
let verif_sender =
MpscTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tm_tx.clone());
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
let verification_handler =
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
let test_srv_tm_sender =
MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx);
let test_srv_tc_receiver = MpscTcReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx);
let in_store_converter =
EcssTcInSharedStoreConverter::new(shared_tc_pool.clone(), 2048);
(
Self {
pus_buf: [0; 2048],
tm_buf: [0; 2048],
tc_pool: shared_tc_pool,
tm_pool: shared_tm_pool,
tc_sender: test_srv_tc_tx,
tm_receiver: tm_rx,
verification_handler: verification_handler.clone(),
},
PusServiceHelper::new(
Box::new(test_srv_tc_receiver),
Box::new(test_srv_tm_sender),
TEST_APID,
verification_handler,
in_store_converter,
),
)
}
pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
let token = self.verification_handler.add_tc(tc);
let token = self
.verification_handler
.acceptance_success(token, Some(&[0; 7]))
.unwrap();
let tc_size = tc.write_to_bytes(&mut self.pus_buf).unwrap();
let mut tc_pool = self.tc_pool.write().unwrap();
let addr = tc_pool.add(&self.pus_buf[..tc_size]).unwrap();
drop(tc_pool);
// Send accepted TC to test service handler.
self.tc_sender
.send(EcssTcAndToken::new(addr, token))
.expect("sending tc failed");
token
}
pub fn read_next_tm(&mut self) -> PusTmReader<'_> {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
let tm_addr = next_msg.unwrap();
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
self.tm_buf[0..tm_raw.len()].copy_from_slice(tm_raw);
PusTmReader::new(&self.tm_buf, 7).unwrap().0
}
pub fn check_no_tm_available(&self) -> bool {
let next_msg = self.tm_receiver.try_recv();
if let TryRecvError::Empty = next_msg.unwrap_err() {
return true;
}
false
}
pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
let tm_addr = next_msg.unwrap();
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
let tm = PusTmReader::new(tm_raw, 7).unwrap().0;
assert_eq!(PusPacket::service(&tm), 1);
assert_eq!(PusPacket::subservice(&tm), subservice);
assert_eq!(tm.apid(), TEST_APID);
let req_id =
RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
assert_eq!(req_id, expected_request_id);
}
}
pub struct PusServiceHandlerWithVecCommon {
current_tm: Option<alloc::vec::Vec<u8>>,
tc_sender: mpsc::Sender<EcssTcAndToken>,
tm_receiver: mpsc::Receiver<alloc::vec::Vec<u8>>,
verification_handler: VerificationReporterWithSender,
}
impl PusServiceHandlerWithVecCommon {
pub fn new() -> (Self, PusServiceHelper<EcssTcInVecConverter>) {
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
let (tm_tx, tm_rx) = mpsc::channel();
let verif_sender = MpscTmAsVecSender::new(0, "verififcatio-sender", tm_tx.clone());
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
let verification_handler =
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
let test_srv_tm_sender = MpscTmAsVecSender::new(0, "test-sender", tm_tx);
let test_srv_tc_receiver = MpscTcReceiver::new(0, "test-receiver", test_srv_tc_rx);
let in_store_converter = EcssTcInVecConverter::default();
(
Self {
current_tm: None,
tc_sender: test_srv_tc_tx,
tm_receiver: tm_rx,
verification_handler: verification_handler.clone(),
},
PusServiceHelper::new(
Box::new(test_srv_tc_receiver),
Box::new(test_srv_tm_sender),
TEST_APID,
verification_handler,
in_store_converter,
),
)
}
pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
let token = self.verification_handler.add_tc(tc);
let token = self
.verification_handler
.acceptance_success(token, Some(&[0; 7]))
.unwrap();
// Send accepted TC to test service handler.
self.tc_sender
.send(EcssTcAndToken::new(
TcInMemory::Vec(tc.to_vec().expect("pus tc conversion to vec failed")),
token,
))
.expect("sending tc failed");
token
}
pub fn read_next_tm(&mut self) -> PusTmReader<'_> {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
self.current_tm = Some(next_msg.unwrap());
PusTmReader::new(self.current_tm.as_ref().unwrap(), 7)
.unwrap()
.0
}
pub fn check_no_tm_available(&self) -> bool {
let next_msg = self.tm_receiver.try_recv();
if let TryRecvError::Empty = next_msg.unwrap_err() {
return true;
}
false
}
pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
let next_msg = next_msg.unwrap();
let tm = PusTmReader::new(next_msg.as_slice(), 7).unwrap().0;
assert_eq!(PusPacket::service(&tm), 1);
assert_eq!(PusPacket::subservice(&tm), subservice);
assert_eq!(tm.apid(), TEST_APID);
let req_id =
RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
assert_eq!(req_id, expected_request_id);
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +1,130 @@
use crate::pool::{SharedPool, StoreAddr}; use super::scheduler::PusSchedulerInterface;
use crate::pus::scheduler::PusScheduler; use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken}; use crate::pool::PoolProviderMemInPlace;
use crate::pus::{ use crate::pus::{PusPacketHandlerResult, PusPacketHandlingError};
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, use alloc::string::ToString;
PusServiceHandler,
};
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::{scheduling, PusPacket}; use spacepackets::ecss::{scheduling, PusPacket};
use spacepackets::time::cds::TimeProvider; use spacepackets::time::cds::TimeProvider;
use std::boxed::Box;
/// This is a helper class for [std] environments to handle generic PUS 11 (scheduling service) /// This is a helper class for [std] environments to handle generic PUS 11 (scheduling service)
/// packets. This handler is constrained to using the [PusScheduler], but is able to process /// packets. This handler is able to handle the most important PUS requests for a scheduling
/// the most important PUS requests for a scheduling service. /// service which provides the [PusSchedulerInterface].
/// ///
/// Please note that this class does not do the regular periodic handling like releasing any /// Please note that this class does not do the regular periodic handling like releasing any
/// telecommands inside the scheduler. The user can retrieve the wrapped scheduler via the /// telecommands inside the scheduler. The user can retrieve the wrapped scheduler via the
/// [Self::scheduler] and [Self::scheduler_mut] function and then use the scheduler API to release /// [Self::scheduler] and [Self::scheduler_mut] function and then use the scheduler API to release
/// telecommands when applicable. /// telecommands when applicable.
pub struct PusService11SchedHandler { pub struct PusService11SchedHandler<
psb: PusServiceBase, TcInMemConverter: EcssTcInMemConverter,
scheduler: PusScheduler, Scheduler: PusSchedulerInterface,
> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
scheduler: Scheduler,
} }
impl PusService11SchedHandler { impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerInterface>
pub fn new( PusService11SchedHandler<TcInMemConverter, Scheduler>
tc_receiver: Box<dyn EcssTcReceiver>, {
shared_tc_store: SharedPool, pub fn new(service_helper: PusServiceHelper<TcInMemConverter>, scheduler: Scheduler) -> Self {
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
scheduler: PusScheduler,
) -> Self {
Self { Self {
psb: PusServiceBase::new( service_helper,
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
scheduler, scheduler,
} }
} }
pub fn scheduler_mut(&mut self) -> &mut PusScheduler { pub fn scheduler_mut(&mut self) -> &mut Scheduler {
&mut self.scheduler &mut self.scheduler
} }
pub fn scheduler(&self) -> &PusScheduler { pub fn scheduler(&self) -> &Scheduler {
&self.scheduler &self.scheduler
} }
}
impl PusServiceHandler for PusService11SchedHandler { pub fn handle_one_tc(
fn psb_mut(&mut self) -> &mut PusServiceBase {
&mut self.psb
}
fn psb(&self) -> &PusServiceBase {
&self.psb
}
fn handle_one_tc(
&mut self, &mut self,
addr: StoreAddr, sched_tc_pool: &mut (impl PoolProviderMemInPlace + ?Sized),
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> { ) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?; let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?; if possible_packet.is_none() {
let subservice = tc.subservice(); return Ok(PusPacketHandlerResult::Empty);
let std_service = scheduling::Subservice::try_from(subservice); }
if std_service.is_err() { let ecss_tc_and_token = possible_packet.unwrap();
let tc = self
.service_helper
.tc_in_mem_converter
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
let subservice = PusPacket::subservice(&tc);
let standard_subservice = scheduling::Subservice::try_from(subservice);
if standard_subservice.is_err() {
return Ok(PusPacketHandlerResult::CustomSubservice( return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(), subservice,
token, ecss_tc_and_token.token,
)); ));
} }
let mut partial_error = None; let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error); let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
match std_service.unwrap() { match standard_subservice.unwrap() {
scheduling::Subservice::TcEnableScheduling => { scheduling::Subservice::TcEnableScheduling => {
let start_token = self let start_token = self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&time_stamp)) .start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success"); .expect("Error sending start success");
self.scheduler.enable(); self.scheduler.enable();
if self.scheduler.is_enabled() { if self.scheduler.is_enabled() {
self.psb self.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&time_stamp)) .completion_success(start_token, Some(&time_stamp))
.expect("Error sending completion success"); .expect("Error sending completion success");
} else { } else {
panic!("Failed to enable scheduler"); return Err(PusPacketHandlingError::Other(
"failed to enabled scheduler".to_string(),
));
} }
} }
scheduling::Subservice::TcDisableScheduling => { scheduling::Subservice::TcDisableScheduling => {
let start_token = self let start_token = self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&time_stamp)) .start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success"); .expect("Error sending start success");
self.scheduler.disable(); self.scheduler.disable();
if !self.scheduler.is_enabled() { if !self.scheduler.is_enabled() {
self.psb self.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&time_stamp)) .completion_success(start_token, Some(&time_stamp))
.expect("Error sending completion success"); .expect("Error sending completion success");
} else { } else {
panic!("Failed to disable scheduler"); return Err(PusPacketHandlingError::Other(
"failed to disable scheduler".to_string(),
));
} }
} }
scheduling::Subservice::TcResetScheduling => { scheduling::Subservice::TcResetScheduling => {
let start_token = self let start_token = self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&time_stamp)) .start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success"); .expect("Error sending start success");
let mut pool = self
.psb
.shared_tc_store
.write()
.expect("Locking pool failed");
self.scheduler self.scheduler
.reset(pool.as_mut()) .reset(sched_tc_pool)
.expect("Error resetting TC Pool"); .expect("Error resetting TC Pool");
self.psb self.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&time_stamp)) .completion_success(start_token, Some(&time_stamp))
@@ -143,31 +132,30 @@ impl PusServiceHandler for PusService11SchedHandler {
} }
scheduling::Subservice::TcInsertActivity => { scheduling::Subservice::TcInsertActivity => {
let start_token = self let start_token = self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&time_stamp)) .start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("error sending start success"); .expect("error sending start success");
let mut pool = self // let mut pool = self.sched_tc_pool.write().expect("locking pool failed");
.psb
.shared_tc_store
.write()
.expect("locking pool failed");
self.scheduler self.scheduler
.insert_wrapped_tc::<TimeProvider>(&tc, pool.as_mut()) .insert_wrapped_tc::<TimeProvider>(&tc, sched_tc_pool)
.expect("insertion of activity into pool failed"); .expect("insertion of activity into pool failed");
self.psb self.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&time_stamp)) .completion_success(start_token, Some(&time_stamp))
.expect("sending completion success failed"); .expect("sending completion success failed");
} }
_ => { _ => {
// Treat unhandled standard subservices as custom subservices for now.
return Ok(PusPacketHandlerResult::CustomSubservice( return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(), subservice,
token, ecss_tc_and_token.token,
)); ));
} }
} }
@@ -176,9 +164,192 @@ impl PusServiceHandler for PusService11SchedHandler {
partial_error, partial_error,
)); ));
} }
Ok(PusPacketHandlerResult::CustomSubservice( Ok(PusPacketHandlerResult::RequestHandled)
tc.subservice(), }
token, }
))
#[cfg(test)]
mod tests {
use crate::pool::{StaticMemoryPool, StaticPoolConfig};
use crate::pus::tests::TEST_APID;
use crate::pus::{
scheduler::{self, PusSchedulerInterface, TcInfo},
tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness},
verification::{RequestId, TcStateAccepted, VerificationToken},
EcssTcInSharedStoreConverter,
};
use alloc::collections::VecDeque;
use delegate::delegate;
use spacepackets::ecss::scheduling::Subservice;
use spacepackets::ecss::tc::PusTcSecondaryHeader;
use spacepackets::ecss::WritablePusPacket;
use spacepackets::time::TimeWriter;
use spacepackets::SpHeader;
use spacepackets::{
ecss::{tc::PusTcCreator, tm::PusTmReader},
time::cds,
};
use super::PusService11SchedHandler;
struct Pus11HandlerWithStoreTester {
common: PusServiceHandlerWithSharedStoreCommon,
handler: PusService11SchedHandler<EcssTcInSharedStoreConverter, TestScheduler>,
sched_tc_pool: StaticMemoryPool,
}
impl Pus11HandlerWithStoreTester {
pub fn new() -> Self {
let test_scheduler = TestScheduler::default();
let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)]);
let sched_tc_pool = StaticMemoryPool::new(pool_cfg.clone());
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
Self {
common,
handler: PusService11SchedHandler::new(srv_handler, test_scheduler),
sched_tc_pool,
}
}
}
impl PusTestHarness for Pus11HandlerWithStoreTester {
delegate! {
to self.common {
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
fn read_next_tm(&mut self) -> PusTmReader<'_>;
fn check_no_tm_available(&self) -> bool;
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
}
}
}
#[derive(Default)]
pub struct TestScheduler {
reset_count: u32,
enabled: bool,
enabled_count: u32,
disabled_count: u32,
inserted_tcs: VecDeque<TcInfo>,
}
impl PusSchedulerInterface for TestScheduler {
type TimeProvider = cds::TimeProvider;
fn reset(
&mut self,
_store: &mut (impl crate::pool::PoolProviderMemInPlace + ?Sized),
) -> Result<(), crate::pool::StoreError> {
self.reset_count += 1;
Ok(())
}
fn is_enabled(&self) -> bool {
self.enabled
}
fn enable(&mut self) {
self.enabled_count += 1;
self.enabled = true;
}
fn disable(&mut self) {
self.disabled_count += 1;
self.enabled = false;
}
fn insert_unwrapped_and_stored_tc(
&mut self,
_time_stamp: spacepackets::time::UnixTimestamp,
info: crate::pus::scheduler::TcInfo,
) -> Result<(), crate::pus::scheduler::ScheduleError> {
self.inserted_tcs.push_back(info);
Ok(())
}
}
fn generic_subservice_send(
test_harness: &mut Pus11HandlerWithStoreTester,
subservice: Subservice,
) {
let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(11, subservice as u8);
let enable_scheduling = PusTcCreator::new(&mut reply_header, tc_header, &[0; 7], true);
let token = test_harness.send_tc(&enable_scheduling);
let request_id = token.req_id();
test_harness
.handler
.handle_one_tc(&mut test_harness.sched_tc_pool)
.unwrap();
test_harness.check_next_verification_tm(1, request_id);
test_harness.check_next_verification_tm(3, request_id);
test_harness.check_next_verification_tm(7, request_id);
}
#[test]
fn test_scheduling_enabling_tc() {
let mut test_harness = Pus11HandlerWithStoreTester::new();
test_harness.handler.scheduler_mut().disable();
assert!(!test_harness.handler.scheduler().is_enabled());
generic_subservice_send(&mut test_harness, Subservice::TcEnableScheduling);
assert!(test_harness.handler.scheduler().is_enabled());
assert_eq!(test_harness.handler.scheduler().enabled_count, 1);
}
#[test]
fn test_scheduling_disabling_tc() {
let mut test_harness = Pus11HandlerWithStoreTester::new();
test_harness.handler.scheduler_mut().enable();
assert!(test_harness.handler.scheduler().is_enabled());
generic_subservice_send(&mut test_harness, Subservice::TcDisableScheduling);
assert!(!test_harness.handler.scheduler().is_enabled());
assert_eq!(test_harness.handler.scheduler().disabled_count, 1);
}
#[test]
fn test_reset_scheduler_tc() {
let mut test_harness = Pus11HandlerWithStoreTester::new();
generic_subservice_send(&mut test_harness, Subservice::TcResetScheduling);
assert_eq!(test_harness.handler.scheduler().reset_count, 1);
}
#[test]
fn test_insert_activity_tc() {
let mut test_harness = Pus11HandlerWithStoreTester::new();
let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap();
let mut sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new(&mut reply_header, sec_header, &[], true);
let req_id_ping_tc = scheduler::RequestId::from_tc(&ping_tc);
let stamper = cds::TimeProvider::from_now_with_u16_days().expect("time provider failed");
let mut sched_app_data: [u8; 64] = [0; 64];
let mut written_len = stamper.write_to_bytes(&mut sched_app_data).unwrap();
let ping_raw = ping_tc.to_vec().expect("generating raw tc failed");
sched_app_data[written_len..written_len + ping_raw.len()].copy_from_slice(&ping_raw);
written_len += ping_raw.len();
reply_header = SpHeader::tm_unseg(TEST_APID, 1, 0).unwrap();
sec_header = PusTcSecondaryHeader::new_simple(11, Subservice::TcInsertActivity as u8);
let enable_scheduling = PusTcCreator::new(
&mut reply_header,
sec_header,
&sched_app_data[..written_len],
true,
);
let token = test_harness.send_tc(&enable_scheduling);
let request_id = token.req_id();
test_harness
.handler
.handle_one_tc(&mut test_harness.sched_tc_pool)
.unwrap();
test_harness.check_next_verification_tm(1, request_id);
test_harness.check_next_verification_tm(3, request_id);
test_harness.check_next_verification_tm(7, request_id);
let tc_info = test_harness
.handler
.scheduler_mut()
.inserted_tcs
.pop_front()
.unwrap();
assert_eq!(tc_info.request_id(), req_id_ping_tc);
} }
} }

View File

@@ -1,67 +1,45 @@
use crate::pool::{SharedPool, StoreAddr};
use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken};
use crate::pus::{ use crate::pus::{
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult, PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError, PusTmWrapper,
PusPacketHandlingError, PusServiceBase, PusServiceHandler, PusTmWrapper,
}; };
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use spacepackets::ecss::PusPacket; use spacepackets::ecss::PusPacket;
use spacepackets::SpHeader; use spacepackets::SpHeader;
use std::boxed::Box;
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
/// This is a helper class for [std] environments to handle generic PUS 17 (test service) packets. /// This is a helper class for [std] environments to handle generic PUS 17 (test service) packets.
/// This handler only processes ping requests and generates a ping reply for them accordingly. /// This handler only processes ping requests and generates a ping reply for them accordingly.
pub struct PusService17TestHandler { pub struct PusService17TestHandler<TcInMemConverter: EcssTcInMemConverter> {
psb: PusServiceBase, pub service_helper: PusServiceHelper<TcInMemConverter>,
} }
impl PusService17TestHandler { impl<TcInMemConverter: EcssTcInMemConverter> PusService17TestHandler<TcInMemConverter> {
pub fn new( pub fn new(service_helper: PusServiceHelper<TcInMemConverter>) -> Self {
tc_receiver: Box<dyn EcssTcReceiver>, Self { service_helper }
shared_tc_store: SharedPool, }
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16, pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
verification_handler: StdVerifReporterWithSender, let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
) -> Self { if possible_packet.is_none() {
Self { return Ok(PusPacketHandlerResult::Empty);
psb: PusServiceBase::new(
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
} }
} let ecss_tc_and_token = possible_packet.unwrap();
} let tc = self
.service_helper
impl PusServiceHandler for PusService17TestHandler { .tc_in_mem_converter
fn psb_mut(&mut self) -> &mut PusServiceBase { .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
&mut self.psb
}
fn psb(&self) -> &PusServiceBase {
&self.psb
}
fn handle_one_tc(
&mut self,
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?;
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
if tc.service() != 17 { if tc.service() != 17 {
return Err(PusPacketHandlingError::WrongService(tc.service())); return Err(PusPacketHandlingError::WrongService(tc.service()));
} }
if tc.subservice() == 1 { if tc.subservice() == 1 {
let mut partial_error = None; let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error); let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
let result = self let result = self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&time_stamp)) .start_success(ecss_tc_and_token.token, Some(&time_stamp))
.map_err(|_| PartialPusHandlingError::Verification); .map_err(|_| PartialPusHandlingError::Verification);
let start_token = if let Ok(result) = result { let start_token = if let Ok(result) = result {
Some(result) Some(result)
@@ -70,11 +48,13 @@ impl PusServiceHandler for PusService17TestHandler {
None None
}; };
// Sequence count will be handled centrally in TM funnel. // Sequence count will be handled centrally in TM funnel.
let mut reply_header = SpHeader::tm_unseg(self.psb.tm_apid, 0, 0).unwrap(); let mut reply_header =
SpHeader::tm_unseg(self.service_helper.common.tm_apid, 0, 0).unwrap();
let tc_header = PusTmSecondaryHeader::new_simple(17, 2, &time_stamp); let tc_header = PusTmSecondaryHeader::new_simple(17, 2, &time_stamp);
let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, None, true); let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, &[], true);
let result = self let result = self
.psb .service_helper
.common
.tm_sender .tm_sender
.send_tm(PusTmWrapper::Direct(ping_reply)) .send_tm(PusTmWrapper::Direct(ping_reply))
.map_err(PartialPusHandlingError::TmSend); .map_err(PartialPusHandlingError::TmSend);
@@ -84,7 +64,8 @@ impl PusServiceHandler for PusService17TestHandler {
if let Some(start_token) = start_token { if let Some(start_token) = start_token {
if self if self
.psb .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&time_stamp)) .completion_success(start_token, Some(&time_stamp))
@@ -98,121 +79,194 @@ impl PusServiceHandler for PusService17TestHandler {
partial_error, partial_error,
)); ));
}; };
return Ok(PusPacketHandlerResult::RequestHandled); } else {
return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
ecss_tc_and_token.token,
));
} }
Ok(PusPacketHandlerResult::CustomSubservice( Ok(PusPacketHandlerResult::RequestHandled)
tc.subservice(),
token,
))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::pool::{LocalPool, PoolCfg, SharedPool}; use crate::pus::tests::{
use crate::pus::test::PusService17TestHandler; PusServiceHandlerWithSharedStoreCommon, PusServiceHandlerWithVecCommon, PusTestHarness,
use crate::pus::verification::{ SimplePusPacketHandler, TEST_APID,
RequestId, StdVerifReporterWithSender, VerificationReporterCfg,
}; };
use crate::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender, PusServiceHandler}; use crate::pus::verification::RequestId;
use crate::tmtc::tm_helper::SharedTmStore; use crate::pus::verification::{TcStateAccepted, VerificationToken};
use crate::pus::{
EcssTcInSharedStoreConverter, EcssTcInVecConverter, PusPacketHandlerResult,
PusPacketHandlingError,
};
use delegate::delegate;
use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader}; use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader};
use spacepackets::ecss::tm::PusTmReader; use spacepackets::ecss::tm::PusTmReader;
use spacepackets::ecss::{PusPacket, SerializablePusPacket}; use spacepackets::ecss::PusPacket;
use spacepackets::{SequenceFlags, SpHeader}; use spacepackets::{SequenceFlags, SpHeader};
use std::boxed::Box;
use std::sync::{mpsc, RwLock};
use std::vec;
const TEST_APID: u16 = 0x101; use super::PusService17TestHandler;
#[test] struct Pus17HandlerWithStoreTester {
fn test_basic_ping_processing() { common: PusServiceHandlerWithSharedStoreCommon,
let mut pus_buf: [u8; 64] = [0; 64]; handler: PusService17TestHandler<EcssTcInSharedStoreConverter>,
let pool_cfg = PoolCfg::new(vec![(16, 16), (8, 32), (4, 64)]); }
let tc_pool = LocalPool::new(pool_cfg.clone());
let tm_pool = LocalPool::new(pool_cfg); impl Pus17HandlerWithStoreTester {
let tc_pool_shared = SharedPool::new(RwLock::new(Box::new(tc_pool))); pub fn new() -> Self {
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
let tm_pool_shared = shared_tm_store.clone_backing_pool(); let pus_17_handler = PusService17TestHandler::new(srv_handler);
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); Self {
let (tm_tx, tm_rx) = mpsc::channel(); common,
let verif_sender = handler: pus_17_handler,
MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store.clone(), tm_tx.clone()); }
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); }
let mut verification_handler = }
StdVerifReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
let test_srv_tm_sender = MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_store, tm_tx); impl PusTestHarness for Pus17HandlerWithStoreTester {
let test_srv_tc_receiver = MpscTcInStoreReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx); delegate! {
let mut pus_17_handler = PusService17TestHandler::new( to self.common {
Box::new(test_srv_tc_receiver), fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
tc_pool_shared.clone(), fn read_next_tm(&mut self) -> PusTmReader<'_>;
Box::new(test_srv_tm_sender), fn check_no_tm_available(&self) -> bool;
TEST_APID, fn check_next_verification_tm(
verification_handler.clone(), &self,
); subservice: u8,
expected_request_id: RequestId
);
}
}
}
impl SimplePusPacketHandler for Pus17HandlerWithStoreTester {
delegate! {
to self.handler {
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
}
}
}
struct Pus17HandlerWithVecTester {
common: PusServiceHandlerWithVecCommon,
handler: PusService17TestHandler<EcssTcInVecConverter>,
}
impl Pus17HandlerWithVecTester {
pub fn new() -> Self {
let (common, srv_handler) = PusServiceHandlerWithVecCommon::new();
Self {
common,
handler: PusService17TestHandler::new(srv_handler),
}
}
}
impl PusTestHarness for Pus17HandlerWithVecTester {
delegate! {
to self.common {
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
fn read_next_tm(&mut self) -> PusTmReader<'_>;
fn check_no_tm_available(&self) -> bool;
fn check_next_verification_tm(
&self,
subservice: u8,
expected_request_id: RequestId,
);
}
}
}
impl SimplePusPacketHandler for Pus17HandlerWithVecTester {
delegate! {
to self.handler {
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
}
}
}
fn ping_test(test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler)) {
// Create a ping TC, verify acceptance. // Create a ping TC, verify acceptance.
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(17, 1); let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, None, true); let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
let token = verification_handler.add_tc(&ping_tc); let token = test_harness.send_tc(&ping_tc);
let token = verification_handler let request_id = token.req_id();
.acceptance_success(token, None) let result = test_harness.handle_one_tc();
.unwrap();
let tc_size = ping_tc.write_to_bytes(&mut pus_buf).unwrap();
let mut tc_pool = tc_pool_shared.write().unwrap();
let addr = tc_pool.add(&pus_buf[..tc_size]).unwrap();
drop(tc_pool);
// Send accepted TC to test service handler.
test_srv_tc_tx.send((addr, token.into())).unwrap();
let result = pus_17_handler.handle_next_packet();
assert!(result.is_ok()); assert!(result.is_ok());
// We should see 4 replies in the TM queue now: Acceptance TM, Start TM, ping reply and // We should see 4 replies in the TM queue now: Acceptance TM, Start TM, ping reply and
// Completion TM // Completion TM
let mut next_msg = tm_rx.try_recv();
assert!(next_msg.is_ok());
let mut tm_addr = next_msg.unwrap();
let tm_pool = tm_pool_shared.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
let (tm, _) = PusTmReader::new(&tm_raw, 0).unwrap();
assert_eq!(tm.service(), 1);
assert_eq!(tm.subservice(), 1);
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
assert_eq!(req_id, token.req_id());
// Acceptance TM // Acceptance TM
next_msg = tm_rx.try_recv(); test_harness.check_next_verification_tm(1, request_id);
assert!(next_msg.is_ok());
tm_addr = next_msg.unwrap(); // Start TM
let tm_raw = tm_pool.read(&tm_addr).unwrap(); test_harness.check_next_verification_tm(3, request_id);
// Is generated with CDS short timestamp.
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap();
assert_eq!(tm.service(), 1);
assert_eq!(tm.subservice(), 3);
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
assert_eq!(req_id, token.req_id());
// Ping reply // Ping reply
next_msg = tm_rx.try_recv(); let tm = test_harness.read_next_tm();
assert!(next_msg.is_ok());
tm_addr = next_msg.unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
// Is generated with CDS short timestamp.
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap();
assert_eq!(tm.service(), 17); assert_eq!(tm.service(), 17);
assert_eq!(tm.subservice(), 2); assert_eq!(tm.subservice(), 2);
assert!(tm.user_data().is_empty()); assert!(tm.user_data().is_empty());
// TM completion // TM completion
next_msg = tm_rx.try_recv(); test_harness.check_next_verification_tm(7, request_id);
assert!(next_msg.is_ok()); }
tm_addr = next_msg.unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap(); #[test]
// Is generated with CDS short timestamp. fn test_basic_ping_processing_using_store() {
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap(); let mut test_harness = Pus17HandlerWithStoreTester::new();
assert_eq!(tm.service(), 1); ping_test(&mut test_harness);
assert_eq!(tm.subservice(), 7); }
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
assert_eq!(req_id, token.req_id()); #[test]
fn test_basic_ping_processing_using_vec() {
let mut test_harness = Pus17HandlerWithVecTester::new();
ping_test(&mut test_harness);
}
#[test]
fn test_empty_tc_queue() {
let mut test_harness = Pus17HandlerWithStoreTester::new();
let result = test_harness.handle_one_tc();
assert!(result.is_ok());
let result = result.unwrap();
if let PusPacketHandlerResult::Empty = result {
} else {
panic!("unexpected result type {result:?}")
}
}
#[test]
fn test_sending_unsupported_service() {
let mut test_harness = Pus17HandlerWithStoreTester::new();
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(3, 1);
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
test_harness.send_tc(&ping_tc);
let result = test_harness.handle_one_tc();
assert!(result.is_err());
let error = result.unwrap_err();
if let PusPacketHandlingError::WrongService(num) = error {
assert_eq!(num, 3);
} else {
panic!("unexpected error type {error}")
}
}
#[test]
fn test_sending_custom_subservice() {
let mut test_harness = Pus17HandlerWithStoreTester::new();
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(17, 200);
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
test_harness.send_tc(&ping_tc);
let result = test_harness.handle_one_tc();
assert!(result.is_ok());
let result = result.unwrap();
if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result {
assert_eq!(subservice, 200);
} else {
panic!("unexpected result type {result:?}")
}
} }
} }

View File

@@ -15,7 +15,7 @@
//! ``` //! ```
//! use std::sync::{Arc, mpsc, RwLock}; //! use std::sync::{Arc, mpsc, RwLock};
//! use std::time::Duration; //! use std::time::Duration;
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider, SharedPool}; //! use satrs_core::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig};
//! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender}; //! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
//! use satrs_core::seq_count::SeqCountProviderSimple; //! use satrs_core::seq_count::SeqCountProviderSimple;
//! use satrs_core::pus::MpscTmInStoreSender; //! use satrs_core::pus::MpscTmInStoreSender;
@@ -28,9 +28,9 @@
//! const EMPTY_STAMP: [u8; 7] = [0; 7]; //! const EMPTY_STAMP: [u8; 7] = [0; 7];
//! const TEST_APID: u16 = 0x02; //! const TEST_APID: u16 = 0x02;
//! //!
//! let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); //! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
//! let tm_pool = LocalPool::new(pool_cfg.clone()); //! let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
//! let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); //! let shared_tm_store = SharedTmStore::new(tm_pool);
//! let tm_store = shared_tm_store.clone_backing_pool(); //! let tm_store = shared_tm_store.clone_backing_pool();
//! let (verif_tx, verif_rx) = mpsc::channel(); //! let (verif_tx, verif_rx) = mpsc::channel();
//! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx); //! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx);
@@ -39,7 +39,7 @@
//! //!
//! let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); //! let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
//! let tc_header = PusTcSecondaryHeader::new_simple(17, 1); //! let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
//! let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); //! let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
//! let init_token = reporter.add_tc(&pus_tc_0); //! let init_token = reporter.add_tc(&pus_tc_0);
//! //!
//! // Complete success sequence for a telecommand //! // Complete success sequence for a telecommand
@@ -87,7 +87,7 @@ use delegate::delegate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use spacepackets::ecss::tc::IsPusTelecommand; use spacepackets::ecss::tc::IsPusTelecommand;
use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use spacepackets::ecss::{EcssEnumeration, PusError, SerializablePusPacket}; use spacepackets::ecss::{EcssEnumeration, PusError, WritablePusPacket};
use spacepackets::{CcsdsPacket, PacketId, PacketSequenceCtrl}; use spacepackets::{CcsdsPacket, PacketId, PacketSequenceCtrl};
use spacepackets::{SpHeader, MAX_APID}; use spacepackets::{SpHeader, MAX_APID};
@@ -208,6 +208,8 @@ impl WasAtLeastAccepted for TcStateAccepted {}
impl WasAtLeastAccepted for TcStateStarted {} impl WasAtLeastAccepted for TcStateStarted {}
impl WasAtLeastAccepted for TcStateCompleted {} impl WasAtLeastAccepted for TcStateCompleted {}
/// Token wrapper to model all possible verification tokens. These tokens are used to
/// enforce the correct order for the verification steps when doing verification reporting.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TcStateToken { pub enum TcStateToken {
None(VerificationToken<TcStateNone>), None(VerificationToken<TcStateNone>),
@@ -353,7 +355,7 @@ impl<'src_data, State, SuccessOrFailure> VerificationSendable<'src_data, State,
} }
pub fn len_packed(&self) -> usize { pub fn len_packed(&self) -> usize {
self.pus_tm.as_ref().unwrap().len_packed() self.pus_tm.as_ref().unwrap().len_written()
} }
pub fn pus_tm(&self) -> &PusTmCreator<'src_data> { pub fn pus_tm(&self) -> &PusTmCreator<'src_data> {
@@ -877,7 +879,7 @@ impl VerificationReporterCore {
PusTmCreator::new( PusTmCreator::new(
sp_header, sp_header,
tm_sec_header, tm_sec_header,
Some(&src_data_buf[0..source_data_len]), &src_data_buf[0..source_data_len],
true, true,
) )
} }
@@ -1323,7 +1325,7 @@ mod std_mod {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::pool::{LocalPool, PoolCfg}; use crate::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig};
use crate::pus::tests::CommonTmInfo; use crate::pus::tests::CommonTmInfo;
use crate::pus::verification::{ use crate::pus::verification::{
EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone, EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone,
@@ -1438,6 +1440,7 @@ mod tests {
fn base_tc_init(app_data: Option<&[u8]>) -> (PusTcCreator, RequestId) { fn base_tc_init(app_data: Option<&[u8]>) -> (PusTcCreator, RequestId) {
let mut sph = SpHeader::tc_unseg(TEST_APID, 0x34, 0).unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 0x34, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(17, 1); let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
let app_data = app_data.unwrap_or(&[]);
let pus_tc = PusTcCreator::new(&mut sph, tc_header, app_data, true); let pus_tc = PusTcCreator::new(&mut sph, tc_header, app_data, true);
let req_id = RequestId::new(&pus_tc); let req_id = RequestId::new(&pus_tc);
(pus_tc, req_id) (pus_tc, req_id)
@@ -1446,12 +1449,11 @@ mod tests {
fn base_init(api_sel: bool) -> (TestBase<'static>, VerificationToken<TcStateNone>) { fn base_init(api_sel: bool) -> (TestBase<'static>, VerificationToken<TcStateNone>) {
let mut reporter = base_reporter(); let mut reporter = base_reporter();
let (tc, req_id) = base_tc_init(None); let (tc, req_id) = base_tc_init(None);
let init_tok; let init_tok = if api_sel {
if api_sel { reporter.add_tc_with_req_id(req_id)
init_tok = reporter.add_tc_with_req_id(req_id);
} else { } else {
init_tok = reporter.add_tc(&tc); reporter.add_tc(&tc)
} };
(TestBase { vr: reporter, tc }, init_tok) (TestBase { vr: reporter, tc }, init_tok)
} }
@@ -1474,7 +1476,7 @@ mod tests {
time_stamp: EMPTY_STAMP, time_stamp: EMPTY_STAMP,
}, },
additional_data: None, additional_data: None,
req_id: req_id.clone(), req_id: *req_id,
}; };
let mut service_queue = sender.service_queue.borrow_mut(); let mut service_queue = sender.service_queue.borrow_mut();
assert_eq!(service_queue.len(), 1); assert_eq!(service_queue.len(), 1);
@@ -1484,9 +1486,8 @@ mod tests {
#[test] #[test]
fn test_mpsc_verif_send_sync() { fn test_mpsc_verif_send_sync() {
let pool = LocalPool::new(PoolCfg::new(vec![(8, 8)])); let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)]));
let tm_store = Box::new(pool); let shared_tm_store = SharedTmStore::new(pool);
let shared_tm_store = SharedTmStore::new(tm_store);
let (tx, _) = mpsc::channel(); let (tx, _) = mpsc::channel();
let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx); let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx);
is_send(&mpsc_verif_sender); is_send(&mpsc_verif_sender);
@@ -1504,7 +1505,7 @@ mod tests {
fn test_basic_acceptance_success() { fn test_basic_acceptance_success() {
let (b, tok) = base_init(false); let (b, tok) = base_init(false);
let mut sender = TestSender::default(); let mut sender = TestSender::default();
b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP)) b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
.expect("Sending acceptance success failed"); .expect("Sending acceptance success failed");
acceptance_check(&mut sender, &tok.req_id); acceptance_check(&mut sender, &tok.req_id);
} }
@@ -1604,7 +1605,7 @@ mod tests {
#[test] #[test]
fn test_basic_acceptance_failure_with_fail_data() { fn test_basic_acceptance_failure_with_fail_data() {
let (b, tok) = base_init(false); let (b, tok) = base_init(false);
let mut sender = TestSender::default(); let sender = TestSender::default();
let fail_code = EcssEnumU8::new(10); let fail_code = EcssEnumU8::new(10);
let fail_data = EcssEnumU32::new(12); let fail_data = EcssEnumU32::new(12);
let mut fail_data_raw = [0; 4]; let mut fail_data_raw = [0; 4];
@@ -1614,7 +1615,7 @@ mod tests {
&fail_code, &fail_code,
Some(fail_data_raw.as_slice()), Some(fail_data_raw.as_slice()),
); );
b.vr.acceptance_failure(tok, &mut sender, fail_params) b.vr.acceptance_failure(tok, &sender, fail_params)
.expect("Sending acceptance success failed"); .expect("Sending acceptance success failed");
let cmp_info = TmInfo { let cmp_info = TmInfo {
common: CommonTmInfo { common: CommonTmInfo {
@@ -1783,8 +1784,7 @@ mod tests {
.rep() .rep()
.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0])) .start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed"); .expect("Sending start success failed");
let mut empty = b b.rep()
.rep()
.step_success( .step_success(
&started_token, &started_token,
&mut sender, &mut sender,
@@ -1792,16 +1792,13 @@ mod tests {
EcssEnumU8::new(0), EcssEnumU8::new(0),
) )
.expect("Sending step 0 success failed"); .expect("Sending step 0 success failed");
assert_eq!(empty, ()); b.vr.step_success(
empty = &started_token,
b.vr.step_success( &mut sender,
&started_token, Some(&EMPTY_STAMP),
&mut sender, EcssEnumU8::new(1),
Some(&EMPTY_STAMP), )
EcssEnumU8::new(1), .expect("Sending step 1 success failed");
)
.expect("Sending step 1 success failed");
assert_eq!(empty, ());
assert_eq!(sender.service_queue.borrow().len(), 4); assert_eq!(sender.service_queue.borrow().len(), 4);
step_success_check(&mut sender, tok.req_id); step_success_check(&mut sender, tok.req_id);
} }
@@ -1817,16 +1814,12 @@ mod tests {
.helper .helper
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0])) .start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed"); .expect("Sending start success failed");
let mut empty = b b.helper
.helper
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(0)) .step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(0))
.expect("Sending step 0 success failed"); .expect("Sending step 0 success failed");
assert_eq!(empty, ()); b.helper
empty = b
.helper
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(1)) .step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(1))
.expect("Sending step 1 success failed"); .expect("Sending step 1 success failed");
assert_eq!(empty, ());
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap(); let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
assert_eq!(sender.service_queue.borrow().len(), 4); assert_eq!(sender.service_queue.borrow().len(), 4);
step_success_check(sender, tok.req_id); step_success_check(sender, tok.req_id);
@@ -2121,10 +2114,8 @@ mod tests {
let started_token = let started_token =
b.vr.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0])) b.vr.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed"); .expect("Sending start success failed");
let empty = b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP))
b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP)) .expect("Sending completion success failed");
.expect("Sending completion success failed");
assert_eq!(empty, ());
completion_success_check(&mut sender, tok.req_id); completion_success_check(&mut sender, tok.req_id);
} }
@@ -2139,11 +2130,9 @@ mod tests {
.helper .helper
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0])) .start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed"); .expect("Sending start success failed");
let empty = b b.helper
.helper
.completion_success(started_token, Some(&EMPTY_STAMP)) .completion_success(started_token, Some(&EMPTY_STAMP))
.expect("Sending completion success failed"); .expect("Sending completion success failed");
assert_eq!(empty, ());
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap(); let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
completion_success_check(sender, tok.req_id); completion_success_check(sender, tok.req_id);
} }
@@ -2151,8 +2140,8 @@ mod tests {
#[test] #[test]
// TODO: maybe a bit more extensive testing, all I have time for right now // TODO: maybe a bit more extensive testing, all I have time for right now
fn test_seq_count_increment() { fn test_seq_count_increment() {
let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let tm_pool = Box::new(LocalPool::new(pool_cfg.clone())); let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
let shared_tm_store = SharedTmStore::new(tm_pool); let shared_tm_store = SharedTmStore::new(tm_pool);
let shared_tm_pool = shared_tm_store.clone_backing_pool(); let shared_tm_pool = shared_tm_store.clone_backing_pool();
let (verif_tx, verif_rx) = mpsc::channel(); let (verif_tx, verif_rx) = mpsc::channel();
@@ -2162,7 +2151,7 @@ mod tests {
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(17, 1); let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
let init_token = reporter.add_tc(&pus_tc_0); let init_token = reporter.add_tc(&pus_tc_0);
// Complete success sequence for a telecommand // Complete success sequence for a telecommand

View File

@@ -21,7 +21,7 @@
//! use satrs_core::tmtc::ccsds_distrib::{CcsdsPacketHandler, CcsdsDistributor}; //! use satrs_core::tmtc::ccsds_distrib::{CcsdsPacketHandler, CcsdsDistributor};
//! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; //! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
//! use spacepackets::{CcsdsPacket, SpHeader}; //! use spacepackets::{CcsdsPacket, SpHeader};
//! use spacepackets::ecss::SerializablePusPacket; //! use spacepackets::ecss::WritablePusPacket;
//! use spacepackets::ecss::tc::{PusTc, PusTcCreator}; //! use spacepackets::ecss::tc::{PusTc, PusTcCreator};
//! //!
//! #[derive (Default)] //! #[derive (Default)]
@@ -226,7 +226,7 @@ pub(crate) mod tests {
use super::*; use super::*;
use crate::tmtc::ccsds_distrib::{CcsdsDistributor, CcsdsPacketHandler}; use crate::tmtc::ccsds_distrib::{CcsdsDistributor, CcsdsPacketHandler};
use spacepackets::ecss::tc::PusTcCreator; use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::SerializablePusPacket; use spacepackets::ecss::WritablePusPacket;
use spacepackets::CcsdsPacket; use spacepackets::CcsdsPacket;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -244,9 +244,10 @@ pub(crate) mod tests {
&buf[0..size] &buf[0..size]
} }
type SharedPacketQueue = Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>;
pub struct BasicApidHandlerSharedQueue { pub struct BasicApidHandlerSharedQueue {
pub known_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>, pub known_packet_queue: SharedPacketQueue,
pub unknown_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>, pub unknown_packet_queue: SharedPacketQueue,
} }
#[derive(Default)] #[derive(Default)]
@@ -268,11 +269,11 @@ pub(crate) mod tests {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
let mut vec = Vec::new(); let mut vec = Vec::new();
vec.extend_from_slice(tc_raw); vec.extend_from_slice(tc_raw);
Ok(self self.known_packet_queue
.known_packet_queue
.lock() .lock()
.unwrap() .unwrap()
.push_back((sp_header.apid(), vec))) .push_back((sp_header.apid(), vec));
Ok(())
} }
fn handle_unknown_apid( fn handle_unknown_apid(
@@ -282,11 +283,11 @@ pub(crate) mod tests {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
let mut vec = Vec::new(); let mut vec = Vec::new();
vec.extend_from_slice(tc_raw); vec.extend_from_slice(tc_raw);
Ok(self self.unknown_packet_queue
.unknown_packet_queue
.lock() .lock()
.unwrap() .unwrap()
.push_back((sp_header.apid(), vec))) .push_back((sp_header.apid(), vec));
Ok(())
} }
} }

View File

@@ -7,9 +7,7 @@
//! routing without the overhead and complication of using message queues. However, it also requires //! routing without the overhead and complication of using message queues. However, it also requires
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use downcast_rs::{impl_downcast, Downcast}; use downcast_rs::{impl_downcast, Downcast};
#[cfg(feature = "serde")] use spacepackets::SpHeader;
use serde::{Deserialize, Serialize};
use spacepackets::{ByteConversionError, SpHeader};
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
pub mod ccsds_distrib; pub mod ccsds_distrib;
@@ -24,40 +22,6 @@ pub use pus_distrib::{PusDistributor, PusServiceProvider};
pub type TargetId = u32; pub type TargetId = u32;
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AddressableId {
pub target_id: TargetId,
pub unique_id: u32,
}
impl AddressableId {
pub fn from_raw_be(buf: &[u8]) -> Result<Self, ByteConversionError> {
if buf.len() < 8 {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: 8,
});
}
Ok(Self {
target_id: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
unique_id: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
})
}
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.to_be_bytes());
buf[4..8].copy_from_slice(&self.unique_id.to_be_bytes());
Ok(8)
}
}
/// Generic trait for object which can receive any telecommands in form of a raw bytestream, with /// Generic trait for object which can receive any telecommands in form of a raw bytestream, with
/// no assumptions about the received protocol. /// no assumptions about the received protocol.
/// ///

View File

@@ -18,7 +18,7 @@
//! # Example //! # Example
//! //!
//! ```rust //! ```rust
//! use spacepackets::ecss::SerializablePusPacket; //! use spacepackets::ecss::WritablePusPacket;
//! use satrs_core::tmtc::pus_distrib::{PusDistributor, PusServiceProvider}; //! use satrs_core::tmtc::pus_distrib::{PusDistributor, PusServiceProvider};
//! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; //! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
//! use spacepackets::SpHeader; //! use spacepackets::SpHeader;

View File

@@ -8,31 +8,36 @@ pub use std_mod::*;
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub mod std_mod { pub mod std_mod {
use crate::pool::{ShareablePoolProvider, SharedPool, StoreAddr}; use crate::pool::{
PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr,
};
use crate::pus::EcssTmtcError; use crate::pus::EcssTmtcError;
use spacepackets::ecss::tm::PusTmCreator; use spacepackets::ecss::tm::PusTmCreator;
use spacepackets::ecss::SerializablePusPacket; use spacepackets::ecss::WritablePusPacket;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
#[derive(Clone)] #[derive(Clone)]
pub struct SharedTmStore { pub struct SharedTmStore {
pool: SharedPool, pub shared_pool: SharedStaticMemoryPool,
} }
impl SharedTmStore { impl SharedTmStore {
pub fn new(backing_pool: ShareablePoolProvider) -> Self { pub fn new(shared_pool: StaticMemoryPool) -> Self {
Self { Self {
pool: Arc::new(RwLock::new(backing_pool)), shared_pool: Arc::new(RwLock::new(shared_pool)),
} }
} }
pub fn clone_backing_pool(&self) -> SharedPool { pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.pool.clone() self.shared_pool.clone()
} }
pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result<StoreAddr, EcssTmtcError> { pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result<StoreAddr, EcssTmtcError> {
let mut pg = self.pool.write().map_err(|_| EcssTmtcError::StoreLock)?; let mut pg = self
let (addr, buf) = pg.free_element(pus_tm.len_packed())?; .shared_pool
.write()
.map_err(|_| EcssTmtcError::StoreLock)?;
let (addr, buf) = pg.free_element(pus_tm.len_written())?;
pus_tm pus_tm
.write_to_bytes(buf) .write_to_bytes(buf)
.expect("writing PUS TM to store failed"); .expect("writing PUS TM to store failed");
@@ -59,7 +64,7 @@ impl PusTmWithCdsShortHelper {
&'a mut self, &'a mut self,
service: u8, service: u8,
subservice: u8, subservice: u8,
source_data: Option<&'a [u8]>, source_data: &'a [u8],
seq_count: u16, seq_count: u16,
) -> PusTmCreator { ) -> PusTmCreator {
let time_stamp = TimeProvider::from_now_with_u16_days().unwrap(); let time_stamp = TimeProvider::from_now_with_u16_days().unwrap();
@@ -71,7 +76,7 @@ impl PusTmWithCdsShortHelper {
&'a mut self, &'a mut self,
service: u8, service: u8,
subservice: u8, subservice: u8,
source_data: Option<&'a [u8]>, source_data: &'a [u8],
stamper: &TimeProvider, stamper: &TimeProvider,
seq_count: u16, seq_count: u16,
) -> PusTmCreator { ) -> PusTmCreator {
@@ -83,7 +88,7 @@ impl PusTmWithCdsShortHelper {
&'a self, &'a self,
service: u8, service: u8,
subservice: u8, subservice: u8,
source_data: Option<&'a [u8]>, source_data: &'a [u8],
seq_count: u16, seq_count: u16,
) -> PusTmCreator { ) -> PusTmCreator {
let mut reply_header = SpHeader::tm_unseg(self.apid, seq_count, 0).unwrap(); let mut reply_header = SpHeader::tm_unseg(self.apid, seq_count, 0).unwrap();
@@ -91,3 +96,33 @@ impl PusTmWithCdsShortHelper {
PusTmCreator::new(&mut reply_header, tc_header, source_data, true) PusTmCreator::new(&mut reply_header, tc_header, source_data, true)
} }
} }
#[cfg(test)]
mod tests {
use spacepackets::{ecss::PusPacket, time::cds::TimeProvider, CcsdsPacket};
use super::PusTmWithCdsShortHelper;
#[test]
fn test_helper_with_stamper() {
let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123);
let stamper = TimeProvider::new_with_u16_days(0, 0);
let tm = pus_tm_helper.create_pus_tm_with_stamper(17, 1, &[1, 2, 3, 4], &stamper, 25);
assert_eq!(tm.service(), 17);
assert_eq!(tm.subservice(), 1);
assert_eq!(tm.user_data(), &[1, 2, 3, 4]);
assert_eq!(tm.seq_count(), 25);
assert_eq!(tm.timestamp(), [64, 0, 0, 0, 0, 0, 0])
}
#[test]
fn test_helper_from_now() {
let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123);
let tm = pus_tm_helper.create_pus_tm_timestamp_now(17, 1, &[1, 2, 3, 4], 25);
assert_eq!(tm.service(), 17);
assert_eq!(tm.subservice(), 1);
assert_eq!(tm.user_data(), &[1, 2, 3, 4]);
assert_eq!(tm.seq_count(), 25);
assert_eq!(tm.timestamp().len(), 7);
}
}

View File

@@ -1,7 +1,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use core::mem::size_of; use core::mem::size_of;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use spacepackets::ecss::{Ptc, RealPfc, UnsignedPfc}; use spacepackets::ecss::{PfcReal, PfcUnsigned, Ptc};
use spacepackets::time::cds::TimeProvider; use spacepackets::time::cds::TimeProvider;
use spacepackets::time::{CcsdsTimeProvider, TimeWriter}; use spacepackets::time::{CcsdsTimeProvider, TimeWriter};
@@ -64,7 +64,7 @@ impl TestMgmHkWithIndividualValidity {
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = Ptc::Real as u8; buf[curr_idx] = Ptc::Real as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = RealPfc::Float as u8; buf[curr_idx] = PfcReal::Float as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.val.to_be_bytes()); buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.val.to_be_bytes());
curr_idx += size_of::<f32>(); curr_idx += size_of::<f32>();
@@ -75,7 +75,7 @@ impl TestMgmHkWithIndividualValidity {
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = Ptc::UnsignedInt as u8; buf[curr_idx] = Ptc::UnsignedInt as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = UnsignedPfc::TwoBytes as u8; buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = 3; buf[curr_idx] = 3;
curr_idx += 1; curr_idx += 1;
@@ -100,7 +100,7 @@ impl TestMgmHkWithGroupValidity {
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = Ptc::Real as u8; buf[curr_idx] = Ptc::Real as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = RealPfc::Float as u8; buf[curr_idx] = PfcReal::Float as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.to_be_bytes()); buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.to_be_bytes());
curr_idx += size_of::<f32>(); curr_idx += size_of::<f32>();
@@ -109,7 +109,7 @@ impl TestMgmHkWithGroupValidity {
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = Ptc::UnsignedInt as u8; buf[curr_idx] = Ptc::UnsignedInt as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = UnsignedPfc::TwoBytes as u8; buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
curr_idx += 1; curr_idx += 1;
buf[curr_idx] = 3; buf[curr_idx] = 3;
for val in self.mgm_vals { for val in self.mgm_vals {

View File

@@ -1,4 +1,6 @@
use satrs_core::pool::{LocalPool, PoolCfg, PoolGuard, PoolProvider, StoreAddr}; use satrs_core::pool::{
PoolGuard, PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig, StoreAddr,
};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::{Receiver, Sender};
@@ -9,8 +11,8 @@ const DUMMY_DATA: [u8; 4] = [0, 1, 2, 3];
#[test] #[test]
fn threaded_usage() { fn threaded_usage() {
let pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]); let pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
let shared_pool = Arc::new(RwLock::new(LocalPool::new(pool_cfg))); let shared_pool = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
let shared_clone = shared_pool.clone(); let shared_clone = shared_pool.clone();
let (tx, rx): (Sender<StoreAddr>, Receiver<StoreAddr>) = mpsc::channel(); let (tx, rx): (Sender<StoreAddr>, Receiver<StoreAddr>) = mpsc::channel();
let jh0 = thread::spawn(move || { let jh0 = thread::spawn(move || {

View File

@@ -1,7 +1,10 @@
#[cfg(feature = "crossbeam")] #[cfg(feature = "crossbeam")]
pub mod crossbeam_test { pub mod crossbeam_test {
use hashbrown::HashMap; use hashbrown::HashMap;
use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider}; use satrs_core::pool::{
PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, StaticMemoryPool,
StaticPoolConfig,
};
use satrs_core::pus::verification::{ use satrs_core::pus::verification::{
FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender, FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender,
}; };
@@ -9,7 +12,7 @@ pub mod crossbeam_test {
use satrs_core::tmtc::tm_helper::SharedTmStore; use satrs_core::tmtc::tm_helper::SharedTmStore;
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader}; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
use spacepackets::ecss::tm::PusTmReader; use spacepackets::ecss::tm::PusTmReader;
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, SerializablePusPacket}; use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, WritablePusPacket};
use spacepackets::SpHeader; use spacepackets::SpHeader;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
@@ -33,9 +36,9 @@ pub mod crossbeam_test {
// each reporter have an own sequence count provider. // each reporter have an own sequence count provider.
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
// Shared pool object to store the verification PUS telemetry // Shared pool object to store the verification PUS telemetry
let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let shared_tm_store = SharedTmStore::new(Box::new(LocalPool::new(pool_cfg.clone()))); let shared_tm_store = SharedTmStore::new(StaticMemoryPool::new(pool_cfg.clone()));
let shared_tc_pool_0 = Arc::new(RwLock::new(LocalPool::new(pool_cfg))); let shared_tc_pool_0 = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
let shared_tc_pool_1 = shared_tc_pool_0.clone(); let shared_tc_pool_1 = shared_tc_pool_0.clone();
let (tx, rx) = crossbeam_channel::bounded(10); let (tx, rx) = crossbeam_channel::bounded(10);
let sender = let sender =
@@ -54,17 +57,17 @@ pub mod crossbeam_test {
let mut tc_guard = shared_tc_pool_0.write().unwrap(); let mut tc_guard = shared_tc_pool_0.write().unwrap();
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(17, 1); let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
req_id_0 = RequestId::new(&pus_tc_0); req_id_0 = RequestId::new(&pus_tc_0);
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap(); let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_0.write_to_bytes(&mut buf).unwrap(); pus_tc_0.write_to_bytes(buf).unwrap();
tx_tc_0.send(addr).unwrap(); tx_tc_0.send(addr).unwrap();
let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(5, 1); let tc_header = PusTcSecondaryHeader::new_simple(5, 1);
let pus_tc_1 = PusTcCreator::new(&mut sph, tc_header, None, true); let pus_tc_1 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
req_id_1 = RequestId::new(&pus_tc_1); req_id_1 = RequestId::new(&pus_tc_1);
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap(); let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_1.write_to_bytes(&mut buf).unwrap(); pus_tc_1.write_to_bytes(buf).unwrap();
tx_tc_1.send(addr).unwrap(); tx_tc_1.send(addr).unwrap();
} }
let verif_sender_0 = thread::spawn(move || { let verif_sender_0 = thread::spawn(move || {
@@ -81,16 +84,14 @@ pub mod crossbeam_test {
tc_buf[0..tc_len].copy_from_slice(buf); tc_buf[0..tc_len].copy_from_slice(buf);
} }
let (_tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap(); let (_tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
let accepted_token;
let token = reporter_with_sender_0.add_tc_with_req_id(req_id_0); let token = reporter_with_sender_0.add_tc_with_req_id(req_id_0);
accepted_token = reporter_with_sender_0 let accepted_token = reporter_with_sender_0
.acceptance_success(token, Some(&FIXED_STAMP)) .acceptance_success(token, Some(&FIXED_STAMP))
.expect("Acceptance success failed"); .expect("Acceptance success failed");
// Do some start handling here // Do some start handling here
let started_token; let started_token = reporter_with_sender_0
started_token = reporter_with_sender_0
.start_success(accepted_token, Some(&FIXED_STAMP)) .start_success(accepted_token, Some(&FIXED_STAMP))
.expect("Start success failed"); .expect("Start success failed");
// Do some step handling here // Do some step handling here
@@ -158,8 +159,7 @@ pub mod crossbeam_test {
RequestId::from_bytes(&pus_tm.source_data()[0..RequestId::SIZE_AS_BYTES]) RequestId::from_bytes(&pus_tm.source_data()[0..RequestId::SIZE_AS_BYTES])
.expect("reading request ID from PUS TM source data failed"); .expect("reading request ID from PUS TM source data failed");
if !verif_map.contains_key(&req_id) { if !verif_map.contains_key(&req_id) {
let mut content = Vec::new(); let content = vec![pus_tm.subservice()];
content.push(pus_tm.subservice());
verif_map.insert(req_id, content); verif_map.insert(req_id, content);
} else { } else {
let content = verif_map.get_mut(&req_id).unwrap(); let content = verif_map.get_mut(&req_id).unwrap();

View File

@@ -28,7 +28,7 @@ use satrs_core::{
tmtc::{ReceivesTcCore, TmPacketSourceCore}, tmtc::{ReceivesTcCore, TmPacketSourceCore},
}; };
use spacepackets::{ use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket}, ecss::{tc::PusTcCreator, WritablePusPacket},
PacketId, SpHeader, PacketId, SpHeader,
}; };
use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec};
@@ -94,8 +94,8 @@ fn test_cobs_server() {
tm_source.add_tm(&INVERTED_PACKET); tm_source.add_tm(&INVERTED_PACKET);
let mut tcp_server = TcpTmtcInCobsServer::new( let mut tcp_server = TcpTmtcInCobsServer::new(
ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024), ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source), tm_source,
Box::new(tc_receiver.clone()), tc_receiver.clone(),
) )
.expect("TCP server generation failed"); .expect("TCP server generation failed");
let dest_addr = tcp_server let dest_addr = tcp_server
@@ -176,8 +176,8 @@ fn test_ccsds_server() {
packet_id_lookup.insert(TEST_PACKET_ID_0); packet_id_lookup.insert(TEST_PACKET_ID_0);
let mut tcp_server = TcpSpacepacketsServer::new( let mut tcp_server = TcpSpacepacketsServer::new(
ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024), ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source), tm_source,
Box::new(tc_receiver.clone()), tc_receiver.clone(),
Box::new(packet_id_lookup), Box::new(packet_id_lookup),
) )
.expect("TCP server generation failed"); .expect("TCP server generation failed");

1
satrs-example/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/output.log

View File

@@ -17,11 +17,12 @@ zerocopy = "0.6"
csv = "1" csv = "1"
num_enum = "0.7" num_enum = "0.7"
thiserror = "1" thiserror = "1"
derive-new = "0.5"
[dependencies.satrs-core] [dependencies.satrs-core]
# version = "0.1.0-alpha.0" # version = "0.1.0-alpha.1"
path = "../satrs-core" path = "../satrs-core"
[dependencies.satrs-mib] [dependencies.satrs-mib]
# version = "0.1.0-alpha.1"
path = "../satrs-mib" path = "../satrs-mib"

View File

@@ -17,7 +17,7 @@ cargo run --bin simpleclient
This repository also contains a more complex client using the This repository also contains a more complex client using the
[Python tmtccmd](https://github.com/robamu-org/tmtccmd) module. [Python tmtccmd](https://github.com/robamu-org/tmtccmd) module.
# Using the tmtccmd Python client # <a id="tmtccmd"></a> Using the tmtccmd Python client
The python client requires a valid installation of the The python client requires a valid installation of the
[tmtccmd package](https://github.com/robamu-org/tmtccmd). [tmtccmd package](https://github.com/robamu-org/tmtccmd).
@@ -51,3 +51,26 @@ the `simpleclient`:
You can also simply call the script without any arguments to view a list of services (`-s` flag) You can also simply call the script without any arguments to view a list of services (`-s` flag)
and corresponding op codes (`-o` flag) for each service. and corresponding op codes (`-o` flag) for each service.
# Structure of the example project
The example project contains components which could also be expected to be part of a production
On-Board Software.
1. A UDP and TCP server to receive telecommands and poll telemetry from. This might be an optional
component for an OBSW which is only used during the development phase on ground. The TCP
server parses space packets by using the CCSDS space packet ID as the packet start delimiter.
2. A PUS service stack which exposes some functionality conformant with the ECSS PUS service. This
currently includes the following services:
- Service 1 for telecommand verification.
- Service 3 for housekeeping telemetry handling.
- Service 5 for management and downlink of on-board events.
- Service 8 for handling on-board actions.
- Service 11 for scheduling telecommands to be released at a specific time.
- Service 17 for test purposes (pings)
3. An event manager component which handles the event IPC mechanism.
4. A TC source component which demultiplexes and routes telecommands based on parameters like
packet APID or PUS service and subservice type.
5. A TM sink sink component which is the target of all sent telemetry and sends it to downlink
handlers like the UDP and TCP server.
6. An AOCS example task which can also process some PUS commands.

View File

@@ -6,3 +6,4 @@ __pycache__
!/.idea/runConfigurations !/.idea/runConfigurations
/seqcnt.txt /seqcnt.txt
/.tmtc-history.txt

View File

@@ -4,7 +4,11 @@ import dataclasses
import enum import enum
import struct import struct
from spacepackets.ecss.tc import PacketId, PacketType
EXAMPLE_PUS_APID = 0x02 EXAMPLE_PUS_APID = 0x02
EXAMPLE_PUS_PACKET_ID_TM = PacketId(PacketType.TM, True, EXAMPLE_PUS_APID)
TM_PACKET_IDS = [EXAMPLE_PUS_PACKET_ID_TM]
class EventSeverity(enum.IntEnum): class EventSeverity(enum.IntEnum):
@@ -40,10 +44,6 @@ class AcsHkIds(enum.IntEnum):
MGM_SET = 1 MGM_SET = 1
class HkOpCodes:
GENERATE_ONE_SHOT = ["0", "oneshot"]
def make_addressable_id(target_id: int, unique_id: int) -> bytes: def make_addressable_id(target_id: int, unique_id: int) -> bytes:
byte_string = bytearray(struct.pack("!I", target_id)) byte_string = bytearray(struct.pack("!I", target_id))
byte_string.extend(struct.pack("!I", unique_id)) byte_string.extend(struct.pack("!I", unique_id))

View File

@@ -4,6 +4,8 @@ import logging
import sys import sys
import time import time
from typing import Optional from typing import Optional
from prompt_toolkit.history import History
from prompt_toolkit.history import FileHistory
import tmtccmd import tmtccmd
from spacepackets.ecss import PusTelemetry, PusVerificator from spacepackets.ecss import PusTelemetry, PusVerificator
@@ -11,16 +13,16 @@ from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
from spacepackets.ccsds.time import CdsShortTimestamp from spacepackets.ccsds.time import CdsShortTimestamp
from tmtccmd import CcsdsTmtcBackend, TcHandlerBase, ProcedureParamsWrapper from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest from tmtccmd.core.base import BackendRequest
from tmtccmd.pus import VerificationWrapper from tmtccmd.pus import VerificationWrapper
from tmtccmd.tm import CcsdsTmHandler, SpecificApidHandlerBase from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
from tmtccmd.com import ComInterface from tmtccmd.com import ComInterface
from tmtccmd.config import ( from tmtccmd.config import (
CmdTreeNode,
default_json_path, default_json_path,
SetupParams, SetupParams,
HookBase, HookBase,
TmtcDefinitionWrapper,
params_to_procedure_conversion, params_to_procedure_conversion,
) )
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
@@ -30,7 +32,7 @@ from tmtccmd.logging.pus import (
RawTmtcTimedLogWrapper, RawTmtcTimedLogWrapper,
TimedLogWhen, TimedLogWhen,
) )
from tmtccmd.tc import ( from tmtccmd.tmtc import (
TcQueueEntryType, TcQueueEntryType,
ProcedureWrapper, ProcedureWrapper,
TcProcedureType, TcProcedureType,
@@ -39,13 +41,12 @@ from tmtccmd.tc import (
DefaultPusQueueHelper, DefaultPusQueueHelper,
QueueWrapper, QueueWrapper,
) )
from tmtccmd.util import FileSeqCountProvider, PusFileSeqCountProvider from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
from tmtccmd.util.obj_id import ObjectIdDictT from tmtccmd.util.obj_id import ObjectIdDictT
import pus_tc import pus_tc
import tc_definitions from common import EXAMPLE_PUS_APID, TM_PACKET_IDS, EventU32
from common import EXAMPLE_PUS_APID, EventU32
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@@ -54,25 +55,29 @@ class SatRsConfigHook(HookBase):
def __init__(self, json_cfg_path: str): def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path=json_cfg_path) super().__init__(json_cfg_path=json_cfg_path)
def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import ( from tmtccmd.config.com import (
create_com_interface_default, create_com_interface_default,
create_com_interface_cfg_default, create_com_interface_cfg_default,
) )
assert self.cfg_path is not None
cfg = create_com_interface_cfg_default( cfg = create_com_interface_cfg_default(
com_if_key=com_if_key, com_if_key=com_if_key,
json_cfg_path=self.cfg_path, json_cfg_path=self.cfg_path,
space_packet_ids=None, space_packet_ids=TM_PACKET_IDS,
) )
assert cfg is not None
return create_com_interface_default(cfg) return create_com_interface_default(cfg)
def get_tmtc_definitions(self) -> TmtcDefinitionWrapper: def get_command_definitions(self) -> CmdTreeNode:
return tc_definitions.tc_definitions() """This function should return the root node of the command definition tree."""
return pus_tc.create_cmd_definition_tree()
def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int): def get_cmd_history(self) -> Optional[History]:
_LOGGER.info("Mode operation hook was called") """Optionlly return a history class for the past command paths which will be used
pass when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT: def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids from tmtccmd.config.objects import get_core_object_ids
@@ -94,15 +99,12 @@ class PusHandler(SpecificApidHandlerBase):
def handle_tm(self, packet: bytes, _user_args: any): def handle_tm(self, packet: bytes, _user_args: any):
try: try:
tm_packet = PusTelemetry.unpack( pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty())
packet, time_reader=CdsShortTimestamp.empty()
)
except ValueError as e: except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data") _LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}") _LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e raise e
service = tm_packet.service service = pus_tm.service
dedicated_handler = False
if service == 1: if service == 1:
tm_packet = Service1Tm.unpack( tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.empty(), 1, 2) data=packet, params=UnpackParams(CdsShortTimestamp.empty(), 1, 2)
@@ -119,8 +121,7 @@ class PusHandler(SpecificApidHandlerBase):
else: else:
self.verif_wrapper.log_to_console(tm_packet, res) self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res) self.verif_wrapper.log_to_file(tm_packet, res)
dedicated_handler = True elif service == 3:
if service == 3:
_LOGGER.info("No handling for HK packets implemented") _LOGGER.info("No handling for HK packets implemented")
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]") _LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty()) pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty())
@@ -128,8 +129,8 @@ class PusHandler(SpecificApidHandlerBase):
if len(pus_tm.source_data) < 8: if len(pus_tm.source_data) < 8:
raise ValueError("No addressable ID in HK packet") raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:] json_str = pus_tm.source_data[8:]
dedicated_handler = True _LOGGER.info(json_str)
if service == 5: elif service == 5:
tm_packet = PusTelemetry.unpack( tm_packet = PusTelemetry.unpack(
packet, time_reader=CdsShortTimestamp.empty() packet, time_reader=CdsShortTimestamp.empty()
) )
@@ -138,11 +139,10 @@ class PusHandler(SpecificApidHandlerBase):
_LOGGER.info(f"Received event packet. Event: {event_u32}") _LOGGER.info(f"Received event packet. Event: {event_u32}")
if event_u32.group_id == 0 and event_u32.unique_id == 0: if event_u32.group_id == 0 and event_u32.unique_id == 0:
_LOGGER.info("Received test event") _LOGGER.info("Received test event")
if service == 17: elif service == 17:
tm_packet = Service17Tm.unpack( tm_packet = Service17Tm.unpack(
packet, time_reader=CdsShortTimestamp.empty() packet, time_reader=CdsShortTimestamp.empty()
) )
dedicated_handler = True
if tm_packet.subservice == 2: if tm_packet.subservice == 2:
self.file_logger.info("Received Ping Reply TM[17,2]") self.file_logger.info("Received Ping Reply TM[17,2]")
_LOGGER.info("Received Ping Reply TM[17,2]") _LOGGER.info("Received Ping Reply TM[17,2]")
@@ -153,17 +153,14 @@ class PusHandler(SpecificApidHandlerBase):
_LOGGER.info( _LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}" f"Received Test Packet with unknown subservice {tm_packet.subservice}"
) )
if tm_packet is None: else:
_LOGGER.info( _LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory" f"The service {service} is not implemented in Telemetry Factory"
) )
tm_packet = PusTelemetry.unpack( tm_packet = PusTelemetry.unpack(
packet, time_reader=CdsShortTimestamp.empty() packet, time_reader=CdsShortTimestamp.empty()
) )
self.raw_logger.log_tm(tm_packet) self.raw_logger.log_tm(pus_tm)
if not dedicated_handler and tm_packet is not None:
pass
# self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet)
class TcHandler(TcHandlerBase): class TcHandler(TcHandlerBase):
@@ -195,22 +192,18 @@ class TcHandler(TcHandlerBase):
log_entry = entry_helper.to_log_entry() log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str) _LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, helper: ProcedureWrapper): def queue_finished_cb(self, info: ProcedureWrapper):
if helper.proc_type == TcProcedureType.DEFAULT: if info.proc_type == TcProcedureType.DEFAULT:
def_proc = helper.to_def_procedure() def_proc = info.to_def_procedure()
_LOGGER.info( _LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
f"Queue handling finished for service {def_proc.service} and "
f"op code {def_proc.op_code}"
)
def feed_cb(self, helper: ProcedureWrapper, wrapper: FeedWrapper): def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper q.queue_wrapper = wrapper.queue_wrapper
if helper.proc_type == TcProcedureType.DEFAULT: if info.proc_type == TcProcedureType.DEFAULT:
def_proc = helper.to_def_procedure() def_proc = info.to_def_procedure()
service = def_proc.service assert def_proc.cmd_path is not None
op_code = def_proc.op_code pus_tc.pack_pus_telecommands(q, def_proc.cmd_path)
pus_tc.pack_pus_telecommands(q, service, op_code)
def main(): def main():

View File

@@ -1,50 +1,85 @@
import datetime import datetime
import logging
from spacepackets.ccsds import CdsShortTimestamp from spacepackets.ccsds import CdsShortTimestamp
from spacepackets.ecss import PusTelecommand from spacepackets.ecss import PusTelecommand
from tmtccmd.config import CoreServiceList from tmtccmd.config import CmdTreeNode
from tmtccmd.tc import DefaultPusQueueHelper from tmtccmd.tmtc import DefaultPusQueueHelper
from tmtccmd.tc.pus_11_tc_sched import create_time_tagged_cmd from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd
from tmtccmd.tc.pus_3_fsfw_hk import create_request_one_hk_command from tmtccmd.pus.tc.s3_fsfw_hk import create_request_one_hk_command
from common import ( from common import (
EXAMPLE_PUS_APID, EXAMPLE_PUS_APID,
HkOpCodes,
make_addressable_id, make_addressable_id,
RequestTargetId, RequestTargetId,
AcsHkIds, AcsHkIds,
) )
_LOGGER = logging.getLogger(__name__)
def pack_pus_telecommands(q: DefaultPusQueueHelper, service: str, op_code: str):
if ( def create_cmd_definition_tree() -> CmdTreeNode:
service == CoreServiceList.SERVICE_17
or service == CoreServiceList.SERVICE_17_ALT root_node = CmdTreeNode.root_node()
):
if op_code == "ping": test_node = CmdTreeNode("test", "Test Node")
test_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
test_node.add_child(CmdTreeNode("trigger_event", "Send PUS test to trigger event"))
root_node.add_child(test_node)
scheduler_node = CmdTreeNode("scheduler", "Scheduler Node")
scheduler_node.add_child(
CmdTreeNode(
"schedule_ping_10_secs_ahead", "Schedule Ping to execute in 10 seconds"
)
)
root_node.add_child(scheduler_node)
acs_node = CmdTreeNode("acs", "ACS Subsystem Node")
mgm_node = CmdTreeNode("mgms", "MGM devices node")
mgm_node.add_child(CmdTreeNode("one_shot_hk", "Request one shot HK"))
acs_node.add_child(mgm_node)
root_node.add_child(acs_node)
return root_node
def pack_pus_telecommands(q: DefaultPusQueueHelper, cmd_path: str):
# It should always be at least the root path "/", so we split of the empty portion left of it.
cmd_path_list = cmd_path.split("/")[1:]
if len(cmd_path_list) == 0:
_LOGGER.warning("empty command path")
return
if cmd_path_list[0] == "test":
assert len(cmd_path_list) >= 2
if cmd_path_list[1] == "ping":
q.add_log_cmd("Sending PUS ping telecommand") q.add_log_cmd("Sending PUS ping telecommand")
return q.add_pus_tc(PusTelecommand(service=17, subservice=1)) return q.add_pus_tc(PusTelecommand(service=17, subservice=1))
elif op_code == "trigger_event": elif cmd_path_list[1] == "trigger_event":
q.add_log_cmd("Triggering test event") q.add_log_cmd("Triggering test event")
return q.add_pus_tc(PusTelecommand(service=17, subservice=128)) return q.add_pus_tc(PusTelecommand(service=17, subservice=128))
if service == CoreServiceList.SERVICE_11: if cmd_path_list[0] == "scheduler":
q.add_log_cmd("Sending PUS scheduled TC telecommand") assert len(cmd_path_list) >= 2
crt_time = CdsShortTimestamp.from_now() if cmd_path_list[1] == "schedule_ping_10_secs_ahead":
time_stamp = crt_time + datetime.timedelta(seconds=10) q.add_log_cmd("Sending PUS scheduled TC telecommand")
time_stamp = time_stamp.pack() crt_time = CdsShortTimestamp.from_now()
return q.add_pus_tc( time_stamp = crt_time + datetime.timedelta(seconds=10)
create_time_tagged_cmd( time_stamp = time_stamp.pack()
time_stamp, return q.add_pus_tc(
PusTelecommand(service=17, subservice=1), create_time_tagged_cmd(
apid=EXAMPLE_PUS_APID, time_stamp,
) PusTelecommand(service=17, subservice=1),
) apid=EXAMPLE_PUS_APID,
if service == CoreServiceList.SERVICE_3:
if op_code in HkOpCodes.GENERATE_ONE_SHOT:
q.add_log_cmd("Sending HK one shot request")
q.add_pus_tc(
create_request_one_hk_command(
make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET)
) )
) )
pass if cmd_path_list[0] == "acs":
assert len(cmd_path_list) >= 2
if cmd_path_list[1] == "mgm":
assert len(cmd_path_list) >= 3
if cmd_path_list[2] == "one_shot_hk":
q.add_log_cmd("Sending HK one shot request")
q.add_pus_tc(
create_request_one_hk_command(
make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET)
)
)

View File

@@ -1,2 +1,2 @@
tmtccmd == 5.0.0rc0 tmtccmd == 8.0.0rc1
# -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd # -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd

View File

@@ -1,6 +1,8 @@
{ {
"com_if": "udp", "com_if": "tcp",
"tcpip_udp_ip_addr": "127.0.0.1", "tcpip_udp_ip_addr": "127.0.0.1",
"tcpip_udp_port": 7301, "tcpip_udp_port": 7301,
"tcpip_udp_recv_max_size": 1500 "tcpip_udp_recv_max_size": 1500,
"tcpip_tcp_ip_addr": "127.0.0.1",
"tcpip_tcp_port": 7301
} }

View File

@@ -2,7 +2,7 @@ use satrs_core::pus::verification::RequestId;
use satrs_core::spacepackets::ecss::tc::PusTcCreator; use satrs_core::spacepackets::ecss::tc::PusTcCreator;
use satrs_core::spacepackets::ecss::tm::PusTmReader; use satrs_core::spacepackets::ecss::tm::PusTmReader;
use satrs_core::{ use satrs_core::{
spacepackets::ecss::{PusPacket, SerializablePusPacket}, spacepackets::ecss::{PusPacket, WritablePusPacket},
spacepackets::SpHeader, spacepackets::SpHeader,
}; };
use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT}; use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT};

View File

@@ -3,6 +3,7 @@ use satrs_core::spacepackets::{CcsdsPacket, SpHeader};
use satrs_core::tmtc::{CcsdsPacketHandler, ReceivesCcsdsTc}; use satrs_core::tmtc::{CcsdsPacketHandler, ReceivesCcsdsTc};
use satrs_example::PUS_APID; use satrs_example::PUS_APID;
#[derive(Clone)]
pub struct CcsdsReceiver { pub struct CcsdsReceiver {
pub tc_source: PusTcSource, pub tc_source: PusTcSource,
} }

View File

@@ -1,4 +1,37 @@
use derive_new::new;
use satrs_core::spacepackets::ByteConversionError;
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AcsHkIds { pub enum AcsHkIds {
TestMgmSet = 1, TestMgmSet = 1,
} }
#[derive(Debug, new, Copy, Clone)]
pub struct HkUniqueId {
target_id: u32,
set_id: u32,
}
impl HkUniqueId {
#[allow(dead_code)]
pub fn target_id(&self) -> u32 {
self.target_id
}
#[allow(dead_code)]
pub fn set_id(&self) -> u32 {
self.set_id
}
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.to_be_bytes());
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
Ok(8)
}
}

View File

@@ -1,11 +1,68 @@
use derive_new::new;
use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs_core::events::{EventU32TypedSev, SeverityInfo}; use satrs_core::events::{EventU32TypedSev, SeverityInfo};
use satrs_core::objects::ObjectId; use satrs_core::objects::ObjectId;
use satrs_core::spacepackets::ecss::tc::IsPusTelecommand;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::{ByteConversionError, CcsdsPacket};
use satrs_core::tmtc::TargetId;
use std::fmt;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use thiserror::Error;
use satrs_mib::res_code::{ResultU16, ResultU16Info}; use satrs_mib::res_code::{ResultU16, ResultU16Info};
use satrs_mib::resultcode; use satrs_mib::resultcode;
pub type Apid = u16;
#[derive(Debug, Error)]
pub enum TargetIdCreationError {
#[error("byte conversion")]
ByteConversion(#[from] ByteConversionError),
#[error("not enough app data to generate target ID")]
NotEnoughAppData(usize),
}
// TODO: can these stay pub?
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, new)]
pub struct TargetIdWithApid {
pub apid: Apid,
pub target: TargetId,
}
impl fmt::Display for TargetIdWithApid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}, {}", self.apid, self.target)
}
}
impl TargetIdWithApid {
pub fn apid(&self) -> Apid {
self.apid
}
pub fn target_id(&self) -> TargetId {
self.target
}
}
impl TargetIdWithApid {
pub fn from_tc(
tc: &(impl CcsdsPacket + PusPacket + IsPusTelecommand),
) -> Result<Self, TargetIdCreationError> {
if tc.user_data().len() < 4 {
return Err(ByteConversionError::FromSliceTooSmall {
found: tc.user_data().len(),
expected: 8,
}
.into());
}
Ok(Self {
apid: tc.apid(),
target: u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap()),
})
}
}
pub const PUS_APID: u16 = 0x02; pub const PUS_APID: u16 = 0x02;
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)] #[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
@@ -20,6 +77,8 @@ pub enum RequestTargetId {
AcsSubsystem = 1, AcsSubsystem = 1,
} }
pub const AOCS_APID: u16 = 1;
pub const ACS_OBJECT_ID: ObjectId = ObjectId { pub const ACS_OBJECT_ID: ObjectId = ObjectId {
id: RequestTargetId::AcsSubsystem as u32, id: RequestTargetId::AcsSubsystem as u32,
name: "ACS_SUBSYSTEM", name: "ACS_SUBSYSTEM",

View File

@@ -4,13 +4,14 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
out.finish(format_args!( out.finish(format_args!(
"{}[{}][{}] {}", "{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(), //(std::thread::current().name().expect("unnamed_thread"), std::thread::current().name().expect("unnamed_thread"),
record.level(), record.level(),
message message
)) ))
}) })
.level(log::LevelFilter::Debug) .level(log::LevelFilter::Debug)
.chain(std::io::stdout()) .chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?; .apply()?;
Ok(()) Ok(())
} }

View File

@@ -1,28 +1,35 @@
mod ccsds; mod ccsds;
mod hk; mod hk;
mod logging; mod logger;
mod pus; mod pus;
mod requests; mod requests;
mod tcp;
mod tmtc; mod tmtc;
mod udp;
use log::{info, warn}; use log::{info, warn};
use satrs_core::hal::std::tcp_server::ServerConfig;
use satrs_core::hal::std::udp_server::UdpTcServer;
use crate::hk::AcsHkIds; use crate::ccsds::CcsdsReceiver;
use crate::logging::setup_logger; use crate::hk::{AcsHkIds, HkUniqueId};
use crate::logger::setup_logger;
use crate::pus::action::{Pus8Wrapper, PusService8ActionHandler}; use crate::pus::action::{Pus8Wrapper, PusService8ActionHandler};
use crate::pus::event::Pus5Wrapper; use crate::pus::event::Pus5Wrapper;
use crate::pus::hk::{Pus3Wrapper, PusService3HkHandler}; use crate::pus::hk::{Pus3Wrapper, PusService3HkHandler};
use crate::pus::scheduler::Pus11Wrapper; use crate::pus::scheduler::Pus11Wrapper;
use crate::pus::test::Service17CustomWrapper; use crate::pus::test::Service17CustomWrapper;
use crate::pus::PusTcMpscRouter; use crate::pus::{PusReceiver, PusTcMpscRouter};
use crate::requests::{Request, RequestWithToken}; use crate::requests::{Request, RequestWithToken};
use crate::tmtc::{core_tmtc_task, PusTcSource, TcArgs, TcStore, TmArgs, TmFunnel}; use crate::tcp::{SyncTcpTmSource, TcpTask};
use crate::tmtc::{PusTcSource, TcArgs, TcStore, TmArgs, TmFunnel, TmtcTask};
use crate::udp::UdpTmtcServer;
use satrs_core::event_man::{ use satrs_core::event_man::{
EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider, SendEventProvider, EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider, SendEventProvider,
}; };
use satrs_core::events::EventU32; use satrs_core::events::EventU32;
use satrs_core::hk::HkRequest; use satrs_core::hk::HkRequest;
use satrs_core::pool::{LocalPool, PoolCfg}; use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig};
use satrs_core::pus::event_man::{ use satrs_core::pus::event_man::{
DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken, DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken,
PusEventDispatcher, PusEventDispatcher,
@@ -35,7 +42,9 @@ use satrs_core::pus::test::PusService17TestHandler;
use satrs_core::pus::verification::{ use satrs_core::pus::verification::{
TcStateStarted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken, TcStateStarted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
}; };
use satrs_core::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender}; use satrs_core::pus::{
EcssTcInSharedStoreConverter, MpscTcReceiver, MpscTmInStoreSender, PusServiceHelper,
};
use satrs_core::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore}; use satrs_core::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore};
use satrs_core::spacepackets::ecss::tm::{PusTmCreator, PusTmZeroCopyWriter}; use satrs_core::spacepackets::ecss::tm::{PusTmCreator, PusTmZeroCopyWriter};
use satrs_core::spacepackets::{ use satrs_core::spacepackets::{
@@ -43,10 +52,11 @@ use satrs_core::spacepackets::{
SpHeader, SpHeader,
}; };
use satrs_core::tmtc::tm_helper::SharedTmStore; use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::{AddressableId, TargetId}; use satrs_core::tmtc::{CcsdsDistributor, TargetId};
use satrs_core::ChannelId; use satrs_core::ChannelId;
use satrs_example::{ use satrs_example::{
RequestTargetId, TcReceiverId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID, SERVER_PORT, RequestTargetId, TargetIdWithApid, TcReceiverId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID,
SERVER_PORT,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
@@ -58,7 +68,7 @@ use std::time::Duration;
fn main() { fn main() {
setup_logger().expect("setting up logging with fern failed"); setup_logger().expect("setting up logging with fern failed");
println!("Running OBSW example"); println!("Running OBSW example");
let tm_pool = LocalPool::new(PoolCfg::new(vec![ let tm_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32), (30, 32),
(15, 64), (15, 64),
(15, 128), (15, 128),
@@ -66,9 +76,9 @@ fn main() {
(15, 1024), (15, 1024),
(15, 2048), (15, 2048),
])); ]));
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); let shared_tm_store = SharedTmStore::new(tm_pool);
let tm_store_event = shared_tm_store.clone(); let tm_store_event = shared_tm_store.clone();
let tc_pool = LocalPool::new(PoolCfg::new(vec![ let tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32), (30, 32),
(15, 64), (15, 64),
(15, 128), (15, 128),
@@ -77,8 +87,16 @@ fn main() {
(15, 2048), (15, 2048),
])); ]));
let tc_store = TcStore { let tc_store = TcStore {
pool: Arc::new(RwLock::new(Box::new(tc_pool))), pool: Arc::new(RwLock::new(tc_pool)),
}; };
let sched_tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let seq_count_provider = CcsdsSimpleSeqCountProvider::new(); let seq_count_provider = CcsdsSimpleSeqCountProvider::new();
let mut msg_counter_map: HashMap<u8, u16> = HashMap::new(); let mut msg_counter_map: HashMap<u8, u16> = HashMap::new();
@@ -124,7 +142,8 @@ fn main() {
// Some request are targetable. This map is used to retrieve sender handles based on a target ID. // Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let mut request_map = HashMap::new(); let mut request_map = HashMap::new();
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>(); let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
request_map.insert(RequestTargetId::AcsSubsystem as TargetId, acs_thread_tx); let target_apid = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
request_map.insert(target_apid, acs_thread_tx);
let tc_source_wrapper = PusTcSource { let tc_source_wrapper = PusTcSource {
tc_store: tc_store.clone(), tc_store: tc_store.clone(),
@@ -139,7 +158,7 @@ fn main() {
let tm_args = TmArgs { let tm_args = TmArgs {
tm_store: shared_tm_store.clone(), tm_store: shared_tm_store.clone(),
tm_sink_sender: tm_funnel_tx.clone(), tm_sink_sender: tm_funnel_tx.clone(),
tm_server_rx, tm_udp_server_rx: tm_server_rx,
}; };
let aocs_tm_funnel = tm_funnel_tx.clone(); let aocs_tm_funnel = tm_funnel_tx.clone();
@@ -163,18 +182,18 @@ fn main() {
shared_tm_store.clone(), shared_tm_store.clone(),
tm_funnel_tx.clone(), tm_funnel_tx.clone(),
); );
let test_srv_receiver = MpscTcInStoreReceiver::new( let test_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusTest as ChannelId, TcReceiverId::PusTest as ChannelId,
"PUS_17_TC_RECV", "PUS_17_TC_RECV",
pus_test_rx, pus_test_rx,
); );
let pus17_handler = PusService17TestHandler::new( let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
Box::new(test_srv_receiver), Box::new(test_srv_receiver),
tc_store.pool.clone(),
Box::new(test_srv_tm_sender), Box::new(test_srv_tm_sender),
PUS_APID, PUS_APID,
verif_reporter.clone(), verif_reporter.clone(),
); EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
));
let mut pus_17_wrapper = Service17CustomWrapper { let mut pus_17_wrapper = Service17CustomWrapper {
pus17_handler, pus17_handler,
test_srv_event_sender, test_srv_event_sender,
@@ -186,7 +205,7 @@ fn main() {
shared_tm_store.clone(), shared_tm_store.clone(),
tm_funnel_tx.clone(), tm_funnel_tx.clone(),
); );
let sched_srv_receiver = MpscTcInStoreReceiver::new( let sched_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusSched as ChannelId, TcReceiverId::PusSched as ChannelId,
"PUS_11_TC_RECV", "PUS_11_TC_RECV",
pus_sched_rx, pus_sched_rx,
@@ -194,15 +213,18 @@ fn main() {
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5)) let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed"); .expect("Creating PUS Scheduler failed");
let pus_11_handler = PusService11SchedHandler::new( let pus_11_handler = PusService11SchedHandler::new(
Box::new(sched_srv_receiver), PusServiceHelper::new(
tc_store.pool.clone(), Box::new(sched_srv_receiver),
Box::new(sched_srv_tm_sender), Box::new(sched_srv_tm_sender),
PUS_APID, PUS_APID,
verif_reporter.clone(), verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
),
scheduler, scheduler,
); );
let mut pus_11_wrapper = Pus11Wrapper { let mut pus_11_wrapper = Pus11Wrapper {
pus_11_handler, pus_11_handler,
sched_tc_pool,
tc_source_wrapper, tc_source_wrapper,
}; };
@@ -212,17 +234,19 @@ fn main() {
shared_tm_store.clone(), shared_tm_store.clone(),
tm_funnel_tx.clone(), tm_funnel_tx.clone(),
); );
let event_srv_receiver = MpscTcInStoreReceiver::new( let event_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusEvent as ChannelId, TcReceiverId::PusEvent as ChannelId,
"PUS_5_TC_RECV", "PUS_5_TC_RECV",
pus_event_rx, pus_event_rx,
); );
let pus_5_handler = PusService5EventHandler::new( let pus_5_handler = PusService5EventHandler::new(
Box::new(event_srv_receiver), PusServiceHelper::new(
tc_store.pool.clone(), Box::new(event_srv_receiver),
Box::new(event_srv_tm_sender), Box::new(event_srv_tm_sender),
PUS_APID, PUS_APID,
verif_reporter.clone(), verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
),
event_request_tx, event_request_tx,
); );
let mut pus_5_wrapper = Pus5Wrapper { pus_5_handler }; let mut pus_5_wrapper = Pus5Wrapper { pus_5_handler };
@@ -233,17 +257,17 @@ fn main() {
shared_tm_store.clone(), shared_tm_store.clone(),
tm_funnel_tx.clone(), tm_funnel_tx.clone(),
); );
let action_srv_receiver = MpscTcInStoreReceiver::new( let action_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusAction as ChannelId, TcReceiverId::PusAction as ChannelId,
"PUS_8_TC_RECV", "PUS_8_TC_RECV",
pus_action_rx, pus_action_rx,
); );
let pus_8_handler = PusService8ActionHandler::new( let pus_8_handler = PusService8ActionHandler::new(
Box::new(action_srv_receiver), Box::new(action_srv_receiver),
tc_store.pool.clone(),
Box::new(action_srv_tm_sender), Box::new(action_srv_tm_sender),
PUS_APID, PUS_APID,
verif_reporter.clone(), verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
request_map.clone(), request_map.clone(),
); );
let mut pus_8_wrapper = Pus8Wrapper { pus_8_handler }; let mut pus_8_wrapper = Pus8Wrapper { pus_8_handler };
@@ -255,22 +279,61 @@ fn main() {
tm_funnel_tx.clone(), tm_funnel_tx.clone(),
); );
let hk_srv_receiver = let hk_srv_receiver =
MpscTcInStoreReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx); MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
let pus_3_handler = PusService3HkHandler::new( let pus_3_handler = PusService3HkHandler::new(
Box::new(hk_srv_receiver), Box::new(hk_srv_receiver),
tc_store.pool.clone(),
Box::new(hk_srv_tm_sender), Box::new(hk_srv_tm_sender),
PUS_APID, PUS_APID,
verif_reporter.clone(), verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
request_map, request_map,
); );
let mut pus_3_wrapper = Pus3Wrapper { pus_3_handler }; let mut pus_3_wrapper = Pus3Wrapper { pus_3_handler };
info!("Starting TMTC task"); let ccsds_receiver = CcsdsReceiver {
let jh0 = thread::Builder::new() tc_source: tc_args.tc_source.clone(),
.name("TMTC".to_string()) };
let mut tmtc_task = TmtcTask::new(tc_args, PusReceiver::new(verif_reporter, pus_router));
let udp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver.clone()));
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(udp_ccsds_distributor))
.expect("creating UDP TMTC server failed");
let mut udp_tmtc_server = UdpTmtcServer {
udp_tc_server,
tm_rx: tm_args.tm_udp_server_rx,
tm_store: tm_args.tm_store.clone_backing_pool(),
};
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC and UDP".to_string())
.spawn(move || { .spawn(move || {
core_tmtc_task(sock_addr, tc_args, tm_args, verif_reporter, pus_router); info!("Running UDP server on port {SERVER_PORT}");
loop {
udp_tmtc_server.periodic_operation();
tmtc_task.periodic_operation();
thread::sleep(Duration::from_millis(400));
}
})
.unwrap();
let tcp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
let tcp_server_cfg = ServerConfig::new(sock_addr, Duration::from_millis(400), 4096, 8192);
let mut sync_tm_tcp_source = SyncTcpTmSource::new(200);
let mut tcp_server = TcpTask::new(
tcp_server_cfg,
sync_tm_tcp_source.clone(),
tcp_ccsds_distributor,
)
.expect("tcp server creation failed");
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 {
tcp_server.periodic_operation();
}
}) })
.unwrap(); .unwrap();
@@ -311,6 +374,7 @@ fn main() {
.tm_server_tx .tm_server_tx
.send(addr) .send(addr)
.expect("Sending TM to server failed"); .expect("Sending TM to server failed");
sync_tm_tcp_source.add_tm(tm_raw);
} }
} }
}) })
@@ -382,6 +446,7 @@ fn main() {
let mut timestamp: [u8; 7] = [0; 7]; let mut timestamp: [u8; 7] = [0; 7];
let mut time_provider = TimeProvider::new_with_u16_days(0, 0); let mut time_provider = TimeProvider::new_with_u16_days(0, 0);
loop { loop {
// TODO: Move this into a separate function/task/module..
match acs_thread_rx.try_recv() { match acs_thread_rx.try_recv() {
Ok(request) => { Ok(request) => {
info!( info!(
@@ -392,9 +457,13 @@ fn main() {
match request.targeted_request.request { match request.targeted_request.request {
Request::Hk(hk_req) => match hk_req { Request::Hk(hk_req) => match hk_req {
HkRequest::OneShot(unique_id) => { HkRequest::OneShot(unique_id) => {
// TODO: We should check whether the unique ID is even valid.
let target = request.targeted_request.target_id; let target = request.targeted_request.target_id;
assert_eq!(target, RequestTargetId::AcsSubsystem as u32); assert_eq!(
if request.targeted_request.target_id target.target_id(),
RequestTargetId::AcsSubsystem as u32
);
if request.targeted_request.target_id.target
== AcsHkIds::TestMgmSet as u32 == AcsHkIds::TestMgmSet as u32
{ {
let mut sp_header = SpHeader::tm( let mut sp_header = SpHeader::tm(
@@ -410,15 +479,12 @@ fn main() {
&timestamp, &timestamp,
); );
let mut buf: [u8; 8] = [0; 8]; let mut buf: [u8; 8] = [0; 8];
let addressable_id = AddressableId { let hk_id = HkUniqueId::new(target.target_id(), unique_id);
target_id: target, hk_id.write_to_be_bytes(&mut buf).unwrap();
unique_id,
};
addressable_id.write_to_be_bytes(&mut buf).unwrap();
let pus_tm = PusTmCreator::new( let pus_tm = PusTmCreator::new(
&mut sp_header, &mut sp_header,
sec_header, sec_header,
Some(&buf), &buf,
true, true,
); );
let addr = aocs_tm_store let addr = aocs_tm_store
@@ -481,7 +547,12 @@ fn main() {
thread::sleep(Duration::from_millis(200)); thread::sleep(Duration::from_millis(200));
}) })
.unwrap(); .unwrap();
jh0.join().expect("Joining UDP TMTC server thread failed"); jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh1.join().expect("Joining TM Funnel thread failed"); jh1.join().expect("Joining TM Funnel thread failed");
jh2.join().expect("Joining Event Manager thread failed"); jh2.join().expect("Joining Event Manager thread failed");
jh3.join().expect("Joining AOCS thread failed"); jh3.join().expect("Joining AOCS thread failed");

View File

@@ -1,48 +1,44 @@
use crate::requests::{ActionRequest, Request, RequestWithToken}; use crate::requests::{ActionRequest, Request, RequestWithToken};
use log::{error, warn}; use log::{error, warn};
use satrs_core::pool::{SharedPool, StoreAddr};
use satrs_core::pus::verification::{ use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken, FailParams, TcStateAccepted, VerificationReporterWithSender, VerificationToken,
}; };
use satrs_core::pus::{ use satrs_core::pus::{
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
PusServiceHandler, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
}; };
use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket; use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::tmtc::TargetId; use satrs_example::{tmtc_err, TargetIdWithApid};
use satrs_example::tmtc_err;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
pub struct PusService8ActionHandler { pub struct PusService8ActionHandler<TcInMemConverter: EcssTcInMemConverter> {
psb: PusServiceBase, service_helper: PusServiceHelper<TcInMemConverter>,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>, request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
} }
impl PusService8ActionHandler { impl<TcInMemConverter: EcssTcInMemConverter> PusService8ActionHandler<TcInMemConverter> {
pub fn new( pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>, tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_pool: SharedPool,
tm_sender: Box<dyn EcssTmSender>, tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16, tm_apid: u16,
verification_handler: StdVerifReporterWithSender, verification_handler: VerificationReporterWithSender,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>, tc_in_mem_converter: TcInMemConverter,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
) -> Self { ) -> Self {
Self { Self {
psb: PusServiceBase::new( service_helper: PusServiceHelper::new(
tc_receiver, tc_receiver,
shared_tc_pool,
tm_sender, tm_sender,
tm_apid, tm_apid,
verification_handler, verification_handler,
tc_in_mem_converter,
), ),
request_handlers, request_handlers,
} }
} }
}
impl PusService8ActionHandler {
fn handle_action_request_with_id( fn handle_action_request_with_id(
&self, &self,
token: VerificationToken<TcStateAccepted>, token: VerificationToken<TcStateAccepted>,
@@ -51,7 +47,8 @@ impl PusService8ActionHandler {
) -> Result<(), PusPacketHandlingError> { ) -> Result<(), PusPacketHandlingError> {
let user_data = tc.user_data(); let user_data = tc.user_data();
if user_data.len() < 8 { if user_data.len() < 8 {
self.psb() self.service_helper
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure( .start_failure(
@@ -63,7 +60,8 @@ impl PusService8ActionHandler {
"Expected at least 4 bytes".into(), "Expected at least 4 bytes".into(),
)); ));
} }
let target_id = u32::from_be_bytes(user_data[0..4].try_into().unwrap()); //let target_id = u32::from_be_bytes(user_data[0..4].try_into().unwrap());
let target_id = TargetIdWithApid::from_tc(tc).unwrap();
let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap()); let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap());
if let Some(sender) = self.request_handlers.get(&target_id) { if let Some(sender) = self.request_handlers.get(&target_id) {
sender sender
@@ -78,8 +76,9 @@ impl PusService8ActionHandler {
.expect("Forwarding action request failed"); .expect("Forwarding action request failed");
} else { } else {
let mut fail_data: [u8; 4] = [0; 4]; let mut fail_data: [u8; 4] = [0; 4];
fail_data.copy_from_slice(&target_id.to_be_bytes()); fail_data.copy_from_slice(&target_id.target.to_be_bytes());
self.psb() self.service_helper
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure( .start_failure(
@@ -97,37 +96,32 @@ impl PusService8ActionHandler {
} }
Ok(()) Ok(())
} }
}
impl PusServiceHandler for PusService8ActionHandler { fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
fn psb_mut(&mut self) -> &mut PusServiceBase { let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
&mut self.psb if possible_packet.is_none() {
} return Ok(PusPacketHandlerResult::Empty);
fn psb(&self) -> &PusServiceBase { }
&self.psb let ecss_tc_and_token = possible_packet.unwrap();
} self.service_helper
.tc_in_mem_converter
fn handle_one_tc( .cache_ecss_tc_in_memory(&ecss_tc_and_token.tc_in_memory)?;
&mut self, let tc = PusTcReader::new(self.service_helper.tc_in_mem_converter.tc_slice_raw())?.0;
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?;
let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap();
let subservice = tc.subservice(); let subservice = tc.subservice();
let mut partial_error = None; let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error); let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
match subservice { match subservice {
128 => { 128 => {
self.handle_action_request_with_id(token, &tc, &time_stamp)?; self.handle_action_request_with_id(ecss_tc_and_token.token, &tc, &time_stamp)?;
} }
_ => { _ => {
let fail_data = [subservice]; let fail_data = [subservice];
self.psb_mut() self.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_failure( .start_failure(
token, ecss_tc_and_token.token,
FailParams::new( FailParams::new(
Some(&time_stamp), Some(&time_stamp),
&tmtc_err::INVALID_PUS_SUBSERVICE, &tmtc_err::INVALID_PUS_SUBSERVICE,
@@ -148,12 +142,12 @@ impl PusServiceHandler for PusService8ActionHandler {
} }
pub struct Pus8Wrapper { pub struct Pus8Wrapper {
pub(crate) pus_8_handler: PusService8ActionHandler, pub(crate) pus_8_handler: PusService8ActionHandler<EcssTcInSharedStoreConverter>,
} }
impl Pus8Wrapper { impl Pus8Wrapper {
pub fn handle_next_packet(&mut self) -> bool { pub fn handle_next_packet(&mut self) -> bool {
match self.pus_8_handler.handle_next_packet() { match self.pus_8_handler.handle_one_tc() {
Ok(result) => match result { Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@@ -1,14 +1,14 @@
use log::{error, warn}; use log::{error, warn};
use satrs_core::pus::event_srv::PusService5EventHandler; use satrs_core::pus::event_srv::PusService5EventHandler;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult};
pub struct Pus5Wrapper { pub struct Pus5Wrapper {
pub pus_5_handler: PusService5EventHandler, pub pus_5_handler: PusService5EventHandler<EcssTcInSharedStoreConverter>,
} }
impl Pus5Wrapper { impl Pus5Wrapper {
pub fn handle_next_packet(&mut self) -> bool { pub fn handle_next_packet(&mut self) -> bool {
match self.pus_5_handler.handle_next_packet() { match self.pus_5_handler.handle_one_tc() {
Ok(result) => match result { Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@@ -1,73 +1,63 @@
use crate::requests::{Request, RequestWithToken}; use crate::requests::{Request, RequestWithToken};
use log::{error, warn}; use log::{error, warn};
use satrs_core::hk::{CollectionIntervalFactor, HkRequest}; use satrs_core::hk::{CollectionIntervalFactor, HkRequest};
use satrs_core::pool::{SharedPool, StoreAddr}; use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender};
use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
};
use satrs_core::pus::{ use satrs_core::pus::{
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
PusServiceHandler, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
}; };
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::{hk, PusPacket}; use satrs_core::spacepackets::ecss::{hk, PusPacket};
use satrs_core::tmtc::{AddressableId, TargetId}; use satrs_example::{hk_err, tmtc_err, TargetIdWithApid};
use satrs_example::{hk_err, tmtc_err};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
pub struct PusService3HkHandler { pub struct PusService3HkHandler<TcInMemConverter: EcssTcInMemConverter> {
psb: PusServiceBase, psb: PusServiceHelper<TcInMemConverter>,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>, request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
} }
impl PusService3HkHandler { impl<TcInMemConverter: EcssTcInMemConverter> PusService3HkHandler<TcInMemConverter> {
pub fn new( pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>, tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_pool: SharedPool,
tm_sender: Box<dyn EcssTmSender>, tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16, tm_apid: u16,
verification_handler: StdVerifReporterWithSender, verification_handler: StdVerifReporterWithSender,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>, tc_in_mem_converter: TcInMemConverter,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
) -> Self { ) -> Self {
Self { Self {
psb: PusServiceBase::new( psb: PusServiceHelper::new(
tc_receiver, tc_receiver,
shared_tc_pool,
tm_sender, tm_sender,
tm_apid, tm_apid,
verification_handler, verification_handler,
tc_in_mem_converter,
), ),
request_handlers, request_handlers,
} }
} }
}
impl PusServiceHandler for PusService3HkHandler { fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
fn psb_mut(&mut self) -> &mut PusServiceBase { let possible_packet = self.psb.retrieve_and_accept_next_packet()?;
&mut self.psb if possible_packet.is_none() {
} return Ok(PusPacketHandlerResult::Empty);
fn psb(&self) -> &PusServiceBase { }
&self.psb let ecss_tc_and_token = possible_packet.unwrap();
} let tc = self
.psb
fn handle_one_tc( .tc_in_mem_converter
&mut self, .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?;
let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap();
let subservice = tc.subservice(); let subservice = tc.subservice();
let mut partial_error = None; let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error); let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
let user_data = tc.user_data(); let user_data = tc.user_data();
if user_data.is_empty() { if user_data.is_empty() {
self.psb self.psb
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure( .start_failure(
token, ecss_tc_and_token.token,
FailParams::new(Some(&time_stamp), &tmtc_err::NOT_ENOUGH_APP_DATA, None), FailParams::new(Some(&time_stamp), &tmtc_err::NOT_ENOUGH_APP_DATA, None),
) )
.expect("Sending start failure TM failed"); .expect("Sending start failure TM failed");
@@ -82,63 +72,58 @@ impl PusServiceHandler for PusService3HkHandler {
&hk_err::UNIQUE_ID_MISSING &hk_err::UNIQUE_ID_MISSING
}; };
self.psb self.psb
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure(token, FailParams::new(Some(&time_stamp), err, None)) .start_failure(
ecss_tc_and_token.token,
FailParams::new(Some(&time_stamp), err, None),
)
.expect("Sending start failure TM failed"); .expect("Sending start failure TM failed");
return Err(PusPacketHandlingError::NotEnoughAppData( return Err(PusPacketHandlingError::NotEnoughAppData(
"Expected at least 8 bytes of app data".into(), "Expected at least 8 bytes of app data".into(),
)); ));
} }
let addressable_id = AddressableId::from_raw_be(user_data).unwrap(); let target_id = TargetIdWithApid::from_tc(&tc).expect("invalid tc format");
if !self let unique_id = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap());
.request_handlers if !self.request_handlers.contains_key(&target_id) {
.contains_key(&addressable_id.target_id)
{
self.psb self.psb
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure( .start_failure(
token, ecss_tc_and_token.token,
FailParams::new(Some(&time_stamp), &hk_err::UNKNOWN_TARGET_ID, None), FailParams::new(Some(&time_stamp), &hk_err::UNKNOWN_TARGET_ID, None),
) )
.expect("Sending start failure TM failed"); .expect("Sending start failure TM failed");
let tgt_id = addressable_id.target_id;
return Err(PusPacketHandlingError::NotEnoughAppData(format!( return Err(PusPacketHandlingError::NotEnoughAppData(format!(
"Unknown target ID {tgt_id}" "Unknown target ID {target_id}"
))); )));
} }
let send_request = |target: TargetId, request: HkRequest| { let send_request = |target: TargetIdWithApid, request: HkRequest| {
let sender = self let sender = self.request_handlers.get(&target).unwrap();
.request_handlers
.get(&addressable_id.target_id)
.unwrap();
sender sender
.send(RequestWithToken::new(target, Request::Hk(request), token)) .send(RequestWithToken::new(
target,
Request::Hk(request),
ecss_tc_and_token.token,
))
.unwrap_or_else(|_| panic!("Sending HK request {request:?} failed")); .unwrap_or_else(|_| panic!("Sending HK request {request:?} failed"));
}; };
if subservice == hk::Subservice::TcEnableHkGeneration as u8 { if subservice == hk::Subservice::TcEnableHkGeneration as u8 {
send_request( send_request(target_id, HkRequest::Enable(unique_id));
addressable_id.target_id,
HkRequest::Enable(addressable_id.unique_id),
);
} else if subservice == hk::Subservice::TcDisableHkGeneration as u8 { } else if subservice == hk::Subservice::TcDisableHkGeneration as u8 {
send_request( send_request(target_id, HkRequest::Disable(unique_id));
addressable_id.target_id,
HkRequest::Disable(addressable_id.unique_id),
);
} else if subservice == hk::Subservice::TcGenerateOneShotHk as u8 { } else if subservice == hk::Subservice::TcGenerateOneShotHk as u8 {
send_request( send_request(target_id, HkRequest::OneShot(unique_id));
addressable_id.target_id,
HkRequest::OneShot(addressable_id.unique_id),
);
} else if subservice == hk::Subservice::TcModifyHkCollectionInterval as u8 { } else if subservice == hk::Subservice::TcModifyHkCollectionInterval as u8 {
if user_data.len() < 12 { if user_data.len() < 12 {
self.psb self.psb
.common
.verification_handler .verification_handler
.borrow_mut() .borrow_mut()
.start_failure( .start_failure(
token, ecss_tc_and_token.token,
FailParams::new( FailParams::new(
Some(&time_stamp), Some(&time_stamp),
&hk_err::COLLECTION_INTERVAL_MISSING, &hk_err::COLLECTION_INTERVAL_MISSING,
@@ -151,9 +136,9 @@ impl PusServiceHandler for PusService3HkHandler {
)); ));
} }
send_request( send_request(
addressable_id.target_id, target_id,
HkRequest::ModifyCollectionInterval( HkRequest::ModifyCollectionInterval(
addressable_id.unique_id, unique_id,
CollectionIntervalFactor::from_be_bytes(user_data[8..12].try_into().unwrap()), CollectionIntervalFactor::from_be_bytes(user_data[8..12].try_into().unwrap()),
), ),
); );
@@ -163,12 +148,12 @@ impl PusServiceHandler for PusService3HkHandler {
} }
pub struct Pus3Wrapper { pub struct Pus3Wrapper {
pub(crate) pus_3_handler: PusService3HkHandler, pub(crate) pus_3_handler: PusService3HkHandler<EcssTcInSharedStoreConverter>,
} }
impl Pus3Wrapper { impl Pus3Wrapper {
pub fn handle_next_packet(&mut self) -> bool { pub fn handle_next_packet(&mut self) -> bool {
match self.pus_3_handler.handle_next_packet() { match self.pus_3_handler.handle_one_tc() {
Ok(result) => match result { Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@@ -1,8 +1,7 @@
use crate::tmtc::MpscStoreAndSendError; use crate::tmtc::MpscStoreAndSendError;
use log::warn; use log::warn;
use satrs_core::pool::StoreAddr;
use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender}; use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender};
use satrs_core::pus::{PusPacketHandlerResult, TcAddrWithToken}; use satrs_core::pus::{EcssTcAndToken, PusPacketHandlerResult, TcInMemory};
use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusServiceId; use satrs_core::spacepackets::ecss::PusServiceId;
use satrs_core::spacepackets::time::cds::TimeProvider; use satrs_core::spacepackets::time::cds::TimeProvider;
@@ -17,11 +16,11 @@ pub mod scheduler;
pub mod test; pub mod test;
pub struct PusTcMpscRouter { pub struct PusTcMpscRouter {
pub test_service_receiver: Sender<TcAddrWithToken>, pub test_service_receiver: Sender<EcssTcAndToken>,
pub event_service_receiver: Sender<TcAddrWithToken>, pub event_service_receiver: Sender<EcssTcAndToken>,
pub sched_service_receiver: Sender<TcAddrWithToken>, pub sched_service_receiver: Sender<EcssTcAndToken>,
pub hk_service_receiver: Sender<TcAddrWithToken>, pub hk_service_receiver: Sender<EcssTcAndToken>,
pub action_service_receiver: Sender<TcAddrWithToken>, pub action_service_receiver: Sender<EcssTcAndToken>,
} }
pub struct PusReceiver { pub struct PusReceiver {
@@ -70,7 +69,7 @@ impl PusReceiver {
impl PusReceiver { impl PusReceiver {
pub fn handle_tc_packet( pub fn handle_tc_packet(
&mut self, &mut self,
store_addr: StoreAddr, tc_in_memory: TcInMemory,
service: u8, service: u8,
pus_tc: &PusTcReader, pus_tc: &PusTcReader,
) -> Result<PusPacketHandlerResult, MpscStoreAndSendError> { ) -> Result<PusPacketHandlerResult, MpscStoreAndSendError> {
@@ -84,22 +83,33 @@ impl PusReceiver {
match service { match service {
Ok(standard_service) => match standard_service { Ok(standard_service) => match standard_service {
PusServiceId::Test => { PusServiceId::Test => {
self.pus_router self.pus_router.test_service_receiver.send(EcssTcAndToken {
.test_service_receiver tc_in_memory,
.send((store_addr, accepted_token.into()))?; token: Some(accepted_token.into()),
})?
}
PusServiceId::Housekeeping => {
self.pus_router.hk_service_receiver.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?
}
PusServiceId::Event => {
self.pus_router
.event_service_receiver
.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?
}
PusServiceId::Scheduling => {
self.pus_router
.sched_service_receiver
.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?
} }
PusServiceId::Housekeeping => self
.pus_router
.hk_service_receiver
.send((store_addr, accepted_token.into()))?,
PusServiceId::Event => self
.pus_router
.event_service_receiver
.send((store_addr, accepted_token.into()))?,
PusServiceId::Scheduling => self
.pus_router
.sched_service_receiver
.send((store_addr, accepted_token.into()))?,
_ => { _ => {
let result = self.verif_reporter.start_failure( let result = self.verif_reporter.start_failure(
accepted_token, accepted_token,

View File

@@ -1,50 +1,54 @@
use crate::tmtc::PusTcSource; use crate::tmtc::PusTcSource;
use log::{error, info, warn}; use log::{error, info, warn};
use satrs_core::pus::scheduler::TcInfo; use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool};
use satrs_core::pus::scheduler::{PusScheduler, TcInfo};
use satrs_core::pus::scheduler_srv::PusService11SchedHandler; use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult};
pub struct Pus11Wrapper { pub struct Pus11Wrapper {
pub pus_11_handler: PusService11SchedHandler, pub pus_11_handler: PusService11SchedHandler<EcssTcInSharedStoreConverter, PusScheduler>,
pub sched_tc_pool: StaticMemoryPool,
pub tc_source_wrapper: PusTcSource, pub tc_source_wrapper: PusTcSource,
} }
impl Pus11Wrapper { impl Pus11Wrapper {
pub fn release_tcs(&mut self) { pub fn release_tcs(&mut self) {
let releaser = |enabled: bool, info: &TcInfo| -> bool { let releaser = |enabled: bool, _info: &TcInfo, tc: &[u8]| -> bool {
if enabled { if enabled {
// Transfer TC from scheduler TC pool to shared TC pool.
let released_tc_addr = self
.tc_source_wrapper
.tc_store
.pool
.write()
.expect("locking pool failed")
.add(tc)
.expect("adding TC to shared pool failed");
self.tc_source_wrapper self.tc_source_wrapper
.tc_source .tc_source
.send(info.addr()) .send(released_tc_addr)
.expect("sending TC to TC source failed"); .expect("sending TC to TC source failed");
} }
true true
}; };
let mut pool = self
.tc_source_wrapper
.tc_store
.pool
.write()
.expect("error locking pool");
self.pus_11_handler self.pus_11_handler
.scheduler_mut() .scheduler_mut()
.update_time_from_now() .update_time_from_now()
.unwrap(); .unwrap();
if let Ok(released_tcs) = self let released_tcs = self
.pus_11_handler .pus_11_handler
.scheduler_mut() .scheduler_mut()
.release_telecommands(releaser, pool.as_mut()) .release_telecommands(releaser, &mut self.sched_tc_pool)
{ .expect("releasing TCs failed");
if released_tcs > 0 { if released_tcs > 0 {
info!("{released_tcs} TC(s) released from scheduler"); info!("{released_tcs} TC(s) released from scheduler");
}
} }
} }
pub fn handle_next_packet(&mut self) -> bool { pub fn handle_next_packet(&mut self) -> bool {
match self.pus_11_handler.handle_next_packet() { match self.pus_11_handler.handle_one_tc(&mut self.sched_tc_pool) {
Ok(result) => match result { Ok(result) => match result {
PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@@ -1,24 +1,24 @@
use log::{info, warn}; use log::{info, warn};
use satrs_core::events::EventU32;
use satrs_core::params::Params; use satrs_core::params::Params;
use satrs_core::pus::test::PusService17TestHandler; use satrs_core::pus::test::PusService17TestHandler;
use satrs_core::pus::verification::FailParams; use satrs_core::pus::verification::FailParams;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; use satrs_core::pus::{EcssTcInMemConverter, PusPacketHandlerResult};
use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket; use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::time::cds::TimeProvider; use satrs_core::spacepackets::time::cds::TimeProvider;
use satrs_core::spacepackets::time::TimeWriter; use satrs_core::spacepackets::time::TimeWriter;
use satrs_core::{events::EventU32, pus::EcssTcInSharedStoreConverter};
use satrs_example::{tmtc_err, TEST_EVENT}; use satrs_example::{tmtc_err, TEST_EVENT};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
pub struct Service17CustomWrapper { pub struct Service17CustomWrapper {
pub pus17_handler: PusService17TestHandler, pub pus17_handler: PusService17TestHandler<EcssTcInSharedStoreConverter>,
pub test_srv_event_sender: Sender<(EventU32, Option<Params>)>, pub test_srv_event_sender: Sender<(EventU32, Option<Params>)>,
} }
impl Service17CustomWrapper { impl Service17CustomWrapper {
pub fn handle_next_packet(&mut self) -> bool { pub fn handle_next_packet(&mut self) -> bool {
let res = self.pus17_handler.handle_next_packet(); let res = self.pus17_handler.handle_one_tc();
if res.is_err() { if res.is_err() {
warn!("PUS17 handler failed with error {:?}", res.unwrap_err()); warn!("PUS17 handler failed with error {:?}", res.unwrap_err());
return true; return true;
@@ -38,9 +38,13 @@ impl Service17CustomWrapper {
warn!("PUS17: Subservice {subservice} not implemented") warn!("PUS17: Subservice {subservice} not implemented")
} }
PusPacketHandlerResult::CustomSubservice(subservice, token) => { PusPacketHandlerResult::CustomSubservice(subservice, token) => {
let psb_mut = self.pus17_handler.psb_mut(); let (tc, _) = PusTcReader::new(
let buf = psb_mut.pus_buf; self.pus17_handler
let (tc, _) = PusTcReader::new(&buf).unwrap(); .service_helper
.tc_in_mem_converter
.tc_slice_raw(),
)
.unwrap();
let time_stamper = TimeProvider::from_now_with_u16_days().unwrap(); let time_stamper = TimeProvider::from_now_with_u16_days().unwrap();
let mut stamp_buf: [u8; 7] = [0; 7]; let mut stamp_buf: [u8; 7] = [0; 7];
time_stamper.write_to_bytes(&mut stamp_buf).unwrap(); time_stamper.write_to_bytes(&mut stamp_buf).unwrap();
@@ -49,12 +53,17 @@ impl Service17CustomWrapper {
self.test_srv_event_sender self.test_srv_event_sender
.send((TEST_EVENT.into(), None)) .send((TEST_EVENT.into(), None))
.expect("Sending test event failed"); .expect("Sending test event failed");
let start_token = psb_mut let start_token = self
.pus17_handler
.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_success(token, Some(&stamp_buf)) .start_success(token, Some(&stamp_buf))
.expect("Error sending start success"); .expect("Error sending start success");
psb_mut self.pus17_handler
.service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.completion_success(start_token, Some(&stamp_buf)) .completion_success(start_token, Some(&stamp_buf))
@@ -62,7 +71,8 @@ impl Service17CustomWrapper {
} else { } else {
let fail_data = [tc.subservice()]; let fail_data = [tc.subservice()];
self.pus17_handler self.pus17_handler
.psb_mut() .service_helper
.common
.verification_handler .verification_handler
.get_mut() .get_mut()
.start_failure( .start_failure(

View File

@@ -1,7 +1,8 @@
use derive_new::new;
use satrs_core::hk::HkRequest; use satrs_core::hk::HkRequest;
use satrs_core::mode::ModeRequest; use satrs_core::mode::ModeRequest;
use satrs_core::pus::verification::{TcStateAccepted, VerificationToken}; use satrs_core::pus::verification::{TcStateAccepted, VerificationToken};
use satrs_core::tmtc::TargetId; use satrs_example::TargetIdWithApid;
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
@@ -19,18 +20,12 @@ pub enum Request {
Action(ActionRequest), Action(ActionRequest),
} }
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug, new)]
pub struct TargetedRequest { pub struct TargetedRequest {
pub(crate) target_id: TargetId, pub(crate) target_id: TargetIdWithApid,
pub(crate) request: Request, pub(crate) request: Request,
} }
impl TargetedRequest {
pub fn new(target_id: TargetId, request: Request) -> Self {
Self { target_id, request }
}
}
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
pub struct RequestWithToken { pub struct RequestWithToken {
pub(crate) targeted_request: TargetedRequest, pub(crate) targeted_request: TargetedRequest,
@@ -39,7 +34,7 @@ pub struct RequestWithToken {
impl RequestWithToken { impl RequestWithToken {
pub fn new( pub fn new(
target_id: u32, target_id: TargetIdWithApid,
request: Request, request: Request,
token: VerificationToken<TcStateAccepted>, token: VerificationToken<TcStateAccepted>,
) -> Self { ) -> Self {

115
satrs-example/src/tcp.rs Normal file
View File

@@ -0,0 +1,115 @@
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
};
use log::{info, warn};
use satrs_core::{
hal::std::tcp_server::{ServerConfig, TcpSpacepacketsServer},
spacepackets::PacketId,
tmtc::{CcsdsDistributor, CcsdsError, TmPacketSourceCore},
};
use satrs_example::PUS_APID;
use crate::tmtc::MpscStoreAndSendError;
pub const PACKET_ID_LOOKUP: &[PacketId] = &[PacketId::const_tc(true, PUS_APID)];
#[derive(Default, Clone)]
pub struct SyncTcpTmSource {
tm_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
max_packets_stored: usize,
pub silent_packet_overwrite: bool,
}
impl SyncTcpTmSource {
pub fn new(max_packets_stored: usize) -> Self {
Self {
tm_queue: Arc::default(),
max_packets_stored,
silent_packet_overwrite: true,
}
}
pub fn add_tm(&mut self, tm: &[u8]) {
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec");
if tm_queue.len() > self.max_packets_stored {
if !self.silent_packet_overwrite {
warn!("TPC TM source is full, deleting oldest packet");
}
tm_queue.pop_front();
}
tm_queue.push_back(tm.to_vec());
}
}
impl TmPacketSourceCore for SyncTcpTmSource {
type Error = ();
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed");
if !tm_queue.is_empty() {
let next_vec = tm_queue.front().unwrap();
if buffer.len() < next_vec.len() {
panic!(
"provided buffer too small, must be at least {} bytes",
next_vec.len()
);
}
let next_vec = tm_queue.pop_front().unwrap();
buffer[0..next_vec.len()].copy_from_slice(&next_vec);
if next_vec.len() > 9 {
let service = next_vec[7];
let subservice = next_vec[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
return Ok(next_vec.len());
}
Ok(0)
}
}
pub struct TcpTask {
server: TcpSpacepacketsServer<
(),
CcsdsError<MpscStoreAndSendError>,
SyncTcpTmSource,
CcsdsDistributor<MpscStoreAndSendError>,
>,
}
impl TcpTask {
pub fn new(
cfg: ServerConfig,
tm_source: SyncTcpTmSource,
tc_receiver: CcsdsDistributor<MpscStoreAndSendError>,
) -> Result<Self, std::io::Error> {
Ok(Self {
server: TcpSpacepacketsServer::new(
cfg,
tm_source,
tc_receiver,
Box::new(PACKET_ID_LOOKUP),
)?,
})
}
pub fn periodic_operation(&mut self) {
loop {
let result = self.server.handle_next_connection();
match result {
Ok(conn_result) => {
info!(
"Served {} TMs and {} TCs for client {:?}",
conn_result.num_sent_tms, conn_result.num_received_tcs, conn_result.addr
);
}
Err(e) => {
warn!("TCP server error: {e:?}");
}
}
}
}
}

View File

@@ -1,26 +1,20 @@
use log::{info, warn}; use log::warn;
use satrs_core::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc};
use std::net::SocketAddr; use satrs_core::spacepackets::SpHeader;
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError}; use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
use std::thread;
use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use crate::ccsds::CcsdsReceiver; use crate::pus::PusReceiver;
use crate::pus::{PusReceiver, PusTcMpscRouter}; use satrs_core::pool::{PoolProviderMemInPlace, SharedStaticMemoryPool, StoreAddr, StoreError};
use satrs_core::pool::{SharedPool, StoreAddr, StoreError};
use satrs_core::pus::verification::StdVerifReporterWithSender;
use satrs_core::pus::{ReceivesEcssPusTc, TcAddrWithToken};
use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket; use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::SpHeader;
use satrs_core::tmtc::tm_helper::SharedTmStore; use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::{CcsdsDistributor, CcsdsError, ReceivesCcsdsTc}; use satrs_core::tmtc::ReceivesCcsdsTc;
pub struct TmArgs { pub struct TmArgs {
pub tm_store: SharedTmStore, pub tm_store: SharedTmStore,
pub tm_sink_sender: Sender<StoreAddr>, pub tm_sink_sender: Sender<StoreAddr>,
pub tm_server_rx: Receiver<StoreAddr>, pub tm_udp_server_rx: Receiver<StoreAddr>,
} }
pub struct TcArgs { pub struct TcArgs {
@@ -40,14 +34,14 @@ pub enum MpscStoreAndSendError {
#[error("Store error: {0}")] #[error("Store error: {0}")]
Store(#[from] StoreError), Store(#[from] StoreError),
#[error("TC send error: {0}")] #[error("TC send error: {0}")]
TcSend(#[from] SendError<TcAddrWithToken>), TcSend(#[from] SendError<EcssTcAndToken>),
#[error("TMTC send error: {0}")] #[error("TMTC send error: {0}")]
TmTcSend(#[from] SendError<StoreAddr>), TmTcSend(#[from] SendError<StoreAddr>),
} }
#[derive(Clone)] #[derive(Clone)]
pub struct TcStore { pub struct TcStore {
pub pool: SharedPool, pub pool: SharedStaticMemoryPool,
} }
impl TcStore { impl TcStore {
@@ -64,12 +58,6 @@ pub struct TmFunnel {
pub tm_server_tx: Sender<StoreAddr>, pub tm_server_tx: Sender<StoreAddr>,
} }
pub struct UdpTmtcServer {
udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
tm_rx: Receiver<StoreAddr>,
tm_store: SharedPool,
}
#[derive(Clone)] #[derive(Clone)]
pub struct PusTcSource { pub struct PusTcSource {
pub tc_source: Sender<StoreAddr>, pub tc_source: Sender<StoreAddr>,
@@ -98,131 +86,63 @@ impl ReceivesCcsdsTc for PusTcSource {
} }
} }
pub fn core_tmtc_task( pub struct TmtcTask {
socket_addr: SocketAddr, tc_args: TcArgs,
mut tc_args: TcArgs, tc_buf: [u8; 4096],
tm_args: TmArgs, pus_receiver: PusReceiver,
verif_reporter: StdVerifReporterWithSender,
pus_router: PusTcMpscRouter,
) {
let mut pus_receiver = PusReceiver::new(verif_reporter, pus_router);
let ccsds_receiver = CcsdsReceiver {
tc_source: tc_args.tc_source.clone(),
};
let ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
let udp_tc_server = UdpTcServer::new(socket_addr, 2048, Box::new(ccsds_distributor))
.expect("creating UDP TMTC server failed");
let mut udp_tmtc_server = UdpTmtcServer {
udp_tc_server,
tm_rx: tm_args.tm_server_rx,
tm_store: tm_args.tm_store.clone_backing_pool(),
};
let mut tc_buf: [u8; 4096] = [0; 4096];
loop {
core_tmtc_loop(
&mut udp_tmtc_server,
&mut tc_args,
&mut tc_buf,
&mut pus_receiver,
);
thread::sleep(Duration::from_millis(400));
}
} }
fn core_tmtc_loop( impl TmtcTask {
udp_tmtc_server: &mut UdpTmtcServer, pub fn new(tc_args: TcArgs, pus_receiver: PusReceiver) -> Self {
tc_args: &mut TcArgs, Self {
tc_buf: &mut [u8], tc_args,
pus_receiver: &mut PusReceiver, tc_buf: [0; 4096],
) { pus_receiver,
while poll_tc_server(udp_tmtc_server) {}
match tc_args.tc_receiver.try_recv() {
Ok(addr) => {
let pool = tc_args
.tc_source
.tc_store
.pool
.read()
.expect("locking tc pool failed");
let data = pool.read(&addr).expect("reading pool failed");
tc_buf[0..data.len()].copy_from_slice(data);
drop(pool);
match PusTcReader::new(tc_buf) {
Ok((pus_tc, _)) => {
pus_receiver
.handle_tc_packet(addr, pus_tc.service(), &pus_tc)
.ok();
}
Err(e) => {
warn!("error creating PUS TC from raw data: {e}");
warn!("raw data: {tc_buf:x?}");
}
}
}
Err(e) => {
if let TryRecvError::Disconnected = e {
warn!("tmtc thread: sender disconnected")
}
} }
} }
if let Some(recv_addr) = udp_tmtc_server.udp_tc_server.last_sender() {
core_tm_handling(udp_tmtc_server, &recv_addr);
}
}
fn poll_tc_server(udp_tmtc_server: &mut UdpTmtcServer) -> bool { pub fn periodic_operation(&mut self) {
match udp_tmtc_server.udp_tc_server.try_recv_tc() { self.poll_tc();
Ok(_) => true, }
Err(e) => match e {
ReceiveResult::ReceiverError(e) => match e { pub fn poll_tc(&mut self) -> bool {
CcsdsError::ByteConversionError(e) => { match self.tc_args.tc_receiver.try_recv() {
warn!("packet error: {e:?}"); Ok(addr) => {
true let pool = self
.tc_args
.tc_source
.tc_store
.pool
.read()
.expect("locking tc pool failed");
let data = pool.read(&addr).expect("reading pool failed");
self.tc_buf[0..data.len()].copy_from_slice(data);
drop(pool);
match PusTcReader::new(&self.tc_buf) {
Ok((pus_tc, _)) => {
self.pus_receiver
.handle_tc_packet(
satrs_core::pus::TcInMemory::StoreAddr(addr),
pus_tc.service(),
&pus_tc,
)
.ok();
true
}
Err(e) => {
warn!("error creating PUS TC from raw data: {e}");
warn!("raw data: {:x?}", self.tc_buf);
true
}
} }
CcsdsError::CustomError(e) => { }
warn!("mpsc store and send error {e:?}"); Err(e) => match e {
true TryRecvError::Empty => false,
TryRecvError::Disconnected => {
warn!("tmtc thread: sender disconnected");
false
} }
}, },
ReceiveResult::IoError(e) => {
warn!("IO error {e}");
false
}
ReceiveResult::NothingReceived => false,
},
}
}
fn core_tm_handling(udp_tmtc_server: &mut UdpTmtcServer, recv_addr: &SocketAddr) {
while let Ok(addr) = udp_tmtc_server.tm_rx.try_recv() {
let store_lock = udp_tmtc_server.tm_store.write();
if store_lock.is_err() {
warn!("Locking TM store failed");
continue;
}
let mut store_lock = store_lock.unwrap();
let pg = store_lock.read_with_guard(addr);
let read_res = pg.read();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
if buf.len() > 9 {
let service = buf[7];
let subservice = buf[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = udp_tmtc_server.udp_tc_server.socket.send_to(buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
} }
} }
} }

76
satrs-example/src/udp.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::{net::SocketAddr, sync::mpsc::Receiver};
use log::{info, warn};
use satrs_core::{
hal::std::udp_server::{ReceiveResult, UdpTcServer},
pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr},
tmtc::CcsdsError,
};
use crate::tmtc::MpscStoreAndSendError;
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
pub tm_rx: Receiver<StoreAddr>,
pub tm_store: SharedStaticMemoryPool,
}
impl UdpTmtcServer {
pub fn periodic_operation(&mut self) {
while self.poll_tc_server() {}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.send_tm_to_udp_client(&recv_addr);
}
}
fn poll_tc_server(&mut self) -> bool {
match self.udp_tc_server.try_recv_tc() {
Ok(_) => true,
Err(e) => match e {
ReceiveResult::ReceiverError(e) => match e {
CcsdsError::ByteConversionError(e) => {
warn!("packet error: {e:?}");
true
}
CcsdsError::CustomError(e) => {
warn!("mpsc store and send error {e:?}");
true
}
},
ReceiveResult::IoError(e) => {
warn!("IO error {e}");
false
}
ReceiveResult::NothingReceived => false,
},
}
}
fn send_tm_to_udp_client(&mut self, recv_addr: &SocketAddr) {
while let Ok(addr) = self.tm_rx.try_recv() {
let store_lock = self.tm_store.write();
if store_lock.is_err() {
warn!("Locking TM store failed");
continue;
}
let mut store_lock = store_lock.unwrap();
let pg = store_lock.read_with_guard(addr);
let read_res = pg.read();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
if buf.len() > 9 {
let service = buf[7];
let subservice = buf[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = self.udp_tc_server.socket.send_to(buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}

View File

@@ -23,7 +23,8 @@ version = "1"
optional = true optional = true
[dependencies.satrs-core] [dependencies.satrs-core]
version = "0.1.0-alpha.1" path = "../satrs-core"
# version = "0.1.0-alpha.1"
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" # git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
# branch = "main" # branch = "main"
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89" # rev = "35e1f7a983f6535c5571186e361fe101d4306b89"

View File

@@ -20,7 +20,8 @@ quote = "1"
proc-macro2 = "1" proc-macro2 = "1"
[dependencies.satrs-core] [dependencies.satrs-core]
version = "0.1.0-alpha.1" path = "../../satrs-core"
# version = "0.1.0-alpha.1"
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" # git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
# branch = "main" # branch = "main"
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89" # rev = "35e1f7a983f6535c5571186e361fe101d4306b89"