119 Commits

Author SHA1 Message Date
d017b9c179 Refactored pool abstraction
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Redesigned PoolProvider and PoolProviderWithGuards to allow
  easer optimizations and increase flexbility
2024-02-10 11:59:26 +01:00
18a5095d0f Merge pull request 'come on.. show it' (#102) from deployment-correction into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #102
2024-02-09 00:44:35 +01:00
66d9da23b3 come on.. show it
Some checks are pending
Rust/sat-rs/pipeline/head Build started...
2024-02-09 00:43:20 +01:00
930da294ad Merge pull request 'prepare next alpha release' (#101) from prep-satrs-core-v0.1.0-alpha.2 into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #101
2024-02-08 18:11:06 +01:00
729ef4be05 prepare next alpha release
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
2024-02-08 17:55:37 +01:00
0eb1a9cb08 Merge pull request 'Example Update 2' (#100) from update-example-2 into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #100
2024-02-08 17:50:24 +01:00
5ccc50d9ec link correction
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-08 17:45:03 +01:00
176a9f1612 Improvements for example and documentation
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Added diagrams for sat-rs example for both structure and data flow.
- Added explanations for those diagrams as well.
- Some renaming: Use `Pool` instead of `Store` for pool components.
- General improvements for satrs-book.
2024-02-08 17:42:36 +01:00
cb8405ca65 Merge pull request 'Update Example' (#99) from update-example into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #99
2024-02-07 18:21:03 +01:00
f68221a73f small link replacements
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build started...
2024-02-07 18:17:14 +01:00
0fd70c08c2 Major example update
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Increased example modularization by moving the majority
  of app logic inside dedicated modules
- Added a new `dyn_tmtc` feature for the satrs-example which is used
  to configure the heap as the backing store for TMTC packages instead
  of static stores.
- Added dedicated satrs-example chapter in satrs-book
2024-02-07 18:10:47 +01:00
28da48ca6e Merge pull request 'PUS Scheduler test additions' (#98) from pus-scheduler-test-additions into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #98
2024-02-05 16:17:03 +01:00
292ba1f1cd PUS schedule update
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Added new API to generate PUS scheduling telecommands
- Added more tests for PUS scheduling
2024-02-05 15:58:01 +01:00
8a5b81b67f Merge pull request 'CFDP filestore unittests' (#97) from cfdp-filestore-unittests into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #97
2024-02-05 12:12:38 +01:00
414bda6751 CFDP filestore update
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
1. Added modular checksum implementation
2. Added remaining unit tests to have acceptable coverage
2024-02-05 12:06:12 +01:00
d1bc00f27c Merge pull request 'introduce simplification for backing traits' (#96) from pus-service-optimization into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #96
2024-02-03 13:53:14 +01:00
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
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
70e535e397 Merge pull request 'bump mib versions' (#84) from bump-mib-versions into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #84
2023-09-26 23:02:56 +02:00
b13e9b59ac bump mib versions
Some checks are pending
Rust/sat-rs/pipeline/head Build started...
2023-09-26 23:02:30 +02:00
d21e98d2e5 start adding tcp server 2023-09-26 23:00:47 +02:00
89fd44f752 Merge pull request 'bump mib dependencies' (#83) from bump-mib-deps into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #83
2023-09-26 18:24:10 +02:00
466206e133 better
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build started...
2023-09-26 18:23:59 +02:00
e0b8280c41 bump README
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-26 18:21:10 +02:00
d20e205c32 small fix
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2023-09-26 18:14:07 +02:00
777630c499 bump mib dependencies
Some checks failed
Rust/sat-rs/pipeline/head There was a failure building this commit
2023-09-26 18:07:41 +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
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
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
075dc38434 first implementation of apid with target id, currently main broken 2023-08-15 08:49:54 +02:00
90 changed files with 13376 additions and 3043 deletions

View File

@ -5,7 +5,9 @@ sat-rs
This is the repository of the sat-rs framework. Its primary goal is to provide re-usable components
to write on-board software for remote systems like rovers or satellites. It is specifically written
for the special requirements for these systems.
for the special requirements for these systems. You can find an overview of the project and the
link to the [more high-level sat-rs book](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS documentation website](https://absatsw.irs.uni-stuttgart.de/sat-rs.html).
A lot of the architecture and general design considerations are based on the
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
@ -16,6 +18,10 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
This project currently contains following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
Primary information resource in addition to the API documentation, hosted
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read
this first before delving into the example application and the API documentation.
* [`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core):
Core components of sat-rs.
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-example):
@ -37,3 +43,16 @@ Each project has its own `CHANGELOG.md`.
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)
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

@ -14,8 +14,13 @@ RUN rustup install nightly && \
rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \
rustup component add rustfmt clippy
WORKDIR "/tmp"
# RUN cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
RUN curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.34/mdbook-v0.4.34-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory /usr/local/bin
RUN curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory /usr/local/bin
RUN curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o mdbook-linkcheck.zip && \
unzip mdbook-linkcheck.zip && \
chmod +x mdbook-linkcheck && \
cp mdbook-linkcheck /usr/local/bin
# SSH stuff to allow deployment to doc server
RUN adduser --uid 114 jenkins

View File

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

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"?>
<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 for="port" id="d1" yfiles.type="portgraphics"/>
<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: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: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:ShapeNode>
</data>
@ -31,7 +31,7 @@
<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: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:ShapeNode>
</data>
@ -39,10 +39,10 @@
<node id="n2">
<data key="d6">
<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: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>
<y:Shape type="rectangle"/>
</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: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
<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>
<y:Shape type="rectangle"/>
</y:ShapeNode>
@ -63,10 +63,10 @@ Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n4">
<data key="d6">
<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: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>
<y:Shape type="rectangle"/>
</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: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
<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>
<y:Shape type="rectangle"/>
</y:ShapeNode>
@ -87,10 +87,10 @@ Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n6">
<data key="d6">
<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: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
<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"/>
@ -100,10 +100,10 @@ Event Reporting
<node id="n7">
<data key="d6">
<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: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>
<y:Shape type="rectangle"/>
</y:ShapeNode>
@ -112,10 +112,10 @@ Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel>
<node id="n8">
<data key="d6">
<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: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>
<y:Shape type="rectangle"/>
</y:ShapeNode>
@ -124,10 +124,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
<node id="n9">
<data key="d6">
<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: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
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">
<data key="d10">
<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: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>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
@ -161,7 +161,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</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
<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>
<y:BendStyle smoothed="false"/>
</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: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
<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>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
@ -187,8 +187,8 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</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: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="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:PolyLineEdge>
</data>
@ -196,10 +196,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<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:Path sx="-80.36000000000001" sy="0.0" tx="28.333962264150955" 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: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:PolyLineEdge>
</data>
@ -207,10 +207,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<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:Path sx="32.38107215104537" sy="0.0" tx="-22.34892784895453" 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: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:PolyLineEdge>
</data>
@ -219,11 +219,11 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<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:Point x="653.48" 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
<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>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
@ -232,10 +232,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<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:Path sx="-35.69940886699487" sy="0.0" tx="-9.780492610837314" 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: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:PolyLineEdge>
</data>
@ -243,10 +243,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<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:Path sx="28.960000000000036" sy="0.0" tx="0.0" ty="-21.423522388059723"/>
<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
<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>
<y:BendStyle smoothed="false"/>
</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,991 @@
<?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" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="111.16238125000103" width="641.0000000000002" x="809.2454000000014" y="463.9111499999988"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="641.0000000000002" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="6" leftF="6.400000000000091" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 7</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n0:">
<node id="n0::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="77.16238125000098" width="598.8461000000009" x="835.7654000000015" y="482.9111499999988"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="297.42305000000044" y="36.58119062500049">
<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="n0::n1" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="54.0" width="157.5999999999999" x="830.6454000000015" y="503.13804374999916"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="157.5999999999999" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="8" rightF="7.67999999999995" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 5</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n0::n1:">
<node id="n0::n1::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="845.6454000000015" y="522.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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="n0::n1::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="952.5654000000014" y="522.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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>
</graph>
</node>
<node id="n0::n2" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="56.25" width="171.68000000000018" x="965.5654000000014" y="502.88804374999916"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="171.68000000000018" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="7" rightF="7.039999999999964" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="1565.2374000000002" y="273.72953125000004"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 8</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n0::n2:">
<node id="n0::n2::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="980.5654000000014" y="521.8880437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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="n0::n2::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="1102.2054000000016" y="524.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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>
</graph>
</node>
<node id="n0::n3" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="50.0" width="164.0" x="1271.2454000000016" y="509.13804374999916"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="164.0" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 8</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n0::n3:">
<node id="n0::n3::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="1286.2454000000016" y="524.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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="n0::n3::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="1407.2454000000016" y="524.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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>
</graph>
</node>
<node id="n0::n4" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="50.0" width="178.3169499999999" x="1107.9284500000017" y="509.13804374999916"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="178.3169499999999" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="5" leftF="5.120000000000118" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 9</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n0::n4:">
<node id="n0::n4::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="1128.0484500000018" y="524.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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="n0::n4::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="13.0" x="1258.2454000000016" y="524.1380437499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.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>
</graph>
</node>
</graph>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="415.0" width="739.300200000002" x="695.3113000000005" y="568.8694874999993"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="367.650100000001" y="205.5">
<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" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="290.08000000000004" width="195.36000000000013" x="1210.809400000001" y="618.4077874999996"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="195.36000000000013" x="13.645244799999773" xml:space="preserve" y="10.027339123239472">PUS Stack<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4301533333333345" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46543250440140804" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="10" leftF="10.240000000000236" right="5" rightF="5.119999999999891" top="26" topF="26.431249999999977"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 1</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n2:">
<node id="n2::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="703.4877874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="128.39453125" x="10.802734375" xml:space="preserve" y="6.015625">PUS 3 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="n2::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="743.4877874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.529296875" x="33.2353515625" xml:space="preserve" y="6.015625">PUS 5 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="n2::n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="783.4877874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.9453125" x="31.52734375" xml:space="preserve" y="6.015625">PUS 8 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="n2::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="863.4877874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.205078125" x="36.8974609375" xml:space="preserve" y="6.015625">PUS 17 Test<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::n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="823.4877874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="116.8515625" x="16.57421875" xml:space="preserve" y="6.015625">PUS 11 Scheduling<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::n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="660.8077874999996"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.255859375" x="19.3720703125" xml:space="preserve" y="-0.96875">PUS 1 Verification
Reporter<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>
</graph>
</node>
<node id="n3" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="105.13453125000012" width="155.4000000000001" x="711.4787000000007" y="632.7428312499998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="155.4000000000001" x="12.272399999999493" xml:space="preserve" y="3.249732314531343">Application
Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4210270270270303" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46908977216245173" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="5" rightF="5.400000000000091" top="45" topF="45.13453125000012"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n3:">
<node id="n3::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="726.4787000000007" y="692.8773624999999"/>
<y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.2216796875" x="25.38916015625" xml:space="preserve" y="4.8515625">ACS Task<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>
</graph>
</node>
<node id="n4" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="227.60000000000014" width="310.3248000000002" x="882.3347000000007" y="682.1055312499999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="310.3248000000002" x="23.48672560479929" xml:space="preserve" y="6.686613155747068">TMTC Components<y:LabelModel><y:SmartNodeLabelModel distance="0.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.42431566666666864" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.4706212075758038" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="5" bottomF="5.1200000000000045" left="0" leftF="0.0" right="5" rightF="5.324800000000096" top="26" topF="26.432000000000016"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 7</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n4:">
<node id="n4::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="1052.3347000000008" y="788.14553125"/>
<y:Fill color="#CCFFFF" 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="65.93359375" x="27.033203125" xml:space="preserve" y="6.015625">TM Funnel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="902.3347000000007" y="859.58553125"/>
<y:Fill color="#CCFFFF" 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="72.42578125" x="23.787109375" xml:space="preserve" y="6.015625">UDP Server<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::n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="1047.3347000000006" y="859.58553125"/>
<y:Fill color="#CCFFFF" 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.111328125" x="24.9443359375" xml:space="preserve" y="6.015625">TCP Server<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::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="902.3347000000007" y="788.14553125"/>
<y:Fill color="#CCFFFF" 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="65.001953125" x="27.4990234375" xml:space="preserve" y="6.015625">TC Source<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::n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="897.3347000000007" y="723.5375312499999"/>
<y:Fill color="#CCFFFF" 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="83.904296875" x="18.0478515625" xml:space="preserve" y="6.015625">PUS Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="130.44000000000005" x="958.2147000000009" y="946.9855312500001"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.5302734375" x="42.95486328125003" xml:space="preserve" y="4.8515625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="127.03999999999996" x="1030.4707000000008" y="629.3055312499998"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="109.9228515625" x="8.558574218749982" xml:space="preserve" y="4.8515625">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="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="120.0" x="896.8787000000008" y="621.6432750000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.1650390625" x="17.91748046875" xml:space="preserve" y="1.703125">Shared
TMTC Pools<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" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="106.85641458333345" width="167.16000000000008" x="678.4854000000014" y="467.9111499999987"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="167.16000000000008" x="0.0" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 9</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n8:">
<node id="n8::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="76.8564145833335" width="137.16000000000008" x="693.4854000000014" y="482.9111499999987"/>
<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="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="59.875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="118.25" x="9.455000000000041" xml:space="preserve" y="8.490707291666752">satrs-example
Data Flow
Diagram<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>
</graph>
</node>
<edge id="n0::n1::e0" source="n0::n1::n0" target="n0::n1::n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="4.0"/>
<y:Arrows source="standard" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.5234375" x="22.12435476073938" xml:space="preserve" y="-29.312517773438344">TMTC<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="18.0" distanceToCenter="true" position="left" ratio="0.48378541003594444" 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="n4::e0" source="n4::n1" target="n4::n3">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999983203125566" y="-22.719979003906246">
<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.5" segment="0"/>
</y:ModelParameter>
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n4::e1" source="n4::n2" target="n4::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="5.000000000000227" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="standard" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n4::e2" source="n4::n0" target="n4::n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="27.85279999999989" ty="1.470468750000009">
<y:Point x="1112.3347000000008" y="829.09977125"/>
<y:Point x="990.1875000000006" y="829.09977125"/>
</y:Path>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n4::e3" source="n4::n3" target="n4::n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="-16.954559999999674" ty="3.7001374999999825">
<y:Point x="962.3347000000007" y="839.58553125"/>
<y:Point x="1090.380140000001" y="839.58553125"/>
</y:Path>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="standard" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n4::e4" source="n4::n3" target="n4::n4">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="5.0" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e0" source="n2" target="n4::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="39.69774375000043" tx="0.0" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e1" source="n2" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-51.37930695586169" sy="-119.14225624999972" tx="60.74632526271894" ty="0.0"/>
<y:LineStyle color="#993300" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n3" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-19.839999999999918" sy="0.0" tx="-38.399999999999864" ty="8.753668750000086">
<y:Point x="769.3387000000008" y="593.0482125000003"/>
<y:Point x="1055.5907000000009" y="593.0482125000003"/>
</y:Path>
<y:LineStyle color="#993300" type="line" width="2.0"/>
<y:Arrows source="standard" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n0::n2::e0" source="n0::n2::n1" target="n0::n2::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="-2.25" tx="0.0" ty="0.0"/>
<y:LineStyle color="#993300" type="line" width="4.0"/>
<y:Arrows source="standard" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.171875" x="-76.64202462463709" xml:space="preserve" y="-31.1845177734383">Events<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="19.872000000000014" distanceToCenter="true" position="right" ratio="0.33295067336054324" 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="n3" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="52.53735479999898" sy="29.121903125000244" tx="0.1231654464985258" ty="139.1977687499998">
<y:Point x="841.7160547999997" y="918.67033125"/>
<y:Point x="1308.6125654464995" y="918.67033125"/>
</y:Path>
<y:LineStyle color="#0000FF" type="line" width="2.0"/>
<y:Arrows source="standard" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n0::n3::e0" source="n0::n3::n1" target="n0::n3::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#FF6600" type="line" width="4.0"/>
<y:Arrows source="standard" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.6328125" x="-91.81636757812339" xml:space="preserve" y="-48.84101777343835">Function
Interface<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="28.21599999999995" 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>
<edge id="e4" source="n4::n4" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="-0.049743750000288856" tx="-82.89626674268379" ty="-24.959999999999923"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n3" target="n2::n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="7.053999999999746" sy="-52.567265625000005" tx="57.5920000000001" ty="0.0">
<y:Point x="796.2327000000005" y="581.6432750000002"/>
<y:Point x="1368.6414000000013" y="581.6432750000002"/>
</y:Path>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999972949219227" y="-30.999879003906244">
<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.5" segment="0"/>
</y:ModelParameter>
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n2" target="n2::n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="47.144060319997834" ty="0.0">
<y:Point x="1415.2947000000008" y="763.4477874999995"/>
<y:Point x="1415.2947000000008" y="675.8077874999996"/>
</y:Path>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n6" target="n2::n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="39.02668799999979" sy="0.0" tx="0.0" ty="-8.276800000000094">
<y:Point x="1133.0173880000004" y="750.2109874999995"/>
</y:Path>
<y:LineStyle color="#993300" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n3" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="-38.78246875000036" tx="0.0" ty="4.884353124999166"/>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e9" source="n2" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-65.73714292000136" sy="-118.80000000000018" tx="35.35927123999659" ty="-20.0">
<y:Point x="1242.7522570799997" y="601.6432750000002"/>
<y:Point x="992.2379712399974" y="601.6432750000002"/>
</y:Path>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e10" source="n2::n4" target="n4::n3">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="60.0" ty="0.0">
<y:Point x="1415.2947000000008" y="838.4877874999996"/>
<y:Point x="1415.2947000000008" y="926.9855312500001"/>
<y:Point x="1042.3347000000008" y="926.9855312500001"/>
<y:Point x="1042.3347000000008" y="803.14553125"/>
</y:Path>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e11" source="n5" target="n4::n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-45.90000000000032" sy="0.0" tx="15.199999999999932" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="standard" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e12" source="n5" target="n4::n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="46.63667953667937" sy="0.0" tx="-37.26332046332027" ty="0.0"/>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="standard" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999968403867797" y="-27.220816406249924">
<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.5" segment="0"/>
</y:ModelParameter>
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e13" source="n3" target="n4::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="66.54799999999989" sy="0.0" tx="-35.74228584000252" ty="-3.0175312500000473">
<y:Point x="855.7267000000006" y="770.8415312499999"/>
<y:Point x="1076.5924141599983" y="770.8415312499999"/>
</y:Path>
<y:LineStyle color="#008000" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e14" source="n4" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="18.251857503227473" sy="-77.84525000000008" tx="-38.24174249677253" ty="3.238468750000152"/>
<y:LineStyle color="#993300" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="n0::n4::e0" source="n0::n4::n1" target="n0::n4::n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#0000FF" type="line" width="4.0"/>
<y:Arrows source="standard" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="82.4609375" x="-88.20577865560745" xml:space="preserve" y="-48.84101777343835">Other
Messages<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="28.21599999999995" distanceToCenter="true" position="right" ratio="0.030113173151242907" 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="e15" source="n4::n4" target="n2::n5">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="-7.129743750000216" tx="0.0" ty="-0.10225624999964111">
<y:Point x="1112.3347000000008" y="731.4077874999997"/>
<y:Point x="1112.3347000000008" y="675.7055312499999"/>
</y:Path>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e16" source="n4" target="n7">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-80.61839999999995" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

@ -0,0 +1,954 @@
%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:20240208153304+01'00')
/ModDate (D:20240208153304+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[]96a;XrZ`X=6'nI']0G-Xc?4G)r9+Z>EKNG2R4%q0^eCF&Ru6"F&ec4BQ"pOe/h16:(d[p&-0iM
R9opj8<89"/O*Y3#Ch."r:0gdGZOb<A+'"-pfj+Mq>To#iqpb<5Q:*ns8CKnJ,8':s.;OT9E+^)s8Q;e
r9++WDnl>Ls7O/,aM7WLDu]Utnc/F"n6>@gGouoFD;Jt3gAG(frg14qpF?3tR.eLKCMf1E/rJd]h<YjS
mGD7bARfQBp#S*PVrM^S]f.Y2^LOJIa,g8s^MJN%[*$]\r9NuN+9200PLa+)s7ij6s.&\UXn'$/;7*dX
+oEqIa/dnh_Q<ct^&B*<rr#>I+0]Xq54JJRl^sr%oMRkoH[Tc(Fl@8Z9pkV2Z0MAfYO,X[qi%`Gd4bFb
=KSY'48t^Vqiu:fHcg^5au[k`^K@Dm^]*8Q5%E@BWd<I+oq8Sqs6^S!NA^TG55<+UL]2)'-GIDO<*oE6
)<?%%bJ-rqR7sfj>PcQ*g/,>on`NHfJ,ED%VO=X%VKLU@NFeI&ObCIOn'>/J?L+qW?ZCN,D&FCelSgfg
o6]qNBj>G_ci!"Sph]Qid=+g?lO3Z4)eDqCCc0@ZC7fSIMUeJml=]@ri`D1E24RK,J_.nX#6t4/^9EZc
rI;jB_O\\Z9ejMUa(J^[O+g0#S]bY\^LA;$9,QB$Yh.M$hJr1d%+3EYORT<EIPf8tZ/n`8e+u&&<blX9
(OFIFkVZ(A57YuSV7]FdBA813CCtfJn]F1rmRunIPU;mQ([A2I?c/;f^0/XYqnb8VI%&r<S\b"9.;B>l
5?G>*`353h&-(`NU[`NjB3j'nI.d/rQb]RjVh+%bd&DrNW"^J)TsgI$03"Cj"dNEOg&&Z93'YHW$L'WJ
"(+@_FVHk"]p?q,95tqN^qm3Hk<!0_[O^U5^jh:7:53r#rm)dAZ0q%VYCATD[8*K*Ds73Z)U?Zh5*jf&
?iQp:'R_k&aQZEh5PP*F?bX#tH+T.SbiANaMq;_@:lj'&>fljN7!e#ED5IDBBEJ]9bn@^riKja+$:5-m
p@7540@QMqrVn6G&6GY,?qL3mp%!1k>R>1Q5!sTejfTF!G2.aRruJ0>hd<@(IOW\]m13-f'e/=t>W_q.
g`3*g4')<p[o0u:a66[_aR\*#s36fiVk)-M/*W6r1-;IU]`7DJMAkg"^UD:Rk'Q'ReAO">?=.a:.lF:G
D7i?&UX&2+0@@Mg5e4W!B<B[eU$U6G1=^@?nM1E^n9<fW]OG\7IEl3iX*8'PIf0`$7pfQphspIm<V:is
k7R0$n*`>=J,_LC=J&\5=2p'ik7g+pGZfUEgFk-l\:<X2q08rIXhK;T,mb*0Ih+^+_tmJqmedst2<=un
lpK5,4rUbu(!=l]SQP"^ojhFqa5$GND8#)WHN./b%=q]d^:[8KA9I5&R?ps?^)!1SC+QD+Fkbm.6hbUn
]?T%hgh$iXhJB+u4O<m#rPNEsZ_i%AoC8B.[Ea+Rca%-gZT0u@5D2#IEg8!&IBB_CV4uN7e:$_98>lGq
-lGHD$WN/sBAI)\\E\kLo#6C+gO61SMf_O_Y];u8U]/[@lu*Wuf_SV%::k.]R%PU=PZ_oC-;k-iOq1&F
q<&,*eVn8)/Aj[SkKf:\-.k&$!Ptf$=hP`XZ5P-OnZ3]T0LUfmX#(\AVR$[m-X)SGIQ3ss@kg%U^OEp!
=b"3Cq@=G](k4PNr\LcOeS+$C-SC1p/t>5B!A7$;ld0rWd2)=:.d:FW(et*!V@A'WDZ1VWBdPEqI3k#=
SF;S"CWYcMH=Ut;;/s;Tp@7eJe0;]%F\r)YT2gF68EA5s]k>AHB!7oUp:A;ONYu@PcG??P:to\X26bIC
*5.DL]]KKalNMDml'hL)cX8s!e8p)T_pbQ,[L(<<`'2%@.9fT\l*!q^2gf.`gpf^99V]oAXU[aI#**re
%W087/RA-<R:Xm,kQc)!.qXh7"/hO>(EGF,S[CVV[5PD-=OapZq-Rf#LQ+PW51UbKprE:aG5AKC4!'QX
HTiR9.oH.M<;PkSa:[eX:cP#Rs7Q96Ps9C6nDbkGCh/^((ZO<>E7LJN>KH))ksd0D)HQul4d!R%ZVq@6
[G/UqE0RU<]Pq@dnE4>oOPS0m_e-#9dU*:.>6apsD,rMPki^ef8^(qc]M1q3KcDX_?9Pp20@4ZIO(5#S
lVoBc3qdJ.hAn0WnB(q:(FH55YHLi2G0mkAb5(=P^"68EDE>JIYEMpPg$#0h`Qcl`p'emQ*9dN@G/Eta
Y<@p+b$(gNm!L_2fd0ts+(of\4g`bqqX6L_dQ_$6Mf`Y8B^7/b?,lDs)M4;gbSuhE]%pt.E?6X31*UD$
HLl?hI>OY,ZYF8!l!7jR]g`)g_LP?r^22hd*d"AOZQ%0)H#nk?*UDPhMJ#PH:`_Ro,o_ha1c)iIq4nH&
qEm?SbYd]@qYsY2VCYu:/sb566b-Re/#4=\kPC:[d/%bJQbU4@r`Ai6J*h\FQSfgGfWt6]#>=DF<n;c@
Irbklr)u0DX6XQ'UrV)C]lhnUa8aA4R<3eDA5BN/p@!KX^Ke7-YH#W#Vnf`JAtPg;2B>mH][AHL4Ki+j
r3alA]rn;]IAPhD?J'(aBERAm`h0\HY/dgAd9\tP[U63!XBid%Jb/C`)1J\A^VQdg$m3Ug'bC(1^&A]>
<4I9DJ)7I)kE#5$UW;(pBANG&\AB$-osFm/^VPi^9%Z82L<fO%&_EIg?.WECi*lClPIqGXc$\)[6W;(k
@^i/]Ap%Ou=2^tBaLnK*Hn<UmLY"09:bbpiTLI*V*90'Vq0.3`X?n_Wj.mQn`gs;^*j'`"H@>Lj#:2`a
CJ;Y"3d0UNa\Z7[IhO$.g/XU:b=!h4EXcM.S+J\o=u3[LDs03rAng;;B>bZMH=QG4)s"#1].`*##0e^r
2gjo.edQqL4pP*JiNiS-PRJl1]euQLp>Q;l6g`7(<GSM[ZDt#DYGnn5m]`m*CT!e%=P_3)T!eKhA3I5$
+u1f-l__]BDc6JP(Q\([n$pb,=3UtlE2iUN,\UJsY-_4>#6354&Jr]GTe/Z9mAmUp=(e,Z0[8#k57O'U
RGF_Xm:HYhjqg/-n`tMrp1N<DB^Z=QcI1B!eW(Mq;>tsr4?<^e7-<1,=W/[lD-jSqTup\Mk?iRE]-o8*
UENI=,<#&.M.84gn&^#=b^L.m[#9SRqhng!E(Ymr-1!ej<Kj$E_&f$$l09o6a-Nl+pi(%>5rtBV@t-SJ
+8Tb'c3p>\KDH""^TamDXDOED>:R\ap[8gMYj+Y;Tc`Uj%_Y0#PA?EnSZC2_Y9"e8Ti/jC-uESb*\D_.
B:c:o6gu.Ye!h2$AGjkfY,YcsA#ob*<KhqBH=%ZL+n<kJj;sS<ZRX^HPcIu>hBib9oX:rFd@:%CN^&p?
[Lsea6.fapeDFa,_L>#RGJ%Lp3'LZX$=?W8+K6N'#'NiKK@7cA6YkmDPZbFt[uss>O+(BGMY.'Y(;Ph<
S,@YW+pSiMSJ9#Fr&$]._56tr66%*5p3Z^crY@To=T9BhXVpk\?kjU.G497t0qO\fiHqBf\Ot\m^KXt?
86*#&r#K-`.:`g)=S5IJo?q!D`<gmMqJ9ba^?MsQrDfQgAtj`1CWJV9VU'!"QS7?P22&`OU_,u@4.M]f
X38"eB%eUjs7=ZV<jt\R=fGV.7$rJjJ*Uq!U5*rim&F%e7=Vp=dm!\rYE9U.CNLt>(Fthd7dEoJn=oC'
9l[XKB"cg7WAuon1Ac.#R=dDK=b_!:"YlKs-q43L]e3u%S(]a"NM)5%ebW[7>fW#eY(>cRY(JZ0^P"O"
g3qoo?QJJ#&WugSR%ponqFIaERgt:$'NE4CH$qg>$];,rj_bMG^>s+Dak^^=/h#D5bo0j6g9_&cUC>Kf
>iGqIZ/sP'"\Q6]\B8Y=Y0=Jc`ii)5L$QfPZjt>!1f&bQMTDncqB:5KF$DPKgs;7:^OO:A%B`+rW3Dm?
XML108kk9oLGcTE\ZkVUHYm">n-aC<d]C82cDNP0A0%+$E^jaPhX_,ibA2U?l!;Wj'e4fsPP1+lr;-F"
q[iGXZ0MAZ6T`3;^:Q]*!llsO=W?i)<PQ!^;bIgMh\,ZCn<aY]VC[$jO+6jGGi+N/qWu"QOO]Wj%Ae]"
c/pH\hH4_dl25;L.#$!nX='a&W1<?7T),6"nF7nCi(gKG'A'!,LuEnCh-:XY0e"T)d4]NZ)2b.IN0mqf
"LNkRX*p6_15B.Z@\;SXXB"-A)LMEMNV2h3?g]`3;sa"q!65?W)?0ll@t2Knq\MA_HQ[-M;23@E2g<?!
4`/mI'f.9l"ha`e4N-%F"+<U&[X9R?0ls:Ci>sdjHi@TT'/`-^DrD;LS,NFV+r@UNOc>FYK(Tt1+n_He
kZ>k>%h/;a^Wd#Xq3/P_KGee=%mB[+^L0ImZ,="D[&mW:d6DDRMu:@(ZT^HR:u#a6=s36Ka&UhbJ!9NU
T<luN9,mW5.)er.Os;m9:Z.BQdph]#2uG8j2/>.!&EUAmIa&4<Ad]@F-Q5TD*:(+,1GtTjAP;CKU^mb$
+2n*g66sSESjYrX_C?R>\uBT<K@u/78WQUA0VBKp]aQKg\lJ"tb'\!6bW"dL9KrS&$J]Q]VNk,LN&nhV
E:M8@4@dbMUW@KZ*=:UnQc@s-OlnL#..2?tUA,U5M%LRK`V3A9(aCcM)EG&Xjrcj)(-sqd4dtHcV#GhV
ocGs;eZ0&CL\WINno$!6(FndQY!1<`lV4W'Q9qA$Rai_?M;Ej,mj.=Y)nt#jjInR!d*`CBCE[l4R;oVs
!tF^OiQtWHbBV($2>I&mm-@Ko>Tq\K1Tj(m_>IVp3%"#N.s!rk]d;e&8U)tmdLK6,L>lC<97E`S#*E-b
Dn<5uQFm8_J)AuU,Bn`%9"a!SZBaP23!n?RFXjPs4gN,#%q^d&U(hl#1BkqT:\D3i%pjcnYj;2c[QaDo
FfE-X9>FX-:7cdkR-F-G:V*W`Q>]G=`bU&7Wn:M%-hCm0X..7-Bc-XW`SCd(,]i^-E/#Kh6SaFf4g6)o
SlfqPb$3=YW7"9t=\87G0\]97ai!5mFrUA@L:1YocrSq0fWHiJpWNIYj<U[RY2`"_SM*2r[L@4E>k?Y:
j=4pA_(iJAN#HBFGCSR4jfcaVs6q%=[_k*i*=#B5Km0'SGQT[A\!Qj^XImt^ERqOt;52S<Rff(j>Q`$S
?BTWd$9<ucciAZW7/t5f,r+Za)I_50/bqSeVe%b,"?sY<(Zrof+3XKI/8[fMAUhYp72M#s3Tg<r\64bs
F,9VjTbH!(=tF/[m][Mrdea1rhs9!0>1H8b3nFM]&FFF!g`]XU(=E\l-sF#=SjH^M=WXjh90pbNq(j9G
bDI4@W<`:\[Y/6U[.kKdgBhtQL,$I9=Bm@%-/,2HL0M2@/Pq4%S)6`ing0>]"4nb:[O[uGCGRW`eG>Sa
BN_heLfCgog@HV:ZIV/`Zeb412'^Y5An5P"dpQCIUsHoj=SRg^1Krjl.E[pD+9'H;I4*cTbWaHV2ojJp
+#Ec*0>CMt8r"t`&p2dS]VNs[c]E_tr%tehb3Xa]RoV1g+)OiEhNDQ"V^,lrZ5:QI*p?N4>l\58BF8:L
G\Q8@iMe<V<ms4&'6LNDAEFi5'I`/s/(!X#rj'u$=_);+c!q4ILod%ur]Y.4dT`P8Q:_[6BBf7AGt9ON
?!t.tr(Xr\@Q#?QB9'MA>PYg6>PZF45/&8CD=@deEhr9+j945.Xa5'<C0J=7PL@_pV9X#AU%5U.b0P`=
PN!nfK'-oakC_FiNnrA$%s@`I<p\+tefO8)QI[,3SZTrdZeCV<<dg2Jmd"\5[W8J0iVV*lCal:;1`qkK
cB^4Xc!FMXf$#aOZC7!Soq;0h4de<TAmL]Ua*-.X9D^HuSZPI9+\.ZlgmsQpjQ4#Ka$$u[8@3c$\NAi<
\3!nUQ,)5/OtY%&BBra1Hj$T.=1q\?V4EV5cB`"#]/*?>]/0"3G@s=X41kCirAQONrmf5h^GnS3(L-Xn
N"boTEQ/;Ec8;6o48pT``!U\lSrYf$1ER&co$:/4*WL1f)]Rtt:7h$8;rkmS_L#9jepk_Hs.;8brB06A
b9Z.uP`Jl!1YXHj#CGR-OTJja=Ct.cUD8#.4I;#gO*ie0l1Gdt(HiilGDj(Y)_D$<o?m,0"LFTJPYVEe
pk=f*KBUcM-UpqBbKd#O72/L_a$JB75_K>OGf%N>eh:m$j6^aL2gj/MhrB>P3\dgK^JlHf?BMl*o.<d"
(`C@IVo:Wih8N&8I!&SkHF0%uVuA<g7i(FACSYW&kV,Op:s!UHE!fqD2'&a4,bA7$ot.H9]aPpr`q_7O
TCJc&N/"Z:Q/m3(/!*4C]M*8/Qe7k'?->+a#U\U.X*q3BMEm'YMGKi>-s*CBa7uJd'@a1**:0_AF4RH`
6r%`2N(1S#8C?j"LJ/V<9#ui%"Y]]j`CjsiYs]poYi9XKbt#EA=IBMrs,NAd&U<%m2!4fY$g41+ATF$2
bf%D#-2#@00Joq+%eLu$3J8,[<;jTaifYr?\_.Fj?C;cZPq/'YT]):Y5`s/3*95A[`Bh90>HCMYa*a7U
U7?W..tS1WN@ZTTl,F<$%Xdtc#7Z_:+&oELo"iEma<b12KJ.H/%pIf+R7F6^p;R/+<s6((;^<DM5,`c1
=\'-8i-XV;,=2k[GpiOg48&sN_:'XMVe=[-a9VAgJpbS&4G%sBk>N6U4P,It.T`V4bNsjBW<(?-_:p3U
RScJ7+7fl.A=g)t"Sip[S^u)B$KV4-Dt%O6b$Z5TlcSoQ7JV9X^U4d%,!NYFC*9R(jmTlZA%n/s;/+7q
UHB=<m'T#*?Z-c"/?95&#?ufS6+'F>GtZ,k_lR3a7qp,^/*Ltdj+BHAQt%.<Bju7jBqkOL(+FN+4+enD
?gC%B4*':g-]8I$:.EoMQWY-#Mm>dsNX=3ZSgf_:&aTJ28sQ+%#H.],j9er+6_KStgN3@=ndda\e)EX\
<oB/1-"Ij`W9Zaj0Zm.1l@^XE/&US0@!tF\9IY/if0T*+AKm[QSVU)=\U)+(HBdTJF7c."n^2i"`N!6=
fU3MY[;R'lXd(Yk:<]$(5'QL'Xg/Bo8u]i@SZ@eWHr6//Mc+9"j!?[@Gs\U%8@`I%9$?lKBR1cWZs7n*
Ee^c?K"lfUY2sCBhj(6Te8k4VbLMB6X'1W@Bm&U\*8DU3e;0n?IL2XHW*[a?UD^%mTrM?sdH[/bg&:Eq
H/;67=YA$iaZP&2c^t'mPs>"KDE0kp@/&8cb6[qJT/4Lkm;I<hKGXTE_OY<r+#quDcRb@%GeRbHY0/=1
G]H2J0@dnh]i#(EpIZHq0OJ.cK;X'mN`F`emrWA47rBP@WNltFL;fN4YLLDE]i(cKah:RWouejSls;-l
VH8X,d49@R\hC6$HU4jbl/?Ztdm<Zt8tE6T-s]L&e-)!gY>ehlZZs,\H0NV42$1lM,n-uAIL32WG&Iu*
;Wg-j]i(Rgr(a0)l1.H;Z'DAs1,C=sTQPOV7+4aX20t[&qfqZBG>+9G1VbLe%'LB>,P4U!?HgSCF`;K,
eEO;P$2=eePregWIf;.WiEdsQU:aC`e&K)F@ZP=Y8WK*<lHt)pG$*XNH\&CrGTI9CRB=1pdJ^Dd3S*np
6Z=_BD$.C8HIfSn/4/,V2D75*5g4E6?5)_WVB%@>gURF[$j0#eg+Dp1%+]"H)r&?qZe#B\eV&?"@r;Y[
3F^\oq<]Iq3t5TaZjJ[I=XD_<GV&LRG^.)Nqu5cs9k-,HFll>AgMEIITK*e^7aSAq>[t7<Cp)7)>DDmi
ZcpgBl(W(Pj$WGj#r]'U_-M+$b)3%LNZ?qm>onbESb2kJ[52VZpa6obp"@t[N=4'e,!Mb$VADYsFme`B
W:V3%\B0Lp?O==)NKl507^:2BGLW-qJ^(JnMU>0CSf"(YJ'ZG5r^Qfi?4G,hFRu@7IWa"K0uan++H(t-
+E61\:D/)raXM\Gj9X8j#GV!$+7=84g9%#RpuA>l[ncgu)DVOLGt)l@:lS5HY$l[uIHK>rdL*rgVFtDA
0CtlE?leFS?;TZmBangIXNXDkqmpnM)Fl_!8E72q)(84G`fV$f"<=&Q\k85<L0c>>c95)E]%<mU[(40g
o"BV7_7JG[c9_'M3:r\:iuHQSj.-,"LW3K.X,%[,'4XPopO6qOI;>?(c8CTF`RXH6-ng,i-VIW:j((TH
9]K[F:/g36'KI)_UPOr%rna&t*1`EGcFSd0aYhA[GIq-`M%tK?I;'[1f/T[OF,e)'W,XX5dQ!sc1lgcQ
Sas-u%\?b$[+!sS?*BA$GA%uOE$gS)L`4<Ri]O)2j62L<BcQ5>6K?-2cQi9:NMFllj.*@bf/TK41_0J,
Y&Oq(Y&WY5UrF"0`Y%?.3H`?6_=*u]]l)MR`f\J:VlY\SJ%=aa9=H+)rPNp<2%I"q2%DJJ29r=2W,ZG)
cp=;\'W/?13Eknu7?5s8>eB<W]RS/Q&lO`j@*"`kXc@ONg4<L#l8gq=cps@Rf<T$U;!Jaa7]ESc=l,=_
d4.-#^`9J/BPZ*_Td>ASU\18nXM^4"^Q>3Zm!C@nn;]Y%Mgc`0BX+bbd5i[]3EkV3_fN0p8teE=GjIs\
94$=_#LU_`Y8j\)8L-01*b8WS)+Zbk_NURiqjofmD_IO#P*3gF"2Z]=lQShlFA\'!@-jG<Oe[%1G=(qW
EJ:%ULLK9oq*S-A:>tj)c3PWM][ZP,G&X8S'?IAud5hPK*i,N-k%>(JFl[3Vb\UI3L\:`0Mq5aAT4gmE
++)ZYGs"iLBWN]6m6mAt$sb[F_o80=s)?7PNn/iDl(d!to0OPTVEJ;Qf:+i^r-FKC>cl.HQCN5C+*L)@
@=s7-eP!MtEr/5<f;i3E54R1#]Y5G#BI,!sR'#A:W(">0a[TXnG5=Wjg@RUE73m'lAQGLHBl.-ALM1o'
48A*<c%-pAoEE47g[#;3`'gd1HsL@1PX<O.it,2.m\Rdq?TdbK0V0RYMV"oYZobG12:S^un8p#8i\W8&
g]l[1n]LUL(OgZ9).r8F=`Nrr*].&J8#MIVc9NEMmI1uAKcYL*W!Xc,"fK)Ie5j]?0'@kr]s!nXfqQ<Y
3INrWJaG/o@GF@VMEG(>(Q5pe\]%X6H^[*MY(L@ll[/Yq<m5TV0k0313krb.eNN&G4-_2,h:Q(cL6XhK
EP+%B2bRkKpLmW2EVuj/;7X#tf\b%1O#(G.J&Ws(LE,iG-2WE2k3\HNa&fD4`6(pns1`(#8q&%].1\+X
2@%iLE*R8&+*S-/"TIpD4*(KY(Ra,h5=]>Bic05h99m_B#-4O@V-:d7q6.b+e/'^YhELRH8S?^OO*G,&
r^pYo=0aa_Q)4=kO8+b4/(k=#G$S23jlufXXb/YUV4HHPZYa^)/SqKQq5ZDSA`T>N<o#gkgcH#s3AKgX
)7A>(r0<_1_?duKH$T8PCECMSY>RU:\3X!GfDVEerKY@J$^k>6q1h#_]iYG!GOncI+r.\k\+%#R;Qf:"
:Cdn24?ScFJ_M&b>i&0E)Z5l[7/8?%)>G`$_i7N`(0P]cDuHSmCe5m';I]AA1jK_YHdI/:Sj8qh]a)f!
VpghVs0H]>o=Sl2RchLdlK;(Q&C9`/q_XkN1@Kdd%Pt__aLqh=#O>hgCd$e22tn@MS_H$j6.6n#]E`;p
ZV*J"r%5SeD%`<ahK75>XgUs<.dd\5G+FJd1UM1$e:q/0a\QQUfq]Y4HdK-s:kQ4l&KGAk8P7a&C!EoC
X40p)%]<3L%pF=eX148YH!7&iiD.):cq3+Z<*S2]5OI.,fl6B\ktgb6l`j)]s$NtHAJ_+`En<$*nu8eq
?0".^McQ*g%iiuDi&G]tH0b,!n2Y[\!Vr5AK/U9(`D#MYNc]=i8'lV0[+:0JFJ8GZ-QIO^e>bW4$)Pj'
*eE*ncu]nrZP72Npmo$SV0V15o,9AS/_S-BLGn7tUXV/;&%QoQ!OW!B#LjU_*o-VR+23Et:M4qOeMfSD
=nm")GK\^b*5%?00M<S(i=ed*2REor5(ghMa'"TgIQn9CY'R#6KS2F]m7f^)Ohb=I?VRSp`g%?ImBG'm
UW1h@a*>>T`^e@nRF?0,@#UJf0`2rn=6.<(?a3XUCT[Jk<3Dg7CT0\Q&B+p+(KX''EaqV^G"k1(29lQ#
S]XZMM"MQ47L8o4f^`:]ID##Nj0c:Dg=Ls$8#tM]1#1!e\o?dk&)WGf*K$EF*j+uSXCHqu_e+=b%5Q6-
G:#$jODEJs2'26o2Z%*ngME,PC2<Onlrnk;^T>'8n^r`n$\$X`l,JFC^u1Pr?YfE6SN^d'Z^GSLo4V=l
TRMm(.pp]*<\H<leUL_gNQ9eUe)m0-`=/_97#<:h*qP.)m209L4%PBScUb=1pCB/gm,DK\:@6j,,sc^/
q-[DOm<nLk[VrO^$](meA.bZ$7@PNKahVG@FVA?Anqt$+VW*Wq5U3=ILFf\_,g5CbpYHq/oon99*)3l(
;X3W*]7\0n*6f*DoZ#TsjkB*:7k2armt27rFe]IfRB;U";C633+=a\]M4'E7B8k/AUPIZ;]#jrgCp_Kk
K($f;p,^.2;0LDoP\0KQfcWF\s)\@n_.YdV2FC+dCF)&ji'*12.a*[Vl-ns]BlHJ7`7#d3Rg02D>$):g
R:cJ)ipA)*-oFb@\hqREGcUG:QMG1V\kW1iNTf1@cHqq!17M^eoL<]/Wq[P0e%F[`(Z.N4gg1EN/:)pR
2YIRQ(<42KpVbK`AFCEi("p2:X'p"*=8LO*L/q(/cFqr/"E%+%)62qe,OXma23`_s7fSZKd+_24om@ui
YR](cUfOFuj/=[^<ps8-@)"ZIldRo4Y&Pd:7?')HcdXpagLsmQN6Tq5^W'`^d<I4.OXnH4MN$!"\u'J%
ZlV*MkbP4mS,qlO`c4D+6VNKW?W,nmBEHdFFro9ncBt,7)sK%>*ZBg/EK+H]&@f`JM>!1U^:gOcBq/>I
S#11;&Qb;aY&T8sEU8-:QRk3=F_Mi-.Es`q^j8bHK)UPFYuBRX$;F!L1Bt6!23+4dGE"BsooWsY+^(__
+a/j_g=/(ank>$!L=*gpcB(4INPcf8Vm-3(EU:)i.,EB;(E\CO+^M$dVTG5_N[&9Y@VQnP[knTW3&Jq5
>WGL<fR"I<EPUJ*EQI%?EU8,Uf/TF]1lgD=E+tiiXE8`%%ZNj:)Ma[uB?8BSUjfj3%]+^pLGDAj1)@8e
nGBdhNOP^'./0la5]:?VC(+jI>JF"p%,)c%6fST88/c.@(>Gl"hbjC_\"_+&.D*q"2Kf/Upi-=s+3>sa
l_F8HlpS4#("!ThnpUR/^VW0IrP4%`A0)OKR;Gj/JACJq+/R1.DDsT-$>P#q,/$K7I#YnhWd<Zt?_`@'
@\`ttgO-dD?(Lg!n#@*aJPBr/R>dNrDo"3Mo(]:QHceHZ$i5eYDF`2``-=F3^SIK0(!b;IC6uM@TF]C9
[[Uk.SBk9#9l?e5ek#ldD:pQ:UHi:SFqAg@K.&6V]!UA1*H)HLO`HYM@Z6H%H%IW\j!rs9)WWWBH1nY^
0=57so(:"<dc&dKLV:7$r=#ZLm9isqY9qU`UT_V>g,ca:&Lfq4*p$SRQPNG%M7YcP.97e'\EE#P-C/*G
Z4Xo[NV[USMpfDu2s0Bu]Wc=m*1jRLK21&,:i>ZT!*f->LT"6$+qRE(%%\@;h7U,9ek7*Z+kkn"P2XqF
>1sUbYhKfOFQbQa"tNVA/WKqU*!on';0Cgi=JljS=-fYo(8(9bk:SbqPn'NSDT6?8-'R?i%4A,e]Cq_T
TsaXUQe&rF,&Su@7eRs1c^iu!J`c-SAD+9*\?Fe\?g\l03G?-rp5#)u?17UC<AZp^gP%KIM7Ho*e_;)1
b[$le"G'a>X\-gd9))sJlu+&nF"Q2<DjV;-NuIH>A:K.i[X=?`m?=R!Tf$BlJP+?aT]"4B2H&p)C"EtV
]^b?rl%NM<P\>ND++'3"Ji79n9q_r<$dHJlQE+!^<:c=,.01@s3--IMSe_XfM42q=O!s4o4dO&>nB5>#
%(&[,l^Gj6dX`HrVo^%+nJI7PG5<<-DO59^>kG0]0gq$BYfL_]-HC&m\M@J38@!#U6Q'i9oWZmlg#J'X
Bg1mNRL6_mI%)D',0*k`;h(kXC>WBT+ni;GrHlTMhsk4V]pfkEl0>p+#-oQ(TtoR&4'?Cq>P6mYfDfL?
l</Vs`f'NPkdG/6qO"8I2VrT8DoYkFp#!%>+%eX"s$(RM_4TSnIrJc7=dpdFp%fPNnKgX!`GSqUg^m'k
]eloGj/k07:T@ie?G#08.dN5<1*g#s@)`iTbDu7Qm<85>i2bWdZ:FI3U;'E'h*!rJ5a/TVq6mMV;>HN^
>-G/efWRd5h6E\%?9N@JP4aLb,kPmE5VYU`M,fkV%*EES>`nj4L"HHR`/"Ej];=OJ_G.-uN3&^k+7D;B
G`<n;O5(FP7fAtcn3/T*^rkuJ4!5oV.L`EqdOgO!hH2bi,/*r"^(.F`dr'CUZ5m@c?'Js0AA>Kq3NmTr
\\Dm'h0G'3csP?EpNZ$.e6h`03^%FRoIs8<dPS\oR<[*PIPdfP]H5c&;6Vk<p^5o>4elpm9"OPZI6H>m
)^YH`Qm)-Yo<l"&hXgInT3DKN:ANZf:US?SZjai>PfVmNY;3p9/jWtdoboB#o19oITA(.@T)L@5P/Rht
=W-d;:o`>Q0_tF8coCVg57TCFF84kTge[h0<8Inq1G:DB9AM:j,M/Ao_f$tCk)VM>Scqr)EM"o/fHGJ$
GF4flm*+OaSfO@QKqc0:A]koIn(g:<?SHE<i<*\R?oCWnScI8aN`o5=)U%oM3/(3G^9B%L0Z+]=r3efQ
WBA7DZhZEg.Qr0LemJ/'5r*;l4,//2dn;PIRA5W<.(/u(+hi\)Y;_<qr9Y\Hc&432^MNkG$ZeCBU;X6W
URG1!)SgI)q:m;P":[Y(<qe%]_hjt_DR6oTnFps1j<>*g1WV\9VRO1djm^H8$KF<6pPg'Z'rUiKPt3is
G10'8FuuO:nLNElbI8Hkm'=X,DR8[]]45kNN]$%fV42dN??0C12q/2YA#1_5XPUF8JM"Jh8%XluAp9Tp
qUVq3"Pp*HF,c(Fd9/1+k5DR<WfE*O:YMiNDu+Fg0uJ4G-:q?eiZ9)SdOe&^Ot6Ccb#O=!XFQ)hU-pc'
BeHNd7LMeYl1$4%@r;Wn<eg(5P^pd-)j3&JSVkfgI>@AI5NiP5=^h?B&H*k39$L/KQ8CNjHC*gs#rPN;
P--RtW[#pGoc!-_XtgU!Ir4iZdK4>VU((Tp4?%D^Uj8(eGcHC@b)0bYa)$a5A2$2]Fn:#$`7;a&67uL/
o:X]2Lt35Za/q;uAOjHpg\#nMQ=OB_(a&>pg#",JS,9<E"*'p1jI*B*1mt`VSre(&IQ;fni[79HcJk`2
.G`:KZ<aB(T@&AWdQa:TU^Q<s45kCqoEZ^6Vanra2>spiaYPLO8oo2Z?UjLKDHZ/uQW6!aRru8^\+&QT
jIWnO_>U5AI7"IN3!*9g45Er"A*9fHJtjq<p&!3Q":dL3p"qK?q!39F39m9Rn+a3JC.dRI<V<fg(,j,0
e,U\pkd?L7R8%=lU<RjaJ_Y\W`[`)+V(s.%ZjnZ*HrZU=4ZZd0gfKoN6:2j1T+Ij)HL)*8,`;rq<]f\4
Fb?if8s_\:jS]h5Mp)%[)<p^8SIYQIWPfj?Je.^,h%]<_`G3PHm7S=tF9TOMc!,WqAO#NFk>]"3fipI)
0nRR(MnRr"?QNB%Dtg!#<].Kh)*Qt?-&VRL[WgAZ2Nu=6=mSP*Zs2lb^:nnio,\KL*&>XjKq>C1,A"J^
#C_q;@ZR%2(O\mFcCGDRFi8PZ#$02\lOSC.QRB3\j3up9BJ371fr9@E0b*]Dh$0FW1>Dcp8^u\1$#)Op
[1_5PT6;R:el1?5Rl4tn\+-=iBsKgNJ[-Km_EOQ]IYV2ZW3pi>h8d!A`5lRkchJ8f>-^i%`e:oAs1UI,
4ufMgm1rMZ`.LMgm1qJCG1eJ7j-ihAj-j?#\CNU\fS-$nZM&L``tt?<KA'I4Y4m[fY>jo=X0!e38irYY
gj-8%mNM+TEle7g2rdo'2RRWTE7R4#"oMdV5c8MVcW0kbFX,<i9,Es*i\-Dt;R8K!c0N6$Z<NuCUS]i<
RH5_=0A1&4g0^W`/8)SW3=gC$(=8?fpB@!p(M*H7-2i#e\g,Tk^'Nc_/@jH02g:9u=8PVM"ZrXH>]_NM
S&[4oX#.B39e*]u?#tG$>GTr.^q6=L@=JYrW_i-N0(:E/CTg8!3u203qA8#^cQ6]h5bf+QIJ;nEj]1$f
P.fOX-c#+"6Zg.*g,L$b\(![bgD.0/jg-dKh&i*Giic73^Q3cEi:"q$C%N+bFj^(ob)uUZI5kMmc6l>q
G_n6\f@p'0$>2b.KI/iOJI>\6L7A)JUoK\a2?r#_TZhD`+MW!*E.1lfp;jl'EmHhWW>,WoFJ-VjGj"\L
0@39ZPgXo._MAEJ]X\Dri.??W>*ATTon.2,gZj=B/D?$8.l\i1NV#R.C3$F07CtUHDYK0](\?q:WMO?(
ZI1*!g-ApVnFa\=3IUc'iO*[N<%Sj;;uD[R7d:/i9'RR2[M6k8-+`0Q2Y%KH9@h63/mfTh=\)[RW:_R^
65>j.I^SpCnB5>%-+$=I]gQdBBaoLC_)$8HiPeZjLkc=,%\Bql8kPqf4`;71<J^9Z>bla0q1fq9/^Kbs
*8>7o74Z=;G@A,43\5tCCa+kB22`^K261*qY#=YF;fYW$O"\XL=>>)B26lllZM*H"mF?]fb%5N&fWf=\
)>H;_d^uFN>2j,HeFL!5MWQlWQ.Od=Z*UMJB)?a$2"4L/IYZH<H]`<sDb_a*9W>2^G?@aLS#j:!_06AB
.Bcn_-=:8Q+ie^fpI/)*-q`;*m6Ha*W60U);I5\jiXN84.Bg&O.HOQY/%k^,Y>sKM*LH=H^X_D7O=TCZ
rHB]orEct6FGi_9ebVidYL6T$j]1/!oLPQ;8i\W&*5@`&f.&kB@o`HRU`@mN*7kdYA%Pd,3jh2oTLuno
*&NnEP=$b[2uiB,cc$r(arQt9o1P>=3i,'\7<(nEeI#l.>d`mkPh42<'W%@@Cf4T/iem]3%&LTp`G>8j
B>15?mf0b&0Pn4W32TBV9IY?sab[V6i%i`f\lF5-ZA4CkWBHT"<^S48DN!=#?c[=,Z*U^%N7]RCQ]]j;
CTi`M>CZTjlnZH3PS(>l)<#dE7bEXYYBYfB=T*F]C$"29[7jlWM<ZZ;p-tq&85$>hd%a!uNA<go)/4r>
&<i6B`AJWN(8nVP^=)7DctX\`mFnHsm>O_.R\CJeVP.OR*'tORq1ilt.ah$/EGRDY3nlD-Bhd!dEVF5E
];%B^J0T"NC%_2eJCc%k9qFbq8<]BRn.j0'gPEaQ7dg7S7QdP20I#GXQ)q+FiE5nPrtddN%#`-^nnsr'
B_Hq"l\=jtb(jPHc1He!drX0b<k$k1Ya:h@HBfMeG9a0dBRGl(R^iWCD\fR#-s`!PH^cYdcb2X=SQkqe
O!H%8DobaN[tg^$f/8K8+ab6%Ousg\IP?+_lCKZuACp7H@KqZ*?7fcD`Pg&rCH'_k9uVrpG0dYSmeQ*E
kiqdhoQ:,]:'"Yf2YhE<pHGWJ6r%`^#Td4Lo(YtH\*KFpU/V=UFlclpm/7AfK[G$YH2Q*7eP1.QnPrC^
4hBnTV0?I:GhsjOLd$)*339SA^&5[GJ$nM6-sLe?Pdngs_YW`rZ4H?q6$caa3fZBX'BBTEbtsuko4:\"
_gI<)3g23U(AX0\"[_U=(LMj>6<6%'Ml\ba)hrPtPC0Y7ROGn-[l4u2W'&FEW`pN$hN%pGEQf#Es,O"l
$7Tl%omJ)SH89e@]iCY6_YE`)[mkZ.X\j#M25Kt&hd_>jf;kF5A[XdM7OM7'H[bdj@\uRq^m2ir0*3KG
d3_1aN@t_fg8Bc\2Qi/6Z(L_/j7"O(deEH.AZ2h=jA5!?6_=%$Te1%laAZ`C7:ojOGRK:9bC%-FaJ3Tj
h@5Mt2t&D]e_Te!Zk&-q^LXL$[gO\MX.Z,3FOQQj0='o*o1E9WG3FUM<;ChbHmMM\,JVadb=*HW<a"a.
(?0#Z<Jm>Da%Oq+RaITR'mWCT=aZqmU9U!:/^7Yt"Kof=r/MYnjA!#234%7W21KN\rqFF!:uUr[M&a2'
8]2L6;9H.%;AD!\dqGJ6?KKCqi]1qQpH5;BR8(i]Au@(7?68XeU/_Gd12"jCO'N]NrsQ/``,.!S?G5o8
?eXt5n%gjfZQnpOn^iD3NZYWX$fC$AhgKa]&Wst4'sFC^8W.CsT1R'ci`&:-el8.LTt@122Q#0+-'74?
NqYNLbYK5dIXfD"LYq/lrs;iRm4FO3]k%i&\'3fTh#>/Ak<H.gIm;-0:9;_.=jFsWeSO\>4Rj-Jq6bi+
=4^4Bc0e`[drt/M=jGKtNm91g,0p9:AsP6&frpTl#?@ss%3lAd4_!gcqs-82p\:*VCAXXkfb"(8B/?c7
AW!-fb]:.9=qBniF+R_&QF"9(EKF`l'?(6.R@1s"MK3C^Z^ghkAi[1fb]7<F4j06<oIOY5iRnk]mlTr]
:9=8`K-JBU$&T:@Q8A#<:,-dY#4mh:q(t2Q)-K@[\]W!:)pE'(1L<IVaC(K=Bk(HHZEYFXgE']ZRWsl]
+_Ea<[i)W\=heFlEX!T/.>GJEjr5u.o+')9?KUVpb]-j!Et)_!E."70J;c39c)ch1VJ!N^RaP7l6g>9H
bB"6_q880F3@WulA2t<MQLhPuB4mO:]iK/g,9s(Ia!j;t53q.kB=SMhaW6l<mTIgs4\BR6kfe'R2PTVB
cM$[uonfO/&b>LC4NlD41*:@7f9ta50RD&c[eE]1/j'MA6gF%"&(=)9Dl/7eV@mEl=jD3ea`JO81aB>s
mJRib;56c/%j@^DOb8l;e0l&tjc!OVB(O_,+bpT6di2P$\UpKgG\3j$8*r^t<bXHKVpf4%QF"88@f0j6
I#FhuS(D*eZ^ck8bF,c+jij$Xjc"I)T&jcA383S@g8p''Zi;ZJ>M9+SL$_W"$$];42dMg4D!(jormQkl
F_^EiL?W[J2m]#uOn<1lgC,I$LP+`s/8+O_B0"`"\V$=3CbOh9hX4.*c7EUT6cXF#M5K3!bAUZi*K]/+
,jhIo"RRg5,S?i;=jE6VAiWfPR%:a.TV/c\6F*$P832!]1[*Y_j\1ZM/E^6PG^`c+erZ?Ep[1(CfR42%
]#V"!P2d%,`,FeVFeB'uJ='nXHF48q,[j[E#p;a>]>l&E^kSL4ZF/F8rUajZh>:hKmG2R=noVSp:UINj
"Wm=3i0T[iC2\YEkt4s(J/!4i_hEcMpEQu'1?EU4a/qVDm44$^K"H^^D(l/d.IYK4S<EWM+-,rTphue>
_+^/jeYlj4a\J0R3K:QefmC5_XDX/Rk7?\G%HTrWrfT>[dY0CgHS+k4oVcb((VPMqNHnd;7^:cj(2KsV
kjcT]8ZW_DG^m><BTT]t'JaGFLSWI1:XL-0T-@pR04nfCn]k%goPq(^eQr]q,R/Nb+Yd,cV_H]!GH<?$
Aodg$Q"@#mD]!P(G`i0#fR7)imlPuW86UtDbcFfKEGaB`S[`\\=*^R*$^YK:8pXUJO7+f`SRi0bN\/JB
>QIRUn5-R9\G=HgWjXdG)'"hQK5?gl;WOThdSAIFS+<?&jf@0nhEqJ]a%iq%XY`Z\2c.+4UhUARa-I0k
W3PQ=3-M%!:q"N4[[?8W%D?4(11".^beSt38M[5499jW]7qZ-7Hcsm6Funn0UEaB0%_q@hUnHNbfd`23
*8)g*eg'`_(@>79X2"p=*'9;O&B@ON*Tg)6XuLUUI^a\eq1itV`#^Fh/W;Z4dZ0[OKmlk,b,.QN>=Htj
I$CHbMqKTBKCRBh?RFsS*n`$,h_Dl<OdoX7cB1SCp&A6Yq#q_W,P15e.`CK,EHbd(A+Se,>GBT5?`.S\
ke'@T4+6qGS^?(pLZlRQ5PFUsm2WJ.nnY"d<^&q3=!o6gd(JIkkf^0sWOIsPnW$!&nFuD(IJLr6kem!p
^np2`@^"T0k$uj7o>RPYqk(sKW)r5ZJ&M8EaMmB2bI<roNB6CCB9^..H[ZiF0!r"ni^'^%HIJV/os]0U
`kD?R*uWrp`iVX[OS;SIo,17Ue/j7=e>X4CP]]5uHdf-3[]<aiF0$In7(FMp7lN2dc!;)@(1b<6AnNCH
hZtOr&rEg^-d(^9(B%54M2hMlDtDq-F3HQ6\srDtbj00#j(:btX))@*U#WW+lY_X3Z2(NV]!Y9H=",q5
9'<K:af`("Kde(L](584B4>O5ZYlQYO$R"']^ktHo=F=lGI'ZL0)3sPQJ84;AhfK-WQc8iBT"r'g-?YT
p:)i#E[@Hnh=J2imHr-`mT:KtfBju\dj;*S0&(V@"FYN$GM?Q&=H?XeS9?^^[B<t\?95G;0;m7f]^@[!
m<5W!6tf(Im-HCs\lh*MI8X/a:$]9/KSHRliUe4NF0kn/O0_62*+NPdG?^<n0lFQVYIhnh:hh-!lZR)i
h>AS*-JG3'OjS=@pL%;d1.XIt]$]F'H/7O.cg,J$jaB*o3qKqFbL8VX)qcg6H/23,^Nq'\2)m4)$$ND!
$(4_`P3a13l__lT]BHP@37`,8/m145Q@$WTRTnOjMguR>*e`jGYJ2?KZJORUlPZm.q&,fLa'SeLLo(#p
IJX8S'\c:KLnt)Am)8TZ(H+M9<\1)P'G1AP13UT6bg+juV3no6`DV41dK$@N$@\sV$JLJA$GNNB$@8LM
$Mk^HP\YZ**7taG7k=hYLfd7Wm/9g5`T1l+/7l';LntOR$aCD=4'L-Mr.q3-Q_GJRNEAecl-;S+`DUHD
CP39m3I%BVSXf5a@it.]*YGt3@_1p##dD?I%'RUjKq5V9a?84#%XB@]H.Y^LK1AkYi1SVClBg`##>ZO6
BjjYl_Q:@T]@#$@CA74CO-V1nr0DoS$npAAs+2J]dd5/7V&Tm2I(Af3K5#"G5I+.qo6/;l8C<SEoS",]
NmbI@(H2f)B=eq)o<6jVH&jpl&PK[sO0Op9R8>H2:nA@oYs%,J/B-tuX]S1>O"6N*44C<68n,sZ/V1Zc
HRraq8#b;TZ5$"e4(q,8`R^$N=NY08&5*%]=GeeA.;!"*`DSDdLnuZfR8Q.Z+K7_%Ak+Bu;*udu@f,Sq
=GhA1[pgf7N88C_XH:itT)Yop8g+EgQ6KX1oNHmBVVaI[pcu*K9J/U^,s=5&^N%aYW@1MMe=BN[Z$*!X
fYDD5&OM^+Q82R>=N[;,.[Dh<`JC?aQ?LR/P+6jB^dcA3c+7G3^Ic)FK-Do7U.aK!>JKEs=A$$*.$c'Q
pkX4_[UL]JJ_l9\,Y=';UP@_AZ&$Y,$MiFd.>VL#LnuZ*L96@d`\4,PGY><+5u)5b'L+]"pW+C$mA?/E
nKq"',enpG8[4LOI?$Bg(3LR6HJ!cmcDXW5[UL]"/mS%WG0-SBoMds\Z_+Le"\LJ7Osh>o1W8-$2-l5@
jBLK$@dgmt^1Jfp)d>b?Y;K7$JS4i;09%?:g,4o\gC:qaPJ1r(jmr0H&Uim1.a=#](4N+Um0;C]MA-!S
;'>;[EW2T*_(RZDhK@auTO4<jinXU1i.Jamh^#;*]lfqMoUa`%34HYJ$hsp-9^_q*b'tWoF\n6tEZs<0
6-L95m$7EL%Ib+I4(N8Me;ebT\P4:[0:q^YX0ci*hF.6UpI%B;J+]Jn4VCYB"8CkrQ)uH>e_75a456a1
fpNn89;6>mKf6WILk'?TU9+>c:E5<rcg:*%%ht_,6Rm@5<K5#*.GMgF^cY/.*kg[rd^VHW&a*WDg0cjh
ka/e(,hbC@qcD+'&CD_rOMH.$7bk9I[ZPn%9ADs&.e33E;XhN;cI(RmS8XE/\1n23Q0^ebUgJm`fWJMV
I3t_S>1Is1=c_"TQ<Lh34lj*K=2c?H;4sFhG0G\t>>JqiW*NOjqPn,$HB/t_;b<4RiA%t0V&F(.b."o?
U/aW3dAeAodA$qpTs\m/)o)B/p_[VLnRQa:Aer,GHURA[4&gPK4)A]f>7eCWS8#`XZZ>Z,(f!ks2S!!M
AurK8nsf$i1X5+N?K<@J8TjEXckXg0:>g#%DjO/R6,+\>J"(mYS;5s@3d1p6h/o"G9ch4>%H)Uu*Klb6
hp0_)Y(o!I^LB.=]!*J:9=`bm[2Z_%I+@;&I]u>NVJZ!mLh"%iZW>+!3nX9f<Hj4!.U!@b^I>23]hZ85
)<)(1[[lgUQ>kTI^>o2[p4D-i472G3XX)KJI1n]Hd04pGM97t^HNW*fA>c1MX[SFu6T;\+FdR'mT[bsM
Y0Ct<gaq)pS9%4+idHZHWH_dI#12%GBSS`g^.U?ID3C>OS7@>fYO^UiSkeb0B/*FR*&20pQejH,!FGfG
7\>*=`_,U"AF3&$!`sN*QBG/`SHaSt)IJ6/W[T8SKtd`IB>,InDR!8P6f9KNOT8^]hCae9[^Eb)b(\(h
OiL'dMj8uNrpa241&pqdm'2_#5,aYfs.3eP3-V`;p`?D8lPk3LX$o6+`>8Eb'X;:WYl6_ucdVX8?e1r@
\TgF^l_6g<C8FmO5O4=%G4Qt=ep5HW+`dCYY\A?jb^O:&CoUd);id?Dct?@bLm.NqoG?PRj!l(1?YVg+
G)*FQUUo3n7mM9JL\C[_Q^W9#h4"o76)@>jd5c-Cn(19R^H8E`f6p/jFbXd"<&uj3S[)FDP5q]3Z3/&O
(ACL^/OA[E;S+k&#A(+q2LK4H;ESh,BU:qV1T=a(?`/(TB&K`OdA-9`hsqVDen^5O:udECPq-r"Qfa3X
(Oc8S13EE_e'%EM1HPd>Nm4.I:Mb=p-^_;g^mX`[Gm$hsP'B<_o+s.Bm<JhjY'R&u;FCesHrc2oAFeC6
q=59-El8u092FlQmbcqq&+TD_f^f@[J_nTq5^[l6O+=q;49?Ug,4%I)#,f(CD/;)\9#]"5o*AN0]?DM8
C%fe6!mW4c#k%NM8QDmoN)C=9Pe)da-Mq)U9g%D3YQfHcXhk"@pCOqDCTpB?0?1W7i^O>s8!J=>7"s#j
7SVHnefE#jMGc*Bg-S3"Mq=e2a`b.,UPSE".()/o3`#'J0qO;X%QEY>=h[X#WAAaOMl"dtLf+U08hE]A
Ol#_Vo=QrVYB]]om/b5D?/)`a-PW>6dR>*s/lCl=1>BIrD,Yb=3<Ji2HDH$`DghZTg58d$Q]<-EM(&&.
D%QD;ETkYj,+?0=P[e9h]#ubL.!:k&hj>.oa@Hd$;/4.(/PYW8G^A/Xe1LImi5Fh<ej3jY?mdp8d+`M;
<WdcO6Zb&B:'rrM''Tq0SZGI[b(V$3/R0LK@LrJbA?M2%NYJ<r&"U!ns,c*h>"ctLPhP5j;<k<4T1,`k
p4SisiVMXhM,LPIN!'ea,^@'mK&?+nl_;mY@c./NPhMpN7[+fNU=-LI]C:i$SkD*mn^Pu[[cY6j7W*r"
,F%#Gq7_YIL.f`=21O<X7e1@d;/1JY;<f/HnGYnAqAS78]R%)"`+8Cr4!s:Gqo'tkS^KQtqeK[)[9hBN
^IA_J,6X%=\GJ]T[-LS(IT96[oLMjE($HQE7&^X!Le?EHhsn?gRY^_C7I1JnM^$)Zim7*l9\B8:9CZKG
/ZsmVRiT!QCUSZR;U!:,TdsSQPB@V"g".n%mr5\H*a^e^NVCN@N$pQ&N5tk*^YjoG)d\D"DtXf@C>1tU
Lf2u"ZkT@F<GO$uM"*o-Z*Q&T)bY<SnC_]e\e,l7=-#%B*i6U6mos_K2/qs4)f^re7B(.kSVg;Uni3An
i/`[lRic)h0$u+DK<M:$+fj?=V,ahB'6BBQ[Tb'./5;4s)9AT&N]YQ+AOFYe,+2d-GLN3t$=>5T+a*Kj
R;!pr,7_IgP^4/m'r;,"'W%+-<0Z,!H7DNMa[83Q=qRSH0-a(Pa\dl6:<KK/e:V"X.!<9bC:>Q\D;/'9
Bk?*l)PW0fC-07Cr3e^c^)3^'R:X>^P`Sg5WEG]fl>;Zn`@*UY'.<c/96&reI(aju5>M-!rcXF5ejYqA
?ReZ2-kV85%n\s3og4Nu%r-X&K>4n12@"XJFd(e&Qg0=M.Sfnal=ECi)L5`cak6m3h460^;/4HoHBd,-
]>f3:.6\WGG#[0as4P(k8=@al%R]b5,4mljr)N$jaYt3+:sdri48%UGj?B<fTUMV)OG2hZ/(ZG5;V0:H
]UsQXIliCFq>S2iSk8srERS&+erLJsdqJ6*YP)%Fs2#B(W5$<QcZk@tJL@\ArC,`.nWYL`D2R89#6q<j
KU-G0-beUkR1s9Eb0r*X%SriVo)hJnMR:>)?hH5JrY&X%K'.'l#qFomGF/:50AQc"W!./SBU0.%O"3k!
,l+0.'@8PCL$4G0=)#@L#l;\ES=jWH'>r.RQklB.'7J46W_P-Jr?bH^8Y]@OC\s.\`(co6[\]%&a^\E)
cLS6CX41a1ae+nZ2P$Mp**qN;Z#2C7e'X*f<1A%T:/Ge2R(*IPp7*pV!k$^K\d:2lrnu*QJN1)XPr?'M
G`O:qe\#TUFb36^1RCB$$*(udlNEJ,:/EgV22aTWf(-=!5'7MeX<.1f0nQnA[)b]ZmpY%m9q[oTI-ik(
F;&?![(&(X:/G6pk@oq"SW$<ZAD/-U4aZrB:2B&t33@b_j0V;(R-`Rs1PRW#IRbX<[#%Vje<8_?C1$N.
9<g:jge[X;XW**u-`rssjRGF4&/D9j=m]\s1Cm:3??*4D.&.mN]HI`sjT,N<b86XVZS2,=D8l2f3k:ig
'RE42F+E<nb`[4Bqgl]e?V@7)la0+gNG0eH;S$"Tq,hPQ[ZN6>_*FjYI]tm%G,odsh4l*i@rat'W0=rF
-uRr]9=EjDnX]R.A*V%loO%64e]i8Z_sjP;KpO@4/h93BV%(fU?\PC78L)bNl+3,pGd&Jt@e?*VHPQ:D
=2R"sVL@j7hVYjc2[gYf8\7D$0g?N=1?f%Eg&i^`iQuJ#.+G%7N6(s*N[5Xk)tWo`qnXZioRg$'Ua7to
=ONo6;JadX!-P3_N7/]nc&"B[647<rL?fYq=rQ"@S`";5I>TTb[J#?aa'.W/Gf:!'7SZO6),/qEioXCM
rJP+h8XuQScdE+`\*)N-nQg-dD`hklf"Y]_4/Br,(FD)Rknk&2'7AMV^a!*lB2=*<"CIFsoFlK8[oi9$
r?mU=I+NcD-$PQTo&^B4Dr/;/k5BYIjY-7*rcrqh#;7d#9\4N?))tr-2G_PNGEh\nfn8dSYj'$iT#tKq
YgQHapN7NfHQ==)it9a=a^=<:B!`L10]T3[JU,9A]YEt<o//_G2f/%]n+VRK0Lt?ZqaF;Wh04/V4dG_Q
Yk>rkd57Cll)>qc&H_8M=T">HA%V0.jB)!U1O>i5QrfqrK^f,Mi>+(!gN_Uq^&2&W)sg"U@5`C-K\/GQ
^eO8<Rg*SQ$e]\j/RD%-D:A-;ZMEdS?<f@[6&YJ>d;tRJ[Y/!_[\J#jB(E.ZMX<P^<9"-oi#%ON*e2#r
!jgj_(RR..M&2OrrG;!*1]!d]Qu*)coHGp-([pI/UROjp9C^4%eum]br`,F2mh+^*4dGd>q$F#/Y+:G7
5b@TLZgG?)?=4i8EP0'qrWllGAO':B.da%d60=)q2VZbFQ$0Sf8ulJW"/-7t<)-X..Qs''YXdiT,]Z\W
A+NYpgQb`a.]7PHX*Z=/V4K+e*?fRSBO9LnajJ$7ba(Iko`F`Hmc.N=d!XXo"OMRAJU*E6H?Rft"_R:F
ik:$'W(H8Mj+D`Md5-&BTQ^j:R>kd(2muVm7:C/m,Rl]dRA+*p^Fb[:9Alm]Af3as`9sU8D0d[EZ1$%<
amZa;E`S0;I<Z8Oe*OhBr02A,jK3UQV=U%"ZZYcHg$bsuq+irt$d=5A5kdnebRMlY6=6aqq0+;OQ(W>t
Vp\=\*^ifc2#gU!:F19iYKRc&7%R04lCLUpZ.jN,JE"gufK>FZF:'aDKkl(l)lq9D=/mEi.Odm[<)VlN
Y%e=Yo_)1A<A1ZS\o?-1l36<98Pa_Vf_*FSU0Xq(IsP(ka`C!Xjf.;Ia=9u^YWS2NJ);>.J&PLd/&$Ml
Gi,n-(5Je#Vlm07^]PQ.!lkT5T#5=H`'+7RNu)%?WVsV6q]ih)iHX<=rt\tUehfBWd/(W-q?LZbqKQLI
=s%ZaL=SS\ipBqLT^I.)TQ4aph!#TN=p&jcft=]k_:>qWc\kWc$Oo<`,2@IlUq5-t3LS7jq@9n3ksKHm
J'bd6^.'p7CCP,8r@Q1Y>5t7EBmmW1.BS*tiRtV&O_]ARB`7PGjkB*pH>.fQ#.6XohfMbnKpF+"lW%+k
GER^JdeYrt,H];`37Rdait5I%3;`Z)KecnY;J`trf6=%`eVhXUo3,7>hLB8<1V.ap%XrO9k<mktU'n@-
6//erT^&_8DV+aEn'+ceK3!'C/["aa(STCrJ&gfP7NTpWVm%TchqB[CM!Sbj6L3ZL6tq'IQs)^NBc$`I
om)FU>5t7<BmiPTZp&BmP?u;SLnXK5n#LQBesG/nT,[%J>s];Fc[&mnL$*Fom4t8=+ni5m=nYnY$_k#X
phD7Z35(S\\H_&q<67=!VdT/e<bcoEC1i7,/"*]HZR^m\O0^?nF,.(P.4fu"L#&%t\d[QaMY+$rY/j)L
l_oNCDS1c([Q)T1+/SYmJ'anbfbI>_na&l6,^$g,MH,RI=,?hA9(kD:Fa?!<?/^"q2be!<o#9&l3[GTn
e_-3-f]WBrdu!?Qf!IFY41et*'-A-Es#9=#]gOIP!Aoc5#An^t29rAW=0'WLA?IG-NO&`pH@)W-NBM0n
9D,$'jk1Ah/C1KSr;EJ@>1a!4,J02\DOY9hhJr4ICr_r^,_@r[:MA-Ci$Yn3ei:C2B_3,`3]U2',MQP$
N7,Z[/b-g&,TUE<Vu/A07]gXkJXDQ/*950$GI[K0%fZGJHU`1MP[o\Jk!_:J49YnL8b%!`;-2d2f:;9l
N8&-3?Ac'uPo87Df(JO]8t^LikqGq(09#c*Id,o#ee,U@.QmVde+G5/aj1nP6b)S_/"c7Ae/i9!m>S1o
lpf&hY0*!tKVbqp_G-HL%aP/.=iZJ/U88X1cPE^jDU,1eBAN2X+?$eZq08HQ;8;>/bB_h`7D,?6/^pgh
b.';;:.%U.'^7JDCLp3S^'P1LXS61tZPBUmC0^R0m?[ItWN$m2?$>dCiJFXan]V9WZ!A9F/FCg^/l1Ne
Rtc'3`#-*U]Ztin%lnCDY*'Y/(]drU?#4<I]<l+(H:?[:NE`K:*'p+a*7CM@4oR2s-e>frZI;PdEB6fP
%66OWB>+01)%A2q56JsjY!JQ;JO>@FViOa.iZH*@k7Z\,";b*'Vo1HRZ"2?a[(?ImebWP&C:W_JHNVi&
@Q^,q3$VUp10,7hL0W?t.+U7b^0$.tMC6ehn0G^tSj\XXFgXE@*(EjOK4'@]"<FMWbm,^u^>&r#<kdjC
cPJAC$?n7QP7G+ee6AW-GOdT?mGl1/WG\t?=Z;gq%I'0p7/FU(5CV9SiMI3Gl`6S&2>7qR:g=dKSTO%.
q3i<Qi>,jef2).4p5kFeCI%[qop2gc+29B)MV9Z[c=Ue)H?@R/9CDn6K3iEnFBn:@9_hb;lB`k>N$$M1
Q@LXoc(iB:p/ioicd'%M(]+!EF&5`6\].j=g&o?_0mMXhJ$$$LR\R2.9<-_fit:)h3-7FWSa6GkmDAYc
_OM-.EqT/c0:gGslY?@V28sjK9.(j>m%rt'5M&WcP!/[15+$+TGQ2p5n!A&u>ag/V@=S<#8l&WMq:)1T
(JW\#E8u88P-n4qUb^8[4h(hY>3P3bcecEdZktG<B'qhTnmU[P0hu>Whr),-Mm?!F)YiU8r#H9:g9L0s
T9Ms3Ma"PonN>E&'[k:S%-caP<,\8;GJtH$<*BDPbSojW=s705?aH_Yj1_BQ&8c1?CHPTteW$amC/;qp
G+6'%p=$+aY'%NrlE4-;2RST;j1k:ZI5$,L(L&88FXWff0$L3\Ms\rimYg*aS!l-*S,/^`8A[Wqr8kg>
Y<1SYPV%JG#i0jJ(CHXTq[KBg-XbHNj]8jD7:`?&^N@d1gXWG,?d8-T>U\H[b?UGJho*IUKU34G5P=T@
7n_![HIVTX@AWDBbD2(lSK(kKIOQq=g?m>go0X5a*aSCa`I2Y*:CT$c4rhQINBX%eFk9tajHoZ4i;iW;
D8R:5lG=<5.96tmHjD5H?9Y`dZDNs!1V^Ns@V<Y=PNl3@O=q]M[U)-c9?h+X(s:,-BY!2k!PNk_=][6u
ejh1/b*NW!"9.j7l,&btLTJoaDaaWtODVld%fS@J9qgP<W[N(%j5p]^Hrrc!HiN-f_\0Q"kV?XJ*:dM%
jhi@(1:glL9E&+1IrKduQXG:+Qh&W[QQ][!bg\B/P.Gm&[G3fTe9dPDd[Z3D+`nMXJ_kM!?Ht*ciS&Q_
oF@DuIXfE*YC$(a*#dq[i$%e&T!<uPruJ0>?]\Rc-hcpLA/CeQ^%%PLP)sG]]TfI-;Z4O?DKu]1;!gO(
:h#GYZ+:TgB%&gH+hIsB`K?`d!F3g'rt7GlFurb\IC38hUj?PrnG[TBQJCM1G0sFta,<kh74o$u@JCeD
48++\`-H2W3+t*dV-0dDdb$3`8--[8JLBk'j=_=U7k,sc4;<1.pD:,*gLOIc20=i)n@.Eko3]WN)o#jA
pCCfJbQgS^hZ'GYn32-1JS28>fjZVkNq89F=+8C[SNLk=1^AX[a!NfpYm^Y[A!Q6c=Zn9-:]'ZT]4:VN
G@p_(^H.F)o,[q+^T:Z-nQ6RBY?2[O`7$KB%:OY*QoTMue&V%C8)YoPAGrI&"ib22[+JG/kf-u`9=7j5
@rQhh<QpPN`;/,G'0PF'hm10/p8n.X2^AZknT0l1E8tYEE\ZJ?*4H'A*-fA8+g1n:'JFVBI3Bio/fg1q
bhNg&U&"ZBps/BQOs;Q)bnR*.eHOOiVVNLhm6\uqc\d7E;(bFme7E2[*4.io#J[*OfU^,S%Wr:>FWV2P
ht"+ri3L5kdbl:=iD&AUSX1*F\,='`h.Nl(WK3HK`fm3-o@.gkT!8^%Mk_id,>*a$O*O=G;"?.UaPQ((
lB&F<=]YiNcP!;0hb&?3PeQ,NXrk#SI)k<4IDY2grpUt$VWUn;-a\LuNRJAC](=n&XEi/=o60HE5/`H>
LYef,/oW8Uho[f[e<d2IN19P"9@<X\.et#pfl(+#Ir'T>GT36'Zhm$O4,"1u<RI&$30\&u:hbK8CH&@@
F"9c%jG5bJB9V_m'-I(`VNA?3jfoC$]tJNm--VV)"jP<Sb#eFpCQ.C/Z+>BmG0pS4rUU9_CaT.)4M#/9
%?7UK\=Hd5lFW(bH\[*edJhG]r<rM&a8Wb^f&p>4^2Ep<G0aZ`f^m\"))'e`5PdssngJF;hrDDcWsAO,
:@f_X=8&Z+1P'aO(<ejpQC5t)J@sNkV)5T/0Pb':r-8/N.oG9;JW!E.Wa`^<.[>HLI`LS(doO]EkF'_C
dgV08ZO`!1C98K"4\4'3MHLbIHaon.Zkq`KdVT6"*omd%TbW[n&Z(,T;_AHuk*[%D,M^o*WAdjuFOjU@
\:T)Y`sSloC>SB@B?sm%OQc?\lG!nmAd.2HC6Tc4*P^r+>jH8GT$6Nni>f]sQ^`JlrLl*+O\t*AIg*To
i%MFh54-glpfU[f;g^8pKHl:7D7,C9k#'[pQSWr8;$M==jP`BX%ak!t?N$d\^_Sf2*`":^Xo:G`Ca]CZ
Q\B](KDe,c_N4t6h/ptMmeX&N2LUhr?W$5[,?5-C'/J-l)//?k&?opNA9Ig%LRl*SpA2P^H03cN8Ie50
:*\AYG:IfVda4AsB,:6eYa`RR,C#<WC6-gB461M&=-9,*)&^*SFRUfgW34I=^R+10k'NNB&aqZ9XZ)->
RpoHWQ8*NiBK00X(2gfiGfQn(nR0_o>9n)V1K[&Zrdp'4ALq5s1egiO[r&\[]G0)9VQCik4bett]B_Lp
8s>hD_"5e!GLV,[6+`lIbQD5Fdr05R0E2f3g.-]%n)hRr3dCr^m#*n^2VKW,BNCNa;1njTDtEK>2qD;?
<5olR`nST3gW97TP,lQho5T9/Z#^B?^Z2AU[B`SSF0]h0##E$8atf^<\%`(/]PT"np<-)n=mXT0Dp=u8
4k:XMRa]DXhcZuso-@`&(-d"]H_)6kqq]E!$kb?V2YR53!M?l;md*:b]p%=Vi"cOn)tjM+j/%Hq9AWF!
K2P+GX^+EV@?'Kb"R[Bp;@FUsc)KENVheUl#JH2sd3<UI2?i>nfcPC[rj"b']'YP&M[@hHq9@nPSoSJ_
pN1,s%7_5J(Y?gG[o:*T.s\a>N]l92IrIo?EOOctGKk[u[WIcMXV*)XDFdC#Y><^54dc7toCnSX=iCXN
ZVJ_""6O</)]s"47G2;(Krmm!>%Eq2n63QRp,cm&D3]7EqbHXtW*-'JUFs5R]Xub^5stUZ%dSG*b*/,R
Q@)iEFn;Zl;p!$-Nm?:iDAO>-hrr!<F!<L"P9UQc<VKFAe`VBP]d_GPngTNCO(1nD&q5JOZet*.m4#d<
U>ElDn26hEn93f:GnH.g@FX<VZ'_<^5\[5NAlKX8Y[f,&7Hi]((M=`V=pZfbOKZ4s?'I8FI:MLAiPMqr
:"iEC48RUo6sDHqLha(m-d/<,AHQT%(;2A<I9ENh%pE_D%-i"J;F+O5/3741RTXo;>;i'OAg"j`N]5X^
[&bb7/A<Ytbke.LejEq`f#>cWh*U@AM,_s+#pUjEo.b;;WdG&m@j:W3\rEWdf6h3gPZ>#=`RE,#?8uFb
@-?XBlWPObd>ho2HR4tU^3T<g')M0J!.WmNqeMr;]@:_A-Ka!,P<%K#9Re!;>."!jUt@\Thie[!qCIK,
nJCRt*]W@b33uCso`'B0&*fb^[&)K&g/@;U;.0?HI+qQsMq_-Gd;>kkT!f@!^Z3M'3iD3g?^?gPmN#u-
FbEt573Ts&ca7\6T.i)%E[^M-`W;Y;RCW+SA3:XpcE-tEQ_e0HKHA6m6%<D#n*RQ6/70ZOff9#fZWn>U
@J0uX7GQGZRPLip49E%m\8s<<qdHjYd@i_%e8?f,VXN,2ERP1Xna"A6_(X3mka`WPeju,9,POZSr_Wa9
#i)I<5P3,D$ea:JW7VhPBclW&eg=;M=0?F<6ncgU$Lu^2nn;+PVSTm.miJ=7rhd_LP.W3tgV)enbH$h"
Y?gfq?Y4sj`d&1@_8%4,j)$^[4>u'EEP,\[0oF*i'c9</aqGk[&_!27]4!aSGop1FR0Il7FqA84Fn[)B
G;lL2]7RcY6*#RFr!:(mT"]Z/6P&_]n(<%5I<3>=Y;&dEhoJ@Z\.=Q*F()eFo-SP&bfES0h'h?,Pfc<N
%5B?U2X=E;TeX$.nYPmJS6t(DSBm[MYMV"nbFcT#0]#ecb+BJ:rB8&N(f@X2XToAdcYtH3qX*CdK,B`V
3uc3YoHgP#E8n&='=E$9&'fP[&mq7'6b`>:F0!%^=:A9f9"PNIU*aDnbpEJj%da-rB&C8]\bo3/X@7\a
@#a1$gQe:8-iR%4Y>_H&!jR[<QL@2J'(%;[Gh3g=n/FG`b*RM8GRsB'`n)BGTufLnr%`;E*QZf+&>XhF
Jn'HMhGLHGY/2-.bq`MG'ZSmk#ES6$=ID+d!lYG!7C`J7'Q4e3:jiM42dc%/0`mu0X`r:_n_K<CnEnO@
iuXq9mX"m;la8Lsq$TB_K=[bm3>K:ShN&#H`gEW#6i$JdO#BUIkHK]U44hghji*sKX7OnHE-V*ndtT#8
:Pm-V'-f[80('ZI97qGql9!bX;*;OZIUd7SPl8U,U9>ZO\MQoqj^5S>\7oc.@r;@=XN.2`Nc`T4Q$sA)
_Y5hUbC&-a4)c,jl!96`FN82!&(M3ISOu:rYH-14Xt0$#h9E.!#7\#Sfsr5&,nTb++.sNAOEg6ciWu!f
9ka+>pB;uZrq[aJVWOOr`_AXDZ<*"D+suNMrUJ(V?]KS6-D+QVDq#f?@5BlJUV6NMRfWqBC;\aLhFFIt
Rb3+BdohC>U]*@#(WP6HMe1SA[1l360f'TMWLUH_-hfKV#L.%E)&;bkEH)4.ER$Wn1/sdM(aE6I$VOIt
PWUXJa8BN3]`Wn&RA*Q2ntmgAaH68anMH8<qAYq4dT;#1b2<nt#%(:\e23Gg^O<h@1U-8c0nQ$K$h?k$
0>u,a4P`b)>ZBe?F/]B\nuR3-,gQ'QBdJPoQfrFr>:<#'lEbe9BNp;P^^V0Wr$AY!e8FH,YHN-4VCgWa
r>cpsmF8HjQ2]W%kRd+kIOFZ'JU7"QU@4H.k>RUQr4E/DEai`oPSJ:D*be<^W\V_F-ke?(M_^df;2]\$
qT*ila8>AV%]c'^)N5ABnh'7o>c>J9.Z/./D1B#FMC4^1Y'M5Z06+PJ[L_-slKncr$98\t.^U%5rHQcb
61H3RAM/Q49*q2km!RM%#mDF*r?]":1]%Xr,62V?CGt`W?dWafZ#W9mF7Ls@m-hUHbNgWPP],c\*OZ&_
4iLKb1SGj9l#1QtefVQ)X0lg<BK,.:W-^UE5?J^*o;u:M.O?%:_P:FJiDmT'M:ohJPd1MR/PjSBphPXC
2Ul[$S<mj28scml&\b":&10o:q%b<e%<FDKMsYgL1[KD,P(1<^DR'1&c=cMl`gBY]Ss`<re*9EUla472
`O@_j7Hkh7-`qQVQ?:e;(LkT^1MBsW1QiS@F/sVfY0Z"u]'S+8n?`sMN4:_;qLPun)%1*FM;.Pc>e#tb
Zq_(%V2())oHWWNSV&?aZ%3k@'`*]9)#h6(B@?*aIjN^i1L;1]S!\-56A?GS=.*<f(NjHPVa-5eJ\j-j
l1l\:qRp$(N.EXQNk=J,ESLEDY6<]'>COVt?bn;pg99E;IdC-T5%"1%S1OCLT^9^Jjl"dpS=!XL=)aXT
lrO[Wr<dHAep`>C9&HfXCVQUtK@-$-m8n8=HL+]MBB4Mq&RjqQZ6/t/1V.t!C,`SG[f>3fi%BVKo7$(.
KX>(u0"ahc>l,8Dl["OtV+"#A9<QlV^Oggo]bD(FV$LU\lrO[?lea]*qiqEd0J8e8q\<PrnQY*08;sA%
q\;sJ]+.]..s+p&$^0N$qH`7DQSWq]b!<"s,t8s,*>@J<?[;:91,<?n@&3O#Qg268V5dgdU9*B]Qg2*4
,t)Z]rbB4"V:tB\#r0A#Q`=kZbZ>7VFc"2kV:qO,;Q\8fWRduu@/Y<9lV_To]#:1[?)mk6/oaRBI?&Y[
,&NhtlaSs@PRNfFFfC8M913g./oeUQ?+T4fT0$o?rl/ZQp3*10i4o9OmGq-N?.qE='.kQ\/3Ajd>Bl\)
IRH6%K,4I\+$'^^^Drj"Op1]32tGq&"i]Q/`gG;c^GXYnp%[*JN3etRD)BEqq>"O7K_;`E(sVKKqgOg^
6gb?>ohQ*225V5R"Q7ZAZWc&anbIl!YX^@&DpNK-@(C8?aXuPc"_ma:A<LkIa!@DY\UnAJ^V@OE*po_c
a,,`-R"U6@[*F9q$U/R_D&Dqq_&Zk[K/^q[s3,Q:E>a)Eb!@]>4iefOZ^Jt?[dqJWjZ;G7R_)n)*)RtB
FmI_`H"f?tl27h66[MrXm#::YrPrT+pSlnE3_teWnKqqi7T',iVrJ"Y]:Jh;IJ$%bED"WR?4Tu5^H`Xj
)3%6Cdap-ZqFG_l]I?NRl?6s9.+mo,`H/c=l>q:A;L0>!cbL<kQltg1j&cPaO38t<.Uoh9qRh><K%,H5
4"EqP#tt%H9h@dceJuj1Wd"S.&e`0;pDK*Bg$L[F[<q+%8XY,Iki5#nl?6ZrqV-:FN(0R?N)[Op*MQh9
8REbIdA$qp6#fO@&"+#qN&1phrdnEF=T90gRH\mn>VadqH4S-Al#O@GVVB[.1=PWZ(J2RP8<->T?Hb?#
PdY(n94gr_[9bW;]6N;UbSa#qR1[jU$:AG#9?I[tFd_2i.5U<&V1SYgRGhgYMNLeK8-bJ!;Md.r$;tIW
o49`1m9iQ-N%d6`oW^IO^-9=+`S3'$LN@pRA1:)=0@4)i7aP>'-:qPJC?]L-4?.[Z.X?VD9]TB7QlLuj
P'blbo/=?Uc=`1d($r-I$qI'jN)[Op*MQh98REbIdA$qp6#kY0N?c6n.)%u+_r$K&V.0CIXST+97fg:s
MI%kE];dZ_MV)-@rHG@>=,Z@=>#ZStmCoU$Km+Mb4HIpa:R6@f\Dnp>K.HG,fq-YtHK(!,KFf%;P)`Kf
PL*7[Y3Idfg4lPUc(^gd@6I$C3Ng/@J_1J-DCk:`SSadI]#r;f3"k*TgK@e[ZCBCGG+Ckme\aPJ^Z\P(
;hjQjd-32QUJd&XgK?hiTkE-^+Y%WX72?'==mD`"c\NT$edSO$3Gj%7jd^7pmngb5dCVaB<9nO2Y)B/o
'LPeB[8Ebk8!K#QG!2+MX]f"'+$'+-<R,#*'@WL&OOK]OEZXkrB'8B[?#Y>MNFZCm22,1>U$H@20=;E8
)9ke^ef44*38ZT`!^;u%cKdP]6!.<eIm!P]jEr8$a\LM1NjlAjictV$5W3h0?-P[rJdLbmc3GASBbq1J
4i)JciL-gTJ7EUS6lW*?f%o7BX1OB/8B79V:Ygd#ZMr/6V(9+,YRE,/S@,uq5jdgRCQEOuXfds]jG8!u
?auNtH-jZT`N#;ZM-Q2C6//jnf<X^7qKinpJV*pJ#bKpj^1^/V>XOspgC'DUOL92e\^;#P>)F#0<KSC0
J05+W%XFm0r5+knXgt`ANMc<KY;>eo2mu4\ZD=Gb;BG`@D_J7tkA;!!Mq11?C!@&*\fkU2RWa6_g>:rm
DFr&E4".A6N8oX$dJ,"J+1"+8J#)%n6)i?oB8*Af^C9E@n[CgKah+MgVD[L\^n0O$hiQMe/,>W#G*g<F
4_4'!QhHQA/6.,?gr2.I<a7]+l6Q+al0mSUA1'WW%BeL3<A@`+csh#(_s&+beQD"KAf>AG\`t,Tm=Nq=
"a3>,f!,_Jk0CbR!hF#]b//#jMK*^BKk6(@KPhUufOu\Y*Tg"AnhF^mU_SQmQ(N\7;f<q(r/I!',G'D*
\iK)d,;YOW?A%PZdWuh3SC,G!FbU*)bI,b@g4?u4+'fca"31aRce\E6'gc=rM&%Ps4g7jDK8[#;$'2>h
N5"A%P:Sj.a(N_A[m2hM#'onM48o*QGFY'UpGj=W4hGZ%\Lk5JN4ii!\jk^Np&$ncCaoCFgJ\#?\"t(V
B(kEFES+/>V/*ct)`Mt)>]1Bud:5U^"9W.66e6&VaVl5O3k8-#=W#5&Y%-8W`%T5:[H"r^Yt6aXV<M^S
Cm=IR($#78)oo2L(:8+1OUhn<m76tg+fE.^@n9ptfD5iMrW^B<qC`f#s8LlP$@VnR;TX8R$9m_OF]07m
i<R-#@7B*Igj'Z&>4qa"i@/!MqV3J.;2Zu%N'MXZ=oQk#6AtTLIeKsggjS]6;)\Zt?$#C%*D"Fd1V*FM
L@RIqSJDPhr2YQmp)XAE%M?]or0JD&^GiNO^.Kl#O.:e\I6hjGQ+gPMr<2h:[d4"/s2M8*Z'#26:U6Zf
??k_-Pr$22RBtUdkDCu8Z]"%Q-4M,H]T?!=]*HSam/WZH;Jd,kG8D*MiCohi:"2e"\$TNr[)aQb+[K0J
DM(0XbE_l$47;_rgUE)'-NuXP7%M#W%P(IflB59f,<g0W#l77OA(3Nkkh9S[^Xc+K;?Q.&N+X[5O7fK#
`/GnCkW-D:&N68b;19Wklf[3Cl=dLJ/8>Se+$@CBaE28f!Y7ZR,?jqQiaeW=";W5+R!IXu$4;3t4)J6h
XXIdr^GKkj>sDTX"8<Q7W?_r-%\[hXBXpR[6#JJ=`b$NGM9K)l?:C't=)gm[HShF.]tZrT>:uh=,Fpgo
E&tWCm2i/<mNMi=2tjURhkbam0XLY4hR\^!5266)iQB2bGXmEKMi-G/h(17EWo$RWrn$:gA*kdMm4ah"
R`mrH8GnJH6ZUl=SRVWdf`=:F4(RHGTXQUq8]EEW55i/>bNdGU#O;#.O8>`?QMi,hfMXf5e[fV=G0VYM
A;I[(V$/UeIFKqY%Ft=FjDXljYn]`hi>$'k4iHLCD9NHQL_B0AO",'fO1Rf7.W;H12dVb4@I)UhgG>8"
>rRB_^$<al[cCS/\t;%B=\cOU7'rs=HC"FlXd(]A?WK_?U"JX%;5G7"P:8\\OXcIef.dOH^[0Teqg4fE
?!eVVoY!ai_4/.LMF2@]i7D['SkQVMeX_8-#OK461Q\IQAJHR:Ae,dRNIt[C_TSQgV"#;l?J]c:AHm)H
V%<$W<ejU9T+d>NccXtUP3eMd,';[G`Uj!WMbJDW*!M*'F#<khN`>HV3p<V>-2,D/Ggi#YcApFbq%2cY
K_#Xklh$;U+Hp+PkqiV#L\.Jt@ee%'MXrLo7BY"(b\rC3U"0[&beUp13T)HnYK32d-0TAr^kbF&[sSaq
-%'n-#'-4(caNckO28>r_"9@)>/CA?N]r=9oJ4lr\'2#r,HC!)B!RJ_<f5:6pX9I,$EiQ[`F`Edd3:Pm
Dm\6t<cdS?n<Df")%TMrhXMIjFhY%A',^V*O6=5iI$JcKnKA#r7Y$^0fD+R;n^a)$`02:LAn$/M,\5IX
COMnmS6KUO3qDeIa*=%0bSqUW3PFJIn$k*%/'\tMqUp/.;E:UJ1s(rg/8S"Nr*+1#YV$D;OL'^l&TeYf
'tt[Nr'nCLIe(@>\,dt[+fup:nW3?t(!)VpONRa_HspPF2K(`LrX/r7o_^At9JmB*cD4#>9Brg8,'NLX
>ne0oBj_VmP?&XCCoNWH71il$McT\EA9gmaD!H?CE=:BNfcTc^>8FYfONT]*fYtc^+k6G''8L8fK3LM0
m#R7Bl>OaUNQ,0_;LZjS0NGm'WPgn":;McSh<0/o#0e.A5(]DZ#JK)co!qt`RA&3%&u)+4MrtpO;C-nn
UH-chXOB5?,5"TC[!456C#c8R$2$j1.IJ%\]Ao4cd`LD4es=$bUYd0M,O.,SDeM+i``kH$s/"ojmhM,_
<MpsdoUmid0$\R^Cl[8QR#I#dAN"+"]4El^D$jHY'<X[2bT$d>$I^d%?d^^!Z6%gWS%+.1p=1/!WB<J.
h+nJ=QgYO59(.`=Ek>W(h:"SF*s8XN>i8-.:V4V$/[H=X\+K8u\'4JE=_?r];t-[QKApd:9h+bgohY!Q
ATOpeRaTM(;@.](_-X;5*;Kj[07PmFY`\S7i%KLYp6Bs"of_co:@E:"VmV`fqBaH\c[33XcrXkVN+@th
"iZ2-5:aG"CV2UiTWa5FlB,$7VCs@320N(_5Cl0XXr.*>LF(lGT#S1;cPj1;gAH+M@Xl3]4=OEla4A/V
=VGEf@0s"[Kf].NAVMM8A1tM?@,^!!/=hJ>iL"m6[fhB1GY$#%*.#n[s,PnJ=P^B(Vm"hI*d_2iV>&QG
A1L,sl+Jr'9jI.hF^_'+Un+a6X2[+AZXhlJ5A;"gHJGb8b_ttX?huLrqh&m8JZ$fQ[WXbLQ7RQ9Qk:n3
ZWKJF;M=t_LMO"Y:Z617Il9o'*9jhmfJ-PihO3T4Q@5=Z4-a2$c:!^tEY$0mn9Utol\Jm'6nO<h7K2pI
bWOaXq:9c"I`WUW\Yci7$M0aZIFdQK.+\59>gI.RnlK2a]6XFeei,`:>3ie`RsDU.8D,Bm__[-VNj*4U
UJ/PafjU/64gYIiJipBu<]_9In%?,IpL22<>.OK!]4bk$*\%$0bhMmQ+3K]4];:s=(9o<jE+YbK(b5"l
dRnCPnTB">3`1uffTWp>QY_"RHZAhOlhP+ir(r)Nn0u,Y3p=!tV)D3@rdMQR+/^XOs34%`3d)qQDs@1Z
>q-'0ahjp.L/`i@GTWnuIb?_d.`,g;EmHkZB=IL:&+CL8Fh#,SlmnX=G=b6@FgtC-PcIHr5a%"bcZm<j
pV8Bbj7i"E;UKGf/eud?j\jKPII@@oVX.4.7P't\'0YCm%i]X$2G29S;UN/,V&lTAYB>&g(c)Mupmb]_
2/rL(h=_6C,L#eVs2($94Q?0\]Z`XQ0S0Z@2._O^5b4JiGWZD\jd+WC_`_agB\nnLr?+<&MOF)!"8?SC
=)O8EaS]GEn[goYbKYIO[dJ4u8ql1/ZtPd1Nq!q2l8oLPl1oOn["9@jJMh-X@C6'B.)iu]p#QWao&.^h
=TsM,HrX>7:eD4hRh_<c=@^qG@iq+hEcu!>QLImWC\AKV4D39H['.?g:s!Sq]GBWZ#l_\u>R.7"^kM/,
Uh'4<%_c$<*`#X*VB(U/>jG7Q:1$8E()bEiXoFR149[3o]S=Xa7OA#aej8Qm!`/Y!$ubQb?;.]Jn/^9L
c%.e=-XCs+]LdH,4-d5EWmpt9<EL?uMBolJ=NT.oEtU`akBpI>HYuMpQ0`@9Lk[/8Z6=KfEJB5nCLtmP
%c+j_6<-fD9n(q.^[:[6B$CNM;jPm>[S-FP*^[IX)OF`JUFZ5K<a#sS9F7JH:,^`PUC1oT46q<g)10%)
A`qag*STSD3qoe:8A9LEL!o0T6H"uYTlF+7-bY@M09tPohG/OAB"SR617Ts!GMim@l8_6S)GMT)X_^m?
*g^lA[%.-N0,M&LY"1XB0083n["1LBk0a9G0"bZEh]5`-l.rMnb#MFQ8RT3g>doeI>>cl#93i3nB9.H9
qL)$#H2H^"5tk<[K`8W[OaIk]hi?Yna$,(#^Z0S<=!aXAYl`F5&c]-<PlC]trX3@Hp?l3#WR,P\m<BCT
4o^Q&q1#[Oe3Dj"^T.?GhXm.730Rf0b[YNClD,=7k1;NsDW&S,Lto8pNqSCJ)pIg<GK9?>^3F>Rn;Cm`
;`L.qs)t%F^M69UapZ+c50mG<q7?+pI7@'Y1=uA)kp$d@IW"agg,:oV,T(8ak.dDPNK'"u$Fi&)GP]V/
6r-o]PGX3KrG9$8<YY;DRjpOBY@VNWl!=9MgSe=N!W)Y/Mj+EUEJ:8PAWH$DdP#[9^QItB;m+3RC1#5q
"m8%kN/UeeZYHN8B[j&EB%6["3ooY!1/]R=%WVp=`&L'22^dIZ%#eR<d@2pmkXd]'A7WHnG<!3EM;ED`
Or4pG4j61bOJRgB`3G>J>+s4Nfq6(lq4*kl`a\HC\]T)[/B]a][T\66hSPhT=8js4H@,JE<12^j^RFA1
:lc7K>J7enUKn,MA%Ye\q1MthTS$:i]',\-rISO!ZLR!oO.s&l?Rj:()&Nr5MB4,%DL*hRp+[\mZN/r2
0`7&\]Br<?U,0U6N(Z$r9uOQk>Yj[)g'gHMC,KgghmK/^:\"]/)X/B`%pc\`Ns*IM<-jfKek."S."]U?
UQ*pY$D`Jt:iS24=c?r`<a$bia35pNCKnl+@[u<`%B`$<gOdCSX0=;g\<;Km]$^5=@`'(MN[Cq1@q@CC
\ru)ppm&t?6U']J4?[t\-TKtWWU"7=hR_nU?]p=71t6^R4mJ5<0nN(o68!^g6oZp=.\>efJ`"5Sgh<3.
j5R/qmYEfnPTkhOhL'LCe,Sd_'T)X[F`DPf/kj2r8?q-J$Y,BtA6%Xb;kTB2hnDDVc3S?-g?&itNBY#;
D>AN71%(G\b%4]EM#>Ra4Opll<<G3<`DS'UMFC8sEW&6!C?+TNbSY0Kk`Sbn(DJ4J@PgEBGI7N3p7hBZ
*fP`te1uubR[kF)\2FmG*&;*:*2rP)\$mumqni4sRkg\IK\tY?l/UAK#?Sp-09eF)h@?[&H-IbE^>mA+
k2p7Bof!@4^):DPNoPEbhE*o)IrCr@=u,6>9TBQjZolso8=L.c:VYSY/>h[;-c`sc)'^p(QM%>`h>#Xl
`]u14RWGi/CMqoQ_:kj[Q4i/C\Af^\+'T87=AR%t6@UW>Ol@l/CmG_t6<pAohN81Wp\*b.FSL<['-j?3
YE7Jsmk-4ojLaCjIUPbIqMuG(9<+nna?:&P18WDi)WdWX?M&X-,L?)S?W,T\afVUd@Q@XGkW'aPP$f55
I8I,_f=<ahocU_>KGgR8T5\`RHu5#V(M>:Hgq9i?(RlK*\fG(@#6)@,S55Xm57L7!QT7=="k7pS$SB48
^1_2qjsL0roFOCY7P!O=c,;d$ZHU.ue;/f"i^IAj@Io@L)R;XKGN8U>i6>RBc:;A[:*0.I.(Rs`631C*
9f%]61p07SAg35B3/2(:UWMS/'6C.ZhOX0`j9e#s_7>@BII]*gEhjKoW.,9uXChno1o55h[kq#mj3'E?
"!-2*h3(urL1J&;`[RsTMT66"TbWFUnKstM1I0:Wd`c?KJg]_hVLtc?+YY+>]5*q!nD;ZcgXlbT_"9fU
CFmKno=Cg0db.tW1fB?Rd8N*3JM'q8,h&[H0):ZA2=\+@:>?L+>gcJEN.qUs]J,)'XFnbR!(r"2T'Wjj
l6&\]A:sPVg1"-CF^D3*7chM]G08K))e>)%j1UG8[#g"op(4$X0V"Hd^mc*4jdTiWo]`7]OP'!/05LgB
e3ch%kn6t)'W0Vh9.(_5Vi_K9qtkKHaA*X9/)dhsI<)-b^)*r"YH4Uk4PCf3<Lpm=_p4(Na/k;oG>\m1
7J;E6gcE^4r%1Yc8Jc-/7?J^!,#-+")p;'UPs+GmEn@X$3K8*6SOk2o&<R4^1js4;.cP?$esp9nj=WVA
NpJm9;D4rX"^lD6<1th<6FZ%[M';+OFQ<g*8I@=CZ!QohL?oVGq`%o@emE]CQ$2:fZI=XfDqnS>Tk^<I
"k.$1g$`.VEP-ei$_MV9mi?cTr\!fgdoD\5#>\D"2jo.phL;nAeGaI#o29^?eoUbCe)F**ruH$V;fAmr
n]b#:(mBG8<r6V[72YFlOj$&iG_RKr,o-DTh1u1$9=G:M[64<#0Q&Jg%q\AlIg.A==GA*PokdX<`;LLo
.rqr`\@;M$1F-feX0E8#q=A63Utujc5.I(nBc%)Nmhid,!h@/0F5c`'J+"EDHiNqPChJ4jk`MF.N2oIF
dbfZr&r(\KI+iSqn;^;*b+pkA@d?n:^b,7c`5Qq,`0iQhj7t54'I-rNh\4ll/<'(V&Q[a]NZL9*)V*+F
54SG8j[G=kI%L*$^#RS:`_6N$>MReNi5&)mL@)'Y_t_mo`K1MAP`-c*"O;o`e"PF+?35R7T%7ahM_nrl
k?GJAG=,bbZj(H^^:G8i)K;`bcXLBmI@Bk9hMYp[9;'N:&]-=I3*X-SN`jK]Mgap0Zdnfl0+k$%_)#0*
^I%WF*Ko:%`%UbkM%4b@h[:pMCLHU:guB*/FO>[jXNDEjRd(Kul'm%uYr<,4=PN``p$.o>Da+'P.rV8Q
T[gf"%A&Ac*ER$M0+J"E7O=&%Ytp2l.ahcD:Vsh2:kI&f[prB4e1\X$`S[`h@dh9;D9`gXHr/\ll2;>0
]s$"<P=UEIJIZL#c07r/9'%9W(:Q8qr,#KO*nC(L*:MN?IdfHt*']g%7iXa<g)_4uGaCf<5Hs6lSf[B0
*ff^d=ol7=Ggi^[SN#GYj#_WqcLIGc#K6.mHj5Ctj1Ds9jS0c[.8lF<FcPqfic4YcE'-W(XdQd8E]iT*
31a)iN[G"$j%8.GnW'1#mHG?*5Nm%ta,e(Y3ld<Jo$04DBMVEq*qXj`a1oX*:2?K/*Qdi"pfPZ.^%5co
*8`i[:3&JE7;lUT4FcT8chAK@k?"57fd#Zop@iMg7h`!BB@l-f?8+M^5Bt=%Nd$L0a$9?g*074SobdKG
\!\;\Yp##A0P?*ah.58Lo\!hrZbai3L+Ff__g<V=S2Z=s:"7MI&c<:555i/S7D\>p&._Ms')DU_!I@?9
'm`#Y!=:&UFTk(GA8/4<?3X(!=':('HfW9F90DDRdk<H(jmP?Si)cIAlqpUKVXK/sME0J)YUXJXL42L[
_OcImDfRM==fBW[D!SopAdre<Ecd:+31J)Y^GO(M=3TZ-M=QF/eL@GC'Pg2QDeF79d2":FkeA*ZWlKHV
Vn[pm[1K#M4`V5V!pl]@07BXDn"Biul;V5RS:V:JK&TGhAb#cA<YgpZ6RY<mmsaXr[UhV\Y7\6W(j7,V
Yj^+U(%P"uq>)E[/Dc$Lf"p%U6,7(NBDW%K97%gIhW;17<-)A_76=D$Hf8JV/Q)1tm<N5A]s(@>1$9Kn
KZt\NTZ'a"Z%d6`C*Qp3-80Nr<0ecWU9E%g>LFI*`2GZjQ-<kKk@?js/Dc\&#d'354AKDga*84CmLrsf
$]OU%;I/<m/7jcY1*<(,2n.,@NPKR(okSqr3A&T-^!#ppF&@mWk@kd*XdDC)6Ru#?(a#A[+4'<Af=8)*
9#p`)D=;^)GJ*Sp&Jt/IP!dL>R!8mA2gnAbr*(o8)'!GAd!$eVA?Ui>R>K<O5>aWSk=MI!^oFZjm$uJp
a)eq&"n!A.e_&V9T$._Mf;@a2M%9dbr,4\83F]L>2&UnWOFD$\fu<2CMJU:RMBX^]=jNIZV=&G_lul<<
GNo<7?@,N;:.B:BhAgZ`8X1k6KraN+8(K]Gm,?m5!=U9f(GZ[;B^8n8!I*gk94.DeaC^GZ=rI7"i2RD1
n/g"$PNFjZ'Eo'`5RO+YiXC"uB9OlV@d(i>b1Jl2%3rkh#tg;:MnCeq7So.p1a==hrJ3Z+don^mqIQP[
+7IIgo'o`MKR0F+"N?',NiHG]-J?dn6%A>jV&&BmRpq<"@V(%ML>usA[F%XG?YZ2Jn*P,?]0CnAlHr=5
A>;p,2,`3b!tIu`Ki%)q2@%T?Oh<@t'%/N;C2TFG`KpY4eiA4/)bKK-$VFckIAJ",p/6CX14.u[BA)!&
e+LoXfcKUi]^4g^rA._B,!,Zk`c50oqP>`!^%\Bk'?s(E.[lKO(O[A>QXtp7\)[5rC_$*sZHQVl23DRU
!-Qlu6MCbA@0c%`ZB=e9i3/d^lmqa"lT6NOY1`&@.qb$2)/lrgI(RLi[*7mtZ@ulP.+Z\<<2=]t+1KI9
A*0Kf3R=[Xq?oU*MnZ!3m\8/u4Z26GN%]!i^ts'5!Z+TiinUmJXNl"3W-%$b!-!!P!%>C\J"cENFq:05
m>YdJV,^\#0B?n!lWI2=TVVaTgsQcA9-A24#8N*PETjrXgH1]I!)32j2SUS3TWm%T<QATCd!HUL4u8BE
1BZp$WtkWu4h)P9okD_hIod^=7a)`]/_S2BElK]H*Y^'%89+.%*75P@%]8$1a`sK@4prK*B$G9d/tHf*
/EHfOT5r%'4Bm3(Ts]?(IfkLDXMeR-U.\jlkpolj(7<?1E"*.5T"r=oNH25tqAk2ZG2VjRZ$!%dmAspL
ijmWdn^WV"e;4M]JVm'd%*;/;K7lOm-EQlgOL"]g%jpDe0;WZ>_Ro@28$a@c8CS0A.J[qE`\t#$kH?%d
P(W$`1rK_'c+:+KnaYZ^E>56+*:?$M[L,-r\.n/$3-PV/I7&mI9IN@.J:8pBediVT^05sH%IrOknHNQD
6nBBKNuht?ga.oZ/ZFmS+VfP].8cX3DL>$p^lXG]W,IcT[:ju'bu@YiEXL;5o-2>d<+Ij2De0q1Q\raP
/?)IS3!W?dOE2Z^$`.0#`UY>ZbPZ1kn0J7]S!5JLQ]d6-E9'p%31"OUF5KlXQ'5H&ZDs(/C!E:%D?.&D
=)4(;L/_\0RNiP_=<=,N'Al)*hXaA]Y,DWu1*8<KU2Z'qjej.2T$2<mcTa-ZO7O"qbj1DpbF(mK'H6(r
*=%F$\"otQbVM(oak3n'Fo1gsk03doft&H:^b^.39W9=+)$^+$YiIb<FrWL]W%cLrp`=qb9-!TQoC'3:
ci9LGY0",J0.3lAiPj9P(S9X&Ac6DnV1[1-7INXP+^P2'gUQWsb?93cS^q\Xe!a<F(Q)KPSul1YgLO3_
Z;\mJZ!PtsHa-T:ZP$,aY_'E6LD%*MI[/5Sop<,GVF7i0)6Ri(AO%%X]m7a5[VB*&poG`L,F<$8Gf`R4
L))7)(['M8fudUQdX0)I7<FsCQ'q7Ss!@N2DnV%USQ<3aXi_Q]K_K;\@D1N%gf!#tr+-OIML+K.n_I*j
Ru?aEj#`OR=j[W.FnZqD]1;lgf3@OKX*&gTA4C7&?+p"GI2JaupVcr[HI.a5muO+ir"jSgcU;A*r,!;:
)Z4HN?M*qPRl20g,HOs;*hEdG%D!h-=Ad_ODAeaIE3ba,[9dOG)P(AF)b$g/l76,o]DK65^Qc(e,k].F
TbEkfQ7>M*[==#,Tj*E':qB+QS4A(ao&\cbiEM4hObN+k056YpnQMQHB>ckTJ\ef[s,Q.8^UqC]97$Cs
]'(gIs1eR!oa].hO!H.lOtt>kIlk)h.UkUANoTqbndaPAhm^A4_j?Qm2u-8eYj%)Mbt@n^n.'9?ruE>h
5U&*]I)l!Qr8'NdpIpJsm$LP/G5`J*O3F<]iI8T39jI)Wr''W#,NIZg(h(kBZeK3t>=bDsPFsjhrGXo<
p`AsSId\<g7[WFQM@o!X+!Ya;"(ORcL(V^9M&/YRNb`#ds,OdJm*@bBoGFui(U_?1_27o%?T<Q'^VEQ`
G`EC%p]BPU!Qpt30E`7shlm_r/@I#RZPqh.$s$F2hH)4QO-7a!6Yd9lq8-EEIE`^-@B,?0:ugB=^h5di
Zi1+<MWHdS$6tKJ\>c*1ZY'AR$RmW0rJ]E3*&6'3mHZQCD@;;%okMaD$s5*@8AX&,Q@+R7opA%C`\kM<
SFMJ>^t"%/2b]eklmLTa56hE&Y]'K"X6O"P0'%W$GqS0>66=33GD5'F8DX`:S#Eq.2+MmL4^+%Qc2Ph`
^UtDg$CYe5S-G#,paU[MJLYT1"?pn>rXp:ofmeiUS55MR(<h]!;$e-W(a]iE]9\M7lcj6g)fbDY!1Jp9
X*1o%:Slr%7&3<"ZW2FXrJSuECX53,QTTh6^XnDP#6ejg;FEKV>Z`dg3A[,olJqUVhjR!Z\C6IZ\j+YR
`n-u.WKf8'28bmQ1-b)mERaW?)&J/J(Hrg$[/-@RAm!Boj-Xi]#gSC8B0P?Kn+%?'RRPNsden/]\"o-u
]I(lX>&3sSB?"^&2)O@D<srVF1T22+k>sIb03.Bf*B-Cfk=UU.NY/]1`*iJ&)0Yhf.?@;V(\5YG@=d'q
jIKsF_R&hi?`d6F_Y0k;:<?PlZ$YFU*F[N:Y"N]UpuVM972amg$&gAnhgaX">YU5;9iSt%mn?V,H8.k#
GpG3B+&Z<Q]qdV;r8K4ne.l98<nSSQa]_tGQ%PNHZS9N'`%Q*l)"6RDP-1l\EagEA7`]ZC2iu$[YL_ej
A.f?a[lnD,:p8M3Uqio#i#csgdXP)i%[\bc285RKWfo99:.W?N7G]&;XlSiWcNphV11ti1:2>@WZF^lC
2e<?`L>#&7<Bl)1CQ.uP5P8F-ZJJDjpP#l1-N8198I[f^NoTrM]1)8i]?f0C]XsP4s6#HUa26_bQ`r^S
NrAd'EhDsLp[.GC3>OVm(\7k!Q<E90Y;HoA#Ng1%$fci:TpKLKZjVt$,+=Yt];!0F@>h7g&GM*:i-%Ph
$DB+@\6G[^Dlk@^j-L2kK?ehK`JLe8R#W3oFl%@aI)a'^:@IuNHlG"6+Ln,j?<KZ`7@Np-E<h6eDB3s9
o\$A^>@:>mj'0`6@&%Z9ZmNLg^J)uJQHf0pX-t4)aAJj(rgZ2@r`960/g_P53ES4P/XCa\)oE75CpDc]
KPN(*)ZdpN1o'OjT7p%n._[YMQ56FhVJ+o1fRNj_c4#2@p0.XqC1_S#H@ukuJ!9`u(`u8\E'5dt\1o"+
bcqJC^md3UmJ"14?qJgZApuIm:J^6nA+-OMbUafl%hp6&#0Xq<Tel5q$9I1J!aff3@uDFCKe!6km+u*D
Y8@f?pY5K@G"5?[*'8o,e4.Z_U4GB]%LI'>#;q_t%Q\[m<T\>S/F$s%B!oN2Lfpeal/qeqe,"l-,25i)
2q&E\?UXs4oLO&)rq[U!`lAZ9Nq/WKrZ^L7,O(]DZ1eVD@5eS.)&eJ+ju$0-8b%8_N&un/$A3'PQ]4l$
HEThMq,]Z<bLn+5BMm%\]4+<ZKO&en*;c7A]^PXT!@9I1%`Y,nOemm(pR1%k]^IPrf1BuFm@s>?>+rnT
N.sAp#ibb.`KS=GUIJ\!,+fn4P]?9pCGk])ZYgt[:MdEhjV,K0JI:j=?eAi'3`B'SjHqgS`J%I@6C1Y@
%4Ci\BDdK"gL.A,BW<I@$<L]uh%%qk^rf25=Gb1uNgP0rn73-ZdR3L5"<)ah+gb4?Ga!7.@O/^bWah,?
j8A:Z/sD(K%`T"70G7rjCQ<tWU#+K8?"qAjNo^G.UmRm71^R4@,1RH1nhP7u&r:a/KI=H?Ga`pZG9n%#
*r"U_Sa-'=hq<D.@d6JC/5pb>s';rAVFgF2b]U_RC#/rC[co_V(`ge1ieud,(M[+!![AjPI`^,P0tfs.
\"@:@B.a_CS*Lj)7%05q<EIZGhoHCgnEk1.@[6[T([UKR1J3Fn6rW?_?>qi7';]f4o#LsZra+2kS?#'f
f^le:&m$_>Rks`&ofimULYeqO;,/b"F-7kd2nbF42hAn4^?_\E5of.=9'XVrHsrO==JUH(:HZ-R9QjTW
6LmG%h#l>Wc0qD9il-6hd[lqP"L&4\I3<]+_"hj-cG1F9?VGKspOK$9oF-l`Ch?-=^MVcXW3gq%j2-Kd
F*$8RXe3_)'d_Gt7#$H&nqJD3fB(Z!hQ`f#C&b8J*&L1ON&@(jV93-l52432Lp&P_A.+EM3rM3m)fMo6
+m.;!Y!IoJ4HkLc#q,l@_k_DZ5/7_`34;#G<@NK3@2sT&rsJ]\S#OIPMq^a@8:qt4n..N/f%M%R:SSL9
h?^uG<6C*cX,3,q;lW/3,Z7n-Wu/ZHNoF);ZqbYMIQ8hr[$(Z)3!/<Q6!A6@%qNaIJ,KT#ks)O1`Morq
+3+1jf3EJ<?r:7+ah%b)Q?N;XqOtR+AJ6k3#kGgpO+JufHe@&,>%g/bbMnEn=c#(YBL/5tK9U3rYhL0S
1tNBWh5^,1hf34n4Muf9[rjhQ2''WAoibk"#.625RPa>Finu&k::,#4S:e-iXcp+IfeNfoHT?2uhSZ_1
cNjl\A%og3rHaqtT@"E!rkY^q;6LQb\0>MKJ)a"WqP&-KX?8IcJtaYl!?Z]=%mAQR8Q+st9KB]h7s*c'
Ed'64Y2o!hhHS-.hIU`<bBb!X=kDY>hrX98&n#Y.:6]dC0r[ach1`I&/pDMA#UXbtE^[V%Y%*g14&K$U
mOf8[OPF-2]^0Z_D9a6bY4_;NZIc[`0ZRAnBk7>[akbLHEF%ubXXM?J-,-52Fl[Sp1$.igZstP/Q_^jh
>?9p+\\#dsjPmSG;k1n[Ef0t#gSH[nZcM9O\\"'odnK0jLJ[D&pkQ?DAA>\1?/%kWEgLo5>ja4t&fnNp
2Ku'`Snj$"WZ4=8jR/JdR;-<t@K.WPK885f_kk&gh)._2b$F1DQb/=XF,^Ur8mLd>.+%)Def(4:h1M$)
bM><d7[r2@gDi_hm`tLbo=)>)-+$:U;=<l74)*O!WO12)GB]lhXLg_a/pA>J'\LAbq2&C7q"*t,jY!_J
]A25%[&)Qc?E[#iDL!i_GB`5AJm>dQr&o:F&'fX2jq2X#=l57p[8<;1En=rWEd&&JAM/0KoCpRWHPO/6
4K<\NLIY?hKnm)7-2p\bHiNoUrI%Yg`0lL0;%R;ZGs`UAiCc:QK(o_3.pj,@&&7UZ*oD=ba,>Ks&FMdf
j/%Ig!`Y\LG"EH@](g\B)(qG:bcFa8AEo([Z`'sZP:,s9!"H=f`^MXH-X*"RJuIDRe-Et2/Y!#DAcn/o
90#Oc<<=DuDYt,oep-Z5\W))]1<s&dJj,1nC)7ncn^dLRg,A/fh"j;_SuOdSO^^-141JK']U0WgSmIA-
><^qMoTkOfht0D/[-6IX>bV!Ih2B[GX0i6WQ?;*XI84C"HXVh_6Q[nSEoFZ@G/f[P*I8A8hT+%tFlPmY
Q#AUqMF(A#,OUc/^V5X"d7!<mFcm7U;S:U_$0!I(5pY98ni,UJGL:mu3I^j]b4)p@otWP$,6hU-37Tc^
I"^r(R(b/RQ;*Bkml$7iRhnic,CZ$uorRn"O!<.V1N.E77u?c'lE>Slcgc"7n,/D2r5FdZHol??.ks(%
;/AN_i,SF#W*b?n(-3ZcL?0&_CqRl`pW@\!?\Rr,`nS&D2+%+V@taF/"qE*'n\4$VlK'kA0nj88_XcLa
duS8$ccO4D[qH28Mr#5W7f)/@3-EK*`=Q\;[g_.G,1u>7.S_k(2T.di]bb>Fo\BSR7l\7!P4/+]G"9C\
Xc6VBaHJX]ZfF`s+Y^or^0;^_:s6o!h^WXl+hioL>G1KAJnA78KBN)FC=\Q=?72mY`H;L/G8jYH#71tj
o\BRGXqf@'-//ubB?2srs"3mPQ`HC!KY7`>[?Gmp,0>baQ;jY_4(^>lWJkJrA@b8`(29)#pfm,Ve+o#O
\%ip<qM61jYB^(Qle(Fde_9]QPn+s3O$uXCZ-$N+2'8PB!\"mR(r"c"-!V?RoXUT,\[t<&L?RIcbPle_
DUEsV;VkPrT'"iiV[L`@TWigQ^uRmO9;O2mJ`mQ:i\<(&3Sph.b!nepeapJ7P#a6eSFh^3G@+NDoSut2
Q+gJ#KDTP=C5FZ6Or0#f3k'K6ggV("1[WE%P>UL'rPIO*"Ua-=+sq#BBSJ(!lA'`mLqT%LU6sNn8<Kn5
C9<iIUBjZi+aDclI0`)c';D?@8a8Km_"&Yee*nO!Ro[IqVL.DZL==a#@nD*nk&T%^`d4b2`eVut!5q&H
c2?/V.O9^P?@<gC4]Y@g1&aq_XB`SfDt+)fI?jM>cIql8q=#fk)6lJqdaq]m,Vrsaj)*V-!#j:SB^I$(
"?qJds+mFtXhfd3@BBK&\\_Q6er.nn(WEGLN'gt%HN^rA`V7oO+2?$ta.a"8#P=L1=R/,3O7qkR,'U@=
!,qEW@[<Gj."&KhS>!kS1S31Z"`B^=<N6neC]=$?5HVFQ/KY%gAM5,,Yb]cVX%o2>61Q0%?4g?+Rd5gl
1Pk5OZ$ZiIm@naJT#$24?Ak#iF%hD1_sQmM3mf:!P#8knC\SHmb2o@ur!iTLAu&7hC;*FOlSX/HUZ&9P
q;_"oB>gW<EfK1CX5hSk5N`.CLS^n.i<s#M@h5SDP*3G.-"e+Aq[6'b1A]Va0&qVGgo]fsI(+o0%H$O\
Pp9r3CiN%Y)%i9sgo\<jP2*0>P'6PT_fWFnh*R4YX`!Z"mag%Dc&[+5jK?31E`]r9gf4k2=_g#q=Riqg
<3Aga!4c=BA?t8Kj.K]GQLu,.AqBdn,9:E/nOq![Z9L#"/\YDKgo\D@M@mSJ-d_m9[8uf<@rL,bjgts?
9(#a;iKa?2j<$gJ<qTR82_a<1FmF?&o!R7n[hb<DM)5]6$Zqr-Gslb]VT1@=kB1APP`aa4gpP=2EHK:X
]Dma&FkN`og<sX/DLqA@].I`O;FK3XR2JhGB7]nhrg%=4`TsrWG8Xh9j=XqY?1-:C]d)U@:rqJXILo)%
3rTbI'[cO@392\:Wa5$nQdT/Zg\mGM0dan+n'<sh!"'d[BVea,;KB7WNY5Weo/brMF+"8'gHqJbIl?HX
58'rBDZ)<dp"]CB1p#U*oB=dbr#`J3ps8^%D^0BT$U"7P396=<m`u'nNhah?N?c0kU/uR<Ks0\4]ltI*
h^j,X9"#GrR-)M99!%l)\FK9^SbKM-6Lc!,;%70OPBj8I9:;]E7^qU'_6Ht6]GrYlnmH!Xc<ed"RA"mN
UbM3L\V_-XD^[@:h(k!O.Z/t#X?=N!ARdi8p;.2r.cXTMPDsRf3=7%1^I!m4*W*4D#`E/*H04LZ911)]
qPt](5o_'QI62MP"@du;d@9#^So0f_REX@.8EMb>,=F2,07uD/EF04S0B\W*U3"q#c!JW2cI*WW(0M@L
$aI0ppG2(TdKC-M_:/iQ]cH,[7f3nLDY7>l-KX#V(?/32n&YW1Lo*UjKgbBQ>#>:G:h)]Sc;gY6ZGOdk
N;">DeW/^R:]AAEWT@FJGF..X$62pA#Z4+^q1YodqYae3iNDjlgr-H]Q(*#=4k's@LEh/SYs=LV)[g.m
N2TgD^=p`ugf^1WUN2Wnb55'PGC@h1HF@hunj[ED%@./SihdTch7P[VO^tXZ<8N]l54+;f2a7T."$6n,
'Zh-Q:L:,\AlL7kml#]NDWNXG2Igls5lmCU1H6f/)t=p#1"%V!6mHWkg3XcW6S551g4!hZ:l:`42O`GT
NphrrHD8sA8g.Ko\aO&gCLoBA":orMZ,M"cMI\P`k'A*0Q\$V$]@/V+k0+cJRAf_d]ZJNi9*SU'f#&I_
`V]*:Vg#:V[IUslTr9+/OM=aGmC5db+O<%/>T3o*SUMZ>Lsj*p8;o=f_rjnCWq[4uN;$(-Tr@P-6=u@X
)qfHPo5]83VGfU>%pa/G#VjuCWT_abQ/%$0oYRjWe1?]eP[$1(0"gHJGf5.P9(Q@Z]VEkor8KK.GF&)G
M*lSZCWI4O,]CgFO:JsTaeb9;[-3g2Hst%Z6H\6#edl_gN\.h`E(qgKHdR7c`qUk*&(:>mQ7N&rgHj(o
quSOk+E-+<D"YHMCXnF2`N^mK]8J=(,B$*PL`;5-HnM>)P"5/f7]@!gD4J_.FZ,=b#X(d=8fhj*H,Hg>
10ZjpY4i,@oMna[4j'@Qrm:C*ES^+R9R,>I`KuI'PbBC@_q^9t,$)e0JZRLgOh2\L,Nul8'-fS=rX)NN
H>cVW?_*-_<f\Z"]YBS9)]*msQ+&CDQO,A`nQs1Sl`5G4C9U3g]YBIe)ZQX'WqQ]<oMna[4V@p:qNBi?
"-H"qE:Sh`77m'1PsliMp1&h<7kpD8J!/iTa6?nk_BOg]Mh7W[*%AJ*7k;IaPXWDt=US<Pp+5PnncinO
81KNa+D"CUm,OKFCqm5Zk5P9?<YgD6]#MmeJ.22#3K-mCr%5=W*D8d9"[k)C_^jJ!RlG+NTs"bpef9!R
9NhP9j.@4R7;WbqW1+For-I.:]%>R:1[9"#/i"s5,PuUo3^=*#n$ij>`bo%8]X)+gA0l?5$Y@$YZT+q>
ZHoeuri(%MNAHDPn(D;:45=`<MGIg?L7E.W\F\ZJ!B"M\I.rY2cLq)sefFS2UIs@)8+\<oieTN>eFFGW
m<Q_BXQ$tjVIi]+Dt?<Le_57<*qAdQl7HUeij2!rmkaQt$Pf<\eF`*q5Kr>+/d6KPb;!lRIfhXTWG21Y
@f-Q.lDJGq7V>bB4D/MFpQ`E=^^:PH`JO]VZt?5kA@D(rRE#+r/,9YYZ(!2DP&cZ-_h/b0QS`;=n*RDB
r2\=ohBV;Rn#spqFnV!s,=X_]SR$>6<iVh@?o4i237qTcGN`Y6@heMG]sC?96]qR!6W`q$+*ZM@EeU4J
+*r/gbUP69-oEQ.r3<?T6iX5UDY(,:NG7XL_@6I/j(?CnEdBfZVKMnb@i#$VY('K0'u;oF<kq$ZNO&[e
FdoVA14iPWEOF7=0et2VPb;6NJe51_rat#0hX^:i&V62^!qFA)%FYMH]^KrWfNMOC\)$)#[>p@q_U.6b
`(7(ib)82+e@LibM-7N]pGY3-K]Q,KpJp<YO":mc3ZhKK!\6nHSlT8mAoZ-bKjSsC`bbC.n^a:(1#C23
]^9o&$=!7<SSmoW))9+%-,QFgR3f_FLm]P5OKq7W7[=>X1:\qio["70)j)EJeA$6?]=lWU(:tQ5B.(tE
Z:#O0R]Oi.@<3k7*.f^R:]Blf%%1.7/#pRM\L*W:^8[`@T=5JF:.4Ls(?E`A;mJ&*M%R+!AkKM4F]%U=
YAgeUpK3[=#g=o)de2Cr/Hg\oQAB2Rs&i.fg>#77lnFQ_g`g6^^B8[gJ,UJcoDbVapn.$0hg1/9WsR7K
qfD6^[^9e#X8mERgN=-5F[YgR<CDKYZ<_F?])>i8Q_Leb:18RT$F`8CQTGGAO2ug-X`D(`bJWFKm>)8H
*jP^5pUA8&nee!#R'\>N("[SXc3Su(n^7'r`6g77PR8\M+kd^.ZB]W_9CmV#*TAJ:-ql)P^D,f%hBdZD
Is"&,H&lq/3Vp?fTuKU4(#:OuMYop^mlU75>"-Sob$0WF_j&Y>&`IS"qr!2G<j/IT.'j9Iak<uf.MUGu
DlCfpeMmq@(\:R-4WpIPjJT6)DJHRhgCh9&X;)H<X\bSf<BK]@d3Bh.pZkU_+&?H:\R0UI:6BM5j`NqI
]eU[WAOV`Y**_V3WGD6h.Vj$.rmGf+=*.ZVYj/L&:;3(0/E?Q_deR3u<AUElq0[&DiQ(kA?4X(<B'8JK
k<HbQ=i:_Q_>/uK"j7clhdX_@6bQbUW8bn?Yk_<L;L]NM[M;h--MkTid2]&!pCQl<JW)#>iN?K'cgmoO
m8UMt28RFmMiNO8U'I0RZsP.&bWY:Se>Z`#m4N$&ImkCE+]hr\A]-W4$QGiak1ZcDTk#!'7U?+"KpcUP
f'--=[tef9'!#?-(V\mDS"l;I"@Y<AHmLdCRHCYk.8D32+hb#KO0.#gfuPUeVP59aiMam;UtuI-$]$XH
)L8I+f5GeAaaGL7D2S*'PKag[8.7p6*8o&@e9IG;=de$acI<d,bk@Uil]ch?>1F@9lJns[p"rhuS`FRo
i^Q&o1<>7K-1,fP$hM)3guZHMml.8<pE`aIDUQmB`=(?oXY.noT+G%(m'WIIqb<OLA".:EM!"i)'u&t]
rQWKRo+h8Yc/f)aaBV*Z97G(3P/2'WiZuLb_e+6:,j?Z2q;)=ZE,DB`QDl$RM!Q:f:TWD=T&n3V/cm3D
H_"3%0pkY-"F=^TmIY:\.cf#M/Sj);+^]KJPu*0rX5aGpAZ(cd_:GHQ15Q_.#uL>\e<i0!?eVT'$F^IV
eoRpoH>0^WTA$EBR[H=rRW.EU0'80sP+32!YTKZeK5HV`bppE0K8Js%O6?s3<c[-e^sS3$>kUC$ka])/
-A;,I0ac@\p/2pC!^i)3V@db/c5DkA,9Jc?`'\c)cO@61c@a?P!tP!?52uOXX?2Mt=<>farDJq%:r"['
6]k@9&I*tN(67ooK7Jn>lj7a4o]/09jHi>/$E',>1dYX=ir#.6E`hf'$t'!6>[iX5WE-G'l[&"S5%rK,
jS.ZgT,S;Q:28:RJNK]6@Rch^R@6*NN+ls.;Q.M&L$[iVA#CU-4D1_:&Ppt'NKsMgLD?Z0/3oe<A.qc$
Agr+GQOf_:=/V8!-oB+`-A<?@)+rgGE4sb(=$<o`8"gOH4aka^UL*67[EJ-Gk+f>+Ibb?,iL1>YA)jmD
'7i>*SmR\94U9t]jsf*F?D[1^'09MVB1er[*aT^$#69psTK`0^o?94;8#3V.*cK#gr^l,SR>;buOe<lL
>NNE:k).=Qj2jNMRdbu&^\eXY\&C/g_,Z#lmtGM>eSjJlBe!.qlj1-i5Bp(nL*N7mNml^1[3`g'N9i\C
AS;0?T"_'C3_^T"Q]Lk.5;Q"C?_*H0@`!@Oo=SVf@tg1W-Zu0/[@32LOCaGdo8IUi(rLkX4<nAHV.di%
+a[-_;,=c0R=dOL\PIY<WgkR5?@^@:02U<TWEkV_We<gHeVD;4W3aB!@[3t.Z%b+SDs_H<+F=^b^r#c?
_j"HF)S'!4.cu7qr+9IbC",5oIq/86UpJeCUA['-ROlDFo[^&(alkdtQ%>U[Z\A^/XPR,cEce%_n$-Jk
8[WdQ,tPumkg2E'8"m6"G:0ll*:L!s(<S%ifY_K2[iMN6B>/!fo9;(841,rjZI>KWq>*K*1M#6q;m8#i
jFb3m5#j7Ok3'b6!eY[^"7OSDQA[)F<Yu*N3a,r=h]XKpdGZ7=.^"]P'*`]g`Z-P\QYu-;$4J;*XY>l5
e$Xc22"sWn3-MDX;+AL3_CXbgC5#f\9;iRB,3i??b*F9ubj>4hj^ZR,E"osD_,6-&("We&dGE3@O_Ek'
$::F$*)eAdk.%e8=O'4TrNg3$:b2%2d<Wa7<7Zu&GjRUs$`,8ZJq^\"9.N]S!P)6M%\ZpJn..O:27u:,
bi0j42=TUP<7;0_mQF`^(.r7TKpaSCM%R*fAkq(&/i-Y"='B$Kl4_bVbaINq@;OAD>%+)6d*W2]rptfO
]2b:3$ca5L_V]p^>1W<6fUoq8L`#LENPO_:B"bVL%I[EFs2uo@?[Y=nRFB1[D`m)5EHS7Sp'-HCi8W1L
@pn_sCJ=Ks^?)Vl[s`NU--&@"Qj6s<^?"O599M0G\(,8PY@ColSE.8_Ye2s)ICXOrU=d&<IE@\(,>!'!
WFM*%B9tZM[>*FQ?TN@%Z?(9+$I9iTICX(4LEp$Wjil^%d*Y>(13;3:CJr$L'uu.`8_2gKd3XY":p2$6
<qkE^c[eEG32Rs(S^nc,aVsT2MJP38fU#)+@pdprghgCRLQ8i]`5fA5fnm=[QR#&<@p_9P:m*s9k=aL$
`bf'$55+)5oIEb$D#(C]gQV@+Wr0Qd")"oKXioGX),N?Ef922VP%;h\!Yf4+IC\ECVBcaDLEq0;T5.uc
DP(+/Hd_)o#ij/e:o@rrOi>C)qptV;cXdWrQoFWaZuXFKQ=i^ih/.(A]h'1U^%Ij,k[mU+ABa;tP't\X
k#10"!kUK@5B'.*s8>$N2Hds:'67a->%]ash0hu9o+=^M3Qu;=_En<@g;T8&\Aq^R9=EB.3p+uQR5R4]
FmrX61IBiR`Tr)DSiu]>(T2T*e#CW&VD]V'NSVA_Nljs"C8(BWcbEt>rnh;"5ntCC"1'VenD`g[<7^.8
OEH>b>-k44F6fbE-FehhNA0WBg[-\2dM>]#ABuU7_fK"Z>*6K%=U_MLA53Nfo1"m=<R>?.B&*]'h;3?\
RC.[^*6JK&pR55i]arnE+<(T51cjs]Z+m&\rB?7]><,T!;Oe5lqq+H9d8ag;X#5H[T^QQcK0sl;l6MrB
*37qLE2NEULt]=]-OC(s4+#bN/eb!_(sUQYV1e1GVfh4:aBO:$oa&'db2V3jTl)>8:gg&BTgNbX`-`g-
-.B3sHi$e9!I_AV=t+gJ#<@_@,ndogC#qmtWn*$qL?HGd<t8j>&][3U4o&<)\_ALLNd9ZLFe"0#!tT(;
ms+gPqt#'mqMb'kGiSd>NkY['DciI;#PR>a2kE3NdE;>WHtm6aq0.5ZZ)g`<>#dG2\9bP;e5:O$#4Q5(
[B6(S=,naM)o%W9C!8.TC,hM+Ic%/>g`>A,Tln?*iN7rMql7N>jOp[C`kFW6@B_<QCf(ArV-.Pl(ap'X
)6?H22[rfheT[h2J_>p,U$A=PMY\F\aH!X2euGn`<OF/Z[VGj)cfKQX@OO,bpC^KPbp<eu8,P-!h?@au
@sGG21?gaY$`W#"ZNHIFft"SQX^8,.S5p#[>]fk?@>_Y99EpK&X>g0X2YHuYG^D%'.,l3\cTk&Yj`ndt
ie][bm,7V*WR?1t,I0h)p,T9VQT(`_:5nO0U^U0c#7n^f'HHO$8fmq4D//><S4`c#*:)+7^A\0M`Wu]J
D,AaM6)aN%&oIGe4#f;9W%l:\PTA`9.Vc5rLfuZaW9,t?-`F=i&`'d.?7Wh(+&onm\aUJ4qAd9?O^ie<
*KS.PNTtA5T5sS*DsA25\o4Kq`QIG7H3<p"K1i7\&b2\<j'JinD^+:thQrSrW]I%dD.j+cCA46*o!!Bs
W@JT3*tu[#>CB-7jti,[@Qsf"2tGIP=`HAlK!AKqG3_L1<8D;VM66_Y(p1<$NU"1$n^Xh13B_k24rF7V
e"[S]:!t#*`$*+8`EW9`[C9[K/cKS$a'Y2.pjk)Yg#@i9Sr@Z<cH!]+Pej!j@ou_e<gPXYdd_P>mT@fM
@t4L8=j#l6rYQ%`;3gM8pcEFfW`,sUe\?(b/dBX_^VJ2,fCSHU"g(UTrYOF?]:/\Ha511`rf6jEN&ImP
ftjXtqaF#GpP(gP0Id'0m1VXoTATAZ]1M$%?f,?(#3ph"CbC3T4SV@/O)N\-TIRi+:!l2+d?[7&De6nV
*%$*\C+I/_AV!CIMKZ(=B=L002j(4mJJuXI.6j<\7B3!k*BdlW01-CD=n@<5GoOKt`7'V[]ABo/WS4?R
.`qQUC-p48X.O'E1u"/43j7*[PS0+ub4TWLmsTR,Ep]N['^D:A:`'G#n]OONS"+u%iYA+D)p46jl=\\.
*iS$kUp81F[HUiH*Hk81p\M4lTk\e06RqD2hu#iu/t1dWo?Y61.*a^%&lA[%61KX^F`B]Y5NWga&=tKK
ZF=Gi:*E?NM8_=O72aU]$m)S$#su/[q=W<gY)u<@^^9RQE3-)8[`X7B]tAnHZ]Ir3")jH0ASu;WP[C&]
3pm]L75FsZc)`hnL0^@]>*Gn=Sa%>9<3sIrWf>n:XkTs4Vg0M$E8_Y[=AQIS*)];Q^Z2&Y7>;%dWU)!^
NSLLV*'hg8pR'n4`e`s>;TkMToC+:i&'sOS=@ArZVH#OrBn^=i1C.XiF+26KjsGRLlKslpnJU:X:V")H
-9,Hts,I>0.PU0e)PT>26Cm[@4f;>t_>=VNc@J2kDFmPo7eL9%`Y1P+\_fAj*K8dpPe+q84/^peBl:I$
.E0*A"4,,:,W'bT.e">Ud!C*!TV%g*_kd>,gO(D"ZWWHX/[4rbb%NgYQW@X9/WY(]>(>KuRJj!SKp17c
aJ]Hj-_*R8iPdfOD`HUorkIL:2R"`l*@$Y_Q1DaE6k.UJ__c?JFfck*Pfd*<QI,iA;]\L-,s8^ghHT"W
NUJ0cH3VOQr&33=?*BrOmF$%!3I+edoIt?W;?fnNR='8q9Z*HQEh\=@*J@Wd?[j[Z)tTqj.+^2"KL5YX
m'QIF]OSG2Qk.be]DB++lNk$WU"QK-P-Q0pB%(T#(s3r@7PK/=BJiD_(QGBO.oVjeQhggkZ!A]8E=fd-
HBpOiFTlF1HQ-Y'f?G2U':"`&>uaAAmHC:On"gLHl,G-h'Q^3h<MEZR]Vhpq;"%[h;mk9=HEA[CCj,\c
*_nA35SZ+,N27$P/X2eE/b;jXU8DMQ294B0s-m+kO18=JWLai9]+:g/FH:&-kTujDf1S;.Dlmp+Drt![
fWrNN1qK[a[C!''"8Lm9W9q;`N[<[E3'!<QB+)<tig?#;o2rRMP-`KV<c1jFY>@eM?ab_>dN;-#10<&U
RUT#!;>W>=85<B!7"*t"`.?"#(2-.fj'.!fC[?!+HGcA)a)6ad[n6Nr:X??;$uc)F1,Sl7#nf.obLWof
a8Cirq.fTu-fqAlR[!o\C;u$4BrA?H*D;'2.PEB8f>5Ub)-S.YXaD5,PZGTpnrsS]*b[E\[Y,Fc?V1^e
WEl+&5[J>\.-"s]rTIU%Ja=-Z>*3GSZp73GC=31&pQ(n-ZQ(P>'NuW`[(oB`A_&DqHo!NnhRdcW_`J>[
:gCW?X09qQ)FnIlm'7U7+R^QL6'6V3JNRikOf+eqT_+>J2auu)54Q>(pXeFJf)c1)[GIE.['$2.1qtD2
CHYiD]>fpJ&7%U1"I9f7Q57ge8dgk/b?nGG,cq6k=A_e554>hM':4]ZFnne2IKU*c$Y8$fF=gmqh4Xu\
oh,ZPFtI+:9h,SI1=?5LatAqp&>aEkln&nEOTF'9eQT7^T`n'>fVA\.b?ei_EN6C]c,[u18a\rZpWp[E
^Z%>i+XF1kflRPAed%2X'\tSJA_pV%%$t1PW'9)(?3.M.gTD)SJ%tsc*p!.VI(S*Xn+>/)Y>/\;<KEI:
*W(:SH2ENgPd5>4k3J@<ZGte6[:<Y<H;[R&]'1`&B6fJ/,/pl>\ZWTRY7^EF$3:m-6dO.1I&Uc'/\&H?
IUG]_,#Q!ZXl/7-H&'5F4aU1_@bK=`;p`$eaeu:*N9M$plli_*lY2qHq3t?)6/PO/.UYGuI.j(]T%,3%
"sV;W]3;]-Z8<ZiK6,Y1DVl8Tl"b?nr)@$Y_r9YQ5/0Md:sA=Tg=gGJQ^]K`c3k)8eG&qU?/4SE?2We@
,nD9KV27"=3gsl(aBP@Una9hYhJ9F6aJ[k->heG!11sjmcDAUH/Qm_dD],n2g<-*VfJCLKh5-XN[#E-r
n)DY#LE0Cd_o)(J&!8$%,GlRY@/>j79CVie9L!R#?()4EYMH;kAQJMM!;3AX87iO,DLEpWYKI'WL2BhE
_E7>]8%E<hO3Gh\mRLl4g#GSGm!`JpRXSd7jJoT5GMJ6q\6cg2eQS6I2tho3bf"k]%^ip>JR>kK%_L<P
lNX(rIcQ.:O6jAJ_YMJ/p[__c,Pu<(O)-sOLWA\-C:UonbI;BhAL!@]aYs[n7s5B9+8N`;P.=e?#5uW'
CJ\9J#\>-UIl@PMAS6fh/iuEF#VB,57aKX<*SBQ67/%ofA`uWrk%j]94%Hb^6Wa;q7bfU6XJs[gjm=rO
S#-h?&'PWniB+-df#ntHhIfDQP<2jHmk>n\>'%BZ/BY^,JF%*W_]K,+fCA7QDpldL2^TMBUA5\E'?>!l
2-G=J,HdV05.1+>NG2(LD6;\n;7$Y$Cjo"=Xo3er6&Rt.2i6hO0#+qu;NgjI^OLGcqVNBIfej-9r#\_C
nULQi7YmX7dmSO?:/-FtYWr/Js2S'es1U\jQD#1AG=Fui4.aTMCl>:q(Z!=^9"`K?pfkb<<n&NUX;h#(
Xh'-LRj6aLhkq3CF,&[t.rKP?^4<\S8+Jk.rj!\0/$p!BI.6.%]u!EYiKm4Y@&S>mdKCW(^Ye9a`_C?H
bVQX)`cBYP0k/.jG\$<@2sr(i+Nr,3OV$6V6oX@MUL>Q/S]N#ZUX'$f-<\A[a1Q\'(qVW\2:kDBqi@h0
L2hDV66EgbhS1\d&]Zs,C$jcGH0a5IVKlu/DggZZp=3p:0ntc9TRn7hf,Zci9l[ua>84urV;.#7OEd:r
_6N5O3fG4QGhuVroZuQ0\,,!)(Q*dod>Iht5o8=Q_,p2%Q\N/%'kri)<Q:#Uh_(OcWk+$N[,WD]3\eeB
_knte\s"dm<63s])D/6Z<0o)Y>d,V:1-%(5o#iA6H'2!pVM/8jpOR,AC=jMcOCr6H4P>X-T,YA@=HV@W
;"`T@i:-11.bY"Dj;4opmPKU?-!D\J4YEdCh%T=mSkB6!j#)\0?Z'JV3:_dt[1P_6b2?m/j.Vuh%0?=?
Z0KO::0^LQ0Z1J$p1pgEh?ms&8(EJU<!K0b7oo#F*.9!BGcI[LR2J'`L-BY/Bh#guc/!j'O:]W4-SfR]
PSmO=36"n<ol$5n8&"$Z?[+AfP$pp:>7$9>XQ!]XThZ;)gl5g'OdS_PFWejPd^lnBO.[r5EE[@]!-5IJ
JZb,X!8B4"WIDbo110ccIX/OB"qT8s.Q;QC'P8r_.(KSR5Cn,D<XuDW'2Rb$fWnKuL`&Ds_kOPG-@-Zu
r^2Gq#G4T>HcL6odX%-+D)-$RpHZCg?05ii^b'(-F.a>l5M_(^HnQ+n)d8KoV&rY^^CSV=Du\2K>IiLp
c6GGqHgV^=p`&'E,V8JYhkK8^=*T?P%^tV>2Th4M-_q8aBQnmP.t?g;Ua%=7[)-eeIBf2!@pU(*=X-0p
ATQe2i9%[K<CU$0?jZaO*mVQ4/kOrY1GWmY(YBB1W;q)(?Ke,*<c4pi;nk'U!r),`47TJ!c/A8_7Ail:
YP_:^'OdU+=WB.0J8#R5/N,1GbkeG!<'AQOibst444BMLS?!(pdi9L.(Tui?4u+qG4`c9h[T7jTF.HCY
T:bn[B"$5loc;(<7jckjO%ecI[kg,[#82YU%RmOHNj\HkM):`CZ6)8BraZ/:j([9SZi&E4,ddgSXJ#(h
W`IHlh+8(9C0lK/bar8l#NFS+ek[f$A,7[,K(ER6%HN")T,BmCX%h9AKm)N+J#pR_*WCe=J,DC"s+OaY
&!Tcl20g.S;r/uo\g4[=<>Si^Pj5p[mrN$HWRhQ=8j8-`.5XV-4mTWdr(Xh9OF$DQce$VDZ,ccjlaCbq
2ehKaE74P)1L:U7O:,.XhCA\8]2&;KclZ/NUR9Q)Pdt;OW^MsXh.%8\%H4`jX).7MXM]n'6dr/1At"Ie
jA#>?gN?39J;lb^2R+r(.Pk1.78tPpX?O)fKIJjKkHVEGh8e@n82B,OPP=X/C`S_f8$)kLAmaL#R$95J
1ckN\R.DAcMW"RR#8PoNNYA`nBEsVin'6'iF[M3k1^4D';#f/WA%AZQLMcJGfK,3\gQ^=%iP#'0,!MC&
.6&T(b9-ZB)7QLp^TEILPC44c4&I0?9D722-n]d3/uUA_n5rI84Vf?<>,P#P`j&/d;sTCY*C@WfeU^].
DT@gSg$h[DO8a3[CeEj7kha*ml`EQlnqKN3kkST<ot=q@N\aJm_KL>l(SC(Bj0j,Za9@S4?-[S6>Whe8
>O(hDHPW-kRr$W*q4uo9@mFh01'9gD7):p_8E)Vk?#fljP"k^uK.$=lnYCKl>fZgJ:MM`C3h8X]$p'%u
'pU%[76tg#T(>CBCpnW;#_!7a7eKLtL0umgW`+Ko=S2V:$N^rK]2^khe!)A0:0Q%[?1&G0b]r"-rEa"?
Mm&!ce0CZ[pi]A:QtLl3IsX9G9qK%e?aZ0'5J0Ki-0fEM+cXKG3E#mRkRd4M^@")YB?eo#S%`8!W9oO:
`HLO3Z"aWO0FE:O>Wj_g')2@A_^kTu*'!o6n[sA?*UA(P6KV./+K%^;.m82XZ2(Y/%B4(9T@2PUX3\P"
oS@c&IpjjU6n\rB2Bt4Op4J"@iqr$TT+SF0n+[f0SK*3AcpZ1P6suo)"+>XWpk"pfq)=>srHn#.oZ1iG
dWdLQT5`6r4+2OjEaM:_Op-\MjcW!rP-)Dh+72<4a-(c8Ar*fARF0_b-HcrMTlDs_-JP:=B;o='N","\
L2AQ_5uTPh0uQlc'(-B^mmng5S:,NjmT/#9_7C!=mt_Bi*0%FJUk>^@F;W]O7Omq'UJl--W6[l'8mYk>
o=(t!n2.nd\1:=EmhAd4`c0["Hi-`CN=*4t#4K3A*eTLc48oBOUKSUW]&:9o/L:"]^6QfQ;*iu^''k"/
qEdJd@2,!u?ZoBkp%NL?E,ISEY;0l=T$?'>ZbJmJ>90UVL#RVN#B@$;VmM=r$$u?e3-Y/PX86YSTqXS?
^i5!V_kOJ;IkWL[$0,4%Tg>.cf8^;_\+%1O_sJ;BJ(EsJ.!mR"i'qL$300tWhHQ6e`P5T]/,(*i)MRpp
]%qLiO'13K+EsWAs2(Um^V)=n)RVaV^aun!%fMK-Bq:i'b;5O*02RU(=SB:SWI/W1n'jP:^H+Mfg>XU5
SdG@$o&ffT([l`5%On@H^>hlOZ'6iMeoNO1i>D8F>:6Q?ar.BOp?1QX_bNkcgW9dY6;.;ma.&Kji6'!L
@V$\:WK!j?6>ltpZ_Br1W%fe:I[PW9`:EtI"o72FOs';O7Ob%7ifGm@,S^1l`hA1fP1^';]n[*qW`+a;
EXL[qrOOG<]dGDE<#fhB"GVT-S^[87-7^Kc"tW:!`5QpkTQ3kf5^7Y3SkP$foA8HZo^pZ7a,]mb'%/5s
noMZ6(9R5&q(K93NVAH-4+L\g^/R79jSa5&N/@J/O-K)rFPp5k9'h[=n>7ie^,!+&oD%<O=u3OY"Cq.%
_s<aqR-)6FE!Z]/$O+M'`DQQ0EuT>e_2A]?=IiBm7jYZIr='fT#hk3f!r:chXOLAB?>9UkonCDC*]`fj
3[TpRQf<"u2s?p<MG!/r;t-[QKAt2:YNL7?<R1-f>E-(ecg>KF&ncZ5=!k(hZqdKuF>un-'Y!>?_2J/i
3)r*S(H]U,a0dRD*etC\G^NAG`7dLr9WTe:iVBJe$4[[!MWHL^a8+T0j#_'?EbS1%JiA*Ln]CSn9^*('
i=*caJO:+7q022Os,3;=[)VT)a$\FnmZ`=Umi0DSTm#:1fp\j.OJBWGXDQZ\]93N^SrM6A_*(iXAbXYW
aH\k8gqe3Wb*K<-mlVK?3B1[.8::cV=gr%+YKWWR$a'4R&+Gl#A3S9TRXB:_pQ\bkZ^1%EDq^en$K".n
QSN:nWhe&gs"[2T%oSs2D4>1dAu![;,>$K/D"cGWVf(S:J46S2gLPAlbLnb;%l\D0clQNNOsb"`@0eFK
5q6Rj[m\-Fc[1j[h0F902d"L7>Gm1pq$(/1-_#%WdaZL62_"9]_(0VVjm5OtRI>T+fC>l5;dlGG.fp[Z
9;8$U/#!FEd:V^19^Xj6B4&/eIhph#`7i:cVVP5"ZdNZT*"<po3O\4=`V-Q.$89^8\A],_/lcW5M>erK
M)66[d&/Y_;4=.1<H@muKDt2<%G>*i6+qQX_kb=V)ZsjrPgGkoXP&lN=8.tu,gMHO"C)$`$f%QGm@K//
qq&[NEh0=rkfB*oIhUQn2KkDR-+8qiIBcP)n@.H7;-N)9T7=oOr86[cQl5IffmPlmlRCUfjt52QER<3<
q9%'"$eSS_D.Zb,m9]J3E94El;E;Q'0P!b&'5S_`f<YbgDUAee@V;gZRAL9R6]aBa>=Y;:'d/F%GLf*n
&EIA0*,-=k)lW;?YE!eWpZ&SA!F$2M=!(bThcXC5qO?l=Q2V',@fhu$\fb\'YU+53RD6`*X.a,'D=kIY
:!3UHe?'VJVm@l+L#<[VN8)($ZMfq8p+e6q(r@/oojLuF=b6kl$m^^^qWlKIoN*Q+?2ui*[6Z06(9I6(
Z<4+M9:DC/O0h)edR@9+W?r"K)RdSL#mFTCD9W8T"$]?'*tEtf+XYR_%Rg0f*N^2KT:sJk\+kjEdb"fq
Cgmq@4@iK=/o7@US>+(0@gK_#f`D)DR)_YpSf>a_.P:Y*;-"X=`:0bZ=F2AjZpPWV'MjA[YI'i/VZiS+
.DETa=B.[KgUV:_HS\_Kp`83l>MFL\hChl;fTO&t\F^553lcnqQ9qDAMoiUh,Bp13/#SLQ@1=AX6W*r0
J\m&A[[&$+`FRlM#kRJcQ"gqEg4R6u&$C<t:516";W/%+EWonn$k9?i97H>#EE@\rf3.:*6oA=uDe>J>
*E)?WT*ecr4M6D#d,DpUfI+)D^XZJ*G@ncD@Q!Yg3&AP=Ia)qbpBrK9+66>:ETIMVP>3IFB??BFnEg,o
]]eU>2n\0:Hd-lC3"j<69rU0FGFW#RQ#i2J8JooGK-O)!ja"t64=G:$g.<rfD];<3=^5m)l[T-a1+'H9
2J&Q,plN9#Z6VebG#k!>pQg--RG>@P=N3L>I22a%EOM-X39X7RY?`"<LBAb,$?O9'Ak?nsp*s0_>r/"M
X"7[q?Bj=9q\l#;G4Yr50DVtA6isX6V>GU!Sk8=[aQ\u9@T>7F2:MFMChjIT)!pM,oa(AK21noTp%8q,
=K>/;[tu[WDMGla<=dY?1;Ut6UU@n.NHJ!6SfBal8]f@i.pXPubt,):i+'i]JhrLsECB_Gn7He>/2/O1
EG$l_Dh%aFf'.*\#Za=>*osnaZfY"kq1WIXJN$f0UFZjmR;A@]IIDrkl3<7`W946nC'AA'JdXYp&lGN\
6Y+NMMbK:#7UL."bC2G94K/T+j0+\[:OLP#ci;"KQ$o7q!SP@=gs0ZD5JZ:)ai)fkXFc=f0uknWS[-PP
dOIlPI*\6b6M>5l*)\:R>4>NbO^'j^3hJ.^AG^5NQ@W%pAGSXCq]P5];6d@^nY$5\J&gcp&FRJ:4d2aS
b50,#K"+g>WOTZ1dAItbaCpd?29*L&=0#AB<UhcW(S3*Q]n]X#>e7+Lc54jSZY07kUNW?o[etuCjj2,K
DY`lZS'Q<%e4i-E/QTJ";iqfhZ5C]GojguSZaK5Unums0Q!=4rRWZfg0$XJLs+>"\4/V+Hf5:?8osFY_
\h3r^]a69?VNj0-InNS\^Lch<U0J%S9?Y'b>+VnG>J2V<`ZMhSIh6H.a0bbiakJCs?<fOW5LV*l*,55W
r9G^&f:dE#>*n";Xa/q'A,02/jN@o8e`esm;Sc:Qe&lk#Q@GH1X0he-E7$7eRgmK].Jb]#o.6-RZk$On
[iL:smjhQnLle`A`AHU*U.bH(bPt#=`#dKTOB_\hHT=GsO+h7DIo+G^dZRi6/Q$--nuqq)q(iHP=T2;Y
61\Xb93*'srjQlM0ta"7Q@B>oVRM35nio)_OZcdAMISh$J"qK&bF_(2/n5`KFX3'.mV:!!#Aa!a7E=ts
<,g$qTb+&)rX\f+/`/fgX<_KT[V7\ce1u!,bk:/VEL_&EL5"90E8D2:l&CW1IUK9<e#C#Wg9>g&YEWH_
l_[bO(g,_%2ek<WP*ir'ScJ]9L[Ra\3[eiIDN5=@a(Cuc[<%NpH(YJ(-(;s-PW^N*LWK4^Gaa?'3oYt"
54oq"'8Ub7??Fle'n([UV\CiJfXPH[Dra!f(>fK:(GZ3kXN-o\SQf6LSu>^m:;KdBh`6Gd_Cl;Y2ntM(
lT=?_6l+^IN7liR&,,EQ9_)j=\".8O60_N%!7R:h(YjX'%5Xt/%UfD##QK<EHgF4ADE_3Wc(;C2?sOBW
2c?^nRi2r[&YQYH@h!kCZsCo0n(Ou??D'5tVHU%d]ue.K21(BQ)1KWeNU^uN#UWAPhVYNGraUore=;^K
o,?$?lm:r>J"qGJ]?#Oi'GkY_5\%cn3=9I9m>:`!^j%RbYGHF<"1X65V>e8L`BlN$mU2d[X8VHN7Pl'2
llLg_Bi360^e._s6't.DJBMQeDN5ck.6E8*"@qlGE(geZba`Nr&>ne[pE7]>r[Bs6.B08P==`[<TW,J'
.uKq1#@VfD0AV77[;P4pR"P)84U@)'g2QC"KEn&=3UKAnKAbDFcJS,Oh1IE#e+=d,mT'YXhOQ=`6*SuB
nqC4^ZdKLR#r=)U].SSFE?7I4nS9s(Bmb4<k-5/-*g005gWMTt%t\;;&10R'nfTVUrTN++.J;mjR"bL8
#@TO"/a5Q?]!r"(@!WA(dt9=ZB:'`nWEl-C&8%iRah?/^SXdfU)01:+A)ae*ZkiD"GP5,s3'@OG?>+Wr
?:\*V<^l"Mh1It?4:(ZQ$4k!A#aTfK=F'=Z&8(J*C)UKs/9],tX7&A'\*(HZ0tQEe*X%B`-69HcB(X_h
-r-TDiPEQrgK'Mo4qU0r)d\]AY@"J%b9EX9VNmO:a"j&u]6D_?@b.CH547KY/rj<a>u@tpHN^sBDj+]@
cgNOiYF4KdWqIro$I?irqo[Z]/+"?YmEcm6J7.qIdGA^rZuaTgWR&;&CDbIc5oJ70&gApm^?HsSZ[EH^
f"j5:`L?,qWh(/u4?kR4^ep@Sin2;XhB^>Qfu<K`=A^ujKpWo'hs.?T3PHf-amlK.n$tp7B^*J1E/#:5
0gcdbE?<U(MQ^Ks-<fFVq]*TMMH`mq%pUdVph2NKs,N.i@25C>.gEMf;2%0Z?HZDGs2)Vn>5'\_7XR>o
L=Ws2GP0_fY]-]K^[+4Fl^YT2(R>57L!S?B2.ZT\a*PW1glp/$5Pb\VZXff$lDJ"a:k6m,A,1S02mHLp
4*EGMOhKQE6&KJ`4a)^!>[l9O=%_'Orq`s!^*p9"DR[aq>tI3lfr0W18,\mHPo<l3W<cH@R1L3:7H9`e
MoL)kjfcB*Mq_Dk/Mq-.p%RZBNlH2f8*X9$bCAM4QrJ)8m1cAiIa"/'i8R4),PX89jsU#Hopl`l\:P?h
@cQfKp0"ss,=lh*b7Ws.(aE.s+t$6uhJ9f0_nR+<B$#9fU3L'Nc\c)hH:tnP<F4^d39l>AJ]4D!ZI`PB
C=PlFbFjLE)cX[UK7`=7PJ3hP;HgIk\D(+W*qqtd>r\X9Sq"fNr9K>k3i*j:@-rJG7a1N-YLjGj>\In]
7r2k#7[UZM.t%iY&KBH/,@7qVfb/DaiYb.kG3?NiNb7B=RB.F6@6[,c5"Mte7r8-"f8_hpG/Wo-G/VR"
mT+_ug(@OB<pYVOe^tI<s6&K$ZdI<K>)T"l]i9p;XBd3*.jsV1m"c+9p7WLH+ROse@n2Jb`fA^.>H]&%
Ag;K;et]PV[f4FE:FVUAlN";PnAG`3<0e^$'LBW1.Z$Ns,C]1oGVgoh5WdtjL\jh^k*KOICoG])^lB6G
NubL!&j'Q]i7\dtpgu[keRcb%jMYh(%*HMK(d(J0@I#^^g6()$5PrdN6?^ph'Zi^h@c>c6";$1\_C/b?
ChEo@^DB5I7G:\:\\AlG(43$hGr0%o=&_jq83-VgkKU<'r+04,kOZAL*rl)s?ff\4pP:'`rXsXmNK7_m
($e&-<e%useoY^Qh!e)Nj"t9KTpRTP9emI01XM?MB/DN-=TsJ8c3T+Sb?4>%7A[ZmhU:6WR7J>6j=ZpC
^lk>TDc'Lj*tRo9g\?#&"m;d\^.k,"Tkp@*Tp3m9n;P-G`WNlQ)84>d]PaCV%r/blIGl'_Q>K=+H\k^\
E9P!!Ho5%NeTQD<oDFL/rC&R]XoH()/$g*2)D$haX0,_,Bp6d%6$UAfJaK,)F-;/qHi-G_Sdb-'UL`'Z
JXtBkL3JS='uYrMQ0;R50;ZoC;.eR)Du&=]Dk:7%m9V,8*-c3`7-_1*=pO:L]Pt"CD].`NbA@"&]eFBR
XK7$mmjJ@BIW@?S@Ti]^Ho7bA^K(q0Cp_T8'h4'W7ecfo?'NXY<,c9K3QK5L(kWb?$`n.:]1(I^p):++
i1!N+#P'k'.?8Lb<_b.TCk,Wb[ILpk6_s.pgIi5r/Z\NSNA^#KGi6PfCPGP5S%Ra;:F9!8hEb9[-U*qR
A+T'4]OcAoi&\"H$Yc\"=rXn[p81/ohL$tXM,)&GI1so*l%-.p>I-mrG31pr]U3c`r7/fX^[Us'5<]%$
?^ua`["35=e<%%p;5>o^^dKtk'\NdtV_li":u\qFgrnAP5DW+Mc3i(mJa'S.ltiWuCC'%3)1tb?]G/Rr
KU$$=L#EB\Z4<nYlf%Vt+-*bdf]hSF^Z3VPNR.FYM%3F]*N@j4lQW<Fk7gFUG5>3+CA1=<8ZAgj$qY;+
0!u3J8_e=<lVa3r/s"NMa:mZ5]@.QE\]bG]7r!6I'TZ9a7S.@h5tdoDP9@pt9'qWiBJ-\5\^Wg1;Yra-
;N=WtUpadr$%KiSG,_Tb.S4LCCCR9!)4_I>S(ATbC>7jb&TZQ"2Yn`-H_hc^ld7!\W9-`fGP\Qi[6*DS
-++c(e94Nbh<F<ke@%/rHqE_Ndr\1KhDeD*D4QB7e98F5e396tgG*85TB$2KmEUt4;OciK0"%,?a5q@1
[%^f?pI=IEXUXu\Uim7fMc./g8k_kE?79S<[<ln][JX9(19LEkkh%&d[:U\l4&8a@C3LXL\CBq;kMgNM
=$D\pmP*Ci'c0PKCA1:Oj&3KBmkJK(:/)7@V"R%_pg^p2mM*N]FU[B.';P19Gaf#]k=bc59BH[,J,U8X
Q)">kj3..dN5NmOLHK38&"9+,bi]r<r&aW8c+i'eDY\k0:Lc(r=cClC5Ocr:G`k8:bdECHWhI_c_2g%@
4#2@;]mQ7A.P<U;]p+r7#)g)KBs:Tcca;n[e/jIPH*sko(OWT4SmO7-]JTH$fAN*4LteS(&j>Lb7=U#R
/M_d3K:[=kC@1k!Zi'afO]/JI<oZX=aa4luldCF#([Xr!,'cdj/ME^>B?#QU3%(pXRKpe;KI`\V02(K7
_fnXV2_1t7ZS0fQ8QMb!bM?1.6W.]CM*6\n^YatAMuM7`1g]#`8)@Un)@_2\,NX7_*V`X4&pr[R]]i(H
UR83cKccghUi]`MS+.C#3N!;WAGWYD[hOO-S]C>\N:M/`h:6m;^UY`a<OcVT"if@S)r62&q1$q#bDttu
&_t]6bSN75pg/93p!$cQ*=NL+0dfNYKmu>PWii4pRD:M(D<.c.l;44r(>3N4MVWHIn%:<B^'4mNqW"0&
9Q"+<hFGU%'fn@Yd+4#,1%cN=!I3,8*(+=f>Pf<(hY/?tMDK(QoT.Rkq4%QKaYSO"@<Uq]9[9]jCKi:Y
%.1%)P!R)'Ym5#^Y1e5mqa\s7aGVo#VTsh<aXkXWg05lCId<)h4iI*V)eu.MCAS4D1\kq9m_k=r'Km^R
XFK'JW9gOn'^b0(AQA!4+=G_!/oIk-0@="rL\Ui!5OP06REA^tbXt9&FSm1>C@LQX=P'0[bdOIAC<YC7
3j+Ec);!cN?iM@\<p!1$SaM%LKR$(D%;dQ6%[./u7H6.n=DAn[6N9P!ULk[Te:]V<GWnGM^8NdcXM'<>
G!nDGY,-?<cr\0H!05p_rYcXN+kF#aKk,@!ag/eI*T9+8A9J<T7L)Op$O':Q(M>bUcDCbL2YS4!lg:[+
4"cJZBmAZs#LA7`&]Xas^93:D?0E:Xme*8tUf@25kT(h@oZF.thN>Z8>I\R8En\R=9ng2ARr]!n)eaF*
QS@DVim,B&_7*\u$h>JK]d#uqj?'^Ak/f^L*?eULcEb;&VI!BWXab]8H'%&Tmmmqk)ndI6+/J9q[^.:e
dTU)Q,gdQ#HJ)@K]d"D-S0>eMH8p5qDDa?`7liKYi2iS1,l<Gm^mu@H4-N*(V\ZT0(t$#QrIuJ924-;>
YjGGYCo,PtK=B]j]WX=f/%m1\pJX%[CMhu\WOl?(7dRf^UAaaGNbN.<*_Lkg_f<ZsR*L?(]";g,dW"Ys
_oFl1MS)"AjQ=b$/C2CJ0mG1_8uf893mGKTKJliDN.i0e`Y!F&a:[1MCE(kiHWtrsBqc[A>"$Ej]=cE,
Z&s6uM2Z*9m3R(*;/a+sHg5gd*26c+P!;qZAW&23V8Rd.^nL&6_Dni2#lVRg%(dHiP%%(#1WCM=GWV"h
U6VXeLa_fA<bZ2@+Er@p=F`AdX3]`*)qnh+JLXfu4.hqK^BJMD_gXIgh$foTRDh\nk0e[l=BGUt#QC,W
>GdVELp-o?p]fH.;X:C8`:m\@=)#AZD.K%i>>WD2IPmhY.t_ea$^&9=U_hd_pHC!6fB^.XkLmQNJ"E>H
<WUq3:,[a017(Gg?39Bh`mq&EgIcoA@uk)cbu"le3f4MFS=TJu43c7m;/b$R!Iq-8,B&^O#hut5%""aI
p,$9*8IimC+Xo.JK-\MK.\T._YOLpofkA%U1$8X_dQF-p47rWO)XL+E_??WPL>MC%K=^MdqF4`l.[VC2
6DiudpB'#X>8Z5^.`EcEDn#-&Vlr6'@C1eFU)k/oXsLPB`A;lFQ8N(?@CM;B8\f,RA'[2Q\?VUuN\0+&
U$O.'h9U"oq\d(+d.6j)4+]\u2pQZ^Vpak^P#^1;0@+;d_Gt0A"2'a.CR<mZ>9)Airo>#t@:;=7$-Y7X
:M5Zujk1#!Yq/[\dakWb43h!Nnm%!:V_B\u]5nA'PJ"hFGri^h/^Ia6LRN)5.p!3`T"Si\5\\\.!j4"K
G_Y>5l/R!2Z]d'*QD]fqc\6(Y?K=Mr:$OlET#)C=Wo8tLH$3@3DL$CRBka9tBG&DXm+D6XnoW0WgQiU"
/=e=f7a[+AXSia@]K87T@<jWJR5AaS23!p"O=X2d,XsEY3\=.5:7((qOU4=>kKtK4=B_;eBA^SsFLd(d
\M,V]Kh\?8YYQ1pg-/;uB1EN4cp6YV\8Bp`=UrW]_\!KW"^a&kj7H"c,KmFb"K,Iq`[C4`H$8b!_X\D,
jk6o\Q)6L^['2Z>;44bDahHBn`SqY:TUmuppU.3QY4VqAFIptibHk!P(c(/CFXul8Nog)Q=p;>2T]^uO
/=;cfn"Z*B\*6K*7qQu]ouMM7FnK/5e7/'#&Se$sT6i8Mp(5_2'3Q@&"(ROWV9^D^66'i\07c"PB')e!
@rRnRAn5^FKtV\BYfjm$E):3b)&#0]mY/=2&TW2ma/lC@`l5/:7&KFm;%lW1]No%g&FYr-QA(4Ajp8"P
UW`AA@eJ'PpMVaWMt_/":Ik@&e=#1._/Ek1o8W33r>V$(o[7tO)a1^ATDOu*L5h_V:)s?7-dh4Oj6.#A
^NUZuZ>.!]XjX!.4W$Ou'^[@`><U=+dt#Tk.Qse(_,"sSQ`Bu+Bj#;uWQmWn3/6c_g4k5e/U/hqOO#(3
%<._RT0st$bP1K*rp_)ponT/H4PSO4Lib$u7mHtAq>YsAlO-Xs^&E&31R*%jXTIKtWlWLF/BamMpQr)(
5Gm?l=&ds0(t[ueQ2rZ,n8`TuHJX=)^*Gl==#tF$=e3,&*1cofEZ"KZORCb0OnaY-jr'AlDE7>Qi^qu*
2?"K?9:"P^"t,bMb8l4/q?'q+h'da_3?c`G0cKbgd6PpG.#e1p5J\(I@5sj'7iJDD(o>7naEmWqShR+L
4S*l`,@7rr7@^po,2jT&+%;:m]t@%1J[WcS7b(K6F32![)a8/F5Kg@X\,rsF<fSKL16hs0Z;s!Hn."6p
s1^,2j)B=?l$SWTGtcK;R$tK=eS\l3hSuC&WG<c[GcJqbcZh'FVJO:ON`H[&/P3o0gVWErP:REsFLJOP
JCf[`jnkLQmFO4.bL\UabL\V,j\N>IHl+Foh0IVSB72Htbl;[Ps5Q$;lYZW^B9O!0eruWJ.94iBmXgI_
f";(c,cu?)8"a'mo.J1X6Omh:6PU#>K6ar\+W"RG#?l37MI`@%5sr]W[H!9W?bYr)\aBEg7h-<P\2#K_
nub%qF7?"%f=7Ps^LAl+7a;u.;1gY+/:P&U,-@#\C)p]#4lVn^;c`Ef,_p],B;k_gA9h:NQM!FPl1cMs
>p3>lf^_joXJ,ocje6-7Zhd+VW`?@S23Ls%eRb^mr)p<f2Iqp&I_5:%s*nl=Krd`,ASTH+?'qtPoX0+s
3F@>#J7TCd[*QsU]^AV,q7-D4IJn$Dcoi0_)7'k43jW%eRuq\Ti3mRbomMf<o'SB^6e?q9X=$DPMN"Lb
.d*2j>!W8,QqN`>4]S(M;gEu`$[=I1X8FtgIC)%>YD`P(?"^QNB58a3gh%/K)&\lEfM$a27]phnR#PLA
edLYI*EGPbor[C+oN8CD9^H?1bQ>56o&+;[h(8MYD65Y'P5_`:e4.of;<;M/m<eeL?>;`NXn2Vkd;ncg
^FQct5Q1V3q__PeI!TG<%eKL=@?\TrmIZHF_/,lXEaLVWk6CN><8KkoHkn`<h7c)oZQ"JG*eAT7nX]4,
b=B2i8Hru<rI^<(QAkl6<F\;@F[qnUPK\SRD,L:faQ\V_]U!sAkM,%,r'.sZBtrn8f09>g$M(.%5X/_m
biJHkQ[^#-rpoVe;>LlA~>
endstream
endobj
7 0 obj
68467
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 798.00 551.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
0000069210 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000069186 00000 n
0000069664 00000 n
0000069380 00000 n
0000069419 00000 n
0000069521 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
69737
%%EOF

View File

@ -0,0 +1,312 @@
<?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="372.44000000000005" width="681.2" x="808.8000000000001" y="142.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="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="296.09375" x="28.614190924657578" xml:space="preserve" y="13.404178370786525">satrs-example Component Structure<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.4579944349315068" nodeRatioY="-0.46400983146067415" 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" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="311.8450000000001" width="155.39999999999998" x="823.9300000000002" y="187.59499999999997"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="155.39999999999998" x="12.272399999999493" xml:space="preserve" y="9.639200000000272">Application
Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4210270270270303" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46908977216245173" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="207" bottomF="206.71046874999996" left="0" leftF="0.0" right="5" rightF="5.399999999999977" top="45" topF="45.13453125000012"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n1:">
<node id="n1::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="838.9300000000002" y="247.7295312500001"/>
<y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.2216796875" x="25.38916015625" xml:space="preserve" y="4.8515625">ACS Task<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>
</graph>
</node>
<node id="n2" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="355.9183000000014" width="539.4029243565859" x="971.9147999999997" y="158.5217000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="content" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="4.0" x="267.7014621782929" y="0.0"/>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="1.4779288903810084E-12" left="0" leftF="1.1368683772161603E-13" right="0" rightF="0.0" top="14" topF="14.073299999999762"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 4</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n2:">
<node id="n2::n0" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="311.84500000000014" width="490.5852000000002" x="986.9147999999998" y="187.59499999999997"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="490.5852000000002" x="18.817724356585472" xml:space="preserve" y="8.551700000000238">Generic Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.46164229096885634" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.4725770815629552" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="57" bottomF="56.71046875000002" left="7" leftF="7.372800000000552" right="193" rightF="193.19119999999975" top="28" topF="27.68500000000006"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 2</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n2::n0:">
<node id="n2::n0::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1144.3088000000002" y="284.00476562500006"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.255859375" x="24.3720703125" xml:space="preserve" y="4.8515625">TM Funnel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2::n0::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1009.3300000000002" y="397.7295312500001"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.830078125" x="20.5849609375" xml:space="preserve" y="4.8515625">UDP Server<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::n0::n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="120.0" x="1144.3300000000002" y="335.7295312500001"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="81.1298828125" x="19.43505859375" xml:space="preserve" y="4.8515625">TCP Server<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::n0::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1009.3300000000002" y="337.7295312500001"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.1689453125" x="24.91552734375" xml:space="preserve" y="4.8515625">TC Source<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::n0::n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1009.2876000000003" y="230.28000000000003"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="109.9228515625" x="7.53857421875" xml:space="preserve" y="4.8515625">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::n0::n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1009.2876000000003" y="284.00476562500006"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.2216796875" x="13.88916015625" xml:space="preserve" y="4.8515625">PUS Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2::n0::n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="125.0" x="1144.3088000000002" y="230.28000000000003"/>
<y:Fill color="#CCFFFF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.1650390625" x="20.41748046875" xml:space="preserve" y="1.703125">Shared
TMTC Pools<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>
</graph>
</node>
<node id="n2::n1" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="284.0" width="180.0" x="1279.3300000000002" y="208.7295312500001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="180.0" x="12.572399999999789" xml:space="preserve" y="9.817168750000121">PUS Stack<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4301533333333345" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46543250440140804" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="24" topF="24.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 1</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n2::n1:">
<node id="n2::n1::n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="247.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.255859375" x="19.3720703125" xml:space="preserve" y="6.015625">PUS 1 Verification<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::n1::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="287.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="128.39453125" x="10.802734375" xml:space="preserve" y="6.015625">PUS 3 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="n2::n1::n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="327.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.529296875" x="33.2353515625" xml:space="preserve" y="6.015625">PUS 5 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="n2::n1::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="367.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.9453125" x="31.52734375" xml:space="preserve" y="6.015625">PUS 8 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="n2::n1::n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="447.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.205078125" x="36.8974609375" xml:space="preserve" y="6.015625">PUS 17 Test<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::n1::n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="407.7295312500001"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="116.8515625" x="16.57421875" xml:space="preserve" y="6.015625">PUS 11 Scheduling<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>
</graph>
</node>
</graph>
</node>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

@ -0,0 +1,835 @@
%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:20240208115813+01'00')
/ModDate (D:20240208115813+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[afZr%OsN$$?ZCN;:U+hm2(CkA#-&YV>R3Uj"qa0Zrt!2qelkQ>N_hnSoNp?Fgi_i8SMXM?UQLkW
e+;HuHN!Fgq#t!&ci8Ckq"<6gqs44=YJ4\WB8(gBo-Zd&HGAJ&?[qt=s8;3_IbsZ<ro;?)qR?Kf^]*VM
I(YXBr:b^<oABT>J,1\ql0j-"j4!&gj+$$s5&'UKp.sCidLNY[7oB&2WpB]A_uDL$^\V;XRH0!YUA=Iu
%Z:8Umghk_1;<)ME\HfAn5&"L'8_D*5B/gc\%hpGs()lA%t"(7M#VbsVm$-fq+9l_qVV<VBmm2JkuL@&
[aTHYT>Eb#n(&Q(k<ErKs0ocBorhgHn8:?1G^liTn".;bW8DYY?OPZ(F0W2;3'V=#m<e$"eb]#BNb3K7
R\S9FIQYq7s71ehq_D-_UUD7D^Xj+(BBsa_VmUo5MDlhOANF#6T?Z9*Gb:/42h009W6Q>FT>n3tT5N]B
2'"Qe2]"sdc^+%2*)H38D+AqtIu!]8IcnkYjmKW<Q(P>El*q2#K`?4E:[?L)Y4eFZSX9A"(i^"/Zu5d+
[PqQ,<Bb2C-DRQ6kFXm.GM/!SG4^1DL\IK+n!t!"]$n8C8#3Uk*sc@^=6h[5%7D\>X`i-s!Z)e[qHeEq
6Td_IHQP0B9:X(Op2k::Mnce\8%[bB(epHns2=+T45Pm3POJG\"T#$9=s"MICEALZf2kM0N'8MZRt_tF
^ApFhhMohoGKcgZoK`Z%H%1=Ai:#h"\A6u*ZT9$$3ut^UH3;daFNKp5AS`X'4T)KMi=4XYP;M[BBmHc&
d=>1.FaH9C,u/+oON<ad/#Bgn59,Z.?'+`N+`kn*!Pci0KX$iD[6^gmNQ1'$cTY\:%:(J`@mJW_BQ[Il
ouiCd)m*GoX(ffIBUiCFR)H$iI0P#=[<aU50WGj0_1O8:fSd4NbeTeTmI*pIXPmgK:!W*li6)Tecj$Ir
n-2tkZ_edrf;sJpiku'`LU4CW!c[#NN]-[GiP!M8a/.O^IFR#)jHkLp-%qg!5<nB(dj[f0:8>h4^l[$;
nV5r*b6>+C@s4DSIDr^dmaW/@;N=Ul8::XNN]&WgN-=HOLO-:tec^:6A"_?n_+N'WOGa;@k,`91E`RLZ
LH[Ncfa0h,]#;S73Fb+=l&lpRhZ<+-]n[>'^jc+OILMp.dZ/h6pe`LWm]Ns6%O#6iZ^,9_>UMXUl`b/7
qj02lQV#h^neSNDZTT#)Y4`idH[W!p*fR#4HEQV0V.DN`G`\!Z&OY2@ib(TAl^A8e>@%7HAN$?u6_Z!0
NI@hB&&A[V&Q3PR?8%kr?)47'5tb4A\(,1'S"uiMO,\nTghqlEc,Zt#7Y2E$7bh>U];BmDADQX&^s0Ul
o0!&0[rLlQ&"f7ZI8Jh;FUsq&'%]s72l4MtZYWSB,55R?fam#iYE2"4+#l9/BiuOB@"n"(I7#AnF+oR)
gH!%p-$sF%Af6.R5!5=g2?#dFTDaL55<]%'Sc1i<k/2>&I5lj$?#.dEc\H7idL7QA>J;0V6;u"eHaL3r
Hp>'rdsWJqT[>f/Z'sok6dDa'UGcP9A3PBZBNBA%j.NA8!2#\CDJ)0'i@t91'0"D$L8QG+dBc;8gDk'6
^nibODG5ZkBL#1Pd:Q]?B$0Sm\hJ%[J]`45Fh*9AP?#%/)'R+jY=`!3jps7/>r.9!0.bHL_$RG[T)Teo
-TqD6Q$U^Em'l0XTLplu)8g9/ci:hJk<E7#aRao.bO^4\BBSc%*oqABci5,4rke1&+1.^4)rBm!8"TaV
[g(1Y6'pd7o^Co;5GQ?ie_p6fA'\gI8USnHq*-\lcSKD6K4k4Ik<C>q"$HU9i.DTio:PXJr221_O6?Oj
Q1iims/1WhLAd[Mo!6QIhO_KAr-84I]KQ9tDU73FKCB0UQ$%BO_XdUP]M!SM'=NOb^O<"cT"jr-K]fN[
_P=F9hk'ji;]Djp81Dmb5s*X6Ue;'@s$:LUk>)e`s1o`Ik^G?Nrs'k&NthL$5BMIk0@/PP$NuB&UB-,4
5Nriso-[i-q&;'Cji'?l".PjMiPi<+&]sW/32-$9r+uMYfCAcur^khfAM3>\om_]ra&pO8g8,$[`sh_o
o),=!i;#TBRaLE+I-VTXnoL;TmZQX3IT<I!*</-G;<R6?:X&!o2t!]\FFU(TdMiG`a,M6`AlI<7[b:()
5%oLk+?+(eK0;E-eohF6>klOt:9c-bqEC;4;/a_KbHna/L.8k(L0Zp)?p+>-aY$bR4?ftA5>TDNO1ta/
le9\"iosoqg##3n`-46f]=O$W)0/["1jKU1BS!Q72s$"+*tS@E"P3^pKb)GJ^&D/?ErNR0L>8qJ[lQj>
Hm_%(eb,TGSRYAA5O\!D"5W54I$NQc;8T8]m,4TC+WfJ(p/?/NG<a;b<2"em2&L%@G&;^4LCE4!2ubju
mT>J2j;_1XfaLgGr[#Y@A(_;K=Jq>8X3Kj1F,JH+R`m@d=0r`f`jaE2?-k"EKZto"Q[2?J^bVCnGu0*`
/!q?s%WWb;Sc5+;lr7E.>kW#&)nhagQTr0s?E;/<*e(e(Q-]%.qJ<\NmGTl`mI$KMGX;'tUQON<GQ=Q7
q>GWu%52>/=1.MS^AbbE(t")'AHZG>-&_]ZSL-2(7hBi))Se>\E+:H%G\_k:Il'D57M(73B)+*D+p`p8
4V7ho.eSEY47+36N;"tI#r90BC#+k6O!&6He!:Y50\dgYO4H6H@AP$'*/NH@?q(R]FE5N8Y"0Er&<=<a
)h,\`+;eub&HqQ<<Wp7Z;,.810]N-J1h]l-`oo5%d6C+F<DMk:CU==hkuNif(fhp/.,C],HOlnX6=>is
fIo$sYKqBIPgT4$r5.06lh$;`7UWp-Ds7jeTJRl!(\S,$@#lH**5YWkR'd&Kkk;gU1a)[*3u(\\?Eima
LaR_rjufaZCDL8-3=V2/KCMpD=L+!R?FWWsa&j3F#g<R+i=5b^.'8T!$5uk'0S85I!j9fFZm0JPZ[P=X
IB9-mN=tkNL=%;_,mYsA3I5Ku,j0IViQ]E?[s,sXEAq$F34fmCi1!C*K-n7+LNRd>MJ]q;@[PQfB10A=
/*aB4G92eWP`EE_[Z23@6fTr,^X/op*mD%:g>9DKLRr2ejP5K&f+kp(2n/]H2UAV_Gh@6M'o_!]W:Z`O
B:lSTCYi5c)2r_A!\MtcXg&q:gSma07p6V[^U_#-ID]L[&88p"&O,7FE6E1pN/s@d95]qM^*J'HnNd\*
I!HqhKb<D-;9g$9K1e8-0gdk5#N^)f0+UXZ[gr2q"1iEl;oeZhO65edmC[C?2?!Lf1VR7A5qCIE4Y)UK
Q\DD@rE(%<VAEQd`:Is36]KeQrdF8G:Wb*;nSA;5-2$3</.X)\ZWT'5a4FPP>QS\\m:s)(R03k@lGT,&
NW-^Z!C$N&G4u'J!C'&u&A8@E80smc+qH"0N$>YY[UWfrd#h^_[U8Ql>mGW0^!L&qL7s[>l]Nps\,qCG
38V$Icnr.aB00qW_ZBT[etRJofeD$mr9AB:;a$j4A:MKtE\qO5ZD@J+clm3ZWC.+g$IX78jpNt)&_m!k
r&T)T%!WjKg6<R3#2PH@GU2N>e@lngO,+<YLR*fBTLAW0$S5Pq[qH6;?S1g`]gkNbS.m>W2PTuC#LrD&
ko31W6\4.!6+:u!1A3[)23(/.(o_rj3h5r)n3GQ+:lmiOl0B%mh,9HVK[n([N".+Xn%tbeq"uMjo?SAd
4/E[3@'5$()q_d7ENFs*oiMgpL@r2[`.k[_IHI9+LsnKAe@,(:@/CidQYc'J2a1j.d(WQ!>Pi9O+u[A!
Qq:[=MW)A*&E\j0T(Wo(^F<4%rge.3`1)*A=:oK>WDJTr<3?t@NHOpC3jcft/?X0^l9G'=H,u4#iba%=
Yp'[r'ldVg&T@%j6_C_,[b'bQ]DkA;[<_XE[Z>U6Li$rgilFTE*%PJc'9QP$.7ch-07uW>7Ju9ik>CUX
ECK4R<lFt`fZ9t3er+gUL^f\Q&K?_?a(>+qp@DaACnk:o9X[]8U9B@^d:^a/PehQ5o@^+J,'Jtf?a1f(
pYTOmE(-75DLW[3D(#ojg#CB8kJ<IDWAA(L21lDBhAsi-E.0Y)CYe@,)"agaXjT!BT,"e8XOP'bK=NDL
=Os(gZApj;[L8A<P8"q*:LU>?oZM*HM>(oY+29jbdn;hZ\NKfo_UoRC>$i2I8aQdEb_/O#W+>3JHS6d\
>7sC?dT<J)@LHQJ]p[JM5]9c3PjodjNL>25[!nZl3igEN?]f7GCHn,*E35UHq-m<t4n`RS*g&TnHCK8$
(;iiOM;CBrD!$`BQPK+JpC?Z*]pC5akVi-J\@#._LMVa*VOc:hLf(JNWRE"P)FO/:Z]F.N&^E)UZFI].
^KlRA@Pj1K?]NS@=du[c\ptE9P)nNE?cUmO1p#]gm]ZhW"Vu9Z4+=`"E=D5j`nRg7hM=7ahP2.cK)H-o
@SYsg-FO#/rEGc5#q\.Np+WrKXgU7Yqs;B@rIpu\;j+NWCr/A4WpsDjK?lTn.Oq,^U>Sc'(WBZ%s70Np
UV6%fi0aU&moB?lda.(QcNTBZ_\7T1"N4Njb$+L`DqHLfPu`i,U3+V8i^%aZg$/+8(.j\h110o9p2%g5
Il5%uB9.kC^Vh*dQ9Xc_`eqM9kH.)toLK\/l0B$j$dtdUJ45P[6dKSO"hTKp^tS9R'`iRKid0SJeP92q
@TsiN[^<Y[(Y2e`XMl5Ef\t#*Dg,42fC\ch\6j*]6:_[gipc>Y6-d=PNYSiR<lhmd2op*(ooZV#R<Z_*
He@9^e[#J:.3d6#[+K$i]XG+*br`D52=8$)ikZ(IfsKmNZjIBF+g$i_@lu9m4>.]7'?9f`(LkG[*lL_P
N,K0m<SR:L=#OatP+R<q[>:8SCWqm$Y:>RLnJ-!\+oi,ln<@bf,S]8@53'c+5Pr7$^=D[:Yp'/YhTRZ%
hD7Wt9X@p+_3=@>CeTf0fs=DT`hBF`2h>HWf[4*m:em(tfi28`+oo%b"M.b>^imf_^Q#G+]X\15'l,:[
!!mAZ^Ca9;`'FDfru4uA@)GcJ"Q2SiNA-6+X?V:\HYZEu(*4nf,-<Ra.">ScSe$:/o[WdL?Coc$Si=gC
qm-g4^Z_paJnNI%6.C%.4T_9`_Z&f5M?)k@L]FADd(\eKFL+G(h7R5<@hA^h+X-o^&gIB<4>#UOf6l6[
HETY>s#SfagHc]aM`YZ\Yu5ccP2q%O*"_T/*<FgiapBeu<Plbg?LKbq8A)&c`=n%-#g'X`A%f!O_PcF@
A#7JSEZNII_5=3HZ>^^V[/*>`9@6&o%4WN`7hkS:^$,o^k<qAABD+Hm^V_jKPiQmJkaB0FR5rR`bT8T$
40'BDGS(eO;#Sg*1VHk]RD'$S9Ke!rV>eUnHZ8u0=6j@0jI&00LuTX?%j(;c*%cGJJYKotm[;$IiU6<N
o$$`+;$fAu"`O7O-r/+4T9$\HplbLo%slanq=TC=F:8!]8)]j>3[4X]nSFALV"Yr0_H5]'WCN-NHq?D4
kLL*#aF@on0e^6]ht)6G[Y.D:L^o1?F#<Zg7Z-p]3fnh^-1iT6GgR<Aa-e&p3kka>1-Sg8IpmU(J`2+u
U8KeuG[:RO=Rp[+:iR2@^k8G:g_4pY]"duR[8?\H?[7',HNB.r4<6ppSk^DS!lAS4)7/g`A`r(IT9u4#
A,'H>ibPoJm.Fra*3WlHb004<&%boc_2]@"2skKe5Q4C2-efL7@fBcZB_M\MCOS"u<O1O[aM<`q5/3JL
rAXEujFa8!>e`I9Sg`ZPpNKIKda:8TVl#+5fp]\QeI'!\_+,#ngg,E<9'[AV*n1pYo_ta%Kac0RA3=.j
M>=%>qG%WH4FPq@698jOR\0S_>Lq!3>9?Je,O5lbqCV$p-0ljf]cG$>A71HblBh'U9#s'WnpIhbIgFO?
cd[&Y-c\-Kf&3J5WmDuh(C^u*$P)\TjI:Y?;2+_fUQC$i$5m3f5nl]Ug.PJM+q!Q_FF&lg1gAYjla2sP
8XpOf+6rAb)37*Fh9_q6fnlZfO,s;?oF\JAXk5hE@f+N0m6`D4QW@oUXk5hE@XDh.p<nic6S$`S<idsV
=?j0%I;-[sdYd]b/(P65=?j0%rQpGp6S$^uX]Sq6Z+dRhHQ?:p^9jUTRQ=^-5."dGBuTeoMX<b]g9IWP
(#)A3[bnH$F8=?8LVPX]T)XV7`?3EU@\.TO`[M-_n4S*5@\+`'_Ilc<=pplnZu4-EId!"`i5:u[HbJo0
1]*iEPt_L8ef)m:[`S0\;[s<Vh`OUm!pPq:i%t@-Y(#E(+.rSO3@*gW&'1RB4'rjq5^j_YoELK]a0)6i
I.#(`rkM,\bF]^BoeU^1-u?<<Z(W`0cfX\:[3k)'N)c2JR/JP"jc1'BnL\9=c&pc'/oRC94j1L)NE2Xl
>sQmGlpIMm^h=?<.Rol+q7+H'BXj6"YsrVhdfl2>39o!#'=2BC!PREd)rFc&Gn5c03/Tp^BQs'`S%&Pd
lGM0`a<G5+(4NZ_27*%lhfa/'=)3'5-0R+BoKehYm="2Vr4AMSoD?[)$_20j,+eKFI_6!-jUB,m4OP<L
Q4s,%O0:>"a3aC969cQoPqmIOiCq'lNF-g$3OC9`9EF7XB:4<6dE6OS(%:5k/W%5fC3>*&R??#0IsW4u
km\]dn<ZV=DKL=4?&W,K-k+t$V#R/i7I-1d^/2eo;W%U@TJ&pjRQus`qFd'JKP*e!WXJIU?a2C3H:P%-
ek#;g8;AJf8ZdO(2JJ*Q$[M%SfRs8S%%AsE>DLJo7iXR.lp'L6$,EhQ@t2P@\*%$gr!6&cM=tGc2,:kK
kct4RU*%E&&;L^^+#_"0!fp%mnn\3[A(-pE9c-M8#No4B0t.HSa!M@in^"\(,S'\@q+htgiLUS4KXm/'
rrkN\2]_Gc)i1D[80u5nS6P.hmnTN!jNl+Ql]ekg>VoqJc=[AF>YB#iP`IX5c03&iqd$PArCZ"+_oCI"
+qDB2%aH'/b76cX^[(-,\c"/8Ji\A4>drTZg4\?@?VuKUF[f(j4G?]*/9=SP=3O4S>)2]LK3$V%@7F=1
9Q"8j5o_+Ujh(Nn0@SYlhO(9BW5m`^/?L+1Q'NUWY+&Lhe@8La9e/1egD(IoekT@j9="h6P87sDb.4@"
,R`Bp.e5mK,fZIIhs!"i%iG@2*.d:&_oXfdIcB+D:QH2Q#em(S*ET-[i4=\R.\88#,EDH!UFW[qWDftN
_HpdqJ#5BiWcVfe"^eBA#'iK)28@MDX.=CtY7D&5f](CNhL6L3],NtcTiX?']&eeFWlU2g5jH2YHV!H#
fmZG5*u$^hrbT2JUKb>>DOl"MNpIm<[P7^HF1r%6n?sbj'[]&&"^Sj]Z08B9Ph']pA&-`c\8Xic:IJd;
#:lSPoB&rt!?M[6E,dQ[a:2P*G:%U8`*#4BV[L-\^8\o[fRo\klUC8F^lRh*\S1^N/`g(f%buag(#O/e
iirPX^,+g$c$PrpUY5qnH7-_5II2r"R_Grp?CTuL\RB@dB/q^LJj2RPP'b\>.*6`&TUookUA)Hs-?1Rh
Z?buLb=#*n.h[(J_U8cO[6J,'<^$J>j`qEdo.W#=o.[O)cQdMG$_E-ILs/"1]\AHZRoTRgl;qXL4b%E2
g<@<:_Xkht;'3^Ia=29k10R=q*^nt4#`#=_'e=Uj6bi8!rda*U]]S6%AnY"q@Aee))s/Y[+tPf%$p0`Z
"%559e)K"fVfL4s`p?Z"o#M0c]^/VCl.E*C8b\[<)sWc\b.5kSREit1\Ug:aP+<5Gn`9Q/5i=H)ggBor
Z;]>@*HoS%$ThoJ?$8hMJt4Z=eqZ0_HO%Q(eZRKF?KD?T00<`d2Sr4/#&0uAo1PM,hm%8&lN;be8q)&\
L>s<Z7l@qV6i$:BF%OeIhO=3dkDk_>YBq*-$LGA^n&lZp=Xu=$6<5)U]MV%/D'pTIPk^]Rn_J]8f:9%2
mij<$e8Pk8eBeVBe9ZOiUDafY;&f%^A;`@o]D:;6#ti&R'p[m?)K)762'_T[2.,oD?h9$jXblQZm$.il
D^:\CSc=5F=(2I$Tfmu#/UaHgWkI<Mf^`kQceXoCn@VqkU(J@.NX4dQ[8M*=C5lph"P/'KWbt7Z[9'*#
&N/%,8pgm;(bt'0#WqcQXG=j%)nIW5NHJR0Z3kJt9).54"aN@FX`Mn(1hKF$p.`+HcJC'T[sTr\c]td&
?cCe^,s_FeTlKE]a*)Nkin%[l.a46X(CdoV+qDFqabtYA7CYj_]b4c?Po,*S3>T?PN2Na`EQ=gX`T`rI
#U8HZT++#OUW'f`f&4L!.hQ?s=(6tGkNaWX>'hR!ro6bpJ+!S/-Wt_rra(&+@CD"BB!_E4QKEBM>kid)
D\92bs*F\(a4Yq?`S9L1EM$.O?R:%5eaPD(^9A9Zj=KXHdMI(LJ"EimZbZ82qtmnfbMAAFrQr]jK-u"V
pQq`6EUneBTCMhtfQAt=A*+3S>q+T*8(uE*pOYP,Ar*os<E%_.mB2T.b^D1`43U_A_Q?Upl&A^Sq>-TI
.t1!KIC%qIZXqTilk$5R&@KlrE"<\GD?;$e7J4jjO^>`qnI9^f(VZ@:/?=)]4c<)6"i<uA-(S(q1cN&`
FNC&>M&Xj$9%g>'.%'%lX>=3JGZWX;)SrV0QetQfrUdUZIGo@NZ:,)VS]0:!but:,,A:9eZSUo]XVRpr
?P[\bY4qe+RdjDK"FENN'^(;MS:H'VrFu>G2_ugg?d4&ALjJ\i-E$&\&B8=W;(G,9?\/dN]qCP&$4<e'
2go\$R/cNoN3Hi'pgW8B*&>CoJ&@EeC][1?L>NarR2LJ.k)gNcB'#,Bd3*:fMW+B07qRM=@n,dgm/Y7e
.J8,2N&^o%AB)LY4(LfO1It3-i@cW9HuI(7^Ijo&O!kb0Do`>WUK3><1.+*Ub`GG]),cPOo9q>Vi.OR]
bQolXkd=H10@H'pDiXq!K(G*K-`MUuHt6hgbh^VOb!*QkOM,nBD[OHbaV4"Q;,Ec-!f-?5jBfPZGai%d
kHd[C$MHDhL8.Tr7]KgI%d1NuP.h81W^f!rJ+-LJD)CoISUUAt5u+o5)^C3W;XsQYXQTWG[g+q,>`+"+
%70<G1?.m04TC92r:S(n,Ms>d,d(8B`q,htoPs[9?M9B;2kLCP,OA4@[:[9:g?i8`^c^=q29=t-4Y@f*
EF.AUR`Tbs;bMiDF_&f6fL*)nNkW9[mX3-$O8/.ZcHi)&]bYb@o"2_k`-9/QpR(TrSC(]V2u&#426#>;
lB1=^*W6/YfQ'0X(oa2IFElYpS$tqblj&P>_-Cqh%Hs8hDiGC\%-_KPjRtMiE5]\=^k$d8:Mru+Q3]jW
L+[fKhqP!,T$`h(eW6t,a#u7p$)>=R>2,pjl"%8;q.W[#U#15%'_E/IIKL+1C$YbLpQ>V>k>C)8FaBCL
lW;%YAI300la/S&Ru[5$fnH/iQ"X)GDIK9;jnC^ir2j1_#5kIZ5-j!L["RhD,56o&ceX5DC.V[gM9L_Q
97FL)Tp6,Y"2&lEfP],AIbrOe>'X`r3+V6uX9oEHdCP?F@/UHT!k0)5E`rimL`]X)4.IcX+Pp>icdi?5
")^'M1>M#;:r82d^nATk%m&[(Z&Vu>7ZCc+6XT);T=Eg&-0l#R`e)sCC'f6srB?3N/U)aS_3X*)Nk!\9
+l0g0cmuTCgDfdZbkV-'p0QORM7^-bf8$hIcH80rA+r#<mRCIi<d&8eBg6RSf\4bGf")Mq&^,[7_n3kS
?D=geT/VK`-LA3>q&IfiG=RlHO1!`Sh<Oc(<)0ir<H/E#dCD6M's1^giE"%eBc;@0`^;u,[FU)n%VW5'
YtfU=_L=Z):]G978'W86an'(KU*-#:6#@u@CS'DCEkK;\iQk.ETNFId<d;=6mp=`iS9'`*;UX**V]e(P
YFp:70m(9e(K>XSX"Z-o:>jFTYjZQ/4O8N8m!D#OD"i_EehM4$:4?C&:rg77#qKat#TK3tb>;4Yc?QQ8
^o(P&^j&&Ihht*CrSW!4XY8OJAuJ">/9tQFVrD%A>O@&jI!E*5kV6A8rJ'G0kkS"ZGi/725&ehNe%4=_
.DH[7h(o+[7Z-@nnaWI80D)+#NV&23dk\u8T/_3I]8<L?ak&3$g(YUT#-fRR]G8Vd)L(`H4[/;oFPMe^
i8`%6FCq%&];q]6Af#W?Dq3PG2b2sU+3/_mjMe(4no8iMojB<2MgT3\'c)]$X#4j"[TE6`54lF4T[R9-
j_+PfbN--]lFi%`=6Zc[&pbf1hXpbd;qs7I0E:\PNT"5rf3n@-jfUIV"i^F0482`9mP)NSdI&k(M@g9!
+8K^rNE<bop/tb1J`JKW..cjN;Oj&rk+e6kRLfkE09]bVGM\heE0.eK`a_DtLn_Z$96]gk&gQLb%it.>
;$fAu"`O7sDQt@7chHCO6dA:+LOEQW^;$qDTXV]nGen8O-k5&T[QjK'T1tCaj0;Klr[.'jRR4M_/#Ujb
'cF=3e-c,-1jKS8,g>\p9kD\\n6c?8git@ij2jqXp^;iuddKE*?XU[]]GNZFbu#@nT'1<0N%f<H#s,-F
(9oY37Lf4B*)jJRToBnJh<nagBWU0UmQdPn0R-qCW&)lRH[^Hh]\.$1)!-\5(@g]ucN?ZGf\T^-@VJ3<
`j?C9W7k_/:^Yg1\9#\EX,>Pc90rLm58m;h_kY]#`G.!=U+$8R5<L+A^);&sb^V&N<D]n,/3XGmj>.,>
Oi[=Z7Z@nj[jKYs^fN%%1n[@hpW1nBWa"aK)RX%MG3UCZ*GX)C!L9\R0oLXKQ#8kER^gWVf$1\K[pr$Q
?*"=E?"eqL3bDHn<Ye_eUcMh\;IjR6l3/bAUPK3J2L\S?G5GVSl37"G\FfkdGMOT.ejiph503N/\0Dh]
oG7/YR=8l0>iWM@N_OY!m&S)YW_ngL;O&-c0@16[X,5D9lHRZ[omMiVIE)O&Up,0,?Op'XE,Y_F`7iPV
)`MV5(P1uTp<"/ma&@LlGI1<_`O(q:8r2aOk5F$5i]_&',8]2X1#XdFVMJ?D:K2V?N7V694p,u/[Ikq.
Z5\DK?Q_b&;`h*%1Wa##r;ciR`5lu>G7*?T::KW_(Q/2)q`,GjdRLh.]qof$q:3nj)[8G<"`O7O..5sB
0E60u_Ytk.hsLsBId)"Qg+,3@s,omirLt4rQ1&u0`<_8YT#E?u_R"K*ojn@WP?e6;cNZP,FGSq>SS(,a
3NY`5e58fU/F7X8AVjcJ:Tb`N7=U/LSonc+8FbP(LE[XL<3`\LM1R[7'k)!IY>H0G[WSI\NA:"gaL%;1
d/Yo,]$\Y;q=YX.^GHV6JmYh<fNg\nl:2*G9t(%Md(hRX9ra:`6uqWp*6_s(d,\K``76L]_4CJ8rJlg!
jBi`Y`p*5J))@a>F3l,9c\3!l=qgL(`Y-5mJJR+GT\JHZYn(9Sg#gIl0rFeD:as;0Fj@,9<:(FY(_;W0
r.\^/_AF@e5A5Ff(o:Nu)]a!QnVn%#jTq<%$YOk>_bt9o:r1H'bkLl>\88+oiY]Y<SIQ@o"$Yb=<.*W`
p\+WGkC:Bu.N>m,'GL?o',nm_f^N*_>r&G"Pl+ffMe`$H+l^l-2*)6I%X3l,eX`YsquONGBBNjd4'kWo
6;%.id9GIVYj0;[2pi8=N]?a5Y7Sr;0)WXR!C$N&m`:L\.N4[GYK,TO7220N;c.(CdD..c2nV&-H)4/c
&]a9?.q29G]U">kC$U8ANWJ4Wr"$fG$`]*j8_48egj5#g<khX>n#a5e:1#jfbK,,BmE>K`DWYWbb)o^m
9M#J9%q=FhP+A&d?_Us$U5SL9nek\]Wu":qlemWskR7$hhiVDs>[*Em($RXkoNBla8.qm68CMeOo;qK*
8W0J?:;8qp?PQA07m`r[n4$t+r3CXq>OS]b[eSD<iFqli2GO,Z$cgYn'dMfn8V_ODWPkutK':U?^7[GG
;kD[r[pQD%)aVedEEsd7m(r6o:ufDJ<TohW"YUM+1KtO$+.h/;pDBLT>4UboWZ.$u-P67UWO`@?,>?T`
aN(45=an2Eq?8Z;ZX)Y&e9-lL.h'a?LZWG8hA`%(^Ilh24V@p:qS"@1cYOi$LXNqE50#9a.CK3&B)!kq
Yn5VNTbmh#f!B4JO2UbsJ*iSP`?%VIDe[8YoR9skk`9('m$Ad-`>X>8>obs=4VA)3+i\3j99,8*%6F!Z
U_8#"rHZld#HLh2,u<]*?s,sWntu+BQ7!4\0sm0RcZ!N*b4p-]>mF`hdM)+`F8.X7R1@R*LIYqAnH?;X
XY!=V0A/D=M!^\>noV;F(LlN`j9ZBQjklS^%ifaf^]27C"SS&pT'0h+Qn$J+md9LSc0jtG/o:HfSsf+`
*;-ZS8<I91BP9Y@j3Fc>c3gL[[q9AEENe_EIM'W%nBWHPp&Zo6]kqS>^[ZKm*2q31Thuco1M!X@3b*_E
r=+heeLga+Z&opXam`''(#uD6YupkSRF/!=+JHNg?t_okjloXg[O"G"l8JF?]E+U"qrPHh-H7V3*QTo%
,Ks;VhKD/i.pYcRTe_Y0gK7P8N=@_e]%:\18(Ki;?e*2Vbq#RQ]$THMT>C\N0bhr9Vua0SnTaE$>glf.
c`"U2B<^'83/Z+/[?cVW+neN'`Ej'h=Lo^:M_[K2hYa.i3*/bdZar`IlG3-&\-d9i=>/l6N1bEsI^GjM
?,i<aU"PBsc<B#`cCdTq`J]]LN%W7<-fh)W1u9HVM()C%rUJ9bhd%ZGlOmo#im<:RBEeCPTbW9T'ha:9
L/oj.lQKr\OsRe2JWd/NOB5P?eInkfe/YTVF<Bg5YHT^@<jZTF.n_B2l<@FHckPA9W\p@fOh;Kr(6%.#
@VS?L]1OSWc`##;HbGJ@Zn[ghX_;CEi6m4YNo4>F7YoXT]:%0+`=JR_X]VUSeYX'=UX4n%q8H[X<",t(
NX;s[=:n4+%<llBI\,^enOE0uiL3U#A'/Gl]Cj<E":]Q,>Om3+Ve0Uq0&1$=%@WFX!'^?BSB9prYA7aP
+-g";nbA)%m*V)l=Konq)=l0la+t#]_m>S;Nk^JkhcTO"o:J!8K'rGRRC$aC\Gl05P_D73rmRnp9^%*i
r6G>bokF,7!?3eKrN/+g't\31Ufd.hSUitX;"nD+e(Ppq?:Ds#([Qr>U:LB="'/N;oS(ISK=d0LG<FWR
TkpV>FV0QsPPUBNZ>->%!<)"Z2NYCJ-MR-_o'VKf^'dmF<dS-meLP$$[l1)u*Z?C#R3!_0CiSCmb6gUA
Z.Y]c;,+(JZ*ZjBW6(_j>5,3/g0hR;?='H#-m-Y.oJb=DY+c7kZo1?j9Q/I][ZsD$1sUH+U8E)MHHfPP
'ig(2>"PF=&t%PQ^\l?5HOcPTN2@E=+7a/*e#S7Ab.Rb@?Z11tdlQDEG[@<lm;/_9`J0/46+&fiKt%[E
EWf.)a+Tkd]A)][-cG_X'aEYgSh];k1[o2b8m(8n?`HidhW_Y2[;+M];UEB`,,"FUS_Fu31B!(`=UcZ"
edQ!bV5bjLUGX'o't/L`-H_'KK+CrGIoOqo^g8V#^@?Pp3BpID-!CPLWhL!E5THTj[WuS@Wk'=[6nZZl
IeiO.:;7KBEF1>@G0d\%=,;>52bSMfGT*1R`:<c_rqb7@fD`[;r:s_"p0[CB^\s5.bXajGHW]/))?.s3
s0k5mrBL<05NcL%]Qm?*.S&:hBAc@_k<H5C2bL^eJ+YCe&+>">^A[Tjs-17rlb>AO'@Gt1DUU8WnK0b/
Ic(?+:@k+g^7Rn2FE;orDDm"?'5!S];&e&7Jb3m!5u$+jfX-:-QK.f+IeU[?KCcaBlFl]cI:Z8or+0GO
;&cs5I=-a[@'r(:9Q<G/TnXtJq%?`f>hR"4Xd41=Hu$[W]Fd@crs8tYeUoGWf(c-1EOLjF1AZ]IPhjiA
b\14uTVQq-U?k*R<"[Y6&_6#jLM,rsIV3$,]m&bh(s8%+#SCA^NES&7gu0A:$nnT;=A2&^kAGp(-kd3b
D-660p+:L:)rG3:Yg<&/7*_hJ'\oQYH<8-+4grnqR)feT^5E&GZ't]qf'IcsVgC]GZeO,I5n(,.4:-;^
[be7;\/,dkKoolI?b07bHYldskD%bu&S&6jF^OsD^O:tl?GAR*rJGjsAO<!\!,(i8?3A%$>D(d\+c#[O
miSVhCf[igkk!EB)kN['P6EU?jKN7cU=C/OSH)$>d9OfRLfTs&J^X'.egZ:I.=8?tak%id;\qTtH:h*i
-Prn57"4i^FpXsKXY/u140NC.o8sk'h2\5HZ0MdYW$m*UE6;^$jZ"G^M:(6.4ChGF86TI\0DM1A=X7GF
'ThF`<_/oclc#7ogUcYT2.^P9G@X8AL6P=HW'A(q/MY\B+WUq&>k<]ke(\g'59.EM&QEOtAt7f'/\S`\
1MjVP\:qnrS-s?qOBZ#",a*H*ORD-+@S]e?*AQ>AS\g18-#9b]"o?k%5C8N8knNet)@'9"UD5-k7@/U5
Fu))o>rnuR=5V1hD:1];B&ZtQN+Ykj7Rd#01,VPOZgc=27G[#+X]LeJh!o4/rW"gnR`qVbi/+]L?2Q[I
kI(I=jf<TEm6Kf\V+t_0\b7ELLJ*8j$s:6rE&kd:1,Qi2[i@!d6hgh6dO@;L&/p>r<\&F<1_)u=n6XBL
khWp_EG1;b?&;e#5i[)tiir8ce+L$CJ[]U;[m.*u\4=Zmmo[oVpHES!IBNY+0[K0*V@?8XfX!2]"qZ'1
RG5n0\AOB<-tYQp*PjW!VTd?p5%WlSm9bmVYB7oViNom_WHkk;P0V:t?A/sXc%3P0/M-6XUiKBT,?#<q
`RZOWqlKtnA:uH0E%q4X(S)aKRPI\WSa;]`(2rudrF!o^5tlM*FWeC+FZVA(n;Mo(,u,[MT6^&aZlAUK
k@m?naEiCcqe/KPD5;%c'F*;O%,p&83f*dWKa[/I(Q@5A0,%e?cjo+F8A*LDYX8a$2M(DY,]lnp0IQhW
;;[a\?Al<'_uhd?`f1Cdi:B_pLReGfS\)V;b=ojQmlISlJRueP*,FM[aLuh:"e?E4>>oBXf3!`KEh$,/
*&5X!Y]m?[%t2o9,<=UW;4eIP\F3sFO%KmH>iE"QKJ:u&>&Bl6hKm)BLYJr`]-u+&Pr\I5;"Wm[`807+
Z!X9;R'6g_OY9BXgY%XHr#je4BdePpbh\nq:[X/\We]_^JhJr*)4;mfV=2WUF]T(P9upu!$oR+L2gR%:
TL+JpfOg2Tm^R2ecic9O^2Mm0D>_<Pnr\llMk^GmQkTjNWN=Up),[TR9"j;ZMOa=8hbq5I1t(;GaTbhQ
MD7sE>lkO9!7&qA)>#!>*2qm5]`!DPVMTFEF@#W-WhV$6J-<(k2_3fMEi0Q,]1&(Ac[-TnWY+DDn4h?8
an+bQgA;U/m\<a21G+1.d*c`Pm6l&A>csjT_kAjoA)=d=OXmh7d!LSX(;<*A=Rq@sAqTE<2K-P.&gN,V
MYbKLG9AS<e2S,+Lq:kH<\K)X#MN&f]tjg*cOj]h?Jof,>q.Bj6iWq,F+[N`j'thmH;lO3V`-#8%<&Q)
n\C_]7LnbR,GLE^"as@`a%iJ,rpN)hCV<nakcP'!L]66M(n=GC#r78VV3/,`F7bBj]dJ&4F0OB1HW:`W
/qo'1H=B7_hl#c(H9ZBA]QLPNMsY`\`(q&ZE4g\5E3uBiT32'hpLAMU:BU>te>dgD\/V:&p11p*l?=5W
/)+Kfhm%#R#Nc=hY?PBn-2G)VY?PBn-2FNFgu2oo*,m+Bl\UjESCT,q&&E.p,%Br%H!:/I0L&3A[iCk-
_l7b/?5_W_B50@YRTd\p`-'E0*sM)61$$)CQ'GJL]ucTuXM'57rkg7mG`Pa7n%HJmq=-Y;EjaVl:kf;h
I`]2\3<pE\q:`Q@,JRC8ms23&O:K5%U3CYEpIOjL'mCbk4%6VnZPP$\8U;c@9O@(?5U(\9EU;QIGI718
aqsBofN3;.XXThH2tXpSdj[?+6&4<\r9!a<o9UD]U[VT+OE8]egH]Db)9hY8r<57SNn3c\4(U[pb4j++
3oD9ohlCR%C4)i^"X+th%K`6kXlj%MhRKoEn'h+RDqoU&2.T<[XdA\)F.TI3@Q2,KcWE=5Vc!DifiemV
B.J6KXcT@fb#S,'1Mn(W(noQYl"k3)B]FO0`!)P0nT;Sfe9k2On*lp'W+uL7bcJZZNf;[]at:H<pNs<F
Dr*<K5p^n7Yu8a($t\"eags>p-nRh)!`.<A/)X,tf4IGh5<VKI,aX&$.O(9/Z+j:pA*[33oCnEgC]EPq
$1jm"lD,%PA:Oc"reUtO]ITt\Je>4[X_h+tX=pX)i(H-[NYC5t1OcHB/KH'lo51;u\6@AU^4)LFlu"an
;#Jc*`ZCDKi96"gm(<;!2hUY4KDd<>Fq;Ei@6!pDEMC\b4@$[483ak/\lJHn<hj)03c<Vf5ln/^&eokl
\"J8\@_OoLh.tL7.6^c?EK7toI9*@f]sSAS_=s&V(f+GE<\Kt*&cQE*ru6-ZlfVWV)IjW7<>+uh@';@G
YM<W&W#=REG_/T%U<LToXWHhN7BbK)[ncAKXFS_j=1:Md;-'=%k8-895I=o"d#)[ipCDXc`V.`;rBi+/
QWEH+Xk5kF@Xi+2?E*[iO8=W4ln>Kn?Ws^aBZ9tE(&LWSp6ShH+'PqaZsoAgj*InYBZ9u!MZ'@dgP8gG
Mf_9cVY-L?<WkHeOdK=W]sM8Gd#.du(&LWSp(+:X?Ws_LZss'^MZ'@dqb&I5hm?kmd#.du(&LWSp-qX1
61ISup<$FfHQ5=b`Lb37BROU6rh1(CVp^IiV%qa?<p[S8`M$U\g^J,1_ZI0%$t_ej5'Y7[<+1V&*`s)r
;>t%D&0b.?\Q_GEZ1\IeQMIVhggHZiBXsE=>3\KTcuOQJ[5d['CAgNCoa`W5&kAId.S=BbnY,'a9?Rch
e)WQJk2/9m>,eo%,ITu$L,afqo(2d:).<t!c;tBW?1SjiS?bWJ1HOB=*r&P;c[TCeJ%[RnlhSO'/dbjf
HQUR`icM\JG#G65cL))i`80BqBpiI<43`6/7'G5$4NUbD9q&pYCKA0k-"kf<[kTQ)_eZXKloe4.rpaV7
=-[+,C*n6/?IOF<Blu_SfjFcUXmKl92ohp+?d0I3"[/#c5_"jVQs,KD[;#TdK+9n7[R$b6H<[&8XVf'E
kc5Fci0YZO>u8FjD^q\i_!b7Q17GKo?C,-PHd+W*n![bB=)6esI>'Qm=$2s0L,NmSp6mt!eedn_..sT_
[AXa8lcO^F#%SM<eP0J4+aIB7!BlSccMFshoAtmlZPq`U,^K!UGA:6FP@GQ=eBQmT*4p0l>$,.,E#$-r
cbK`<<r@D6do>sL-d!6RKE[pc2=fJq)VMRl^90?aR]k;Y?F]B/>,O/'iRLN:`i_cMjE6uK@Z='o^r?5J
Djp"*juK]]K7)EVSY_oX0`$.IaD8Oo2qPaA#ElUVlE%sea[%QTTO:Nn_uokuKU^Wr(Yms]:DZtWj-pD@
6gd?VT]NW,BQ/>dT.Y%qX`k1eNE[Hi4L2eL0,WGome5?oXs\UuKf!mYK':Rn:p3l"fg-$XQ$@6<hYXXM
Yo<?[d3D)*d,,K=_J#[o8#dC;q(?,AjN7n39oenBqDWnfj4Eg?=Abq8h7JI/((k[BB<+]"Z3C\]&)Zd8
4R*"-%3(:6)GO?n"WTjEM6GoV4S;8!(&^;,f+'lJXl@Mq"FZ_@&L!"c2C`:B8)k`b=*UGH!IgF@C1cJ=
BkZ)fe^R_nB.4>C^Y\U2;;AS20DtZk,l//GTV6d:7TA8a*BLo:1IN`dTeh9EYFZIt7X4iYDla13)/>]?
Or[F_A7rYfLGPo:asNscDG[DjG.W&gFE'j`7`/SRUjVY$&BK.((?gnI_+e:l1nQRLHD>XL%_mF@Z1`r9
^Ch+UYg/+FQ*!fj/AKKn.^q3TrbT>^13_HVC:Es#TliBQ4/aT_5<BT7BErO:/0)Br]m9O9,6f>S;"l)V
qX%T(Jpm8#.=ZQR<\6>@'D:kB/g8Xe?`cbtr!J)hL_f""?<#H#30d#h&E7IfM&c9,fD$%gg8t1WcM5h"
Y;j7l'rm>@NhN!UUa9<%nTk*.e#F;Vn_&6Y*hWBQ`ojI]C7o;N:lI'@@oub>W;_gppXejuYuX*_D9PA9
kSZ@'&CHOgpR&2]6$8`nCY%J,McOTKlilNFHAPW:ZIW9Y:5up`Qb<8n4d`*+q6,F+H+H4;o47l]%gDJ#
Dci91F3toBF^:+tUIp4cRM71od0$d2:)!aE+"iG8QUI16Q":2hZLpFYRH-b*:oAtjK*D\=%%LI/#JYrj
dsH.J1lYk]S]0k+[4hCr\s]=Mek.s>H^tZ"YGM3fQ`--s[oF-\%F";:iX@<Z18%!k-gU=o)GlJ^q?Ldm
;3@#@ZnbT&Qd@TOC=PpQ<DCp-^!\XA7A$>Fq6fo0hGJ?-KMNsfaP5]UZfbTs(#>ad^W1B7^1=fe8&+Dr
\OA<BY-3G/lHr%-:qY?m?1Nk7jmjo1jm^K7e_Vc<ZWsn1p3KBeSUb?"'U0bqB?"<,1ZNi"1ZMR$?fcWA
8c9+E/9<11Xc7*!r)+,*H=kqmU1[M^H8H*mr>cg$rmEIaRIRuLRIR?Mbh/\`i'lN>`T<NLWg/lCJ%^7?
lbsmNaE<[*Y(>rtfWfWgB*=DK00NO"B86.%ri3GW`?%UbCON="Cd'K-@-=Di?`(`=5?-Mg^0:6dUU&eU
-(aYn\ji.6c4IOc?<"M2D(ka;ib_SO@(1pd=N8qVCVK<."I`@6()Jk3IGe/)Fj;mgHZBKia%"H_Skf1P
FBW?$Dgc>n-7qV)d5(YNQg0mE9>(iC:B:0KVs_$,;"r`8\,>q.Lr0m^?)b2+cO^==oWii2AbXp%m^763
IXZgMq.^@7f&j=Kg1&d62C)cr@`a1&>Iru61YsDcf^1[9]/uEXoRj!2/Cn_FCN6O_ocW`Xq&f-Pme12Z
Q;IT(c@<3"&&%A4>u@a31_e:*B,);(IFMi#^#!55V"_o:J[O7-Vm5!O2jN!=[+h?^@(.LnT0guMrmtqe
j(ciEm>aA@]tIsiD<=<`K4;;[(qVs/V0n^a=f9<hjd!A@e;GO/("C"m&$>kukLntU[LB0$'2r[j`?7FB
8bq*#TX3B_^W(!pEUSOnG%OgAO]U#tMHK>?398jr#JD.K*[:s8f#q3SMq?sT=cr5$b.nKe[_cL!/\+-I
M7]M^W3nQJ[,Ul-MW7&SNXr6O?<V?][,R0#7]eXV>&h3^iqUlXj/!L29.Q#5XZ0WjEP/[<ps=oY6S%j@
Xk5kFiqUlXj$P>WQW>(ZXZ/MOico1Vn^<<T6S"1`<ZEcp3'O_/G\P.edYd'DXZ2U;25(bA@t_2%f0<4*
<&*E3&WIU8@g)Ue>sAm3eF\Zi%RrRj)aI298uF;9I;;=E,=g;&q9I:4o*=G7Z]5mKomTGIYm0<K/?OJ(
puoH/-;*i%J^=,3rZaW!97Ol_&p2G)A9)BSU8(m8EOXKJ:LM3$9foFMn(aaSL#.KB=PCH13LX/uV?0a@
+5)gDlE,pj3mZ'MSM@50VXij267X68^A85U)N]LdT*7_s^)-cTD0cuFNQs^QQJDbk/T&L1PHVL$11,p>
qReCO^<=>jL+%3[o"pi->J@-_XOo)qN[Uq@\$5&p4/-l$GV=GX`[Y#`%JK)$;gZodke-:47Mi1*HOBXC
o[U)A%j!BAQ>L!SYO^EQl-cu&g>F[\'Xk>V#d*.2#)X+Vreq-TTYFNtF1p2#F.`,XNuV'X2/cREcGmN]
_TKbH;7:O0-m8Y2iQ=)bnVa573jf%bHUIO%jm0E."MN9M^GHb!:#M?ul&VndF@cipME-o0>XVNW0A?C4
.@_`]8iCYLMuFk=K6!2<&7Sf7JLCs:Q@<01A09(>-?+'C>tM0iaF2O;HmpkFO@r@ZeP![T^&&N;DV+gn
O#Caeb8L%!&BF_UI/XfZJ%AF,(FJYJKH,1Or:07Uq*/;Y<PPR%/BE9t5knu"h,0j.5Ogtu5)"K6hu:=d
'HZ,8rP,@ROd)<7F,GOXY[.,$4'$gEo4e(B?dIbZT/b+bP0<87<WD(CO\&LmXJ,,kbD<jdp\Fd(Vajd,
k]_t!nrCuDYG2?22>W<W)nV_dI(_1_HnXCUYn?E_DpSc<ch-M-E6u[M(Os=Iis)Guq(VCNQo@FPo:J!g
M#Me]X2hs#mkdNlF^^m<aI]+*#M`F>r*ZjpY'Z\5VH$mQ3'pnlf.W0/Vt0&bpW[lHh5_+XB00ge<q)]a
E#_SBW5J%=)pP.Qi]XTgk!=I1/FXP'o'_$:;,#;cim-9C^<Z9B1=7o!N5[i+e`1>KT:-4J^\b?d;TquR
,-C.\>"PbpJ09]6FKHP_nulL,7?))3Ipg:.c[qcKqXoEU',ga"gp%P')YgJkn$q;)YR<N/ABZjT0Gs)8
3-Q8#6q)7%ET8GpTI/aG(;:*WKrqeDE1b%qi-R7+`@Rdf0-'Wm<R`Q,I)_BLLkA4mr$TdgmG\s<jP9$I
PD7:?=0E;*B8A:=(k9q<31<10<P)GHO4`okEeLm?B00g-IDhROo?1J3$_AcLM9h+o#6fMLk'%e$*nY,<
BP)Y3$cX8E?8C]r7pJGDT2RfSG0bD)YL^WmbkUlqSf-G28Xf2:?e();SJ_Yr1VE^CcOB8,i\TVaeAmMd
cC<.)8c1/Qgb)2]1N$2bcCA'dPC(;Bi>cK.Eh""#Q#3&],&-QbNu@U.^ZQCh?LakL)p0i#opE&B&cDt$
#9Orqf.lKUoP`mukH1lRb!]q6$8dJ7=j7(Q2u1[lK;HAto,MPSqp]#Wbr[ie.q#oHIP2Z:!;f&EXA;(\
MPpEUT6SiSLkNL[qb!S!PI0(71OK-hCNHSD(:QYnpdtdCm=5H0pSjjIrmacej?4]eiauXa805N6CtW`!
KiG:kKD]?/5el:FigFp#a&YuO:!t1*#[^.V)WElO35glk^\NX)Q2$]1_,6%6BGL!U]A3,U/rn+m;Qm%B
4'Efjko'Ru[/9XleN3BHFuV5:I`7rg,3s]-,C5Th/e6KHYPuELqPg9j'=89nImtebKoag#o:PgXogJ-G
T63n$5*&o`M-M\2;n*QN4TC8uq1%Thk9&QiIs7H9F(m5Jk>'PeK(rtdo-O6m)pUeFDJ2Bd6:]d-6b[QJ
pjh>bc^O9:\4"0Z5'<P@3r9Vnc1H>[da(*_n+BtuphJ"O?M92R.Ou8/N2KfXo/E1BFY.RMWM=&`_nP30
=FH7UqO"$"c*?G.qFRuY-Y\/%8^lAVgXS@`=oQjP\@g_bqoJ;-b`I-=<%7^Jq;Wndo:$[OWVI8g8*7Rg
J7!P@ei(j\JFYZ;P5*cOB7rO@c8.fRb;uT[-C;NPW;:'l2DF!G'>Hd2dT7<!@ZN2Eof$a\i>u?r43TVo
2s8>!Nq]60IiGM=EY,oX]Eca.#pmW`BC5S*f'@aX1VuI]d$_6D>3/*\hB9`m75i2WB=`sFnFr_0/Fo'e
-VG,e`eP.1frTO\JT8B4rM"q>DB/%m@L'$,=)W<3q)n23JDF&`fJBNhUc.69),\7F3>_C*B)FE$j@nUI
(.jHt6!9_fRV)cgn!W3_Hp;a+XPS$B?c3rUm@*'(WNSKPbs,[CL;?a+cN^4m`duD@M*7V=^C88%Er=-?
2(HmQONQ,I6t/V8G=XQ#Ndi)iF,od>igU1#cc,\sVeq99=.j&B[aSX&R'Y(Z>kB(q>1*.n/A?E$A4R='
B-ghse]bR11Z&Z!jOl!(C1f&CjI-9$o<,k:H_*-ER<-$MK+!I+"]tQ":#D.m3O4f`Nd-&h&3lAA<a*^[
bteB8=VaAi#]_\G![Z5TG[Bup*ND*:5fC89"8bM0nOl8c$t2&^kE"<;"/g]8IG"hu5:jjB\)m+m8OrU'
7D?6>=;$t9+eHR(]$<3K04NcQ(f0b9!8l9\effsY@]hVjF[;'AgWf_QAKU5?H67DnF9*KGY&c.iMbZWW
da2WaW9J+ji65i#cctHp,tQX5(7#;?,8[!F',f`s>a>%IUam8G17Wi=_M!$iBcl6)G@#$Y.62AhMJ&Go
+\M<7J9@U2U4/3!%CBB[&GqY2/Y9tND@\'hO,\;/U%d(Ea\J2XYF(e2mtb3U8U"HilPH(?R#iSqH4O=E
j&Z29Z0F3"g%=m_ZZ5+f6aOlbiV>Ica39`j>KEEW[&%FVLYj.0.>:msVC<$uYn_]&$,36"nD)qG+(Nj%
V[(N[L6\q)4!Ak_)MrJpTtMk\m!hRuCe)>_5qZf?Vt@75GtRjA39aPX%kUO7K$23aZf#(Q]?ajTr4MJG
[-NZ]Ps<9PGHg+pRskph2Qg(O4''%/9&$3Z%=*j@3<ZD>(]TSl-:`SdY:s"jqGO1#S[-2uVKh5pdm;)K
f*JbK24Ul$7Z63uhp3FN`tr<3H[q_ioH*Ph.n62Z?N#XZ[`a:$f+h>32>5`eL8B++'jd0=Uu/p:qh7sf
huoSiqj>,uoINeH'n,a@=YnB&E1'[JcZEaP5IUtf(%jioLYoAbgCGH./5OTA@`A$LK6LS'IF#s\i!DMd
d^[>^Jl_\d*GH0L8_T`d2d/I4=*umsN$.b3iOMR.7AgNsq26-X)PPJjB2:?nXeus)Ama3)[IV`:l\!'[
V8CjYI\'JNQei0F4hL#u3$5iGX4D5'B\n&Z2sEt"NRbNkbrcRRHIe_Ue?im0eBtFCpY\Z3H?W0=H.TZT
b0F(]H0\bqd(<N]KPS+=o\ikY*flpX%)'9bZnP:IKbifQbLJ(:M\E'YA%PLBL>L`h;1X*kkZqZ_h(;d"
mCYC=QErlj]A(Tj]-"trV5na#Y?^O6q7Pr\6gEL0'j_mc?$_ho%D>D.]t:+D?172IM5K,U`Mn'*V]JI^
#X\"J7_]tGTaTW6`6c,#)UA".H'ioE..lr`J*\2RatH%QF-?5JIWe?Pp(K3\ZBptla6Mcaj`Jb7UNt+V
iI&%FNkQ.V9CLl9VY%$b'm`qPH<3)5^lB/H$VQEAq+iQTCOJdJGL\M5>sn53]/XXW]kaPOf82EF8^Zt5
Q9V(9CCRtX)+cOfrR/<]q>1CeH^X]75;Gb&4*0IN;"=H2roZ[rs5AD'4'=02(%'7E.t)!"."WZ`EL7[M
=!D;L2gdclkh7!5=Y3h1G`<6mbShSIC2#`f[+(_6J21RPi1\]RqrCm_NDVLX]S$6GQEnQiCIDt#=%ZPk
@nU$OO\<"Bq;No];s-HJ3>&d\r$$WIGU_O>,?LJn&G+W>rP5#1^Ti[YBVMmTCF_I,Y+3D+Q2E%;U0qTi
.I##;ZidYS7m&r%rMo\eI*nbr_WhrNo3a5?/Fb^uQgN&i:t;$!4!<QA.D!<b<CHpNkC+UE:iTs3.-OB!
I6=>PPsO*+fIjq-(:"h*,Hd6L+#E-/Xu3XICZ/M"k`AiX[kf4/CX>-pADH&>NI:8=`SQ%IN`^/TZ4k+G
f:D)KAuk7_/Si:2mikuLdE6UW4@kUZK.V8i?SZfJpI=M8?^6#37uZ90^Ca+dNK5j/hHG]L#d)"R,Bh7Z
0m9$pq'1&uGXUoLd#-I-d5Ad5RqQe-r\>lLs+7\Je3NA$<(?4T1SWO(Ak]I6iB)+`r/T6s2qG>@JW(1M
`GUeEVV&$.*ttp6!\55^7ZNX@[sei^6tZlYHc0h#VNIo:;<oAu.^9M?h3Ol3EG''EfI]t=D@`<PXn2\j
bOEIF/@`!2CVJfoKq=-1[iA,\D@8T7`,:]#Vq<SA^/"#1)01VjqMC@g`GUAD]+mg%f!C4J'Vk:?i3*g+
j_QVo>Nd.Ydk&8@T&9-[[3ik@1(^sM)>&:!"*=k)[&e0>p,ZC1T@KMR@^gV5Np:e"CC^qceK3S)\`,eh
q2-4KB8$5MeZmNPX&ZF'4b=1hFt:-HG)MkP`GUAD]0/.*i,?VQ295B9(;>:CLYe;aNJ4^-ZKM`=:juhN
XCE?`+Ef7B.s>]qS2`fSXod<CMVSn$G&+k:&\BnWY1Tl;BJH7n1'bF#r=*l4*Nb%CfgNBs=6rsk!O5jC
N5\K59>;Ua,T"IGR'@?m)+nWBL(S,>bR0p3>Wg1Cre>#udGI3<l))+$DT1/M=Rs'PU,AjlkOC@i?RLTd
V]3>a6U4tN@L.G;_:OMe#Y2lIT4Z)@Ni3`.\hD/mL1858:7!'_2pT)^HG/BFHQA3TWNgI*'bg+rV'OC1
HUJPc9>o%Vi\-_)PdK"CG43*A;,NrG8SK9bZ*i&L1KSLdZ';YB9gM/1mCp>e6^Y-#o+*i2Es0]hb*'G=
c^1$(PdNP``_r*5m28PWSZ"i5+>T82@a$[R1KnBAOV8sL.4GT*q\IT?Hr+?)h>M!Km\0oabhQ37)qh'6
[p.fR'n9dijA`LpU%NZ,--m5\pjnC^I^P4-<R3e("b]@gS[4D6>-[+(<^B82"Q;GK3<'#O4NsrKfG?_s
R9eJVmIX)?\3lcCmP-.(G:;H3^OeA!-g/aZUuXh#j9Am-bM?jNHSGauE=7g,R_P6;LfXOBVJ3e8>a.ge
L_XK%H[A1e8h"ZcQZpsAot@!?'OA3)%=BoupE-Up7CAV]9[S=Th4i^VAW$2V=j'!Tp'5U@:Lk90MkP?q
emBe'F</&!Q-#aod;eL..ZZl`U@K[r.CtPrXic4JOY`AbhCVgK'_=dI]j''iPtON$R4117]j%jfX\@,f
oIeO^.6<OH/WrP6MT-\Fl!9>+/aJ;>HBmHYa3,7U:?@4bhQ??odu/r.;fj%nHh1$DMU#Gp&t_kFEV+-C
g5h7fd=(Y)\MPo07PDGZ(,_O97JG3AjM[62.LW(Hd=(Y)E;7NhNW)jGUcU$%<SGoE8X9!kRBP<JUjf9&
o$\"9[+8*hkHB&h!Nl<JePX'RDJbmIf=SF>#@JPdHFG@6E\^_n+)km*56$I,n+t9fTm*P\"^LNI/l!>Z
R&G50U1`DqEHmSP]!O4AGu26JDAB@ehQ>N'eJtr#n)![1ccO\=+5*mkSIK?PjKIq%:L*pVRC`F.^AXTr
?!DC"G-W)N5JE[>8)PE?o;=fm5?W\"J+nN?oDIGm^9\CL]MVVoQFJGgMd,3.%>4D[UHaakP/EE"^b>&&
MVJ-tbrbJiM>Z%KYienB]ZJR\-l2+dpkcB'rO.G:Obhg#.@)B@fWBVE>Z\SIa0#(1C.IWV4mLf17=TYp
5[cLsl6G[ZE_;*3jX+H&&A'4l47@YHI9U5s<mDgs[sE?SZPDXX\VnW!.!BACgp#JliOSZr1YOW@n%!B%
ft#Ma(.U/OQ\>A0Y5HDhiHBT5dEsIcMh)AOJXOEk?Qs[:pU8/d`E1fO48>/7jkVH`gEH_,UPW"nBj7Y]
!5;["n*<qfMcn3\=N#)ar/H-Rd3o+@\Km3*9:e^Uc=J:T,3MkPTUa3JINdm[Lu[e;n+J."BCi/]931s@
M-j-*A`@np>L=\V>UE'qE\<'.F6UI+3Slh<.Hf-oe!5!BSc=srkpNV0agiu#X`?%6<gbt7""-C%F(o93
*&6X)De_"uaioF=Nf]%K.?hsm+_K#rB@OJ&%D9/9)M6hqGH;(3%o3`"_NT'*kN]3N5+=hS8<S!G-2Yc0
hNEol`=-0IUiY:fF>2m@:FkT`;>+Weh`3]`R1bFJb=l&/^7]SYo(jX!Wb@JI\k&aC(L<8Hg%_X\.ROH(
0#G]?`%RBsl/sYqagq$WF^7"ocpl0g/8S81AL;mTk;#n5N.'p,S4h9]E\=t.dE\9*^8^(44",,%JK&Y_
].?I0b\r\"j]j.t!p!V_M(RtV!Gcgu0$CA:L3-&!jSn4F8ulKCBuO09f3?!dj7kXZmU#ZAi%3+-B")CZ
#aC^=3P:n\-1[e$`oWjQ7mc#/:)<f6Es.D!-E151bfuYUn>5\pLQFY\,orEH$0M'Fb`p_C6&k?+('?Od
kB_rY^VmdsR_LC5LVWWg*MtYrAaP:o;=L=KMrHeVNBC>&p_(oXpmjtTCZZ*BBglWAJl@rH3F=!2B&s^*
ZK%g.^@tG9?WE,"\udjeHZU(jZW`^.JH!Mmo'<u\`uSO[9]tUOdG^0Q)>2DHQQ.I3S$/@NFK!,!Ob,N?
R0D<CeT$M!E+I1<jbM`uDG`B<1&(;1RCl:Jqbt_:*Oh$d/KD8J_9`tgBBQ<q14]kq78;X,@4I[aD)%C>
G2J)oCPj/O&,`J(*1ltja7[H`73VIi;sjn9dkfdRJG`ZEs/@a3=ln.><cF'X-[FS>C$jtIe.ubo<@(oi
WP4`q>VBhso,Dtikp9gUB%CN@J"<[an4&dtkpQXbqipR&W<\PUgF;U4PjQs0b+ihM/[K9RX3<SH4`XPd
0F>hEhM"rr*c7b*U-..je^c47_`'b^Tp:r/NDCU4V+g6!HDWgY->=D;':oB0)S$6dZ(5aSUQoYp[JpO:
>q*=c5lr[i*8i?iUV5fN=fe%;GK`'ek.O9!bG/mtZ,HFNB&?A5Zh2t\4B8#1rR0oBk9b3\f7(-A/EU,P
j(Y\C)sNW3es'7!%LRVTb^),DAOdbn-1*t4@thm4%?Sa[ZG.d1p15s=;J"N.9!`7hA9i@B;M>Q_XDe7n
0rghAB3K5"X"K.>9@om-*Nloc?AiS#<\:kh`nJJiE3.Z+EP:_bqQN2"OD:^u1MH=7P9W-\*jk^UIDmjB
/S':^TCoKSm#ef(+2kGGdn4p6BJ.^VR:a.rSR>Ym_8Jq](On6a(@uqE[Y:!Eh1!8gPK;LE7a9/JV57cV
ZW992g[9iPR#?B6eDqT[Am`$*UVsXagD8s)jq35a-`_<A\%lYH<TS'NIMZ/8Z9p-_^9:P#kBP%9C6.,t
lu?'5*:(!No,RG]W809gEb^j]'j8IEd?tM+H/$d*aI03lU"R_<:Yp8imVAnT'3`o_qpMHJOUSTl7`?9]
b)fP/1fW?*f#i4WFQ[Kk6D_>37BfrY?W,bSUo%)Ip7t\fFeI*E0nPUu/ShDgDZ6p^:-<gVNOfrfW6n($
M`7fqcY0*Rs6*!np<&<%j0<\.I,LWJLG5A#`-VD))_(bVp[.E;MZ$:?]8p@!<co&SH#O8[GBs1BS%e<;
%):gTs-q>aialB:n)Zdl*>nbR`9a\Z9(q"$S&Yb]6sar?7D,;P(JC[*EI[i8me9"SfIJ7Q?hA\M0782/
Z#6bHf3C!.3dBkA4_j#kYgI.D/9o'grRP@HkUgR^i>2.,)[VtjU1nV'Lfb<F%Q^an^VSG3/%SbO4mJ>R
*dF"RhGb>s7NX&K?#u)F92;!%pE?$iUZcIs[3EW)*I#MYnYVFEs0jJ`[HThTlejn82hi4Nm8U:V*$+GM
E>bu/nMk4O5#L:ag:$dQY;fM1\:rf&Cp<UTWF%VDo,s'1?*0T7_Mo(T6`]%_s.Wq3lj4Z[&F6k%#J-E^
Fc67bi.`']^XQA2frP!,h*rlO1\-:3^,E]@"BDk?)L,DeIdaD\_`jUC<0<B/C"t;&gnS%ok6Ups\Z<XE
]8ZN%X3Y,kUm*j7WXn[B)nYigiPrJg#3goA?hBTWBag#:Z0!a\gqS*#!g,_%?EK4:ni1T,*&76p2oiC^
eMAZ^)`R.'c)?4V?K3JQ4k;j)&>0!@;5(9sc40Jc)S+?\;,e6C&MZ`AkuM9kj`"r&e93W]eZMqD@,]tr
,W/]KZ;:21fC>BI#0FUBQa&=0d4Bd#pr%1<>>Go6lcIKEr$S)A1k?HY)FM95IVG[NI+:G>=bql_d8%t8
dZ!k;VMVL*`Q[lJKCbL6mY&)f4;Eh0;?_4#;^mMI1Njd"N+f4H4`QFqDfYLihf82a#FsaK^#t$5L&K<Z
isX(tD.OF0o]SQ1@7"!iq`)@*E):0?[gWg\o?JA'*DNkf?/Ngap0/N>kD9`2fDBphGShn#qnnoT\aW.P
hpgF`jW+#!S"1ZONYnV_8=P[s,IErIf^kc8S"p+\JhMS`i[^C-hX7\f"7F_"`bjA9k[.H&Q!^Y"F[s0]
.W6iN$G?ebI4As!ej1PW:_+RqIaI=?X)@K%hB3;3o*q<Or&@YL2%*P_?T4_uKtj`YUKig@c_*Y>g^@5@
D5u<YNmHXJ-AN<YGRPscME[!/@3@B0d?N,o&(j)FEj0R>,d"DiRc]qOiGfgHH+<a0BA/1p8j!#tj1K9R
\2rfW0`pIE?s;&Mk58<9$q@8EFt[Q&>'A=gXm('j6,OUf+Q2+6>-bYPr^"9mN-it,WOoh=gH643%bp"L
*n!eRU5JBL,,JE)G@fKs=jF+(?!?>tV\8A0:]JlrFjV$a*:_AKU?KS:587m<?^E.G`I6h&S_dP(3'7d^
H6PFjNZ+["mHa=2[OCN$$MAi/CFn^kH6R-E(=9WPT<;&Be1*!69&ltCZ,lVRSk'uM,/cuLa4l%TLY^n[
T/\qF^XF;fOgXE9(<0&6fB%j.9I7]NY;tG2J7d0nRdls4_mh)"`SL/SG*[?j>0-1;;5a/Whs3]-AHbns
=B5D:N+E1jPknqpG.Xl,7s`#Z&;:i)]GM$eXMED5V+)0\Z3'o,h=,7\AHbmh3Ei=;/RrlU/$S!-d0cOY
Y,hV#=Fn8p!]Q[!.SBNB!lRSe5.l."YQF19ME'L)"ZVOH!aFMKB@QCU!kZ4L!,"m*9cdF4@c[0e!k[;i
@+X-?&;:iAVA8#BBa%>DpfIRBDi^i(5?;0QK.]cO'aRJ`YiSNQMY=]b!3$`7!-PVZ"\:N9GuX>u<e,9j
CY)a/*_m4rYhO!uG.XjVC,d:c5TEfISkf*#5-;WgD_"(Z=1.dOYH[bG>60aaX]g3mhH]TV!dia(@.2hW
gjG'E[]+>FOZO_Fq9,b$k(<pim(tgrRascRImUbJfK%JLnK38NhLj9\?GA&hMV_Rek>V+:H6a"Q9g&ls
GV903]f+/_B@KP>037[GoS@.APGFsTNNHKgmIeZ*'c5c^q&Oi_'O<AfgkhO*I9BU>9p0_(lA',p(*si*
Wl!Pd4t44q=fK`n?ku4sD$5E=k[o[`'lWY83QXE6b<9X8%%a#F`n8a?AbCjj$R^k*rJVWXXBEV'V3kCJ
]AmLbmI0VW3g]q89t+C>E<bi3T)j9lWL=jtX<HhkGiOV2>Q("A3#s$,ETP[b90@.$Q0L@#^/a-H>d89E
5\`gTX8bF)lb$E_lhgMFUieXI32K<_`U"l(/jCkS'L`bW.nZ\MpPdAhoA444eOu&TD@'^3TLI@;knoCL
X\IYJ5'2lN,:_6B#>P`8&&##]L.Ka6n>!qjT@i6+BAmp,%RM.<=E3Akp)c$KaG-quo5S<C13:t<T&DQ2
OuFW)L:31.>HTAqcI*cO.tVQ-a4((!df3ush;U0XJ%^4C@E"B%1ucT&'C/6O=C4hrBcc-Vjmio2IsD8Z
?Y`,2Q"M)iW._F\-</D3&NmP$d;[jbP%Rnh/?Co#!a>4WTqB5lkNm+j,Hi1XlBB8/I`:5]1ui>Qnt?^.
33^/2LcD\""EEK=s$5b1VMZT+,EAiY+c^Ap2SS&_7iXPPEn`=/+KShnP`%iaK-[s-bt7HiG#JTbjI@F"
4hcNCH]7[`5@gS.M@+O=.*h8ORRMA&b*o`@`C_QA/M46THG#rA5NXW>C<Tml2<JRek$ba*4>bZc=mI`E
"r<$(E9-b./EcbB9K9CII]pbE[2R56E_MR1`V3Y&jnEN6n2S!2hlAKZk4%G\EU5(4^.6P85+L5?:XZ0$
/*<nb=bn.AoCl$@%ocSTS[fn-B5Va8T<\4-nbbW14O?<sDguDdfB(J!OM`*s`)AaZeNK!1?G[4fD"ZVk
lDlF>)tF2Z[u%O?ot1'K?+ZM38CWn\("dSGq9+LD6,'gB8!75579>NgmJpmLS'FOZj5Vbr8%:*cK=)'U
M]*t-g;21?MHgKQ2eRs0Cp4.^4c*D9^"<.S1IC#NPph,2b2L$43J./Yf:M?V_KdC"2rXoZ@I./ZXsm+?
TVBL4Nq%-sI(h#gp?J@Xn;?K?^tcu4?7@.rW)uk:WHs3[FdYb^H\%cRQ882D4i9<L^/acPj_-5n'7T2V
n`RSlb<3Mm5SNN9MaFH#n1hh0gY)%?7J-lCc0>ae>/BWr8U*1mWTjrTY?051F_&gaW%!j^8;hn"X(T*2
QZ^"!lW#XuX.1@,LhK6ML26duLRL5U4CLY5FM\<+].He:4*&%KAR*GBk'fr7p@(#mpS\=7L_U2ad+RTB
<jsOo!H;dII\k%*r]m+.MB;/*:tH#K^n,-$Pn$KNo\Knf1=QQmRDq@@G,2:O-*p.`<AD@pm`0O$A5Aff
njp)u4:sk81'cN/KXm%2<<kmt+1.;+le#gu1P0V4bjKH/3B><2];fAN]LKWSiMc_;.*pp5X\6uPdDatK
@g=*]\%%'O<b-^h[df"_ERG.<5+QqUEO'^"%EU'r"MVn/4nMQ+]g!ckZPV73:OMUX=5ppb7@/\`;k2Dp
q<pX>Drrn]c?pD]F>;YSd\CFgB<IEpWHl\Gn"hG?T-#m2:^5:B2&ohBESG_Y,`J'6.tX9N7<(]91m+o%
G*=6B3%jbPFs@GD/J<70-k2-9;s$^5Ifp+)7r(Y=]&^TFGuJC1MA&J/(mPgu0)`XMr5,^7ShPa!9On5%
8Ns'4!8=)*n%5cf,`b)t(Z"LVid%a/8`H/"7N6?m"MUtlOQ;DRK*NbLme5\a\67^!!G3_=C`MdAG3$V"
RJZS]aJlL[@i1QORCpuZcZ8g!Qd$E4f%CpR1W/P:-n7!*8[!rbP!Y-kX7U@34t'1+&.b?sX43YlBDA.W
QriN"c<T8s/1G]"?lrj,1#o&pG`K^@kD2.8KC`f<nT)C\:O]krAl!g)^'@Fj(.q=C#FuG6BD)usBD1rR
(>L-QYf$k#n/U-=A,!.rYuXI\I/E6Lbe3Yu9K.^%DV4G%XblXBd[E>E>JZrbXQ9JbWRGfdaIeaKe7lF?
FQeCeQ_JMO[,tib&bo=hkM<)eGu@5*UFI'jIM[,._CjC1SZj_&h6mQdZXfC,A><CBbKAB#RH,eeN=_+%
C0!UG,6OjmoCO(9/S3uT:[F>A0AY2<^"7Oc*9R'5RsQnsrPEi9B?QGLl+KmYKTFJ_\KLJol(Xkm0'U%m
;<-ue5<i+qB?@mc3d@rN6/oLOobWtN,`6Y+eO=KH#=jl08F_/:b!SMWU<-at':Y4(=(NHA;*J[4BWqF8
<-8^B,L>OHq-Q7V1DbDh=:GB3=5bU7/m[RWT<aT\L9]&A:pKtX&=)1K[;ubA-c`YT_4PRLD<3`gk*@J;
'Ya7i>+i,8dcEK]35'Ld,WOPE&q!fOHOD8C8##t8nS4Wh\d55";O5pcds(Llef00./=mH,B/q.t"k#g9
2s$%Dq%[hj4QEC/n9b_knX3YQ*90(XMDL8_L2fC4=rX:R-VGDnWNfK%Xg@j-enT)TVq,UW\)IjfogZ_/
1[2S:a'3Te>M%lQk)n)3&F$m2ek(?!-OV;o/b@?D=!(0/p9-=#S,E6FS2%[X6=<uJPs00$Xb(3GKN7N3
TZ\hCI%RLW.^rEn=h[77Y?l3PgMI[+HWu3SDQC#<D&%"RmoTbQYR5cH<e51`<PUW9@his[:f[V[%E1-(
^9*"M/=k@r4eC\shH"']gF=>h8lgKkX9.pN/@s"SXRrq.a=WQT5d$T$#+7o8UQA+_XmNP(I[Bb>'qC"e
(!%Q*$9/"_:e'9a.M1&Yn4U+qX1bWe;`q.uWjc!d#f`,7fWNe!/QnN%nZiMC)O"/D&(<#0r3crVs-Y)k
.B,@`]+4fhI!NnNrW1(2f,G+`_Wm,7F^]unr+eLrM2UZ9At57$qREk@&?NZFWG,M/AP:pZI?4Pa-%W)0
:u5]n^DGEJk+tWZf#bZ!)dZ$uCrP@P?>QsW3pF+b6Q1,A>NcF\O<.,*\A)3kBG0Z<+&.>-;eZloFc[rd
roqfU_U9]U3leu0$r*JbjOb9pMOeb-4fhLMQ&;>.ZJQlIRUg2F2OWaXDb[MPBC!bu)Fou&KdIt?-:*ZK
1s)&=V0p!/X21m#UFWi=*L[+r1et+&1Tc-U+!<BHpS=c[`$*JQ0E,B,7H"NF8VCaC\G+>qg^+Kc@_Gs=
8O.:[$Ig=]RNs9VVMVspN[EHVG^uS`3gt^a9MMf-Bhjd"&cJVdCjC3J%GB6(KtDrdIMr$ckbTbc/89J!
apu[`UNXmh:@_*+3mUkJi@CqT1,oZ?cnafjU-d%2gd!k1it\a_B##cX>"$.*`$6b=%.XnhHI>9V6AqS8
WmDs)7XN_Jijs8Wn42hg?`c9e`qkL5'sD9<Dpu2<Z.ls=<+ri)n"3LOjd_B"eWY&<P'FulanA7&:H?&_
&+A91Xj@]Y%STLmm[[6=QbhO5e`+CYGoT3E;S:H6h<qPk$Wgqu@RoYT4]Z>p2VVhlMTWX2#!SXE2iO7u
kHLr`$/[,%9A-1D[1:*GNPJ5.X:[3Da+prYQohJ%b]++n;F0/9^T*3:er3ZH.=@=lK.sM,<)O;31A]=p
-h:F8VTB0$RFt3V`F9&KZo*O2R&;[](G!63ENK,G``[se`3JjcajGB&NB)-Q?lmfCLW##:lB_\-H[Ka4
Q4La!;7D5.ViBjWQ)#FLSh<iTk.)JXY(FFA,#=53(^dR3a<:^%>&O=J`=lft+gN4q=>qSu?+g4]8)_t=
WW)"E&&g?qPW48rN%giC?n4aj?]l(\11/,#cWARsn1^U&6M276oKOgo/A)p)>YB24g'nGYcE-?<8A!!D
3K%OW:fD#s";g>B9oY532Dmg>*Vq5t7SohB%qYj-LPBjCqa"PfSBnpV^hVrk2bqL\=ZSkJ`h%2Go6`Ve
Y1^.?atdagPNk<."E0DM%E*DTn/uFF?`ad9*@dtFXoipAQ1(8N0ji&.Q%",)hbR86M%P)oRf"3]nY+>/
T\-t-G2E"-?MZa."7USGh9sMprY)KAb.p("H6]ZOU$?RgFj1;,i<M6Sh_bQm_/&,giEX;#;qnc+CU3QQ
q0*Qd"VZ8JQ8+\a$!Ak?2bkkiqfgPDbQLaLpWS:WpKAs8YeDS""@*S"JR@':'r((;YBP!YCM^]Lg%hAe
dYtnZ<SRtSs27[EMn;0F31b&<`::P?T4#ZC!I(%C:^?2M[U.cb.Pk6F2ZOnk5"C&iEMr9aD2-YiE/&O4
`b+F^;!NFeI/ad+0#E+dS=dUe`o%b!s,!M3N=k!32e@Y:1O]EnQ;N9F:jNXfm-o0GX'ps_T,OJ@Tmn$,
1p3AgU9qU6F*a/;\8W2[nJCKkl2EpuQ1rW$%]"FYJM15IP7)M+4JJEqcu'/T1O@b`W5$OAfo0"ui6Go9
TYk4IUSrji];"4mJ,SdIhlDS0<\h@tfdufnD)P%8B/>iW^"BD!daS0tojD8/d7Od_FMU_LZd)HqNE(>/
_YR!ZdBt#DkZt\Z<`UrlHJNbW[sIG28j-B<kKC9DqC>i)HE20ZoC&>>Tp1F,`Um)JCah[=MHJAXW<N^)
?B+!1>G9=qee_2JH,J>+g"gZOg?l&BH6^FF2\9o$-%,J=BT2dMLRcu"T&mZOMOGp,+8#PHGDQ)DhHG&s
GuiJ$M;9b`RMc(.0@mhA]m0o@e-F$Yr(O#,<=OQ@m@pUH=^UD&T6MXj)Oq[nh5la5rA%gg)InR^X6&QR
occiYMmoNQdAe.)K2l9opSulPfnZu_b56,a9u\Of%p#FY\eE$I5)bc`R5;FV'Uh<fWD\[1obm"'pcE!N
!9+2g;ftmEV\rOLgW%<Mpk;*nC":Bbl.l/NeZNP1h&u,`'DKA]KGm5O<Nhs'CW$gU-tB@%[UYB]PURBQ
/r9qg]omG;qhZ>bB&.S2a3%F\WF0;;dOg\IhA9#-+m%P\72$bA-^_sMM1A"M<FB15;+p%qX?DB2s2;MI
(\k57cOI%X96W5+*Y;,NCo)N?036LEq@W[.]V-Sj'"D9F;UjqS[XN2_\R2H4@lI[rCIbFm8P3!;*E4(X
I[tZTm!ar//!%RU#Q4j>p4Yg4muR:;m^]U'qh\K<GqUH`,J>21kiQ/Z][?IOI"UmaKXH$Po:J#0V:pkA
kYK\93[&E9]-EN<VE7>=oqlOZ-3pYgO)kF!Rd8UN]."984Rq:Io9I&44\cs$X7?2c]&1GmBO,n-e/!P5
pU[M<4W*D&4#9caFanP<M@jb7V"Y'6Q?IlWbBkKAc@!K)'JHJQgfDuJr"A`C9j3FdO`kD^YZ`JMVgM\f
M.-UnC,ORCW`j!4]g9V3)=4GKdpm6(IspEnK3X:7mFgu%AaIcjA]MYhgT*j3UVH1R)Vm;d/)D4ekctPX
91P^aU+bgAl-A%6rKGmqXM7PP_O\5[KQ0rc6!3AfJ(k3@"S[YB8i]"N=A>Biq:e/7aI[7Pf#F+BXI985
;hZ-lW-0ZSatuq%#^-@0(f(GIR]u+O0"]dUb;Sp!g<OI*%(;:rf2_,`a$cKKd<]86H@OW6*nmIhEn:fS
:-lCDn>qUsG"TdBQd9&l,j5"%@CC6`iB9,*NA"uJ?A8\:WlNKY]$Cmb[;=;Vo?]C/rL.nrd*VC-la@p?
$(jmd9m3H5_YEQ\*H^u?nB8?9M[]P!fk@6,IOk!:bY?8h@kTdCN*FAp[@Ju?\)ZZWZHdjWBstJ=^LY5U
\-;S'1tq=^YKiD4'^E%1JjP+?\q/XDj*7p')9%4%`Zdk*%%B6E(b`A@@a6HMjRZ"`/L\uVOpNf(A^Uhd
[qPO+/m?+R^YG/,mW^W>Rm1Cb0;K0]&#\2:e8DpC3;;@3SMn1t,Ok^/D;dT@>#ot=mE`T2=b4?fP%F?@
)gHA4/TP1,iZ?:p.3AZe72=i9>8O>3:Ed%gg&Bi49iF3R!Gt3:\SlW$mCfULNAmiN2r6o/3jEu1Bf2B7
;8X>k@MO7N>']l^[T)<sH*X$$0@mlrFFf/Xa#]q0<nZXdfBq7l]^=BJ+#dA]dElWR'1GoDi/LhCr,OE9
C$`O)eXp2[eKIh?%T_aEJN1*,=tA_%Z=8-hpIqLDJV:EakE+fU`fo9V(S4*W-Y/%#?@%&,2PLFB>3e]n
B0@\Ar#&k$&PV#/5$&@rA$j^u%GkSm#-p1rgXi&=NBfRbf)&5\R@M%.b3cuFc\[tCo/hDA'/!F\miV%@
Co7qG!&6='$T8Q/L,G=,k0<+&HSD'\3sDH@55_PJ1C&,s<dF0ElCNp?:`\<#Xp-r@T+G@5%j]8npj4)h
pNk8P"Ks0n13C%o;2-bCDr$XPfUs%1.I6l*]NNG[.(BVbL8Xhk:L:3=Prs?&P'9q6V\\`.XCFss,UuQ6
C@7d3J1&S[&j2"%0&F1lMI.ub7IOO[%_`:A:lg-ebD#HQ52'B98"F]tU1hNd='$P(D<bM.:u?(n5.7%d
V8)QD9:G.ZQb$MReQ;$5U@/WGf8D'I'=Q'BPbF?Up!R@QI44,5]qOOG>G5L%$5C-$Htc=BhFG9rS/Jcc
<SXT:I)TNDI6D>dC#Q2[oh22Bhn(9IPHC@UQBD$U1s^R<pAG*NS*7(_<16D9^$,?fplLc(^E0c;M\fsH
,+k`9g@jXrQGfcfhM3VE7W>_@<O\'l_@$%_D*(U,<GId6o'R3QWmsK-IXNRAQS%n@[-IDXeb89*qrhsF
NM^CnmJbs1LJI(djO0JF34!f,(T]\kHMf-E2!d-4;CBN>gt&Y$M:Sh)SI[5LMj<)Eagr?-.D3&\X%^!c
:5a.fN>8M])K%G22>5TV\m?-MVYrWWeGk8Oh9taVYq#79g3H'!&(rD:Vj3ec<NS5-d.YgA\Ur8*NRfi3
/m=)]:^;$P@]p2EC"N2@^3mYLr-!Ab^6:^%>"4[ZLSbZ!Kmi"G[]ufqaU;J<KFb`!gI%V"[DVC*cT_%L
H`2#rm%LK,hm)rLHGn/@i947d,bNDm$HjL%<8i8r]6>TK-rtFm/@fsN_YCthR`7QUkVGEdLZ^n,dJs)R
52=_VcRZjnT/:*7Gi:Wc4=JK*W*:>+Q)>+<S6$c=@g-Q5F^B@_9s$RL=!!UP/!"Nrc7?7Yn[q1mWLm7F
F^EJEW`;2AYC4%=bg.dMNq:EB_q8`f.%aS4d>cXZdWqZ4bDG[#)D]>-SDSEp&^kqn)!;Y5;S,Q62tS#$
K0)t-?W,b7lb#RMS[\<7?D4LQcRYe7cR[udS6'%p6X/i#SQCK2>FFNa64:/m=%I>&3_47md&k",/'n"=
qV@qtn]UJaYD=t+Koh'+/)6KCH0*TVn#25;eg4kNd(7_"?C,t`Y=l+cBjL#gPe_&%H2_\fQeKRI;U^K'
\^`^B95Wj7ajG7Cq+Fr"oj4!=H>@*krY,1?"M;(m-2*;`;EO>iWSP3>[@>bod`-R<Qcu_JBI0Z3jE+,^
&ncm`P#\PYlVa=hIjW1fZh#kgf<b&KpZC.T`h2aMSiQ"!^(ro`m*G]2%.XAo4/.f9^0Z:W+9+jrk=6dQ
EqeFjZTiHiIF&38_jtt+AZ.Jq/3Ghf-<nbMD#)A<D>;iP771(22W<GK4'%=VUWK\n!m>?U=eF>M%GAj^
W&$M*@+\$tmt_c(grjV?cQ^9K?n>kHZoScd,OM201&jh-SIff4kcl?"m@FQ!ATrG?U>NGQSVOZfFs)i/
^<1BA45PW0,PS\Wk[*CHeneU?8>*]QIGO0Q&aA+MldW6mk]2r4'60_lHYIt!l_[_`D9VlJI-;0odg+S)
pnkrBc,mT0fB3M8UC&H1d91QJl*!/%Zj_9Hk'M%!]:Q4U9G_:r4FUpS"kq*lk?.Fagd!mMXhT(VB5?9X
3Qc,98+="F"!W4jh&AAcNL&r2(Y+mee%\g>?2+ot?p&C@?65"--gJpC+Pcn.O:>"mpM].dP;G\Gm_`am
n_(FcmJ"WAmqRPe=3f=SHSd[S\&F36/6(>BPCE>$=@Yp-V6s/V9?Fd)K/^6OHS=0PgPVBIX!14-'7"n8
='Cc36d-C`kgq'O"'3\%WiR53dA0,cncN2$aEs=mPJ<<I,M`X#Kpt'A;p"]NClpL1nbaaUcZm4\KCM@:
Y36b0)^<W$mB0.fT=)3p7.0[D")8MndV>[\o5>k+N,3C@1PZNYQ^@\%T2F&aH%'J<d66&Lb=u!&'3\@\
:QL=@B0Bi=\,Rs_/%"EZkVbS^4>i\Uo&b7@/+,PAT2_ar?o@J%6kObQ6jlY*eXa0BHOD[XFF,$M*%]:(
GKj[%_mkallZDq$TG+)FFdH2bNIr.A?>%"2';'rGGHF8?0.g!pG:B^b8\$pU6*)Dhpl*`+e;Vtq)tW4f
KWR(3LoXM`Ecnq,Xb=-iNoNSJCj41'IVZ3tbS_)!.A]odG=3>O,t^ti4q>6f)qX>_\NBR=aLZEA1uodn
"+,Am%Cr)LZ53WA!2oS@W]Y1,#=n(!O8*iO$N3jaa]=C6=&3.X8*UuhI^LZP])gT=72Z`gh<J/mpPfd&
_kirEeZKQB<jbQO%,.HG9hF7#=9g(M=/'Bf@uo2HdNCdS;Of-mF[OGajoJr`j+E6UHC,;WqnB"Tiu\",
f_B2r4o-W?kIDt`n'gI^QJXd'QZV+a&]=LB`G#YX%<pY2eaoD8>+KZ$eEZ=%Vfi".?/t!#FkVjodd!Qd
cX9`_0srdciXIC36s&L^@moSj5,lgr3Be2L8)'cq_1f%4/F0:BgD_q<W_0<mg6SEY(jQXmVfXPmD"hE<
@8je\E;nM>C20<fji&g7O-ZcN62HVk&9cg4BQ(%#MTOYr@CKfXABr6pj_q`;>3e8ZX&%4=gZY>H:WrF$
FDL8pALi8E6IbSK<%IY?Uog.I^fmDm<j'SnPHBW<-$:3/eKHeME^,IdR@T,]/qRU!WHF@'']hr8pnApH
+,`K5XF6LdW$@UGXWs"8H58\JN:ANYKrWl&dF1tkX@Jau#sDOYY?''Cg0p?;C\TQZrE%+*@72@bNk(2]
%bGAms(U<>];bIZ[F]bA?1STZ[7XBc]f/.IbgTmrotJ,jqt@;O\(>r`?L*$?EE]0O0!4lPF$=N25XXj!
D=uNn_+Sg5V/?K@5"g[3K_:GA'Mi&`8j'(1c?QX[b`f.5&(25=;JW&YCHLb,H0\d9lEp+2(G0HcBtf$E
\p.un56E,pK)0^'+lBX/jj'P7m2QB->eYSe$0u:rG'iU:>C6<HHVV7UHVSsr;m23"a=0jmT=o]1>QB)R
gZeL`_73M0#fN]kluY3*[?fDt[=6:PV/.*11u$EQ9OGaf0')-%Fnn$PDQ@adr#L#B[YjCC\9;Fihj2mT
'?B_BhC%I[Sho_&Q4iZ66"\W7DO^Z@Y?"f\,9Cq'MVeZ!*[-<f-SC<91$ZR\RUaOT&pPY9hg4<>iq*.U
*Y7_I,6+?tcq]Lua2C`J>Mh7>N\=M:[GPpccgQ;AkYYjh)7-@A$uG`DH$TOO(/HLF:VYWF5,e;%6hF]i
65][g3mA63R7+.lp$N3.V]ajW6"fIfgoO;(Yt\"9Na!tS'^$[XcTZ(nNof4:X4Dpt<kl\8Y?#8p7us7,
a\Q83EAmDh)ej/<WZ-2DT*<"h\jqTNopr>igNmsCS->MY[Z]i[Flq%uYFp+,l*QH$lOd.1%b/J&ZO7Ac
\l.i@kH:s9WMbNln)e)$Q?<K+ltbR.Aj_F3^^%;&r:5\F\c1IFR:rb5Y;<3E6*+mRrW8q&0Q1fiZR5-3
Ja]J.V^aPqDbfca-=cq!o/H%`fA:<U[6Fat(3RVAPRkE[D)[&IpbD<@UL%$_W]M8oE=B5iB`%`Z@Hd#H
8Nerf]9krX?Y0-AHc3N*eYV^!*96(SN$$H*X)!,t*PJ!k2"h<&!"g[=oJ:km:BnR^G+]ZqnDQP6^V'&-
o=`bN2=.Bi\Z.`VT9Ids&H]oWJ\M:[$Z`nP3,?3br/bO(k,E)&F$FO5G'&1Yd.[G$Yb'lKKq.+go4I2H
j/$FJ8(p&^"?fW[>kB/1II!c[RE$1.H=C(3Qhh*.*SsPnQg<55g>V/f=q7o0j)>N+d%_W&q[<=\L@=5_
'n:.r_gPDHO@l0RU@4.:nL^57^#YG>j%H>8p@JDa(&MO_dGh=98coBr&HJR=)LknZmr&#\MP*Z+=VLWR
)H%9qB<7D&(HDo=SCGI,:J!^`MK>bC6kgUro4[VdN>1gj3frr(V5dP#cEB0TrO1>!4PkhLAs=0LD7o+6
oA$cL/RKC1Y_'oH'#fFDj7oHq*:F>C`)jpW(3/;<N;a"rgYrR!<p\r?9<D<Bmj=#In!Nddj$;#L:!I_r
78SWWn_QZh7D7&mqlV1O-MkG)mQB?,@/lC7Hul,4IV5f0*^A./i,Rte(]gjc>e?A+r4RrES!jF<eA/QS
@T)!,gs3mdg9YnR#qj!%ELnQ(`48D*UVH1RRRU#VNk[O#Tt`Db1:W3nM7u1gZdNZVY]97&3+3d*FrW"5
T)Z`ThVPcQruK`4&>*S]rW^BD)fKk+.4cr<gr"P3(\cjjg^9Tc2oe[4O5ZC.LYft=Ic(stM!ot"@dmOu
&eQCAlOi=0Z,LDHQPXX5I;nAUr)OfuI/?(:#N>+0Usta.Z!lbP*WfiTN)/alEs2ia4m]ppT<*jcJ*mG,
G&=u`e=8/.MsmL>SR7hDa,X3N-_?>6kaaPRs58$Yc<KZ]=U2;LW)QdS"ZIsR28!(+EoAr-\Wc0^kV$Eq
q(sRRktR.U)I+Y7LE,r9c5=Ud?Vqb.rU7&HFC0)M`ekfRo&7Ge:8UZ/LM3:\mLdlo2tmOm4jID8=HiXA
fnD=*n9;"=7tA?ijkNOE"PSMh)U%9fS#t$L(<K%W'],k>$IcFs$Eh(_.:-\;F2ZlfZ"0V.A*B9+/W<)m
C`oq:K5TnA5,ed#rs$5bX)%DBEkc2)n'^j!g^bE;lXSL!5CS[AHH-UTG_Fh9Xd8`G<iCe2nEdjX&[!,q
X+hTp*PU3MGcL3$c9?$cr\F+.CL5nOE3^9LhA#q:@L)hEPI=_E'fF7c,=ZIS^Je;]Nt;('H(nJDbSG]U
)3^o+@oZFAL:t:<5M9SLmdRP?=N!6_NmAVm!lYFrU"As1SFZ=/bB#VWg(Bar*d,uCAUf8?KYZJr-S*A[
]Ur<drR^WuMtV.JFQ>b_'J\C.Tii40?Zn=`F:HntMao.W65FFe;6-]AcV@".5t<6J:+L2Kn7/F4;>^3d
SOh4ViVCi%O^1@<+tBrX&BB%R2u(>)+rd]FAi?1i0?U%E%c3385XX3c@aqf2`6EpI-0m;"8^6(RLW*I`
2u^*6NgB,ck+0n$0gk4<OYks2r\1+'%W6p^EFJ[f407NQFqT^'2"X0cFkDUXJiYI&cVF?$$0$RVi_V`'
`BZm%ZN@0Boe"qV*1>.2q'M^sr6@(?)/U;_R0:Ae?[.dWpuM]#X790DN'-@')u@M(0(bime0,16*H=r?
L*@uf:W"j`G_nIj],PK;G=9dW65NU>(3e7?Q/CQjH*RhM5g%.mgeD."`81/RH?XBmXR$tYq`ai+9'Nag
UAr&,p#]<M@CjFI)sWN3Vr>XB$RcFtIEk$i0(o>3\T$a1&3\jo#MDuE[!-Hl.^M<Z\jcX7pt;pPLQlNH
h^'?##B-`//b'u@$$hGNZ"ilb+;+@%&1\o/D,3S!m^VT9ET<_X*m'&.:KKn*d+XQPpToYDM8,\Ho)ZGT
c/qN7(T0nnh<b]EBfk,1Fneqj@I)qL[mqRoUP4i&W5E-$E;Q]2rp4^OX]^ZFT=mY!U&RJ.kj>L;H@DHI
qbFm^l.rQOQ,`#i]sE0mF&MIiF_Vn*f/Oe8U;lGZB53rs&\5odh2Z9@^:44`NU9]/1_+P?GjF:oVK'2S
*]8uFeQh,W@u3%&,AJ3-C)gdYON+Q]A(!?XM^".5N%378aR'QXLSJjK@s^/d3$#Sh)Q0&MKuWADl`b)n
DYkJh*$Wuo`7-bP.kQ'\RQ*-!f=HX=C0e\Y-aa4;:*7t0BS?ql`^*(]coA`'^".tY2jq\MQ$o=FkuuuD
EOqt'1_/D2NPk4p`p(pLVK(6Ul4isL2%HY*HL%<.8phHoLNK60Z#cXT*(SL<`7-d&,*JaAqPG%%`p&sC
LM7_io=2fF;chC,I1T?;FKIm-3*i>"]!#FG1d.OP5[e@H,1efme2Y->4q"@)ZGrRX;<&d3OE?ANC6HR;
]tG/D9oWXqpnVo.'rZ,N0rT)tIY#KVS\;%8I2$#93+Mn!DoNFTq%)8'XqpGeY@2PAn<6!@S3[T<HprNs
AIS-B1Gm(AI!1YDiK"Z'r`mbS*aG[7_3Z<bebW]O7M!O,&k:=[HD.a@cQuD_/V?$B^]"fMrRr?udJs)R
5-PEc^=&IHX&#:$*ZJia?-J=D-lqtH];i:4JP>pEpN>!?I'd,&M#*#HE?YqQI[paj&/L@F[8;kZ)dgQh
fP,M0[ZSmKj6d&PNR+?6S[:h]gOm<o-8AFHLrV.r<#G]eCioIOTG&b5FoEnaS?stqJoCG&K5h80"p`l+
Z`eP=bf;G`2n#L<m\@ZkK=%DD-.W,q3h9Zu,qk_L64MPnkX556>6fq*kq7OUZk"n^KB\^;/&aKD(=1c'
X.ZkPM]R;VdJG.&PQq-4R&dE#4#^UY)t(L(W#;>b:6W(mno6^hfWZTfq!J\;Qeg.pfXf2<B]E5;[;_bo
`'c,W"NG$5`*S$PSb=U9"4UUo(7?eCBrDEs;2)HX4q)_M+b*limG8(jT66e`<jcD]=Vf!'k?rcc-'LIr
%4<_oG&#?N%'@2IHZ:WESZoC^4rCnDe"iqJqTu)A@2Hq)4mKM6Y,d[C7e7qn`Yh':;K".Tk4<WnL5=4^
@i#>@^H;\Vjeu8J<Jb^:LVN2cj2UhJc!nf/@9RKX[OL'?+HC<Q=s;dO3ZT7^LAlcsdS-<A7mo,sgUm0e
`-)50kUe:fhaE=`G>p_.]g-2QNr$!."fMD@RUR"\B9<hV3@;(fX&c,n5!3+GRuP[8(qd>g%Z&t&@mRFA
nfE&P:dWd^goQSIe=dUQUOm?%4LlhCDo`:]:A;lKQ,kl_:pAn/`Tc3G[FI6co?>_h3<!m3e0)ZEO,RK(
Z('U1);=,aO$V=G?Kcrdg.u#_G'gD%I".t776'"Fln?(;qYQ%P^*!&YeF=rB[_jg5rY?R,DoD'.@=S9c
)mJRM/oM-h$?=kHrT!pMgUZ]$+$9?X')m'eL&(Wof(4B,h4hZtDuXD),2*=bAPHmu7tNJYYgNZ/SHd^X
>&?4IP])RD.bN+>RbuGkK8AA1>Dr(6D7B6>2qfA#X4.![aB%rn07@seRYO&C#h7b2>GeUo@0]dq^Cb;9
r5E*86G8rU/Gf$Mk?(!j<I(Sq+l+qIFCa0OH@6g1&Lrj6SQkVWVjtGE2-TE:%JUi1A^jT(N-)3"S2%q=
JJ[dSEkJ#Hj#*=q;#VV7UtMlm6kSK]l-,qQ4D5&-Q$c<-lci1Mb?'FQ*&3X8@QVsg7$%"qXFc<NlV0N4
X4\]pQ^@0gHgg:$TS[t?$B;#t?SgJ#4']e+o@BN&i-?/b'2b0l,GW1\DSNk)KC+2NWF9Z;$KG&Sqe,OQ
[@7'7IlT^L4#jrC]5WTJ&TrB4o))oc]5h9hg>!qW\EVH=AepoNr3X^b<4%%+"IZ1<Z@cGi7eSU/l"AW`
k,T47;Rth?7;-!s'$rQ1oSF>-oE])UjeLcu_!qp#h32i5mk)Zc7(W`sb[B`HWIrH4<O_'=Wt8j[4OB-/
QMDimbZ!DjFrds[6k(Zb_Tf+dJVWq-j?=Ml7#B?XpBt8)+#?.:[KMUOZpQ."K$>c)$8s#F2=&,3C=$P^
2BQdGc[/5l[$hd;3LSLYr<$K/A4G;dUsJaCI+..Y7n>W:"MQLq!SriOp>7110D;-%[q;?Bo[[Q;VW]dL
ZeD4KAni0rVqWeSCNobaSX-NTl-Zhq\M-+UT:.]T<iaE9#+P0u3]4dgp^>g`8%LPPAmm&nWrMS8Pao79
i`>HbOm8AK:Ln/&a<s,(WMEeA4MO^QOA/k5LT#ic*77#;Df8)<^-d?P>h_(oS)9>3'U%<dJTLcVS(e3c
D)Sj<3,DaE0sk4pKXgJ#Hs!@Kcb#2YqlR2Ue,/SUZ*/K?h^@RXs73K#ShS@1G^j_Y]PcHp>j5Xs=nTJN
XS22]9=\F@o(UK5e;.A#GKBn.5EYmNe!@<EjlO$[e<a3IEr".pI-nk)Q"]\mW#\4dM.a/mBU+$%mTWeg
-gu7D(c$Hj8t!(oC5pdn=)0,FQqoB,dZ=$!O70UP>*s6n[Zslon%WYWAY(#_SYWT+X!R.4VbV%5IA@ka
qZuhm;o:7Qh*Jd<rUO;rXN=,9:2N$d?V.V'^Mn'k+HC<8<b46Jhni:jg%(kGAKS'UW"l9.h)<ZfZPp)&
:XK#;V*.']ppd@I`KA4.6-.PVdKllN/5,;B+n$St)_;4m3SnYN9VphTlZ7\)eN$m0:&Tj+TMFdPX9J0&
?iBB1Mg*oNAo$esSdOXll.iC'Ff+Eta8G>-hcd)OeCj]3Q(a\Zp"`/=Zeh@\la%q.5[DqD@b1[nC:/QM
,-)NPs2:23c6!JO0tH/4j):[DjJ8V,iE=R.#1ntqcBETcNr9D1"(9K@=WnlYgr`9J3LK^O]iKFX327un
CbrXe.'Poa[/Pm)k8WSrV0]ja])Q5T^Iuhqk<JM19(e_mlhug]LNN\[ZdrbOKam!'AX_UXm?&Q)W/[f_
Z6Ss-3oV%J.-E^,F#<c4>Zl8:c[XrISPCtZm*G,F.<sg=)eI*?iTXBca,me].TMG0CIc4GENdfCAfIWm
dW`m<PNRQSe>"BL6fWesj#qEHs+qC*H0#4Sq8hH8ls.Hk5B5t`h4V,+S!F@[\2#jEO)4e+IBM<t%M8:Q
\E4RON'1D?XQa)&*"2.]DJhU@;9\cpN*'3$lrfm"T'4U_/5a`B[fl8C-S[4#r$CdQl+MOPk>s%VG=O^q
gL7>$^Am,)U34NUK615LOcrDIb:*cnoU#2mb;8\:Ss)qpDk#U:qdfXt;WsO``gtGIU'86@_a)2mfRKET
:M!$X);BY2c#7X(W0X[O@mc9`25iH7fHon;`O0`H0E5V&)1h7Yg:d*Y"VJ2ai$J>M9;"]AB]U^@Je!Zn
(6o7!'.J`r^G#T+ro7_r'7O8k6X[Nm4*fD_dhD$%/nLjD=1_[pRD"5#3]gP+U(/K@Pa0'Jl/[3-H1VQ>
]'cko%Yf?qB]si?h'"T)X&UNF>ALFfUYO\^:#7ZfjHXQ6^@Fm:N^%kYL[Pa7;-+`8K/U8ehYQR+\,ISC
$%pPhOj3j=q.Hl(qm9nh`LXL*"'M%Bgl%QXi9gEI35F,31=Q&*=qRng%a\'9>I92c,pKS\),]^D0uaqg
c^(RdMDOHh,8W5=Y/kVfPUPAK'6,QD+C$p&O<Tlgc>]$Y]95/.foTss\WaUp3qmA"XB+[0CC?0(MZ5$<
i\#jQ>:EK3A^JJG[8&<TCWLM9$4*@7m]Q9L4#-2&:QkZ;PB]4tl%K#`(okH'Q0B^S2Pok]XSmT.Vi%4*
O%gu1jO]iD9B)jZ?`Af^3I#'$61k(&*qG@'>80Fk&(_9O@p@)"p0_'ofZH):4@oAW%TP`X[89s<W949+
VKV<C>'lj)nN]:L7oC8^?$.2ABZ1kddIT]IBdFbZf@p8E6,1R1I::M&qdVh(0gkQ?QPsGKp/8a^deV<[
YM\M%qHdo]9o6R3)h%1Nk<EtQV$:ZA5e_[K`sHiem>dc$cD[HKB,qkt*oBV=k^0_XBn<YR$<un's!=Ds
o\R^]eNa>@LI8/NEh.L:5lK6E5B-50>FV]Mp.JOmp"h<*B0e'!`Q-RRjS^BBAjt*#Okj+-%6Cj4Q!2lf
^?GG=;qs4H:]*8T(A\[;)[#g[HoEg\A9\t,Flo;Z:lJdq%s5P`QcQ+upBDAEHmnU0H\AdurRK%:Sks2o
<hY)'C[p7*4-k<&SD9l[hYeb_B_Y5\[&B:Pkb+Ku86dVO-Mi>RX\t?haC2,f\SL;f\390Q$U9!G#@U)o
[<d)sHpfTG-5eP?aaEq"qP^Dl=QKtU"G'UE+PI!n'aa?Jh1EZSDKEL&5taa/SqMMSFhKYK?14&0c[TEW
7NMt$o^(1!eW+n!k]Lk_;^ETH^SR@/0AFQ5m(?L63PR,.aUm/9T9UBZ)-C?;9n27"rPO&r^ELJQHpeR:
G8,Var('ZeT@fe9-R'Q0^:sQt&J!$^kRe<K;%aHNa@MK!\+:BeK?RrIgaF6&^s"6dE2^9Pc3OY!p]!=`
#`#G8D5ZX3QIjBR74'bEPc6A=p,DPU^W+;uHcS&^&It):(MT;qgDStcr-+*=5cpL@&IoOYP8W?;GoFr]
cQ"Gi6=BXn/+PZUEk#oR&ItT@,906c76&d^5@VVp_SNS&n>p-[rPUm+MtraaW[EZHMd]H#DClZcE$/aT
6!_],KDUD5@I'5=\s_MGGFgoANhM[-iuiN_>:jm7Iq`(4]se_g%k<O?\^;XrLi>U#g5+BHB"'^JSnE]Y
Z/Q'>a+mcLqJVsn/G4X8$5L+7GlHaur,c%`_\fMu_6f&*n#_f6Z`dEj=(%)Fh"$TjO"i/O1WTiWjuJ=8
,H-K>)<$*<+.!QLW"C$_e3*IfUJdJ!\H=]4HZm)+(ZcgZ^0H_<5\/)MKL8ZR;r!R7`_],a36%Mh8NijC
:?[t>B;&u)bh%p[89%(k\OSWdc/2DWR,*B,%dd0+FWtK_g'-1VQQa5>OGDce0B=6E3/EpG6ddSIl7tC*
):fY`cji(Y+rlR<c80;@#Edk+[8&kh^DK7H)8DX(_Mh=BLI#'I]S22#g*VV=_LYK=_L_Mt(u`:!G_?0G
_$GfC^4o<7N_M6&Rse8SdnOq8hXUpWdY/8CcYX]Drs[s#%ZY?)nm=/hTcNFAGd]nf<qP?Y<h&m=DP1(Y
)uMu0I^@fKKb4fj$R6W^1QQK_-6G]:1s;prk\VR8#`#:W':\MdaBVpD("@G8KL8\'$+!hmA;Ok+")>>Q
O)Sd2EY,@S'TqJUUj+\sI$IqDSVd-[UgO$@,=[U_:EN"e"1rJMfP:=?O>X%0CQ(m=Yo+D*Ye84;PW+?k
d"j(<#Us,L=kH1e2g*,h>\_p/\=t6?ID9p2kCQM\njF2,BkEOHpPPjZD^sVjj*aP&(frOC3J0%c;le%>
i[lhL5@[BKKhjEp>H;?6@apF3eGnY/5?^.Nohs]&qW2cS]hcf>ffQ,:?$/e0U:9dKiGD/hX?GF@bU3q@
0';X=-?_A)dQ-BDLf\Wp*OttEc9l:P]S1c]MH`IIZg.("Qd6k%kYeiac%s!0Y2$b&/Vc2u*kg0+XJ'9B
lnl>SXrpp3E7_$jUA]!l[ArAEB!Ot!a\U-742hlI]BuU9U\M;iRI.X/]XS%'2J_$Wq0.kVpY=OOW#$h'
!Fh?];tM#tK9Bd64l?oc$H=Wa4SoUsW3uh>]lN4P,KkDL4H>T4i$^]@:^^0m.q['a8f>Hlc0YO-bYu]s
fQ$JbLcBD(Cr&u/e+9o\GE&q-c5!&&ZqeAT@m1KQmh83Me`OH8nS8q"`Ug=n4h(gQN=N=aVfH[7Nd!H.
?acl%M^t!*66b;O'#TU)?h+]meGamkHZs.h#q;](Ebac(I:.79U=FHN(<`Em&`:4-^ULguX5jouOnY3P
0c$dNc?=6hH\2fp420!e7QjU&dJ$/.\?'r$o8`cBoGT@P/u<emmNl^'nCS>Xiq5Pk!as[)oLmico\PB_
e#Kj%k:3#GfO&")Am3^neb%&('gf7537%<K3HL7;JjX,k%=cK^o"@d'I[*m$WmXl\`P[T-`/3G`e)1<S
0'Q]squ\"On*\iZi\e5tG"/$2S^UlQWE#(CDkol*npJaVm!?'3-4h8tI,*0qe9:1M(.LF-#=@2L0%$Hk
XeP[tlrmh3SUY/"6:>pcAUWC^1O:h.da#ekd.$C5>[RqG=nBm0CGMqkiE\&#Xp0Q-8bXW!;#Tn7f1=C^
o?/"Yj:BC8`ZE*RD#CUhM@EHF):^UdJ#47[aEFCCW!=1HZ=S/EoN;]]B*=UF^Vtu!eCs[E>EsE-?K#2N
>q,^f_76PsOqpTVncISFG$"*\T+$+nH_I@ZUV"V\`:u<*]'F?ZFrCB7m7t?naK34KZgG<tTU%P4hNP^N
O*sgH9k>iNhdRtAC_*7ugEIKN#$t-5)gI<jr]S-pA3EW_B_UF6Nch2L-@Tbs-/We4qKd(FAht\ON+aWo
ct(p3V;0gs@k1'W=m<Ar4,hg'0ApVF%iO?Ard4R\ma;\`jgg_"TrrB!`)enX.3UEUB;G83Y'u.gLmTX'
&^!*Tg*?,LK<^c6^T9Nn3ts?jdI`3khnd2m(R$Jq0sPjJA"J]4S!pJ*UbMA<#GT1n=L-oOAGV2K/rT]4
B+`9knc$""/UVXPB*nI8ko%pGa9k>;:agCKcZX?5l^&]NAtr'8Z/pu`9>8_j(f\QNAZ7r0A>c)K@Ja2d
;1?5LZDbM-Q.G"paS")Yr7j>RW_S>.@8^u^j5\V,?QJ?$!r+>'bLgi!c.MT^*9O0>GMI(XWm8J?92,5D
;o8OPYpOhI5'c=s[??Bi2p]4HVqB^U>*$,\:c\.$^KShoJ'$>`^,KMbO*54BoQD:]rq]g9Z'EgBaYO]:
I[YH7:.'!.omn+"f^d6Nj=&+cA'9]H>CJ(!Hl9\UPt$mPQ"uAM!)L^Pf$XIt](p+8[D/6`(gF^MoU(l:
J#7]U%l:YaaCED7ru(?`\l0423I-'Q`93BH$o2k+A<*oSjh7u^RcAkH^:r#jkM5h-aPXa=382FM4I5lf
W3OV?O](V7;LKKAi#>V;Dj-K!1fqdsI^SK-oUC"pM"2E,oChOpVl$MCBOTEJbu0!,Fc/ie?.:HqnQ7YA
q:0qd;LW2dg5O,<+luf&h<T'11SgO(N&\8thXL6ODI;WD7"tI5aV[<XXiUju(/dog;GKn7^H,.6E_ZEa
9)4aed%pS_Se<_9,7qoH;sPW7%Rr%Q/ENZNV48Ml.F7qP&Y)!Wq-=tk8blY>f4D&Aa\mScG,cUCk&SZX
7(f%k`J(r`6H[E,0'-/\$LrU?rn@463*`4QrF[\0i5lKb&m-o/.>;\&(dY$&1?SHe3n$mEP83Jm(qJaD
T>asD9`._7=Dkf),D[WR)0t^p)0pNJ]=eIFg%r`rN[5or)D\$G5t:JeabY6D)*tX"UgTG\g=m+g78[io
<'Wojll0dWjNEpL$!^%kNbfeAc`SN<LO9+?d99A9kZqI`#&;R_$AR.oA^j234J>cQn1fK3OqH(Q1*R3Z
N4eq@&WJiJ5XpDM,GT@plRnj!A-=WBOqCQ]0j(9,9T\6s[2/^]"=Hf26lF+fe_49C&r180WW/3F9qJp"
,6CnORO;E[quK<@i-s?9KPSGlS'.0Jkc7/51\4!(<S[=P6rcsfG[nMd"7]Br]1)ppEsF5I3:Bi2gH?8L
AmDeI78-nFR:J$UO-[I"mp-tTjX7P-jZSckgfRX,r8j\NX/q5'"W_..&=$Z!&-W['rL,C#enZ2?cbRdV
c,4$tl;\fB.?URM\A3-KAfg%<LU'u/[;,_q?QOJTnD:HnMRq2Jp4t,kl8HtAS^6l>hVu5+PgEL0F-H(0
]t_u`)@]oOoXFe)l;Z%&HEACgC=UIe8C0b3;LHQ\O(c3f\M%$+g3i;]o_^(3;6tDlA&5+s3`QW2`>d(&
dc1eOitOB*)YJV)$JW`>2d1Xs3/&TO:`'duZ3d)(a>_R5*r1Q>Kun1q[^)::-4Z6`gtZ(E_a[&QPqA:W
Y4Vf-Kh$UDKHd/[KM@.4J`@8#k30JN(3)_V8f&YF;#>`1'OL%+V(j:0eOnCsXh==-X''AK.UTa6Wc;sT
LdLKX&5fAD77:RQ?cJ16ffV7S=$@Rm62<=;+^]"8^3$;.9ujUSkH\H.^\2T@XfNh;/EZ`(,#n3ka^3u-
ZFoXZPSm14FXQk*JN2m@h_Mj2qC3tLX%/-2Dlt7E``?%(i>s&Te8@_D]p*nlp%$>V'4QG5MK`2c(n'6g
!gqPKeX+2L!fn>g+8MaK[bTi,GWJ@QaI\+)?EH0n?&Rb]N(LReD%0MV656&*$?,9=FGm0-#]JjueC05:
d$p'A/?6O.k5nV@))Gm?Wg[+@Rh5Ih>9R^9<)m0@i[7-\NRaFRhbHO>JohpBS`h#[qsO+=7G3iQi2Y7*
$Z`SoIbm!D;20q^e>fkO,iX^"4q4(k0KZRhRa*)aNZ[!Teh@%LDY]9J$bMiXm%[%53ba]sB0t$.m=.,o
Bd`..l7PtkR]U;MF[se_IKEs0RD&M104Z4(#GR`\.%_JN9H925o-9Fn'=WHDb5#uBgOC7*CjDF#IKNHm
^r)q9;2lo2Zel0\3MReD@&tt1&e0nB7fAiCDlDcBH;N@I49jGXA,`!-]i%&XD2;'7phIR8S\&P<l>M_7
<,$a(EFlbima2UD7:c'0J^psL-tcMDl5o&GqE^ZLcD[tr)<D9k7L6*:*;cR2cJCWFb*gaePV];_-tgh]
7lbY14Na)EXaM&T-giFP!YJ@f,.9^-'JV^4b*eVO#`*=9e)RL+U#FBSfC33rbcneq`ksX,kLj:0!cMq'
'7OZYh0?,7XnJq.ZU^ra6ltFu*94;o6,2C;;9m+=.>GJE`Z&.5m\c,rqb[f@k/VqBV>ZJ8F6aqm`mi^Z
\#NdZMC[PA-nfu`1Sc'(Gmm$25L5OQ).\sfYt$fM4*u-?3R90-[`F_#BqkgU6L#_Kal]ocNnNt*'A3Z;
X7]:1$?Io].u\MW9MF"bCYe?>q/_+A7l`Zfbkql4;u"ND]s?,mkaB8jY)p3q?M,^ZN+R=!ZXp$m5%G/c
BrD\21GWQM2KG3S3R:/96:=Bi#:VQ%]PP;QrfYYEZZ>1Ga+i7aN,f-qSYPTX8(i4SX[g^gTSqR0a/83k
:*#7p(=b/UgDK$Glqu&URSh@X';M)<VIW/^DX_dWY3hcjqL2!5AM4D^992IWkUhj9-=*M3QNdmV;ilM*
*#r$DgMCcORqnG!+/EQ:9JmYsCY@K[<h&^Tn0hr=p[BToEOZo2L]>q/8KL>?@hL=KR;q3rMCZD:CPB-b
<YFIO?^-elY;DHJ27"8Wb1Z?2>MXr'`D#2X6#X0BA="S9*p$(t^aNi1$q<6irSm?U4g-$0^Xj$+cZ1eE
f'/-u4!4a8TT^uS*jJV+>34`nPk`BLYn"]s\KPM0f"dSe%Jmk@eqGuYHL<Jg,E3SK3dC`<Da7V`KGZTh
rK7e&);UlejF)?tb\`MVIAg;c=-E;"J,*1][t`FuOW]r=gglVp9%GUN=[NZ0UD7(kmFb3jAemEk4W3@)
.!pa,.?I3K:sN(2F)ulas!G[p\Z)u7_@NF=TK/8"X`7(=S;gqk7^SOZE>i:&1j7)Q_]$Mg2U<@L[7b)T
,\1$Td=`.G,PsOqAL=70OW7*k(J'l^R5fT&T:gY^fWon#XF3ZL&;Lr46QFdUCi0$e_kV=_G2$s%;79\E
gDbM%&&`qpL*ODB[.qF^6sEhG13-)tHfLr]Yj*=)$DJQG(^ZOQ&,(;;D"IDbM8.aP1Km574upg"Pk!g0
eok2/B)XlU^#_:pQDD=%kU]U6nL0kiNU/bh[3!I>U/_]t!nT6rE//83=.5ldXXkVcF.d1]ha>sJJ!6U,
%]*.P.Ti_cXb%RYU/go6?ZB+SUBJ>@;&;T0KD%H\la$?M/:3ZL<,3WNd1Ohta'[7bGBA/,+.HW6+.Lb]
4N\.%HJd4u\6DA.b2l^IMh[*iOhCE'-qC<XN^1uALEdX!^8,G-_j*a>.YeYiAl)a[.[t:t[AEgefeH\,
Z,qjja!RG,7s?6Ak+a/YBU.(sO7g<1'=N;Ia9].t$@?:9ncO*u6/iBCq._U=e)LhGWmf:$A*p[o1MLDi
A;I^`pN+-9rud8&^>?k4/+hn@E)f61jU9+Rl<B;]7l3tfCF*H9S;=+q_9@bFP=mKK,2thAB27NTQoX[H
l79h_SBSlpo!a`_a50e0Qnug$PBc49:q]B+b@li28b5S>ZhA[eS5\'QGu`UW!lFEM0YcYhS5ZAmg2`8F
Qlbu%%CSFJ!BZ4_N&B+ab]P^qIRHCEF&%),S=U6rU0S?I@kf1klBK9_%dd]?D6dTo&P/=2KKYg`4ljjH
%Z\Bs+1Oi'S2;>[RihW=1(rO7Mhp[?7;(Bs?<^U]7bN0td/tlT0Y;I1Hkq1^`Fn:dQ+Wi6C7ZdajKX&J
".hs*KVWh$5gB8V_If(r0+lf!b+S+(WBPi>pVBpd\C^RMD)O4/m.ut(BCiL]_V7/`B4&Jn:Qr7(435QF
SC!BK8S`?kVh'ti76BbFh1InWa3Eu'V-"7<>t+ad=B@AkBVdkj"dKeRrS[V1cF&'6Jb)=JcMgR2E9EfZ
OZqRR79%Ec(njP"NuV%rE\X?hi<<S_`5Gc'[O5@<,WjCbP!`b9(crtUGB7ZIqXRuKC8*tc%JSS/^Cf'L
rZ7T!ak_^r<$qDEES\snYo92HFQnaC)!%$LA#BOj\jbRIXX6G#FO=h\3'5c\(OcOn9g:IG"**.[3Q7gc
lRV["H=*dV0j4mo='W5A6D;V;8(>YsY$_0jN%oM)-i5FFLVMhtpa88qq?MEtIp_+4)deJn-$T,^%%m$[
fQeiQZND9E;AraYlWX5G`Y6'#gLm&A'a'2a(*8#?Sd+/nH32oK(%iLs2EFhSYZUDjV&jLL;C^G^91e18
C`:XS_K167K]f!3<kl(j.Aik`pekpP[,jpur&/pOL=-?Yj\)jc-H]U]]4Pg!QNAY[QVmq)#Z(4nJ:4VW
nUBJgI46MZM'Ah?&k*5$n>--.QXV^tj@cbCk(GD,V"i;_!ft/p%1KQOSZo2qkCm%C:Ab_`NT7^Ik-#9L
&$&2giCbmFj+1S"';+cj!6^_Wpa+H$"Z<@T@i0qR`HH<8B>;lOX9>u#HgBhX2pps)n@\"dQm)-Y."!IX
rod!(S3csK$>"n)'\HADI4;D1DYd8NV6j*Oi/QC[V:^05GtU'W6,&T^C0d&/bgGEOj@`X=C29C,&UZ$^
:A@_?2snu=1O<tl?<PA[IH4&+PugjB5kFKA*<%iGBiL;6_CCej:hT`!BL_eq$G!7d%fd[)1JmjE`66u;
FZr>["3c/BHj*$6RcVX2`VtWG`V(2+](BeJ\qD_J;YCSLN)o^O#2A33!=iJZU_L_51>*"Ab+`DfpB)s;
EHi1&VY>kaS!D*"&)V`#oAO[r$L'la$G7?kqFj3SVq8chY'bEHgmIIIrg`#_'_k1Yns(1Y>FY6Y$"(h%
%ik<u@5W]k`D4<@^6H_tq,2r,E<jJE9@6odmA?.ZZ][(SU[Te_A5'_%7=F%VGBn;"Hms6Mf\oJSoVtoS
?'=!8jQFD&=#h4]FB&`mrBakK,R:ps>*a`)=l?!CfpOd1V7KqS1`pPQC3+9cTeiq5b:8pAUqYj%;\g0u
obTiHXo%St3;`##`8Rgq/%d\7-2]LfhW<0Zn%ju)3>u5koYtR1[)=ro?Ddn?.On`lX`j_je!Cd+gbUcb
HuheTR6^m8&f#s6\P8Cg76Oc+g9jjNLcIYL2`$!.Pue&u#M>V0`9NL(c\+MKgkHCD0.t_#C:1XKr_':!
S)1ca8!C.;7ZW(o^Q)0l`r%;).<73I*Uk(HM6dk%Q7i$2R8j-Q.`lTo/*i%G>scR@<VXi\*a!uF6Q16P
m2-:Rr:;F3lQCPa4BG##roB11_I7;>lk(IkM&[]/hPo>NAS^SY>jq[;Pt+2ZiCLd*5mMjo<,rJ6[,.JE
^HrH57dlreb8[r)cYdLHdILtM>q,*i@N.`R6^78n$9lrm3U8QEmn3LVW2EG9W4PhA>5Le?'CB_(G$s,(
OS^\@!Nr4JF;*"3f;7>MGbQY9kn*:eQ1S'Q:d)tn"iYJN)o#u"0$L2mmP\fliUm&WhU(iH!B9mcDiK\W
EVdJQmtA+#Ar_1Jb]r*8T3ppEIHgY$Va3ebnd)L%*]\p8@.4@WGE9CD%'ELB$#p=C9jI7DDW_cYHhP*U
`N>H*AoG-J/@_CE=a=O2l5e[8?Z@!hB:&=THkUZlk!e3TrD^_=ZLS9J7$$3U3KX5(N[@q2m)2F"Y<5-$
;*Q&f?Vr7@X5V41hN(_ZiR=kjEr:$jo66E8ibA_Q^3&+6)@W4ecu4792?Z4[mI\A9<j0FEXZDk7!4tU^
GbUk?G&bZr!KPeYhWrj7pf1qH@?6Z#hul[E<\$Y>Hj<_gfZs+A?5X&i43#mA^HLHTcEh"0hZ9=)]@l9V
3%?6I=<eKFY='5AP!JM$&+.g0KcSl-Z'FXtq3!-&aB,*baB'RUmsO"'m-W@7neHS_R/`:i,TQYQE81E>
=$ku^'&jaXjda)*pA+17nWpB>2':=>S(9/kCc^"_I)FoF1f@\_?T:ObfRJ)"hmYQ2\A*Nlq,O,&oCW"$
q*'3;rFJ/n;$FFn)tdb9C-s<_oGL]P9]JidXRgV`,]`])ApuN7P535&G!GOd2X)7=GM]$uf\a2oh_jH,
V;K`T#J[#-:2*R+#AIO_4X01UAiJ%`]P.o$h-.HMkV,QF:4Mb)'MS/tOX]_/PH;KTCPY'nFb=#cQfbiA
iPeIlfT"/t-q:FcAm29lK<\W-+hfn6.%/!/k)hMTq\tq*>%!sorNb^9j`R_<qJ(<KW-n6H]LJ[hHJNga
d%O?u%WZ:3Z'i=TH[4>7GpfigQe_,&qJY$T"?NtQJYd?Um7nel]H=m%=EO+aLKQ!c)F!6BG66`Qp/^QO
&P&KWLuJ]CljE@sf0l[_]t*,.mX+\McWe:"B<Q7P"-,?m90IC]F+taCRksE=hlG_'ML4!:cO=R(`7i<0
p>4L[VlWi"3T-ID,!"q.j8ACA@%L`N\5lXUWO9X-QWhmmQhkGUa;_j=2u)=`%FRI`Wd>@s8@Q6aWKm&q
50+CDC:@<n)p>p($PZo^iD*0/1bHAiMctA9k7da8IGFR?jJC`*,03E*`&[Hfb:;(FgB0KaZ>L)"9.tE>
4<^/[\e]&03<HaXg@=_P1SuICh\soj06QhjaJ41ELONGol#HAsN*K]S`RXTA?1N??oG/&+U@.8adTbUY
q:"n49:*LgmAK0;kq=]tG,X="=[K1Hr%G&BYfF=hc<Yk?FHMpdatp=ZMEBa`S&7>`jbp%;b1krhUq>a\
U9rkIi=Q4A:hc?SiMBl&`[\R@90LBMc+Wl0VGc3=I,q+>P%L4FO7Q(N"o0][HgN@'nl"qC:>)=jIX&5W
[q\.Y9#uF*WNUL*XJ#dae0u9<?iCWjob_UC0X5tVa*rAaJ&C<=o2>UJIJ[rqe;r+=).%'q/%j:@2nlQ+
o3Tjt)K.3e?RWS:O*$@-0D-P'%oVfh#DE[i\1n6a[U(C-q(OZ(fkM@1l5_*GIqO#*e+i-j*VQr.PHD^Z
nTK2WX^>"5l=YnA+F0ri[F.]b2nH*Bf27Kg3-'#26ofV_$8498+YP^O/2-Z+'I-lAV$;r!IQ$%Y4Hj'k
4MHXr:\7Ya#k2jf26hPGcRr\.\9lg9`Qc&0ps9s\XY91Q%/XW2XOM<u0#$T>:==O'G8Z+3HgaSl&Q!_O
%B&_Y)X98o_Fk=h0D4K&i2,;+Aps>>?Ku*/QIc].$_Ln;B@Y]qHJK<`^UJ62Mu!6_l4K-HlLXr$90G:f
nkr9+PA^:mUO[7[%<6@k)Hh[S+SFZZkU'*t6dJM"dRuaXX0keGMVi6VDpk4\-.Qg#O'mf%[+n=AfIU<O
TBhUb]lucGph_\J)=pbPB"t.k:!h\/e/6Gf)h5aRWuHPPg8u#So>sY=aCUY1H16o98:uQUV%t_.k)]d;
_+Qq.1i_PoOJ?dTY6PS@s%hdU$*K+cEaJuUVS,L.mRl.ooTV_o(@Q^GNF$@/pJUTsHDtPbn'F_Pn$n^Q
]qC^<MQb3_rk$+pc9@tFn@a$++0N-D2^f9JnMg9FAd\+4>C=;tnMdC@ET:t6?g:cu0:-SSJ_p#scd"Fo
>oNP!ps%K/(`C%R(OE&P1\XdOPf$VoUD/'@?>:\Y;?K^@WQcf,El<3YCGJ@42<BX#C<E&/K0<&X414'B
--:Sjp`!K6._-7V%aS/'0?dJQ6<fqo@uu?(>o6bOESnk8GRp*!U/B+XQ(O*VK9IhS$`EafpjU*=h.V;V
I%E:$r!VY*)5L%$[i!qXX5qe/-1cgVCUX]BrLZ(,R(BQjre)Tf7>'WIRiYVbhf,SbhqgL!Np$5O:Rl`Q
4!dCb5_X>jl;1[@=?!02Gl[SX0(jlQJJLiSO=b3l[J%brP)ZM?>?0PE.%rM9URnuef+E>gpg"K\,.)/a
O*MaV]N:Ld428CA'd`g)Hm^\#^9fJ=(`'`Lp%3r%NCrZdpp`VD?#n_n3i(Kj>r(cSFq#-EW&F=pjBXP)
'\o?B>hc:9FDB5Y:4:geC*:t%VH-pOQSO,8mUOFuL<9YQ\ZbDdlchZ$bVoGjR6V^`+$I?69V@C2I+Yp5
H8jPD-i/rp&=GckcQ\(Ua'Xkb`4/*UWL5DXfhm\)/NW39bM8A$:gKYtR"_0bZg_=M%5r0RbOk#k1Qg;)
`j=2GAB+s7T@F4VG!WbaKFt6(4QtErrmJ'=C&BTtf5dr+GntkLh6Z&YL\]1>>OT514nrs%5!8O3[YMKS
<N^iGBYW&.+U'0'MoA9r/,&(CF^(VS4`@QL<il!>:u'V5?@4DLg#1pa)4(1B[b;VZW>:_/GfSZuoM.0c
Ojfpp._3%]cI[CjFk\7[?>KAC<BBCA07R.D97+X!j#]9e0m,tmqD6Ish9r5XdpK544YGe_kf'L>Nj!?G
"uQ#*)<ni$F(8&tf2ht7BSH0>^4uR1/H#6]*`X50(s3_N+!+f*rk#8&]/\5m=no2G^s'ZFVtOuqKeffZ
TpiVj+WF@+K?llmf?SET/GtaC>hqU0d)5I)-&&6?gSJ2bFtD%&k8e/8`Nu*Tm.uF%pVrb[DX]OWUUqh[
rW21cp\,1XDXcmYhjA0po<#VWi)\[llf(IDP)oMN%FL7#8)3$d,fhA*hS20ko0"%@(FDRV*[3\NGg>:-
OoQ#Taj^SAYI&8-^OFP-IaG9=#$uPo%DFY@s%%#_o-n3c@/7#2p(-KH-SGFlV=O*@gOF(>X<uEH/X=+'
.P@2C9R'&hCW\gHFbEI*[]$uXg9@OXZcQr\"_OF+:FqG3b_Nkmc/2a#!jd<_WWMdP&ZEIpm4<O1X>as.
TUP0UDrL/0].,L=^T&b;RoZ[hJ_>-;<<_O.DkdB?Teo)D<FE7B3p#_A/LiH,)QLR].]WXB$5=_$DQ+oT
o*NG6M/,/5=>nf"O_/t2TEoLX9-pk.*S]hlW>;d2D2V$.VJVde.,3%TWT'r2Kj)u9<R9'-X+^LnnND3e
H@6Mb;3+*g<FGfn@EWQa)fAN()-@W@bnAl4J_@,r/LiG-Nkt(A<ftq<J$'VqeB@5K\*]D`l'Let]p7VV
h<1)M"QYlk4Fa`XKBq>kfd-g/<-C?G='@*N&/Xr8=3VB-4/mFL.h6L+?%$Xe,V>:)[k1cXUK:VK]3/+5
>:P!]GK`t<$[[P!R"h&6.NNHa."e,=H_G#\lM!n61Y@"@l8iVHQ:d+5eRZX;-AR:8!KWr">*s!XmNDRs
<G#P?&+(5_.POXj7;(aOE7AH*6Hm)WCe?0Ra0]K>lKuhnh<\%rC3Hr'ZZ?A<prV61'&%\(@E9?:0OMu!
.PP5P?;#M7*^!S4H5j_'CIgGL&&(O+g.(I^)g(I!n\)8iKtOf,?(e[^;G"IH;V;:hW0pgYDd#FHfC&Rm
\B!\Np[i9QriG;c2/dR8DJlH?NBD/B%jY2nFb;<R<0G*qJbb0:2^j^:!#:m/<_0i:,sh4X_Y(,%.p.pY
d\.Z.;G!9TjjY%okj>Ll>?)D@#M1lYc/<dqB*4="bbD7II`U:*g=j2F4n,r@lB]Ri3C;HuLtttNkB$XD
q`@RA?[p!khmAQah`+aga!H4T"L%mH)lN=p7W>>*21eJiE!1PP._%B7qh<Mr7smP[kjeTrcF2:q)H#f6
]@L=f)V?83-b7C,^=BA[>(7!a.Cd3gft]2#cB1uiYP)^JUl2O@h5p;:k)!mP-(<]Q.($_Km<;uDlS$JS
COK<rV=bXJ.+J+::04.^0q-JccP\(:rmD:d':l5tf:qsmXhPWq7`[m)\1^`[fgQB+X`'IO?+X8<c_U6#
<e80BTh2Fl?GE</=3G@\D!oaJX&@,J^G45*le?h-fD8-U!4>"qC],PBYBrnoZp)7b>(=)SDdV?gl?2gX
[JoIX,?b?0$bAm4mh]9.p"/92R]2U\^>mq<^SUT>eoKC=95?kt6#UN:V)9B58+!DN_:-j/U^ORo.I:6q
m``5m?W]gtoncr>"&%s`_Hg='=%_'?PTspkb+.msoftf]Dq4j%E9&fjI;/q#I;01*rN'@N+/a*c`k*b>
IWo@=^P+.U9fk45(PTfmmRl#ea?$/j)fDQQ9!I`dPh`i&jk6Aqf4>oI>t\f\gseEU4)i/qS\I=7Zu=uo
^j?j-b4k`=A8.*X].<*-DPpH+KC3g1O4M8L><@=T)l^cf/)+,3<WpAka1Ek?IWoB#R:6E7Fa$:3M41<s
/[FAM&"U#2hi><!,rC^C*4n+BK#;N,@e:hom-`$M4ILd#FcmKQAO!DG%SQMJ?/iOpiVH/Mp6YME[o7#a
Nig'"Z+F`skhpYd81!+u+U*.8-1gjAh:@1b1UA$?pa>MPZ"J6$gf][O,<ibk"$W<*6YgCq1IU'8HW=:k
T&#Wmb'Q?d74m@B4,hdtKLF`nQ<(M`O@Ngpa*<>6&K+Zba+<D<EV#"QNE)&Pfm@^m?Sd#qRGBhLAp1LS
QSGE$A&]%/X4p76e%sd2BKLZI$+0!1*QBmBrHcenB.)d9@-H_[f+iUEqerurj3:f_.F%A5l@bp[QTF/3
cd(`Xiloaep8hj-XXg$6/5rFZkK8D`)+;3^:!L*1qh3g(^<=dq.c9@MGNBe*^8/en,2_M)H]KtZ;*:Ub
dNN76%<K,I-EMC5Z-ce1FP^QA;VGVqT^^NI24gm;gSFfH^#?*\jL;u&>qL<`9QHWn?*:8;Nq4Uh9]@SA
Bt1S=3JT$?A`=ePe;@2dcbR\I\_(QU]3O&S'_MgGMgUkTh^og6bH'\Ag+i%Lk;@O#Hh<\iJW@6h43`(N
"%BrIe:2d[okrV&!tHm_DpTjA/TO'lPh5#k."VHb>[tHAE8`'W>4^rN`j>5Z]>[1c"i6CY..Ge3Sn.0j
/TRZ"c<Gq8mg^f8JUC`J7`@FeZG,07[6tDq)E_RgoAjb,/]Z7,lOEe`3iFs/W)Vt:m!SNt[[GGX,7"WP
18mblXBTU3ee[+sHJ*PH:'R%^-<mdWBS2E5Rj>],``)f@o.,-*kBBOV5-%3-Yq(]?OCR[T4+E*%G'^@\
#d8KdZa0!*.Cn0-(6@->'#p(!Q\E%W]baYr=$].F.+.cZe,t0>o4s?b)*%%>*M%plGW`>2_C%I>D),Vf
iB"<N2d'cE6j5F0%_8nD7GZ8JXgeV4'aj].9J?:60mpCV5KLOpbt'd:ZjVd#W94[NB-,lY^ltZM<,a^c
^`-J-Fb4o[obfWa]6*WpV#JAJRNCUT>)tKTUG&9EQpCc#!:j!+"t.+3Bmr38X@r02&'Q#i3.6'P6-E7F
HBU>9`2s4]lZm)*Ffs_PZLO[c-"Z_'oC<s9I\5MbqIje<ICurO`6GC_#FtWm2eA>b*/UF'i-LHiCB)F\
NfNLmA"AiV]B*/&6BYjEjEbQIpc?m,q5MKGK^@A89l7"<rlYZP9q]XaN>e_!$7X:Y#t<snp?(]kMtn;^
V8N"m*9*oEfZ?X^I+UGhW-kOD5a;k8:-T+2*kn4.kbQbkU!?94g:>ria1AbnnWmZjUq5Anneni>Nd6(V
q%15sb(lOV'6OL@2.n50oo9aoV6<bX0'R.P]aX9k>)aPb<Hd0^Qa.du]nO/$X<2__7mpYgN`^E.$Wj30
j1S<`d(LakbBp&=L=0e9I->3mNpWWLG4(4ZhG4$1YBG4SH:"V%@FNn]Pn/^Sq65D\<;@*+B2h(5QW3bl
go&k\i74cRLm,;OYsPJ]@H"PmI#,*E.j'qOXF+,'TB5?8V57>jOADqJ+l7<gAu?eP5/_3/b(l8.U:+Ll
Z`8E*?EG(1EN6l*X3-4gkG%O@Hg9#f7--f'D,g>8CR%V4?=$VGF/@$79d_^iagIi:5:Rb@cn0uEXGU">
f]uO6Yq*'3WEAnL=)0rPN^'f^e1^hJ.j%gZDi;plFNd9R(\L^T#KsB;eCpj^;#*q6D`A2ABXXeEg`8]L
W-m7T3gBRAM>sp)L]=O8Tm)%4+92/skHDr+ci7(nci;1thZ%P2&S9:&Bm0rkqo!mK)=.=tl[Ss"n\>+s
If[Ru\1%~>
endstream
endobj
7 0 obj
58816
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 731.00 403.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
0000059559 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000059535 00000 n
0000060013 00000 n
0000059729 00000 n
0000059768 00000 n
0000059870 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
60086
%%EOF

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

@ -4,3 +4,6 @@ language = "en"
multilingual = false
src = "src"
title = "The sat-rs book"
[output.html]
[output.linkcheck]

View File

@ -2,9 +2,16 @@
- [Introduction](./introduction.md)
- [Design](./design.md)
# Basic concepts and components
- [Communication with Space Systems](./communication.md)
- [Working with Constrained Systems](./constrained-systems.md)
- [Actions](./actions.md)
- [Modes and Health](./modes-and-health.md)
- [Housekeeping Data](./housekeeping.md)
- [Events](./events.md)
# Example project
- [The satrs-example application](./example.md)

View File

@ -1,3 +1,5 @@
<div id="communication-chapter"/>
# Communication with sat-rs based software
Communication is a vital topic for remote system which are usually not (directly)
@ -15,10 +17,16 @@ it is still centered around small packets. `sat-rs` provides support for these E
standards and also attempts to fill the gap to the internet protocol by providing the following
components.
1. [UDP TMTC Server](https://docs.rs/satrs-core/0.1.0-alpha.0/satrs_core/hal/host/udp_server/index.html#).
1. [UDP TMTC Server](https://docs.rs/satrs-core/0.1.0-alpha.0/satrs_core/hal/host/udp_server/index.html).
UDP is already packet based which makes it an excellent fit for exchanging space packets.
2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol
to always deliver complete packets.
2. [TCP TMTC Server Components](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/index.html).
TCP is a stream based protocol, so the framework provides building blocks to parse telemetry
from an arbitrary bytestream. Two concrete implementations are provided:
- [TCP spacepackets server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
to parse tightly packed CCSDS Spacepackets.
- [TCP COBS server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/struct.TcpTmtcInCobsServer.html)
to parse generic frames wrapped with the
[COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).
# Working with telemetry and telecommands (TMTC)

View File

@ -18,7 +18,7 @@ running out of memory (something even Rust can not protect from) or heap fragmen
A huge candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
candidates where the data size might vary greatly. The regular solution for host systems
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
another solution to avoid run-time allocations by offering and recommendng pre-allocated static
another solution to avoid run-time allocations by offering pre-allocated static
pools.
These pools are split into subpools where each subpool can have different page sizes.

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
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)

152
satrs-book/src/example.md Normal file
View File

@ -0,0 +1,152 @@
# sat-rs Example Application
The `sat-rs` framework includes a monolithic example application which can be found inside
the [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
subdirectory of the repository. The primary purpose of this example application is to show how
the various components of the sat-rs framework could be used as part of a larger on-board
software application.
## Structure of the example project
The example project contains components which could also be expected to be part of a production
On-Board Software. A structural diagram of the example application is given to provide
a brief high-level view of the components used inside the example application:
![satrs-example component structure](images/satrs-example/satrs-example-structure.png)
The dotted lines are used to denote optional components. In this case, the static pool components
are optional because the heap can be used as a simpler mechanism to store TMTC packets as well.
Some additional explanation is provided for the various components.
### TCP/IP server components
The example includes 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 UDP server is strongly based on the
[UDP TC server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/udp_server/struct.UdpTcServer.html).
This server component is wrapped by a TMTC server which handles all telemetry to the last connected
client.
The TCP server is based on the [TCP Spacepacket Server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
class. It parses space packets by using the CCSDS space packet ID as the packet
start delimiter. All available telemetry will be sent back to a client after having read all
telecommands from the client.
### TMTC Infrastructure
The most important components of the TMTC infrastructure include the following components:
- A TC source component which demultiplexes and routes telecommands based on parameters like
packet APID or PUS service and subservice type.
- A TM sink sink component which is the target of all sent telemetry and sends it to downlink
handlers like the UDP and TCP server.
You can read the [Communications chapter](./communication.md) for more
background information on the chosen TMTC infrastructure approach.
### PUS Service Components
A PUS service stack is provided which exposes some functionality conformant with the ECSS PUS
services. This currently includes the following services:
- Service 1 for telecommand verification. The verification handling is handled locally: Each
component which generates verification telemetry in some shape or form receives a
[reporter](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/pus/verification/struct.VerificationReporterWithSender.html)
object which can be used to send PUS 1 verification telemetry to the TM funnel.
- Service 3 for housekeeping telemetry handling.
- Service 5 for management and downlink of on-board events.
- Service 8 for handling on-board actions.
- Service 11 for scheduling telecommands to be released at a specific time. This component
uses the [PUS scheduler class](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/pus/scheduler/alloc_mod/struct.PusScheduler.html)
which performs the core logic of scheduling telecommands. All telecommands released by the
scheduler are sent to the central TC source using a message.
- Service 17 for test purposes like pings.
### Event Management Component
An event manager based on the sat-rs
[event manager component](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/event_man/index.html)
is provided to handle the event IPC and FDIR mechanism. The event message are converted to PUS 5
telemetry by the
[PUS event dispatcher](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/pus/event_man/alloc_mod/struct.PusEventDispatcher.html).
You can read the [events](./events.md) chapter for more in-depth information about event management.
### Sample Application Components
These components are example mission specific. They provide an idea how mission specific modules
would look like the sat-rs context. It currently includes the following components:
- An Attitute and Orbit Control (AOCS) example task which can also process some PUS commands.
## Dataflow
The interaction of the various components is provided in the following diagram:
![satrs-example dataflow diagram](images/satrs-example/satrs-example-dataflow.png)
It should be noted that an arrow coming out of a component group refers to multiple components
in that group. An explanation for important component groups will be given.
#### TMTC component group
This groups is the primary interface for clients to communicate with the on-board software
using a standardized TMTC protocol. The example uses the
[ECSS PUS protocol](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
In the future, this might be extended with the
[CCSDS File Delivery Protocol](https://public.ccsds.org/Pubs/727x0b5.pdf).
A client can connect to the UDP or TCP server to send these PUS packets to the on-board software.
These servers then forward the telecommads to a centralized TC source component using a dedicated
message abstraction.
This TC source component then demultiplexes the message and forwards it to the relevant components.
Right now, it forwards all PUS requests to the respective PUS service handlers using the PUS
receiver component. The individual PUS services are running in a separate thread. In the future,
additional forwarding to components like a CFDP handler might be added as well. It should be noted
that PUS11 commands might contain other PUS commands which should be scheduled in the future.
These wrapped commands are forwarded to the PUS11 handler. When the schedule releases those
commands, it forwards the released commands to the TC source again. This allows the scheduler
and the TC source to run in separate threads and keeps them cleanly separated.
All telemetry generated by the on-board software is sent to a centralized TM funnel. This component
also performs a demultiplexing step to forward all telemetry to the relevant TM recipients.
In the example case, this is the last UDP client, or a connected TCP client. In the future,
forwarding to a persistent telemetry store and a simulated communication component might be
added here as well. The centralized TM funnel also takes care of some packet processing steps which
need to be applied for each ECSS PUS packet, for example CCSDS specific APID incrementation and
PUS specific message counter incrementation.
#### Application Group
The application components generally do not receive raw PUS packets directly, even though
this is certainly possible. Instead, they receive internalized messages from the PUS service
handlers. For example, instead of receiving a PUS 8 Action Telecommand directly, an application
component will receive a special `ActionRequest` message type reduced to the basic important
information required to execute a request. These special requests are denoted by the blue arrow
in the diagram.
It should be noted that the arrow pointing towards the event manager points in both directions.
This is because the application components might be interested in events generated by other
components as well. This mechanism is oftentimes used to implement the FDIR functionality on system
and component level.
#### Shared components and functional interfaces
It should be noted that sometimes, a functional interface is used instead of a message. This
is used for the generation of verification telemetry. The verification reporter is a clonable
component which generates and sends PUS1 verification telemetry directly to the TM funnel. This
introduces a loose coupling to the PUS standard but was considered the easiest solution for
a project which utilizes PUS as the main communication protocol. In the future, a generic
verification abstraction might be introduced to completely decouple the application layer from
PUS.
The same concept is applied if the backing store of TMTC packets are shared pools. Every
component which needs to read telecommands inside that shared pool or generate new telemetry
into that shared pool will received a clonable shared handle to that pool.
The same concept could be extended to power or thermal handling. For example, a shared power helper
component might be used to retrieve power state information and send power switch commands through
a functional interface. The actual implementation of the functional interface might still use
shared memory and/or messages, but the functional interface makes using and testing the interaction
with these components easier.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 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/)
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

@ -1,6 +1,6 @@
[package]
name = "satrs-core"
version = "0.1.0-alpha.1"
version = "0.1.0-alpha.2"
edition = "2021"
rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
@ -15,8 +15,6 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
[dependencies]
delegate = ">0.7, <=0.10"
paste = "1"
# TODO: Remove this as soon as the image including the description was moved to the satrs-book.
embed-doc-image = "0.1"
[dependencies.smallvec]
version = "1"
@ -73,11 +71,11 @@ features = ["all"]
optional = true
[dependencies.spacepackets]
version = "0.7.0-beta.2"
version = "0.9.0"
default-features = false
# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git"
# rev = "79d26e1a6"
# branch = ""
# rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7"
# branch = "main"
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
@ -91,6 +89,7 @@ zerocopy = "0.7"
once_cell = "1.13"
serde_json = "1"
rand = "0.8"
tempfile = "3"
[dev-dependencies.postcard]
version = "1"
@ -123,4 +122,4 @@ doc-images = []
[package.metadata.docs.rs]
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,770 @@
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;
use std::path::Path;
#[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, dir_path: &str, all: bool) -> Result<(), FilestoreError>;
fn create_dir(&self, dir_path: &str) -> 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(path: &str) -> Option<&str>
where
Self: Sized,
{
// 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;
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.
///
/// The passed verification buffer argument will be used by the specific implementation as
/// a buffer to read the file into. It is recommended to use common buffer sizes like
/// 4096 or 8192 bytes.
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 Err(FilestoreError::FileDoesNotExist);
}
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 Err(FilestoreError::FileDoesNotExist);
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
}
OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)?;
Ok(())
}
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError> {
fs::create_dir(dir_path).map_err(|e| FilestoreError::Io {
raw_errno: e.raw_os_error(),
string: e.to_string(),
})?;
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 is_file(&self, path: &str) -> bool {
let path = Path::new(path);
path.is_file()
}
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 => {
if self.calc_modular_checksum(file_path)? == expected_checksum {
return Ok(true);
}
Ok(false)
}
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)),
}
}
}
impl NativeFilestore {
pub fn calc_modular_checksum(&self, file_path: &str) -> Result<u32, FilestoreError> {
let mut checksum: u32 = 0;
let file = File::open(file_path)?;
let mut buf_reader = BufReader::new(file);
let mut buffer = [0; 4];
loop {
let bytes_read = buf_reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
// Perform padding directly in the buffer
(bytes_read..4).for_each(|i| {
buffer[i] = 0;
});
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
}
Ok(checksum)
}
}
}
#[cfg(test)]
mod tests {
use std::{fs, path::Path, println};
use super::*;
use alloc::format;
use tempfile::tempdir;
const EXAMPLE_DATA_CFDP: [u8; 15] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
];
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()));
}
#[test]
fn test_basic_native_fs_file_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()));
}
#[test]
fn test_basic_native_fs_dir_exists() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let dir_path = tmpdir.path().join("testdir");
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
NATIVE_FS
.create_dir(dir_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
assert!(NATIVE_FS.is_dir(dir_path.as_path().to_str().unwrap()));
}
#[test]
fn test_basic_native_fs_remove_file() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.expect("creating file failed");
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
NATIVE_FS
.remove_file(file_path.to_str().unwrap())
.expect("removing file failed");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
}
#[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);
}
#[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);
}
#[test]
fn test_truncate_file() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.expect("creating file failed");
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
assert_eq!(fs::read(file_path.clone()).unwrap(), [1, 2, 3, 4]);
NATIVE_FS
.truncate_file(file_path.to_str().unwrap())
.unwrap();
assert_eq!(fs::read(file_path.clone()).unwrap(), []);
}
#[test]
fn test_remove_dir() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let dir_path = tmpdir.path().join("testdir");
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
NATIVE_FS
.create_dir(dir_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
NATIVE_FS
.remove_dir(dir_path.to_str().unwrap(), false)
.unwrap();
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
}
#[test]
fn test_read_file() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.expect("creating file failed");
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
let read_buf: &mut [u8] = &mut [0; 4];
NATIVE_FS
.read_data(file_path.to_str().unwrap(), 0, 4, read_buf)
.unwrap();
assert_eq!([1, 2, 3, 4], read_buf);
NATIVE_FS
.write_data(file_path.to_str().unwrap(), 4, &[5, 6, 7, 8])
.expect("writing to file failed");
NATIVE_FS
.read_data(file_path.to_str().unwrap(), 2, 4, read_buf)
.unwrap();
assert_eq!([3, 4, 5, 6], read_buf);
}
#[test]
fn test_remove_which_does_not_exist() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::FileDoesNotExist = error {
assert_eq!(error.to_string(), "file does not exist");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_file_already_exists() {
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 result =
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::FileAlreadyExists = error {
assert_eq!(error.to_string(), "file already exists");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_remove_file_with_dir_api() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::IsNotDirectory = error {
assert_eq!(error.to_string(), "is not a directory");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_remove_dir_remove_all() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let dir_path = tmpdir.path().join("test");
NATIVE_FS
.create_dir(dir_path.to_str().expect("getting str for file failed"))
.unwrap();
let file_path = dir_path.as_path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
let result = NATIVE_FS.remove_dir(dir_path.to_str().unwrap(), true);
assert!(result.is_ok());
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
}
#[test]
fn test_remove_dir_with_file_api() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test");
NATIVE_FS
.create_dir(file_path.to_str().expect("getting str for file failed"))
.unwrap();
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::IsNotFile = error {
assert_eq!(error.to_string(), "is not a file");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_remove_dir_which_does_not_exist() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test");
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::DirDoesNotExist = error {
assert_eq!(error.to_string(), "directory does not exist");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_remove_file_which_does_not_exist() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::FileDoesNotExist = error {
assert_eq!(error.to_string(), "file does not exist");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_truncate_file_which_does_not_exist() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::FileDoesNotExist = error {
assert_eq!(error.to_string(), "file does not exist");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_truncate_file_on_directory() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test");
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::IsNotFile = error {
assert_eq!(error.to_string(), "is not a file");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_byte_conversion_error_when_reading() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 2, &mut []);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::ByteConversion(byte_conv_error) = error {
if let ByteConversionError::ToSliceTooSmall { found, expected } = byte_conv_error {
assert_eq!(found, 0);
assert_eq!(expected, 2);
} else {
panic!("unexpected error");
}
assert_eq!(
error.to_string(),
format!("filestore error: {}", byte_conv_error)
);
} else {
panic!("unexpected error");
}
}
#[test]
fn test_read_file_on_dir() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let dir_path = tmpdir.path().join("test");
NATIVE_FS
.create_dir(dir_path.to_str().expect("getting str for file failed"))
.unwrap();
let result = NATIVE_FS.read_data(dir_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::IsNotFile = error {
assert_eq!(error.to_string(), "is not a file");
} else {
panic!("unexpected error");
}
}
#[test]
fn test_write_file_non_existing() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::FileDoesNotExist = error {
} else {
panic!("unexpected error");
}
}
#[test]
fn test_write_file_on_dir() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test");
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::IsNotFile = error {
} else {
panic!("unexpected error");
}
}
#[test]
fn test_filename_extraction() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.unwrap();
NativeFilestore::filename_from_full_path(file_path.to_str().unwrap());
}
#[test]
fn test_modular_checksum() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("mod-crc.bin");
fs::write(file_path.as_path(), EXAMPLE_DATA_CFDP).expect("writing test file failed");
// Kind of re-writing the modular checksum impl here which we are trying to test, but the
// numbers/correctness were verified manually using calculators, so this is okay.
let mut checksum: u32 = 0;
let mut buffer: [u8; 4] = [0; 4];
for i in 0..3 {
buffer = EXAMPLE_DATA_CFDP[i * 4..(i + 1) * 4].try_into().unwrap();
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
}
buffer[0..3].copy_from_slice(&EXAMPLE_DATA_CFDP[12..15]);
buffer[3] = 0;
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
let mut verif_buf: [u8; 32] = [0; 32];
let result = NATIVE_FS.checksum_verify(
file_path.to_str().unwrap(),
ChecksumType::Modular,
checksum,
&mut verif_buf,
);
assert!(result.is_ok());
}
#[test]
fn test_null_checksum_impl() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("mod-crc.bin");
// The file to check does not even need to exist, and the verification buffer can be
// empty: the null checksum is always yields the same result.
let result = NATIVE_FS.checksum_verify(
file_path.to_str().unwrap(),
ChecksumType::NullChecksum,
0,
&mut [],
);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_checksum_not_implemented() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("mod-crc.bin");
// The file to check does not even need to exist, and the verification buffer can be
// empty: the null checksum is always yields the same result.
let result = NATIVE_FS.checksum_verify(
file_path.to_str().unwrap(),
ChecksumType::Crc32Proximity1,
0,
&mut [],
);
assert!(result.is_err());
let error = result.unwrap_err();
if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error {
assert_eq!(
error.to_string(),
format!("checksum {:?} not implemented", cksum_type)
);
} else {
panic!("unexpected error");
}
}
}

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 hashbrown::HashMap;
use spacepackets::{
cfdp::{
pdu::{FileDirectiveType, PduError, PduHeader},
ChecksumType, PduType, TransmissionMode,
ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode,
},
util::UnsignedByteField,
};
@ -14,6 +19,8 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
pub mod dest;
#[cfg(feature = "alloc")]
pub mod filestore;
#[cfg(feature = "std")]
pub mod source;
pub mod user;
@ -24,7 +31,27 @@ pub enum EntityType {
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
/// transmission mode.
///
@ -35,30 +62,40 @@ pub enum EntityType {
/// 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
/// 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 reset(&mut self);
}
/// 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
/// and 4.6.3.3. The [CheckTimerProvider] provides more information about the purpose of the
/// check timer.
/// and 4.6.3.3. The [CheckTimer] documentation provides more information about the purpose of the
/// check timer in the context of CFDP.
///
/// This trait also allows the creation of different check timers depending on
/// the ID of the local entity, the ID of the remote entity for a given transaction, and the
/// type of entity.
/// This trait also allows the creation of different check timers depending on context and purpose
/// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or
/// other factors.
#[cfg(feature = "alloc")]
pub trait CheckTimerCreator {
fn get_check_timer_provider(
local_id: &UnsignedByteField,
remote_id: &UnsignedByteField,
entity_type: EntityType,
) -> Box<dyn CheckTimerProvider>;
fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box<dyn CheckTimer>;
}
/// 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.
#[cfg(feature = "std")]
#[derive(Debug)]
pub struct StdCheckTimer {
expiry_time_seconds: u64,
start_time: std::time::Instant,
@ -75,7 +112,7 @@ impl StdCheckTimer {
}
#[cfg(feature = "std")]
impl CheckTimerProvider for StdCheckTimer {
impl CheckTimer for StdCheckTimer {
fn has_expired(&self) -> bool {
let elapsed_time = self.start_time.elapsed();
if elapsed_time.as_secs() > self.expiry_time_seconds {
@ -83,24 +120,322 @@ impl CheckTimerProvider for StdCheckTimer {
}
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 entity_id: UnsignedByteField,
pub max_packet_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 default_transmission_mode: TransmissionMode,
pub default_crc_type: ChecksumType,
pub positive_ack_timer_interval_seconds: f32,
pub positive_ack_timer_expiration_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 {
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))]
pub struct TransactionId {
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)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TransactionStep {
Idle = 0,
TransactionStart = 1,
ReceivingFileDataPdus = 2,
SendingAckPdu = 3,
TransferCompletion = 4,
SendingFinishedPdu = 5,
ReceivingFileDataPdusWithCheckLimitHandling = 3,
SendingAckPdu = 4,
TransferCompletion = 5,
SendingFinishedPdu = 6,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum State {
Idle = 0,
BusyClass1Nacked = 2,
BusyClass2Acked = 3,
Busy = 1,
Suspended = 2,
}
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
@ -248,8 +598,8 @@ mod tests {
pdu::{
eof::EofPdu,
file_data::FileDataPdu,
metadata::{MetadataGenericParams, MetadataPdu},
CommonPduConfig, FileDirectiveType, PduHeader,
metadata::{MetadataGenericParams, MetadataPduCreator},
CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket,
},
PduType,
};
@ -272,7 +622,8 @@ mod tests {
let dest_file_name = "hello-dest.txt";
let src_lv = Lv::new_from_str(src_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
.write_to_bytes(&mut buf)
.expect("writing metadata PDU failed");

View File

@ -1,10 +1,10 @@
use spacepackets::{
cfdp::{
pdu::{
file_data::RecordContinuationState,
file_data::SegmentMetadata,
finished::{DeliveryCode, FileStatus},
},
tlv::msg_to_user::MsgToUserTlv,
tlv::{msg_to_user::MsgToUserTlv, WritableTlv},
ConditionCode,
},
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>],
}
#[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)]
pub struct FileSegmentRecvdParams<'seg_meta> {
pub id: TransactionId,
pub offset: u64,
pub length: usize,
pub rec_cont_state: Option<RecordContinuationState>,
pub segment_metadata: Option<&'seg_meta [u8]>,
pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>,
}
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")]
impl PacketIdLookup for Vec<PacketId> {
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
/// [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.
@ -101,7 +113,7 @@ pub fn parse_buffer_for_ccsds_space_packets<E>(
#[cfg(test)]
mod tests {
use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket},
ecss::{tc::PusTcCreator, WritablePusPacket},
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
//! task is the [EventManager]. It receives all events and then routes them to event subscribers
//! where appropriate.
#![cfg_attr(feature = "doc-images",
cfg_attr(all(),
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.
//! where appropriate. 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:
//!
//! ![Event flow][event_man_arch]
//! It is recommended to read the
//! [sat-rs book chapter](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/events.html)
//! about events first:
//!
//! The event manager has a listener table abstracted by the [ListenerTable], which maps
//! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId].

View File

@ -1,13 +1,13 @@
//! Task scheduling module
use alloc::string::String;
use bus::BusReader;
use std::boxed::Box;
use std::error::Error;
use std::sync::mpsc::TryRecvError;
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;
use std::vec;
use std::vec::Vec;
use std::{io, thread};
#[derive(Debug, PartialEq, Eq)]
pub enum OpResult {
@ -34,47 +34,49 @@ pub trait Executable: Send {
/// # Arguments
///
/// * `executable`: Executable task
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks
/// * `op_code`: Operation code which is passed to the executable task [operation call][Executable::periodic_op]
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks.
/// If [None] is passed, no sleeping will be performed.
/// * `op_code`: Operation code which is passed to the executable task
/// [operation call][Executable::periodic_op]
/// * `termination`: Optional termination handler which can cancel threads with a broadcast
pub fn exec_sched_single<
T: Executable<Error = E> + Send + 'static + ?Sized,
E: Error + Send + 'static,
>(
pub fn exec_sched_single<T: Executable<Error = E> + Send + 'static + ?Sized, E: Send + 'static>(
mut executable: Box<T>,
task_freq: Option<Duration>,
op_code: i32,
mut termination: Option<BusReader<()>>,
) -> JoinHandle<Result<OpResult, E>> {
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
let mut cycle_count = 0;
thread::spawn(move || loop {
if let Some(ref mut terminator) = termination {
match terminator.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
return Ok(OpResult::Ok);
}
Err(TryRecvError::Empty) => (),
}
}
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
return Ok(OpResult::Ok);
}
ExecutionType::Infinite => {
executable.periodic_op(op_code)?;
}
ExecutionType::Cycles(cycles) => {
executable.periodic_op(op_code)?;
cycle_count += 1;
if cycle_count == cycles {
return Ok(OpResult::Ok);
thread::Builder::new()
.name(String::from(executable.task_name()))
.spawn(move || loop {
if let Some(ref mut terminator) = termination {
match terminator.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
return Ok(OpResult::Ok);
}
Err(TryRecvError::Empty) => (),
}
}
}
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
thread::sleep(freq);
})
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
return Ok(OpResult::Ok);
}
ExecutionType::Infinite => {
executable.periodic_op(op_code)?;
}
ExecutionType::Cycles(cycles) => {
executable.periodic_op(op_code)?;
cycle_count += 1;
if cycle_count == cycles {
return Ok(OpResult::Ok);
}
}
}
if let Some(freq) = task_freq {
thread::sleep(freq);
}
})
}
/// This function allows executing multiple tasks as long as the tasks implement the
@ -86,55 +88,56 @@ pub fn exec_sched_single<
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks
/// * `op_code`: Operation code which is passed to the executable task [operation call][Executable::periodic_op]
/// * `termination`: Optional termination handler which can cancel threads with a broadcast
pub fn exec_sched_multi<
T: Executable<Error = E> + Send + 'static + ?Sized,
E: Error + Send + 'static,
>(
pub fn exec_sched_multi<T: Executable<Error = E> + Send + 'static + ?Sized, E: Send + 'static>(
task_name: &'static str,
mut executable_vec: Vec<Box<T>>,
task_freq: Option<Duration>,
op_code: i32,
mut termination: Option<BusReader<()>>,
) -> JoinHandle<Result<OpResult, E>> {
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
let mut cycle_counts = vec![0; executable_vec.len()];
let mut removal_flags = vec![false; executable_vec.len()];
thread::spawn(move || loop {
if let Some(ref mut terminator) = termination {
match terminator.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
removal_flags.iter_mut().for_each(|x| *x = true);
thread::Builder::new()
.name(String::from(task_name))
.spawn(move || loop {
if let Some(ref mut terminator) = termination {
match terminator.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
removal_flags.iter_mut().for_each(|x| *x = true);
}
Err(TryRecvError::Empty) => (),
}
Err(TryRecvError::Empty) => (),
}
}
for (idx, executable) in executable_vec.iter_mut().enumerate() {
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
removal_flags[idx] = true;
}
ExecutionType::Infinite => {
executable.periodic_op(op_code)?;
}
ExecutionType::Cycles(cycles) => {
executable.periodic_op(op_code)?;
cycle_counts[idx] += 1;
if cycle_counts[idx] == cycles {
for (idx, executable) in executable_vec.iter_mut().enumerate() {
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
removal_flags[idx] = true;
}
ExecutionType::Infinite => {
executable.periodic_op(op_code)?;
}
ExecutionType::Cycles(cycles) => {
executable.periodic_op(op_code)?;
cycle_counts[idx] += 1;
if cycle_counts[idx] == cycles {
removal_flags[idx] = true;
}
}
}
}
}
let mut removal_iter = removal_flags.iter();
executable_vec.retain(|_| !*removal_iter.next().unwrap());
removal_iter = removal_flags.iter();
cycle_counts.retain(|_| !*removal_iter.next().unwrap());
removal_flags.retain(|&i| !i);
if executable_vec.is_empty() {
return Ok(OpResult::Ok);
}
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
thread::sleep(freq);
})
let mut removal_iter = removal_flags.iter();
executable_vec.retain(|_| !*removal_iter.next().unwrap());
removal_iter = removal_flags.iter();
cycle_counts.retain(|_| !*removal_iter.next().unwrap());
removal_flags.retain(|&i| !i);
if executable_vec.is_empty() {
return Ok(OpResult::Ok);
}
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
thread::sleep(freq);
})
}
#[cfg(test)]
@ -294,7 +297,8 @@ mod tests {
Some(Duration::from_millis(100)),
expected_op_code,
None,
);
)
.expect("thread creation failed");
let thread_res = jhandle.join().expect("One Shot Task failed");
assert!(thread_res.is_ok());
assert_eq!(thread_res.unwrap(), OpResult::Ok);
@ -319,7 +323,8 @@ mod tests {
Some(Duration::from_millis(100)),
op_code_inducing_failure,
None,
);
)
.expect("thread creation failed");
let thread_res = jhandle.join().expect("One Shot Task failed");
assert!(thread_res.is_err());
let error = thread_res.unwrap_err();
@ -356,11 +361,13 @@ mod tests {
assert_eq!(task.task_name(), ONE_SHOT_TASK_NAME);
}
let jhandle = exec_sched_multi(
"multi-task-name",
task_vec,
Some(Duration::from_millis(100)),
expected_op_code,
None,
);
)
.expect("thread creation failed");
let thread_res = jhandle.join().expect("One Shot Task failed");
assert!(thread_res.is_ok());
assert_eq!(thread_res.unwrap(), OpResult::Ok);
@ -386,7 +393,8 @@ mod tests {
Some(Duration::from_millis(100)),
expected_op_code,
None,
);
)
.expect("thread creation failed");
let thread_res = jh.join().expect("Cycles Task failed");
assert!(thread_res.is_ok());
let data = shared.lock().expect("Locking Mutex failed");
@ -418,11 +426,13 @@ mod tests {
let task_vec: Vec<Box<dyn Executable<Error = ExampleError>>> =
vec![one_shot_task, cycled_task_0, cycled_task_1];
let jh = exec_sched_multi(
"multi-task-name",
task_vec,
Some(Duration::from_millis(100)),
expected_op_code,
None,
);
)
.expect("thread creation failed");
let thread_res = jh.join().expect("Cycles Task failed");
assert!(thread_res.is_ok());
let data = shared.lock().expect("Locking Mutex failed");
@ -449,7 +459,8 @@ mod tests {
Some(Duration::from_millis(20)),
expected_op_code,
Some(terminator.add_rx()),
);
)
.expect("thread creation failed");
thread::sleep(Duration::from_millis(40));
terminator.broadcast(());
let thread_res = jh.join().expect("Periodic Task failed");
@ -485,11 +496,13 @@ mod tests {
let task_vec: Vec<Box<dyn Executable<Error = ExampleError>>> =
vec![cycled_task, periodic_task_0, periodic_task_1];
let jh = exec_sched_multi(
"multi-task-name",
task_vec,
Some(Duration::from_millis(20)),
expected_op_code,
Some(terminator.add_rx()),
);
)
.expect("thread creation failed");
thread::sleep(Duration::from_millis(60));
terminator.broadcast(());
let thread_res = jh.join().expect("Periodic Task failed");

View File

@ -1,4 +1,3 @@
use alloc::boxed::Box;
use alloc::vec;
use cobs::encode;
use delegate::delegate;
@ -29,7 +28,6 @@ impl<TmError, TcError: 'static> TcpTcParser<TmError, TcError> for CobsTcParser {
current_write_idx: usize,
next_write_idx: &mut usize,
) -> Result<(), TcpTmtcError<TmError, TcError>> {
// Reader vec full, need to parse for packets.
conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets(
&mut tc_buffer[..current_write_idx],
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)
/// test also serves as the example application for this module.
pub struct TcpTmtcInCobsServer<TmError, TcError: 'static> {
generic_server: TcpTmtcGenericServer<TmError, TcError, CobsTmSender, CobsTcParser>,
pub struct TcpTmtcInCobsServer<
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
/// [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.
pub fn new(
cfg: ServerConfig,
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
) -> Result<Self, TcpTmtcError<TmError, TcError>> {
tm_source: TmSource,
tc_receiver: TcReceiver,
) -> Result<Self, std::io::Error> {
Ok(Self {
generic_server: TcpTmtcGenericServer::new(
cfg,
@ -177,7 +187,7 @@ mod tests {
ServerConfig,
},
};
use alloc::{boxed::Box, sync::Arc};
use alloc::sync::Arc;
use cobs::encode;
use super::TcpTmtcInCobsServer;
@ -202,11 +212,11 @@ mod tests {
addr: &SocketAddr,
tc_receiver: SyncTcCacher,
tm_source: SyncTmSource,
) -> TcpTmtcInCobsServer<(), ()> {
) -> TcpTmtcInCobsServer<(), (), SyncTmSource, SyncTcCacher> {
TcpTmtcInCobsServer::new(
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source),
Box::new(tc_receiver),
tm_source,
tc_receiver,
)
.expect("TCP server generation failed")
}

View File

@ -1,6 +1,6 @@
//! Generic TCP TMTC servers with different TMTC format flavours.
use alloc::vec;
use alloc::{boxed::Box, vec::Vec};
use alloc::vec::Vec;
use core::time::Duration;
use socket2::{Domain, Socket, Type};
use std::io::Read;
@ -134,20 +134,29 @@ pub trait TcpTmSender<TmError, TcError> {
pub struct TcpTmtcGenericServer<
TmError,
TcError,
TmHandler: TcpTmSender<TmError, TcError>,
TcHandler: TcpTcParser<TmError, TcError>,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
TmSender: TcpTmSender<TmError, TcError>,
TcParser: TcpTcParser<TmError, TcError>,
> {
base: TcpTmtcServerBase<TmError, TcError>,
tc_handler: TcHandler,
tm_handler: TmHandler,
pub(crate) listener: TcpListener,
pub(crate) inner_loop_delay: Duration,
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<
TmError: 'static,
TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
TmSender: TcpTmSender<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.
///
@ -165,25 +174,38 @@ impl<
cfg: ServerConfig,
tc_parser: TcParser,
tm_sender: TmSender,
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
) -> Result<TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>, std::io::Error> {
tm_source: TmSource,
tc_receiver: TcReceiver,
) -> 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 {
base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?,
tc_handler: tc_parser,
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.
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
/// useful if using the port number 0 for OS auto-assignment.
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
@ -205,20 +227,20 @@ impl<
let mut connection_result = ConnectionResult::default();
let mut current_write_idx;
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)?;
connection_result.addr = Some(addr);
current_write_idx = next_write_idx;
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 {
Ok(0) => {
// Connection closed by client. If any TC was read, parse for complete packets.
// After that, break the outer loop.
if current_write_idx > 0 {
self.tc_handler.handle_tc_parsing(
&mut self.base.tc_buffer,
self.base.tc_receiver.as_mut(),
&mut self.tc_buffer,
&mut self.tc_receiver,
&mut connection_result,
current_write_idx,
&mut next_write_idx,
@ -229,10 +251,10 @@ impl<
Ok(read_len) => {
current_write_idx += read_len;
// 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(
&mut self.base.tc_buffer,
self.base.tc_receiver.as_mut(),
&mut self.tc_buffer,
&mut self.tc_receiver,
&mut connection_result,
current_write_idx,
&mut next_write_idx,
@ -245,8 +267,8 @@ impl<
// both UNIX and Windows.
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
self.tc_handler.handle_tc_parsing(
&mut self.base.tc_buffer,
self.base.tc_receiver.as_mut(),
&mut self.tc_buffer,
&mut self.tc_receiver,
&mut connection_result,
current_write_idx,
&mut next_write_idx,
@ -254,14 +276,14 @@ impl<
current_write_idx = next_write_idx;
if !self.tm_handler.handle_tm_sending(
&mut self.base.tm_buffer,
self.base.tm_source.as_mut(),
&mut self.tm_buffer,
&mut self.tm_source,
&mut connection_result,
&mut stream,
)? {
// No TC read, no TM was sent, but the client has not disconnected.
// 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(
&mut self.base.tm_buffer,
self.base.tm_source.as_mut(),
&mut self.tm_buffer,
&mut self.tm_source,
&mut connection_result,
&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)]
pub(crate) mod tests {
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.
///
/// ## Example
///
/// 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.
pub struct TcpSpacepacketsServer<TmError, TcError: 'static> {
generic_server:
TcpTmtcGenericServer<TmError, TcError, SpacepacketsTmSender, SpacepacketsTcParser>,
pub struct TcpSpacepacketsServer<
TmError,
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> {
/// Create a new TCP TMTC server which exchanges CCSDS space packets.
impl<
TmError: 'static,
TcError: 'static,
TmSource: TmPacketSource<Error = TmError>,
TcReceiver: ReceivesTc<Error = TcError>,
> TcpSpacepacketsServer<TmError, TcError, TmSource, TcReceiver>
{
///
/// ## 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.
pub fn new(
cfg: ServerConfig,
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
tm_source: TmSource,
tc_receiver: TcReceiver,
packet_id_lookup: Box<dyn PacketIdLookup + Send>,
) -> Result<Self, TcpTmtcError<TmError, TcError>> {
) -> Result<Self, std::io::Error> {
Ok(Self {
generic_server: TcpTmtcGenericServer::new(
cfg,
@ -158,7 +173,7 @@ mod tests {
use alloc::{boxed::Box, sync::Arc};
use hashbrown::HashSet;
use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket},
ecss::{tc::PusTcCreator, WritablePusPacket},
PacketId, SpHeader,
};
@ -179,11 +194,11 @@ mod tests {
tc_receiver: SyncTcCacher,
tm_source: SyncTmSource,
packet_id_lookup: HashSet<PacketId>,
) -> TcpSpacepacketsServer<(), ()> {
) -> TcpSpacepacketsServer<(), (), SyncTmSource, SyncTcCacher> {
TcpSpacepacketsServer::new(
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
Box::new(tm_source),
Box::new(tc_receiver),
tm_source,
tc_receiver,
Box::new(packet_id_lookup),
)
.expect("TCP server generation failed")

View File

@ -19,7 +19,7 @@ use std::vec::Vec;
///
/// ```
/// 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::tmtc::{ReceivesTc, ReceivesTcCore};
/// use spacepackets::SpHeader;
@ -144,7 +144,7 @@ mod tests {
use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use crate::tmtc::ReceivesTcCore;
use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::SerializablePusPacket;
use spacepackets::ecss::WritablePusPacket;
use spacepackets::SpHeader;
use std::boxed::Box;
use std::collections::VecDeque;

View File

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

View File

@ -1,23 +1,14 @@
//! # 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
//! 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
//! # Example for the [StaticMemoryPool]
//!
//! ```
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider};
//! use satrs_core::pool::{PoolProvider, StaticMemoryPool, StaticPoolConfig};
//!
//! // 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 mut local_pool = LocalPool::new(pool_cfg);
//! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
//! let mut local_pool = StaticMemoryPool::new(pool_cfg);
//! let mut read_buf: [u8; 16] = [0; 16];
//! let mut addr;
//! {
//! // Add new data to the pool
@ -30,25 +21,25 @@
//!
//! {
//! // Read the store data back
//! let res = local_pool.read(&addr);
//! let res = local_pool.read(&addr, &mut read_buf);
//! assert!(res.is_ok());
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 4);
//! assert_eq!(buf_read_back[0], 42);
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 4);
//! assert_eq!(read_buf[0], 42);
//! // Modify the stored data
//! let res = local_pool.modify(&addr);
//! let res = local_pool.modify(&addr, |buf| {
//! buf[0] = 12;
//! });
//! assert!(res.is_ok());
//! let buf_read_back = res.unwrap();
//! buf_read_back[0] = 12;
//! }
//!
//! {
//! // Read the modified data back
//! let res = local_pool.read(&addr);
//! let res = local_pool.read(&addr, &mut read_buf);
//! assert!(res.is_ok());
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 4);
//! assert_eq!(buf_read_back[0], 12);
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 4);
//! assert_eq!(read_buf[0], 12);
//! }
//!
//! // Delete the stored data
@ -56,43 +47,46 @@
//!
//! // Get a free element in the pool with an appropriate size
//! {
//! let res = local_pool.free_element(12);
//! let res = local_pool.free_element(12, |buf| {
//! buf[0] = 7;
//! });
//! assert!(res.is_ok());
//! let (tmp, mut_buf) = res.unwrap();
//! addr = tmp;
//! mut_buf[0] = 7;
//! addr = res.unwrap();
//! }
//!
//! // Read back the data
//! {
//! // Read the store data back
//! let res = local_pool.read(&addr);
//! let res = local_pool.read(&addr, &mut read_buf);
//! assert!(res.is_ok());
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 12);
//! assert_eq!(buf_read_back[0], 7);
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 12);
//! assert_eq!(read_buf[0], 7);
//! }
//! ```
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub use alloc_mod::*;
use core::fmt::{Display, Formatter};
use delegate::delegate;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
type NumBlocks = u16;
pub type StoreAddr = u64;
/// Simple address type used for transactions with the local pool.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreAddr {
pub struct StaticPoolAddr {
pub(crate) pool_idx: u16,
pub(crate) packet_idx: NumBlocks,
}
impl StoreAddr {
impl StaticPoolAddr {
pub const INVALID_ADDR: u32 = 0xFFFFFFFF;
pub fn raw(&self) -> u32 {
@ -100,7 +94,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 {
write!(
f,
@ -144,6 +153,8 @@ pub enum StoreError {
InvalidStoreId(StoreIdError, Option<StoreAddr>),
/// Valid subpool and packet index, but no data is stored at the given address
DataDoesNotExist(StoreAddr),
ByteConversionError(spacepackets::ByteConversionError),
LockError,
/// Internal or configuration errors
InternalError(u32),
}
@ -166,10 +177,22 @@ impl Display for StoreError {
StoreError::InternalError(e) => {
write!(f, "internal error: {e}")
}
StoreError::ByteConversionError(e) => {
write!(f, "store error: {e}")
}
StoreError::LockError => {
write!(f, "lock error")
}
}
}
}
impl From<ByteConversionError> for StoreError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversionError(value)
}
}
#[cfg(feature = "std")]
impl Error for StoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
@ -180,39 +203,176 @@ 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 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 first.
/// It then executes a user-provided closure and passes a mutable reference to that memory
/// block to the closure. This allows the user to write data to the memory block.
/// The function should yield a [StoreAddr] which can be used to access the data stored in the
/// pool.
fn free_element<W: FnMut(&mut [u8])>(
&mut self,
len: usize,
writer: W,
) -> Result<StoreAddr, StoreError>;
/// Modify data added previously using a given [StoreAddr]. The provider should use the store
/// address to determine if a memory block exists for that address. If it does, it should
/// call the user-provided closure and pass a mutable reference to the memory block
/// to the closure. This allows the user to modify the memory block.
fn modify<U: FnMut(&mut [u8])>(
&mut self,
addr: &StoreAddr,
updater: U,
) -> Result<(), StoreError>;
/// The provider should copy the data from the memory block to the user-provided buffer if
/// it exists.
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, 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>;
#[cfg(feature = "alloc")]
fn read_as_vec(&self, addr: &StoreAddr) -> Result<alloc::vec::Vec<u8>, StoreError> {
let mut vec = alloc::vec![0; self.len_of_data(addr)?];
self.read(addr, &mut vec)?;
Ok(vec)
}
}
pub trait PoolProviderWithGuards: PoolProvider {
/// This function behaves like [PoolProvider::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 [PoolProvider::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: PoolProvider + ?Sized> {
pool: &'a mut MemProvider,
pub addr: StoreAddr,
no_deletion: bool,
deletion_failed_error: Option<StoreError>,
}
/// This helper object
impl<'a, MemProvider: PoolProvider> 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, buf: &mut [u8]) -> Result<usize, StoreError> {
self.pool.read(&self.addr, buf)
}
#[cfg(feature = "alloc")]
pub fn read_as_vec(&self) -> Result<alloc::vec::Vec<u8>, StoreError> {
self.pool.read_as_vec(&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: PoolProvider + ?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: PoolProvider + ?Sized> {
guard: PoolGuard<'a, MemProvider>,
}
impl<'a, MemProvider: PoolProvider> PoolRwGuard<'a, MemProvider> {
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
Self {
guard: PoolGuard::new(pool, addr),
}
}
pub fn update<U: FnMut(&mut [u8])>(&mut self, updater: &mut U) -> Result<(), StoreError> {
self.guard.pool.modify(&self.guard.addr, updater)
}
delegate!(
to self.guard {
pub fn read(&self, buf: &mut [u8]) -> Result<usize, 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")]
mod alloc_mod {
use super::{PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticPoolAddr};
use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError};
use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use delegate::delegate;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::sync::{Arc, RwLock};
#[cfg(feature = "std")]
pub type ShareablePoolProvider = Box<dyn PoolProvider + Send + Sync>;
#[cfg(feature = "std")]
pub type SharedPool = Arc<RwLock<ShareablePoolProvider>>;
pub type SharedStaticMemoryPool = Arc<RwLock<StaticMemoryPool>>;
type PoolSize = usize;
const STORE_FREE: PoolSize = PoolSize::MAX;
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
///
/// * `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
#[derive(Clone)]
pub struct PoolCfg {
pub struct StaticPoolConfig {
cfg: Vec<(NumBlocks, usize)>,
}
impl PoolCfg {
impl StaticPoolConfig {
pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self {
PoolCfg { cfg }
StaticPoolConfig { cfg }
}
pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> {
@ -228,135 +388,30 @@ mod alloc_mod {
}
}
pub struct PoolGuard<'a> {
pool: &'a mut LocalPool,
pub addr: StoreAddr,
no_deletion: bool,
deletion_failed_error: Option<StoreError>,
}
/// This helper object
impl<'a> PoolGuard<'a> {
pub fn new(pool: &'a mut LocalPool, 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 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 implementation providing sub-pools with fixed size memory blocks.
///
/// This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool
/// 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][StaticMemoryPool] are done using a generic
/// [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.
pub struct StaticMemoryPool {
pool_cfg: StaticPoolConfig,
pool: Vec<Vec<u8>>,
sizes_lists: Vec<Vec<PoolSize>>,
}
impl LocalPool {
/// Create a new local pool from the [given configuration][PoolCfg]. This function will sanitize
/// the given configuration as well.
pub fn new(mut cfg: PoolCfg) -> LocalPool {
impl StaticMemoryPool {
/// Create a new local pool from the [given configuration][StaticPoolConfig]. This function
/// will sanitize the given configuration as well.
pub fn new(mut cfg: StaticPoolConfig) -> StaticMemoryPool {
let subpools_num = cfg.sanitize();
let mut local_pool = LocalPool {
let mut local_pool = StaticMemoryPool {
pool_cfg: cfg,
pool: Vec::with_capacity(subpools_num),
sizes_lists: Vec::with_capacity(subpools_num),
@ -372,39 +427,39 @@ mod alloc_mod {
local_pool
}
fn addr_check(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
fn addr_check(&self, addr: &StaticPoolAddr) -> Result<usize, StoreError> {
self.validate_addr(addr)?;
let pool_idx = addr.pool_idx as usize;
let size_list = self.sizes_lists.get(pool_idx).unwrap();
let curr_size = size_list[addr.packet_idx as usize];
if curr_size == STORE_FREE {
return Err(StoreError::DataDoesNotExist(*addr));
return Err(StoreError::DataDoesNotExist(StoreAddr::from(*addr)));
}
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;
if pool_idx >= self.pool_cfg.cfg.len() {
return Err(StoreError::InvalidStoreId(
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 {
return Err(StoreError::InvalidStoreId(
StoreIdError::InvalidPacketIdx(addr.packet_idx),
Some(*addr),
Some(StoreAddr::from(*addr)),
));
}
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 (slot, size_slot_ref) = self.find_empty(subpool_idx)?;
*size_slot_ref = data_len;
Ok(StoreAddr {
Ok(StaticPoolAddr {
pool_idx: subpool_idx,
packet_idx: slot,
})
@ -422,7 +477,7 @@ mod alloc_mod {
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 subpool = self
.pool
@ -449,13 +504,13 @@ mod alloc_mod {
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)?;
Some(addr.packet_idx as usize * size)
}
}
impl PoolProvider for LocalPool {
impl PoolProvider for StaticMemoryPool {
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError> {
let data_len = data.len();
if data_len > POOL_MAX_SIZE {
@ -463,10 +518,14 @@ mod alloc_mod {
}
let addr = self.reserve(data_len)?;
self.write(&addr, data)?;
Ok(addr)
Ok(addr.into())
}
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> {
fn free_element<W: FnMut(&mut [u8])>(
&mut self,
len: usize,
mut writer: W,
) -> Result<StoreAddr, StoreError> {
if len > POOL_MAX_SIZE {
return Err(StoreError::DataTooLarge(len));
}
@ -474,34 +533,44 @@ mod alloc_mod {
let raw_pos = self.raw_pos(&addr).unwrap();
let block =
&mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len];
Ok((addr, block))
writer(block);
Ok(addr.into())
}
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> {
let curr_size = self.addr_check(addr)?;
let raw_pos = self.raw_pos(addr).unwrap();
fn modify<U: FnMut(&mut [u8])>(
&mut self,
addr: &StoreAddr,
mut updater: U,
) -> Result<(), StoreError> {
let addr = StaticPoolAddr::from(*addr);
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()
[raw_pos..raw_pos + curr_size];
Ok(block)
updater(block);
Ok(())
}
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard {
PoolRwGuard::new(self, addr)
}
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> {
let curr_size = self.addr_check(addr)?;
let raw_pos = self.raw_pos(addr).unwrap();
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, StoreError> {
let addr = StaticPoolAddr::from(*addr);
let curr_size = self.addr_check(&addr)?;
if buf.len() < curr_size {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: curr_size,
}
.into());
}
let raw_pos = self.raw_pos(&addr).unwrap();
let block =
&self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size];
Ok(block)
}
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard {
PoolGuard::new(self, addr)
//block.copy_from_slice(&src);
buf[..curr_size].copy_from_slice(block);
Ok(curr_size)
}
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> {
let addr = StaticPoolAddr::from(addr);
self.addr_check(&addr)?;
let block_size = self.pool_cfg.cfg.get(addr.pool_idx as usize).unwrap().1;
let raw_pos = self.raw_pos(&addr).unwrap();
@ -514,7 +583,8 @@ mod alloc_mod {
}
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 size_list = self.sizes_lists.get(pool_idx).unwrap();
let curr_size = size_list[addr.packet_idx as usize];
@ -523,35 +593,57 @@ mod alloc_mod {
}
Ok(true)
}
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
let addr = StaticPoolAddr::from(*addr);
self.validate_addr(&addr)?;
let pool_idx = addr.pool_idx as usize;
let size_list = self.sizes_lists.get(pool_idx).unwrap();
let size = size_list[addr.packet_idx as usize];
Ok(match size {
STORE_FREE => 0,
_ => size,
})
}
}
impl PoolProviderWithGuards 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)]
mod tests {
use crate::pool::{
LocalPool, PoolCfg, PoolGuard, PoolProvider, PoolRwGuard, StoreAddr, StoreError,
StoreIdError, POOL_MAX_SIZE,
PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticMemoryPool,
StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError, POOL_MAX_SIZE,
};
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
let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]);
LocalPool::new(pool_cfg)
let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
StaticMemoryPool::new(pool_cfg)
}
#[test]
fn test_cfg() {
// 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();
assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]);
// 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();
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
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();
assert!(
*pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)]
@ -566,13 +658,14 @@ mod tests {
for (i, val) in test_buf.iter_mut().enumerate() {
*val = i as u8;
}
let mut other_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
// Read back data and verify correctness
let res = local_pool.read(&addr);
let res = local_pool.read(&addr, &mut other_buf);
assert!(res.is_ok());
let buf_read_back = res.unwrap();
assert_eq!(buf_read_back.len(), 16);
for (i, &val) in buf_read_back.iter().enumerate() {
let read_len = res.unwrap();
assert_eq!(read_len, 16);
for (i, &val) in other_buf.iter().enumerate() {
assert_eq!(val, i as u8);
}
}
@ -582,8 +675,10 @@ mod tests {
let mut local_pool = basic_small_pool();
let test_buf: [u8; 12] = [0; 12];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let res = local_pool.read(&addr).expect("Read back failed");
assert_eq!(res.len(), 12);
let res = local_pool
.read(&addr, &mut [0; 12])
.expect("Read back failed");
assert_eq!(res, 12);
}
#[test]
@ -594,18 +689,20 @@ mod tests {
// Delete the data
let res = local_pool.delete(addr);
assert!(res.is_ok());
let mut writer = |buf: &mut [u8]| {
assert_eq!(buf.len(), 12);
};
// Verify that the slot is free by trying to get a reference to it
let res = local_pool.free_element(12);
let res = local_pool.free_element(12, &mut writer);
assert!(res.is_ok());
let (addr, buf_ref) = res.unwrap();
let addr = res.unwrap();
assert_eq!(
addr,
StoreAddr {
u64::from(StaticPoolAddr {
pool_idx: 2,
packet_idx: 0
}
})
);
assert_eq!(buf_ref.len(), 12);
}
#[test]
@ -619,29 +716,34 @@ mod tests {
{
// Verify that the slot is free by trying to get a reference to it
let res = local_pool.modify(&addr).expect("Modifying data failed");
res[0] = 0;
res[1] = 0x42;
local_pool
.modify(&addr, &mut |buf: &mut [u8]| {
buf[0] = 0;
buf[1] = 0x42;
})
.expect("Modifying data failed");
}
let res = local_pool.read(&addr).expect("Reading back data failed");
assert_eq!(res[0], 0);
assert_eq!(res[1], 0x42);
assert_eq!(res[2], 2);
assert_eq!(res[3], 3);
local_pool
.read(&addr, &mut test_buf)
.expect("Reading back data failed");
assert_eq!(test_buf[0], 0);
assert_eq!(test_buf[1], 0x42);
assert_eq!(test_buf[2], 2);
assert_eq!(test_buf[3], 3);
}
#[test]
fn test_consecutive_reservation() {
let mut local_pool = basic_small_pool();
// Reserve two smaller blocks consecutively and verify that the third reservation fails
let res = local_pool.free_element(8);
let res = local_pool.free_element(8, |_| {});
assert!(res.is_ok());
let (addr0, _) = res.unwrap();
let res = local_pool.free_element(8);
let addr0 = res.unwrap();
let res = local_pool.free_element(8, |_| {});
assert!(res.is_ok());
let (addr1, _) = res.unwrap();
let res = local_pool.free_element(8);
let addr1 = res.unwrap();
let res = local_pool.free_element(8, |_| {});
assert!(res.is_err());
let err = res.unwrap_err();
assert_eq!(err, StoreError::StoreFull(1));
@ -655,10 +757,14 @@ mod tests {
fn test_read_does_not_exist() {
let local_pool = basic_small_pool();
// Try to access data which does not exist
let res = local_pool.read(&StoreAddr {
packet_idx: 0,
pool_idx: 0,
});
let res = local_pool.read(
&StaticPoolAddr {
packet_idx: 0,
pool_idx: 0,
}
.into(),
&mut [],
);
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
@ -684,11 +790,12 @@ mod tests {
#[test]
fn test_invalid_pool_idx() {
let local_pool = basic_small_pool();
let addr = StoreAddr {
let addr = StaticPoolAddr {
pool_idx: 3,
packet_idx: 0,
};
let res = local_pool.read(&addr);
}
.into();
let res = local_pool.read(&addr, &mut []);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(matches!(
@ -700,12 +807,12 @@ mod tests {
#[test]
fn test_invalid_packet_idx() {
let local_pool = basic_small_pool();
let addr = StoreAddr {
let addr = StaticPoolAddr {
pool_idx: 2,
packet_idx: 1,
};
assert_eq!(addr.raw(), 0x00020001);
let res = local_pool.read(&addr);
let res = local_pool.read(&addr.into(), &mut []);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(matches!(
@ -727,7 +834,7 @@ mod tests {
#[test]
fn test_data_too_large_1() {
let mut local_pool = basic_small_pool();
let res = local_pool.free_element(POOL_MAX_SIZE + 1);
let res = local_pool.free_element(POOL_MAX_SIZE + 1, |_| {});
assert!(res.is_err());
assert_eq!(
res.unwrap_err(),
@ -739,7 +846,7 @@ mod tests {
fn test_free_element_too_large() {
let mut local_pool = basic_small_pool();
// Try to request a slot which is too large
let res = local_pool.free_element(20);
let res = local_pool.free_element(20, |_| {});
assert!(res.is_err());
assert_eq!(res.unwrap_err(), StoreError::DataTooLarge(20));
}
@ -781,7 +888,7 @@ mod tests {
let test_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let mut rw_guard = PoolRwGuard::new(&mut local_pool, addr);
let _ = rw_guard.modify().expect("modify failed");
rw_guard.update(&mut |_| {}).expect("modify failed");
drop(rw_guard);
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
}
@ -792,7 +899,7 @@ mod tests {
let test_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let mut rw_guard = local_pool.modify_with_guard(addr);
let _ = rw_guard.modify().expect("modify failed");
rw_guard.update(&mut |_| {}).expect("modify failed");
drop(rw_guard);
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
}
@ -808,13 +915,25 @@ mod tests {
let addr1 = local_pool.add(&test_buf_1).expect("Adding data failed");
let addr2 = local_pool.add(&test_buf_2).expect("Adding data failed");
let addr3 = local_pool.add(&test_buf_3).expect("Adding data failed");
let tm0_raw = local_pool.modify(&addr0).expect("Modifying data failed");
assert_eq!(tm0_raw, test_buf_0);
let tm1_raw = local_pool.modify(&addr1).expect("Modifying data failed");
assert_eq!(tm1_raw, test_buf_1);
let tm2_raw = local_pool.modify(&addr2).expect("Modifying data failed");
assert_eq!(tm2_raw, test_buf_2);
let tm3_raw = local_pool.modify(&addr3).expect("Modifying data failed");
assert_eq!(tm3_raw, test_buf_3);
local_pool
.modify(&addr0, |buf| {
assert_eq!(buf, test_buf_0);
})
.expect("Modifying data failed");
local_pool
.modify(&addr1, |buf| {
assert_eq!(buf, test_buf_1);
})
.expect("Modifying data failed");
local_pool
.modify(&addr2, |buf| {
assert_eq!(buf, test_buf_2);
})
.expect("Modifying data failed");
local_pool
.modify(&addr3, |buf| {
assert_eq!(buf, test_buf_3);
})
.expect("Modifying data failed");
}
}

View File

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

View File

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

View File

@ -1,85 +1,64 @@
use crate::events::EventU32;
use crate::pool::{SharedPool, StoreAddr};
use crate::pus::event_man::{EventRequest, EventRequestWithToken};
use crate::pus::verification::{
StdVerifReporterWithSender, TcStateAccepted, TcStateToken, VerificationToken,
};
use crate::pus::{
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceBase, PusServiceHandler,
};
use alloc::boxed::Box;
use crate::pus::verification::TcStateToken;
use crate::pus::{PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError};
use spacepackets::ecss::event::Subservice;
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::PusPacket;
use std::sync::mpsc::Sender;
pub struct PusService5EventHandler {
psb: PusServiceBase,
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
pub struct PusService5EventHandler<TcInMemConverter: EcssTcInMemConverter> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
event_request_tx: Sender<EventRequestWithToken>,
}
impl PusService5EventHandler {
impl<TcInMemConverter: EcssTcInMemConverter> PusService5EventHandler<TcInMemConverter> {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_store: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
service_handler: PusServiceHelper<TcInMemConverter>,
event_request_tx: Sender<EventRequestWithToken>,
) -> Self {
Self {
psb: PusServiceBase::new(
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
service_helper: service_handler,
event_request_tx,
}
}
}
impl PusServiceHandler for PusService5EventHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase {
&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)?;
pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
}
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 = tc.subservice();
let srv = Subservice::try_from(subservice);
if srv.is_err() {
return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
token,
ecss_tc_and_token.token,
));
}
let handle_enable_disable_request = |enable: bool, stamp: [u8; 7]| {
if tc.user_data().len() < 4 {
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 event_u32 = EventU32::from(u32::from_be_bytes(user_data[0..4].try_into().unwrap()));
let start_token = self
.psb
.service_helper
.common
.verification_handler
.borrow_mut()
.start_success(token, Some(&stamp))
.start_success(ecss_tc_and_token.token, Some(&stamp))
.map_err(|_| PartialPusHandlingError::Verification);
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 {
token = start_token.into();
}
@ -107,7 +86,7 @@ impl PusServiceHandler for PusService5EventHandler {
Ok(PusPacketHandlerResult::RequestHandled)
};
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() {
Subservice::TmInfoReport
| Subservice::TmLowSeverityReport
@ -123,7 +102,8 @@ impl PusServiceHandler for PusService5EventHandler {
}
Subservice::TcReportDisabledList | Subservice::TmDisabledEventsReport => {
return Ok(PusPacketHandlerResult::SubserviceNotImplemented(
subservice, token,
subservice,
ecss_tc_and_token.token,
));
}
}
@ -131,3 +111,170 @@ impl PusServiceHandler for PusService5EventHandler {
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.
#[derive(Debug, Copy, Clone)]
pub enum GenericSendError {
@ -200,11 +194,75 @@ pub trait EcssTcSenderCore: EcssChannel {
fn send_tc(&self, tc: PusTcCreator, token: Option<TcStateToken>) -> Result<(), EcssTmtcError>;
}
pub struct ReceivedTcWrapper {
pub store_addr: StoreAddr,
/// A PUS telecommand packet can be stored in memory using different methods. Right now,
/// 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>,
}
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)]
pub enum TryRecvTmtcError {
Error(EcssTmtcError),
@ -231,7 +289,7 @@ impl From<StoreError> for TryRecvTmtcError {
/// Generic trait for a user supplied receiver object.
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
@ -260,10 +318,31 @@ mod alloc_mod {
/// [Clone].
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub trait EcssTmSender: EcssTmSenderCore + Downcast + DynClone {}
pub trait EcssTmSender: EcssTmSenderCore + Downcast + DynClone {
// Remove this once trait upcasting coercion has been implemented.
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
fn upcast(&self) -> &dyn EcssTmSenderCore;
// Remove this once trait upcasting coercion has been implemented.
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
fn upcast_mut(&mut self) -> &mut dyn EcssTmSenderCore;
}
/// Blanket implementation for all types which implement [EcssTmSenderCore] and are clonable.
impl<T> EcssTmSender for T where T: EcssTmSenderCore + Clone + 'static {}
impl<T> EcssTmSender for T
where
T: EcssTmSenderCore + Clone + 'static,
{
// Remove this once trait upcasting coercion has been implemented.
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
fn upcast(&self) -> &dyn EcssTmSenderCore {
self
}
// Remove this once trait upcasting coercion has been implemented.
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
fn upcast_mut(&mut self) -> &mut dyn EcssTmSenderCore {
self
}
}
dyn_clone::clone_trait_object!(EcssTmSender);
impl_downcast!(EcssTmSender);
@ -309,21 +388,23 @@ mod alloc_mod {
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod std_mod {
use crate::pool::{SharedPool, StoreAddr};
use crate::pool::{PoolProvider, PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr};
use crate::pus::verification::{
StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
};
use crate::pus::{
EcssChannel, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender, EcssTmSenderCore,
EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper, ReceivedTcWrapper,
TcAddrWithToken, TryRecvTmtcError,
EcssChannel, EcssTcAndToken, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender,
EcssTmSenderCore, EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper,
TryRecvTmtcError,
};
use crate::tmtc::tm_helper::SharedTmStore;
use crate::tmtc::tm_helper::SharedTmPool;
use crate::ChannelId;
use alloc::boxed::Box;
use alloc::vec::Vec;
use crossbeam_channel as cb;
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::tm::PusTmCreator;
use spacepackets::ecss::PusError;
use spacepackets::time::cds::TimeProvider;
@ -335,6 +416,9 @@ pub mod std_mod {
use std::sync::mpsc::TryRecvError;
use thiserror::Error;
use super::verification::VerificationReporterWithSender;
use super::{AcceptedEcssTcAndToken, TcInMemory};
impl From<mpsc::SendError<StoreAddr>> for EcssTmtcError {
fn from(_: mpsc::SendError<StoreAddr>) -> Self {
Self::Send(GenericSendError::RxDisconnected)
@ -357,14 +441,14 @@ pub mod std_mod {
}
#[derive(Clone)]
pub struct MpscTmInStoreSender {
pub struct MpscTmInSharedPoolSender {
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmStore,
shared_tm_store: SharedTmPool,
sender: mpsc::Sender<StoreAddr>,
}
impl EcssChannel for MpscTmInStoreSender {
impl EcssChannel for MpscTmInSharedPoolSender {
fn id(&self) -> ChannelId {
self.id
}
@ -374,7 +458,7 @@ pub mod std_mod {
}
}
impl MpscTmInStoreSender {
impl MpscTmInSharedPoolSender {
pub fn send_direct_tm(&self, tm: PusTmCreator) -> Result<(), EcssTmtcError> {
let addr = self.shared_tm_store.add_pus_tm(&tm)?;
self.sender
@ -383,7 +467,7 @@ pub mod std_mod {
}
}
impl EcssTmSenderCore for MpscTmInStoreSender {
impl EcssTmSenderCore for MpscTmInSharedPoolSender {
fn send_tm(&self, tm: PusTmWrapper) -> Result<(), EcssTmtcError> {
match tm {
PusTmWrapper::InStore(addr) => {
@ -395,11 +479,11 @@ pub mod std_mod {
}
}
impl MpscTmInStoreSender {
impl MpscTmInSharedPoolSender {
pub fn new(
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmStore,
shared_tm_store: SharedTmPool,
sender: mpsc::Sender<StoreAddr>,
) -> Self {
Self {
@ -411,13 +495,13 @@ pub mod std_mod {
}
}
pub struct MpscTcInStoreReceiver {
pub struct MpscTcReceiver {
id: ChannelId,
name: &'static str,
receiver: mpsc::Receiver<TcAddrWithToken>,
receiver: mpsc::Receiver<EcssTcAndToken>,
}
impl EcssChannel for MpscTcInStoreReceiver {
impl EcssChannel for MpscTcReceiver {
fn id(&self) -> ChannelId {
self.id
}
@ -427,26 +511,22 @@ pub mod std_mod {
}
}
impl EcssTcReceiverCore for MpscTcInStoreReceiver {
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> {
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e {
impl EcssTcReceiverCore for MpscTcReceiver {
fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
self.receiver.try_recv().map_err(|e| match e {
TryRecvError::Empty => TryRecvTmtcError::Empty,
TryRecvError::Disconnected => {
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
}
})?;
Ok(ReceivedTcWrapper {
store_addr,
token: Some(token),
})
}
}
impl MpscTcInStoreReceiver {
impl MpscTcReceiver {
pub fn new(
id: ChannelId,
name: &'static str,
receiver: mpsc::Receiver<TcAddrWithToken>,
receiver: mpsc::Receiver<EcssTcAndToken>,
) -> Self {
Self { id, name, receiver }
}
@ -459,8 +539,8 @@ pub mod std_mod {
#[derive(Clone)]
pub struct MpscTmAsVecSender {
id: ChannelId,
sender: mpsc::Sender<Vec<u8>>,
name: &'static str,
sender: mpsc::Sender<Vec<u8>>,
}
impl From<mpsc::SendError<Vec<u8>>> for EcssTmtcError {
@ -502,7 +582,7 @@ pub mod std_mod {
pub struct CrossbeamTmInStoreSender {
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmStore,
shared_tm_store: SharedTmPool,
sender: crossbeam_channel::Sender<StoreAddr>,
}
@ -510,7 +590,7 @@ pub mod std_mod {
pub fn new(
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmStore,
shared_tm_store: SharedTmPool,
sender: crossbeam_channel::Sender<StoreAddr>,
) -> Self {
Self {
@ -545,23 +625,23 @@ pub mod std_mod {
}
}
pub struct CrossbeamTcInStoreReceiver {
pub struct CrossbeamTcReceiver {
id: ChannelId,
name: &'static str,
receiver: cb::Receiver<TcAddrWithToken>,
receiver: cb::Receiver<EcssTcAndToken>,
}
impl CrossbeamTcInStoreReceiver {
impl CrossbeamTcReceiver {
pub fn new(
id: ChannelId,
name: &'static str,
receiver: cb::Receiver<TcAddrWithToken>,
receiver: cb::Receiver<EcssTcAndToken>,
) -> Self {
Self { id, name, receiver }
}
}
impl EcssChannel for CrossbeamTcInStoreReceiver {
impl EcssChannel for CrossbeamTcReceiver {
fn id(&self) -> ChannelId {
self.id
}
@ -571,17 +651,13 @@ pub mod std_mod {
}
}
impl EcssTcReceiverCore for CrossbeamTcInStoreReceiver {
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> {
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e {
impl EcssTcReceiverCore for CrossbeamTcReceiver {
fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
self.receiver.try_recv().map_err(|e| match e {
cb::TryRecvError::Empty => TryRecvTmtcError::Empty,
cb::TryRecvError::Disconnected => {
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
}
})?;
Ok(ReceivedTcWrapper {
store_addr,
token: Some(token),
})
}
}
@ -596,8 +672,12 @@ pub mod std_mod {
InvalidSubservice(u8),
#[error("not enough application data available: {0}")]
NotEnoughAppData(String),
#[error("PUS packet too large, does not fit in buffer: {0}")]
PusPacketTooLarge(usize),
#[error("invalid application data")]
InvalidAppData(String),
#[error("invalid format of TC in memory: {0:?}")]
InvalidTcInMemoryFormat(TcInMemory),
#[error("generic ECSS tmtc error: {0}")]
EcssTmtc(#[from] EcssTmtcError),
#[error("invalid verification token")]
@ -634,42 +714,129 @@ pub mod std_mod {
}
}
/// Base class for handlers which can handle PUS TC packets. Right now, the verification
/// reporter is constrained to the [StdVerifReporterWithSender] and the service handler
/// relies on TMTC packets being exchanged via a [SharedPool].
pub trait EcssTcInMemConverter {
fn cache_ecss_tc_in_memory(
&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_size = tc_pool
.len_of_data(&addr)
.map_err(|e| PusPacketHandlingError::EcssTmtc(EcssTmtcError::Store(e)))?;
if tc_size > self.pus_buf.len() {
return Err(PusPacketHandlingError::PusPacketTooLarge(tc_size));
}
let tc_guard = tc_pool.read_with_guard(addr);
// TODO: Proper error handling.
tc_guard.read(&mut self.pus_buf[0..tc_size]).unwrap();
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 tc_receiver: Box<dyn EcssTcReceiver>,
pub shared_tc_store: SharedPool,
pub tm_sender: Box<dyn EcssTmSender>,
pub tm_apid: u16,
/// 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.
pub verification_handler: RefCell<StdVerifReporterWithSender>,
pub pus_buf: [u8; 2048],
pub pus_size: usize,
}
impl PusServiceBase {
pub fn new(
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,
}
}
#[cfg(feature = "std")]
pub fn get_current_timestamp(
&self,
partial_error: &mut Option<PartialPusHandlingError>,
) -> [u8; 7] {
let mut time_stamp: [u8; 7] = [0; 7];
@ -684,48 +851,73 @@ pub mod std_mod {
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;
self.get_current_timestamp(&mut dummy)
Self::get_current_timestamp(&mut dummy)
}
}
pub trait PusServiceHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase;
fn psb(&self) -> &PusServiceBase;
fn handle_one_tc(
&mut self,
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
/// This is a high-level PUS packet handler helper.
///
/// It performs some of the boilerplate acitivities involved when handling PUS telecommands and
/// it can be used to implement the handling of PUS telecommands for certain PUS telecommands
/// groups (for example individual services).
///
/// This base class can handle PUS telecommands backed by different memory storage machanisms
/// 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> {
// Keep locked section as short as possible.
let psb_mut = self.psb_mut();
let mut tc_pool = psb_mut
.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();
psb_mut.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw);
Ok(())
impl<TcInMemConverter: EcssTcInMemConverter> PusServiceHelper<TcInMemConverter> {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: VerificationReporterWithSender,
tc_in_mem_converter: TcInMemConverter,
) -> Self {
Self {
common: PusServiceBase {
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> {
match self.psb().tc_receiver.recv_tc() {
Ok(ReceivedTcWrapper { store_addr, token }) => {
/// This function can be used to poll the internal [EcssTcReceiver] object for the next
/// telecommand packet. It will return `Ok(None)` if there are not packets available.
/// 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() {
return Err(PusPacketHandlingError::InvalidVerificationToken);
}
let token = token.unwrap();
let accepted_token = VerificationToken::<TcStateAccepted>::try_from(token)
.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 {
TryRecvTmtcError::Error(e) => Err(PusPacketHandlingError::EcssTmtc(e)),
TryRecvTmtcError::Empty => Ok(PusPacketHandlerResult::Empty),
TryRecvTmtcError::Empty => Ok(None),
},
}
}
@ -746,10 +938,34 @@ pub(crate) fn source_buffer_large_enough(cap: usize, len: usize) -> Result<(), E
}
#[cfg(test)]
pub(crate) mod tests {
use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator};
pub mod tests {
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 crate::pool::{
PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig, StoreAddr,
};
use crate::pus::verification::RequestId;
use crate::tmtc::tm_helper::SharedTmPool;
use super::verification::{
TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
};
use super::{
EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver,
MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceHelper, TcInMemory,
};
pub const TEST_APID: u16 = 0x101;
#[derive(Debug, Eq, PartialEq, Clone)]
pub(crate) struct CommonTmInfo {
pub subservice: u8,
@ -759,12 +975,23 @@ pub(crate) mod tests {
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 {
pub fn new_from_tm(tm: &PusTmCreator) -> Self {
let mut time_stamp = [0; 7];
time_stamp.clone_from_slice(&tm.timestamp()[0..7]);
Self {
subservice: tm.subservice(),
subservice: PusPacket::subservice(tm),
apid: tm.apid(),
msg_counter: tm.msg_counter(),
dest_id: tm.dest_id(),
@ -772,4 +999,196 @@ 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: SharedTmPool,
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 = SharedTmPool::new(tm_pool);
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
let (tm_tx, tm_rx) = mpsc::channel();
let verif_sender = MpscTmInSharedPoolSender::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 =
MpscTmInSharedPoolSender::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.0.read().unwrap();
let tm_raw = tm_pool.read_as_vec(&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.0.read().unwrap();
let tm_raw = tm_pool.read_as_vec(&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 crate::pus::scheduler::PusScheduler;
use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken};
use crate::pus::{
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
PusServiceHandler,
};
use spacepackets::ecss::tc::PusTcReader;
use super::scheduler::PusSchedulerInterface;
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
use crate::pool::PoolProvider;
use crate::pus::{PusPacketHandlerResult, PusPacketHandlingError};
use alloc::string::ToString;
use spacepackets::ecss::{scheduling, PusPacket};
use spacepackets::time::cds::TimeProvider;
use std::boxed::Box;
/// 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
/// the most important PUS requests for a scheduling service.
/// packets. This handler is able to handle the most important PUS requests for a scheduling
/// service which provides the [PusSchedulerInterface].
///
/// 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
/// [Self::scheduler] and [Self::scheduler_mut] function and then use the scheduler API to release
/// telecommands when applicable.
pub struct PusService11SchedHandler {
psb: PusServiceBase,
scheduler: PusScheduler,
pub struct PusService11SchedHandler<
TcInMemConverter: EcssTcInMemConverter,
Scheduler: PusSchedulerInterface,
> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
scheduler: Scheduler,
}
impl PusService11SchedHandler {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_store: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
scheduler: PusScheduler,
) -> Self {
impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerInterface>
PusService11SchedHandler<TcInMemConverter, Scheduler>
{
pub fn new(service_helper: PusServiceHelper<TcInMemConverter>, scheduler: Scheduler) -> Self {
Self {
psb: PusServiceBase::new(
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
service_helper,
scheduler,
}
}
pub fn scheduler_mut(&mut self) -> &mut PusScheduler {
pub fn scheduler_mut(&mut self) -> &mut Scheduler {
&mut self.scheduler
}
pub fn scheduler(&self) -> &PusScheduler {
pub fn scheduler(&self) -> &Scheduler {
&self.scheduler
}
}
impl PusServiceHandler for PusService11SchedHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase {
&mut self.psb
}
fn psb(&self) -> &PusServiceBase {
&self.psb
}
fn handle_one_tc(
pub fn handle_one_tc(
&mut self,
addr: StoreAddr,
token: VerificationToken<TcStateAccepted>,
sched_tc_pool: &mut (impl PoolProvider + ?Sized),
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
self.copy_tc_to_buf(addr)?;
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
let subservice = tc.subservice();
let std_service = scheduling::Subservice::try_from(subservice);
if std_service.is_err() {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
}
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(
tc.subservice(),
token,
subservice,
ecss_tc_and_token.token,
));
}
let mut partial_error = None;
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
match std_service.unwrap() {
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
match standard_subservice.unwrap() {
scheduling::Subservice::TcEnableScheduling => {
let start_token = self
.psb
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&time_stamp))
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success");
self.scheduler.enable();
if self.scheduler.is_enabled() {
self.psb
self.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&time_stamp))
.expect("Error sending completion success");
} else {
panic!("Failed to enable scheduler");
return Err(PusPacketHandlingError::Other(
"failed to enabled scheduler".to_string(),
));
}
}
scheduling::Subservice::TcDisableScheduling => {
let start_token = self
.psb
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&time_stamp))
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success");
self.scheduler.disable();
if !self.scheduler.is_enabled() {
self.psb
self.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&time_stamp))
.expect("Error sending completion success");
} else {
panic!("Failed to disable scheduler");
return Err(PusPacketHandlingError::Other(
"failed to disable scheduler".to_string(),
));
}
}
scheduling::Subservice::TcResetScheduling => {
let start_token = self
.psb
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&time_stamp))
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("Error sending start success");
let mut pool = self
.psb
.shared_tc_store
.write()
.expect("Locking pool failed");
self.scheduler
.reset(pool.as_mut())
.reset(sched_tc_pool)
.expect("Error resetting TC Pool");
self.psb
self.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&time_stamp))
@ -143,31 +132,30 @@ impl PusServiceHandler for PusService11SchedHandler {
}
scheduling::Subservice::TcInsertActivity => {
let start_token = self
.psb
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&time_stamp))
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
.expect("error sending start success");
let mut pool = self
.psb
.shared_tc_store
.write()
.expect("locking pool failed");
// let mut pool = self.sched_tc_pool.write().expect("locking pool failed");
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");
self.psb
self.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&time_stamp))
.expect("sending completion success failed");
}
_ => {
// Treat unhandled standard subservices as custom subservices for now.
return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
token,
subservice,
ecss_tc_and_token.token,
));
}
}
@ -176,9 +164,192 @@ impl PusServiceHandler for PusService11SchedHandler {
partial_error,
));
}
Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
token,
))
Ok(PusPacketHandlerResult::RequestHandled)
}
}
#[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::PoolProvider + ?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::{
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceBase, PusServiceHandler, PusTmWrapper,
PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError, PusTmWrapper,
};
use spacepackets::ecss::tc::PusTcReader;
use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use spacepackets::ecss::PusPacket;
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 handler only processes ping requests and generates a ping reply for them accordingly.
pub struct PusService17TestHandler {
psb: PusServiceBase,
pub struct PusService17TestHandler<TcInMemConverter: EcssTcInMemConverter> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
}
impl PusService17TestHandler {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_store: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
) -> Self {
Self {
psb: PusServiceBase::new(
tc_receiver,
shared_tc_store,
tm_sender,
tm_apid,
verification_handler,
),
impl<TcInMemConverter: EcssTcInMemConverter> PusService17TestHandler<TcInMemConverter> {
pub fn new(service_helper: PusServiceHelper<TcInMemConverter>) -> Self {
Self { service_helper }
}
pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
}
}
}
impl PusServiceHandler for PusService17TestHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase {
&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)?;
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)?;
if tc.service() != 17 {
return Err(PusPacketHandlingError::WrongService(tc.service()));
}
if tc.subservice() == 1 {
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
.psb
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&time_stamp))
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
.map_err(|_| PartialPusHandlingError::Verification);
let start_token = if let Ok(result) = result {
Some(result)
@ -70,11 +48,13 @@ impl PusServiceHandler for PusService17TestHandler {
None
};
// 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 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
.psb
.service_helper
.common
.tm_sender
.send_tm(PusTmWrapper::Direct(ping_reply))
.map_err(PartialPusHandlingError::TmSend);
@ -84,7 +64,8 @@ impl PusServiceHandler for PusService17TestHandler {
if let Some(start_token) = start_token {
if self
.psb
.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&time_stamp))
@ -98,121 +79,194 @@ impl PusServiceHandler for PusService17TestHandler {
partial_error,
));
};
return Ok(PusPacketHandlerResult::RequestHandled);
} else {
return Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
ecss_tc_and_token.token,
));
}
Ok(PusPacketHandlerResult::CustomSubservice(
tc.subservice(),
token,
))
Ok(PusPacketHandlerResult::RequestHandled)
}
}
#[cfg(test)]
mod tests {
use crate::pool::{LocalPool, PoolCfg, SharedPool};
use crate::pus::test::PusService17TestHandler;
use crate::pus::verification::{
RequestId, StdVerifReporterWithSender, VerificationReporterCfg,
use crate::pus::tests::{
PusServiceHandlerWithSharedStoreCommon, PusServiceHandlerWithVecCommon, PusTestHarness,
SimplePusPacketHandler, TEST_APID,
};
use crate::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender, PusServiceHandler};
use crate::tmtc::tm_helper::SharedTmStore;
use crate::pus::verification::RequestId;
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::tm::PusTmReader;
use spacepackets::ecss::{PusPacket, SerializablePusPacket};
use spacepackets::ecss::PusPacket;
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]
fn test_basic_ping_processing() {
let mut pus_buf: [u8; 64] = [0; 64];
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);
let tc_pool_shared = SharedPool::new(RwLock::new(Box::new(tc_pool)));
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
let tm_pool_shared = shared_tm_store.clone_backing_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_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);
let test_srv_tc_receiver = MpscTcInStoreReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx);
let mut pus_17_handler = PusService17TestHandler::new(
Box::new(test_srv_tc_receiver),
tc_pool_shared.clone(),
Box::new(test_srv_tm_sender),
TEST_APID,
verification_handler.clone(),
);
struct Pus17HandlerWithStoreTester {
common: PusServiceHandlerWithSharedStoreCommon,
handler: PusService17TestHandler<EcssTcInSharedStoreConverter>,
}
impl Pus17HandlerWithStoreTester {
pub fn new() -> Self {
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
let pus_17_handler = PusService17TestHandler::new(srv_handler);
Self {
common,
handler: pus_17_handler,
}
}
}
impl PusTestHarness for Pus17HandlerWithStoreTester {
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 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.
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, None, true);
let token = verification_handler.add_tc(&ping_tc);
let token = verification_handler
.acceptance_success(token, None)
.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();
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
let token = test_harness.send_tc(&ping_tc);
let request_id = token.req_id();
let result = test_harness.handle_one_tc();
assert!(result.is_ok());
// We should see 4 replies in the TM queue now: Acceptance TM, Start TM, ping reply and
// 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
next_msg = tm_rx.try_recv();
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(), 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());
test_harness.check_next_verification_tm(1, request_id);
// Start TM
test_harness.check_next_verification_tm(3, request_id);
// Ping reply
next_msg = tm_rx.try_recv();
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();
let tm = test_harness.read_next_tm();
assert_eq!(tm.service(), 17);
assert_eq!(tm.subservice(), 2);
assert!(tm.user_data().is_empty());
// TM completion
next_msg = tm_rx.try_recv();
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(), 1);
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_harness.check_next_verification_tm(7, request_id);
}
#[test]
fn test_basic_ping_processing_using_store() {
let mut test_harness = Pus17HandlerWithStoreTester::new();
ping_test(&mut test_harness);
}
#[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,11 +15,11 @@
//! ```
//! use std::sync::{Arc, mpsc, RwLock};
//! use std::time::Duration;
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider, SharedPool};
//! use satrs_core::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
//! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
//! use satrs_core::seq_count::SeqCountProviderSimple;
//! use satrs_core::pus::MpscTmInStoreSender;
//! use satrs_core::tmtc::tm_helper::SharedTmStore;
//! use satrs_core::pus::MpscTmInSharedPoolSender;
//! use satrs_core::tmtc::tm_helper::SharedTmPool;
//! use spacepackets::ecss::PusPacket;
//! use spacepackets::SpHeader;
//! use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader};
@ -28,18 +28,18 @@
//! const EMPTY_STAMP: [u8; 7] = [0; 7];
//! const TEST_APID: u16 = 0x02;
//!
//! let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
//! let tm_pool = LocalPool::new(pool_cfg.clone());
//! let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
//! let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
//! let shared_tm_store = SharedTmPool::new(tm_pool);
//! let tm_store = shared_tm_store.clone_backing_pool();
//! let (verif_tx, verif_rx) = mpsc::channel();
//! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx);
//! let sender = MpscTmInSharedPoolSender::new(0, "Test Sender", shared_tm_store, verif_tx);
//! let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
//! let mut reporter = VerificationReporterWithSender::new(&cfg , Box::new(sender));
//!
//! let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
//! 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);
//!
//! // Complete success sequence for a telecommand
@ -56,9 +56,7 @@
//! {
//! let mut rg = tm_store.write().expect("Error locking shared pool");
//! let store_guard = rg.read_with_guard(addr);
//! let slice = store_guard.read().expect("Error reading TM slice");
//! tm_len = slice.len();
//! tm_buf[0..tm_len].copy_from_slice(slice);
//! tm_len = store_guard.read(&mut tm_buf).expect("Error reading TM slice");
//! }
//! let (pus_tm, _) = PusTmReader::new(&tm_buf[0..tm_len], 7)
//! .expect("Error reading verification TM");
@ -87,7 +85,7 @@ use delegate::delegate;
use serde::{Deserialize, Serialize};
use spacepackets::ecss::tc::IsPusTelecommand;
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::{SpHeader, MAX_APID};
@ -208,6 +206,8 @@ impl WasAtLeastAccepted for TcStateAccepted {}
impl WasAtLeastAccepted for TcStateStarted {}
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)]
pub enum TcStateToken {
None(VerificationToken<TcStateNone>),
@ -353,7 +353,7 @@ impl<'src_data, State, SuccessOrFailure> VerificationSendable<'src_data, State,
}
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> {
@ -877,7 +877,7 @@ impl VerificationReporterCore {
PusTmCreator::new(
sp_header,
tm_sec_header,
Some(&src_data_buf[0..source_data_len]),
&src_data_buf[0..source_data_len],
true,
)
}
@ -1323,15 +1323,15 @@ mod std_mod {
#[cfg(test)]
mod tests {
use crate::pool::{LocalPool, PoolCfg};
use crate::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
use crate::pus::tests::CommonTmInfo;
use crate::pus::verification::{
EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone,
VerificationReporter, VerificationReporterCfg, VerificationReporterWithSender,
VerificationToken,
};
use crate::pus::{EcssChannel, MpscTmInStoreSender, PusTmWrapper};
use crate::tmtc::tm_helper::SharedTmStore;
use crate::pus::{EcssChannel, MpscTmInSharedPoolSender, PusTmWrapper};
use crate::tmtc::tm_helper::SharedTmPool;
use crate::ChannelId;
use alloc::boxed::Box;
use alloc::format;
@ -1438,6 +1438,7 @@ mod tests {
fn base_tc_init(app_data: Option<&[u8]>) -> (PusTcCreator, RequestId) {
let mut sph = SpHeader::tc_unseg(TEST_APID, 0x34, 0).unwrap();
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 req_id = RequestId::new(&pus_tc);
(pus_tc, req_id)
@ -1446,12 +1447,11 @@ mod tests {
fn base_init(api_sel: bool) -> (TestBase<'static>, VerificationToken<TcStateNone>) {
let mut reporter = base_reporter();
let (tc, req_id) = base_tc_init(None);
let init_tok;
if api_sel {
init_tok = reporter.add_tc_with_req_id(req_id);
let init_tok = if api_sel {
reporter.add_tc_with_req_id(req_id)
} else {
init_tok = reporter.add_tc(&tc);
}
reporter.add_tc(&tc)
};
(TestBase { vr: reporter, tc }, init_tok)
}
@ -1474,7 +1474,7 @@ mod tests {
time_stamp: EMPTY_STAMP,
},
additional_data: None,
req_id: req_id.clone(),
req_id: *req_id,
};
let mut service_queue = sender.service_queue.borrow_mut();
assert_eq!(service_queue.len(), 1);
@ -1484,11 +1484,11 @@ mod tests {
#[test]
fn test_mpsc_verif_send_sync() {
let pool = LocalPool::new(PoolCfg::new(vec![(8, 8)]));
let tm_store = Box::new(pool);
let shared_tm_store = SharedTmStore::new(tm_store);
let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)]));
let shared_tm_store = SharedTmPool::new(pool);
let (tx, _) = mpsc::channel();
let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx);
let mpsc_verif_sender =
MpscTmInSharedPoolSender::new(0, "verif_sender", shared_tm_store, tx);
is_send(&mpsc_verif_sender);
}
@ -1504,7 +1504,7 @@ mod tests {
fn test_basic_acceptance_success() {
let (b, tok) = base_init(false);
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");
acceptance_check(&mut sender, &tok.req_id);
}
@ -1545,7 +1545,7 @@ mod tests {
let mut sender = TestSender::default();
let fail_code = EcssEnumU16::new(2);
let fail_params = FailParams::new(Some(stamp_buf.as_slice()), &fail_code, None);
b.vr.acceptance_failure(tok, &mut sender, fail_params)
b.vr.acceptance_failure(tok, &sender, fail_params)
.expect("Sending acceptance success failed");
acceptance_fail_check(&mut sender, tok.req_id, stamp_buf);
}
@ -1604,7 +1604,7 @@ mod tests {
#[test]
fn test_basic_acceptance_failure_with_fail_data() {
let (b, tok) = base_init(false);
let mut sender = TestSender::default();
let sender = TestSender::default();
let fail_code = EcssEnumU8::new(10);
let fail_data = EcssEnumU32::new(12);
let mut fail_data_raw = [0; 4];
@ -1614,7 +1614,7 @@ mod tests {
&fail_code,
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");
let cmp_info = TmInfo {
common: CommonTmInfo {
@ -1680,12 +1680,10 @@ mod tests {
);
let accepted_token =
b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
.expect("Sending acceptance success failed");
let empty =
b.vr.start_failure(accepted_token, &mut sender, fail_params)
.expect("Start failure failure");
assert_eq!(empty, ());
b.vr.start_failure(accepted_token, &mut sender, fail_params)
.expect("Start failure failure");
start_fail_check(&mut sender, tok.req_id, fail_data_raw);
}
@ -1777,31 +1775,27 @@ mod tests {
let mut sender = TestSender::default();
let accepted_token = b
.rep()
.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
.expect("Sending acceptance success failed");
let started_token = b
.rep()
.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.start_success(accepted_token, &sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed");
let mut empty = b
.rep()
b.rep()
.step_success(
&started_token,
&mut sender,
&sender,
Some(&EMPTY_STAMP),
EcssEnumU8::new(0),
)
.expect("Sending step 0 success failed");
assert_eq!(empty, ());
empty =
b.vr.step_success(
&started_token,
&mut sender,
Some(&EMPTY_STAMP),
EcssEnumU8::new(1),
)
.expect("Sending step 1 success failed");
assert_eq!(empty, ());
b.vr.step_success(
&started_token,
&sender,
Some(&EMPTY_STAMP),
EcssEnumU8::new(1),
)
.expect("Sending step 1 success failed");
assert_eq!(sender.service_queue.borrow().len(), 4);
step_success_check(&mut sender, tok.req_id);
}
@ -1817,16 +1811,12 @@ mod tests {
.helper
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed");
let mut empty = b
.helper
b.helper
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(0))
.expect("Sending step 0 success failed");
assert_eq!(empty, ());
empty = b
.helper
b.helper
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(1))
.expect("Sending step 1 success failed");
assert_eq!(empty, ());
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
assert_eq!(sender.service_queue.borrow().len(), 4);
step_success_check(sender, tok.req_id);
@ -2121,10 +2111,8 @@ mod tests {
let started_token =
b.vr.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed");
let empty =
b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP))
.expect("Sending completion success failed");
assert_eq!(empty, ());
b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP))
.expect("Sending completion success failed");
completion_success_check(&mut sender, tok.req_id);
}
@ -2139,11 +2127,9 @@ mod tests {
.helper
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed");
let empty = b
.helper
b.helper
.completion_success(started_token, Some(&EMPTY_STAMP))
.expect("Sending completion success failed");
assert_eq!(empty, ());
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
completion_success_check(sender, tok.req_id);
}
@ -2151,18 +2137,19 @@ mod tests {
#[test]
// TODO: maybe a bit more extensive testing, all I have time for right now
fn test_seq_count_increment() {
let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let tm_pool = Box::new(LocalPool::new(pool_cfg.clone()));
let shared_tm_store = SharedTmStore::new(tm_pool);
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
let shared_tm_store = SharedTmPool::new(tm_pool);
let shared_tm_pool = shared_tm_store.clone_backing_pool();
let (verif_tx, verif_rx) = mpsc::channel();
let sender = MpscTmInStoreSender::new(0, "Verification Sender", shared_tm_store, verif_tx);
let sender =
MpscTmInSharedPoolSender::new(0, "Verification Sender", shared_tm_store, verif_tx);
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
let mut reporter = VerificationReporterWithSender::new(&cfg, Box::new(sender));
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
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);
// Complete success sequence for a telecommand
@ -2185,9 +2172,9 @@ mod tests {
{
let mut rg = shared_tm_pool.write().expect("Error locking shared pool");
let store_guard = rg.read_with_guard(addr);
let slice = store_guard.read().expect("Error reading TM slice");
tm_len = slice.len();
tm_buf[0..tm_len].copy_from_slice(slice);
tm_len = store_guard
.read(&mut tm_buf)
.expect("Error reading TM slice");
}
let (pus_tm, _) =
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");

View File

@ -21,7 +21,7 @@
//! use satrs_core::tmtc::ccsds_distrib::{CcsdsPacketHandler, CcsdsDistributor};
//! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
//! use spacepackets::{CcsdsPacket, SpHeader};
//! use spacepackets::ecss::SerializablePusPacket;
//! use spacepackets::ecss::WritablePusPacket;
//! use spacepackets::ecss::tc::{PusTc, PusTcCreator};
//!
//! #[derive (Default)]
@ -226,7 +226,7 @@ pub(crate) mod tests {
use super::*;
use crate::tmtc::ccsds_distrib::{CcsdsDistributor, CcsdsPacketHandler};
use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::SerializablePusPacket;
use spacepackets::ecss::WritablePusPacket;
use spacepackets::CcsdsPacket;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
@ -244,9 +244,10 @@ pub(crate) mod tests {
&buf[0..size]
}
type SharedPacketQueue = Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>;
pub struct BasicApidHandlerSharedQueue {
pub known_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>,
pub unknown_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>,
pub known_packet_queue: SharedPacketQueue,
pub unknown_packet_queue: SharedPacketQueue,
}
#[derive(Default)]
@ -268,11 +269,11 @@ pub(crate) mod tests {
) -> Result<(), Self::Error> {
let mut vec = Vec::new();
vec.extend_from_slice(tc_raw);
Ok(self
.known_packet_queue
self.known_packet_queue
.lock()
.unwrap()
.push_back((sp_header.apid(), vec)))
.push_back((sp_header.apid(), vec));
Ok(())
}
fn handle_unknown_apid(
@ -282,11 +283,11 @@ pub(crate) mod tests {
) -> Result<(), Self::Error> {
let mut vec = Vec::new();
vec.extend_from_slice(tc_raw);
Ok(self
.unknown_packet_queue
self.unknown_packet_queue
.lock()
.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
#[cfg(feature = "alloc")]
use downcast_rs::{impl_downcast, Downcast};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use spacepackets::{ByteConversionError, SpHeader};
use spacepackets::SpHeader;
#[cfg(feature = "alloc")]
pub mod ccsds_distrib;
@ -24,40 +22,6 @@ pub use pus_distrib::{PusDistributor, PusServiceProvider};
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
/// no assumptions about the received protocol.
///

View File

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

View File

@ -8,34 +8,38 @@ pub use std_mod::*;
#[cfg(feature = "std")]
pub mod std_mod {
use crate::pool::{ShareablePoolProvider, SharedPool, StoreAddr};
use crate::pool::{PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr};
use crate::pus::EcssTmtcError;
use spacepackets::ecss::tm::PusTmCreator;
use spacepackets::ecss::SerializablePusPacket;
use spacepackets::ecss::WritablePusPacket;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct SharedTmStore {
pool: SharedPool,
}
pub struct SharedTmPool(pub SharedStaticMemoryPool);
impl SharedTmStore {
pub fn new(backing_pool: ShareablePoolProvider) -> Self {
Self {
pool: Arc::new(RwLock::new(backing_pool)),
}
impl SharedTmPool {
pub fn new(shared_pool: StaticMemoryPool) -> Self {
Self(Arc::new(RwLock::new(shared_pool)))
}
pub fn clone_backing_pool(&self) -> SharedPool {
self.pool.clone()
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.0.clone()
}
pub fn shared_pool(&self) -> &SharedStaticMemoryPool {
&self.0
}
pub fn shared_pool_mut(&mut self) -> &mut SharedStaticMemoryPool {
&mut self.0
}
pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result<StoreAddr, EcssTmtcError> {
let mut pg = self.pool.write().map_err(|_| EcssTmtcError::StoreLock)?;
let (addr, buf) = pg.free_element(pus_tm.len_packed())?;
pus_tm
.write_to_bytes(buf)
.expect("writing PUS TM to store failed");
let mut pg = self.0.write().map_err(|_| EcssTmtcError::StoreLock)?;
let addr = pg.free_element(pus_tm.len_written(), |buf| {
pus_tm
.write_to_bytes(buf)
.expect("writing PUS TM to store failed");
})?;
Ok(addr)
}
}
@ -59,7 +63,7 @@ impl PusTmWithCdsShortHelper {
&'a mut self,
service: u8,
subservice: u8,
source_data: Option<&'a [u8]>,
source_data: &'a [u8],
seq_count: u16,
) -> PusTmCreator {
let time_stamp = TimeProvider::from_now_with_u16_days().unwrap();
@ -71,7 +75,7 @@ impl PusTmWithCdsShortHelper {
&'a mut self,
service: u8,
subservice: u8,
source_data: Option<&'a [u8]>,
source_data: &'a [u8],
stamper: &TimeProvider,
seq_count: u16,
) -> PusTmCreator {
@ -83,7 +87,7 @@ impl PusTmWithCdsShortHelper {
&'a self,
service: u8,
subservice: u8,
source_data: Option<&'a [u8]>,
source_data: &'a [u8],
seq_count: u16,
) -> PusTmCreator {
let mut reply_header = SpHeader::tm_unseg(self.apid, seq_count, 0).unwrap();
@ -91,3 +95,33 @@ impl PusTmWithCdsShortHelper {
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)]
use core::mem::size_of;
use serde::{Deserialize, Serialize};
use spacepackets::ecss::{Ptc, RealPfc, UnsignedPfc};
use spacepackets::ecss::{PfcReal, PfcUnsigned, Ptc};
use spacepackets::time::cds::TimeProvider;
use spacepackets::time::{CcsdsTimeProvider, TimeWriter};
@ -64,7 +64,7 @@ impl TestMgmHkWithIndividualValidity {
curr_idx += 1;
buf[curr_idx] = Ptc::Real as u8;
curr_idx += 1;
buf[curr_idx] = RealPfc::Float as u8;
buf[curr_idx] = PfcReal::Float as u8;
curr_idx += 1;
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.val.to_be_bytes());
curr_idx += size_of::<f32>();
@ -75,7 +75,7 @@ impl TestMgmHkWithIndividualValidity {
curr_idx += 1;
buf[curr_idx] = Ptc::UnsignedInt as u8;
curr_idx += 1;
buf[curr_idx] = UnsignedPfc::TwoBytes as u8;
buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
curr_idx += 1;
buf[curr_idx] = 3;
curr_idx += 1;
@ -100,7 +100,7 @@ impl TestMgmHkWithGroupValidity {
curr_idx += 1;
buf[curr_idx] = Ptc::Real as u8;
curr_idx += 1;
buf[curr_idx] = RealPfc::Float as u8;
buf[curr_idx] = PfcReal::Float as u8;
curr_idx += 1;
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.to_be_bytes());
curr_idx += size_of::<f32>();
@ -109,7 +109,7 @@ impl TestMgmHkWithGroupValidity {
curr_idx += 1;
buf[curr_idx] = Ptc::UnsignedInt as u8;
curr_idx += 1;
buf[curr_idx] = UnsignedPfc::TwoBytes as u8;
buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
curr_idx += 1;
buf[curr_idx] = 3;
for val in self.mgm_vals {

View File

@ -1,4 +1,4 @@
use satrs_core::pool::{LocalPool, PoolCfg, PoolGuard, PoolProvider, StoreAddr};
use satrs_core::pool::{PoolGuard, PoolProvider, StaticMemoryPool, StaticPoolConfig, StoreAddr};
use std::ops::DerefMut;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
@ -9,8 +9,8 @@ const DUMMY_DATA: [u8; 4] = [0, 1, 2, 3];
#[test]
fn threaded_usage() {
let pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]);
let shared_pool = Arc::new(RwLock::new(LocalPool::new(pool_cfg)));
let pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
let shared_pool = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
let shared_clone = shared_pool.clone();
let (tx, rx): (Sender<StoreAddr>, Receiver<StoreAddr>) = mpsc::channel();
let jh0 = thread::spawn(move || {
@ -25,8 +25,10 @@ fn threaded_usage() {
addr = rx.recv().expect("Receiving store address failed");
let mut pool_access = shared_clone.write().unwrap();
let pg = PoolGuard::new(pool_access.deref_mut(), addr);
let read_res = pg.read().expect("Reading failed");
assert_eq!(read_res, DUMMY_DATA);
let mut read_buf: [u8; 4] = [0; 4];
let read_bytes = pg.read(&mut read_buf).expect("Reading failed");
assert_eq!(read_buf, DUMMY_DATA);
assert_eq!(read_bytes, 4);
}
let pool_access = shared_clone.read().unwrap();
assert!(!pool_access.has_element_at(&addr).expect("Invalid address"));

View File

@ -1,15 +1,17 @@
#[cfg(feature = "crossbeam")]
//#[cfg(feature = "crossbeam")]
pub mod crossbeam_test {
use hashbrown::HashMap;
use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider};
use satrs_core::pool::{
PoolProvider, PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig,
};
use satrs_core::pus::verification::{
FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender,
};
use satrs_core::pus::CrossbeamTmInStoreSender;
use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::tm_helper::SharedTmPool;
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
use spacepackets::ecss::tm::PusTmReader;
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, SerializablePusPacket};
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, WritablePusPacket};
use spacepackets::SpHeader;
use std::sync::{Arc, RwLock};
use std::thread;
@ -33,13 +35,13 @@ pub mod crossbeam_test {
// each reporter have an own sequence count provider.
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
// Shared pool object to store the verification PUS telemetry
let pool_cfg = PoolCfg::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_tc_pool_0 = Arc::new(RwLock::new(LocalPool::new(pool_cfg)));
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let shared_tm_pool = SharedTmPool::new(StaticMemoryPool::new(pool_cfg.clone()));
let shared_tc_pool_0 = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
let shared_tc_pool_1 = shared_tc_pool_0.clone();
let (tx, rx) = crossbeam_channel::bounded(10);
let sender =
CrossbeamTmInStoreSender::new(0, "verif_sender", shared_tm_store.clone(), tx.clone());
CrossbeamTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tx.clone());
let mut reporter_with_sender_0 =
VerificationReporterWithSender::new(&cfg, Box::new(sender));
let mut reporter_with_sender_1 = reporter_with_sender_0.clone();
@ -54,17 +56,23 @@ pub mod crossbeam_test {
let mut tc_guard = shared_tc_pool_0.write().unwrap();
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
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);
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap();
pus_tc_0.write_to_bytes(&mut buf).unwrap();
let addr = tc_guard
.free_element(pus_tc_0.len_written(), |buf| {
pus_tc_0.write_to_bytes(buf).unwrap();
})
.unwrap();
tx_tc_0.send(addr).unwrap();
let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap();
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);
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap();
pus_tc_1.write_to_bytes(&mut buf).unwrap();
let addr = tc_guard
.free_element(pus_tc_0.len_written(), |buf| {
pus_tc_1.write_to_bytes(buf).unwrap();
})
.unwrap();
tx_tc_1.send(addr).unwrap();
}
let verif_sender_0 = thread::spawn(move || {
@ -76,21 +84,17 @@ pub mod crossbeam_test {
{
let mut tc_guard = shared_tc_pool_0.write().unwrap();
let pg = tc_guard.read_with_guard(tc_addr);
let buf = pg.read().unwrap();
tc_len = buf.len();
tc_buf[0..tc_len].copy_from_slice(buf);
tc_len = pg.read(&mut tc_buf).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);
accepted_token = reporter_with_sender_0
let accepted_token = reporter_with_sender_0
.acceptance_success(token, Some(&FIXED_STAMP))
.expect("Acceptance success failed");
// Do some start handling here
let started_token;
started_token = reporter_with_sender_0
let started_token = reporter_with_sender_0
.start_success(accepted_token, Some(&FIXED_STAMP))
.expect("Start success failed");
// Do some step handling here
@ -116,9 +120,7 @@ pub mod crossbeam_test {
{
let mut tc_guard = shared_tc_pool_1.write().unwrap();
let pg = tc_guard.read_with_guard(tc_addr);
let buf = pg.read().unwrap();
tc_len = buf.len();
tc_buf[0..tc_len].copy_from_slice(buf);
tc_len = pg.read(&mut tc_buf).unwrap();
}
let (tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
let token = reporter_with_sender_1.add_tc(&tc);
@ -144,13 +146,13 @@ pub mod crossbeam_test {
.recv_timeout(Duration::from_millis(50))
.expect("Packet reception timeout");
let tm_len;
let shared_tm_store = shared_tm_store.clone_backing_pool();
let shared_tm_store = shared_tm_pool.clone_backing_pool();
{
let mut rg = shared_tm_store.write().expect("Error locking shared pool");
let store_guard = rg.read_with_guard(verif_addr);
let slice = store_guard.read().expect("Error reading TM slice");
tm_len = slice.len();
tm_buf[0..tm_len].copy_from_slice(slice);
tm_len = store_guard
.read(&mut tm_buf)
.expect("Error reading TM slice");
}
let (pus_tm, _) =
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");
@ -158,8 +160,7 @@ pub mod crossbeam_test {
RequestId::from_bytes(&pus_tm.source_data()[0..RequestId::SIZE_AS_BYTES])
.expect("reading request ID from PUS TM source data failed");
if !verif_map.contains_key(&req_id) {
let mut content = Vec::new();
content.push(pus_tm.subservice());
let content = vec![pus_tm.subservice()];
verif_map.insert(req_id, content);
} else {
let content = verif_map.get_mut(&req_id).unwrap();

View File

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

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

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

View File

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

View File

@ -3,10 +3,32 @@ sat-rs example
This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components.
The application opens a UDP server on port 7301 to receive telecommands.
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
You can run the application using `cargo run`. The `simpleclient` binary target sends a
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
You can run the application using `cargo run`.
# Features
The example has the `dyn_tmtc` feature which is enabled by default. With this feature enabled,
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
stores.
You can run the application without this feature using
```sh
cargo run --no-default-features
```
# Interacting with the sat-rs example
## Simple Client
The `simpleclient` binary target sends a
ping telecommand and then verifies the telemetry generated by the example application.
It can be run like this:
@ -17,7 +39,7 @@ cargo run --bin simpleclient
This repository also contains a more complex client using the
[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
[tmtccmd package](https://github.com/robamu-org/tmtccmd).
@ -46,8 +68,7 @@ as Python code. For example, you can use the following command to send a ping li
the `simpleclient`:
```sh
./main.py -s test -o ping
./main.py -p /test/ping
```
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.
You can also simply call the script without any arguments to view the command tree.

View File

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

View File

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

View File

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

View File

@ -1,50 +1,85 @@
import datetime
import logging
from spacepackets.ccsds import CdsShortTimestamp
from spacepackets.ecss import PusTelecommand
from tmtccmd.config import CoreServiceList
from tmtccmd.tc import DefaultPusQueueHelper
from tmtccmd.tc.pus_11_tc_sched import create_time_tagged_cmd
from tmtccmd.tc.pus_3_fsfw_hk import create_request_one_hk_command
from tmtccmd.config import CmdTreeNode
from tmtccmd.tmtc import DefaultPusQueueHelper
from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd
from tmtccmd.pus.tc.s3_fsfw_hk import create_request_one_hk_command
from common import (
EXAMPLE_PUS_APID,
HkOpCodes,
make_addressable_id,
RequestTargetId,
AcsHkIds,
)
_LOGGER = logging.getLogger(__name__)
def pack_pus_telecommands(q: DefaultPusQueueHelper, service: str, op_code: str):
if (
service == CoreServiceList.SERVICE_17
or service == CoreServiceList.SERVICE_17_ALT
):
if op_code == "ping":
def create_cmd_definition_tree() -> CmdTreeNode:
root_node = CmdTreeNode.root_node()
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")
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")
return q.add_pus_tc(PusTelecommand(service=17, subservice=128))
if service == CoreServiceList.SERVICE_11:
q.add_log_cmd("Sending PUS scheduled TC telecommand")
crt_time = CdsShortTimestamp.from_now()
time_stamp = crt_time + datetime.timedelta(seconds=10)
time_stamp = time_stamp.pack()
return q.add_pus_tc(
create_time_tagged_cmd(
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)
if cmd_path_list[0] == "scheduler":
assert len(cmd_path_list) >= 2
if cmd_path_list[1] == "schedule_ping_10_secs_ahead":
q.add_log_cmd("Sending PUS scheduled TC telecommand")
crt_time = CdsShortTimestamp.from_now()
time_stamp = crt_time + datetime.timedelta(seconds=10)
time_stamp = time_stamp.pack()
return q.add_pus_tc(
create_time_tagged_cmd(
time_stamp,
PusTelecommand(service=17, subservice=1),
apid=EXAMPLE_PUS_APID,
)
)
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

View File

@ -1,6 +1,8 @@
{
"com_if": "udp",
"com_if": "tcp",
"tcpip_udp_ip_addr": "127.0.0.1",
"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
}

117
satrs-example/src/acs.rs Normal file
View File

@ -0,0 +1,117 @@
use std::sync::mpsc::{self, TryRecvError};
use log::{info, warn};
use satrs_core::pus::verification::VerificationReporterWithSender;
use satrs_core::pus::{EcssTmSender, PusTmWrapper};
use satrs_core::spacepackets::ecss::hk::Subservice as HkSubservice;
use satrs_core::{
hk::HkRequest,
spacepackets::{
ecss::tm::{PusTmCreator, PusTmSecondaryHeader},
time::cds::{DaysLen16Bits, TimeProvider},
SequenceFlags, SpHeader,
},
};
use satrs_example::config::{RequestTargetId, PUS_APID};
use crate::{
hk::{AcsHkIds, HkUniqueId},
requests::{Request, RequestWithToken},
update_time,
};
pub struct AcsTask {
timestamp: [u8; 7],
time_provider: TimeProvider<DaysLen16Bits>,
verif_reporter: VerificationReporterWithSender,
tm_sender: Box<dyn EcssTmSender>,
request_rx: mpsc::Receiver<RequestWithToken>,
}
impl AcsTask {
pub fn new(
tm_sender: impl EcssTmSender,
request_rx: mpsc::Receiver<RequestWithToken>,
verif_reporter: VerificationReporterWithSender,
) -> Self {
Self {
timestamp: [0; 7],
time_provider: TimeProvider::new_with_u16_days(0, 0),
verif_reporter,
tm_sender: Box::new(tm_sender),
request_rx,
}
}
fn handle_hk_request(&mut self, target_id: u32, unique_id: u32) {
assert_eq!(target_id, RequestTargetId::AcsSubsystem as u32);
if unique_id == AcsHkIds::TestMgmSet as u32 {
let mut sp_header = SpHeader::tm(PUS_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
let sec_header = PusTmSecondaryHeader::new_simple(
3,
HkSubservice::TmHkPacket as u8,
&self.timestamp,
);
let mut buf: [u8; 8] = [0; 8];
let hk_id = HkUniqueId::new(target_id, unique_id);
hk_id.write_to_be_bytes(&mut buf).unwrap();
let pus_tm = PusTmCreator::new(&mut sp_header, sec_header, &buf, true);
self.tm_sender
.send_tm(PusTmWrapper::Direct(pus_tm))
.expect("Sending HK TM failed");
}
// TODO: Verification failure for invalid unique IDs.
}
pub fn try_reading_one_request(&mut self) -> bool {
match self.request_rx.try_recv() {
Ok(request) => {
info!(
"ACS thread: Received HK request {:?}",
request.targeted_request
);
match request.targeted_request.request {
Request::Hk(hk_req) => match hk_req {
HkRequest::OneShot(unique_id) => self.handle_hk_request(
request.targeted_request.target_id_with_apid.target_id(),
unique_id,
),
HkRequest::Enable(_) => {}
HkRequest::Disable(_) => {}
HkRequest::ModifyCollectionInterval(_, _) => {}
},
Request::Mode(_mode_req) => {
warn!("mode request handling not implemented yet")
}
Request::Action(_action_req) => {
warn!("action request handling not implemented yet")
}
}
let started_token = self
.verif_reporter
.start_success(request.token, Some(&self.timestamp))
.expect("Sending start success failed");
self.verif_reporter
.completion_success(started_token, Some(&self.timestamp))
.expect("Sending completion success failed");
true
}
Err(e) => match e {
TryRecvError::Empty => false,
TryRecvError::Disconnected => {
warn!("ACS thread: Message Queue TX disconnected!");
false
}
},
}
}
pub fn periodic_operation(&mut self) {
update_time(&mut self.time_provider, &mut self.timestamp);
loop {
if !self.try_reading_one_request() {
break;
}
}
}
}

View File

@ -2,10 +2,10 @@ use satrs_core::pus::verification::RequestId;
use satrs_core::spacepackets::ecss::tc::PusTcCreator;
use satrs_core::spacepackets::ecss::tm::PusTmReader;
use satrs_core::{
spacepackets::ecss::{PusPacket, SerializablePusPacket},
spacepackets::ecss::{PusPacket, WritablePusPacket},
spacepackets::SpHeader,
};
use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT};
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::Duration;

View File

@ -1,14 +1,22 @@
use crate::tmtc::{MpscStoreAndSendError, PusTcSource};
use satrs_core::pus::ReceivesEcssPusTc;
use satrs_core::spacepackets::{CcsdsPacket, SpHeader};
use satrs_core::tmtc::{CcsdsPacketHandler, ReceivesCcsdsTc};
use satrs_example::PUS_APID;
use satrs_example::config::PUS_APID;
pub struct CcsdsReceiver {
pub tc_source: PusTcSource,
#[derive(Clone)]
pub struct CcsdsReceiver<
TcSource: ReceivesCcsdsTc<Error = E> + ReceivesEcssPusTc<Error = E> + Clone,
E,
> {
pub tc_source: TcSource,
}
impl CcsdsPacketHandler for CcsdsReceiver {
type Error = MpscStoreAndSendError;
impl<
TcSource: ReceivesCcsdsTc<Error = E> + ReceivesEcssPusTc<Error = E> + Clone + 'static,
E: 'static,
> CcsdsPacketHandler for CcsdsReceiver<TcSource, E>
{
type Error = E;
fn valid_apids(&self) -> &'static [u16] {
&[PUS_APID]

142
satrs-example/src/config.rs Normal file
View File

@ -0,0 +1,142 @@
use satrs_core::res_code::ResultU16;
use satrs_mib::res_code::ResultU16Info;
use satrs_mib::resultcode;
use std::net::Ipv4Addr;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs_core::{
events::{EventU32TypedSev, SeverityInfo},
pool::{StaticMemoryPool, StaticPoolConfig},
};
pub const PUS_APID: u16 = 0x02;
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum CustomPusServiceId {
Mode = 200,
Health = 201,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum RequestTargetId {
AcsSubsystem = 1,
}
pub const AOCS_APID: u16 = 1;
#[derive(Debug)]
pub enum GroupId {
Tmtc = 0,
Hk = 1,
}
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
pub const SERVER_PORT: u16 = 7301;
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
pub mod tmtc_err {
use super::*;
#[resultcode]
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 0);
#[resultcode]
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 1);
#[resultcode]
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 3);
#[resultcode(
info = "Not enough data inside the TC application data field. Optionally includes: \
8 bytes of failure data containing 2 failure parameters, \
P1 (u32 big endian): Expected data length, P2: Found data length"
)]
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
pub const TMTC_RESULTS: &[ResultU16Info] = &[
INVALID_PUS_SERVICE_EXT,
INVALID_PUS_SUBSERVICE_EXT,
NOT_ENOUGH_APP_DATA_EXT,
];
}
pub mod hk_err {
use super::*;
#[resultcode]
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 0);
#[resultcode]
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 1);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 2);
#[resultcode]
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 3);
}
#[allow(clippy::enum_variant_names)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TmSenderId {
PusVerification = 0,
PusTest = 1,
PusEvent = 2,
PusHk = 3,
PusAction = 4,
PusSched = 5,
AllEvents = 6,
AcsSubsystem = 7,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TcReceiverId {
PusTest = 1,
PusEvent = 2,
PusHk = 3,
PusAction = 4,
PusSched = 5,
}
pub mod pool {
use super::*;
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
(
StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
])),
StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
])),
)
}
pub fn create_sched_tc_pool() -> StaticMemoryPool {
StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]))
}
}
pub mod tasks {
pub const FREQ_MS_UDP_TMTC: u64 = 200;
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
pub const FREQ_MS_AOCS: u64 = 500;
pub const FREQ_MS_PUS_STACK: u64 = 200;
}

187
satrs-example/src/events.rs Normal file
View File

@ -0,0 +1,187 @@
use std::sync::mpsc::{self, SendError};
use satrs_core::{
event_man::{
EventManager, EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider,
SendEventProvider,
},
events::EventU32,
params::Params,
pus::{
event_man::{
DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken,
PusEventDispatcher,
},
verification::{TcStateStarted, VerificationReporterWithSender, VerificationToken},
EcssTmSender,
},
spacepackets::time::cds::{self, TimeProvider},
};
use satrs_example::config::PUS_APID;
use crate::update_time;
pub type MpscEventManager = EventManager<SendError<(EventU32, Option<Params>)>>;
pub struct PusEventHandler {
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
pus_event_dispatcher: PusEventDispatcher<(), EventU32>,
pus_event_man_rx: mpsc::Receiver<(EventU32, Option<Params>)>,
tm_sender: Box<dyn EcssTmSender>,
time_provider: TimeProvider,
timestamp: [u8; 7],
verif_handler: VerificationReporterWithSender,
}
/*
*/
impl PusEventHandler {
pub fn new(
verif_handler: VerificationReporterWithSender,
event_manager: &mut MpscEventManager,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
tm_sender: impl EcssTmSender,
) -> Self {
let (pus_event_man_tx, pus_event_man_rx) = mpsc::channel();
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
// telemetry for each event.
let event_reporter = EventReporter::new(PUS_APID, 128).unwrap();
let pus_tm_backend = DefaultPusMgmtBackendProvider::<EventU32>::default();
let pus_event_dispatcher =
PusEventDispatcher::new(event_reporter, Box::new(pus_tm_backend));
let pus_event_man_send_provider = MpscEventU32SendProvider::new(1, pus_event_man_tx);
event_manager.subscribe_all(pus_event_man_send_provider.id());
event_manager.add_sender(pus_event_man_send_provider);
Self {
event_request_rx,
pus_event_dispatcher,
pus_event_man_rx,
time_provider: cds::TimeProvider::new_with_u16_days(0, 0),
timestamp: [0; 7],
verif_handler,
tm_sender: Box::new(tm_sender),
}
}
pub fn handle_event_requests(&mut self) {
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
let started_token: VerificationToken<TcStateStarted> = event_req
.token
.try_into()
.expect("expected start verification token");
self.verif_handler
.completion_success(started_token, Some(timestamp))
.expect("Sending completion success failed");
};
// handle event requests
if let Ok(event_req) = self.event_request_rx.try_recv() {
match event_req.request {
EventRequest::Enable(event) => {
self.pus_event_dispatcher
.enable_tm_for_event(&event)
.expect("Enabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
EventRequest::Disable(event) => {
self.pus_event_dispatcher
.disable_tm_for_event(&event)
.expect("Disabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
}
}
}
pub fn generate_pus_event_tm(&mut self) {
// Perform the generation of PUS event packets
if let Ok((event, _param)) = self.pus_event_man_rx.try_recv() {
update_time(&mut self.time_provider, &mut self.timestamp);
self.pus_event_dispatcher
.generate_pus_event_tm_generic(
self.tm_sender.upcast_mut(),
&self.timestamp,
event,
None,
)
.expect("Sending TM as event failed");
}
}
}
pub struct EventManagerWrapper {
event_manager: MpscEventManager,
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
}
impl EventManagerWrapper {
pub fn new() -> Self {
// The sender handle is the primary sender handle for all components which want to create events.
// The event manager will receive the RX handle to receive all the events.
let (event_sender, event_man_rx) = mpsc::channel();
let event_recv = MpscEventReceiver::<EventU32>::new(event_man_rx);
Self {
event_manager: EventManagerWithMpscQueue::new(Box::new(event_recv)),
event_sender,
}
}
pub fn clone_event_sender(&self) -> mpsc::Sender<(EventU32, Option<Params>)> {
self.event_sender.clone()
}
pub fn event_manager(&mut self) -> &mut MpscEventManager {
&mut self.event_manager
}
pub fn try_event_routing(&mut self) {
// Perform the event routing.
self.event_manager
.try_event_handling()
.expect("event handling failed");
}
}
pub struct EventHandler {
pub event_man_wrapper: EventManagerWrapper,
pub pus_event_handler: PusEventHandler,
}
impl EventHandler {
pub fn new(
tm_sender: impl EcssTmSender,
verif_handler: VerificationReporterWithSender,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
) -> Self {
let mut event_man_wrapper = EventManagerWrapper::new();
let pus_event_handler = PusEventHandler::new(
verif_handler,
event_man_wrapper.event_manager(),
event_request_rx,
tm_sender,
);
Self {
event_man_wrapper,
pus_event_handler,
}
}
pub fn clone_event_sender(&self) -> mpsc::Sender<(EventU32, Option<Params>)> {
self.event_man_wrapper.clone_event_sender()
}
#[allow(dead_code)]
pub fn event_manager(&mut self) -> &mut MpscEventManager {
self.event_man_wrapper.event_manager()
}
pub fn periodic_operation(&mut self) {
self.pus_event_handler.handle_event_requests();
self.event_man_wrapper.try_event_routing();
self.pus_event_handler.generate_pus_event_tm();
}
}

View File

@ -1,4 +1,37 @@
use derive_new::new;
use satrs_core::spacepackets::ByteConversionError;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AcsHkIds {
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,98 +1,59 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs_core::events::{EventU32TypedSev, SeverityInfo};
use satrs_core::objects::ObjectId;
use std::net::Ipv4Addr;
use derive_new::new;
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 thiserror::Error;
use satrs_mib::res_code::{ResultU16, ResultU16Info};
use satrs_mib::resultcode;
pub mod config;
pub const PUS_APID: u16 = 0x02;
pub type Apid = u16;
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum CustomPusServiceId {
Mode = 200,
Health = 201,
#[derive(Debug, Error)]
pub enum TargetIdCreationError {
#[error("byte conversion")]
ByteConversion(#[from] ByteConversionError),
#[error("not enough app data to generate target ID")]
NotEnoughAppData(usize),
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum RequestTargetId {
AcsSubsystem = 1,
// TODO: can these stay pub?
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, new)]
pub struct TargetIdWithApid {
pub apid: Apid,
pub target: TargetId,
}
pub const ACS_OBJECT_ID: ObjectId = ObjectId {
id: RequestTargetId::AcsSubsystem as u32,
name: "ACS_SUBSYSTEM",
};
#[derive(Debug)]
pub enum GroupId {
Tmtc = 0,
Hk = 1,
impl fmt::Display for TargetIdWithApid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}, {}", self.apid, self.target)
}
}
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
pub const SERVER_PORT: u16 = 7301;
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
pub mod tmtc_err {
use super::*;
#[resultcode]
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 0);
#[resultcode]
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 1);
#[resultcode]
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 3);
#[resultcode(
info = "Not enough data inside the TC application data field. Optionally includes: \
8 bytes of failure data containing 2 failure parameters, \
P1 (u32 big endian): Expected data length, P2: Found data length"
)]
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
pub const TMTC_RESULTS: &[ResultU16Info] = &[
INVALID_PUS_SERVICE_EXT,
INVALID_PUS_SUBSERVICE_EXT,
NOT_ENOUGH_APP_DATA_EXT,
];
impl TargetIdWithApid {
pub fn apid(&self) -> Apid {
self.apid
}
pub fn target_id(&self) -> TargetId {
self.target
}
}
pub mod hk_err {
use super::*;
#[resultcode]
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 0);
#[resultcode]
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 1);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 2);
#[resultcode]
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 3);
}
#[allow(clippy::enum_variant_names)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TmSenderId {
PusVerification = 0,
PusTest = 1,
PusEvent = 2,
PusHk = 3,
PusAction = 4,
PusSched = 5,
AllEvents = 6,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TcReceiverId {
PusTest = 1,
PusEvent = 2,
PusHk = 3,
PusAction = 4,
PusSched = 5,
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()),
})
}
}

View File

@ -4,13 +4,14 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
out.finish(format_args!(
"{}[{}][{}] {}",
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(),
message
))
})
.level(log::LevelFilter::Debug)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}

View File

@ -1,149 +1,117 @@
mod acs;
mod ccsds;
mod events;
mod hk;
mod logging;
mod logger;
mod pus;
mod requests;
mod tcp;
mod tm_funnel;
mod tmtc;
mod udp;
use log::{info, warn};
use crate::events::EventHandler;
use crate::pus::stack::PusStack;
use crate::tm_funnel::{TmFunnelDynamic, TmFunnelStatic};
use log::info;
use pus::test::create_test_service_dynamic;
use satrs_core::hal::std::tcp_server::ServerConfig;
use satrs_core::hal::std::udp_server::UdpTcServer;
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_example::config::pool::{create_sched_tc_pool, create_static_pools};
use satrs_example::config::tasks::{
FREQ_MS_AOCS, FREQ_MS_EVENT_HANDLING, FREQ_MS_PUS_STACK, FREQ_MS_UDP_TMTC,
};
use satrs_example::config::{RequestTargetId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID, SERVER_PORT};
use tmtc::PusTcSourceProviderDynamic;
use udp::DynamicUdpTmHandler;
use crate::hk::AcsHkIds;
use crate::logging::setup_logger;
use crate::pus::action::{Pus8Wrapper, PusService8ActionHandler};
use crate::pus::event::Pus5Wrapper;
use crate::pus::hk::{Pus3Wrapper, PusService3HkHandler};
use crate::pus::scheduler::Pus11Wrapper;
use crate::pus::test::Service17CustomWrapper;
use crate::pus::PusTcMpscRouter;
use crate::requests::{Request, RequestWithToken};
use crate::tmtc::{core_tmtc_task, PusTcSource, TcArgs, TcStore, TmArgs, TmFunnel};
use satrs_core::event_man::{
EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider, SendEventProvider,
use crate::acs::AcsTask;
use crate::ccsds::CcsdsReceiver;
use crate::logger::setup_logger;
use crate::pus::action::{create_action_service_dynamic, create_action_service_static};
use crate::pus::event::{create_event_service_dynamic, create_event_service_static};
use crate::pus::hk::{create_hk_service_dynamic, create_hk_service_static};
use crate::pus::scheduler::{create_scheduler_service_dynamic, create_scheduler_service_static};
use crate::pus::test::create_test_service_static;
use crate::pus::{PusReceiver, PusTcMpscRouter};
use crate::requests::RequestWithToken;
use crate::tcp::{SyncTcpTmSource, TcpTask};
use crate::tmtc::{
PusTcSourceProviderSharedPool, SharedTcPool, TcSourceTaskDynamic, TcSourceTaskStatic,
};
use satrs_core::events::EventU32;
use satrs_core::hk::HkRequest;
use satrs_core::pool::{LocalPool, PoolCfg};
use satrs_core::pus::event_man::{
DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken,
PusEventDispatcher,
};
use satrs_core::pus::event_srv::PusService5EventHandler;
use satrs_core::pus::hk::Subservice as HkSubservice;
use satrs_core::pus::scheduler::PusScheduler;
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
use satrs_core::pus::test::PusService17TestHandler;
use satrs_core::pus::verification::{
TcStateStarted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
};
use satrs_core::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender};
use satrs_core::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore};
use satrs_core::spacepackets::ecss::tm::{PusTmCreator, PusTmZeroCopyWriter};
use satrs_core::spacepackets::{
ecss::tm::PusTmSecondaryHeader, time::cds::TimeProvider, time::TimeWriter, SequenceFlags,
SpHeader,
};
use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::{AddressableId, TargetId};
use crate::udp::{StaticUdpTmHandler, UdpTmtcServer};
use satrs_core::pus::event_man::EventRequestWithToken;
use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
use satrs_core::pus::{EcssTmSender, MpscTmAsVecSender, MpscTmInSharedPoolSender};
use satrs_core::spacepackets::{time::cds::TimeProvider, time::TimeWriter};
use satrs_core::tmtc::{CcsdsDistributor, TargetId};
use satrs_core::ChannelId;
use satrs_example::{
RequestTargetId, TcReceiverId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID, SERVER_PORT,
};
use satrs_example::TargetIdWithApid;
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::mpsc::{channel, TryRecvError};
use std::sync::mpsc::{self, channel};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
setup_logger().expect("setting up logging with fern failed");
println!("Running OBSW example");
let tm_pool = LocalPool::new(PoolCfg::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
let tm_store_event = shared_tm_store.clone();
let tc_pool = LocalPool::new(PoolCfg::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let tc_store = TcStore {
pool: Arc::new(RwLock::new(Box::new(tc_pool))),
};
let seq_count_provider = CcsdsSimpleSeqCountProvider::new();
let mut msg_counter_map: HashMap<u8, u16> = HashMap::new();
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
let (tc_source_tx, tc_source_rx) = channel();
let (tm_funnel_tx, tm_funnel_rx) = channel();
let (tm_server_tx, tm_server_rx) = channel();
let verif_sender = MpscTmInStoreSender::new(
TmSenderId::PusVerification as ChannelId,
"verif_sender",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
fn create_verification_reporter(verif_sender: impl EcssTmSender) -> VerificationReporterWithSender {
let verif_cfg = VerificationReporterCfg::new(PUS_APID, 1, 2, 8).unwrap();
// Every software component which needs to generate verification telemetry, gets a cloned
// verification reporter.
let verif_reporter = VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
let reporter_event_handler = verif_reporter.clone();
let reporter_aocs = verif_reporter.clone();
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender))
}
// Create event handling components
// These sender handles are used to send event requests, for example to enable or disable
// certain events
let (event_request_tx, event_request_rx) = channel::<EventRequestWithToken>();
// The sender handle is the primary sender handle for all components which want to create events.
// The event manager will receive the RX handle to receive all the events.
let (event_sender, event_man_rx) = channel();
let event_recv = MpscEventReceiver::<EventU32>::new(event_man_rx);
let test_srv_event_sender = event_sender;
let mut event_man = EventManagerWithMpscQueue::new(Box::new(event_recv));
#[allow(dead_code)]
fn static_tmtc_pool_main() {
let (tm_pool, tc_pool) = create_static_pools();
let shared_tm_pool = SharedTmPool::new(tm_pool);
let shared_tc_pool = SharedTcPool {
pool: Arc::new(RwLock::new(tc_pool)),
};
let (tc_source_tx, tc_source_rx) = channel();
let (tm_funnel_tx, tm_funnel_rx) = channel();
let (tm_server_tx, tm_server_rx) = channel();
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
// telemetry for each event.
let event_reporter = EventReporter::new(PUS_APID, 128).unwrap();
let pus_tm_backend = DefaultPusMgmtBackendProvider::<EventU32>::default();
let mut pus_event_dispatcher =
PusEventDispatcher::new(event_reporter, Box::new(pus_tm_backend));
let (pus_event_man_tx, pus_event_man_rx) = channel();
let pus_event_man_send_provider = MpscEventU32SendProvider::new(1, pus_event_man_tx);
event_man.subscribe_all(pus_event_man_send_provider.id());
event_man.add_sender(pus_event_man_send_provider);
// Every software component which needs to generate verification telemetry, receives a cloned
// verification reporter.
let verif_reporter = create_verification_reporter(MpscTmInSharedPoolSender::new(
TmSenderId::PusVerification as ChannelId,
"verif_sender",
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
));
let acs_target_id = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
// Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let mut request_map = HashMap::new();
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
request_map.insert(RequestTargetId::AcsSubsystem as TargetId, acs_thread_tx);
request_map.insert(acs_target_id, acs_thread_tx);
let tc_source_wrapper = PusTcSource {
tc_store: tc_store.clone(),
// This helper structure is used by all telecommand providers which need to send telecommands
// to the TC source.
let tc_source = PusTcSourceProviderSharedPool {
shared_pool: shared_tc_pool.clone(),
tc_source: tc_source_tx,
};
// Create clones here to allow moving the values
let tc_args = TcArgs {
tc_source: tc_source_wrapper.clone(),
tc_receiver: tc_source_rx,
};
let tm_args = TmArgs {
tm_store: shared_tm_store.clone(),
tm_sink_sender: tm_funnel_tx.clone(),
tm_server_rx,
};
// Create event handling components
// These sender handles are used to send event requests, for example to enable or disable
// certain events.
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
let aocs_tm_funnel = tm_funnel_tx.clone();
let aocs_tm_store = shared_tm_store.clone();
// The event task is the core handler to perform the event routing and TM handling as specified
// in the sat-rs documentation.
let mut event_handler = EventHandler::new(
MpscTmInSharedPoolSender::new(
TmSenderId::AllEvents as ChannelId,
"ALL_EVENTS_TX",
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
),
verif_reporter.clone(),
event_request_rx,
);
let (pus_test_tx, pus_test_rx) = channel();
let (pus_event_tx, pus_event_rx) = channel();
@ -157,335 +125,391 @@ fn main() {
hk_service_receiver: pus_hk_tx,
action_service_receiver: pus_action_tx,
};
let test_srv_tm_sender = MpscTmInStoreSender::new(
TmSenderId::PusTest as ChannelId,
"PUS_17_TM_SENDER",
shared_tm_store.clone(),
let pus_test_service = create_test_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
);
let test_srv_receiver = MpscTcInStoreReceiver::new(
TcReceiverId::PusTest as ChannelId,
"PUS_17_TC_RECV",
verif_reporter.clone(),
shared_tc_pool.pool.clone(),
event_handler.clone_event_sender(),
pus_test_rx,
);
let pus17_handler = PusService17TestHandler::new(
Box::new(test_srv_receiver),
tc_store.pool.clone(),
Box::new(test_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
);
let mut pus_17_wrapper = Service17CustomWrapper {
pus17_handler,
test_srv_event_sender,
};
let sched_srv_tm_sender = MpscTmInStoreSender::new(
TmSenderId::PusSched as ChannelId,
"PUS_11_TM_SENDER",
shared_tm_store.clone(),
let pus_scheduler_service = create_scheduler_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
);
let sched_srv_receiver = MpscTcInStoreReceiver::new(
TcReceiverId::PusSched as ChannelId,
"PUS_11_TC_RECV",
verif_reporter.clone(),
tc_source.clone(),
pus_sched_rx,
create_sched_tc_pool(),
);
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed");
let pus_11_handler = PusService11SchedHandler::new(
Box::new(sched_srv_receiver),
tc_store.pool.clone(),
Box::new(sched_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
scheduler,
);
let mut pus_11_wrapper = Pus11Wrapper {
pus_11_handler,
tc_source_wrapper,
};
let event_srv_tm_sender = MpscTmInStoreSender::new(
TmSenderId::PusEvent as ChannelId,
"PUS_5_TM_SENDER",
shared_tm_store.clone(),
let pus_event_service = create_event_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
);
let event_srv_receiver = MpscTcInStoreReceiver::new(
TcReceiverId::PusEvent as ChannelId,
"PUS_5_TC_RECV",
pus_event_rx,
);
let pus_5_handler = PusService5EventHandler::new(
Box::new(event_srv_receiver),
tc_store.pool.clone(),
Box::new(event_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
shared_tc_pool.pool.clone(),
pus_event_rx,
event_request_tx,
);
let mut pus_5_wrapper = Pus5Wrapper { pus_5_handler };
let action_srv_tm_sender = MpscTmInStoreSender::new(
TmSenderId::PusAction as ChannelId,
"PUS_8_TM_SENDER",
shared_tm_store.clone(),
let pus_action_service = create_action_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
);
let action_srv_receiver = MpscTcInStoreReceiver::new(
TcReceiverId::PusAction as ChannelId,
"PUS_8_TC_RECV",
pus_action_rx,
);
let pus_8_handler = PusService8ActionHandler::new(
Box::new(action_srv_receiver),
tc_store.pool.clone(),
Box::new(action_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
shared_tc_pool.pool.clone(),
pus_action_rx,
request_map.clone(),
);
let mut pus_8_wrapper = Pus8Wrapper { pus_8_handler };
let hk_srv_tm_sender = MpscTmInStoreSender::new(
TmSenderId::PusHk as ChannelId,
"PUS_3_TM_SENDER",
shared_tm_store.clone(),
let pus_hk_service = create_hk_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
);
let hk_srv_receiver =
MpscTcInStoreReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
let pus_3_handler = PusService3HkHandler::new(
Box::new(hk_srv_receiver),
tc_store.pool.clone(),
Box::new(hk_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
shared_tc_pool.pool.clone(),
pus_hk_rx,
request_map,
);
let mut pus_3_wrapper = Pus3Wrapper { pus_3_handler };
let mut pus_stack = PusStack::new(
pus_hk_service,
pus_event_service,
pus_action_service,
pus_scheduler_service,
pus_test_service,
);
info!("Starting TMTC task");
let jh0 = thread::Builder::new()
.name("TMTC".to_string())
let ccsds_receiver = CcsdsReceiver { tc_source };
let mut tmtc_task = TcSourceTaskStatic::new(
shared_tc_pool.clone(),
tc_source_rx,
PusReceiver::new(verif_reporter.clone(), pus_router),
);
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
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_handler: StaticUdpTmHandler {
tm_rx: tm_server_rx,
tm_store: shared_tm_pool.clone_backing_pool(),
},
};
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 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");
let mut acs_task = AcsTask::new(
MpscTmInSharedPoolSender::new(
TmSenderId::AcsSubsystem as ChannelId,
"ACS_TASK_SENDER",
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
),
acs_thread_rx,
verif_reporter,
);
let mut tm_funnel = TmFunnelStatic::new(
shared_tm_pool,
sync_tm_tcp_source,
tm_funnel_rx,
tm_server_tx,
);
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC and UDP".to_string())
.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(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
.spawn(move || {
info!("Running TCP server on port {SERVER_PORT}");
loop {
tcp_server.periodic_operation();
}
})
.unwrap();
info!("Starting TM funnel task");
let jh1 = thread::Builder::new()
let jh_tm_funnel = thread::Builder::new()
.name("TM Funnel".to_string())
.spawn(move || {
let tm_funnel = TmFunnel {
tm_server_tx,
tm_funnel_rx,
};
loop {
if let Ok(addr) = tm_funnel.tm_funnel_rx.recv() {
// Read the TM, set sequence counter and message counter, and finally update
// the CRC.
let shared_pool = shared_tm_store.clone_backing_pool();
let mut pool_guard = shared_pool.write().expect("Locking TM pool failed");
let tm_raw = pool_guard
.modify(&addr)
.expect("Reading TM from pool failed");
let mut zero_copy_writer = PusTmZeroCopyWriter::new(tm_raw)
.expect("Creating TM zero copy writer failed");
zero_copy_writer.set_apid(PUS_APID);
zero_copy_writer.set_seq_count(seq_count_provider.get_and_increment());
let entry = msg_counter_map
.entry(zero_copy_writer.service())
.or_insert(0);
zero_copy_writer.set_msg_count(*entry);
if *entry == u16::MAX {
*entry = 0;
} else {
*entry += 1;
}
// This operation has to come last!
zero_copy_writer.finish();
tm_funnel
.tm_server_tx
.send(addr)
.expect("Sending TM to server failed");
}
}
.spawn(move || loop {
tm_funnel.operation();
})
.unwrap();
info!("Starting event handling task");
let jh2 = thread::Builder::new()
let jh_event_handling = thread::Builder::new()
.name("Event".to_string())
.spawn(move || {
let mut timestamp: [u8; 7] = [0; 7];
let mut sender = MpscTmInStoreSender::new(
TmSenderId::AllEvents as ChannelId,
"ALL_EVENTS_TX",
tm_store_event.clone(),
tm_funnel_tx,
);
let mut time_provider = TimeProvider::new_with_u16_days(0, 0);
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
let started_token: VerificationToken<TcStateStarted> = event_req
.token
.try_into()
.expect("expected start verification token");
reporter_event_handler
.completion_success(started_token, Some(timestamp))
.expect("Sending completion success failed");
};
loop {
// handle event requests
if let Ok(event_req) = event_request_rx.try_recv() {
match event_req.request {
EventRequest::Enable(event) => {
pus_event_dispatcher
.enable_tm_for_event(&event)
.expect("Enabling TM failed");
update_time(&mut time_provider, &mut timestamp);
report_completion(event_req, &timestamp);
}
EventRequest::Disable(event) => {
pus_event_dispatcher
.disable_tm_for_event(&event)
.expect("Disabling TM failed");
update_time(&mut time_provider, &mut timestamp);
report_completion(event_req, &timestamp);
}
}
}
// Perform the event routing.
event_man
.try_event_handling()
.expect("event handling failed");
// Perform the generation of PUS event packets
if let Ok((event, _param)) = pus_event_man_rx.try_recv() {
update_time(&mut time_provider, &mut timestamp);
pus_event_dispatcher
.generate_pus_event_tm_generic(&mut sender, &timestamp, event, None)
.expect("Sending TM as event failed");
}
thread::sleep(Duration::from_millis(400));
}
.spawn(move || loop {
event_handler.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
})
.unwrap();
info!("Starting AOCS thread");
let jh3 = thread::Builder::new()
let jh_aocs = thread::Builder::new()
.name("AOCS".to_string())
.spawn(move || {
let mut timestamp: [u8; 7] = [0; 7];
let mut time_provider = TimeProvider::new_with_u16_days(0, 0);
loop {
match acs_thread_rx.try_recv() {
Ok(request) => {
info!(
"ACS thread: Received HK request {:?}",
request.targeted_request
);
update_time(&mut time_provider, &mut timestamp);
match request.targeted_request.request {
Request::Hk(hk_req) => match hk_req {
HkRequest::OneShot(unique_id) => {
let target = request.targeted_request.target_id;
assert_eq!(target, RequestTargetId::AcsSubsystem as u32);
if request.targeted_request.target_id
== AcsHkIds::TestMgmSet as u32
{
let mut sp_header = SpHeader::tm(
PUS_APID,
SequenceFlags::Unsegmented,
0,
0,
)
.unwrap();
let sec_header = PusTmSecondaryHeader::new_simple(
3,
HkSubservice::TmHkPacket as u8,
&timestamp,
);
let mut buf: [u8; 8] = [0; 8];
let addressable_id = AddressableId {
target_id: target,
unique_id,
};
addressable_id.write_to_be_bytes(&mut buf).unwrap();
let pus_tm = PusTmCreator::new(
&mut sp_header,
sec_header,
Some(&buf),
true,
);
let addr = aocs_tm_store
.add_pus_tm(&pus_tm)
.expect("Adding PUS TM failed");
aocs_tm_funnel.send(addr).expect("Sending HK TM failed");
}
}
HkRequest::Enable(_) => {}
HkRequest::Disable(_) => {}
HkRequest::ModifyCollectionInterval(_, _) => {}
},
Request::Mode(_mode_req) => {
warn!("mode request handling not implemented yet")
}
Request::Action(_action_req) => {
warn!("action request handling not implemented yet")
}
}
let started_token = reporter_aocs
.start_success(request.token, Some(&timestamp))
.expect("Sending start success failed");
reporter_aocs
.completion_success(started_token, Some(&timestamp))
.expect("Sending completion success failed");
}
Err(e) => match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
warn!("ACS thread: Message Queue TX disconnected!")
}
},
}
thread::sleep(Duration::from_millis(500));
}
.spawn(move || loop {
acs_task.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
})
.unwrap();
info!("Starting PUS handler thread");
let jh4 = thread::Builder::new()
let jh_pus_handler = thread::Builder::new()
.name("PUS".to_string())
.spawn(move || loop {
pus_11_wrapper.release_tcs();
loop {
let mut all_queues_empty = true;
let mut is_srv_finished = |srv_handler_finished: bool| {
if !srv_handler_finished {
all_queues_empty = false;
}
};
is_srv_finished(pus_17_wrapper.handle_next_packet());
is_srv_finished(pus_11_wrapper.handle_next_packet());
is_srv_finished(pus_5_wrapper.handle_next_packet());
is_srv_finished(pus_8_wrapper.handle_next_packet());
is_srv_finished(pus_3_wrapper.handle_next_packet());
if all_queues_empty {
break;
}
}
thread::sleep(Duration::from_millis(200));
pus_stack.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
})
.unwrap();
jh0.join().expect("Joining UDP TMTC server thread failed");
jh1.join().expect("Joining TM Funnel thread failed");
jh2.join().expect("Joining Event Manager thread failed");
jh3.join().expect("Joining AOCS thread failed");
jh4.join().expect("Joining PUS handler thread failed");
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh_tm_funnel
.join()
.expect("Joining TM Funnel thread failed");
jh_event_handling
.join()
.expect("Joining Event Manager thread failed");
jh_aocs.join().expect("Joining AOCS thread failed");
jh_pus_handler
.join()
.expect("Joining PUS handler thread failed");
}
#[allow(dead_code)]
fn dyn_tmtc_pool_main() {
let (tc_source_tx, tc_source_rx) = channel();
let (tm_funnel_tx, tm_funnel_rx) = channel();
let (tm_server_tx, tm_server_rx) = channel();
// Every software component which needs to generate verification telemetry, gets a cloned
// verification reporter.
let verif_reporter = create_verification_reporter(MpscTmAsVecSender::new(
TmSenderId::PusVerification as ChannelId,
"verif_sender",
tm_funnel_tx.clone(),
));
let acs_target_id = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
// Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let mut request_map = HashMap::new();
request_map.insert(acs_target_id, acs_thread_tx);
let tc_source = PusTcSourceProviderDynamic(tc_source_tx);
// Create event handling components
// These sender handles are used to send event requests, for example to enable or disable
// certain events.
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
// The event task is the core handler to perform the event routing and TM handling as specified
// in the sat-rs documentation.
let mut event_handler = EventHandler::new(
MpscTmAsVecSender::new(
TmSenderId::AllEvents as ChannelId,
"ALL_EVENTS_TX",
tm_funnel_tx.clone(),
),
verif_reporter.clone(),
event_request_rx,
);
let (pus_test_tx, pus_test_rx) = channel();
let (pus_event_tx, pus_event_rx) = channel();
let (pus_sched_tx, pus_sched_rx) = channel();
let (pus_hk_tx, pus_hk_rx) = channel();
let (pus_action_tx, pus_action_rx) = channel();
let pus_router = PusTcMpscRouter {
test_service_receiver: pus_test_tx,
event_service_receiver: pus_event_tx,
sched_service_receiver: pus_sched_tx,
hk_service_receiver: pus_hk_tx,
action_service_receiver: pus_action_tx,
};
let pus_test_service = create_test_service_dynamic(
tm_funnel_tx.clone(),
verif_reporter.clone(),
event_handler.clone_event_sender(),
pus_test_rx,
);
let pus_scheduler_service = create_scheduler_service_dynamic(
tm_funnel_tx.clone(),
verif_reporter.clone(),
tc_source.0.clone(),
pus_sched_rx,
create_sched_tc_pool(),
);
let pus_event_service = create_event_service_dynamic(
tm_funnel_tx.clone(),
verif_reporter.clone(),
pus_event_rx,
event_request_tx,
);
let pus_action_service = create_action_service_dynamic(
tm_funnel_tx.clone(),
verif_reporter.clone(),
pus_action_rx,
request_map.clone(),
);
let pus_hk_service = create_hk_service_dynamic(
tm_funnel_tx.clone(),
verif_reporter.clone(),
pus_hk_rx,
request_map,
);
let mut pus_stack = PusStack::new(
pus_hk_service,
pus_event_service,
pus_action_service,
pus_scheduler_service,
pus_test_service,
);
let ccsds_receiver = CcsdsReceiver { tc_source };
let mut tmtc_task = TcSourceTaskDynamic::new(
tc_source_rx,
PusReceiver::new(verif_reporter.clone(), pus_router),
);
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
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_handler: DynamicUdpTmHandler {
tm_rx: tm_server_rx,
},
};
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 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");
let mut acs_task = AcsTask::new(
MpscTmAsVecSender::new(
TmSenderId::AcsSubsystem as ChannelId,
"ACS_TASK_SENDER",
tm_funnel_tx.clone(),
),
acs_thread_rx,
verif_reporter,
);
let mut tm_funnel = TmFunnelDynamic::new(sync_tm_tcp_source, tm_funnel_rx, tm_server_tx);
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC and UDP".to_string())
.spawn(move || {
info!("Running UDP server on port {SERVER_PORT}");
loop {
udp_tmtc_server.periodic_operation();
tmtc_task.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
.spawn(move || {
info!("Running TCP server on port {SERVER_PORT}");
loop {
tcp_server.periodic_operation();
}
})
.unwrap();
info!("Starting TM funnel task");
let jh_tm_funnel = thread::Builder::new()
.name("TM Funnel".to_string())
.spawn(move || loop {
tm_funnel.operation();
})
.unwrap();
info!("Starting event handling task");
let jh_event_handling = thread::Builder::new()
.name("Event".to_string())
.spawn(move || loop {
event_handler.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
})
.unwrap();
info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new()
.name("AOCS".to_string())
.spawn(move || loop {
acs_task.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
})
.unwrap();
info!("Starting PUS handler thread");
let jh_pus_handler = thread::Builder::new()
.name("PUS".to_string())
.spawn(move || loop {
pus_stack.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
})
.unwrap();
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh_tm_funnel
.join()
.expect("Joining TM Funnel thread failed");
jh_event_handling
.join()
.expect("Joining Event Manager thread failed");
jh_aocs.join().expect("Joining AOCS thread failed");
jh_pus_handler
.join()
.expect("Joining PUS handler thread failed");
}
fn main() {
setup_logger().expect("setting up logging with fern failed");
println!("Running OBSW example");
#[cfg(not(feature = "dyn_tmtc"))]
static_tmtc_pool_main();
#[cfg(feature = "dyn_tmtc")]
dyn_tmtc_pool_main();
}
pub fn update_time(time_provider: &mut TimeProvider, timestamp: &mut [u8]) {

View File

@ -1,48 +1,106 @@
use crate::requests::{ActionRequest, Request, RequestWithToken};
use log::{error, warn};
use satrs_core::pool::{SharedPool, StoreAddr};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
FailParams, TcStateAccepted, VerificationReporterWithSender, VerificationToken,
};
use satrs_core::pus::{
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
PusServiceHandler,
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::tmtc::TargetId;
use satrs_example::tmtc_err;
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_example::config::{tmtc_err, TcReceiverId, TmSenderId, PUS_APID};
use satrs_example::TargetIdWithApid;
use std::collections::HashMap;
use std::sync::mpsc::Sender;
use std::sync::mpsc::{self, Sender};
pub struct PusService8ActionHandler {
psb: PusServiceBase,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
pub fn create_action_service_static(
shared_tm_store: SharedTmPool,
tm_funnel_tx: mpsc::Sender<StoreAddr>,
verif_reporter: VerificationReporterWithSender,
tc_pool: SharedStaticMemoryPool,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
) -> Pus8Wrapper<EcssTcInSharedStoreConverter> {
let action_srv_tm_sender = MpscTmInSharedPoolSender::new(
TmSenderId::PusAction as ChannelId,
"PUS_8_TM_SENDER",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
let action_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusAction as ChannelId,
"PUS_8_TC_RECV",
pus_action_rx,
);
let pus_8_handler = PusService8ActionHandler::new(
Box::new(action_srv_receiver),
Box::new(action_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
request_map.clone(),
);
Pus8Wrapper { pus_8_handler }
}
impl PusService8ActionHandler {
pub fn create_action_service_dynamic(
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
verif_reporter: VerificationReporterWithSender,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
) -> Pus8Wrapper<EcssTcInVecConverter> {
let action_srv_tm_sender = MpscTmAsVecSender::new(
TmSenderId::PusAction as ChannelId,
"PUS_8_TM_SENDER",
tm_funnel_tx.clone(),
);
let action_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusAction as ChannelId,
"PUS_8_TC_RECV",
pus_action_rx,
);
let pus_8_handler = PusService8ActionHandler::new(
Box::new(action_srv_receiver),
Box::new(action_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInVecConverter::default(),
request_map.clone(),
);
Pus8Wrapper { pus_8_handler }
}
pub struct PusService8ActionHandler<TcInMemConverter: EcssTcInMemConverter> {
service_helper: PusServiceHelper<TcInMemConverter>,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
}
impl<TcInMemConverter: EcssTcInMemConverter> PusService8ActionHandler<TcInMemConverter> {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_pool: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
verification_handler: VerificationReporterWithSender,
tc_in_mem_converter: TcInMemConverter,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
) -> Self {
Self {
psb: PusServiceBase::new(
service_helper: PusServiceHelper::new(
tc_receiver,
shared_tc_pool,
tm_sender,
tm_apid,
verification_handler,
tc_in_mem_converter,
),
request_handlers,
}
}
}
impl PusService8ActionHandler {
fn handle_action_request_with_id(
&self,
token: VerificationToken<TcStateAccepted>,
@ -51,7 +109,8 @@ impl PusService8ActionHandler {
) -> Result<(), PusPacketHandlingError> {
let user_data = tc.user_data();
if user_data.len() < 8 {
self.psb()
self.service_helper
.common
.verification_handler
.borrow_mut()
.start_failure(
@ -63,7 +122,8 @@ impl PusService8ActionHandler {
"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());
if let Some(sender) = self.request_handlers.get(&target_id) {
sender
@ -78,8 +138,9 @@ impl PusService8ActionHandler {
.expect("Forwarding action request failed");
} else {
let mut fail_data: [u8; 4] = [0; 4];
fail_data.copy_from_slice(&target_id.to_be_bytes());
self.psb()
fail_data.copy_from_slice(&target_id.target.to_be_bytes());
self.service_helper
.common
.verification_handler
.borrow_mut()
.start_failure(
@ -97,37 +158,32 @@ impl PusService8ActionHandler {
}
Ok(())
}
}
impl PusServiceHandler for PusService8ActionHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase {
&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).unwrap();
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
}
let ecss_tc_and_token = possible_packet.unwrap();
self.service_helper
.tc_in_mem_converter
.cache_ecss_tc_in_memory(&ecss_tc_and_token.tc_in_memory)?;
let tc = PusTcReader::new(self.service_helper.tc_in_mem_converter.tc_slice_raw())?.0;
let subservice = tc.subservice();
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 {
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];
self.psb_mut()
self.service_helper
.common
.verification_handler
.get_mut()
.start_failure(
token,
ecss_tc_and_token.token,
FailParams::new(
Some(&time_stamp),
&tmtc_err::INVALID_PUS_SUBSERVICE,
@ -147,13 +203,13 @@ impl PusServiceHandler for PusService8ActionHandler {
}
}
pub struct Pus8Wrapper {
pub(crate) pus_8_handler: PusService8ActionHandler,
pub struct Pus8Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub(crate) pus_8_handler: PusService8ActionHandler<TcInMemConverter>,
}
impl Pus8Wrapper {
impl<TcInMemConverter: EcssTcInMemConverter> Pus8Wrapper<TcInMemConverter> {
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 {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@ -1,14 +1,87 @@
use log::{error, warn};
use satrs_core::pus::event_srv::PusService5EventHandler;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
use std::sync::mpsc;
pub struct Pus5Wrapper {
pub pus_5_handler: PusService5EventHandler,
use log::{error, warn};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::event_man::EventRequestWithToken;
use satrs_core::pus::event_srv::PusService5EventHandler;
use satrs_core::pus::verification::VerificationReporterWithSender;
use satrs_core::pus::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
PusServiceHelper,
};
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_example::config::{TcReceiverId, TmSenderId, PUS_APID};
pub fn create_event_service_static(
shared_tm_store: SharedTmPool,
tm_funnel_tx: mpsc::Sender<StoreAddr>,
verif_reporter: VerificationReporterWithSender,
tc_pool: SharedStaticMemoryPool,
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
event_request_tx: mpsc::Sender<EventRequestWithToken>,
) -> Pus5Wrapper<EcssTcInSharedStoreConverter> {
let event_srv_tm_sender = MpscTmInSharedPoolSender::new(
TmSenderId::PusEvent as ChannelId,
"PUS_5_TM_SENDER",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
let event_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusEvent as ChannelId,
"PUS_5_TC_RECV",
pus_event_rx,
);
let pus_5_handler = PusService5EventHandler::new(
PusServiceHelper::new(
Box::new(event_srv_receiver),
Box::new(event_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
),
event_request_tx,
);
Pus5Wrapper { pus_5_handler }
}
impl Pus5Wrapper {
pub fn create_event_service_dynamic(
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
verif_reporter: VerificationReporterWithSender,
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
event_request_tx: mpsc::Sender<EventRequestWithToken>,
) -> Pus5Wrapper<EcssTcInVecConverter> {
let event_srv_tm_sender = MpscTmAsVecSender::new(
TmSenderId::PusEvent as ChannelId,
"PUS_5_TM_SENDER",
tm_funnel_tx,
);
let event_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusEvent as ChannelId,
"PUS_5_TC_RECV",
pus_event_rx,
);
let pus_5_handler = PusService5EventHandler::new(
PusServiceHelper::new(
Box::new(event_srv_receiver),
Box::new(event_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInVecConverter::default(),
),
event_request_tx,
);
Pus5Wrapper { pus_5_handler }
}
pub struct Pus5Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub pus_5_handler: PusService5EventHandler<TcInMemConverter>,
}
impl<TcInMemConverter: EcssTcInMemConverter> Pus5Wrapper<TcInMemConverter> {
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 {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@ -1,73 +1,121 @@
use crate::requests::{Request, RequestWithToken};
use log::{error, warn};
use satrs_core::hk::{CollectionIntervalFactor, HkRequest};
use satrs_core::pool::{SharedPool, StoreAddr};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
FailParams, StdVerifReporterWithSender, VerificationReporterWithSender,
};
use satrs_core::pus::{
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
PusServiceHandler,
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::{hk, PusPacket};
use satrs_core::tmtc::{AddressableId, TargetId};
use satrs_example::{hk_err, tmtc_err};
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_example::config::{hk_err, tmtc_err, TcReceiverId, TmSenderId, PUS_APID};
use satrs_example::TargetIdWithApid;
use std::collections::HashMap;
use std::sync::mpsc::Sender;
use std::sync::mpsc::{self, Sender};
pub struct PusService3HkHandler {
psb: PusServiceBase,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
pub fn create_hk_service_static(
shared_tm_store: SharedTmPool,
tm_funnel_tx: mpsc::Sender<StoreAddr>,
verif_reporter: VerificationReporterWithSender,
tc_pool: SharedStaticMemoryPool,
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
) -> Pus3Wrapper<EcssTcInSharedStoreConverter> {
let hk_srv_tm_sender = MpscTmInSharedPoolSender::new(
TmSenderId::PusHk as ChannelId,
"PUS_3_TM_SENDER",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
let hk_srv_receiver =
MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
let pus_3_handler = PusService3HkHandler::new(
Box::new(hk_srv_receiver),
Box::new(hk_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
request_map,
);
Pus3Wrapper { pus_3_handler }
}
impl PusService3HkHandler {
pub fn create_hk_service_dynamic(
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
verif_reporter: VerificationReporterWithSender,
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
) -> Pus3Wrapper<EcssTcInVecConverter> {
let hk_srv_tm_sender = MpscTmAsVecSender::new(
TmSenderId::PusHk as ChannelId,
"PUS_3_TM_SENDER",
tm_funnel_tx.clone(),
);
let hk_srv_receiver =
MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
let pus_3_handler = PusService3HkHandler::new(
Box::new(hk_srv_receiver),
Box::new(hk_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInVecConverter::default(),
request_map,
);
Pus3Wrapper { pus_3_handler }
}
pub struct PusService3HkHandler<TcInMemConverter: EcssTcInMemConverter> {
psb: PusServiceHelper<TcInMemConverter>,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
}
impl<TcInMemConverter: EcssTcInMemConverter> PusService3HkHandler<TcInMemConverter> {
pub fn new(
tc_receiver: Box<dyn EcssTcReceiver>,
shared_tc_pool: SharedPool,
tm_sender: Box<dyn EcssTmSender>,
tm_apid: u16,
verification_handler: StdVerifReporterWithSender,
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
tc_in_mem_converter: TcInMemConverter,
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
) -> Self {
Self {
psb: PusServiceBase::new(
psb: PusServiceHelper::new(
tc_receiver,
shared_tc_pool,
tm_sender,
tm_apid,
verification_handler,
tc_in_mem_converter,
),
request_handlers,
}
}
}
impl PusServiceHandler for PusService3HkHandler {
fn psb_mut(&mut self) -> &mut PusServiceBase {
&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).unwrap();
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
let possible_packet = self.psb.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(PusPacketHandlerResult::Empty);
}
let ecss_tc_and_token = possible_packet.unwrap();
let tc = self
.psb
.tc_in_mem_converter
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
let subservice = tc.subservice();
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();
if user_data.is_empty() {
self.psb
.common
.verification_handler
.borrow_mut()
.start_failure(
token,
ecss_tc_and_token.token,
FailParams::new(Some(&time_stamp), &tmtc_err::NOT_ENOUGH_APP_DATA, None),
)
.expect("Sending start failure TM failed");
@ -82,63 +130,58 @@ impl PusServiceHandler for PusService3HkHandler {
&hk_err::UNIQUE_ID_MISSING
};
self.psb
.common
.verification_handler
.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");
return Err(PusPacketHandlingError::NotEnoughAppData(
"Expected at least 8 bytes of app data".into(),
));
}
let addressable_id = AddressableId::from_raw_be(user_data).unwrap();
if !self
.request_handlers
.contains_key(&addressable_id.target_id)
{
let target_id = TargetIdWithApid::from_tc(&tc).expect("invalid tc format");
let unique_id = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap());
if !self.request_handlers.contains_key(&target_id) {
self.psb
.common
.verification_handler
.borrow_mut()
.start_failure(
token,
ecss_tc_and_token.token,
FailParams::new(Some(&time_stamp), &hk_err::UNKNOWN_TARGET_ID, None),
)
.expect("Sending start failure TM failed");
let tgt_id = addressable_id.target_id;
return Err(PusPacketHandlingError::NotEnoughAppData(format!(
"Unknown target ID {tgt_id}"
"Unknown target ID {target_id}"
)));
}
let send_request = |target: TargetId, request: HkRequest| {
let sender = self
.request_handlers
.get(&addressable_id.target_id)
.unwrap();
let send_request = |target: TargetIdWithApid, request: HkRequest| {
let sender = self.request_handlers.get(&target).unwrap();
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"));
};
if subservice == hk::Subservice::TcEnableHkGeneration as u8 {
send_request(
addressable_id.target_id,
HkRequest::Enable(addressable_id.unique_id),
);
send_request(target_id, HkRequest::Enable(unique_id));
} else if subservice == hk::Subservice::TcDisableHkGeneration as u8 {
send_request(
addressable_id.target_id,
HkRequest::Disable(addressable_id.unique_id),
);
send_request(target_id, HkRequest::Disable(unique_id));
} else if subservice == hk::Subservice::TcGenerateOneShotHk as u8 {
send_request(
addressable_id.target_id,
HkRequest::OneShot(addressable_id.unique_id),
);
send_request(target_id, HkRequest::OneShot(unique_id));
} else if subservice == hk::Subservice::TcModifyHkCollectionInterval as u8 {
if user_data.len() < 12 {
self.psb
.common
.verification_handler
.borrow_mut()
.start_failure(
token,
ecss_tc_and_token.token,
FailParams::new(
Some(&time_stamp),
&hk_err::COLLECTION_INTERVAL_MISSING,
@ -151,9 +194,9 @@ impl PusServiceHandler for PusService3HkHandler {
));
}
send_request(
addressable_id.target_id,
target_id,
HkRequest::ModifyCollectionInterval(
addressable_id.unique_id,
unique_id,
CollectionIntervalFactor::from_be_bytes(user_data[8..12].try_into().unwrap()),
),
);
@ -162,13 +205,13 @@ impl PusServiceHandler for PusService3HkHandler {
}
}
pub struct Pus3Wrapper {
pub(crate) pus_3_handler: PusService3HkHandler,
pub struct Pus3Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub(crate) pus_3_handler: PusService3HkHandler<TcInMemConverter>,
}
impl Pus3Wrapper {
impl<TcInMemConverter: EcssTcInMemConverter> Pus3Wrapper<TcInMemConverter> {
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 {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {

View File

@ -1,27 +1,27 @@
use crate::tmtc::MpscStoreAndSendError;
use log::warn;
use satrs_core::pool::StoreAddr;
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::PusServiceId;
use satrs_core::spacepackets::time::cds::TimeProvider;
use satrs_core::spacepackets::time::TimeWriter;
use satrs_example::{tmtc_err, CustomPusServiceId};
use satrs_example::config::{tmtc_err, CustomPusServiceId};
use std::sync::mpsc::Sender;
pub mod action;
pub mod event;
pub mod hk;
pub mod scheduler;
pub mod stack;
pub mod test;
pub struct PusTcMpscRouter {
pub test_service_receiver: Sender<TcAddrWithToken>,
pub event_service_receiver: Sender<TcAddrWithToken>,
pub sched_service_receiver: Sender<TcAddrWithToken>,
pub hk_service_receiver: Sender<TcAddrWithToken>,
pub action_service_receiver: Sender<TcAddrWithToken>,
pub test_service_receiver: Sender<EcssTcAndToken>,
pub event_service_receiver: Sender<EcssTcAndToken>,
pub sched_service_receiver: Sender<EcssTcAndToken>,
pub hk_service_receiver: Sender<EcssTcAndToken>,
pub action_service_receiver: Sender<EcssTcAndToken>,
}
pub struct PusReceiver {
@ -70,7 +70,7 @@ impl PusReceiver {
impl PusReceiver {
pub fn handle_tc_packet(
&mut self,
store_addr: StoreAddr,
tc_in_memory: TcInMemory,
service: u8,
pus_tc: &PusTcReader,
) -> Result<PusPacketHandlerResult, MpscStoreAndSendError> {
@ -84,22 +84,33 @@ impl PusReceiver {
match service {
Ok(standard_service) => match standard_service {
PusServiceId::Test => {
self.pus_router
.test_service_receiver
.send((store_addr, accepted_token.into()))?;
self.pus_router.test_service_receiver.send(EcssTcAndToken {
tc_in_memory,
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(
accepted_token,

View File

@ -1,50 +1,89 @@
use crate::tmtc::PusTcSource;
use log::{error, info, warn};
use satrs_core::pus::scheduler::TcInfo;
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
use std::sync::mpsc;
use std::time::Duration;
pub struct Pus11Wrapper {
pub pus_11_handler: PusService11SchedHandler,
pub tc_source_wrapper: PusTcSource,
use log::{error, info, warn};
use satrs_core::pool::{PoolProvider, StaticMemoryPool, StoreAddr};
use satrs_core::pus::scheduler::{PusScheduler, TcInfo};
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
use satrs_core::pus::verification::VerificationReporterWithSender;
use satrs_core::pus::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
PusServiceHelper,
};
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_example::config::{TcReceiverId, TmSenderId, PUS_APID};
use crate::tmtc::PusTcSourceProviderSharedPool;
pub trait TcReleaser {
fn release(&mut self, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
}
impl Pus11Wrapper {
pub fn release_tcs(&mut self) {
let releaser = |enabled: bool, info: &TcInfo| -> bool {
if enabled {
self.tc_source_wrapper
.tc_source
.send(info.addr())
.expect("sending TC to TC source failed");
}
true
};
impl TcReleaser for PusTcSourceProviderSharedPool {
fn release(&mut self, enabled: bool, _info: &TcInfo, tc: &[u8]) -> bool {
if enabled {
// Transfer TC from scheduler TC pool to shared TC pool.
let released_tc_addr = self
.shared_pool
.pool
.write()
.expect("locking pool failed")
.add(tc)
.expect("adding TC to shared pool failed");
self.tc_source
.send(released_tc_addr)
.expect("sending TC to TC source failed");
}
true
}
}
let mut pool = self
.tc_source_wrapper
.tc_store
.pool
.write()
.expect("error locking pool");
impl TcReleaser for mpsc::Sender<Vec<u8>> {
fn release(&mut self, enabled: bool, _info: &TcInfo, tc: &[u8]) -> bool {
if enabled {
// Send released TC to centralized TC source.
self.send(tc.to_vec())
.expect("sending TC to TC source failed");
}
true
}
}
pub struct Pus11Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub pus_11_handler: PusService11SchedHandler<TcInMemConverter, PusScheduler>,
pub sched_tc_pool: StaticMemoryPool,
pub releaser_buf: [u8; 4096],
pub tc_releaser: Box<dyn TcReleaser + Send>,
}
impl<TcInMemConverter: EcssTcInMemConverter> Pus11Wrapper<TcInMemConverter> {
pub fn release_tcs(&mut self) {
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
self.tc_releaser.release(enabled, info, tc)
};
self.pus_11_handler
.scheduler_mut()
.update_time_from_now()
.unwrap();
if let Ok(released_tcs) = self
let released_tcs = self
.pus_11_handler
.scheduler_mut()
.release_telecommands(releaser, pool.as_mut())
{
if released_tcs > 0 {
info!("{released_tcs} TC(s) released from scheduler");
}
.release_telecommands_with_buffer(
releaser,
&mut self.sched_tc_pool,
&mut self.releaser_buf,
)
.expect("releasing TCs failed");
if released_tcs > 0 {
info!("{released_tcs} TC(s) released from scheduler");
}
}
pub fn 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 {
PusPacketHandlerResult::RequestHandled => {}
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
@ -67,3 +106,79 @@ impl Pus11Wrapper {
false
}
}
pub fn create_scheduler_service_static(
shared_tm_store: SharedTmPool,
tm_funnel_tx: mpsc::Sender<StoreAddr>,
verif_reporter: VerificationReporterWithSender,
tc_releaser: PusTcSourceProviderSharedPool,
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
sched_tc_pool: StaticMemoryPool,
) -> Pus11Wrapper<EcssTcInSharedStoreConverter> {
let sched_srv_tm_sender = MpscTmInSharedPoolSender::new(
TmSenderId::PusSched as ChannelId,
"PUS_11_TM_SENDER",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
let sched_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusSched as ChannelId,
"PUS_11_TC_RECV",
pus_sched_rx,
);
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed");
let pus_11_handler = PusService11SchedHandler::new(
PusServiceHelper::new(
Box::new(sched_srv_receiver),
Box::new(sched_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_releaser.clone_backing_pool(), 2048),
),
scheduler,
);
Pus11Wrapper {
pus_11_handler,
sched_tc_pool,
releaser_buf: [0; 4096],
tc_releaser: Box::new(tc_releaser),
}
}
pub fn create_scheduler_service_dynamic(
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
verif_reporter: VerificationReporterWithSender,
tc_source_sender: mpsc::Sender<Vec<u8>>,
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
sched_tc_pool: StaticMemoryPool,
) -> Pus11Wrapper<EcssTcInVecConverter> {
let sched_srv_tm_sender = MpscTmAsVecSender::new(
TmSenderId::PusSched as ChannelId,
"PUS_11_TM_SENDER",
tm_funnel_tx,
);
let sched_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusSched as ChannelId,
"PUS_11_TC_RECV",
pus_sched_rx,
);
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed");
let pus_11_handler = PusService11SchedHandler::new(
PusServiceHelper::new(
Box::new(sched_srv_receiver),
Box::new(sched_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInVecConverter::default(),
),
scheduler,
);
Pus11Wrapper {
pus_11_handler,
sched_tc_pool,
releaser_buf: [0; 4096],
tc_releaser: Box::new(tc_source_sender),
}
}

View File

@ -0,0 +1,52 @@
use satrs_core::pus::EcssTcInMemConverter;
use super::{
action::Pus8Wrapper, event::Pus5Wrapper, hk::Pus3Wrapper, scheduler::Pus11Wrapper,
test::Service17CustomWrapper,
};
pub struct PusStack<TcInMemConverter: EcssTcInMemConverter> {
event_srv: Pus5Wrapper<TcInMemConverter>,
hk_srv: Pus3Wrapper<TcInMemConverter>,
action_srv: Pus8Wrapper<TcInMemConverter>,
schedule_srv: Pus11Wrapper<TcInMemConverter>,
test_srv: Service17CustomWrapper<TcInMemConverter>,
}
impl<TcInMemConverter: EcssTcInMemConverter> PusStack<TcInMemConverter> {
pub fn new(
hk_srv: Pus3Wrapper<TcInMemConverter>,
event_srv: Pus5Wrapper<TcInMemConverter>,
action_srv: Pus8Wrapper<TcInMemConverter>,
schedule_srv: Pus11Wrapper<TcInMemConverter>,
test_srv: Service17CustomWrapper<TcInMemConverter>,
) -> Self {
Self {
event_srv,
action_srv,
schedule_srv,
test_srv,
hk_srv,
}
}
pub fn periodic_operation(&mut self) {
self.schedule_srv.release_tcs();
loop {
let mut all_queues_empty = true;
let mut is_srv_finished = |srv_handler_finished: bool| {
if !srv_handler_finished {
all_queues_empty = false;
}
};
is_srv_finished(self.test_srv.handle_next_packet());
is_srv_finished(self.schedule_srv.handle_next_packet());
is_srv_finished(self.event_srv.handle_next_packet());
is_srv_finished(self.action_srv.handle_next_packet());
is_srv_finished(self.hk_srv.handle_next_packet());
if all_queues_empty {
break;
}
}
}
}

View File

@ -1,24 +1,91 @@
use log::{info, warn};
use satrs_core::events::EventU32;
use satrs_core::params::Params;
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::test::PusService17TestHandler;
use satrs_core::pus::verification::FailParams;
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
use satrs_core::pus::verification::{FailParams, VerificationReporterWithSender};
use satrs_core::pus::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInVecConverter, MpscTcReceiver, MpscTmAsVecSender,
MpscTmInSharedPoolSender, PusPacketHandlerResult, PusServiceHelper,
};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::time::cds::TimeProvider;
use satrs_core::spacepackets::time::TimeWriter;
use satrs_example::{tmtc_err, TEST_EVENT};
use std::sync::mpsc::Sender;
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_core::{events::EventU32, pus::EcssTcInSharedStoreConverter};
use satrs_example::config::{tmtc_err, TcReceiverId, TmSenderId, PUS_APID, TEST_EVENT};
use std::sync::mpsc::{self, Sender};
pub struct Service17CustomWrapper {
pub pus17_handler: PusService17TestHandler,
pub fn create_test_service_static(
shared_tm_store: SharedTmPool,
tm_funnel_tx: mpsc::Sender<StoreAddr>,
verif_reporter: VerificationReporterWithSender,
tc_pool: SharedStaticMemoryPool,
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
) -> Service17CustomWrapper<EcssTcInSharedStoreConverter> {
let test_srv_tm_sender = MpscTmInSharedPoolSender::new(
TmSenderId::PusTest as ChannelId,
"PUS_17_TM_SENDER",
shared_tm_store.clone(),
tm_funnel_tx.clone(),
);
let test_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusTest as ChannelId,
"PUS_17_TC_RECV",
pus_test_rx,
);
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
Box::new(test_srv_receiver),
Box::new(test_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
));
Service17CustomWrapper {
pus17_handler,
test_srv_event_sender: event_sender,
}
}
pub fn create_test_service_dynamic(
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
verif_reporter: VerificationReporterWithSender,
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
) -> Service17CustomWrapper<EcssTcInVecConverter> {
let test_srv_tm_sender = MpscTmAsVecSender::new(
TmSenderId::PusTest as ChannelId,
"PUS_17_TM_SENDER",
tm_funnel_tx.clone(),
);
let test_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusTest as ChannelId,
"PUS_17_TC_RECV",
pus_test_rx,
);
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
Box::new(test_srv_receiver),
Box::new(test_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInVecConverter::default(),
));
Service17CustomWrapper {
pus17_handler,
test_srv_event_sender: event_sender,
}
}
pub struct Service17CustomWrapper<TcInMemConverter: EcssTcInMemConverter> {
pub pus17_handler: PusService17TestHandler<TcInMemConverter>,
pub test_srv_event_sender: Sender<(EventU32, Option<Params>)>,
}
impl Service17CustomWrapper {
impl<TcInMemConverter: EcssTcInMemConverter> Service17CustomWrapper<TcInMemConverter> {
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() {
warn!("PUS17 handler failed with error {:?}", res.unwrap_err());
return true;
@ -38,9 +105,13 @@ impl Service17CustomWrapper {
warn!("PUS17: Subservice {subservice} not implemented")
}
PusPacketHandlerResult::CustomSubservice(subservice, token) => {
let psb_mut = self.pus17_handler.psb_mut();
let buf = psb_mut.pus_buf;
let (tc, _) = PusTcReader::new(&buf).unwrap();
let (tc, _) = PusTcReader::new(
self.pus17_handler
.service_helper
.tc_in_mem_converter
.tc_slice_raw(),
)
.unwrap();
let time_stamper = TimeProvider::from_now_with_u16_days().unwrap();
let mut stamp_buf: [u8; 7] = [0; 7];
time_stamper.write_to_bytes(&mut stamp_buf).unwrap();
@ -49,12 +120,17 @@ impl Service17CustomWrapper {
self.test_srv_event_sender
.send((TEST_EVENT.into(), None))
.expect("Sending test event failed");
let start_token = psb_mut
let start_token = self
.pus17_handler
.service_helper
.common
.verification_handler
.get_mut()
.start_success(token, Some(&stamp_buf))
.expect("Error sending start success");
psb_mut
self.pus17_handler
.service_helper
.common
.verification_handler
.get_mut()
.completion_success(start_token, Some(&stamp_buf))
@ -62,7 +138,8 @@ impl Service17CustomWrapper {
} else {
let fail_data = [tc.subservice()];
self.pus17_handler
.psb_mut()
.service_helper
.common
.verification_handler
.get_mut()
.start_failure(

View File

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

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

@ -0,0 +1,113 @@
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::config::PUS_APID;
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<MpscErrorType: 'static> {
server: TcpSpacepacketsServer<
(),
CcsdsError<MpscErrorType>,
SyncTcpTmSource,
CcsdsDistributor<MpscErrorType>,
>,
}
impl<MpscErrorType: 'static + core::fmt::Debug> TcpTask<MpscErrorType> {
pub fn new(
cfg: ServerConfig,
tm_source: SyncTcpTmSource,
tc_receiver: CcsdsDistributor<MpscErrorType>,
) -> 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

@ -0,0 +1,156 @@
use std::{
collections::HashMap,
sync::mpsc::{Receiver, Sender},
};
use log::info;
use satrs_core::{
pool::{PoolProvider, StoreAddr},
seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore},
spacepackets::{
ecss::{tm::PusTmZeroCopyWriter, PusPacket},
time::cds::MIN_CDS_FIELD_LEN,
CcsdsPacket,
},
tmtc::tm_helper::SharedTmPool,
};
use crate::tcp::SyncTcpTmSource;
#[derive(Default)]
pub struct CcsdsSeqCounterMap {
apid_seq_counter_map: HashMap<u16, CcsdsSimpleSeqCountProvider>,
}
impl CcsdsSeqCounterMap {
pub fn get_and_increment(&mut self, apid: u16) -> u16 {
self.apid_seq_counter_map
.entry(apid)
.or_default()
.get_and_increment()
}
}
pub struct TmFunnelCommon {
seq_counter_map: CcsdsSeqCounterMap,
msg_counter_map: HashMap<u8, u16>,
sync_tm_tcp_source: SyncTcpTmSource,
}
impl TmFunnelCommon {
pub fn new(sync_tm_tcp_source: SyncTcpTmSource) -> Self {
Self {
seq_counter_map: Default::default(),
msg_counter_map: Default::default(),
sync_tm_tcp_source,
}
}
// Applies common packet processing operations for PUS TM packets. This includes setting
// a sequence counter
fn apply_packet_processing(&mut self, mut zero_copy_writer: PusTmZeroCopyWriter) {
// zero_copy_writer.set_apid(PUS_APID);
zero_copy_writer.set_seq_count(
self.seq_counter_map
.get_and_increment(zero_copy_writer.apid()),
);
let entry = self
.msg_counter_map
.entry(zero_copy_writer.service())
.or_insert(0);
zero_copy_writer.set_msg_count(*entry);
if *entry == u16::MAX {
*entry = 0;
} else {
*entry += 1;
}
Self::packet_printout(&zero_copy_writer);
// This operation has to come last!
zero_copy_writer.finish();
}
fn packet_printout(tm: &PusTmZeroCopyWriter) {
info!("Sending PUS TM[{},{}]", tm.service(), tm.subservice());
}
}
pub struct TmFunnelStatic {
common: TmFunnelCommon,
shared_tm_store: SharedTmPool,
tm_funnel_rx: Receiver<StoreAddr>,
tm_server_tx: Sender<StoreAddr>,
}
impl TmFunnelStatic {
pub fn new(
shared_tm_store: SharedTmPool,
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: Receiver<StoreAddr>,
tm_server_tx: Sender<StoreAddr>,
) -> Self {
Self {
common: TmFunnelCommon::new(sync_tm_tcp_source),
shared_tm_store,
tm_funnel_rx,
tm_server_tx,
}
}
pub fn operation(&mut self) {
if let Ok(addr) = self.tm_funnel_rx.recv() {
// Read the TM, set sequence counter and message counter, and finally update
// the CRC.
let shared_pool = self.shared_tm_store.clone_backing_pool();
let mut pool_guard = shared_pool.write().expect("Locking TM pool failed");
let mut tm_copy = Vec::new();
pool_guard
.modify(&addr, |buf| {
let zero_copy_writer = PusTmZeroCopyWriter::new(buf, MIN_CDS_FIELD_LEN)
.expect("Creating TM zero copy writer failed");
self.common.apply_packet_processing(zero_copy_writer);
tm_copy = buf.to_vec()
})
.expect("Reading TM from pool failed");
self.tm_server_tx
.send(addr)
.expect("Sending TM to server failed");
// We could also do this step in the update closure, but I'd rather avoid this, could
// lead to nested locking.
self.common.sync_tm_tcp_source.add_tm(&tm_copy);
}
}
}
pub struct TmFunnelDynamic {
common: TmFunnelCommon,
tm_funnel_rx: Receiver<Vec<u8>>,
tm_server_tx: Sender<Vec<u8>>,
}
impl TmFunnelDynamic {
pub fn new(
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: Receiver<Vec<u8>>,
tm_server_tx: Sender<Vec<u8>>,
) -> Self {
Self {
common: TmFunnelCommon::new(sync_tm_tcp_source),
tm_funnel_rx,
tm_server_tx,
}
}
pub fn operation(&mut self) {
if let Ok(mut tm) = self.tm_funnel_rx.recv() {
// Read the TM, set sequence counter and message counter, and finally update
// the CRC.
let zero_copy_writer = PusTmZeroCopyWriter::new(&mut tm, MIN_CDS_FIELD_LEN)
.expect("Creating TM zero copy writer failed");
self.common.apply_packet_processing(zero_copy_writer);
self.tm_server_tx
.send(tm.clone())
.expect("Sending TM to server failed");
self.common.sync_tm_tcp_source.add_tm(&tm);
}
}
}

View File

@ -1,96 +1,68 @@
use log::{info, warn};
use satrs_core::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use std::net::SocketAddr;
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
use std::thread;
use std::time::Duration;
use log::warn;
use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc};
use satrs_core::spacepackets::SpHeader;
use std::sync::mpsc::{self, Receiver, SendError, Sender, TryRecvError};
use thiserror::Error;
use crate::ccsds::CcsdsReceiver;
use crate::pus::{PusReceiver, PusTcMpscRouter};
use satrs_core::pool::{SharedPool, StoreAddr, StoreError};
use satrs_core::pus::verification::StdVerifReporterWithSender;
use satrs_core::pus::{ReceivesEcssPusTc, TcAddrWithToken};
use crate::pus::PusReceiver;
use satrs_core::pool::{PoolProvider, SharedStaticMemoryPool, StoreAddr, StoreError};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::SpHeader;
use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::{CcsdsDistributor, CcsdsError, ReceivesCcsdsTc};
pub struct TmArgs {
pub tm_store: SharedTmStore,
pub tm_sink_sender: Sender<StoreAddr>,
pub tm_server_rx: Receiver<StoreAddr>,
}
pub struct TcArgs {
pub tc_source: PusTcSource,
pub tc_receiver: Receiver<StoreAddr>,
}
impl TcArgs {
#[allow(dead_code)]
fn split(self) -> (PusTcSource, Receiver<StoreAddr>) {
(self.tc_source, self.tc_receiver)
}
}
use satrs_core::tmtc::ReceivesCcsdsTc;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MpscStoreAndSendError {
#[error("Store error: {0}")]
Store(#[from] StoreError),
#[error("TC send error: {0}")]
TcSend(#[from] SendError<TcAddrWithToken>),
TcSend(#[from] SendError<EcssTcAndToken>),
#[error("TMTC send error: {0}")]
TmTcSend(#[from] SendError<StoreAddr>),
}
#[derive(Clone)]
pub struct TcStore {
pub pool: SharedPool,
pub struct SharedTcPool {
pub pool: SharedStaticMemoryPool,
}
impl TcStore {
impl SharedTcPool {
pub fn add_pus_tc(&mut self, pus_tc: &PusTcReader) -> Result<StoreAddr, StoreError> {
let mut pg = self.pool.write().expect("error locking TC store");
let (addr, buf) = pg.free_element(pus_tc.len_packed())?;
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
let addr = pg.free_element(pus_tc.len_packed(), |buf| {
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
})?;
Ok(addr)
}
}
pub struct TmFunnel {
pub tm_funnel_rx: Receiver<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)]
pub struct PusTcSource {
pub struct PusTcSourceProviderSharedPool {
pub tc_source: Sender<StoreAddr>,
pub tc_store: TcStore,
pub shared_pool: SharedTcPool,
}
impl ReceivesEcssPusTc for PusTcSource {
impl PusTcSourceProviderSharedPool {
#[allow(dead_code)]
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.shared_pool.pool.clone()
}
}
impl ReceivesEcssPusTc for PusTcSourceProviderSharedPool {
type Error = MpscStoreAndSendError;
fn pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
let addr = self.tc_store.add_pus_tc(pus_tc)?;
let addr = self.shared_pool.add_pus_tc(pus_tc)?;
self.tc_source.send(addr)?;
Ok(())
}
}
impl ReceivesCcsdsTc for PusTcSource {
impl ReceivesCcsdsTc for PusTcSourceProviderSharedPool {
type Error = MpscStoreAndSendError;
fn pass_ccsds(&mut self, _: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut pool = self.tc_store.pool.write().expect("locking pool failed");
let mut pool = self.shared_pool.pool.write().expect("locking pool failed");
let addr = pool.add(tc_raw)?;
drop(pool);
self.tc_source.send(addr)?;
@ -98,131 +70,138 @@ impl ReceivesCcsdsTc for PusTcSource {
}
}
pub fn core_tmtc_task(
socket_addr: SocketAddr,
mut tc_args: TcArgs,
tm_args: TmArgs,
verif_reporter: StdVerifReporterWithSender,
pus_router: PusTcMpscRouter,
) {
let mut pus_receiver = PusReceiver::new(verif_reporter, pus_router);
// Newtype, can not implement necessary traits on MPSC sender directly because of orphan rules.
#[derive(Clone)]
pub struct PusTcSourceProviderDynamic(pub Sender<Vec<u8>>);
let ccsds_receiver = CcsdsReceiver {
tc_source: tc_args.tc_source.clone(),
};
impl ReceivesEcssPusTc for PusTcSourceProviderDynamic {
type Error = SendError<Vec<u8>>;
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 pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
self.0.send(pus_tc.raw_data().to_vec())?;
Ok(())
}
}
fn core_tmtc_loop(
udp_tmtc_server: &mut UdpTmtcServer,
tc_args: &mut TcArgs,
tc_buf: &mut [u8],
pus_receiver: &mut PusReceiver,
) {
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) {
impl ReceivesCcsdsTc for PusTcSourceProviderDynamic {
type Error = mpsc::SendError<Vec<u8>>;
fn pass_ccsds(&mut self, _: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error> {
self.0.send(tc_raw.to_vec())?;
Ok(())
}
}
// TC source components where static pools are the backing memory of the received telecommands.
pub struct TcSourceTaskStatic {
shared_tc_pool: SharedTcPool,
tc_receiver: Receiver<StoreAddr>,
tc_buf: [u8; 4096],
pus_receiver: PusReceiver,
}
impl TcSourceTaskStatic {
pub fn new(
shared_tc_pool: SharedTcPool,
tc_receiver: Receiver<StoreAddr>,
pus_receiver: PusReceiver,
) -> Self {
Self {
shared_tc_pool,
tc_receiver,
tc_buf: [0; 4096],
pus_receiver,
}
}
pub fn periodic_operation(&mut self) {
self.poll_tc();
}
pub fn poll_tc(&mut self) -> bool {
match self.tc_receiver.try_recv() {
Ok(addr) => {
let pool = self
.shared_tc_pool
.pool
.read()
.expect("locking tc pool failed");
pool.read(&addr, &mut self.tc_buf)
.expect("reading pool failed");
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
}
}
}
Err(e) => match e {
TryRecvError::Empty => false,
TryRecvError::Disconnected => {
warn!("tmtc thread: sender disconnected");
false
}
},
}
}
}
// TC source components where the heap is the backing memory of the received telecommands.
pub struct TcSourceTaskDynamic {
pub tc_receiver: Receiver<Vec<u8>>,
pus_receiver: PusReceiver,
}
impl TcSourceTaskDynamic {
pub fn new(tc_receiver: Receiver<Vec<u8>>, pus_receiver: PusReceiver) -> Self {
Self {
tc_receiver,
pus_receiver,
}
}
pub fn periodic_operation(&mut self) {
self.poll_tc();
}
pub fn poll_tc(&mut self) -> bool {
match self.tc_receiver.try_recv() {
Ok(tc) => match PusTcReader::new(&tc) {
Ok((pus_tc, _)) => {
pus_receiver
.handle_tc_packet(addr, pus_tc.service(), &pus_tc)
self.pus_receiver
.handle_tc_packet(
satrs_core::pus::TcInMemory::Vec(tc.clone()),
pus_tc.service(),
&pus_tc,
)
.ok();
true
}
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 {
match udp_tmtc_server.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:?}");
warn!("raw data: {:x?}", tc);
true
}
},
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}")
Err(e) => match e {
TryRecvError::Empty => false,
TryRecvError::Disconnected => {
warn!("tmtc thread: sender disconnected");
false
}
},
}
}
}

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

@ -0,0 +1,215 @@
use std::{
net::{SocketAddr, UdpSocket},
sync::mpsc::Receiver,
};
use log::{info, warn};
use satrs_core::{
hal::std::udp_server::{ReceiveResult, UdpTcServer},
pool::{PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr},
tmtc::CcsdsError,
};
pub trait UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
pub struct StaticUdpTmHandler {
pub tm_rx: Receiver<StoreAddr>,
pub tm_store: SharedStaticMemoryPool,
}
impl UdpTmHandler for StaticUdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, &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_as_vec();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
let result = socket.send_to(&buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct DynamicUdpTmHandler {
pub tm_rx: Receiver<Vec<u8>>,
}
impl UdpTmHandler for DynamicUdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
while let Ok(tm) = self.tm_rx.try_recv() {
if tm.len() > 9 {
let service = tm[7];
let subservice = tm[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = socket.send_to(&tm, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct UdpTmtcServer<TmHandler: UdpTmHandler, SendError> {
pub udp_tc_server: UdpTcServer<CcsdsError<SendError>>,
pub tm_handler: TmHandler,
}
impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
UdpTmtcServer<TmHandler, SendError>
{
pub fn periodic_operation(&mut self) {
while self.poll_tc_server() {}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
}
}
fn poll_tc_server(&mut self) -> 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 custom error {e:?}");
true
}
},
ReceiveResult::IoError(e) => {
warn!("IO error {e}");
false
}
ReceiveResult::NothingReceived => false,
},
}
}
}
#[cfg(test)]
mod tests {
use std::{
collections::VecDeque,
net::IpAddr,
sync::{Arc, Mutex},
};
use satrs_core::{
spacepackets::{
ecss::{tc::PusTcCreator, WritablePusPacket},
SpHeader,
},
tmtc::ReceivesTcCore,
};
use satrs_example::config::{OBSW_SERVER_ADDR, PUS_APID};
use super::*;
#[derive(Default, Debug, Clone)]
pub struct TestReceiver {
tc_vec: Arc<Mutex<VecDeque<Vec<u8>>>>,
}
impl ReceivesTcCore for TestReceiver {
type Error = CcsdsError<()>;
fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
self.tc_vec.lock().unwrap().push_back(tc_raw.to_vec());
Ok(())
}
}
#[derive(Default, Debug, Clone)]
pub struct TestTmHandler {
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
}
impl UdpTmHandler for TestTmHandler {
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
}
}
#[test]
fn test_basic() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TestReceiver::default();
let tc_queue = test_receiver.tc_vec.clone();
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler,
};
udp_dyn_server.periodic_operation();
assert!(tc_queue.lock().unwrap().is_empty());
assert!(tm_handler_calls.lock().unwrap().is_empty());
}
#[test]
fn test_transactions() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TestReceiver::default();
let tc_queue = test_receiver.tc_vec.clone();
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
let server_addr = udp_tc_server.socket.local_addr().unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler,
};
let mut sph = SpHeader::tc_unseg(PUS_APID, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true)
.to_vec()
.unwrap();
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
let client_addr = client.local_addr().unwrap();
client.connect(server_addr).unwrap();
client.send(&ping_tc).unwrap();
udp_dyn_server.periodic_operation();
{
let mut tc_queue = tc_queue.lock().unwrap();
assert!(!tc_queue.is_empty());
let received_tc = tc_queue.pop_front().unwrap();
assert_eq!(received_tc, ping_tc);
}
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
udp_dyn_server.periodic_operation();
assert!(tc_queue.lock().unwrap().is_empty());
// Still tries to send to the same client.
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "satrs-mib"
version = "0.1.0-alpha.0"
version = "0.1.0-alpha.1"
edition = "2021"
rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
@ -23,14 +23,15 @@ version = "1"
optional = true
[dependencies.satrs-core]
path = "../satrs-core"
# version = "0.1.0-alpha.1"
git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
branch = "main"
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
# branch = "main"
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89"
[dependencies.satrs-mib-codegen]
path = "codegen"
version = "0.1.0-alpha.0"
version = "0.1.0-alpha.1"
[dependencies.serde]
version = "1"

View File

@ -1,6 +1,6 @@
[package]
name = "satrs-mib-codegen"
version = "0.1.0-alpha.0"
version = "0.1.0-alpha.1"
edition = "2021"
description = "satrs-mib proc macro implementation"
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
@ -20,9 +20,10 @@ quote = "1"
proc-macro2 = "1"
[dependencies.satrs-core]
path = "../../satrs-core"
# version = "0.1.0-alpha.1"
git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
branch = "main"
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
# branch = "main"
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89"
[dev-dependencies]