41 Commits

Author SHA1 Message Date
20d5212710 Merge pull request 'satrs-core v0.1.0-alpha.3' (#108) from core-v0.1.0-alpha.3 into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #108
2024-02-12 13:13:12 +01:00
2dd38c163f better name for new trait
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-12 12:54:25 +01:00
79d095b1f7 some doc improvements
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-12 12:48:28 +01:00
377ffc052c bump release
Some checks are pending
Rust/sat-rs/pipeline/head Build started...
2024-02-12 12:43:17 +01:00
c39ce99e2c Merge pull request 'Add Static Pool Spillover feature' (#107) from add-pool-spillover into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #107
2024-02-12 12:21:00 +01:00
c0692a3523 Added static pool spillover feature
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Allows to utilize the full pool even if some subpools are full.
2024-02-12 11:35:10 +01:00
712dc718f9 Merge pull request 'Pool docs improvements' (#105) from pool-docs-improvements into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #105
2024-02-12 11:30:48 +01:00
a69347af7b Merge remote-tracking branch 'origin/main' into pool-docs-improvements
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-12 10:56:13 +01:00
3c7113c231 Merge pull request 'Refactor and improve pool abstraction' (#104) from refactor-pool-impl into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #104
2024-02-10 15:39:38 +01:00
9c310e7a36 Merge branch 'main' into refactor-pool-impl
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-10 12:54:36 +01:00
0bab5799e5 Merge pull request 'Graph for static pools' (#103) from graph-for-static-pools into main
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Reviewed-on: #103
2024-02-10 12:54:29 +01:00
b56bbc8c41 update pool docs
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good
Rust/sat-rs/pipeline/pr-main This commit looks good
2024-02-10 12:54:13 +01:00
12ac5913aa Merge branch 'graph-for-static-pools' into refactor-pool-impl 2024-02-10 12:30:35 +01:00
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
f3ca570e53 Update pool docs in sat-rs book
All checks were successful
Rust/sat-rs/pipeline/pr-main This commit looks good
- Add simple graph to show how it works
2024-02-10 11:57:19 +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
7cbe4f1170 some progress 2024-01-31 01:32:03 +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
64 changed files with 8883 additions and 2060 deletions

View File

@ -6,8 +6,8 @@ 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. You can find an overview of the project and the
link to the [more high-level sat-rs book](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS documentation website](https://documentation.irs.uni-stuttgart.de/sat-rs.html).
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

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/'
}
}
}

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:20240209121455+01'00')
/ModDate (D:20240209121455+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#9#>pfj+Mq>To#iqpb<5Q:*ns8CKnJ,8':s.;OT9E+^)s8Q;e
r9++WDnl>Ls7O/,aM7WLDu]Utnc/F"n6>@gGouoFD;Jt3gAG(frg14qpF?3t)"tpuCMf1E/rJd]h<YjS
mGD7bARfQBp#S*OVrM^S]f.Y2^LOJIa,g8s^MJN%[*$]\r9NuN+9200PLa+)s7ij6s.&\UXn'$/;7*dX
+oEqIa/dm=(^TflZ.K$9a1qX&k6':pf*@mSC]/>HO']tADMLCP](!2h-Hu>T=S;^nf6/g>IE'n4kSO5A
XO^DO*WOBfrQ70n4lnhUAY&#A?`_`G?iTXcT<W6\<BY6QH;N:IIs27!`gZi^+8k-f6iT#N'49`8.PLb,
%!'##j^5"I9W$Fp/Ucb%D5^^spiE7n5CT[Me'(m#dlQm1`j3eN8Aa88pLgT5YP$SgY;j:Q[LA5Coc"nD
q7V!b1p47@B7BJdI("=pBYUG0onECUNA@J\[Mn6h[ElC5`Tkj"WSc-R`NL>jC,ho7!j@U:%Li><I:43P
qu;[dKKG1>RUUq3O3bJA*Xe0$4.":CI`4L&QS>f(@'?d&^#S<R)5<g=+Q6BhrFhSs@`t;NVtG%+Xk)@Q
0D(tlctf)aIN4l/96lfRcaF;Ef-9\sj-><nh3Zcr-quV+0@X@r^4nGUIC#5<pP!J6q)#kX3eQfQ;Uc\b
I]dR2M-h:Z+9'H'7cWm]cap1gq!./m07d&_:'8mNTMlZ%;(5!26>\_'?Do]]$S&j(ZhK5RE.3g7(".;t
#/5]GlR6f#HS(`7Qf:!'J:h3oc?I:HCff%HJc?MMSIFn%rLZLaA)5s6?iP5hC7Ri3hU):>1n:0YI4VS,
^A^^S./I]*OjfdZJ+!-k^O1#snoc00R)egK(U)B_TcU'+\WZ\'M"Jtig.VaccitDQRO*8m__2I6'SI7c
mGl=G?`#tlqu9Hn+Ke36_34FdlfI<`\J!K-I"q3Tb$?[umC3J/r]=0Z^V<\0r)0=Dg)m1U.oOZr\T[o<
\2d%WGHCXjDl%nSO3t;HOla'%s-kN]:-5($<mi@nA9Ll4GlQUs'K+I"Ir1H.c1f(.WJOu\]Y<IR=)(Vn
g3AT*8"J75?_VtXJT?2!cW[;T6eSBnA?+V\j(/jFiU=Q8H,\ENqOG=[=7=.*rV@J'O1r3j^Z4ieWp9To
cQh<(hVWOYrr-ndYs,EJY)Et[cRF6jnZi4i[pN=cE<".Co'obq>(-G19+i<@rZ6C4L\7nlh>&krCW[#g
fS?=7Ho,Fs/!Q`E3jHmFlH-ckO1Fe'g3V#7p&;AO)?R<QI<hM!aQh@*1G>j]I4[?1e6#a5lF+U:L[O8g
G+5mZ\BPT9^"H3uGb4U$qhNdpB2/&bk2SN:CRt3/Sn6.XAohk^IgC%qjXO!+qc[Ef8k#fLWVbBQO\Ykm
:bmog(8r8pcFV,BE7GL!jbjY5\,0?1(?puG@HE"P7fB+^g#"9tZ1Y0)ST`?E0L8(Y.'qbe9V`:\,Ti%k
o$8+3WYs@0=b_D1cCZ?A9<`.(!eOG&Z[+K:AMm:(j&nB3@#,Nc=(sEb9P55d::)'lrGXqpa's35If4^!
ZMo<docGqE0`H+&r+BE(WmSmd:L"Bj>r[Ic"'h-Uf:hkQB2%#g^.0JtMupmG@*#\UOO_UJj!r<LA[8Kb
;NgW7&n`d6h*I)1%O<OLpV8ta^7JL234'1\^Z2X-O<07+3i_r@=NBbc1tl:1R+=!ECrA?DF;/IONN!+?
\q6^OOc@s4JTAGsDC8_2s6poMa..J$06h^[[E^Z2>*7H-3%JS+M)YAJWrIg7N/np%k_EmEH(ic>W6`6k
0AN8g=Lt=:?RW7>RM#JuNdeskZ$tUF1]*oaVS9CFLU>We()Vs5f/3L8%Ht#.aMu93iu\5f^Vrr4epk_-
&p2G)A9&tgs,0K6*?R;9>ltG3ZhD._^U`p5%Kq79Umd9:EqD]RgE!Gco5b:Bh'q/nV*j985d2UkG]%]I
6+(l_=<EPYp44#tX:,#tC_sCh=a/N"'hJ0/6$g^>J#U\m`WlCs)^pPG/XX!R8Ze),"jX/]2tmIo:@)u%
1io4&,uDY]mD?K1V#aoU5iBM4p$_QRlH9Z`pK0F_rNGt,b0`q]2u%NY8gkWa1q^>DqV%Nl\<eFGJB^ja
ro(9*EElX><L03%`]1TKMuMc'm_"=`\m,2_VbR#cX)sIVD#eF$jeBpX5#Oc;nr.jhdT!^1Cb3kAKiLSF
D,YBPcF,A\<,>TP/L8$1m%r<Ii("WgC$2-PdX'9:*mJRZ"6[oXRWbcZ.k$u<<C1udoTsANb0eH.$KRC/
&EjYli%dg)s6BVo9pi5*;7]kE.hC`jSZe-0.%TZk)FNi2X^D)$Ied@,a8NFCj]pA]lsS#nNu]dVmoW`m
*WOA)m.5#[m&[@n[?;S<,PK\kruM+qqdn4CAWf3uo%UqZmJV(]2SQ14f:2osI[PVA)V]@ARIEh`YjoAT
U%6f!bP/TBo(A8sf6Lf2EM<o,lPk?-I^pN?hFDqcYh=K9QEt`HIg=9>[q,9A_YaV-c7s\lmoMX0,PCn/
gf*2"(&`EuhOd$X?]H1CH_#<JBAa,$4P/0pI/O7&C+E`a`#:\oLIGH`[WPl&NJ!#FQVI$;YVbrS]+%IX
X,Ges8t4@b"^_k2H6MmW^@8Bi:ENoTHpbkLaak3bZZ(ldm-!9r7Bm3Epl:PMX;Z;sqK]I560S!p^E/UJ
mB&)PC[9W<#:2`[@JiiUZt`ZVi\(tE%*CGdWGL#CBPYlN5/*`Rr0jIAn"(,(nE>kJ'5`i3QL2D:0,c[R
m,.<3%pLjZg%cUeNJGgeHr)S_d.c+Mrn*,N<"-0%^K.J-7a%g?@_ASd_RdY!P-(:(7U^Eq^O"K%K9GoK
Ar4UAbcI9TF1b*3)db_EhnS2@mp%^ld(uR<CQP33ot&RT3>hZq1IbV+(P]@0Bsk.mY$RZEDSn;(PPd7N
MIq%$B!e$Rr0Uj\7)uLsiS0uU4/0DAgc8#?^Jm:,oL8TIAb5+*`"ADM_e*VU'XW/5l@g^7Kq3_\L$bVA
LlR3T`K&Td$R^tNUqA)PjY6R#B[SLG?L0=o\\T^8P*N9GjB#'%`Wpj$HDdgY6l]FB9&[[m*<k\8g5Z'u
e:-VbXZ.terXS3Lf_r\$%^<BU%WA<dbMU'5BZ$hdjb/)aa#intI86qF3cqMs1V\6`:kqXXUS5&_]mI-Q
)4c&e%Lp7$5Yc6,m#gg2S%5PBD2tk.fN1cK2X#R/I##X%a-1jL$I5Yf[o';C$$r('AC9/GdRA*F2`UeI
1>#3Tr!-YsktuqgbmXChp##;*e`-@Kh(!e<ab$=C^_tcn*=POZ[goO6gJDM8B&-'sh*(UWkpk6pHs$G&
3c+BYfng\J%!>GZ]COSO(^68g^C,%/XP<46p\A%[oOIiDS?un`o79nT8DCt]=iYbj-ALuO[c],_s8HnU
m3>WY,4Kf(_535F]OB>T/^@"=SJEU>1$Z*5an_g[VY!rDm[K(0c)`)[8=;ZBJR#pF(Y0/sdZR'.618c)
VQ;E1VO;.jf_j&g]33r>%;)GSAJ&AMZ2^r48W+YHRBiko#o*@*)GU+u?bT!5e?72mW*QK0VAZL+6,AJn
?T5s[HC=G4';t!JFNa$seRf<F"D/Os_%jn)!hpM4nFjGdBl.V:W&B5_-<9Aa4`VQ9MRs(a&%`>.5lD>P
^hEIr;g`_h]159G?Xf&8Id&)bG&KDel;:A,'BSoWUb=.1L6]A.B_[asod4m`dCPu]4#Ukn6#4pf>`CL(
CN,+HN5q5g$Pb)Gf&/$eXgg.Pg)@^]m2)oM/($C7CX>0Q:+INt#3gp4n]^s]Y]E$1#6+TB]/@)2mA%OI
QEUQpeg`O7VB"1F]&plL?gkCB[D4HFq+J$uQT1Vgi0&4ro#M]F20dG#r%>2OnYULnF'#:G6;H6)^@:Ig
-gX4MJ&eSh`t.PG?i06qh$Y4;-t[>/j&c39DG2WZjF'\Vj1o._5aeqA76UY'KA_/>T'3K_+$s"0hD^Z/
ehrqpFEqB4)5Zr*AYTr'5I6r;3hgZ3f6HSE@_g$Z>_"oL`_Tj#Sfiamg2qC5r5>D0$qA"Q_.>$PhCrDb
"V]5u,-CQ=WlQ0'h2-Q3Vm9?KVebYl%lMqYF+1[GPY\R%-Nb>56tH33>`\h$47-DEAF>#Qj[C%JUYOWJ
,PJKUj&aI#]G0l'Dt=N]`t.eWrRg\?!6<<Wre5^CXR5hMTha)GEP7&r+E.)]&;aZ=r^8Y.H4/;6r.WXM
ih0X):HU>ps8=htguhK0`Y2t*0g,*`(]T(!S];5p1=<OR\OQV#I4b!l:X&\%A3k41;/:`mY8p\Y-iHcf
>CXpn)_>j]f:is@m7E>j=?h@0@SD]fceS$!SCajuoKe`!-M]ML]U:?Nbb&Ps$$!',.a?IR`g89^LQanE
`.&?m57!AggNsPBm0]n9T]r>baU_u:[Nn!ojhlUt..f:%OQolG%k.%Oh(ml`9/?GOLa[5W6(1hPBenrZ
YJS7C?h,1&$]ZFW&Tf:G+'fe;haO-[XJ35f@>NoUHmM6iL_S`JBjlt=r+b<2b*[hVZrmOn+S9?1CQnVl
p""W686;uEX<6T.<6N%UbZq*NZb$!,qlMfrDHI7*DJN:>F=2AB._h1W.rP,ss,`#qX@#WBBWQp?l"7rc
ndI@f%u*GU+YVjrLXtDPhm'@P4`c<Z=3nAdSHSP2*TEHngT8U"=nJ#ERj[16\4ShZ!?-i2YqGj??5Q(J
iP4X"hHBCVq##PUkE"C)ngO$Ohof&dMrQ(!qpqaV](X2dIm05S[YduDS]hcrCHpA,ej"B[I_i2UqqLu"
hq"Y'[OqSfofIHaM.<F'F$8cn,$M]8I2K0d4fDh*6(cAbOfrXP5G_fL6PO_d`0/@#o>);r'Fr5)G4r_+
:jQTeFXV0&qk-P3iFF7^42PeiTd:59A(tkiQQ+YE=[?83K42C'bhXrDMia-qrje%24`nF9'\P7c%<9X*
]9B:om0C\mH8'*,!T3j0l]f..#QDVbN7e^(r,`o@JJR*^[c7h8bhZU^f<]p$+3.O9W^_hCo5>+LmW&aV
f6U$R>q=tadQms@2A5s)6^GZds(d9n8$q@S,bC=TF:e3MZX&qO6[pZM\$NFRiKsKTlPEhhmcKo6+6$le
HHdVmqhIpKRqT'c6+Om>]M%]0TR<qYUM3^]_<G2<5XVornSj7[0j7+0UnVPQk'.5U%:sh+Ft<eK;s2Jg
W%9H6\>C>2PM6_=p!D].88fm&"WGDYemi^%OHaFF"r_C@]!O^C;Jgk)C!2i*rX<%mCtPn<P-3'B(fTa5
#,5\fINVPAl>"Y3kfI$&N6HIkf@5aY6^j..j\:AE@?Ne^o4$Z7.a2Rg&#LaVAq'M.(\XD9G;8tc?el]+
PC1U<n%S^#jXM:VX@poK9Cn<=)a$IpDXc-)/h"%:>&@HVk^Bg(EI(9PnXYi1--KU7>C)r<3HP:LBqZu;
V;e>:?\PDX:g6ZgFAC3)qN.+`Zr3Qi\#_j$]D9WB:M5flfeeaIUa*0[')ML0V+GTj%Tt8EkaeR9VrT#/
TQbtCG4RZkDN!1ugmpIfDJ\FtDJeJcGP_)pYdV@i,g)7lX-+-4GIHe];6+F(U/+BCCQplpR>8r:[OFES
8kk20p^<[sWLXQ3%le`F90C_gUKXoa>?ZB`4'Pm8Cj3</CX6`kN`u*Q>.CcBh:]rUNed(!]<e?(EmoD(
^,oQA-oH?[a]^L4E62iimam*Z/+dB7i0e^j^)Ym=-SOl-Cj3<7\V(qkRW<UA%[!+HiZIpU66jnK-0FWS
?fLKiku7_.B1ormgRYH^h=;/fc<U$K`Kk[*[Q@]V2=Aa\)=Gek_q^dpq<mV;s7bEp&GoNbQi>tQZ!;,$
M_e,\F2f(Fi&K2=qX=3ObRc;['mMprbg1Yni5e,MrHJ1cI;NejIlhqu1JX&GIgb^FJQ`!nHi8HRV"O>H
q_+.ps%ts`C!UV%N#h8MO%'J$NFJ;%OcNQ$`UI+fQ9Xrf32bA0b[0ph[+m8$k&%@#:X;PS>7o(s<`;0o
E8_U"4q45^#Fu\ti@[3a=r'sp=WRfZcN"OqN8Y'fB3;pU4c["`WSd2FMgg]tp?9H-P5S:@1jDI[*mD)Y
l!S$A^RQ)"LisRjSab0@V<ZH;hOOGS(`un^>Ha0`h4VY.%7);PH,;"TdFWKArfVMHjj`b9f(Lk@r#4b1
UjpCN2DZ8s8l_&;9rpSuJabG.WF'+YMKUT'KHlkCl`^#u*[q.>MCUl9k+c1XMB:fEb"PXDb&OYhY0C+Y
*YCEOM(oqI&gKiW2]XYf#V$uF8eer%CD8G$0ujop*5[TRPr'TQ3alSLW;\kZ-:d!2o?`m)G:]8:g6KcW
Otm[]?^9+c=2WCp1-*^BaQ*n5S5Jq][tm\kOQ:"4TR(0iPZ4eZNe<(!EWANTLN&2:rtYk<b&=O?3?Id#
?AW^nmB6dNn27MQN2@Oa"?Gu)Y8oG^Jp^D.,01!:O!'d:AIuT&Ps8r)$YLNWb1[dB-s[GGE__aA"Sn_U
&S*%ROFckg>E9%ME9*SZp[?`Za9PM%+J56:O!#OoY6a\:;t:4b6(Uk`eF6]nd]-QKShID\lD+VHRg3km
+DXFY6%7$2A<M5\FqWkKi,r3b,:\fTGJ(bq,q4Ih!3Id+[hRrmSc%&:4H'"(G,^%omZkQ]6B1e9`^cj[
,EMCt4L<Z9TR=F]BX"Qre/`?3h?VRh%H]5WrslAa_&]@$#Hsf@ToBTRS7%A">c!;1`MSL_1L$[97I:MT
oR*bK:%$)VLRftW#WWc6UKqJ;O=nZk3BJQahg)(]Z:1kpq^PU#?6>gIA<ABE&J'Fbb&#E"j2jOHW(r6Q
\QaVX_O$?mFJ>oC+i>]i`MjNN.'<uR1P593\M9r9)-dEsl^*HMJor8$mH<a@6g.C=+3r8dhT&.dTbXVS
2.:-$][jf>GdUSpr&HK9WqtMeJ$mEWfI3R;4*7Vh,4J$$<\6q?jc5\c+Qa!8DBe2>[4<i\H_BVOGgg]p
:!4Eged;?T*P7W<M0rC-Sl*\]2_A)Q>-;U/b,!RY-Za6RPYr`hEqgr.WhOesc];\66ADL\;+7O$rabc@
F6@g))/$KBD^P)RffXWPK3/'-GIci-r.<a3T.M;O(OXLJ(UUsA&JJLC.ggKld<skcDa(L,*om'^ICh,8
<8bk14hXf"=8,<-1u'U(Cr1RFV%0*bPr_rSF2-2/#p$R9(o>A5Ql-ZL]"CoGGP=_#*[g\S*RlaG'X<"]
=IWVG[R@=Kq<G],;X=S#;Q#;nrD!UJb:@_T\ta@!GuGAQ]"Cpd:!Vbg9')m20@gCSYEU`R;Yh]P;VA`P
n4K8?9_V/9qX?S;rhdjhFiIIbd`Te,]ReYqPnHeAWRU-CRdK.W5MRtnhTZ&(PVBaJglKf4Fa>bqYLLD=
]i(cSSgE;>OB:`:;u$17BSUr2`/4%WM,,)b15e<$]d,UA?E$HpU"Nk&qs=R(3oP?<r+3>5-:R[TroR>;
G7MI*oX#(o!pNkJ%^?:Zs4G!VliJorR<qZ8>FTfOphf1(fJ<pU[;37qq_D9?H#IL8UX*6_r)XR*V2Vr:
+QS>m\)b#b%.,m%Z-t2P,\E<L&*'qAZfC!NkO&/='/tp,:tD$fiGGoH>(Ym=g`&Ps,'&\!Nj?2j%rW?B
&'m"$kG=*JH&iK`[j9VE[Kh$$D($p#C--H`ZZUO5L31l<&(St0jkub=bgpE:o/&'^)iFEHrgR4*l.<J`
LS3(J(uZe`(4@LmOuHF]PSqFu)r+;3Scq$q/h'7OM0NKWhY]N$/,:.SDg=2okL.VE*r+8OQc0e;pk(+9
^4="EgHA.#*@Ic[d?5_NF!$4tK718]l"IPXG2\L[^>1j;T5)Fe[,`+LX[=s,Q+D_;+nqCK5=d:=4..^\
J\93^IXuX31ODEIPGL.8EO%>Y@INO5!GEJ/80EoP>%F9_>oH4:";5[$(@*GMXr)Ja3&pY+C$8luNmd&.
8'(E/NX+lnf/O#[9B4B;6B-%_GE$\?>9=brl)l4FS5"q_Q/DJDJbH3cZ8gGm:)6pKc_.oRC7Hhh0o_A3
AMT7/Prh^"P>gOU=H=XeHMVu$)KKq+W]9;c:*nLO#N+olAL1CPXnJT2LgB&2g<DJb>GB-bCe3TBrm3X!
^N@WIOu[bnV:&+k,Bpgj52;iOE5S)#N[&9Ya,0DT/rcVf<hs`"Es[gNM`d'aY;D8/,E6ca5$[-(0$lTM
f=Am]\@BPP09@)B8/b!c46=s0Ig3Z7Y4oM=!uaZb-_7Oe<rM-O-SO;52"hICIFgI.$Ih&b(t:OSI\FcL
EC0tZH;g3f,3<LSQP;G`#BT:8+7fCc7nZB5>(Ii0b-X4\m:cfG-)p4kTKsZ"%UR>M#skOhC`9.aMIq-,
,"J]l1]qg+%\g(&",3$P7ZQ&P:9Y4$UAdK>(:!>=fYT`D*%h#4R%qX71.UZ'FQI2,Y7Jt,3[f^UaQ'NM
i@K7gP9!%ECVFhrN[Kt=[!@a]Q"D0#BF.I\U@*Bji4Zg7-;-rLk6qX2S?.Vg/^[!I>(Nr&(.O>86/2do
+3#%EarcA'X5jdgNB;jCM`W/>FRrDV(YZ]fmS=t?,"LrAXEfnb7[]l#4P,^jFYL.9q&S/kPV62J6[b[\
6%A64BB8SBnY*4f<4<NPE\hg^F:T13.fI7F(HGY05,OBY;pGc!FIVuOZ!VG;jRDJ(\oG,-93H;teU?'b
AVLX,hH)Atg<&^mq-L7J[JRmDS+?E%TV7X<G[e+=CSDU3l6[HuFkc-sok;hSW"kHM<KpX?d\X?o"Wa>n
c0sSkRl1LtFO:m=YAU["p^e+P$Li2+4];\:"-5fG&\FA:O+-W/J&6Gr&1Lqh;Ub'D)V7flG/D^rJ;_md
<,`e(ho@8)044M=+_6tJMe0lDX*`3,nK"/i.Q@NQr3sRVTG2_1Z(ie--l/?Lnkr[<:V3,-hY+[$\n$^'
PNMN%BEkoc%"rUBBdrUdR9hH8lJr^f:$u`h%(Zq?ENS^.:KfKrIE%)*$gIR5Ioc<2#OYQ_oQF_o"WCG0
TZ$BC=PPI(:#;XNA]=2?dRWEE;IY,Ca#qY;M>nHo"<DO$6^I'X;OFpt?6!=[_@nFi?2X__Gq8]QOJY.U
jOkMXYJG4d.=^,-X:b6J]Bm`N`*?i!Mu=W`H&K`)OXsKfcP`gIHB`.RTB<m((K"PH,^]F8X!O<J4$DS[
(&9A:h-\:XoN.@DV`j$$m]U.S`7oE?R?siOo<o.oGa*At2pA_L.CZO`WopS>=I#"eEa!VOCif&-XQ7<u
LM,+_]CK$<2@:Z3GNS#OYHN3kgUp/W>iQ#WBj^UmL@eO_5;-Pnn`CqY>k_\BkhOE]9o)d4[bemi1/#0X
=$/W&5k4>7C'8Eee8]ai`G=sEMA,*T'P64+'G!,lEEZntID,<G19r3gYXjW,HdLhF2"*a%Q$NL_G4Zh4
\"-8iRf(LO29k`qd;,<Y;lQBb.6p#)"7=QZd`T3-IQG^>=sE\^3$,/Z)6rjjBA_Kkcua0W]E_/X3:e2u
+jVe:AFR)#*jeLTQ?^kQK=9Jlq5UqXB\ISUQHmk"9i2jE\L'P3c"</_0^h<VRR.J?<^^nkh'ORf=/Ijq
3j)@FiAd?"iB:lAdB;Iq[c_@i3eofi<^S6_s%mQX&,`eI]4Sso#GaSt#Hc$!:&PU9dZe$rd`Ge8i5hh1
&p091TIj>IjumZOmt'N;H/u*Vlbho<]a*V:H4S`j:)5X^o/P1CSK'ra6.9!n7aQkG".jK(SPZBX>U>Ui
f6@F=mm5DoONpb*OlCTQ.[K4^kK[ka*49;r:EA/R^Q)4DYW_((]a*$ooH$h=d!K`tQ[ecC_EXUMq,cJ?
BP6;E\_euS2fnIPR_gV0r.[umJ_8=-AGHWLP9+@2o(`/?>e0-j\e>_ELpf845Xkg)fmP`[L)O-"I'pje
B9*s4di@EDH5dqh'5((\=bAG<rke/KZsJk._g]R.h6"*Q.oNp(d]K>U>sf;N"C>=Jn]Qn4.GH`Y&C`Zi
G]kPqfcepRTGSjAG@8U8q=a?m@u]*kqIs:IMB&aI;oEW2qs)%dj#h]R)L!TQIXAHEHa&MF;@K&fAN%Uh
pYVCWPX)B[UXXP(J(a1Xd;H9fnuJnG=0l]<V>ddg]#S;2hCnY(J#=5%=os=`[XG:6o6:&0<Qo'XFj-Up
V1\V%^92iPZWb&llr=!r>rEaB[n`E6IMFZ7*YB]iS9iC$B!c'$%IYE"eib7sDVdV1Gd30bbrW7Ra-Qq^
)<QQgNO8<%#MHi=hC]qV\roLH8eu9(lm2at("Q*DEQ]lt7[o8sM?Aj>\fJ)p\r\J<UYU9@&D'^,B5oc]
7S9YdHg_Fg(ND9)SBr3jS!@cuWu\/r9=td-/D#kFaCVeLp.:[V[=G3sVC,t>Nh%mgF,_5aS*;9s$a\5A
^C%\&ce.Op(J!.QG>XgN?Y4r_AGssd<!T2S9\Tt-V6#sKoRZm!ii4OWetu6H9;]N;M%WU2^ht253,;qc
]O[K0GDWeun=nDlc0<Y)s5L0+Y?L(\Sa/30Y;`2"K/c5.G0"KSrR)Xml)N`]kYLf-gKf13'lnXUPdBQ_
*n]+$fNT0tKAR>8CQZ;G]MrG1Aupa+.:27G-Q6/KM*(HHRhE5&rFDk(>E64sZe&"`C)QtG6:#NCD:J0:
FMU"/)j"6s_N(D2W*$`OrIY"-PcRgD&A,rMTG09/KKN2nN,>>4NAEIs2uX_RV+0LM=@TG1P:JC\#O@hu
):Ei\32;\;VJug9Ri9<Z2%D>u%Y%'01S>%Zj4JWbf/R8\So(:@oQY$hNlEr:]g1KoB-u>2-N!M:_PM6a
+`EghR71@d!kH;oVT,n<nQ`BdFO[.,m%uVk3!8J,FUIse)TPU(7k$8429m&g7asD@hV:!6B[fPLEJA"p
)Mcs&%Vo#'%*14^NkL'kBq->pC)VjjUeY@u?/sRQ'2c??IkEnnNC19Rj%PXmBcKXm35j<+i,<<Lb].<d
Z+4nB`j,7l>iU@L6fX9C?/sT9"Kl`0(MOl$U#FEZUKQjPdMs0\a>FG^29q;S]1MsACE,qK2%F2X3b9jW
NSsPRPo=R4j&#/Lj+-"737Jep34F@1`Yl-H;MW70*0E)'r[kQ[*PS<J)fA*4]2NmFm;;1m[.3/X,!V5h
#1lB:Ok>M(j!pcHo]?#ONdc\1cME(sV8tbOFu%gJBE<\9nNKR5:#'W,XqfIa\jUk*M"hm<V,d3,6,,ku
q8>c&n$i+C'tQI!(#Dq@Fon/c:binCcej=cOQH"8L%^aV_A;b[pYCk.^PME+KRQKW_((tS2:O7$`BYN<
Kkr]kr]-US-C`d_]<%1t7,"qPn4/&J/F=SH6j?Lh]Q@l&JNM^0F<]fn.E*,KZ1A*&GfNguWAVb,2Y69N
K3r<6^?"O@eYL*bk7;o\?2GL;n(PUZXBnVoLIPX-?aY#q5pK9VLcihfUb=<oZk@S([Sj2#C2qSEq+WEZ
%V-f9^Dce(*#GH&^Mh?HQZurrLcIS5FU+JA(SKR;%0<p&QOcrq:.-4j]GWgh!4mGiOfas66pn?!nKc#Q
@C3Bf87AtRaQ_&c>Ra`2N*72+PW9Af>b^N!l`j26k,p7,G:c#Ee^%NAX=>5YKA;F584k5L!uap4c$&J5
+%'i4aY0cIQUKV4_MmhI\t2""%g(#%)9K0qKlLJ7429Q08or@,2sh23,J2h([B!tikdCk5c4b92d&OQ8
fU9?:o*>E*,[6P$Vj6bp<nQ:rT'':ejkhaDL)r5-ZcW1JEGYG#mFipfVaG7B*WhV&R6\T`nSG#TH.&iA
DKt3pCXW[-Q71/Y:-\G!qZLc_=IMPmlkZ%@@kCc]p8R1%ZVpo`)K=h6>WjYO\.^a!nK(QS>C.oZ?/V'i
_AR$R;V*de3MZa?&l4]USDu=B0CABrT\V6XjLFDZH")RL4(+3,r(W4/giI&>3fmn(n:i^VQKVbZ8[?AU
/^o+"0f9\lJ+tonP!tD=TVh]j)q;U:&ju#$#Q"1Te\C.brd-RYACK(SnI/r$VF,pLKm0iTo!do!A$>pY
][a@f`f1f#c-^CU`:Pc_eSVtb_V^BS0^HJ`eN]:C%/tb=nq'*>Du\b0N3bE<]^#/GnD[)un;H,47_'\,
-@0B4?cH;aoUf+#-G=T?iDeAQO_%(hm_Oi.ns:B_(#/-Df>(Iu?Y#5Y9149!XL`b+oRJ9(Xn$=;rM&j7
@C9EM'<to(hRd8d:V;#9hB_Cn]h.G>k^d2r^_<b"Ii4L*L:AH!Wsc09p4s:]p300MF+g#dK,@`Yh`fn@
XRR2tNZP_[ZAeWG&`A(HqLOp`@s:15A5sIM0;MenMnQB7YXNm8nU)?f/#M.l87Xg!bdo4ICrq-?mrDb%
NcKX"m^i!2!MR9047;Xk$`Li"I<9enP3?>Qm%nED]//uMmJU]f?$JN<oqPo-b4-4\++$K7U6Kc[0Dg((
CH"l^=#!)-&a*qFotBPW><]M'Y+>-o6(VhVDeU)DLFA;*/RN(@5?rf[2X3/%i9$4_83XL7`'+')3[K\%
LpAes^S"Q<Y@-qg6(r;\PC=KZpuh.i_7?33c)J>fIV2tn*gaHH1e4]+a+iJL2U!7a0K]`9dc*GtR^#6J
bjR5AmFdoL6TmLl=UIT?_98-!B\p@RCqjB7N5GnE+MhlYJis.5mi8a,*&X&P7<&[0/pq)+H=2I\bkQ%'
cr54Ol"9O$*`RI\33Bs`>CpD>IWl4t:8X@m;DL`UrD@>*"%.GFR9jeE^\QVQhO56d&qAU29c)r8c:!Lr
ksdP2/i8+CE:^V7A-a**^9$a$hM)\,:YfW*Xl/[0^;r_a?tPLG'MWLiVp"%cknM-cI-qq0hCD@kP:Mc5
,jhIo"RUA)g#?)ETpBUWWn:mB8_n"3>B\'JN#ZW[gIjKuik4?dC2TsbR)_g%BNVrdr;p2HAWCpY/'RE6
Zh:G1`sK=Whrbmmh4q;c#0csSZW/"dc(;:D"lAn1J7F?&E_V_&p;B*RI\g83js=+$]p5W4X&oB5cUE\$
0%4'/;2l?S3n*+f;TIQ,iiERG3@Oug[5T@Fi]@mMRJIET@=HQBBX5MSG9QfSC6Y0ZKIpnUNq=sO"VPRA
_BiK!^/[[t\*Gp&Kj815E<"YW"/H+WN'nW)G^+,k;G$I(J+-DBnWXd3qMs5I:W[!ldd#ul3/FVhq1W(^
f]PQ(T+J+Pif75]pre\FF11E/BIq9@)DOG<8[?**OuE&=Ba3QZ:WYD,!GO<)%m2$$(Ar4L0Z6bKrg^r'
)cF8<;e<R98O&sXe"qOT#>#`Uc$$tmG3KT0P"q(*?Y]FQ^QN55#aFK>ZG):S.G_;To.R.`2E'lAj_j&'
S&da6NIq%%+8U^1BQ40^@X#Dc,0.u+]*1]139p79eHd.<2!Vhcf^RMehJgt7J)Bd-`RU3oa/eKBh(<H!
ql'Ta>1%+/Gqtl7:.:Z>Z3%T8#`MhGoLiaZDJ:+p1AIk+$[Qp%cFL,6Bn_:*J3P2LAbF[p6qJ.+g9#PP
IHh#B4-_FFT#gdZN#.iVb;7T.m5c9_<DnZ:+pB(\rOOH7\Veq%"5B.t<p9sE24M)QVTOA?IEng4NO-*7
LggLP],,h+$,8U0=PiI77BL<JX"o">F\3AB<G_-.%rEY[s0C:6r+NRlM0saET]SB]\i=5`[gP?%dmqQ(
SIYQIWPfi<"Y]_8igi.M`9N0M?js-6_:B_'e1XU62o_c3<R>'J9X7UmnmWNu3P,_]Bk=aMgQSSPG8&DN
P\MMQ(I.:\\?2[=)`DQ]c>MZ:S'khk!!?e$l@jXK3W0H"<`!T#34k5igPD!<Wh/`4bd:oK</h3,2?&#D
dX6"3:"h1lFGL0])VQcRf'TbIg;c(GTZ/k(S8kZLP-GB^mq(nTOH2%WjTi3"[Tr81KjUh5WE3;$+I.+6
dCAd]S_QTl%HW]O(N.6n.W&qb:uJF'p^6f6:4Q*q]Uu7uQ0Uo8AX4r&*Q2;/$`%B[d$iR?%;!"6k=/0Y
3]"rP6sd1JQF+Ct(Ket+,BFpsJh3QH473J5`@f7,4%oG;rcL`*MgQ,Vd91Lr8T4#:frk%6`X?rbe%+[E
#WIfQFB<0sS"!AtS%IhZBX^O=&V5-p_nuBgb#G(igKi`?g=BSkjEE/h)"]Jom@QPcjFKcP_#*QG3QeD0
ZR't)`D1s<=,,pjFgQ-Hcu,h6P.)rXGKXW5\@Om]a2MD^"FhNh>/B*cF49>%SElpD_3?2?(&Ms]*1]lo
DXe?R$Egf0)&1APXp@aJMujg(":[<c'hD=T[qlJcnN`N&C-d=3C>o)n;XKTj^ZD&2E)S7q]]h\@1%<KO
X^@`bMC>=qre(jmY'G()oY:Y+<ja4?8a?W=R((THZWXXOZWD*<%NFYdjON<p?]'#L/D]_73A9OFRo;u1
RUi6hr3_*e,iJ!Dj)Y"grPNF2S>f[AYc%7:q2Nl3Ga6cGb%rMk=\<eXJSjHo/=3H'93f1j3jXpRCf'Fp
QA$e@Oo9%`6/e0N`_1!3>'l+_[*flhY_-TTG/K658eDRKN#Xri09qboCO=np4KB%Xc+$=?Gk)A*8m7f>
d5\Sd.Z7>`am[)iVfc)KD:h5p?Y^EIi+lB0*aGPq(T6>aL`TofRBU/kk%\Ao04aIr2;5,2=I%(f*jB/1
/i3!@i2(WnmBsmUZDlr"fbMbODCVj!V.4.@0?j.W<+t3jI/FJTJ+b)HVVdnAm[-mXg[DAB]J`o<on2]Q
?;b+;(/2H^3(&_lGj^KilqiqX*SQ&clVWrIe>0`0Xi)/lh,7Qjmo.B0r)S(8;-p<;j#\mT<IV!j2^)8D
1&LE;?MhER=6UKanuDAH;I63-9ZjEKGNb/OI?\b]8i_>L.Bc`p*65)STh.8C00-0[&BAsr.WoKUP!P@Z
90$Z6.49mIb&@:<)kW$OVNa/#.Hd$Gj`O1FN/Q\Ynu<GIjLAsV6Rf40CFDe:jk-BA2T6F.9c6QKD2Qs9
4jJFJq;)'h]jTK1)0&?<BUMcn^lf:V$c$>LBm$KU:1OkQYu?!%"g*MQ=PXb)<PFRZ11Fm+0,_#nHnHYG
hpu*QZ+'`/iRh^p[4*>:M))s:\;[X.`F&-DO0SQM1=F=YIqkC_X:s,g7dQ+.Q][SIrLl`<2B-4\Qnm)&
G9J:X/W4C(egsR7++0p6V4c@(*8@ksDqZ(sN&3sNk"S4b#W[PnEq9h!`)NP%1?-qsUij+W1=DVcFlVo?
oQNfH8-)6=1=GITgB"KL7[cGrP`[:GdGQOB&[r@-]8YPDVi5joHXK7W>q9&^^Vd>IU&H]i?5OH;^#e'.
B@6)Gma_j)NKasshO<[>CT$@"Q`Y/9ojBF3GP!@HK'u00q9esY,rK`QjPDFqm81A>g%I=0qO`+emQ7oM
TcQctd,0qYSo,'-Q`k0?VaL5;5UC\>Rf$RedaUEei5r.X9uRMR0>(K4%#]t57FuFSVjiPW]Bl,-^"r;/
UX7KW;<[9DV4@\!g26EDmd>L(O,0=c*"b<Ar8mI>/oT>t,t+^@DX,BTf\--@RL#\Yddm10n#Dl@0]T4,
TR(_/pIjtr0(HSO>S2DT4E+D@?$c<t<W?g]m8:=Up*apEGD,YHnRWfrl\"i<r,`p,?m`r65+80-ETYH7
>_i+f-ZpbCDCk?+faEJe,;6b:R>%_[()L+fVDeqPF=-fmrA@H"'lS";E`%+Th,LjGBY5E!$K@Etrr*p6
Ihbl:DX70jMY(HORX2"sa+X!G>AoMle6A^)H>?(?B>2ClXfked4j.Yipl%tLf_>=7<Q.XiY(/$Z(LI;=
S>/f3[MiKk9XIS/?Yisjl!lsF2oafV\tnf^Qc`-5]?$Vk=,/p,rBa`>b`JaJ4H%Fr8FRB)`Kp=OiK-Ra
hRjdK>Ai;)+KN-9ZY1F!WIDro$Z?h\dT8_#p#r8+.:-Q:+m5-#i9ekt4/qfD7efTElplN(_3^'B%]lte
j"/-rY,`6uToPJ<R5L`u;_%@&$F9Fp;ab+GZWim5n-N?\mJ.bY:s#2?WGuOTj(eSM0)Y0t0;68CJJ1%g
nWSbb0VakBD58Xn5c<!t(VRtZ.E-Ab8FLgrj"m0h^WfMPBY6IeX$s'!11eb"b9"8Ro_oY=!K#Q1WkE`?
]=tZ0.Gobn&nXYjcf2,5h]+jort`>j9>CAnFsA2dY:<jKWKS+NbW_5NH,Q\:,-@h(*C=sE`GA5S;+i%T
>9**OJsi5l'`@rj+V2?0WW!R:SD.6pEI5kSSo:%7n$r=M=jDM'hu8(jZ^k3fh6WW.4fd8mJU$0'bk#TT
=g"oun=/7?S>Yc040tHT=WO&9c:"6+9[#&#rj#lY#7mh@2]G4-#^XGE'DC%!2"0rh"4[$/h6S:"$dWEm
lcKo-^R4,!n$F.c4m)s][Oc'<;r]"DE[HYU!])Wf^D,eVY%KcdX3>TrO-o%Qrd'kd8*F+$^r*K+0]%$/
n!mMO56jW=i=ZhJp+0k7_44uq2@\df,P,Vo2DVRo#'6&lm5C?^BP4u6^6$/hYL:G%pgmqsC:?@?C,GH%
M*fF,?>hW9B=!'+T&gACQr=E=)pC6I[c\[&NP*Ki3LJj8biThf1>qpi#6>]P?CG"5^J)ssOYru=@YWAW
cBm]:h"-:=[<#6c[E*A8p+5BDHdBtgB7$cAe4)o$+cfQRm!RC#e'E%TKTeJUgYK3+GC#M-lHks\]PL!l
Z^gF=5O/tUd3I`6!I:!;@q7dWY-'S(H"$?:j_XC`**d_(WeT$Vb9*UDf\]@3V+`2V#.'ieoTSN#,%sGX
3SXEa-DO%s7NOlc8L"Hh2lKS'/B\pG*6dQQ=$dB=A>_Ar`,Z#IIIYNV:QF.cZE;PkBMPJKn184!ai*u!
o(a%o9fK8p11iN[H^6I'1Wu1<1!:I+3'>C;??!:!n[)F0R:cJ)ijZ8s8djCEIM2.Ceo/,RbLt/ERf''&
ea(`CR17X(jDSWjmiq#<C:>'U4:_\ACK\sDDpNI4ZZ?@-bLp4A0tlD6/tH.jp4eUt;A)^?M3d&1ZoT>E
:Is26.gap8P"6cf/$k`p8'H"NbV;2!k<M=O083pml]H;Y/8*>A1Wu1)K3?L/Ie!??FKH]DRIAsSXK_R=
lue@rnKc-q%#NlcM<dDABE0d1`q4!fbXfULkS$4A=EfflWdtUF=(H*U;>TP-[_):[I+l3aaQ;Z6;k@Z<
*t(3po>*(/+k>EN4joUqq>_u4b_HQ4BCA?$)eI1V@ZFZDK\KQ@2$J138_RP\PZ$A+"Z[mt?U#On,OsYT
ItO.?8ZP_'^6>*?=`PF%1U?ao$X):0$-s^9k8VR!Wr+/e0Vf(d,kbF*60M2+RP&moQQ#J*ClMo9k:G1b
]?oSp3KmPuf'rZ^cd3eQ74Dcu4:Xr`a.B"aUd%;nA7*R)9]DV>]'86]UT;'HE_-]1O@!E^/5K]U:%)1e
]$071"Y7bsp"qIc_XJfU4L+drd,0'F(m20B4dfHR[c6#H%6JRk3j/$C.2OV_*gH<$C`hPE*';=ZAfP$B
0t@@S]]#Q#Q#ZDfh,J6,*]#c&,mN$PMl"Kbfqb1E<Arr/V9;rN'>9/0.#sj/+^8ZH*,_Em91[SQ.#oku
&lE/d#BB)u?(l)"/T!5iV]'m#?=^^U:76YB;3PNeM3]XWG)jk\')%Wh8aUWL4[qrc(`e(Y)+d-+4gJ'7
of06W-ETf!UFb-*b#X'&W&SrEh]\#N\2qj'L0\fcXSm&4Gk)C*fSMal**X8"m%bXT^C1"5Cm@_O7(.uK
!Vn-:CDO6VbQm1);nl#OM*8.E-GO&>Pp/5t(nDn2p?M[e,4c1#N(<$^/QIVO^Z4arI)kBm8%(g`0>C90
QI0u9jELIkOZT8E[gpf<;1Irm<SBHFoB"Rn/ZjDV`ii-,ntC?P"4<HBgJ`#6Hf?'$f&?)TZ2=*S<R?Na
Z[b]nj2G?:5Kq0Dn`Pg'#PUHFMc$\+8cOVB)j-'+h3W%=?Q+(<LX.Asq=jeBc<P$u'uY`.\b2ggO6^`S
`f,9C#<joWDi@nW![7$+8EVdt?Dp!k9;tiA)Cb!&^@nF0dqi+#@cgcWE/AU(ZB&m6,b)CsAh;#X=j5/^
^:t7Pga*S?j%koiG:`TirY$7L^Ye1kj5u_Gk&iWM@@>a'[)^FMg/cnL(.6,\RFg71X^gs,[rlJW-E^6Y
^m*,UBp)b=q$L%_Ap*4k6dEWsDJ%]q3?Eg]Z?bR-9BLBO?12B\G[9g>ALt;^>KpV(D,QiX`m>KF=L<W=
p?cj0m,Uc,Hb8H`,MLn?fA3\m19]G07&N[[_d0_@NX5LN]^E5G9!KGF=?5b\lt]nGke)"N7:&`ciHa6r
4PK1+gPO:;B\;!Fk&n"RAGW++0UgR2jTl;u-c>15)!>N2,9LQT&imtS]^E3Vb@B=L@:"/Ac_/cQT%EA^
o"+WiAkfX$D^2%Ci:"LpDIeiNk&mH6h=J5"9qKtm#0Zm_\od(W,F><T?8o/o3qKqPG(90O^2%[mfFmOQ
r=Ul`4=/Z@QIej!;g-Q"j`%<_.UB?#qCVH#?T#=$R25lUTKEM30403#/7lN6/(QL_Z19\7nW(<^?P,s,
oD8.k@O\aPg_!0[NnB3UG:m]\^o^^2@jrT'AnC=bV3mgY'h^9,hhn617AKrdYn[+6YnXPaYnVRcYnZh$
YnZgh@O]q((cef@n;^TXIM&iL(>/M\=Gk5TN`aG3Y7XDFlka+[5^0T6g%Z.P_(S5;Zl?97cI/89b)3%F
5h?8dQ?)Ie&N`#5IE+P.5<L-3;8dS071ipgV&3NqOUF6dIunram2Y[h*qQV"a9Rd'LquBXldRT8f+4H@
Nmbe1'ST\6]69sEJ(<uGo,]J\W;%qHNu;94BE&-*/>_$d'h^P&h"c!PI0S0Ip3pPIDWgDQeW?LO7Hb+6
W#VXTZS$,6=O2.Bo&a;e)=ehg^*8k`bo^FEKef?(8te?rQ=>3\X]UJ6Q!:MX6loD\`HJ+_Z'<1m/Qr4*
rUs@9g5f.D+(Qmf@Wp+Y=Gj4">HsF7*[-gN'X8?\`)=grg5;_7oFX`!e2*qGbWo']R*:Rm8tq&</B.>=
>Hnn6&oPZdU`$f%*`sS\h*(dBZ'7Qb;Ck8LSER?b7npaBnF2!<.@FFm=A!mYe:-GVAPuBq_PfPok[NpQ
dBJ2Z(*\D[74`#S9X0gJ3E7VA:l5iXR4R`q\Ks9/R":6W0q3=-oR<!KBa7(1g-q@H?abF47Br(n?Vr@C
8F[;`g24f,bQGNn91b9fO%0Ys.:NmYYnZc<a&3JhfF^bZ>RamR?&W,]J+/b5.>+/CMC<pu3:;nl8D6cg
8`NO8PXp6,/!=?jPPQ8CW%@3lh"7fQ7@[5OP\e<:HsO8GD=*KZ+mGtP'P9R>.1;AJ/1".Slt2'e=_bMf
VoZ?[*Q?lX2grqKX>[MEDI71Vl2o<(_-\*V'X_]rj0otXK-l$&i616P!@%BegJs[a^F]:To@j_cIha@/
eY>CbZfCsJ7tY/`UqH$/ks3!IWPlCL8APOZrOtj*>C5["$0XOYK5b^Xo+pg)Y*e#EnWa&CD!IZ=9BALQ
j+E^XJClCFYo;ONIWrlijJX*h"EKu$+C71jRoa=_)SM"$eaA9K2#li,_g<Z)rW_f4j!n81\>lrFBm-p`
8[C39$C=0<L^J!F>nI+lGXYd6PFa`NZ_#k-"Wf'Y2htB'>,iL#BiDHu[=EOaj?.b3aea!(KHnd]NjZ]@
rm3<<7nC"6E;$K,)^1qG66UpO)ND7:qe[SNesEn)KN-su,Ga7MJ"<@NPr[_A4`Fm>mOHnBqFJ@2]s(eS
9)D:O/%)DO*jQi`R4Ygec[*ZNorj6?d%`ida6]0&'=XR[^/;MJIe6X`7.hTYhPQGa<Pr<ieOqdJ`g(I`
Da^X`<2*A]$DL$$qEeuX1cKm^'Cdg$B&h"s7a3K3eb=mX]m$FK5'GT0\@3T(+#q_uj(@E82_gho3,N]D
DTl4$mH$:]QY2c5h7^m$/s>MR'qm.H@@b3ibhat?\q>H'7/o1DOuNucj)j]'ZF]9Qc'phMgDds(IQA`-
n\1Qn?X++]Edu&GKQU]rAsRJFfkNHh7JNn)nbcdUCe`[L0+l6JQY*@[?$*=DDlW48X]2YK>(;H2_g7l"
5;U4qA8OE_j"95u21<ODL[52XMg!h/oik"hX8=5e@&",)U_B#?XS)k0dE,-R:KiZ'nV1q-:[0XiSjBDo
W*PnQY.7]>gGnAX%`J5W,ua?J9F[Z$ld-,ggjGH'd+]g'I5o7WHXGmNh`RId\^+`dKK*Ghg+U-L`WEgP
NK$.!`f-28>KPTe*X&Y2*\A\90KK[BZo,Q+EE,q2E!h("^<_0kADL^u`.l'/\g%6g6G6YCqeU%nBp9!p
;^.9fPSPbWA,PAr3+DmS2qVUdpO#db62j$j!-.F?_=0TcXk#H)qH&T(?A^?2Gq!4K]O:\^I%R>*TtdZt
NH4QB4OurZNmp\#k^M)%lg<h+E3/YsM*p@p<FOAGCmk7Mo\!3U-Dqn@9qfn5Ae?EXHg!-&/]UjP*l$3R
<p^B2;+>];H]N^FrXS>mfe,/V;S+k.`e-mSI%tc>:>`7EidQ)L26Zn;=R-COJuEd)SiUGjn0S0<ULa[f
&M3@<)I%gCQ)/YB.LsU/B5I@k^nc51*7-<fQ=a%0PeTu\C<aOqbG.ShXhWsP)QhU.Bq`N]*@b.S68$M!
"Y]`Kl"0)PiMW%./SB56r#M7i>FsL>1/9N=pOG'[g[b]>A0g\u2Wl`&aP6:l&7+s,=epibB)/MF1We^_
an+UEB]Y%3b*JXM;r,4)QQG/Thm)>V+9K6E7tCC,Af^u"m<Jgu-:QR6bR@6ja4f0\XHe[2DrqY=`cdb?
JCEkSGm'X7jq6D?bEJr0=W+%_p*b&"F\VSABUa7`pL+O)FBaCr@IG@_("QcV`a4UJF2a+UjGkY]ieRr:
Z&G-Q)m)2j\J:rIe?jpc4r:Cq]9;Q!d7+`,*DpQsBQTis]ja)?%39?7gTu9uN)F<V#T$.-9bF3;;Nt@N
A0\ePp#^O8871oN\(_"DKtFd-:JWB.(T!$JkZjk1AS&;q\(deP(39WB(\L?!QJVK;iO'DsQ7a1@^(XB/
Fk\&*U#OFgm6idV74H:".!:k`PYp]u9>3,2J<>s4aJ77F#@!YN?1IQsH)Y$;pVD^6DleT6+M^`kY+W8_
UoXds=GQ*L)P%R[i[*"p>WZ)TSs0UOPZkRbVSsVU9?:?g[i?hh]cK4GL]/I1YI0[Zbp^!C4epM4q`]p9
GO5\m-,]E!?P92Aiu&.MN_&YgMZgfTn^g4/s2;YMK.+-!h2Ea>E($O>pNPR:jZJlO:u\INQYaolh2U1u
2b#4-+nL*oGmp%,;/6*9qi0[iOJ:8"WhS!)7c=6)..oI3ms+MEc%sL$(O&R(Z1j/RC-/87;"rS[ctg=p
LOP4(-q4I^GB*A8EbO("eGQ>@eEE$he18m'_MD)/?4iI2n\4%/`AA1s%>rdWE#1s!j2@SKV3nL_R?ljO
[TcuK9f>(P.Fr>4bb^\lm6XmmX=!6_)RHVSRc!(=G<NZT^)QO'Tc$^Bi#%ONDh#S8V)Gt#V;CPbU=?W$
>S3g@Q"VU=D6)5ce.CYuQss]6PW"imXPgG@C3:aZ?EP>X^-##9cel4gLhkr&WAifZ:Pr')g,g%G*oq</
WPKYgV)CQV<*Z.rSd&P%90u[g4\DbcOXK9>.r;[eW`bc54#;sgW?$2S2p-l@T1(NKipWa1cP4)kQ9sm'
bqda=r^IlVX!mWp8h*FB.OdtAc)<%uc1P&#HBO&0G-iGc/RCo2HBP$7NLc;07B$O&"B"H5F4>)'NnUqB
o$Y[lP;GZFUXXI=bP5q"UjOmh]edih7joOYN)Hu?UX\$BEJm\/IO@ng<L2X_TD3a*k^T,tlhp,oDlnpM
K>1BMH;GdsV&kkGq!*PIGokH8CBo?-Ir,\l1?p4(M(dBgRoQ!RldX+8\hrunn<:0fHFl7E-DT(`g%6]#
7<X[bMGnmX9'rRaMuL&0]9"e=M]@K29gn2dSG9/7D3Jlt/tnrIQZhZ5H-(J&WBiC1W?DYo,?M$T(o=C/
Xa*/,')m%_*V95^EBKHaIH.32/?Bp@#,fl,%:AM+q#5sl\l!s8=eG4"rrPu(NVeXPc/U6QEnV5D.1Zhj
`sF?N%SqJm=#)5M*gER6+rm1DC$#Y\*Y`=H#JTJ9lMW*EH3DRON"]1-_s]5V%:6UP1f4Y?-I=:a66B9p
Sk/E@R3nS>C(&E*eZR>H=V::kXZ/VsW$d=l'eq:,cR/L]'3(N+Ykh(ki0$R5.+e/5hhdgfDf&,hh3,g]
Yaslc?!"#Mr/s8tEGR[%[5kh8dph-HfVYD]-`Arj7%Bq(ZnQJ5W9lTXg]VAu0m+.CVYn9&<s!AXj!nn^
WULs-kc/pWE9[RkoOfe-mLrQZ4k9oh@@X!*Chjf3`i\?B;1.XU:fomO!Fl7P'53Mf5i0DiO./Bj=8CQg
b312!FD5`7GatG<qTmV/R6):Wf#Y3_WdFI/24W7E[r>LQXiG.8UbUQ32eNP/f_gb!\7lVMFK$S$Xjt/F
pQo?%L:NrC6X)J^Y>/7aBuo=_:a%;jn$:ZdqI^k@WkID"mnlP=fQfh&Gfp0r?C!6&_e*9HJ(feI2fs('
B.<J1cK>2L5F<be4SJr9"2:'p):iM]'H5Fs\tjGSmddX>12J1BoRh]ebPbgkhX+A2aLXl-9ZQ4MJl!hC
s%qLd23r'tdGm=_XiJqL_s+r"l)L6?k_A%jI1%]2P$M+=\4[d8$bcsDBp(rcJ^SZk/+?q>[)D/cL\<4c
26VaS;GI&B?+n!o5Ah;rb`d4.*+F@Nct1W9<j`0-*lF]-(/_#%58]O[_&Q*+TJu$+(&*>X)X%F$bXkHF
m]iQTje^V*9*+<QQPl\YUmi$9<eV=1PRQ.sN3tLFq`;RFW;@95(i&FIDi_<M?h'!eI#XYZ(c.Pn'?/5&
B)<C`%$ES?;4jZuHu@9>bN$4o"Q+IXA?::$Liu=O`@ai%GgeAm:+,hDl[VL8C<rIQ`<j7.?0Na+eo.hW
Vts1/-hsk?pe1$%>NO*K=Pg]ArQY66NltenWVlAap((J;2OU`<!]+f.\87b($h#M,S$E:X*F[ENeNK28
Zsff/ddHE`aS%Y_m`-QLVingE$d<9C?=:cSo`F`F&U8fc"m0TJHuIZuU#,5IG@?NYZ1j0J9"XQa+F5O)
c!]jo]q]:&V:&:6'I*b3>HmlK.E?Vf;ou6."_RBt0/0Rpf*LC?B#?3,>dj1:j*LO/lo*LS,/\6fTEqfL
.Y4)G4#RProG4b"L0D6SCtbcN*G+im28-M75(aX>Z(gD2SSSVIS8:^_Ra'o#[2%GCA>mKUbePqSo0*))
irs&49eS2=p(B#4g\TU_"_RBo0/2,j^?J'kN%a&2m7'%"2O<?horVN+_u6Vo6f''3Q[:4Nq=>7t)-f6<
It0ZG"_QM3=1uC3aQ%Mr)>k)\I`m8CoMjO9[tDm@3Gl4*HReE!>Ei11@Ko.@kc()\*`RG8B46kF;b?&A
9#p(nS@0rIoMeTW@TN5&c:U5u=?8e6VU<&fVimg3(RQe(V!8-G+l12J0VS<R7gPdbn,i2,&,#:f`='oL
jhuKP_,6EGT>h-33hp?YRR$<[4>Rr"oPSKACl?0g^.B5\Znk.g%XqV1KpKMWa36;b.Z)t[akHAdc=5\)
VioqW:Z\Yrh>6U^;Pr,sQhW#sYfL&9U.aZ&ok*OJDmS>&C%@m6U_fC'21Y+]/=7BJF+h8,?1[MaH7D5L
8l):Sj?/6ib%^jWlCLW=ZsK.PjG5ph_UUf7p0tN$?3/$7nZ[9g*U+8IjpK`8\i3Z\`NN6.oMe_Kkc(B%
l!;gT.-U&p(optRol]5jGL[)FK9Di$lEKT@01EKX>Q=>o9U"Y$\B7al'9H4c8muup<WBctErLs"p/cYA
_XY?Sr@JLT,u.`O,Cg"Y=p0itfKG#A#`*m3X<0+*2H'F'YP(J6s2+<CC]5p"I'%+'NhH'!V=bAEY4_>)
U9m,@XphF^4N6'_r^#B5qEa5kqRG!+9!nle2R7UBrI_.dI@_7Kee]6RH:5:7Ue,^2O)m(Mp%9>=rR6P]
>5t7<BmktQ]%7kS@es;nfsUfFrR1Oe02c!RpqsFRI3R;>_U//BQclek61r\&S.04];SVIl9FEFJHIo4)
ZL/tIgSOR_mRLHQkAdHnEQ061^2WfC!=NJKntao]Xfq1al`1hi\)t*&m(EAHqIap;.NSHP/C'$RogK[1
rI_OorBSn?%Ge2[`S[/_Ad5C"W*<Nm1G2"r0E.!\;2m]2=&37'/AAMra:[+OfS%hrh2'MUUbK3*T[GIh
;sX#h+1;)6ldGWm#&&G?e0hqiRh8$RPMTC*G-*F.Y<FnnZb(lA1nOBOoF'"*1tlM']0bjs=0h1pbSNZ]
WU\M:=E/X;[;l_p>H*d6S5-oaj/pi%.+QTMB3-j"?Y<FQV&s>O''Ba;=&N0"X:)WlZZI)%fbNaUSR%A;
K\''/A_hSArG?PGUHV<9c2XG(o<Ts?;WHGq/4o,S&JV8]c`RiVfp1oNMX'hO4M/K!gi&Y0>3(U>.<J07
XK/ZX3O$m7)Q+8!H`Uq&4Ebne?7.on"TNDoAU92:rW?gNq%)3KG&*URZ*.3!?-->Qn/>pG'_M)sfc#4m
I$a+en&hsejB/!Z\bZ+qVmW`:?q,;gO52_?!57FX);1$+hadlg%\D%ALVQ]!<NU7g1"#2>5'1hFH:G?V
8f=I`.pCqjPt=$Xl;%.hClDQVaoa@C)`Yj$a7"!?Eub,V48+X"4=SAfT?N46gis/5*g>UPDPF;R?`lEF
cPa4D-G:++`@<[LMlc7'Vj0O?`7&.fNa<Rqq>Ja`'s&X+SQ0q0CSpE!gle9g8qq<BV,fe9GH!aNo\16h
9A#5cQX#e)h[)qckCf<[XW<u8>dBA-_*gpJ@\55YBAK(T^!=4RV*Jj=;=a6<l_8jUI@'Q64c4JQ2d3(_
LQldo(n?>D%OohAMUoq=.!u[D>$:=56>%fPX4=`NS_>[_Y>\FYm^qiH\&&e]qr4"ULh"%R9Ng)Cq-BT3
`A\7loS>5FNHh#eiZE3jMHqMa&hjC"cFVZZjEG=[%eY^l9,b9A#<@AqiAm]"aa<qsE_`]Uhdr^V1?*DE
C`7>=]Ztirh_Q0UU4*<,dp7Sa`rh)4WD=V4Yf/h_Z-FZ0[g=[gISP`8*b3<jX(Z\`Y,h6+Y17RIg%GWO
[D[;Jf5O3d11dO@p'nR8gJP%f`-eg"^D'J<h;CkP<BQ*ZM<eo)C,q6M7L$[7r<PG`J"$sJWdM>f'Wc]4
n")1E&/=PCpa.##e#BfC+7J9)W8#@NQW)^LWNpD"o,?%/fnf9IBn-&dMo&15(tVhNokNnLJtTV)GWY08
dIK.V;Sk$&.^,K44maC=<Ru]k!U;7F[6B%Bp_C2@pmo[XI.c]tn!Dl5"p_iqqWYNHB:8=ops^rm4;^;[
G:Wc'%[onOBC)m,QCj^fs"<s:KBJo%<-+&..^]M?1Lopk,Q:kXlSV1F2jRFP4iS_X5B,h:ju_6$cJ"!B
gP%X0P2tosDdGJu("Uq1_eY/$C6q[\'\Q..]mm2S\g;^qhgI31N:-]8SDQ!][(%smI)Gs_m?F3L)Y9pu
JM3J%fAO[A3[JR>^.;h,-[TeIE4"!@e"47&s(F,n50Vf/lK,q-4_(#+9#&2d*uALT2Dt!SL\X"O2RR&T
rB;Zq-5V)FS`>!uW@4R5UD_M!"/t'7Fmk2D=K/Y5:7*om)72`AG.qQepVqVUGp(hj^t3h2XK!4HF4?eE
6gQ^RDsXB)O^7:'g!j<.!,ZHmPK&+>PPS\b;D5IRqW^m<BjI*#h&"hB:#-W+D`0IcPW&1t^Q=,"?MVd"
6XFuL1FQH1D\0MSN\;))mIhJk02KB^U!'QU$&cl$]70"9)7HSO!PY#a-b*!c".-PR-,,4mPmK)c8q6-f
6][Ot&+umcjhj^HX-X>9Ilf[!If3/N1pu)7s6#.ckWQN@98`W'!hL<>^3ho!%iTj_"7*,)I)mu2jhh,e
e_>g9OO]%:TMhj4HMJ+Q7(dn\3Fto<+(ftEo>T"4D^O#:kTO:s6uBVbLmFLUCb_0F$d`q90`uatYd1E<
5-0fCN0Q%<EX)I'6dgZA*tDGMF5D`Pkonf9=WI?VG+(b8kuCUKaMuI>HMOe"cM.=YbD.BcMpZ+UW]9(U
d`[D;*+a6.\sN?&f)FV&,VR"3cnFL[@4jp[I$gmCasD3#IVQE9bc(7gbG@36VaKZ#C')*5WkFp*-H!7_
]'(.q'E%iigL'^GiA`[:OhjEO)*DC:Zi3$erP'JekYLR"5KGH<fm;[9(F:6V#'_Ruae\ci^"i/sMP8aT
\tR%^R:::diln9D%RP=Jd#0,3Rf^*c&-!r#:@35-AGU&M*(]bD7c=74at1*Da6]P,%ZV@'QASocr5.q^
>'K?3C5DK8C2hO3hFb+Poq\j"aH-I\&HYb`1WWoUArfnh:3e8_.do$$O1*09qJ5:5U#Bq1-g&1.+#\Se
)XechHC5;$SA"qs&`A*.GQZ9&=gH"Q]H-/Ue#rV;K0";9ph1-A9o_qgqI1<Dp;ST0(UA?fPb.U/QZgF=
eaCZT>CL]9rT66E6@W.IguGXjJ7#Q:DMb9&<ZApNO>1hM@],:Kd>u.C\B4SE8ILL'&8ADQ;UCE<[$PO2
G/rP9m&QKson`4I)(0[QT>+ro)=1N#05jAm,3.i3L#L&#biE6Ih`5Q\3]Z\adm@@Aj=+F%,l0hs_Hf2N
Mj_+#kX4t=\ac6LR3b@,oU74*>H/(4f1ikKP1kVUWLah*4el8F#-#egME#9pedm4&fs!Q%Nu[F+C?du,
BYNS@Q,.$ckG?!^35_6,5rpYN?>fRkkISuE-W2$BYDai#`TF/_*'@^K:)?E'c:=*/E?Fe8VP3'R1m#Dq
S(/Zte$H1elCK;&XhU/!dO9O]mp/.k[J5d(.JneGjo"(#PYBE$`(t%OPo?P@\od,QP?ILM9BPWmRsg)3
_[jFN='a`^K"mRd[&>WXG8W@,_j@+lR8rf:b-Kp7%fY,[%e)A=rFYeVH?KW;?E0_K3H6InlCcQ,%G,K$
8`j'6ONA)QZHjlPE@3sBGtfH'mE/o`$@7'`@S<(;XUCSec'>L)&(_ZC?Xe0V_X+!/gA9tChUtU#m<Ea!
*Z>r*]l?-?En,@SD>+P?Jtr:8j80Yd<KI**a/mV!6@I\fM<phS.^A0=AM[Z(1Oet[SE3W)*l#aBJQEBU
A"DaR9Wm_^G@q!GXt5ra$nX9'CB8^c'2*%SrF"3ljS89A+',,QG68YXYZ/Sho2R:=K`$kT?`'rmYKo#"
o;3BB!G3Dj/_!raR!Xpn)\fV2Y4lm.B4rs:Tr$KRE]qJ_4@:VC$R/1J8#KT-Nm0@5^dko<[o74HjQN\O
SoFr0<fRnqU"-g3,L;sp1*G!16e$T/bfpL5Z.16cQ9'KJJ[j:e1d%2Xlbb`9GqM&U0`+4Q'I0Bk$iUK^
Bu+.gZ$k?(61M:a2Q-=XSd7gtGM6dPc-2r_f.Ue/pnA2flH-qr?6#^,UV6M")1aAS?9Ol\Sm))<<nU:R
68&`<2?s&4r.H4&be!&1^YJKd.rG,4&\b!pMaj]^jsO]@#rBrZ\KPA4!m'[%"V3\?p$MF%_=PmUkE?mE
N1EG@1[e<2ihW8&J^5n8SRFI6lUL@dC"49$rHH?IMQ\3g!AcKUgZdWtC/J9/5CY?88'L]_T-N32'O,Ji
>;sO((+j%]C=."-GV%Wr\+#D\hr;JFX1(XTNb5MOI/O3b@EVcgC2'-2?uOeA$[=,-K2#Q1Z?m!#X`i<=
g6<pGq17^sVsgSaL4aY<]9ro<lDNP!pg3qOm^/#V(Z*Dp'm0!V>1aX6C2C7W9$jXpH_L`C^Y\,54)V.5
\>Y?F8r^<BpU?:ZHR)[N1$@TrVISd-epk_A+)>G[o]B]GljTJ9Xd!BbTl5Dec?\/<q!FE7I^7V&lee-u
)geYJ8i<N3VnTK<1$2'C_C^6)hY'&nd.Y):@2U>&Ebt.GDur4CfA,&77!^5q$,A,$oJc+IN5eKu/XO,@
%L8hdCg4N1`kr_BI0n[pGbc0=!1Qop$VqAH-4Y>DqkHU@fPCoCqm&HQPl50Ce9'Fe72?Q4J+K/K?TdGq
MXTfXU)T+FH7uFdK5[Z>,?FAYcQ%;GbPpF[,3*LSlgL8EEGXkh/QsGunD01j<aVK!dsEeKqr[?#]s<M-
$tc&tAHpB-au2H?I#G;TX:&6p?F7d)c/?!WU[<9LK%hd@<qi0CKi,0QrC1(=R<WqXgOk7WD&q4#.$Z/3
GqAcsDX6Fd_5EHU;&l.3H/1)6I)"'cETTZ(^aDVlqPrt]ri4<COEsj(4lfNYo,br@)C>:cM.aTT`S4==
haP5:\)D*d^#8:roC=(JX2CmV2e"L3o7>=h?-K6&L1&+uk).O'R4ccEhR*u$Zli^u"hH+!q"r(Z!Q'P'
nXHnZW-7g<dttMFq@M1^X%nsPkK4"S]IeG:gA8*Q3?GQW8/]qEbbTj!Slp0P3]487Y-r./Vp%DT7icNR
4)%ht^0,hdGaS.s-`i*CfW:!r8\0W(f+P'[>8'J&o]BZG=B,)+ON0cU.s2e,qlfh@C+-,ns,*_F1VIbI
Nkir`44gl0Y+m#bjS)7tV0qmsh^%jUr;ci2nb1R(+*OX>lqGX0o-SOkXdK)GB!c7=0hJWkpqA+%9(Z7!
?fNfn\NFiXYAe*Gf]`A62BUF3o4^k*osCRL1SV\]n+a>fp_UE9fV7+uD*;_bepk`,/Tjh;9>2r%V;+/Z
0'`ePdY^A]oq#mXkWWEg!9?u;Vi:d@'/FY!bK4u[g7;CDX"I&a\-Y<]'ncM*]&27Z!Em--r3C>A]4\Z.
_SYXOXGaThf/7,;?$HKks)I4.eFpdnpS($%:oW8Oi_A=6S)U:WIcAtGf7IA(^!So!*3G>U)+O%RpACZg
M@m@"COYK67i&D>HVq$uL=/98,HX\9\?B/nU53-EjV?dWdH/cYU.p8a-f7'ZDuECkd3O!7[^UV26'bS"
QX7;hS%d)PWJpEo7"TtWcGnUV%X;g&V;lg:<aui/0.JZ13gD'V,U90D/U,*S$M-He&2Od`;XGUNp?M'7
,4BDIa*%W7bC`gtD>=co@m#2<%Yl+bn)(DI7f)8'c!FnMPeN9BjDc7['B[/Z,Nhe`R$j)/rjktJlR8$P
C`6b^Q>P3TlpG9#"li8?j#OlakrG@po3T1=`g8o@X'rZAAs>M5K_g%M#;Mrbk?AH<rgr(No_$t^P6Wfd
ATp%m2<[\6H/QVJ1F?l*=rs40C*^MUl\BlXkN5fp'<i=V]TmXZ.R["PlC(EKr#OGeZ-9q-a)h'I]NC'n
n%]j\\[O(L]h\L'LgHuYY68;N[k6KmBI2A<_XO]a]&5MjRZoK$`:4o0_2YWcG!hIAnG-YV"?V^IRik6&
MZh'+0>+&bj'@?t!1RQ.PUVR#-P%9OWNe3sL]rYJ$B&p.e0s0+m\'C`.W<&X\RPb+,?sW8h;c*l/`>BU
2o\!KkM5b0%G#<+VXkUO8dBK+K6&Mk8\?,[>`CI4UPA@.E?B%^g<O+9Is8&h*Qj:/%9M_p\)?cD0;]1G
N1LZCdjIV2n6Og.MO"J8B[q=8^S?%"1&],IDCGCA\O9dhG]RS`g<.0P4)i[$r5RLp3a%M<o,R"dJL^UD
_M.Giig9JPM0Il9>11#"fBA7J"<OQb]K;C7oNkUu`EcR)D7Z3D8Il_OJ$X2^U.QQn]g"b>N$.ZIkJ\#i
J"+gGPOAlW-N*O&a@siEnVirbp11?D_aR8G9Ddb9S\H0CVG'1c,i5I]Gf+-4\41i<T/Qofo$iE%7D+[V
@5P7h#,ed7In0H]S1fi3fbVc:<GO%H=CYqo@T,:*ePGUK((Zkc*&B),V;pPF7cKq^:1U:_B/)laGSZUD
qC-\tqMtBpc?fd#f.odi#3AQV]T=<0fAgQbc1EqS(Sn5;*L+Ad.Lj+h>,Z-b:Oi7hnRc1`MdllS!uL$^
,$p(pVg5kEPdm[5]B<'&0'$10i(9I)O4$W1`@oX!BX^fub11.tW6hsa`ISs`8GPnh!NFo#'jk>8c:qNS
a87q\1>8^?;(DOXGL.Me*W/[lh#d97d7)qG`QtG1_>&9^b2gLhas\e(Zold(HrXaBqbSVKhA9`]f:s,O
>4n$FG)uCVWuY5"HScQgbiE!^e[\M/UJqrbIbOd=<tnF]j]qIDhI#8_SS]IY$e3o?3YZTYW)RFbjSJDB
mt9Uk?HQLSqj.pX=kWVX.hW1Q[ei^<,A6]j-q43l(tWTbRD,EOTOR#?)X$0e^=hCh`L#1VROI(?7ltMl
c?bAO,%j5*9%)VV&?S0A<EEtjm*T^t+]7uWhL;[]$KbR)>#\*P>_P\]aMP!*+VM,<=".\i``o%%>O^5S
D/?0.\iDFgQBa(:'dA#opE<4j2Y/LoE#[qSOr)mGDJ%PmN$\LUIGJB2GF["5:Q.$)'Kn7AQD`8mZI`f5
CQ!+Her.s<kY>=K-nX\hCG*PmR:agQ.VC"]K'$,,<aD?4lZLme/)b1oi#$HJlLb6lO#%Qsg_70WJ[fYH
LM34s4A-2*6g7+j<B:@^lsg,VTIKLJr@n.tA'Un%hu66NQY3rrC,Rg(MM2Ht7^"SX6ndiR`_V.(o)aVU
r6k14*;"TaPSaL03ZWFj#NN8gZ!RgM4gt9KA&!V"!jl=N$2iGZi-`94.l^]".mVdKrQ;s]?TBjSfWX3k
p"6Gf*;^K+r2oo)j-@>c&_;D"[0NH[l.W?!G4jLk0[@c<$7!F9Ve&*d.q`u".b/EQnSJ6=?bp"N1$AMh
Y6\jW,$]AZ_RT:iC=i+s^IjEKe<aFDa*n3db'PQ]fLlgtr7J1mm+/pm#kfbZraB%g4n^DC]]6KD@uqO!
^\97Q3-H=:3*5=L_A4E9YP#NOa"M)tR$</&2bT)lX?.cKA&%Tu5@nBF$Gf:%[4^!TY=hm-.RdL\]e7iT
Y=j].`r+A9Y=l:\lOmZ_YqB&0'Uc-C>jE#bY=l8df;2ZClVai\Xhjl&f[ltRmWF@Lj184eF'[dm'`/mE
\jY73:Y'N,/1DGbhs4/TYEt(+C0fhHq7:[oh`l>=Wr7q+<pf9B;o*t^a4n_2B"PEq:+'4=W*+9^n[J!I
g&6_K@L"L_mltn,$@b&u[j;FSs)-o87gk3rB8nXHDguQ,s6B-V`u&GH=5.DSEUr_AHjT+SP;&H]<VQGA
YD:mL^?_C4c>$$-c3_:].;#up"lf+]F't&8&Ds)BZG:M@HBMLPGmi'i\]?&RfT&NkrGr.a\po3Z+$J=J
-'&]UZS;asok9m"knea,YhLkss+3>Fp]k`%^]'YaYi@M2P*LZ+_L%A5$t_;\@;Y+eM9C%]Gr9fZ:cY7@
FR%G`?1ZKf'[ukh9nrAI51gb+q>-0"%.@-(UNE(K45ru]NfBe0AP^>WTQWEkhtlVr]&T$L8%ft@:k+$M
[PJhe@\g27^@[QOd!8QWW7:4c5Z+c?3K!!IS^loqihc9,JtITG7PR3adDiF$:*5g$JlK[)<V1P;E<N6I
EHYBIVA#9b?^d.8G,mWloi:MbH6abn$h#kqrNb0p:29OMJ!)Ph1XO0tHIV?%)"kB0rYUVaV&61\#s(Jd
TT[#]$DL$$qL[^m667m,//`.;T`#&+N3N:7=>J@m:q:,6g[uVNRFC8>a\91G]k""p2(t,M'9F3#be0<]
PULCh4I=9^ctg=*VK_RIOdS"il7sT[)&.hLn<]@W[d,]dY>Sub,>&.:lS?B*W"^JQb`E;e1SbXpL(pH-
4Qah!YhR!YO;VJGkilT"mX9M4mu+Y^\.@d#DTPe>MI)slHAnN)-r#l=$ImMSmWidFP#UuTV3o?3'JVT^
^EiQZ?0?'RQ(6/A;(C4*QPkfQV&61\#s(JdTT[#]$DL$$qL[_8!?4OZ[o%O*DMGnHqb"j(mo]6i/WKmY
R2!><*-P=p8/8D=_#!M%orf.]8%#9Z$JTu/<t"IVqKO'X6_>q(`c9$n?3aQif<RQ;\b*f^k6N4f/0l78
8g^70Y$KRhS@&f's-cA@B>StNdW8ZEm/jO5nD.4)B04j'#Bc<&NGp/W'=P6WqIMUd=m\8"n>h=k:4LZd
DPmon?_GkTN(0W\Bt(F#U-m8:"[Z2RQA[:$G-f[ehjtEAE3'pTm;gqOQHD5FADa)0.AD$?p8QX'^0DIK
gF.fR6>E8fOcCh12sbi?)dB_]Q9K?YbGq)M>UH=0=!4D"Z/n+3Qd*h'>J&en],Sl+8o"d?]L(5rN*g<G
5Vedp(9BIWnSl3X3!F[`=.IldN.S:@9)j/,\&nG<pl_2R4WJBSS](+&nptWFm(/X<a9<7.WLXP\.+9dc
Nd?<OjME]WB`Z98@Ycp?/$-A!D,tj$0p%DW&"gEBnR_Zidn<b_X'llLS2kA/fn%l+LY!U1+Br,'PK4@)
1qmobmIR!7l&&Iq$PU&p_QL)p_Ct\6OS:BL($)rhi&k)7(!-1MXPh@h%a/U_*k[%\md9b"])uR`A``;;
b?JRm4d*>m-\;Ie!!GS1e$Co5G,s#4T(R5\530`WBM4X*F^<.Bs5<:`FmH]bJo]7"82%*5@7)u,CerM4
cq<'I4MWP*=_S*e<g@[[O<GOW,j/[GL[1!XS22RY3p3S`<T'3P>pB8+-p;i7otn)U*"U\#gu98.qe7?`
;onA^\Z(>m?MMr4;O+)Y0:BG^VUUJrk?+;9hD+sl*09Fh8?K^ngUSeglNCBbNaXnq"0WX/gt,4EN@.eU
A$0,,3i0WpkgMTDiS<3+\4iP/c!+>\%::i-0".tmEL"o<JfYj?`OWm6Bhppi9UN2=&@_4OY\WN`UlN3p
Z@'U^moVM]*Hu%+1=U)Yh*P:@6^9Gg\YIHs?u*;R)FQ[6=7i7<j+#uhJa9j.?arENCad[T`Oc@ul=P!4
Fs,`u<f5:kgi&@P#NGOFg$TWS`nqobIUT.H5Q?a"1Zgma"0uPUXsgcU2Oq&(N)\DB*3#NZa-SC>s)D8c
hrqM5A(Ss(r&YBmC8H$S2gaTIcA1`168!ScW/Y1mXD5hFN?DBA_ceEh.O#6&\^#>V4.c3nH)l,$(G[m_
B!sNZD4HS@I^3Fq^M`'KQJI-nXtT@>Trn8AT.T+\\8)MpbO\nEJ^iSmQrO)sKD49\gJjlm^Ifu$O%@)7
ID9ZW6R)Hdf/9W8I#0r2@S!P+^G\2cX&tu5V'0T9"c<ltDa)?Og0/1[?J!O&eW.6phBV<_TX\d?]r2%4
\SNUZs3a`8kIo>2JVJ[):sRpm5f"V\(leQY,PCec<mXo>-1?spn>7ieDQ2'nla`Y'^<W#bi:l.(ofQ8q
@,sm;PfX:d*OKkL[l\Y3)t$rc`aa0(?.PepUlQ<IWc.X)=fWQnP>eK@@eoEnUG/4[F0CF(M=QrJm"H4c
A3M:&9#GN8#BmWj^OO:p?=s(XOlrO5B7m.hf@")=C.qng.q?M1/dt%\BkE;b#9-s\/E59ma[g.Hh3d_K
L&S>@[?$\P`"bGuQSHA>o/4Z$6u2`@>Z`"`<eNo$Ja%eWGQ=/7X[qAYLjMN_?VI!6dtYYIj:3t!*)&Yl
06jCSMnKkE9^S2/%C5;T$[psiU*,_s@G;q>mK+uL834h/-HFc?`%R5j/)I=?deG;0$$qXZe?&D2Lh"Wi
ZuVrUk9!HS(S]aG<5MqZT]!h]f'0fRcP'o6+44<%&_i(fi$Ve*p^ZbKE+`#[S?87aL/R^!8_3pq*-&'.
7B7H+nm7O5LBWOLd=4\>41nMY^HFZoa**6>*#/#\7P;gX-.Jj-G@XhsqW^?%54;A+o[:%[6l]EGVmT^;
%GB$p7@+G1G-&&97\Yq^Atbo%5G%dIs!bCnkV9^oNO">lI(YHL@1lGs^%,%^/adVFaAD!#(&>kMZ6sHO
>BtQBZaK+2d%j`K=jWHsb>%_0-ToRDpOn?'F[XPrhl!l&-M>Oh8N@Jh4XmCq@q[X:V_V-Ro):ZS_;^e%
4)KIce#_l9".$[qa=2A_pH`d)XrmR&/]X(>2R0hLOR7Ssf6.AHFJGqs7)\F8j1]M0N'MX`Z]-d_,MpV5
:85tjc+`i/4jrV7MW]%)dcXb9gK0R:P!t&LH`MC_?Y,?KmuaC'&7LU=J'!i<]g2mOeZ$J.3<a?tdbi`_
H7U->rLIoAiGG,LncE@MX!=1TT_jiMoL0013F$S'V7t"cqNCYKYs-[KiK"kdQT,5?-2&(!PHY0qD@[Sh
aR5?)_R>-DDVdWr;uYIYJ)`7#QP5`M`_K0O@*6ked=P\:g?P""[:;T^*Qh(LZ-l6!>3p]ATgllk4L<\a
b9:$lVs]OFgYZa`*=\Ug4$M]`M9n=Jj/^K"Q"L/.TCMI,pPc:rN\iqQ5fX.l1>s,n]f+^c1R*.TA(F>4
c^][4\n.us1mi@-lrchB52d>SPD4A#UG/Tir;I]qk&83V6UO("cW;U:P_g`4P*Uf]WKVf^:)qj(T`oK1
n1TAgU:%k)j.,#J2oSR!GS2sn`&4>JA30^phQR'Lndohe^md!9%*.M4#K_JSTR`X6Kq#%Gim*rKBjjS=
G#6+0T!aJ#@lOpV,%K&9j-VE@;S"9n$\-PrT9Ch08W?GM:HraUH8,4`5&/9X<Xs.Qk8:gupa(T^^N\ZK
NI>cH8"TYkdT?AVfrWSG.n@aC3?>!^(H@\I-bq/jEN&c)0?g)W6eb-#F7>h]HrE4QM_R$;RSuOPe84Xp
UIhI2d!mlBR[nS)#?R^M.?,,3`?B)G<BQrr#s#sFUNUkZE^;b^HemE(=)IU,_g=eBk5>_uVpT`>8*[i'
*TFc,'#3]GA<.f(-,mikDOJahgD,pW&[KWXXD55gAuA?hbA>$qka%X3i'h'XB)YG,AlI9U>@nP80\jC2
,<1r9#dX?,iTB\o!a=iZnR$1C$bC]]LZW*><L+$jW;C;\32U7F6h6OlhWpJq^+.<"4`UndoR0Qk/[NQT
*j,F`4fW=YPsSFGq*QW?Y;"VPopm<LJ`Gk<V@keMhAcBhYKFRWO0lXa^"V_AcKV]ZdU0cMH0KT-pCR2d
XH\k6Oe\SZE;+"A(OM/9"s*>9;+Wok$JtCY`OEhs9BR[ABZ:d^J)IO6*7NBH2YC4*(0[3bDFWtGG_d^n
Mt,GG=?:6WN$Bm)Fehih^UVLl"IACC[fe8[H\SB'E$QDZ0BAFLn/2aNq2Rhgq=!4G>6:k;(@Lfp]A@&$
CVTMu3gOU$LbHHN-DcRb]sTJ/Pao)oQ?h!t#@s.%Hfc\-,?p3jeq&)Y*qN#hVKdSEDD1M/=kqSgkJEAc
n..=,`I&F/BMaSJs7^K7<Tkme/`b5IBo=4,2A/)@/&s^b4k?pBPC'2?m)5=UED,`9fG.`=e9i:aX.-&!
;5qX/SqrfTRl'%!%bEAE@kZI+G"Q1lo<Zcsg`=@;hh15tX[rHVCOJ/15M,b/B1-N`=)p)mpDbV$7::?e
n]MM&c"<23=?J[H1:)c0+CC4C2V`&&UZA!?f'PXPq-GRQhe3f8/]8lf?^Z3:T):qnZ@02]Zd'aQV%q8a
ZjT5)*^a,QbpgBPmRJW3dLU\F=6YlbO)$BY^N2QQm$&Tl:^]JKG?+t)QDE"W.c0'd\,PE0`VdLH84CQO
-/^AG^TgGN^cGWNPgGMLn/(O<(!SBnLJ()2Cd[WY8,)A9^>K#uj0T<*GE[HOF8%;dBXeP=MM?.oW>J0(
dIA)MGk)p.V-s6#*D7k#DTek#HF@'.>-IDN!617/.b]\)1OGjOr8M.[cG;ul=^3GFQb(Zk(#cRg/eqkc
+VgX@bHhDUhkAeRJUN3>^@YdBZ@YS/s,NE`BGX-WJYAt-E!*&f1B`boiOR`A>C!_KXUr`]19)G7VdD0"
rd<XQc0WT41)ZK%$oNL#B6:\p0A+j(Y$@q;*.\mmd_IU:_HD<-5sIO4Ng!Mj@m7+Dm37M_^D<X_\?Wc\
iCY/=R6RaFOH6Rp9-$XH_/lpaEtmr]>&3IL'2Zu7m+E#c.Vm;C!fcC0HBn2(3#O4!Xb56B>QqrN=HVnl
eJ#HsSV<<prlr:Rb22/-9$+GmEIqshb0oM^B[J$ARD/?$nsQC@V30Ak\^7:NhqC+>SR15G5Cqe<4j]+$
<Z/V]HbDGdB)SM5YCSWHO(P;q<aM&[+2/*JK3OJ6pC&S*qL;6A8FN'ECNe$`afU`>XUifuKNpD\Y:7cd
E+H(%qSo5`G>bC)M5SJA]A^5irn+=*i+U8RqTF%2+43GhG=1DrlYU`TT@QW8+djcfFO8,6qdafBm:'_A
0/R1BGG4@c41R_JHrK4b[1UNi:T7//rCD_]Z9C+jlKY0(&acDmI,E]#0A2;n5t?[YA`+&gThIV)kX2-0
,d3J-MTb$8DNpI9TDR`ef#8A<-hBd)*0iDnkhfW+@*OC,bkerhas"IQeQTM`$6fe8pRCIadUc&Y-#fi!
,4b)_042J4#sE4A]JPGdb)4Q<QSakdK)"(T>jBrhnG.8Jci:"UoY7L.ojYj(XT53ts,[2s)48*a(I]g\
)h0r@79ImNj*:RlX8qqfp[,.65aaS0AJNLdQjrYSRu9>,`eF6]c%-i/nN8">XLP&(U@8@C4T!qhrM2(f
RQ:;?):oQ4B&LEEW5GgmOg+Dn0WNIj(0A4fI".bce;2>V5$%MNKU&1aI$qDP:(R_d)Njja-@97_UV?j_
:;`X:1GdDl(=].Z,"+Y"DPn5g4n,<U&)D"=gK/]fs3<Of<`=O_52.9X_,ss9.8Q@AqRcCCSEOnPHd.d*
nug\J1N($SO"qrTe_dHs.R"cYC8;`TI[ps/];LTeb-6MgJiPP\OS$eS[j_D(Qq6l3N"pZ%NBA*30C^(.
mT?3m*LLBW`*!U,O?I-RiW=,$A"H`"F$oY60LS*;B!JUCr'@Kk*@8mLb_3*!TV/c5Vrs>[9T@n8^,K&(
\FZmNqAn(\K_SU:Dt7SnJb'^seODa]Q8%Nq<O4<(-V2#1=Ia1iq:h[CD8V\I1Md7b`/14Dag[brb-?@'
M!HTW\-WRn,i<Kddu!8sn200'9kG%n/iD803h/*\;6!e/gZ*Kp..!/1_1kE?8F]%`mb8t;0cuZejbC1+
LHLHe1?rNt2RWZ9@N8>eb]4Jo)3U7#akSWBM=K:8ZCVorpHjM5OPI9Pn^o=-N!$6DTX8UD4f6kN`hfHP
CVX%TN3obKp`+`oEq,5%3X&;T?KVWT[qP&Q>)0R>qSANZiXcRlSO&>5jB%9_PEV)dh\?_ubclu&\U*3.
mk-O;kK::o/%7V"N@1Pn@D"Ok?0UuG]/2rc^1^eWh4tDlnJP5@\ELf^Ua]H3GlAC*AbWZPd(_BR)k=?^
"(@r8YBa@OhFrlT-Aum^(-Ef@1D!?iX^KZZ://"CM3;>1'2;`H&AHl.\gK9<)`G8Q<8[q6agDtF;!2-O
^l?$@^:jj4i%qbLA#j2hLDR*&`!M2mR4\_Z^$+jcMsJ+H^<i`N-naJf[s%WCouk1:ET@KR%YM5XlYg)c
$E`d,18h[.=aU+,6]5l;(qnsShS8fFO4N4CkDkjeGq,"f6PqE$[5QIq6iPPQ2CXPu]'RPDDgKEZ9Y4k!
.r7..2S:ckMi?d<mF%FB2bC53._VZHpa*)J#gp]D"!cAq18E3gbB9WU8d:dH7#qnGBnCQi6J9&cGrQhY
hrr!Ll/F4hG5/\-b8'iUpkE,m&qKuA7G3jhkppf#_ja?!1m1<gW21:nTD?R7^&44]G#iYLC7SJPedHfL
8%U&!``=22/+@\!bt:.?m1Wn4WQHS.S,ludUr1.h$Qh=r/+plYLU_4l(Su\tD`m1rB?!`bd_o],:i-4j
TA`+p0S_;")R0$/J.Ol^d(@[4q*m/[:U_lrE%a(XTndk)R7,]?if]%4&BETdru63h(PVJ.fUA#E]c0T3
[C/n^QUK<HU)/f)Ob/ul@kE`"=m'HR\_4o+lE#2B]Eu+M:NUP.@Rm&<b`a%2"UOCcK*U_q_@');>j3ib
Ph>adcJh)ZpHl@M<hU\in!e=>lD<M1K2QAOidACq.)ubCZb/-d)4od.3LK'(l$Whr8F#U]B-b,KFlt[V
!AcFC&'u5QZ2;2:Bs)ROQO8_:@pJYjNbk@]Pm#5$.CVsaA6DX>D3,G*6jT-$PS9f8![B@WTd8r3o*g/4
Tt2UuD3m;>&eEZ'rKXhD<F_S$/%8Bdn@bB5[a54]AES2UUEhFe@>d8OkopP6jYu\+`DZMD=[%91klN-=
HgM2F5rJ=3dXA\0eLcL>p=@XuL\>HC6co<kSM%43#1#p:XEgEX.BDok/)MkX.4:1"a%TLrDq_EO,4@7e
,Nt[tK4C$,Tg-"]1H'`@dmqi?)J2hlLmLZ]1#9i)e7iL?HDB9gXB&*V>[_9n8L6dc5rq_I)jH5igS$JP
R52=2%uDMm:T`-i.;f1=>U3>5$E;BhGsPn13Y<D._GcUa%8j2C]TF@!L$(X[(SgY)Sm8+_\WlH&@a,iN
f12d.BO8Msfh(o7Q>R]2htt9gSc*oUhS6hJr:>#1jo:;f/,u*_5:US9gG!CB38Sq[3\fq\rk`C>VUrTB
%<^qF(,<OkWW6Q"0?@m8%Aa?*rBHJ@kAS\."M:JHs/?rW^jYHj@BPndrti\WjnZ_$8c6n"jT<Ve`4E["
.[R[F,'cUPefFMYs*TQs9k?1?@PWH4fGUZ4Kte/dAm$%l!U2\#Dl*NtnC:e=4R_0Sa84YBs']_VIeMuf
SY(kA8#=YkK,YGXH,=s_ESU&D:QNYo1jicu;9ls/p0GVUG@&4U2mME=/.!"8?L6P_h]j\2n;@lZhFikM
4*DOmc4N81q*O13OuIc[p[>fUe"aoV"I<9IeoTcc9O3D?XDGfLfaYo^%)%ma+`RY1M`0%XfXkGkIQ*kL
D.JP]A1-PE\Kc;c@!f6bDnWlr)m^H%X'j7_@pseSp0QU6AACj2bmG'"1Ee_^^GLsRakr#)lbp-[pW5'#
V&mH/=1l:7p+.5IhTEbioX9SdGRr[>.`#n.mp4`/'TR:_Jh*qF)7Z.#&\[FuHi:3.%<>S*r!rDbQqhQ`
qq<Q=V4+(-^NC!WLgqH,CS<Bk!BWasNO?#eM&%Sh@MqtY^-GMHd%RpY<)1pr_*,cpX7]rj`E-p%'Baa>
hXp(2'b\3`eq'#UL)Y[RCg50K9jUHR,<-IJ31p+c<l,p.q2u`dfK"EM4Am.`Qt8c-5Btm'*'\\76l\Fq
j`R.9n6mAun,$8h]<?J.X-]A9hh0rNHsb1hSN-qnnW%u$Ln3S<]Op7"*^2#NQ!\Zon,)*ap%NDf7h`"m
]+f9Jl#8VR<-"!H"eF[Fho"uES`B,%'c]I%E"L'N_B(bYO6lG_3.>O::S&X"I0@U*`k"o\T<oW\nU:17
amm(9A;qSd40;7u4OHT9@<+J[koA%D*ffDbS`ABJk@sYaE'-W(XVkd'icnc`4WH\2at/QsN[;_1pmW7a
Xb69`q&[#5*m:f3M\e'[L!AFagLBg.AoY/nbKttp.Ug5A,@m<'MOmPlceoE5'(\<c]f8Hsm'%$3:-'$a
!8@O#8c9BM4_k')-!GEp6,-rEY4Gs(9DV^mJCJr,aSSUmkSDW#1",1P4`oU`SP&6G;"1@NKAqbKgq9j@
D"!+-6_)#bKt;2C0==r]cG",\_2;h6Eo1nZNBfImb<s$$ar"4(i`_30LL8R\W-T.n7hflSG082J/A(,\
*-A4$HUk!KKjV5!Tc-(7'H@2@UZ!kSUjVX)"2F,#@ZR%2&,Ln;9C$0ZrlJ1VP`8$Y6_3[pm7Ofu4lNRV
[qHV%,;-4heS#]g9<7mb-6FeqKCRO/ftftiAf>=Z$G9#Y+[FoG[RF/:?>W+m;MBJVA'$<"oni']A5p7H
;-bp>)fC>R+qC9b:nP"IEBn7`>1=lPFtcm],LW<ie&knC)f!<i3;Ac_B+*m55oM<2Q=C3&ONfm_A-0er
&gcrZ?fMNQnKh(=j'sWON,LHnS]3UtcaAbJE^o_;Huh;"cml`PnL8NVN0!.9(n[pt5q>;Q;-k(]T48th
m-`K8Z#NCVq##!tGVXKUod(1dp9ToLc-D]$#[B<:9U5N[1#a&jCXg^T--'Q"M1:qMd5aEg57QdIUaj0[
Z.A93dIYD"]jV*4*2h?>gGkeK&$a``kPpAL"o/"64;g.g\8sMFJ%^qmD%t[BksL>E`cI"]0==s/R!5Cp
FGfF.;e1+;"Bo,Y;"%.KEuEgSFa6QD/I@-3lU"+m3R7!Mi4/js-+t*M'G>*F>S.!\lB+_Hp@lX?N[@pI
r/fj:&fpH_dNOh:385,N&J[P<T)%@SG4'",/7W/Q`sTuA4k^>i`_Zt6OG\t/1+PgjO+&o1-6N2B54?s(
3'6tJVVN?_c+`c=<Uk`gU_*aKKb9?F?VVs$d<e0/mmEoel_L"ZMp,XAIuf?EBVMo^9;l34T,$h^ER)0\
JthPX9j_G+:C/mVgbJsA9B(Y%l]*k.ct/?P0Id^`1';9/R0gj!!jgH<Ijl=.mgTXs?^6&9*m++RJ&=$g
iMpPDg-4!YbP'U1n$TgpkH"P;>s0AmrC+_G>%[W-/2P!6VF)$A8>Rf)Ob+Bt*u(&QL?H&+p_%BQo]!1#
?5PP^BWhDYf*=Y:#`)mol'J]<o\SN0TFC0;ognC1.,M,[kuTBJP*Qu([Tn3lad]Z]m,YI+A`bDlcD":6
G2j[Rb]I@WU'!F%WJ(*U6,AJnUj2=%dmQ>Vim<W;Cj!PMEEE8Y:,_+eZjNn[G06b*ig'R`F%ZK]\ZUFU
nelT]Sji%+BLSat=opA<CFe^R<.^d>"-hTh&IM/RE/Y#:M;E79\TljjQ6cN)q,fPi-;1#9f<ePu@nb6r
(i__qP92R_lAVbYrolSU*a//McPV.5+!sJ$).bDQa8Btt/\iDA.B#dWn?*bY(<$c\gj[@iT]SEG+A-7j
E(M[QCaL^orlC^PaXTDWI%?t=IYN=SLi'63@Q:)T?`<3>=PFUYbs$(U$9+Ql`RBfeYP@r7K1L1Dn34+"
WQ4D-B%5hNep?l:NE4Pc$7UO`8s]t.9;@,m6*2K8omMq-q?RTYjjPlZEIl!=nb%ZL6hZV?+$<Kmi?4+s
?EML[6PLgOeU;+e'aC+L?fU2,aZnc58ng9?Ir9';GZ&N1cp_sB%TrU_'G$U.MY[AP;"C_10YVtCqT9c:
bKJlSb2A(X4H(=Ed(6_[(fad<,s=RE!aej"3hP4&P)=f6'l-\(+;J\>qUCt5W;,EfW%mF'P]*PBC.@(&
3]o#0MiUt<Tkn]YUgbM181GBI]o\-59a[3^fm_mQ$C3V"=e%,k'j9M:V.+Esh<O1;V'7E;\Q!*>-0N^F
5HilCBN;7e.bP60a@\9Z.b:gd5h@?sK2fMHQTSMM&a3diAkGMN(h(T`4WlkTlR\WW@[nM&-Q\Z,,mfkn
O\R;]asB+Z'!#nA;6-*tB\noWA[gsul.=1Jq`2!*8SWfhm%g"H98G>3rc;JhDt*-RE]-a&S>l!@rr/fT
kbmV$EMK0+3?MLiD+S)0>3!JXVe7YF`IlA1f;;1GUN_XQM/#Dj!XcDVg\$l=pm*tCh,:..XhZP']3ma*
[3E*%UfM')PQt[2We:%TX4Co0UASC&F8HFnE*rMmT:P7#D8T))C54TMgS2lA/kUH]PU1.r\d\+38]!qL
c0Fh^Pme*R<.ZaJ[3`/E]<J.+P08aH17]pOkoKaroJbZR'r*jmDLYiT2,XbLfhTG2]skMJ^5;X.-Fsoa
d-9Q%V4ombhf$PZ.\j>E(Tcrj4bS!>reIYce88:QpsdEEcoAX`nKn#c82]glpB1)]+`Z?\5uID1n/e0X
b]<L50\/,VgJGTIm<(=(1pf_SYE1k/)7g`.8X-KSYZKX9/AL+R/#@!ocr)_^a1aHh<a!sS\@-VUH'Nh-
B?a$5Mr-lL1m]anPYc/q&FVuq'W7XHNG(;4rj_(5^UqCm(B#Ebf/[3d..?0'4sA7.G<u@`Dh%a,a7Fe'
LJO9/np>K+TDnk?r7C6TQ:2id;DKM/SeqBLC]RPSI/Y/,o7,pD^;%F$Is$N"-bF9eTeKm')%2muq"rti
fOY"D7Ugp:Cn19\n)sM[GHC8$(0fEGSK@1r3URrV6d`gMJDm!-g?89[i2]Af^V;J>UoA"9eHiUF5fY3;
D[cO%HJ\-Sir/f34W]P9e'A^Qb\9:2i-5LHIc:e]S@K7cN9D;6^jQ,%HOnf%GPpYZC'u\0Oe77>n(59m
rnN!TS)WoW,M)Agb,5<*4&KDl1:4c(-*gWh<Cg^Z`46_dXouD@<q7:XRj!O;+r9d(]>"-12k\#e*U06:
]4liBGfFCp'-%_0D\NpU(c?825K@B^#1+,`=L.c@Z`uK!XtSP]B@P]P'!:HLRQh[o=rNNT\+M<J'_1HC
K2.DAI_Rl&'siFk"[?pm4Aja,\FY1[%N8a4qAWMd]l[N#"blPQa<?7"OF?O(a,,nj(Ete8G8k:Wn8*EX
-FK<@P>QgT0k(kSfigBF*4,08pqd,5hED6nk'6hR5!6.N'VdX[0X!$p(F,[f1bLIU,5ua;*u!7qg$1KT
ic>,W*JmhaC9=+#^L)dfNM:'dqI'1:T0_&"IEX?i!nDb')&EbieF]PmE6PhrB"W.r1um-,mk_[8Mk"db
*TqNoVT4Y!8*a&'B7+HK2</7sehc?bko2E2'Jh@h3-]-04`S]MSq"d8j7!YN)/%9C`g>QD1VP](SFNHO
kF?E],L(I-RU`_UoJt3Uf.:ZM`iH9Ybd_7]1OdZRAt35.g/0nKM?;(g')*CkC`eJLp)PsUnon"RT`Qn.
O\s!tYpmdVd<(%Sk@_M4V_j(o9llkgXghm8_Flr,XPI-JhN[QRrQ0sQ\8)`p=)PbPLo55M3SlSi"kULF
8Y,(a'^b%FbijOID2n4Oii)@)Y6\!,F?Ni>It/n^FZi_7C<k)@-jpU$`9gsFQRb\>Z8Tg]SN%Q3cdCW8
)I'mYQckI^2"9AqTOC."@crgST@/@A`)02-6'XK=Y/&T_DSE/HdZcd9(%2l_G7:pCZeIYgaK!CQiCu>7
rUJ)!=s2Xc.C:%/',=UI#`c&Bg8k8'K.Y8m6_3$L`\*_Tg[QRInGGA.kPsf_`W+P<cK=6E`Ou%3J&lsf
r#L"gqt':/TDq"I3IcP9rbA'8OlLU>Dr>N$7BZ5c'Cg9S?<=lq/pJ:\W>7k5!8/Zm.7p`o.dS(^fX_E0
^K:bZ=/;i-qEfkI,oZC+C-jt1fl/\0]GFeBpaV6ek`J^)NAnmj4M9nMNaL,AEFT-')sZdT75Ru>BjVNt
@/00@JO`"um96au-KR%t`U!adqNaomid:@AoR(T1HnW77),cM8F^!kPq*X4m76p0JZB:c3F^s!3\S&K.
*'qFWbABh\VW,<%:^frP$GZ.7k`i4/.hc?*JJrHa#-Np`f.ZC4nFpP025%-b7`VQ"(d[dQW=P'[EPr7:
dt3>odt,IXS"L)#=X2XLYM0)D1i6je(UNo/Un0=7h?q?JifX"$0g]<Q9`,Ma'CW&r,L.e.eV6)mplSUe
1(_Clr?_;tnIU2bNR)NeoQ8?_@sC^b`4RQ<\0L$qn7HrB6eUIl1KRmo/r.]GSq_)_3uim?`HmV`SmOIZ
O8($,h8FXr4jie""Q2b11rnmbcgbLm*&6G$Y;t_u@ZEO5^A/aq^Q9%NZf%=H_+8KN;tZ^rn!D;/UDT[Y
QmR:8(_s4JH;p2[[55Oe4gK_Vjc$DoL=A`GFm$lOcah'mN\;GM&i8ZXr&GTsMAUi83Ng6r%[2:2'R/Cd
H4Qed>0OfrW,mY8c<jI3H!E%<'LjVDg#5XC2\3,n1[d'a-3F%^<kD6n&aUU<n@nOK/I&:4A`Z=?=[QJ"
R.W"P8S-Cj@LmIuDeQBb2Hu-;MNX`R-!&mi5b/1^N!+g!W-mld\`X;q(*P7m"Q)J;iV+E\9BD!h8Ja!K
nlr+T#,sB'hlc)sGmNhGm*H1d;&6b*(oSD=XW>fSL.p`3?8+s..Cr'6?j//\D/gjT1D_3cGS`endee`r
W>BMT@Zg,!%j'W-Y)iCLc"AW*IO*2oFgq,&2>^dM6uU=T#E5594V2:rIaa"uYtQLY(p84ZQLud_'&qr<
>\t8*4/@<%H+%\SnfYWVDXgAq)-Km\26Hn<*5Mp<%YlB3Gs?Dec42\+FP'/AQ-S\!G>gCs8d?f+9]5:+
FfiZo>1A'chs2\1p%#CK;dW]-dr9EmI9IHKjO-1=Bu5&B48F:<8.=RkI``JFO\W$ScMYTs`lWm&qY0s,
K.FlNVr]6N+!f]+OM09D50>!S]Jh&%\Tp[FX)QJl@^cK1h_sm2bRscl,aW`d3,2(3+;qt,Ai0#3`dNh=
bGR*J>^.t^#G0NCRCl["@a.YYBK.M\8Fgb+NpZ-Q/"2guc\f_.F0s4pZPc`Br+4loSU?Fl)FI6-i&I[a
h\3Nip'MIB:>P7TPk`BWBc*(b,-X7)>%caq$rMgjlSD_0&pP23TdTQ;cp+k)MB&q3I0><^khudHOJ?9e
XeGVK)F<>A+O!?PXOt7UB_aE[.We!$`APAe&^9;aDC%a;,\QG0PHmJ9=\shtJ:8H5"kWW:L)fj;D_Ra]
6Ru**b3Jq6]RP+LA[_[Uj[GWNCVXRspJ7n5apVE30k5q\++4&9kb>J?X_k"j#3<*A^,V7?GVb*jjB4Q8
fbOY$h(_9<4`SlO8q6g\=(]Q14@ja%f<\?'5s/AT2(!'7)\]>FZ`YVb5(`)g)s>='3;_)a8LouVF&9T;
6W`Y<9T/e,T\qln.kK>V\,#V5D)(#lgr^K,)Y&D0;O7;@DT&J,C1kB*K4?8WMD>q9_>!]NK,p7'jmKsL
ZYOMJ2]a,d(XqG;m]PN^V;10<-C;?GLZ+D=/ai:;mOfgep/^QK.<cSlRY9I%\@^dAGDCl[En;d2e4]3j
G.G3uAJO=p@_i5ih6kAJ/pGq0r))lc&'lkTX\0"BXJMOi-:!^K?(DncjmLQSU>0&6>KU5A/pA=7m]K;H
Q?%1@$N%DGFUjbM9ADuh?Agk`H$h3Q(soj".cW^NCgNI?48D(bjK<P0jmIbNGcOU5S'0WqI0:m@=nfhq
mQr>LOh5CMG=q,1<lguW&X5rVY">1`<gFg#?SXK`aPA^Yg7-l'K=%<,FcLXUqrg8raPa5>jXu/e?1)U4
!Qs4]BW8W!h8<g2a^)O'E?sl<>VA9j]=V$/X/$]rmV]b&eh*Fb>FfeY=5(X)$RFeRf<Iu"?(@+NfVOBg
s,LCQnd;YFh;(V+b$F?+]&`:FkCR_uGO?p$FdpM1K.$EOlpkh4+a9@ImDrQ@V5YV^I7>W/pRkgSEq_;<
J$6=_Yrg"$R`Bf#</"Cu=PKJ-s+IlUn*f"<8$u$\WON`J;'qVge?Kn*`"OMS^Tb6=KlQ\Q$_QVP]m"k:
(l/7P%V.8"=r8kW`S"pkA<E[GhcH'$_PtW#3qZ(A^A'RDb$$cM17nct:toGa,i`s?[HT2tWa/>I:e]Z]
BeX*V&37._K2bnu0TthC0<o/LpJ.:6pfAj'XOSQZe**uYd4lPXSY)TmK+?&++&q7!RSsbH2M8_r]>_RD
\Zt/Y`@/7XoA)iRq5EjYD^8,:*\PGIWrLM\(gX[URh0%#(J`lNA<s\@[Knkbd3:%Vc9#WN/?.!i2f[f)
/H&I*:@qlgG2oVOAE>u652#i!]4`9\ZqasUq9U;(H[V/%^/>Y5dd8E31L>GFrF_>S[u[HrO/lFpN0?//
+LZl#/L/5)phkZIgVR^Yj='e@"3IlpiNtf.Q^L^sH>iL*@XU-*<el!c(ltZqb*VlC+4p;5:T;6=*3$ib
'1]eMUOs/t(A%\M=&4SIKm74'1I>PQ0(I-)RiHaXI(/WGphjTSSSMITPj^M".,Sn"OFeJ@42.Ls:/`-2
GW%$&h^pP\W:p1[I*?bl`_a50D,G)E5>I)7fA'I:EQ3FMnD,:?m;B&c<9%^\lpK?NEJ@6G/J*8^o4oNF
DD,gM,Ri)SfS,,DRBo+NY!a62A*iEk"d0%sHu/__,"E:*bNQPA$K!FK`cX%]Y+MS'/_TB2igNkPQ]QU(
JJEHl&:m1*/J.#I&RAYI%8hA)>dOFe;U9<m>00:eWrQEeR27K=]SLAq7@LVo<qDHrGW&3lHfSXuAnd+:
eW)JWn&T\\XO4/2kTTVVpYWB=NNQTi?12(pnDX"APiNa`/@JIJ+fZ`J@Sqg;]8^17V#`uVqg"L>>=<Cc
hG(cOT9o!^rMM4.SJC=%hQ:k1^e>7GbBF=f:.2Hgb*!d$h@M.Xj!(#R]fN?57$M%Ahs2\)ojZ0;Y?6;K
V0,2pBZ4*<]6-0Eml%,1(fM]:OVjd5b88,LG#:-Gc3ufQFe7&8*55XMnD,9tP4jWBaRZ9YaG5;u93Ea%
BjH`5R9HcEHJ(E<n8ob(a0!dF!5pf>C5S.LmR%72+$r(!+%!VS7@\)'<utj2I9eUoeCSLhB74Qsf6V(W
.")@dlpK4FDiLnd;Rki=a$JNkon4:En+=fG's.-FNtBcABohDH?&t<_NtS#r)jDe"]sUEe6hW:lepRgs
+g$UP5(Z/[6[80_36KhkHjCP.qTkBV][^N*#Yf\D]IM&`oL6G`YcC]M?`j.obC"Ueb#!RQ^PR`Rh?3Re
YPp%m2bO5A-D)JoWO[PrG72>,9)LI)BjG''L8eCRMIF3\Dh%K0MHeR#+Wst^nf\!+8e&5o`46m6+%"m#
OgV@&Sle1/GlA`,LCi>q>1`*YHjJrD32o$\KQoW7mLHB5D1X,Q`I#"I%j@lV0"T3_cp(u/$>$4!lbp#6
o+s2$^=o2D4F[-c\26d[Yrm&L4PJu]I,P".oFr[#iTkjW6$5guE]:5`/E;#*@EDeEs16usSFDZQ21:5J
(/_*j]7$Anj=HlS/nm6h,1D[deb<F6]<t8Zc5ptm/pF@##a2gM/KW$i)EYS,&T<ifeTX@JWY^i1lCfJ=
/Op-u5515_2/+*SGKZ:GQJ?$k[*.:253'j<:qpJ8j30''.Ci[VkN0=p=3H`<27QF:_UnSdqS02a._]EZ
D>Utn[m<C'h<I)7P!>Q9UI`e4*_Yo*L>8nK>q\X\Q<We.iY!i*`Z]&IXtqkpLlU'Ypgs<7cHLkqP.Q[b
G.B,`%_6r\?]h'841.[bq@)75:d/p/--]8D.,-^;!t+KJ:k-^HE_W9Qh0.j3,A`FrAW3b7h)_rfK\R:L
\Qdl#+t6@/TAkqVHN"q?ggrm]bMDG]\gq[1hf:Zq`&QuZ)<V;k_1NQ+e1+'_mP`ZoU2+gVLYQaLcQIF9
aReN>`r,d*,'D565=WDZ>CVNboilkdrVD9K,<$88rkA>BUi'ap-bg[WSC6$f\\Qs>PW?9o`)g18lH[06
ot8$5NoTi&Aa`X@QGOWj4?62o<IYH:hL;g&Si0\!INp&:iN.5YY8(OroCUJp(-OZ;Q:dC.,K<V&N9nmD
bIJ`O1iJ$]`a'ciZ(h+),t>XR`_f5pd4gTGa&p)mN9ADkG_&_'r$lTi0hsVsJHYXILlnoOT2l\8;%,&&
]Y/?BW.\B)ockl@6c>X9B<IN88?aOho=N)mXjSV]3L^o.S0iH5k_8G9YfA!VbG"stgsSH0mQ.K'Mcn\8
>3"9[Mh6F%p9sHsL7L]#!9-//H6K2qA),"Y?_Z8VhQ,D,S=@Ve4NdF^=b$k,[-4]TkhSua$K)e:bX^L"
C@kh5GhJ#F^4W99O^.J!h`*R&]Z^6@ZC>0O,75//l6nj2O>U.T+3Yn`[@WbT37b!J'2Pcdg4AO.AC!.L
BMrVU@B>G#oub]a>PXgOX&Eh6Xtit%?cqB2%pDQN<mEteBPk>sZ@GF"1ukp3pLYt>m+$m6)cF\RSWWI)
#@W!fB-EWdfu![/MfmF+8k&L1#11`!>isLIX:@$7+L?1p#N^4USc_&DRRN9Nj:,7=DrY]5jn)m6D_2i!
EV%7@Zhq`3]s1+([/*rTUe<fDSIB/5XG3,#_658q3,`/dN$chTc\)-n&B/+&RKhW=Gsa(qFoI[c)1$AG
?n9EHQp[`qs'=XVcLn8>Sp)Or'a,WLQ\qU[=%OLp)5j!?j.N9umt23L3`lTC;$MID4R-&a?XfVshC0fd
<Sgm"/Fmm-)t.6BY&D[/MC7tg$Z7"/#$X#$4ds!GRh13Y3*(^$"]CYL$YRJ52r,R:3,h6!hWouA1cgY(
>c./=;I;%r8FL4$RA-549pl+3Tf4[9[.9P!h=?X\bW&m!);Nj?!D_+K7<]6E8`atG]'mo]^Voq6HJcu1
-(o6>W-O>/i5HPoc23C#T(dE1\rjZc<b5lenRZ@omH>X36P5I,jg8(ZLjn#*2<)</1YK%Y##Yi&8*NLb
UTHUrp_?81M[.8phOoM8.OqDffe_"tqE?=1)r?AWO#s66;K7^P[)?gKgX00B$Fr0Y'q6t!pOcoXh:I*b
A>dRW+\Gr,%4^YDPOZi]&Z(]c2tCbEJoA$eeqO,Z+U01JX#P!F[;SYHp;iBjf.feU^XPgIhOoM8.OqDf
U>SaQ;B0HpZ7#iN*jNI.8`$_X-j7P=bY[IN-F;W+Ea8/ph+L7?,]R?l0XQQZ7<eH=AQXMh-s/$9i<n80
KtU"5JB\0fAPi`//.-rV4;3>62g3C"T7=o(U9IeP=BQ?"iEeR#EQX3SV097VM9)EA.->VrA\2Of-`nfh
2*o?Q6dL``CLDDb1F'T3X^VKW;rXBUO8DZG)LGj]>qXDkAu0ib^Q(tmLr&-mf>5\`NN8.bG3kTtKFW7#
bVrrG-hU=03EFft[19Zc-;iqhgWX;cgFZ_$WQa+Ydu>&c5.i;:k,ad8hJpcMSq"el,JSM'WB9[,IO.l3
nYUtC*lG!@cWHc=gfNC_/8Fip?:_\3)t1im4s!qA`=,LhG3J@BAj17/!jmWLSiu\#)-tL`!,lg:`bjaS
i5Rc7/\bJJ59XD4PV(&AbY!oYX=`hRZI]4]Tcq0Q0jpO&m$+!`G$ZTGG[G/2hk;091PJh,%WZi>n7(^H
`pDm%?$;!QQ$Uc'7%NQ?Fd]nf?F#(!8-cK-M[mm%'iZ*1-5?[hq^_LQh/%1l@NM7fG0+,_P/(VlWkRNR
NdFd/0U2K&&!)HJ`MF6EL1QH2QM#RfT9[K75$"^=/cAV8iLb/G/3-3qDo^)/m_<;VU;,=QcUtGd39Sm#
=K&*:<kt_Q'6[DGcjDJ0Fugm<-3]YIFGj'/,50.9be)/)St75PYd-S<*kB!ng&Qs'SPBVuMbEWLHQ9<A
0sX1a3]r'4l#fjKn`GL.GB,;jX*=17L1c@II-T_*Wa+kP,skbt,>1F4AVn4O.W!3Xm[:$CX00@-m@H&h
g/BH?gNeO_r=2>+lQ31Wm@??aVV&4CGZ6W.7!KD#ZU=poQAE>t&qOC:QN`*bHGX8=Z:'6>(XHp,=1ZF]
QXfLN`0iuY#bt"UobfT&/d6kpO<ro.5@06ke*:d=[2:[N`)>)Doone7G,!3;aPFdd]U<n7p*D">Jp,=2
a`b'ElFlO!:=f+[M_Z9'fgSQFSpRl!@$&GBna-;5lR?/04/mfP-M!:'A?>9Ei%FBB^LMcmn+7=orI+Q9
:N9u(@6/L[%UR/@<\9CJ^i!u#:1C8aB\oSCLepII$a*F;IgbR?<7]qUX:J@j('ruAk@!lnq(K!QNq[^B
\GWBJdG`q,emJ%J2L/.@j7<VA<X#Sa]'dl^+YYE1g4#SPqF.#SJ]'K8gS.ZUo$2/fhCl3&*5r2;.E24O
U7C)PjR;]D0'FtW&M?4:J%$oWp^"@'qh87:haldUa$*h18D-jclVM,ZP.E%c]PV@)g^qBg'f?qA<e3Co
>Lm)6_YWF)<LKs-B4:+8IL<+\7.c32'NWm]X`eMbig5q<K&XHYWme^t#JN!?,fK\2r=R]&;cuuc(Z)`%
,i!?ah,%RabQhKX;5>Y](A^>^gAReh>25FYFI_k9h%hWEHL.btn)Z4GGeUY/^6`3W%&l`\?X%jO0n5eH
_lA7?q0LHa%rc.s0uI%2^>aCRE^n]jR'OY,Dq>>r3-$cj^6OT*+,5koL"26@^2RsHa(`LU7/V'J,DpcV
X4P=sHmr;^Q*"(]Yd38_US47B_58OGQO*q=cal,ZC=ce(b8JK1;$VHh<EE<@Ytile`b:1=2C`Sm)+K[L
=27jF-CU0A3n;(6@@MthWgDk%YIN<KW$OYo&^ELG8^:21\LUpBPDMe#Dl`eGla:.ZEXJD;W!IN*Wj2i.
\&.b=+q:YD?7YU>>MWD%T?1'%,(@LAF%$9G6]hZ[33t(lSgJY^k-hNN[DkA9IAcm%AY68/F[_r!3,;^r
pD%$_q-'o1&J)G]nf`KS45K/<]It"Dq69kqNN2/iacr0^1c/A*6FKgnMt?-[KU!:!m3gtDP@jaF-JKBs
\Nj_[YI\h*Za/_@KtheP3C,SZf+@S;hf?)7n=,q-8TF=*fq&F?\d<Sp\%sS")l\L%C5<oTH\gu$X*9Y<
Gr(J=5Y1NRV\NbO-5.eYhO/%=]c3dT\[58XReD3O(S5dr<$C>fp]59c.:m1sFXa4^,/ng/P^Oi8k)I3D
2Ck?8D1e8L*%S'rg8sJ8G3c)=B$[q5S2n3>\RUpNk6)^i#H?QfRq"mn9!@Pg,!50:qEg4-*;DU7oV2k^
r=9l/V_+MIiI,-nK(QIa<6)/W:umXN`1+;Y#3G'i#7Wof&>%*%K.VuP+LCKd6Tpp"GeYru%V-`Ka)c^E
P[t(6E6IcJ,aE_0JYA*XA%^G:+QEoX_H\QRH-[u9V9tIC.:jW]:mejSs(/)JepBQtQ[]a:(o'U$<NZ83
hKt1l>.#UN^%h<a`e0\C)YIe%:2-&^dABj>-I*N>)HrF@l7dT/qfq,Q.1%rZfkPQGP;PpAjo<D174C##
OCoHa`07`7$K'V"_l*.Wl(g!?bqbb[Ta!f;')2o@06PiG&+.eBBZ,$hT=!1JQhkgkZ@&bLBJ#c^\&)(R
Ad#p%pajQUTT>)qf;)j$J[h.o$FdNHBftR3%[bJ.dO;Y]<sHYFJ0ZbG:HmGoF&(O'l"um0>B-Qjd^VRi
U./?4BZTjIGqQD5U@S5,ndhQD4JC1V5K)8mnVhd5(#\G*TpfNbg\>'CMNo&@68$XrSeS'\8E[&6[!#en
M=CZmH]LT5g-Ck"YGm:P'1^[9UV6QN9N/i1BB=u;0]();0:DN9@AVUJSZ$V3-KHObR:Fr5j'+%R0H^eM
#an%/>&dulhNUsgPHJ!LTMA,I;d[*cVol[hi3V,F'Xi1"(Z`:[@?GC!TlMIElu7qdV<W!#@?&92]3@J?
?:6iYE'<G>AJ_U@DUsi*3FEc?)&Z!aoa^ZP(qq6ZXL>`S57XD7$aDe,[;'tB]$>2)+R&L"iP?P8[hb;M
SLElPXP%/_/Y5<9;K2n%oZll%W3#FCaXoZrHg?cI1(#?$n=%sGa"$2=modOj-&pJND$Hj'FJqR@5#KE1
g%OA=XNVRXi,>2_e]XEW\M:M-T\]I[41'V$U<0)D#""];<g_!QaN@%qGR14)l_<s'!?S?t#E/8OgUa`n
_hhYL9]XZEX^D1SrR)%5>q8U5&TOp,$b;d2@2A#=C'B?J\lP3(jiB56qgN)O=S"Wp"YXA)eDbAb[DH%Q
pTnKX=uisA:hU$Qe-`390!-gnF-(GBCg%A!$9W$ET!9f2p#7B+4-.rZ.8$^MiN;4i$57DEXqdHoW"gBO
6]PUbn'DEJp]s\*]_EPuZiYl,O&hX%&t3!4k"A<U"RVA?PI@d?9\/?k<Z%>&`3fYf-Fm.oS$\hh(aq"Y
[)/U<4JD.[Q"ItO0aEd/GMo]kp&$oFkH?fg#5`]kn9Ld3Ds`"k44a$'m1kkh5,,j:l/R@S*cdUW]$sQ'
K11!'EG<QM0_`mNVYb_to%;:C^-%dGA"dS["0Z3AnoVbqadV)@lF"U_YqHngEa;^J?C]ca>W[=1Cu*.G
jN53",WiQd0hJ[W9:a@3_sOlc*P3TM3BU]'B+W+tgpH*/P-"%kB(6;nBHq&U\a@Qt.:Lbaj$]4ZCN.&P
P/M+6f8q8Yqeib'5Tr]=;j+4WCQ=Qg%JNF_2OegT9;DKk0C8o1[P+%9a'1L^@pG2``*KlG&u4Ej0:bHY
8KRg$V//ug[P&(3qf4(\'duY^r)o0L8UGhbrnd'o1Kh!6ASprGW#Y,=a[\lnZK>^!MDE5d>k0IFP),l#
P!&Te2)8mT:T`&.l#U9dn`Cagdkf:j@cX,/C]2WCZA2Z.S,$`$\`RhRZ_Q+>[^WR!,`[];qTT:fZR6,9
k&bUb5%lHfWGkCZ?(I0$32DB1rZ+e(/7OSF\[,hK1Rm"a5,R?8Lo0MYAi;,I+b;Q=ilO@=A$"6-gM0.t
oBPj>S"At-GV5s5Tl%d.ofu6tT)&8I;n_AY<9Gu7)j,cmDI<c@AL.Jed/1`Dhm#EGmkWamM@P5h=Vk7d
nb*PElZ**K]#.[Q=k/,7F]8hagr%*#)"j$YFr3,OM^UTkB9V013NZ5^aPMM"#1s8<VZlL6,KZi5BJVo[
mXqf_mlR\rg;.,+AHUn<bq-$S#Au\d%>6k_Sb;L)hpK,d+%P@n*%<fq:5@5FCo#MCW\^-pRQs0cG;RX^
+ZpTa&l0$u<Gt2`;g`4Gm9@2dKS5;T4ml"1M:!l5B'R*.6M?2`3qQb>Xhh,c"C+E-EGjZ12\d2YYgL)0
Tf6t#^(L@-kumtuI4G,E^=lShPWe;aSAkI?M;J3<Ehlu@lbn"boAPKU#IFAbA"ejQr2mQcm?:u!]Q)(T
cGJIi#IMg$4TCH?H+\-F7f$`0EITmehgqsi/BuoOqt&DY:ER&ZjaAC<[BfW+421Pneu(F.WgZ1X^"O=g
-p99eGVcARf$E\57hq&D?V8g)k*?W;*(ps[Dq-]4NpuD^\mk!pejQln<T.*OCf^dM?_=8N=I+3?W.OQf
/2]m*P_no=<i+?9"tS9i44<3*Cdl$6oh[/-rJu)38(q$[eQ7O+>*CRC[=@f2nid+0'UtA7M5ob$&r-E2
I[NUmJi9\b4Bk%<oGd@TeF/=KJP)9qgtTkMYLY-tX+a(s\p`$]79sCMK18#TRt"D,H@@W^]bYW&E=Rk=
THotH4QA`t`10PFnGAnYkrs\2fqV1:**92aS'hfp7eS')L+/aILZ()t%egqhO8c1<2>$uF-%?DbF_XER
ItA&<"9PV^-u/K!e9_da[f!]WS/hsd(ulg_Nppj"[FeYNXdVFQo,?3o<*?_emTDEN'k#l/.UM.?pl$SK
No0WQ0g>Vt8)iAposV\43I978FUWWQ[o:%Njn)9\JU?n&V:Du0psP1BYL]j8O`EhX=j0$/S^Xf$1kQ(A
Hd49ifY![&'oA3:M.;4@4sbLGRE-@&TDsTaH0W8J\=4b[==lGCoq;_mDDfYVep]0/<a<L2Fl)9Ul;u3"
Sj,1VN.19\d>?L&BoY:7OCI/-XaZAc>;E8qJ%"7pY=j9@^_b%-n\c):`]-3Gj54"^i6VBWXK!/=9:@a&
pcEG=XQSKG5QC1'Ab.R)$jM`j9(mH+*OmEGWE$q&?at+mKBL:I@-EC3q00K'<iA@b$ZEdcc2PFen#p_\
7t#VO)q]u5i7,%1U8-"\_]B=*'nEL]M&tL!n,&%!V"'GJn$**"ot_S*QTmDMr1_<R2f*D/Q!olRkb$C=
9-^LHZ=6N<P<NZf'k,1h5bq7G4'&gHg`NYfaB$)ANT@O4T?ZjL<R?M]*\liETa@NrH,@b;%GuZRdu,!R
hR%!rg8Y3(annJ@6I)($T4i1#gQBK:YLXt`&s:2VokjWqI=*2Ym@]IRlCQU((^H5ob=[)MS:@6l'qdE'
b)e%:B@`(aW8UFGZ-B>WkulXZG#3baqQlukC3PS'.dLR$r'Q;nRc94rcbVIec8F2d;#s9TnV&W=EC"/#
jr"12Sh9`C[q<iZ+\fq4`AIT`):Yu$*j4!!6FbtLp""VqO\+iR$1`.VXK)*)b(L_I]R_T>OA=:-Dj=R=
DJSs1?_=8XmM)$W1BD78X>]U'?(JQ1PGj2+l6("EZbnJsNSdd=gT<3.Gp@$Bi>BmC:Uqel;Ff'Jj.`&4
L'M[slmI:Bgo5H:5gJ]iR,I\XM!O"tbF3bKn16L/cg&2G7&3PS<Aqs5Kkj%3#\X!TI`]Li-@\VPm?TX"
)<"1`\[u;t3+>&NNX'hZ0^DXUU-'8ZSaoH;VcL[Nq[9s[JNm^`8PMcGojbPP'0_qsh`,g#N82L=]#[i4
Vqs">DRg`9`PBFpccUS[1jHJZn/;8QH@4_FT:UPMV#U2DHmgMO=d#+'YkGoUBK@9&<b-/$O*7j5J)Nb_
rME/?PM8gQW!^csS4!nH`ApY238V^T^Z3KqYUkbS,EQc-o"ctI%F\It1iX7QXWkZIb#i/>d0-dS*_?2T
4Lm!!>N6Q[mj4TP#0Q$)2V>THYG$I5dW3PlH!6-u0QWd_##<p@I!HK$_k)F"U5(<@KNgoQIp?)6k"R4#
Y%.)0L&<s`C$fh`a)8pP+0=FEolW"@&TVMNZWA4pANqZhVV;G3PWt1f@.^JHC6VaP)H"=PVD86,oa)7?
]1a(%+F*GK/9+SR$I5mM2UGp:Rkp9V.OF>jWf3\,d7%d,9-%9cNDYEZk"sa27(!YQ:L2;?hRf_`C9bnR
>#h0Im.SI^S6kQ4.VMp$9-)el-p':ik+sh$:!9VRNjEa)2mKG*EO01R4Y+Zu+&4cZ*mO'!90V[EQ[Gmk
>2?(5bbrUgAW=*3FAr!NBj3:snAWFFgO4Pk9>kghE\:/5(3W?h0eF""Z#iVaB:kBignnsrTosB/C_'OA
5F.+:Y,nNY`LIX:p2MXGS@6FnqS$[#"-GYsd#+pF\,Kjff\b?SL-P]uh%Hu2PPV,ISn7"$hYO?bB_K+P
,]$5BX^U7Nq%p1(e`ua%b#QhH.g'J8N"<>E:2:Y,Q3d(]?<SJTK9Zs'Zp:3dq*'BZXaos'.3#'Fct`Pr
#qb)MPSV>=]0'eY]\E]g`m9bDdD+$0fe!oF,0sq9JW_;/U]lYE+$Fg[3EeZrZp8#5JX]EWXB?hEAn9K*
GN8[WG8seZ\9;Xh?_mo_NmIj\AN@+_hFI!d,5p'oH/:S+:Hb15koj8LMUFk?id6ILYK&r3"V+:B]fBS9
>tGE;ISZ9\HF,q\N?Kkr\qCh)/1htq/YG+g(+;b?^U-6t=ppFsU71.jhIf&#/fRmXnsrVU#TC8iaLIf)
>/3V>6OOarQPOQ,>/5TVNYW1;U]p=R>q.EK9')Gn>:/\]"0So/[:H[3$^@`t8IH6!^!Hi?<,$oQO-O(9
gkX8ET7]"N`HQbu86T0&kn9)[%iUc`\nMAk]X;i!0"20bID"1F!c785>p4n#If0k'OQVPnm:tgtdoVIA
3gkP!4^-!Pm/%OZm4Z1Pb0%8`Y?u1h)Lq5,NeQ$L8O<%5VQVD>jY\NE'[O)G](4Rh7aMf!"B8J54\)^F
V4KQRi:?=3JK_:T7.GT2JrdP,*<tEplr=dXAQoC760"O_T=XkQVGk.4LWmcAVg6SKDc4g`I7G0SJGCbe
flI@D<hH$&Lbo5$C>GW-dX880c<XYcI:"h/YK[[M;gfYrLQCp$r"];u0t0ekCV.8dDQ_VO0/OA@htp2k
ELXF\nX%^h4HMst-I1];BX5GtDhS\A]8(r`YK7+rT6BsLD4`?$S8kYlQQfA:A.=R6A^"%f.u+UCcP3j3
QZCs21+F*RMa&^3G#Z]s&'n`K7#"s,R""+Kkr6%%_=LGVrQ)NmBf=>OG.0Z"=kVs=e>o_sR8%q;oqC0S
/eckU[RhVLn&X*&b!kWFA7jtH5;KRg*^?r?pETRaoHP;!=/C>Zj*kOVp#>A<euHBrYp]HY-JlphW86hL
r3AJ0fX2c-2\8.D,9PuqAB.CWc?F0:TD,o=fk5qdAF!)R4BCWDbLL`>^30<+)j.L#^1g,&Z?$<Ma:J94
bh]Q\kXTf$KAtTR-j)4&].#-\4$u,Nfbk\oBXp/\HFA"]^ci8V\f]TS6U*=^?b7E53P*0J])&Tfic`FO
9q=0pDmpnC1mLU\@#h/phdLZ9?IhsE5"f(JgppU:LC&7SRd4g%;>0qnA7pf@r]$jgNc/ibP?+9e1#\3,
c-I5u3-Jf6al5Z/EV3#rWO:o,9Oj6%C]0QVU.t,fo2@D_m11HK>5VO:8Q<HJq9?U9P0E[urO]D`s7nC:
?)-?(s70\>s48?N&Yc%259Z`69)K]S#lF5^fUr1dCAL/uO1f+<7c'\5E/j;Ki(^n"3:T,>lRoP>B,C%(
nl]"LYGb<73:fmoAh.ca,-Fj8\)[3s7H#6!M)WF]S\mC3'-2XLlBCWhO.,uiZd?@&2iihh*JSkDlFbrc
'%$87Ut8:E@YN%\r#5pP)iui$&7`^;RTdeCRu'WD:Yt3ip*O<p2k%,Te3.I6[WaUk7n.M)6Sfb4Q*\Lc
U?XY?$]g/RaNBfc8Gum8*MgICYYUppS-R4La#`s/mG=jIpQMN%Fah92DnO]9=tMkV&FQArn,r.&g9\X`
q6MT[.#'=CYQ`:Us+teSf`0$i:b$EqT-g1!L'71!.Vfk'X.pF(onV^2^g3DFGj:2>__W*&p9:?F@!1sA
iQSps`bA+=J2pq$XfDu%(tgC93G<5p2fg$6Ekq7I'"\2UAg59lH,GW3Qobkn>"P(?R0tM/Jf#h2Qs>Qs
#URg0:joI=(Y+0Uee*E-qPUI_=Z*D&iJ#+h'p[C+m?fs+,:l?ZpGf:=lQ2W/XEu,*Ts*$@/ku8DAQ=-!
(Z*b5#dKA;1'>-BY*JP^Hej1'/+'DG@l<5\b\1RXg]lB(kB(ToP\#T-L8t`E#0a>8PG9dA(Jj_22LD,@
P;s(4lSKhnaQAN?(?TP=-5<OO`f6"IW\QYrnJ'::)eO\jms+gP=(jm/8[6:,AWA2o$[NoU%Z\;dA"!;"
+jlo*j`FRn1;4:A)R\R&!T`MK!aYa/%C]d2;p6?s6U0b#[u4\a(e#"bJo&re0le*ITY^QGSS]?47j?,B
;/M^%2Gq*5i*Q#lNEMN@47U;t1+lL7,gWJA$%hO)Q-W4O,2NFlmWY(s'`ke*4hR+d-s`d$HKTV[1NmuF
HKN+d,eqTXWDaj[qH!$-ab`0G],fOLa5?BHV^a1f::9&#op,9247:W/hK\!-X)rr2\<"uQ^%.Mm9feH3
7k5Qlk_8S>bY(;WCBUTVN"SLadVkqohSPi59B<\H)'sTMl$ID>AKLLGH_OQtaN'iuTo7@7h`%_8`eCre
]!(P%]E$IR]K+CZ\W""u47ZH>C,#\`/<6)(!#d+<H`p19?@]D;.%NG_hI0-@VLo.qQ!MOt,Yg"?;o3CA
Nms_(p\dGs_m+C\SLtf,e>L5t27)KM=+.9B8XB`Aefthhmd8rGX;"Las2Z1b37c=^#+7V\l/Tn+bb'i.
`+W#mI5I;)->tI!B*F8b:j)R3p>/.]ne-USF]<iDp%p/%H"r%K9OC4b0P?\LD'8idCcVc^mN*;$;<*Z\
E3-]H:%OC/mSirQn(=K55hT+-FU#m0NZta#P15_Q"*H_2Wq5r!c]QVP?8FPjq']G0""c[*orPobf3qPf
54SI>I$'KcqpoWC6jgaTH=?4e`ZWT6fPHf[JVe.qPQWkH`'C>C`[>o%ni/;3.[B%>Db%G`gt$2)m>\_)
Rg"iB*mZCNfU@(=\Pc>%aFHF@9'kl)^m":*Ga`s7FuqLBQ7s3H*H?kl>$f8+*h2akl,e;J8%+FLl(chG
)+."G>Y.PSAf@4g5T^Tdgd"_JTAS'D>;HIgRE&VqJ3nR+Y&hohbUXUi\;Ao&a)2a;AW8ZcF8*K$RJ^4:
\l1-%F([KB+eXES.dr2Nr8S[(-`&XMQcqkn;k;oR4#5ZTf7#7U`@lLg>Pp+/D$7>4*fCY.Xj\T9,_B!U
]_mCD"D5gr87h+1r"tr`Wce1%5M*uR*/':N#opD16Cuq=32DH3rZ/Vb[g^jKDe)/,>qUOiQiEBJT#GhX
hp4mKQs;Sj`IhO-V*9sSB>NikGlnsBaAh;_=h1DakB,qC?^W0`e0K#RA(qX2A6)`\YOeQ>Ps":o'%a+t
cBCF1[8,m]jT(Wj>b:Umg*AEEqP)f,$jlZUOXHWH?6f5*R'hQK\/FuKqq.oIJg4BOdYGBc_3;5UkN(iS
A^<pu>-5tlre5X@qn1gu\9dgJoiXEj$ArcR`0Q&$Ip&/*616Csog<(t#!MF;1=XWcc$]I%UJ\']WaCVn
nQo'a?PNapURSu)n>P^_j4\mBNHmgPKi.XB?s)+)%6K%!0#-^)3,n_Jj*C[]44a0*)js3/\U1]fd\B;I
\`3lg@IF+n]%_5tq%o&41!ef3gq'LU<:4ULE=B25i3N.WrkrHfm/K/]7T`Mti/H$7#d,NV4,k*@o*`ST
ls`]gNT5<ahE*na10^!LbZpm&LW8!2lYF_*dqMT9Ang]>/Ma+Q@AoZJYAjg$'KNI/2q,EK=[tX]Df$nd
Kf=.IX>@SiI<"F1<T_$*`0j`#/X\*4;cWjoD.-)M[hl5)a0/\2/+&cP&9=bT^I6eFQ.*m<G-3IFTiW<T
WPigR:;P6"gnJ^g6s&;=M[rUILGF(mM=[iq;iN<1qegcPfu\Z(=\$1$:<0/ONu`_%pJJ%%3F*%n;DbWh
oEq&]LM,e0rgh0;BBt],TssN%Q9G5Qi>/o;Qg9?HZcRXJ6*Wq'#1B9]6l]DOV\OpF&Egj_AW?/8rKL@1
kX"[WI$tTFp8U;\EbMD3dcTddK68m3:H]P]fS"5r??]J84\Cjl8TmejaeA'gW@3XG:fs.&b]E*b**<aN
hSFGjpaW>6H&n<rdJM\Sm^gHZXDJ6)4&X+KiDR_R%ZGic)NpG>b<#8_^85su!b5fGggHclTs3m)rK:Jd
J+(rP5[S)S8+4h5obo&#3[5pL51l^)`qtcLWQ.oFMHSo@WXQqPV^Z%0o3,.gd:IfF`OAQDaHL<)5`EXC
X\/CF<B#_sTWrBrmF8_m4Rps\<=%mt:/T(O>4K3cU4kPsBuOM%r)+.QhV0G)oMrE^UIRYu?fHg':B&Fk
!tp+PT``'p<+:oYJiA+mDMhrm8II0da."B+'pi=Ak^(jF'pL]\W(_i%muTdZcl%CRnTm0_FA!k8gT*Gj
qJ(dag3Y(OknJ@;3AWZnXn8=_0=``qpPqs0J7$j@<'>9_r>!msoLg8Thg=S&Y<%s$,H'Ho?Vl,]C1l`"
`P&WjB]2j3bT8,/RTfo""cq]tB^e"/Tt%<dMSF7Nisn4sEM45MF/no3";e,R_;bom_e:Wd&,Kk6`esLg
,Nu0]1fj7iOnVpZ4K`%Ka7\;^K>&fbWCK_"S%]Dj4-)%2U4%bu3QGcN2;43W\#]=k:e-2N13&#p)1aJ%
9"u(Ll+OokqPlMT4VAPm`LY&W06SDo[%5.qMc(hO%ZX%hAap2T=.N0,#dLR8rmH(^ml`eI_q+,qhBSrV
9j^eGEkWH1[3P:rE;U_91kD6Ja8Z!d1G]a.L&O!N9@goH/d2W(7:es,fm3I4["Xf$S8S3&):*01TCrW[
fgngRgg_(*G!E*_>fh'(?!NG&YFVuoe]R/<igc,Ag>7V#Kc.jqpo<8cM7*gU^fsu]_r(DZP?`l::G?Yk
'5i:CPLX,X@CEZ-qR^mrW.dc4;JB#q0M5aac2qHKOJuu?HtArp?14/g.Q9M5*J\dYJ45l24B'k[I]R!:
o&8_:WTfr)\)`BL$dPJ-X!$b&\^\!ifpQ@%H]q[iFsb(RDM(As^]"LtFn#pRZo?p4.lF`f9,lE[Y`$Rt
J*@QjNn1W6c$+:Nch4be5SL9P0&TGl4o&/I(Tq&MX`j<D](_2=dnmRD9qZ0.`\j?`p$=MOe7FFV!e2TC
l8nQ<XH8E21:_?E+ho@KK"2"7/\4M+9u0Jul!S9B`@4NG8Pgf=rRY0kZHaet..M\_H*4_1Ns=npqO41\
fp00NpQ]YBcAu!/>f1_4VbQG_mK-WC0VeI3glEk=h(<!Jrj(sS@Z+Q6RJ6J^>Nkn_s7'HG0ECCus*i/R
:pSoU6?PWMi69AR8qk%[pt+grZ^h'RM2kr9@&Co(RG'X8;#B$-b[u6'p2Bu#_>M&#Pk;H6@J-QEZ:m>A
T#koM=Xh<S_4+RXrY>H9M]"Wu>'?BCc5*mKq$Z<&a$&^0gA`%4_QBgS2rs)U5[9DcL(_b2MSA,Bk[XJ@
q%&Y4rckljeKFS30A9HKdmAFN[/[Jre8[>:o0t[A.m:/t$+nNkRV0E7HeX!R^hTg>p4!8eP\04)[W%X0
#QKj+PauMmIDhQh`T=j:Wd\UrA]JNbB@%gRAb"N5Vie19DV=Fs#_+ooVm*I2=sW_nI&(SYmrI.QHJNV1
=h'K^AfduL%KA[RrT'Tg:N]UM+9On+NVd>,i;^R]RmmEqFO=f__J#I4_drT>TYha/AGSL)W60B[3^ij0
K+=iIfK:DICK2(L)seF-L)/-],&bt5X@8Oa$Z<]!0>+tUB(u'rdm<GOD(qSHrR:fH2'$L4=nup6?%bT"
N/>fHN>d'M!umeU-\hXU8g;I=R#,t&?5PV#XuE#%_*Roni>;)TFG:qdSi$VRNVtZaNLjf^:KUn1L&j*U
c/4&uXDb9W&FDG_eQWh^55`>TWKO1=X'8T<oaPj>B@%8oK^th5n3p!"_J/h_aA_H4MDp?Wb!G`oV8(Pp
l-D>kiiAfMJ)gPC/O'JS<o/<%VuQd@q_Xg_EW(ok]8Tas<0k;j`6?B5Z=uO`WP!PgX3YsmBD9`F_c0T4
WA^51Nr,uN2ql,eJ)O6;oD/A9hGB-e0H"esEG"rn^/2nIV?"k*jmD+h]bP_tJ!bHJ[H#Sh!]sc6n;o0\
?TRTS$sI-To-M3bq!\rAXPc,;VgIPt)lMqVU)UuKMs]2RR30`/rulKgH^]ede4kG1ZR=<mQECW9\l/P7
?"l2!XjVgU`V1=uDB[F--[YrEO%ln!TVm&h&'eX\$Xoe4m`<ZM$-!E5'S612@7C"U(Z*GFl@0-]EbWTO
PAps!'0,5g+?[.#2h"n7,KV=Ms4CVoUrjHr'-Jn$U?ED]RG4fDL7P5gq`f^.*j*/6l?<N[7(W:Hr\JXD
dpqh5i0b26qF0P<<[jQ6rBEW(`u)fc1f&IX!L5;SCJtUj477O'LO/q?a7FeOCNjB\_)/K(C_nJS;l8q;
1Qm^k`1j7L(GH;$=ceSX7"u9!A@V25.q=obp1hYUV^7ih$b`-[1"P!T'q7b,l1L;u9o0`SAd?5UUJFp>
-/mRR+Y2o(&1Rkf"pZ!F)lW;?YDsM>rp3Cq<k.5@LK8qA':Nk_gWbJRGS\g[%9&&:fFtJ(bfQ9fXXHo-
EB_rP@h!RUBj&5P/h;RLH:"RJKhs3C0Q&lhb4\Jo29ABi@q\WbHW_Fnp)F_S,`73Mld-UC="=mm"#V4/
eW+9tZ&ZRXTbs=D.o_iH")<8:Ul2IW!T<2PCrLu=UpHbsg#-O?90;?uZ\[fg-IC*L7/HWuXVjV`L,)9U
i[^`r@irduM*7s3T[%:O(UeA2:/U*12?_n*b`<;6Ss[Pergk(MAh#D3Z8#\>E$`N\eAg2uH(l=jY_Zd-
m!iTZU#EA/diuaIp_'W"..c6f_MRa+>`mrgkWW5*H\gN3]@3u!i(CLt@E;(),T*.?V"]eh]JUobb)eW"
1/>4cU!e^l!q68F#?u$s5NI`n2pI<2)AkO^cEmh1B-(=T335#;\WR;4TT^mFp1\2Z?!5!@8#i[(G]93r
Y"@7Hp>4"OXZB_2#!Wj0mWj$dC!-u6k/BN`'etF#4^4Vi,EC<ipYqbO<qgC$$R-(co(,.",ohOIhf!Tl
3Bii:!&=hEF1bTi+l;^K#PVl^S&LLTAo1/j8\--Y>cXBi"A^4l_.[e`M4m$bfocSOY$*pOL&A31P%5?[
>N7bPdGBh=`N8]ai8<M"KsP^',XZs21H=-c?>Cm3Y.qhSXfoTK0$^o*]`h\l^MaMkDID7F_]3]N=eW*Z
p^jO9Xr5O7<E>nG01p0<:bXrTm-"hI?LhfaL]h1J9DA1"4HnK?OjbhQ`N!SkCM)@QYcTY@^pU'Zf-B>u
edPSBf@NYP@')?3h'TQCA/Y5!<]sRFa93GtNReo2&nSp&2c#I(X/+h?nV`.IWJH1UnhFZA6/6b?\<c`-
nE-:p=ceL!*J7P";Y^0.gfZ9T0EeF"BFf,h_>TssFP#-K2"5+XFHi%S/!FgKZg[j[oE>AX'r^9t%r)8_
eq.uIL([@\L-sd[C!!+qKoEb*I[4/\e/@ok36#;Sc^sp++.iLl-Vobpo)[O^bQm&raIX=J3E^0;S5iCJ
N9JoJG4AX]cdn>5ms/bRaI]Mu6Y`am]qiT`eV/(<];^OSCqkt\19cX5^f77Y+,#%h.MgSs4quuImhe2*
_O\3:B@=.<'ABrk:kK!_.]tEP0>[E51>8O>NNnlKeZ+mNAMKTJ"K:P;d6fE?(Po8*c54jSZY07kUTH^3
J)Xu61BF.Qr_+HsoA)0EPL"3+ea<UBAVI&sEH5SQg!n2]Xo#R)l_7,8Q9VRX\`I07kCAlu6+$P1e+0^.
[f"ARH!C8jh3NQ@hLq1Z;gBJ.^SRjih_KC.;5+uDV<-SA/Pk!_Xko@.F$@+qIh6H.a0bc4=OSjO-AspN
5?$7H*,55Wr%5P)Xe"c)rRJL7[VsEKN?69Xas(2!UqKO4#uH)7R[[?dA%2B(nAR!Z3b`BVnn/T(A,02/
bI%2\LU=#FNa9I8rVMIQ/$HkAb`TC0MWdr&h"#B;P(o:;,IdR]mOY0JBK[-*62a)0cU:WJq6rb[7E`I6
Vr_:[e+0]J$(;Yq?*``tBXNiCMIA[g\,:#D0teUgQN+cm`C%0[)[eJe/:T!dE6I.igp-c/=^/QbOH&[C
bq96:ociqHnu#H+bMOR7%Enbp?_HL"aGH/b/`/JR7c<`fPpmrLJ&cm7ou<#&O7o3+2`bi&,I9*eYh#?/
e/+S/m?u5O.qRA[]'5JA#2C/i]huZj0o`_@I89kBkem.iBH<<q\A(+n-/mJbI8ZYEP<Jf;f&tG*?aQP,
RkaE.=$N9GKGWJjRpFpF2,TS6e7GsfoEis:;W-+*2D,lp)u372`V0GRKliTOp"6USF"8Qk'D2]*fK-%l
43%n>pgp/sk#W1,I4&<>*"IG>R#QOd@.1hI,`hY=gSqB==6`j;]%r_-(YjX'%5Xt/%Oif%qqrRcBTLt_
:>i'B^B8Kqi_*Nt<c#AYN\7Z\)(7-+=@8]>D&9PP]Cmbh([-jKWRg?\D_b^pB()uQ_s`u*0coa&_$"Mr
qtp';J#j6_66EY-SS**=ZJ)nf7f0c@OTWRP0AK*L`[](Gi/o)JM>Q8IGVLbQV0;Y:\;Ec_01=7c#KNUg
0GK;Tg@,#N7'j/=mQm5*('Qi,ZVZh6K.r>h!11sSh&JQ`;g&R4#`d\mi0YR=R5s$o+ACI@mQm5ZrDRsL
;GpA)c0FkM.qi\ce_2TsfKBMHF+<EqKW0)e&&K<n-g3j8jOT&qpE2T^7*946l5g)K5'R!\GO-B(.A!CA
@<T+P5kj\o5nAS'f-4Rh[5KNu+d=[eEjtB*B,9YRZlZtO"-U[%#@S[#JDmhtg:!>hj;nHEoH6hRcK.?"
?BCq/B;X1R#@SDAQQCh5gpX!`_<Kr#KS/MTE(geZ*1UG`&>jW!mQn(P"kpl&GFGe#1V#2,/l6:"h=bP7
mO.JiZ#"B*&>m+eoH;BE<]F,Ki`P#h5^Y["'?<B+6't2Ej;rtW]T"se(148B!@H$"Qcuf4/qe7lhRaf'
9CCb)R)=-"=sYtNCbuj(dY%&IA&rMf"&GNp.K^VrP.h?&/#6oh!4@3Dh!qT8hG(>5f60;fIGDs=>TCRL
MYLj:pBYpch^6G`hO2.f]OmHg=L;EA<1#`rYcg1$T@!Q?=7+?Tj+(P.-AT:hAn^A#78oh,n('1efn2$q
$QX=5/aC(7-4[A1&oTRWCI!e8fa"%4)4EZP0tY4uPr7RRa0XVJbmN.CnZ>nj2SOllPWlh3>p"*79/;^k
ZeK3[N1eSHM=KQ]9!lSH@=6%>9]IY;MUe//NLT9U']U]]6"rH#]tEC#ebK&(55p79HlcUP)Em+!g[)=q
p%0mg,R&/e_J@*1PAb-:MgngC]akLEhY:XeC0]rWA\PB"R$XLY1T30Aqk_>Z_[>1_dM5"C$(S>4i^Trn
GC1gSkMNOl5K'&D/?p3W)PbA[me\+q2D08:780e:e6+5+/,8FkQ1@A+Q&nj><iC^--ZMf"Z^#le=Kj;5
[UN/IW3Xus?t?s/?XbZ,n"#]XqC4%6CX'65TFKH/UFS>Cf9I>TCY:<>M<ePIaYgG(OP9Rd?T*Op)bl=M
k?s[LkTe]'9'G:B8s"piZSU9IkoPg,`E=iaTQ=S$@/5\M7-/a`gtFFAX3kSP.$,hu(b)`LQUiP>n&8]^
.5P\_RC;i2J]4D!ZI`PBC=PlFbFjLE)cX[UK7`=7PJ3hPap>B+;S[0[-$e8NX`r`c,'s5/a7HJnqWX5f
`.>;u?5\#@*dGYaGhI?uOc:q/SptKF]Q0j0!BXaX4*JQcc6081n_tES[!(Up]6KMEA[6KtLA_P*qB3ns
T_Z7agX;*AY]dtT1j6g\:[V82kLpMP037Fq%Fs)C>Q<mJ27(]\H!6,uE4o5/dB$V$Jh&56II)b!Q1gml
E?OY)"(Oi[?cJ,YhHR0(0kMYR`7C]i#IaWSUJE>)]B]I(Npssq:fN`(p(Tg19IbldYTnVf%sTo])F-CD
Q&#63CZ^sm7r5-N,/B3U'TX9>Tb9FrN8WD%g\>ADXjq"&XQ7<87h!PYNYjlP).DheBA/L%&O60=#Tc'C
0oG%c4gFMBO%oZuQnQ*[E=gl-J^LYD-con^nm7d^>EK])(ZZ#ZA3@n7cTnM*i],sFRf>,rqXaMh*fkgL
iEt]oVY>lknGX&K]oA(c`s?>F\MIP'k???>NGud"6&a9S%:jpJ8m@KQ`UaN1AGR1'c"#W&d1`&_$=cHi
^FWc!IrFT>rCj$4JC\>dGXIC%G1L#pY8C&4h"Gf:4L'M0djU7sq!^:Va-.\,0eYZL01Be<De_bFZTPE1
#j58U0XKhq-(aasgWmRl$8-_cRj/o!mj)9!=e89QcLWQXpCkr8gj?C`K=iAnYKeE9je"HQ9HJjg>TDB+
.QSq;(q1TZFQ1CR:GGJ=*>$k;m;I6ih)\6"3*1t9Ab/fc5p(*`\OIIjc0I3B\'PPOm9V,8*-c3`7-_1*
=pO:LkXclSh_NK&QeCp5@37I!e#]X')Df;9Y'tCL[:0g+CVH68jBlDsl-;92s/34[aH@N3K'9pD;+H]N
#jE2>RKMWJ(RamOgj_-Ep>$]WA8V29dK57*"GYkpNdB(eC?)cTs.DkT$\(>pA'S2(YA\XlD7R\@1oa;T
jhk9,b7?Z5.;YGhaD4T70Zm\-A#3qX7d+I)JUDMLOh#\f):1W/Q;:[DlFJJe"4%<]SEC8"X;lW,NmA3h
VpGa:eW.rcBADk0Ksb_ue?2AZq*\B;<iKBJZ,l1&2rE]>.*8$b*SZ6olLogQl`Jj(-O8M#Ks,0h:YDeD
GQ,DOh"UDYpq,_Q$tWiNp'[+k9!h.0(EfZE0</uaSl5pCV=>#;hTONsEe%6%ed0L_$Y-S>mYih#0^1TO
)JN;&L`!=X'(%4..V?4,;Z1i`^9iY(CHo)Ycd0AdJNrSgpL$MSPSLH*Hf44QX<%0PCGm3c<K"70i8%\?
S\#`\g.#]AKnG5;2LE^&`49*R9t1_?p:qH2]M&<d`S,;_UTIKXUQW%*M,G1+-V0a2CCc+N1M[qt[24S*
?eU@&K*@.?NSPtP_`Kblk<;nOG*KBLCCe(AMRCJYpYP6`C%d+E7u'g`g7pB`Aa5^W:8,*\=F4Gr9%G'Y
]-_;4RC/#_m=DZ$=aT?Dl=+Ts+e#f/Vl$\AmY)Bk]3WY7$cC]<E6O^A#$?4+dYbHcV_W$$C#-Ea"tIcs
4Z_j];g$K@=aQCQK!bpR@b)S*>RP>._jkjp?S+9TGRsGZMm[WM,\n(/A[+.%+O/WF#<Id0LKrgTCCeOq
9%=XbB'h=@A^&lgEf&>$fi,?rZrfC""ID#X^Ln>]>WC&SBZFf`"27,ho)-maia_-SQ_4;/4`[hjn+`S>
-[rRPk;JR/=/uJ[qZZRKa*dk,qt[I7pIB/tP0GZG&A@2B="!kIEPg3QZ$GbrAM$g_%;I7PAtlqfXDl)"
\"d">84r]pREPjOX0S83Y3%UjL#"K889"1N9ANp[dC-UB`/"9SBa_[N`g!3MVH$M?)@KPj'CVA3r2)Au
.c2iR2ZInq4$P$3$hfq7ph93>3K/4[(tHASoOa,X[$9-D^=\k2nHZ[:SC!^</lX]!gj#,DO280#=%J:>
X&O!#M_%^\X'=AlHQB2L13GNc:XD"kq/4p8c[UpfI!:jsaQ5;)g@1tT9"q>HpfFp;-#sAWgQr[WJ@8Db
?eW!A[YK1=n^faBS2)c[djtC(;G<VG]Um/"b3<?HC]!`!Rl-m0iLThIYi:("m<&=ip2%`i?$,6LAGf>t
Zoeo`I)O9/Wak%E1V9Pl@E];kCZm(1#N0fdh!6$X43GnJU:E1=e3g!rd-W(=F">aN&^%L\@*)0Ya^&,r
2jRG:1Q6gCA^^P3-)(Sr0BhGuITRZP]Jpo"?_3J+Ql<tG"`(gEh1j_+Tlp1.gsE(+DP3MHXqp%l%@KO_
6SS88qFtkEFM>G8<Hlkl!uhG.WPjpBGsNNP+7SGtrE):"Rqcnh&F8Kr1#UBpPs[b8NrAl#7n&;ajOh,N
9Iq@\aPe0&o7+s8.dSUodsYp6&_7P5TP^mH_o;#<kC!klG>c>qeZjUG4DGYK9?k!*K9n`#;<'!Sd,HQl
[3@Ybodn[VNie*F=VH4^r1gLI6f`@IKbQSE2leX6,E.gOTi8N(4;Z`=/Q?uX><LXHSW/6g>M9RHrUl\u
4-l*4DiFJ5!T)c3\9jDA^G0eRqfR+a,<`.m=@Zg4e^Yj\)UWm0Oc":W1M/`?7dlC\*+eE:nm]g_"^.ND
ICP5[m?D]LCu>3!4)KOU7)q@:2c=3>NtJ?MZ&X?89/ioUL#I`>;uO`QDAPKC=@ZQj\mZUng5q>EMNp%o
OAUn^PFrHZoR<l@X<Z\iGmK7TVL:k;NeEgUShB0:(_p.?>2Fg3G]H](E^?lh[H!h0:>0qR%:9RI.LmG6
<A(d&(h4nd"VYHQ>GVh2,;%MGMXh?j@$HVf-/Ph/HaOfps6Reim,Xj':LPMd3WKhlXgcW9J%F&hI;1Q6
/VRP;=E</,/^#$"Ae1KLY,#!oe7gG*7^8rajZIFGq:'*"&ga9b4F8j``Ri6kK8F4%)tsX4>ie[L`.m/\
",3@qQ5B2`bI,IAXD,D:m83Uj/*4OW74HVWqpOIa'6I^G'\8D;5cTl?@DpnmF"<hDAQJhp(Z$;LZfp=(
(Pijqk=V<+,j,Y_3-HIbnAm>c(rU1<+Z/[44o!@cor[`J0q;&"f0/V8k2pchRsX#U'_nCdZHlchR>I!b
-)Cn,$_1:p7U7et^.5-)Ic`]``64Aag8iK'Wu:T#)2qcL`)GO<k/DE5GJ@hMjRlc]F1/9qSen>l.T#re
LLpgqbbZ`98moq/R@Up;>Og&+S1b_(LN.*PU$D1+QCb:mfbE,lq)4i_aF?oJaqC+2..?.4C?&9.g,-It
%a@33fh1&.jVDAoV&8:9)?,"`2X^&&R.7saRe+)Re1Z*I8RH3?;]^@2nbu2lau]slr<MdrUa?HQe"<]F
5Tbm*ZAk-L/`J=*XRGVJHg"o%Tr,o$JQfA?<iBq>#%O]b\<C_*M,t'qDX[AC;`Q3h%ai"War+5FDMecX
Noua;4lbuX[sW\S\68-HHu\RNH_c[L(9blEHo[t@XTF-uAbAKpR_/L2cK.F*^7T6eUD2:GHii%NjdBGM
WB=gKM6/mQK3qHH;s16`4;U>0)P]EmY"Gti`%Q*Zch-["jZ0qqBP>RLQoV61kNUYcWMenkq5SJPZ]sXk
`h1Cpi\-Y@e&][Q@?fqJ44/(-]P$>?m(VO#hQ`@/7!5<rS0D[Rcob0Y<h<6J5:b(P=\W`NnVm0so&Sqb
8o7lVad;a``P7WbpI^f]B?k3>:EqAcj4tceicUJ5?DgGA:'D6:nr/[>7k)d+fsakNfe0*`(Ii[&*/ldK
T:C;:j*iE&W<bB#5kmR>[[^Hc96h-TW`$HP1QufjK4CuF-RUlPH%nQZ/EJn^b:2OeCum[ba>MF:bFGCk
B6RO*$0-I&dAtBk=C=6.,O7BllL]IE&TkPWL[tB2Ros4.+[%\_ge0l?j[XBWB_MnX#s)+YQT*f3iq^(Z
T/M+KHQuNJ$6.2k27PZS=>_3NGbqi]%IWXA"C-$69$>%FOCA%>i;E(S847Cnpf5rkmr")kGJ"OXe$rhi
^"I==QT]u-/^a_rKJp@/09B6gq#+OL:*3T^cBV",N9EWRqJPqoo(:Ja]qm?b>+LAfGG=N33QA5J26D:5
Z0R`9lZWre-0ocO9ulq=i688EnXAadT6r<Dmi%EG;sOg.=E*#BeuuUm)a:SA%ZpW&[-F!fl@\9g40D`>
\\2M!dUCm('f>GH*i4rNfX#2S99eaFqAN+oJ+@<e1i-7MQYK0cC2OGZ+BJITD0W=$_6M3L3JQNM^rq!"
6PT=1*9?<@<m`"=r:p,;r."_nrm<t,YLaj'O!/$'VgNUdJFg%58,r2H^Y^@:ldu.;VtX=O~>
endstream
endobj
7 0 obj
68517
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 803.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
0000069260 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000069236 00000 n
0000069714 00000 n
0000069430 00000 n
0000069469 00000 n
0000069571 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
69787
%%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,348 @@
<?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"/>
<node id="n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="174.17919999999998" width="273.1071999999999" x="1000.3040000000003" y="433.61760000000004"/>
<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.380859375" x="12.06892812499973" xml:space="preserve" y="7.247609374999911">Static Pool<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.45580882479480683" nodeRatioY="-0.45838992615076934" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1141.8400000000001" y="536.1087687499992"/>
<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" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.142578125" x="16.9287109375" xml:space="preserve" y="21.015625">1 x 128 bytes<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="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="90.0" width="164.73919999999998" x="1113.1776" y="451.01919999999996"/>
<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" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="164.73919999999998" x="0.0" xml:space="preserve" y="0.0">Group 1</y:NodeLabel>
<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="15" leftF="14.739199999999983" 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 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="60.0" x="1142.9168" y="466.01919999999996"/>
<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="28.0" y="28.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="n2::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1142.9168" y="466.01919999999996"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="20.746093749999773" xml:space="preserve" y="4.862813159989173">2 x 64 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="-1.1657341758564144E-15" nodeRatioY="-0.41895311400018" 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="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="89.99999999999994" width="150.0" x="997.9168" y="451.0192"/>
<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" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="150.0" x="0.0" xml:space="preserve" y="0.0">Group 2</y:NodeLabel>
<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="997.9168" y="451.01919999999996"/>
<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="n3:">
<node id="n3::n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1092.9168" y="466.0192"/>
<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="8.0" y="28.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::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1072.9168" y="466.0192"/>
<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="8.0" y="28.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::n2">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1032.9168" y="466.0192"/>
<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="8.0" y="28.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::n3">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1052.9168" y="466.0192"/>
<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="8.0" y="28.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::n4">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1012.9168" y="466.0192"/>
<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="8.0" y="28.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::n5">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1012.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="14.814773932043863" xml:space="preserve" y="4.392671444094276">6 x 16 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="-0.04942766514963404" nodeRatioY="-0.426788809265095" 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="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="90.0" width="150.0" x="997.9168" y="521.1759843749999"/>
<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" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="150.0" x="0.0" xml:space="preserve" y="0.0">Group 3</y:NodeLabel>
<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 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="n4:">
<node id="n4::n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1012.9168" y="536.1759843749999"/>
<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="13.0" y="28.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="n4::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1042.9168" y="536.1759843749999"/>
<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="13.0" y="28.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="n4::n2">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1072.9168" y="536.1759843749999"/>
<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="13.0" y="28.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="n4::n3">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1012.9168" y="536.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="20.74609375" xml:space="preserve" y="4.392671444094276">4 x 32 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="-0.426788809265095" 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>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

@ -0,0 +1,256 @@
%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:20240209121153+01'00')
/ModDate (D:20240209121153+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
GauHqbDmmZP2*dc,(JOJpjOd%&g0Gt.kP?b'JS!oXFWk;Im?:Y1FF&?pX/3EoVU?*%+=B^)P-:.4Rk3i
s*]RdT>/;PVD+YBq=ssLgOFU_/,o=0j/OX---Sln^>2M2IJ`Ed^O5qQVk*kY?bcTdDuVnJr7h6u29C@I
n%\htn,N!oI>bl:q_/!E8Gq*>`4k*>H&qp+S%%gYRm0Q`^J"46=8=LjUt,*E8fY3j6_j1O)dT5Rr;36d
2TE1sT$toUNk^6OlG0M$`a8\SDu][+2SjlHja.QR?iT*F^H[dDqOmlY/O#F`o1-puZR+(6c_!Z$9=T]T
3D@>hs2JCF[r;<4>?W&7(f(C?hGX+g+/RK5>#oRYmB]N&/Qlj"0&n123'X=7S:R]6PWlg21E6kE\4XeS
2\[`<&[K"H.d`i7:KfE2]p+JeNVt]"Y*L+:.OV5^Fb.U2n0a"#gC<8<]$kU=OJ*NV)=du!KCWJ.>a@;!
AiTJpf@'RJ7&^"(Yb"R/l(pRdQ<kVs%?8)!>V`PO_WeB@PWi!pB+5<W>7FTapG>"eLU:Cra(k/I;;^ah
mAcX]0+W@c9"cLWCDs?VR[8F?6<T5F$_0C:m"MjrBBs4k)B)!QUC!)4)NFtV@<<r9q$g4$m3nNi+q1ru
e]XeUoN<]O"rT6rdIc7nDrJXG_<,>V[+c%pmXpGV6L..t<8>20'_?%rO!N4JZZYrn7!-+)W,<%t>ADS8
/TJAA+-bJKcYS>'s3KC9l,0-0e5A(AITl4'3i\f46/U?l%$3[[D1-a[<d(=Q9X^_,!@p9Wm'TZV7GRh3
rF)Ja082PP(B7n)^+A+X7k=ASO*kK6,(6!VRDEO4*]U9%D@#R--_LN6iTq@`,YPD6Cs8m4P?AfT*5Gd]
[;(LpJ)b!=C%su^D3#$WIOeISK@*am.##XK[S[_P!?^>tVXHJ#qEt3?2Z8-=k2XWs.,17T[bN*>#j]U:
+n?FH0^6*EPO[^;:H%KH3f("]U9q;P]"j#B)eLQ$@1?"IBSn;YH6lYb&!tVhGK>8uB'7MF"FFfqpVGc<
e,4hFfV*0s,_P,I_QT;tW4=HSW2;Ra[gBM4`qdES$e#Amc(@.hj5@*2oF=>L%a<mBs"l[h;q(=d*OOlf
J\oOmWJbKB>#1\>#4"cMCBF9nZ_8(\3B3X.Ce5M5@GC=9ETuK##3d.F9`om9\CVZIc4fpE_>+:BO<J]+
H82#q>$E&a":@K&ZD/,"^/e9SMaTXR4:emV+:YVSV']$J([5Yf)XL^]K57i74uu_XpD*@olnX,&NO9ib
%&N3p%WmY0V3,o$NEeT^eb<HbXfh=$96H48FpusINN>n+-?`G3[[)E[c:Y2<_OIriakDd!l>kAQ%VsAW
AF8/If$8]MoQQX3ZSq>[Jc02#p-mQXojmFp?*'U_akBmhOKg^+ZZ`MGY*I2DWtB:KeU(@CG1Jk?KA87s
@g/HOM6nM9!.9`I^:<BOrbA\h]cSgEhCU?uURj'M1$c"YMq5^c](iQ#&>PQ:2.[P-3c2mhE-_@`J$@fS
Dr,`=a@ib&>8l*Uf6+&oBsDgN[$uSRWT9WU_`,ciCF7Mt7gQI=T/'sF(R6T"e#_EoZA5)r#HW*R#p`]_
;dE]PNpSIdg\6CCQ:6F]TV^1VrK%68^6q0\hmJKElGB.7UIAF+V^67qcJ?fV+8sR\g-Z3Xb'QW-m65AL
UC0S+Tsnq;>ibcqXPfO;:H"8jre($\6sJQ*T!GIQ4Lo&VH+V6@U@aE[<9K#^&i7s"S2Z@R:6kOt\?n?\
@p8!#:7CKqp!R+&Y]'#C/Qrg2RMVR=@b(t;`foH&?N-VC;Id]"Fln?G<NR>6`9P]52`L$-C<*l4plc.7
4LX"1<nT+Nr-?OgD0lC<YFkX:nR7D1lb[?95#;tmOVMWgf]B,h03"K'->^:DU1!shS^`%C-di:.>MJtE
:J,\\RN)(L`nK=]CO(1Tdmj\N7gT%#Z1B<^qo0QCdR_QNI?m?+,6&K!4fE0TH4Xp3/#bs@]Ql.@WaMU]
BWhp*L`O1NZaDS.g0J?#[[Y>lJYclJ%HQ=qZk].NXgD>/K%5%V<?,\IWJYSHNOHKbeii>SSm3KlhX7(m
EbVLh^[9V@pJ^+\Sq#\_][o)h+u?TPo1!NNWGPYC7aKRZ"@1!5)Z=%7Gk!')(2ETj@O9&6$UQ$l,W5#f
q92>l8u!Ra*@P4\#";duf5O:q?Lihd1dQS3oQCa.7'^3sQ.A<TLOPreY-r$(%o1.R9pT8^-Ke$C46_+/
`eS`mioh5qd#K&0oVC7:jQWId]7!m5,a&m0hQ=c"ZPH0.p1eq0dAGT7:YaXJ`ZN^=N>&k8^p7rB.2tdh
>7]pt>(T/ji2PU,j)3m?DJX0V4aUHr-0P!<FjKu<`jQ5I@g'TS-p/qo6bA*h`@>K!O@WDGNGfcFPDn8k
]N0g#G1lYPO'jlnkp+!`qs>Ub7G_G4*lj8smU>/I@Y3o^Fm>;3YP,@#_qiT]gE-XV3s"W]nb.b<p$Y?3
@p.AZZUN>E*hXPKbOH9YrARdUTCN!PnOqQaZPu(cT3X,pj&";:8,VKDOZqD-aSKhVqQ$O/25[h-T_6]R
C9+8bQB+N5[;fCAnV-i0_0PGC#EHtl39hG%(g^PAcZlqVGQ/U8AnYrZ(Kg>P)i3=ig+4SW7aRXtd$=q8
L%9Mtd$HV]Gf>!pdr-2@flXdJ(LPtj,MLHnp\kW*SaaSJb1esgYlB$7O7W0)U9S1#a./*sn^kcE-_o=M
I85TH:.DQ2I1D!Gi$Z1lT>/6I"-Du2cT.EHFQ=[6rO+36h85+pMnhi2=P5BW=f<9C4LP$c2Li\nn2Q>/
86aIt6etQI5`[KZ2E3b_<i]HljKMH:KY"CGZ?V7LVr;CX2Oif;5.FFdQb;jG+0?nF_p@U)<,M@*<4`aK
)7I3"r9K'"d!+\r*2V?N3SEg7/[&sW'\#jaP1H<_Znr1[lc&EWhnM0pMdc"_:8QfFUdiam=V#(qFVU2P
6`9k0jf`3QgpUO`EW+<g2@2a5%OX_YV6HW:\If`r(V&+CI%c@aW^)q#c$?X%X2Vsm_OT3YPd"2`l=-kK
Das:U\!CiR(YnG5i>#L&.%$!kq/NSCgUU3Qm"p'\LpGKB":LoFAXH@$VMV`V[,8V9DI=TQ(@-18S2PtA
#p\$$-S5\jm7qD$]fs/,rG[Ujfqmc#bj$fCNm`;<$&dI'B'Vt#8i)P7BZhobd:A7W,=T4K&,FXOip8p_
LOQ5mBJ%hI`BU#`MR>[AM[6@;S!hjY_\ScsBmPt3:"*f2Dc=`_qlG]U3EaMtbh9gYQsI$c->q1Tn^X<F
B^MnK`hQN!$r;KHG.U1*@D,\s_idP]nd8k`lcJB9?)Sci:d<<iAk*aB)c^K%1m?I&*P6=t5d9rE,m2_$
Vn<<o:eoNRDDM*'FcQa361sg4]H*<Ip6FiBg6!taI_2m/9DeN52='`Z"+09,F'QE1KsjB9h>)I?UQK\,
Up?#_#P95ma[2];_gZ=I=Nu-1=+4sjm!L#3.`Ou)]!rM#B/5^beuNBCAU0$?f$DdgQ%CA>f7M`$=0J%X
#SL]o3a$TV'C?#TA`hfd@]!MG@]tt`ak0Va-H6NQ)O3e[.sNgi/Z@KEodYrunT!k2)NU0T2=8&]Z&LD;
c<'Bue0<IHeB2N,>,\p9X>jgXQ&<0+A6*E=(@bpE05=Rp<EU4MM1k"f?*Z'J_JWF3?gT?.Y$SZE.+,Ca
JE/T+Z'D)kYS9lJ]rKo2IL8aF+jlujC-9UUnMXTFe4`&:/f&qAB\Iin/1n_3=D--;0Ul[BZ1&_8Db2^b
['Hk%40co"!"29l%*"Z3rktlWD2eW.9%^ZRNk3u9Ma+0YGen(&OLn,.GrJr91gVYDbJj.Yg8UN4YgmP3
Y!GJBV;Jg7ITqVTi[KWJoL/WD1pI$.H@n./I[Wi"]__D15CcfGaQP!u1TR[g9*6Yui6*Kp(MI7$T^Bl4
l4#-LRH0"HG"N)]RB]N\G0l5^ge]fV#>g27[#Sj(5X\1A7I["a?*hQ&War9\^@fsYo-*5J)#@>mOb#;?
;rr^6]^*0nWSp<R?kd:ah*%WZO-=Wa0k[Tf`^=_Ta#4<ck0d5oZlbe`(65[J6f8.>=Y/`2oR4nkGg!L3
%J;@oKhW'B3<Gm]mR*t:^!&E,Pp]&mJ/lP,])<P^q`ktSqPu_Ah:VB"opQE8:i>@QNd)%R?^]@8KY8]$
.r78lfYIc>Z5b_;0L8GRq9"\nSE.(2XlM$>`^k0:f5@FBorM*W69'%!m.skVQ$M@lE?!AMY-NVO\nH:c
I]NFkbrYC#-2CHlUBd/88?o[?IRlfq>0bkMj&cg6/OrroTlBHl3H3lDAfm4L&<hpKl\%T0QiHDJP2GYt
U4`DcUq^i\OV:fom2J'(`3G#k[*W\[L/r8*/XHqTASA"9I+f,)`E[XCaqGQPOc=m_YU=#U$X\O5;D-bN
)KjiEYJCg\-^#-k8sCYcRC4CWSN<^;C2_WT">0?Ci>16s["nQ],:-2g"!];L8L@r=lHRtr7#oB4OWs60
q1)H*ql\Sf6AMaE!paTSP<K+=347A-D?f:";sM31R/=_i`c*-TWc/n!ra];sAjc'/K%G]7C_t<Z&M3N\
o2O3_Bl#c^,'p/l2gC*(nr,[5&7Q7Yns@;$M!\[u0nNi882R-/,!R5mP_*HroaS(^qChALk+Wk-X)r5T
L&+!bYbJHM\<[mH\6b2=ZQcl@#jk0<SaoR\E`Q-QN&@sqofX7`lNXo;FpmT-hCRaF]Ma><OaX=s8_V`T
]d`0A9isrCnq?=ce?6$HHMJib`Y-?ooV$!Vh;1M(#kM0aQFH``Rk2hFk/$UJ[U'l"_q\sAX\5RrVE@Z^
Mk1`ZpI_NpdE*>q]jT*IDlNbQDjG._M'_?Y`&ZD]98h)+.paUXcgoPB\@KipB8heU`Q9djs)G2Sq9W]T
;!cin^gFr9$)@J=Hi8H`c,WVn[9%;6O7i2O^*efCg"R1`h]1Y;,lHbK%)+[AqJWJ39CJJ9qBI'trTSA$
SK+&//4\jh6tD.*^G[^]c/)S#$%)V_mk9Z2Z+G_DK;)r/='roX^-%4mLQ5c)]S2rg*Ui2]A4I`kOEEfP
rP96Qn+P%L+/mr]/_^N\Y.DO?ikpHkqrupaO,_jm?!tiu+:HrYnnnmO6a`q30V$j_(GT'QV)'H6A*ldc
]n+r.`R3,V1mAT'D>#PR[kEgOTr$k+/H):Z7Vdi2^R.pdMTcN6WlRK1T?9+C,m@a`P-A9`=38dnZBTZd
cP=(*]@"kFZb#iW6#VQ4_d\oA&(DIWh&*J,mg2-5Pp>"RL,A(Zd9eoZMUH5TY[Zo+774bhDLhCRQZiSA
Q=Qr+h0$%kMf@YrO7f&KrOCIsG@W80QL#&Cb?3Jf'#W#5?RR#ZMAYSa+X:q8PbO[]5KbWkHIi\YIN:MJ
bPjZfHf0IcRJ\7Ioj=lmHp7QL118I/*6;M[ZcG.!PK775p!*trXbdXWmgIVK1/'rqmlP?%X,0V!fFZaW
pAG0$FOWBnG[M7jBoj-eftY.@*`$C;leS_WVs1m,cMY1Ko"+c:%A)'?1\jOeY[1U.l-eL`?eCJ%D:aZ=
%1tOj_N@\O"pcgtpjni0>?sfuP%g;u*1LRhLU5?d="2]P4<Da`>A)#?>K]bs8g?;P!F\B1SoqS!Vtd_+
+KXRk,Qm>c]!^n6[9Gh^q^+A2WmWG)B=8a0?9-NPrM"IH^MUs9NV:R3Y49T!?k];[]8upK4&*tHAr[8M
Kda\eZ1(J+=fk<41YLXa_4(U?Q]QE![nEsZ0dr`,?\/(740cZ?,#.CFA2@`.4f3C`Wjr,m=6d82hPNFq
X4(6OR`5)FgKD3k_no,.?f(lZHhMh]P0Ni-&SjRBLJJe:Ki+JD2"tn,XDnjOEdY6U\3=gNQa_IoJ)fNp
/gPaQeORpaB,oM"hd8$^EQX?1bhWl3"R_K$o0eYZEl0WRKCRSN<WZaegA*;"'Ve\8,$e(3%Ai$21/.O.
CcC!NMX!(O>K`08#kG8R7U0(4Hu16/[/6NY1=/g5+-L:lnoT/.XD[)k8BfhY:P-c6]8?JgYLI7XbY6[[
roOisYrC=ZUfPC!\L[[mk]nkB'!Z=8F#ELjOaDr?6Z^n*W*PkEY!n4!HXsJ1PeHQ(?uEmUNTb%kS2aE`
/00(<b-Y2Ec/>-]8jqg8]Zs.iNL'iq(bpCX1H2(@*UT;Kd,9P<;4Njo>SaWsb]p65_n@Hn_>/B:iP%k"
QbUGtF[(CX&(TOH8baq85o.GU=3dT2r/gCWrFh]eS336gbE25d0bEqP?2/2Fb;^GYm!%J:bY6VTaImZJ
`@(lW',Y-"KJ/mg&R![Bb.B0$Fh>"V3TB#"-%$/O@-Ai_)am0X-r3]@m.FYVbb0d&)mYF@]7ornZ"^uH
*)62G&'m*Fj1F$!Yfg0c(Z.SELp8da&Ij(B"C3.n;/)B>>Vt0kcI?+-a8-aA?#H&WZu$DEPhBtd*\3/B
9TDO0^nR5b;=US1T1%]E(VK:h?b6/V@FP8.CHe+OalME4S^0%)k"kc$o>9/5T]71+Qam+?7[o;7;r[uM
IX9Gd"`tp-\DpHO4uhT]n?V):Xg!!?#;\0u;0Bh1.77$G#/0$?MR^:C9i[Lq<Wmd2Br^U2]5\YY6egKj
pptXT*knOP.DBjDpt@:Xqr#.8V656ensl7ts!SfiSo9H%LZ]12.=Oe!S,La8p',2;qZabrhRa-/\G"@h
F]g('HASO0Q]I\$A0I"%AU`G._[K/s#\&KV-'"Y,h:#m=9>1W),\Xh$k,]d(7=HbkN1NS'[uk?NA<,W7
#`5Muq!/]:LCJIIghG0P7si>'2'O-PEC?"k&8sHOb0<iij1r+L0L"nP&4U=nVQ+=t*b!Z<cE(J:4g?PU
7/!@so4gA1AMQ!$LaNcjXuo5'<ZRNIQ8TqW#+4jj5PQ]biLAeW0ADi+n0k#$W_lBl3=fl1Gg=?=(JV/K
j-?KT3d;r.`S),CA#&+2&KXMH"Zo(?jZRFn\",*kB$Qt;O%:;:Et)d$(ZX*96I[0h@7Z/d]9OoL/JW;"
pIoB:-S^Mu%`YMXam??RE*X-$jK8IK2D@p5_&(s7g?Dh<TR=]:9kE_Zb.+u0Hm#g)*:7NEBp=&kk;^V>
L4-7]P@lBsg(h*0d8sL]]4"5`Y`N"r?349-dU&1*.]aSICOB:PKi0r'mq\\;V*t"Y*ZX-D)kFP:G-PSU
^Ea<VoRBh400\iI^U$@:L!FM5ib6Jf3PYL0$qcn2:*[b?VZFGO(o[JSs(hmNIHP_Gp3QSuf))Z?5M]Om
oi>G5eQ;)4=88EUXhK,IYMS,GH1tWp=)[WZ>CQ8=*m8mOiudM;^3Y]eYdZi[q;Ucj/&Pe6N5s;4@4M%i
#'ed,roNh1=S;^$bh&I>fAFf"Kq.9!7,=GuKQUYDg:\jF]G5l=QkO])Q;EIK7#WMQD(NU;hm_Xfj_8mV
GHng4Kcf-fLEp&9WUV-WJZNb"8pu3L9d8P2[ji$J;-FHdSE%;#3CZs3Bt!*i*dSBu6VHOhi1XMmeKl=V
0QjE(U>DSRoi4I$B6LgdQAGmPb3hD;P1_g@`&[4C:!-?3etmbZl9$sb>KjdOZdnTk!:R9t&YsjH:ER1D
/Lu0Z[gCt\e9OCec8[<e<@%a[He^lN\MNmgjDh1eX"(5f2c(%;3@jDR=si[4C!*+r8&rh3H$&.8GIiao
<e/g9U-c/QCjTaGhGi3pR.Un>X]D8Z.G@kD4MN2/f5e2BdV8k1CsA+>aMJs)e)nHU\*g.\*`TW@,gL\.
I\U'"Bb/2M!S4PL#LM+H<pnMb$Z66DKbghd_k+X/`YmiAB]6q^'(<'he4o%a.#iNf0$HR(VS-ji)-=Ao
jL]lj>%Mt\s+[k]H)eU!T\4h.aq+=HILYd=jo$*_IOQQC`]SXB5-e0KS@IRu%^05]IqW]Ik_YAoa6ruC
Y(<$kM-6I[1L*'"/tQttcaO_:[u9G(`UiK9oBkQ5G4Bs,nU.rn^0NkY?9I*tSo5Fk[hmB"Ms&j6oBjNg
mGdqbnU,]RI(OX;>s."niq`<-q=`hDG4BslGeUk9^0NkY/tQu7So5Fk[u9G(Ms&j6oBkQ5p>YmknU.rn
^:ed*>s.!sSo69RD\pa/Ms&iQkM*uImGds8iq[cfI(OX;]m.7s4Pqc_D_I]#(XKRKkM(pXgVrdOiq`<-
q0):U]6M''GeRHGh_(L=0:m(ucaShqgPb7L`UiKVq=YKJpWg1->U=C14-o2E#/m7[#'sc)YOFFY7JP-u
HBh2=THn&jh*S3M>YciqUiuA":/o"lc7VuV>3DHA`"?1kPKR:0\%=D7/d(kRGC+T&4Lr,Bn&"It/sNg1
ai9l=m4&a"?9?gR&fphF`\V,&jHQ78);R9XZZfVL0qMQbi1"&`ncr#(3bCmYNU>F?CV<XN13;oSk@41S
%.4jRIj<=6n\P/(&pCsLOM0JhnGUiFn)^>\\a$aM<S]MRN`)ud]A8E%Qc46s'!iY/i*q;L/]?7Z5;FQJ
LZb<"dmJ[6_>/B:^-.=8'_eq=D8:Kj.o3UL);$SmG\BM-gQ9BV\/Mdn*6;(J2K-<:j`c,sj>.6pD-ZlO
f;`1a\-j7EL/sOB4Vcca2Bqa``Y(YL$+l[D7(kNW<-Ln1PTua5ed-X.82ua0l-%aS(MA$,IC3"GeF:i^
*#TY9HrGhsbZBl)`Xff/;Z.D<2EPi9=VtE"G,s:m/<fh3%`L(O9:=Hk*D\jjT&tB/L::FBel[F^Z#0(u
MQtl0FJJ/M<34KpjZRaNdU.kN7GY?VI#ME+IH*HmrV!CN:&eS(q7mE`Y.*Qq,'h&U,C.&@&NZNJG^D%#
m@BA]2r;bKr&@A]AaS'C=d5Pr?%0-N=d7(,2t+fT!nl8=0]bOOSV'l?\T-*bCkk)4EL+)[g)9YE9kAE^
f_o@:F%%N5p?/DkYjpEu[jU2(BVAD'UdB1j8U093l,e72oT7<:RX1<a\*WZk[>EYarEV;@pSg)Qao7Y:
m1%FVp5N35in2CNg=VSEkVX<'Z?GMpfdU+Gb*jVHYTN4id-:,UED)PVQ.o55"((qj:WYa+)XmGjAtcGW
%?A4eOf9A0e+UlN&]jQXLLpb9./n+[h&JS#_U<*.q=,M.OO).[;hMhl_[)o]ZhJTpB#3=X%nV_7pZR3L
Wlkd0+gPu=5ku7*%,p]o@Sj4NGqq,<h4[E>-[1$o1[Lt@AP>%GXU<C7;OkPi?,!!+.WNXJ%!p+1<0Zq_
7nV)Y1u?OdH>Al9rGq9"&j#$ANchp;D^L4?/0NeR%%188c;K-&p-@&acD.qE'RmnI]9fVn+[\;XGtBuC
Q,XA;\,0:^<=4b)e$V%1QLHnYgqutDB9oNQ!,q.nMASbLEdAr3N2=#;^h3RH><DIAn.iAW9XK@!7SS[;
O&E4EH:MB-+WfL)oO>R>@E:T?8($'+Y3@LFGC_Kh,O0.kp1&#qQjbrcNO%XTqU-P^)(\B5Y`R;B?348B
.c]<[;gs_RTlFVDdt*7d=3"D^b-Enu_H^Eu]OK->Yh'js;YD^Qjlj?VkP*Bt\o50TMQC2E'f]]F4'Ptf
nrGSTaN<L42@AQ]?]M&Epe.^bs!VAV?G6#5DuJ5_]D(m.HL#cn=O"H:iqYN[n%Yp4QR8.!g:cT4VTD&e
%E!D,l[-[THu9SrEa/k,f4Y%@<rG69)tmpfos_89q!rUaL!8>2h)\R=hj7aH)l*CPIsYJ12`BLL?A-'5
+"c-sf0A5m;ZQ-uEVA/A+jh6UIEQR,N5P3M^L$IPG=gE7(NW`\47W\IbH$'+^O*&*pW.mf7mR*hfU's?
K`.hF_:h[jjk`4oT-pf8ASdc`2fpean+(iiDin.+1TR(GfQX_=+sXSG;!s44<W)-98'`Z6A&qKeCk[l@
(5[#$>FLJD*Ye9PUXi/@RId:(/S9.pmbo_8aP]0Q"8+TtpMbdDZ1$2Afic#>a`]3'-V$7WKFfNW;V633
2=XFZqoW#b`&Gn<njm3IkQXZb&EQ\S*]`)RYS['?S/_D"(A._H,&bH'VI;o&l_X'o\S8,Q44Sc5_<-l,
[md53DP!2n](+VEn0Zk!g\8T!&"U@9Iq,j?gH.C/Fec1]hs5:ZXj)4/Q[b6Fh-HXKUkg^D_eFa!i]V58
*kkJCnPq8?g>f6HXZ*PEkM?uX;Ob:Pr%;c#-V>2RC\#=heh7OB+@r;TkpP/GHW@I8G1Nhs<4>g\NN@A9
ioX\0$YQrA;I'2JFGs4rRCreKoZ;>3f5kM`^fIdZfdH(][0=V5'k@4=9GPim%[rkq=>L0i6,)C;)s2a,
<_nrM'8=1ZG##F5M1<NOhnCaM(q/;/BCINllnJ>P'`4bM@H7bBf)]^<fV7TjL&8GUEf7J_D<HWl/@heK
J::T-QF>S',Q?sMEPkAYK>ALUEMUM3R^./2c)k,Rh=A]hkI017ctbj<LTQoc=g5MlYW%&ec7o!#ICj3,
=TWN3LL'3]o>Y,44q2:/hQ5SESjM93'PA=T;=OH?Pd"E`]-VO"/>ZZ&Da9Aj>lH(%I:$gNnU&JSiWJFn
^l:&f`kSA12b*02ijsWg`P*u`9.hUr(C&`O%\CVN]tqE92XBU3hW`SmhagF)\Q@&W4\ROiDj^:lJ/'gp
]n!tuXn^XeH6h4Dr<gLT__OhK`4m'Z)Njps@!"F:=^>.79#LI8T3Tj&r_Mqaen_bN;%`os?eQ]apr*_*
=W*u&]@^aF"'<6-mMD<5CF<Th]H=!7H;Zi(g,1I]=)*s4SbcnM$0)oooD0a0=khF0IrU0glAD!h+QaXc
bSJjrp3ZiPV+O6l@_9):L3>[j$hBkiMuCQi&CW8m0u_^p,:1R`Z/$Whj@(J,Om!MtZU5r4;^62;Mdni]
XW_V`DgPVn>M*)EWo&hh"tRFKO[UPA;,)@Zir!eCm_G[_f;:MlD`kI847&Q*>1Q;XVIT;7hiAaZA,(3t
i!/ZUEE=!#QV21OO:LV)r!c<S-4XR/mIi?oYDoU\`mNH-`_G+40P#)/YbueY-Is"\ZP_AW7;<'&$:V_C
__G\N6aYp1VrqudM%^EF!)FAr;)ud3U_W@pY"rXZ$MPbDh?3:cTtOVD<c4mQk1t:\DFH^.@T9jVFVdNn
+h]W5I\'prGX8lu.-0o/i[rcj2:gZpluJ"p6dM#PIB8Lj1%;-LV73TXmOK*2r5+Ef(k7_Cqngcg]$G]@
<PR10cGpME\+<bWEUo"EO0p@T:Dh:a*=mHkPOV\t#/(f-jBHgJ'W%"l$Sa7MO4$R2Sl&A4>14)<bhE"4
V6bGbMUIn$<H720g<Uh"C7#r09mMV4MDX'=(<JsVbDLt1f+80M=:b.5MtNsmm$6egP@iF\@rpKEEA2<K
.Ul:<O+[%G@HOG)IRh#>rR%\>#&'Y+#,7i==$b\PQcd4NRfYq9\fRaZ$SkhkOMY3O:A_jZ(!nCY\*D"j
;r'Tr,F;<uH^%aplh;I4CRi+n7c9lZe.+QRHeG$e,o2!2"a7J'AZQu&8]iF/BFjpL7OD2>k(k@tI,)m?
&Do[H)F@eSj$srLMmb^cBP)bmn>6uD`s<bGn*n72+u^")W85\_<pomAn5nli)&+7HWpms/&WSUYU,f&C
#5Ac'b6FZf]Bth(A]g3ZQ;6$>CcPBH_iW^Q&9OpeP^i8RATr]Y8RAZK;&i.\MQGHi*^P7lUqYf9B`/I\
qZ8&2`u8jM;rLjE<=4aV*i+Z4\od.U6pol:BW7($Ur,NE1[b*JD)#'+91GP5KBnu9,J!g<bt<HTar4PG
9"SP)e4Vp:.G?(tDgW`&KH3L"=1F5#f-pPdc#M5b<3Yg0R?d&rcH`u^%DL7/j%m?]0<c#"Z\j&8A=Z>>
i>j2R^V<J)bmI8.G3]Ran2u^b7T[MIlijF^3R+SfBA72s/?(G/3)h87Q"'1kBs7D/5>A6+\`OTi[<lmg
IecDbl*!]>gnF[RrpflHVtto@Tle->l0J>$j)m_ArI._+p*8=1b\E&As5i"M58+"hg]%MM_N%=~>
endstream
endobj
7 0 obj
11983
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 311.00 209.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
0000012726 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000012702 00000 n
0000013180 00000 n
0000012896 00000 n
0000012935 00000 n
0000013037 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
13253
%%EOF

View File

@ -5,7 +5,14 @@ High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttga
## Building
If you have not done so, install `mdbook` using `cargo install mdbook --locked`.
If you have not done so, install the pre-requisites first:
```sh
cargo install mdbook --locked
cargo install mdbook-linkcheck --locked
```
After that, you can build the book with:
```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

@ -11,23 +11,54 @@ time where the OBSW might be running on Linux based systems with hundreds of MBs
A useful pattern used commonly in space systems is to limit heap allocations to program
initialization time and avoid frequent run-time allocations. This prevents issues like
running out of memory (something even Rust can not protect from) or heap fragmentation.
running out of memory (something even Rust can not protect from) or heap fragmentation on systems
without a MMU.
# Using pre-allocated pool structures
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
pools.
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.
For example, a very small telecommand (TC) pool might look like this:
These pools are split into subpools where each subpool can have different page sizes.
For example, a very small TC pool might look like this:
![Example Pool](images/pools/static-pools.png)
TODO: Add image
The core of the pool abstractions is the
[PoolProvider trait](https://docs.rs/satrs-core/0.1.0-alpha.3/satrs_core/pool/trait.PoolProvider.html).
This trait specifies the general API a pool structure should have without making assumption
of how the data is stored.
This trait is implemented by a static memory pool implementation.
The code to generate this static pool would look like this:
```rust
use satrs_core::pool::{StaticMemoryPool, StaticPoolConfig};
let tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(6, 16),
(4, 32),
(2, 64),
(1, 128)
]));
```
It should be noted that the buckets only show the maximum size of data being stored inside them.
The store will keep a separate structure to track the actual size of the data being stored.
A TC entry inside this pool has a store address which can then be sent around without having
to dynamically allocate memory. The same principle can also be applied to the TM and IPC data.
to dynamically allocate memory. The same principle can also be applied to the telemetry (TM) and
inter-process communication (IPC) data.
You can read
- [`StaticPoolConfig` API](https://docs.rs/satrs-core/0.1.0-alpha.3/satrs_core/pool/struct.StaticPoolConfig.html)
- [`StaticMemoryPool` API](https://docs.rs/satrs-core/0.1.0-alpha.3/satrs_core/pool/struct.StaticMemoryPool.html)
for more details.
In the future, optimized pool structures which use standard containers or are
[`Sync`](https://doc.rust-lang.org/std/marker/trait.Sync.html) by default might be added as well.
# Using special crates to prevent smaller allocations
@ -35,7 +66,7 @@ Another common way to use the heap on host systems is using containers like `Str
to work with data where the size is not known beforehand. The most common solution for embedded
systems is to determine the maximum expected size and then use a pre-allocated `u8` buffer and a
size variable. Alternatively, you can use the following crates for more convenience or a smart
behaviour which at the very least reduce heap allocations:
behaviour which at the very least reduces heap allocations:
1. [`smallvec`](https://docs.rs/smallvec/latest/smallvec/).
2. [`arrayvec`](https://docs.rs/arrayvec/latest/arrayvec/index.html) which also contains an

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/events/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.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@ -1,6 +1,6 @@
[package]
name = "satrs-core"
version = "0.1.0-alpha.1"
version = "0.1.0-alpha.3"
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,10 +71,11 @@ features = ["all"]
optional = true
[dependencies.spacepackets]
version = "0.7.0-beta.4"
version = "0.9.0"
default-features = false
# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git"
# rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7"
# branch = "main"
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
@ -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

@ -5,6 +5,7 @@ use spacepackets::cfdp::ChecksumType;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
use std::path::Path;
#[cfg(feature = "std")]
pub use stdmod::*;
@ -98,7 +99,8 @@ pub trait VirtualFilestore {
/// This can be more efficient than removing and re-creating a file.
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>;
fn remove_dir(&self, file_path: &str, all: bool) -> Result<(), FilestoreError>;
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError>;
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError>;
fn read_data(
&self,
@ -110,7 +112,16 @@ pub trait VirtualFilestore {
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>;
fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str>;
fn 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;
@ -123,6 +134,10 @@ pub trait VirtualFilestore {
/// 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,
@ -155,7 +170,7 @@ pub mod stdmod {
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Ok(());
return Err(FilestoreError::FileDoesNotExist);
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
@ -166,7 +181,7 @@ pub mod stdmod {
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Ok(());
return Err(FilestoreError::FileDoesNotExist);
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
@ -178,6 +193,14 @@ pub mod stdmod {
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);
@ -232,24 +255,11 @@ pub mod stdmod {
Ok(())
}
fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str> {
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
fn is_file(&self, path: &str) -> bool {
let path = Path::new(path);
path.is_file()
}
fn is_dir(&self, path: &str) -> bool {
let path = Path::new(path);
path.is_dir()
}
fn exists(&self, path: &str) -> bool {
let path = Path::new(path);
if !path.exists() {
@ -267,7 +277,10 @@ pub mod stdmod {
) -> Result<bool, FilestoreError> {
match checksum_type {
ChecksumType::Modular => {
todo!();
if self.calc_modular_checksum(file_path)? == expected_checksum {
return Ok(true);
}
Ok(false)
}
ChecksumType::Crc32 => {
let mut digest = CRC_32.digest();
@ -290,6 +303,29 @@ pub mod stdmod {
}
}
}
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)]
@ -297,8 +333,13 @@ 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]
@ -312,11 +353,10 @@ mod tests {
assert!(path.exists());
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_basic_native_fs_exists() {
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()));
@ -325,7 +365,32 @@ mod tests {
.unwrap();
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_basic_native_fs_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]
@ -345,7 +410,6 @@ mod tests {
.expect("writing to file failed");
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
assert_eq!(read_back, write_data);
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
@ -365,6 +429,342 @@ mod tests {
.expect("writing to file failed");
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
assert_eq!(read_back, write_data);
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
fn test_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,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

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

@ -24,7 +24,6 @@ extern crate std;
#[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;

File diff suppressed because it is too large Load Diff

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)], false);
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::PusSchedulerProvider;
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,
pub struct PusService11SchedHandler<
TcInMemConverter: EcssTcInMemConverter,
PusScheduler: PusSchedulerProvider,
> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
scheduler: PusScheduler,
}
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: PusSchedulerProvider>
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, PusSchedulerProvider, 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)], false);
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 PusSchedulerProvider 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, &[], 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, WritablePusPacket};
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_no_app_data(&mut sp_header, sec_header, 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 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,12 +28,12 @@
//! 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)], false);
//! 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));
//!
@ -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");
@ -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>),
@ -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;
@ -1447,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)
}
@ -1475,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);
@ -1485,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)], false));
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);
}
@ -1505,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);
}
@ -1546,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);
}
@ -1605,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];
@ -1615,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 {
@ -1681,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);
}
@ -1778,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);
}
@ -1818,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);
@ -2122,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);
}
@ -2140,24 +2127,23 @@ 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);
}
#[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)], false);
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));
@ -2186,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

@ -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::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_written())?;
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)
}
}
@ -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,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)], false);
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,12 +1,14 @@
#[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, WritablePusPacket};
@ -33,13 +35,14 @@ 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)], false);
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();
@ -56,15 +59,21 @@ pub mod crossbeam_test {
let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
req_id_0 = RequestId::new(&pus_tc_0);
let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_0.write_to_bytes(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_no_app_data(&mut sph, tc_header, true);
req_id_1 = RequestId::new(&pus_tc_1);
let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_1.write_to_bytes(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,9 +85,7 @@ 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();
@ -114,9 +121,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);
@ -142,13 +147,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");

View File

@ -26,3 +26,7 @@ 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.
# <a id="tmtccmd"></a> 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,31 +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.
# Structure of the example project
The example project contains components which could also be expected to be part of a production
On-Board Software.
1. A UDP and TCP server to receive telecommands and poll telemetry from. This might be an optional
component for an OBSW which is only used during the development phase on ground. The TCP
server parses space packets by using the CCSDS space packet ID as the packet start delimiter.
2. A PUS service stack which exposes some functionality conformant with the ECSS PUS service. This
currently includes the following services:
- Service 1 for telecommand verification.
- Service 3 for housekeeping telemetry handling.
- Service 5 for management and downlink of on-board events.
- Service 8 for handling on-board actions.
- Service 11 for scheduling telecommands to be released at a specific time.
- Service 17 for test purposes (pings)
3. An event manager component which handles the event IPC mechanism.
4. A TC source component which demultiplexes and routes telecommands based on parameters like
packet APID or PUS service and subservice type.
5. A TM sink sink component which is the target of all sent telemetry and sends it to downlink
handlers like the UDP and TCP server.
6. An AOCS example task which can also process some PUS commands.
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

@ -44,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.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
@ -39,12 +41,11 @@ from tmtccmd.tmtc 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, 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=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())
@ -129,8 +130,7 @@ class PusHandler(SpecificApidHandlerBase):
raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:]
_LOGGER.info(json_str)
dedicated_handler = True
if service == 5:
elif service == 5:
tm_packet = PusTelemetry.unpack(
packet, time_reader=CdsShortTimestamp.empty()
)
@ -139,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]")
@ -154,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):
@ -196,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.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 == 7.0.0
tmtccmd == 8.0.0rc1
# -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd

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

@ -5,7 +5,7 @@ use satrs_core::{
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,15 +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;
#[derive(Clone)]
pub struct CcsdsReceiver {
pub tc_source: PusTcSource,
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]

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

@ -0,0 +1,151 @@
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),
],
true,
)),
StaticMemoryPool::new(StaticPoolConfig::new(
vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
],
true,
)),
)
}
pub fn create_sched_tc_pool() -> StaticMemoryPool {
StaticMemoryPool::new(StaticPoolConfig::new(
vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
],
true,
))
}
}
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,17 +1,12 @@
use derive_new::new;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs_core::events::{EventU32TypedSev, SeverityInfo};
use satrs_core::objects::ObjectId;
use satrs_core::spacepackets::ecss::tc::IsPusTelecommand;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::spacepackets::{ByteConversionError, CcsdsPacket};
use satrs_core::tmtc::TargetId;
use std::fmt;
use std::net::Ipv4Addr;
use thiserror::Error;
use satrs_mib::res_code::{ResultU16, ResultU16Info};
use satrs_mib::resultcode;
pub mod config;
pub type Apid = u16;
@ -62,96 +57,3 @@ impl TargetIdWithApid {
})
}
}
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;
pub const ACS_OBJECT_ID: ObjectId = ObjectId {
id: RequestTargetId::AcsSubsystem as u32,
name: "ACS_SUBSYSTEM",
};
#[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,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TcReceiverId {
PusTest = 1,
PusEvent = 2,
PusHk = 3,
PusAction = 4,
PusSched = 5,
}

View File

@ -1,158 +1,117 @@
mod acs;
mod ccsds;
mod events;
mod hk;
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::acs::AcsTask;
use crate::ccsds::CcsdsReceiver;
use crate::hk::{AcsHkIds, HkUniqueId};
use crate::logger::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::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::{Request, RequestWithToken};
use crate::requests::RequestWithToken;
use crate::tcp::{SyncTcpTmSource, TcpTask};
use crate::tmtc::{PusTcSource, TcArgs, TcStore, TmArgs, TmFunnel, TmtcTask};
use crate::udp::UdpTmtcServer;
use satrs_core::event_man::{
EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider, SendEventProvider,
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 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, TargetIdWithApid, 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>();
let target_apid = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
request_map.insert(target_apid, 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_udp_server_rx: 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();
@ -166,129 +125,101 @@ 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,
);
let ccsds_receiver = CcsdsReceiver {
tc_source: tc_args.tc_source.clone(),
};
let mut tmtc_task = TmtcTask::new(tc_args, PusReceiver::new(verif_reporter, pus_router));
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_rx: tm_args.tm_udp_server_rx,
tm_store: tm_args.tm_store.clone_backing_pool(),
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())
@ -297,20 +228,11 @@ fn main() {
loop {
udp_tmtc_server.periodic_operation();
tmtc_task.periodic_operation();
thread::sleep(Duration::from_millis(400));
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
let tcp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
let tcp_server_cfg = ServerConfig::new(sock_addr, Duration::from_millis(400), 4096, 8192);
let mut sync_tm_tcp_source = SyncTcpTmSource::new(200);
let mut tcp_server = TcpTask::new(
tcp_server_cfg,
sync_tm_tcp_source.clone(),
tcp_ccsds_distributor,
)
.expect("tcp server creation failed");
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
@ -323,225 +245,271 @@ fn main() {
.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");
sync_tm_tcp_source.add_tm(tm_raw);
}
}
.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 {
// TODO: Move this into a separate function/task/module..
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) => {
// TODO: We should check whether the unique ID is even valid.
let target = request.targeted_request.target_id;
assert_eq!(
target.target_id(),
RequestTargetId::AcsSubsystem as u32
);
if request.targeted_request.target_id.target
== 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 hk_id = HkUniqueId::new(target.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,
);
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();
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh1.join().expect("Joining TM Funnel thread failed");
jh2.join().expect("Joining Event Manager thread failed");
jh3.join().expect("Joining AOCS thread failed");
jh4.join().expect("Joining PUS handler 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,47 +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_example::{tmtc_err, TargetIdWithApid};
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,
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 }
}
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 PusService8ActionHandler {
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,
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>,
@ -50,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(
@ -79,7 +139,8 @@ impl PusService8ActionHandler {
} else {
let mut fail_data: [u8; 4] = [0; 4];
fail_data.copy_from_slice(&target_id.target.to_be_bytes());
self.psb()
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,72 +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_example::{hk_err, tmtc_err, TargetIdWithApid};
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,
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 }
}
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 PusService3HkHandler {
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,
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");
@ -81,9 +130,13 @@ 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(),
@ -93,10 +146,11 @@ impl PusServiceHandler for PusService3HkHandler {
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");
@ -107,7 +161,11 @@ impl PusServiceHandler for PusService3HkHandler {
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 {
@ -119,10 +177,11 @@ impl PusServiceHandler for PusService3HkHandler {
} 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,
@ -146,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

@ -22,7 +22,7 @@ pub enum Request {
#[derive(Clone, Eq, PartialEq, Debug, new)]
pub struct TargetedRequest {
pub(crate) target_id: TargetIdWithApid,
pub(crate) target_id_with_apid: TargetIdWithApid,
pub(crate) request: Request,
}

View File

@ -9,9 +9,7 @@ use satrs_core::{
spacepackets::PacketId,
tmtc::{CcsdsDistributor, CcsdsError, TmPacketSourceCore},
};
use satrs_example::PUS_APID;
use crate::tmtc::MpscStoreAndSendError;
use satrs_example::config::PUS_APID;
pub const PACKET_ID_LOOKUP: &[PacketId] = &[PacketId::const_tc(true, PUS_APID)];
@ -71,20 +69,20 @@ impl TmPacketSourceCore for SyncTcpTmSource {
}
}
pub struct TcpTask {
pub struct TcpTask<MpscErrorType: 'static> {
server: TcpSpacepacketsServer<
(),
CcsdsError<MpscStoreAndSendError>,
CcsdsError<MpscErrorType>,
SyncTcpTmSource,
CcsdsDistributor<MpscStoreAndSendError>,
CcsdsDistributor<MpscErrorType>,
>,
}
impl TcpTask {
impl<MpscErrorType: 'static + core::fmt::Debug> TcpTask<MpscErrorType> {
pub fn new(
cfg: ServerConfig,
tm_source: SyncTcpTmSource,
tc_receiver: CcsdsDistributor<MpscStoreAndSendError>,
tc_receiver: CcsdsDistributor<MpscErrorType>,
) -> Result<Self, std::io::Error> {
Ok(Self {
server: TcpSpacepacketsServer::new(

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,85 +1,68 @@
use log::warn;
use satrs_core::pus::ReceivesEcssPusTc;
use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc};
use satrs_core::spacepackets::SpHeader;
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
use std::sync::mpsc::{self, Receiver, SendError, Sender, TryRecvError};
use thiserror::Error;
use crate::pus::PusReceiver;
use satrs_core::pool::{SharedPool, StoreAddr, StoreError};
use satrs_core::pus::TcAddrWithToken;
use satrs_core::pool::{PoolProvider, SharedStaticMemoryPool, StoreAddr, StoreError};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket;
use satrs_core::tmtc::tm_helper::SharedTmStore;
use satrs_core::tmtc::ReceivesCcsdsTc;
pub struct TmArgs {
pub tm_store: SharedTmStore,
pub tm_sink_sender: Sender<StoreAddr>,
pub tm_udp_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)
}
}
#[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>,
}
#[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)?;
@ -87,43 +70,73 @@ impl ReceivesCcsdsTc for PusTcSource {
}
}
pub struct TmtcTask {
tc_args: TcArgs,
// Newtype, can not implement necessary traits on MPSC sender directly because of orphan rules.
#[derive(Clone)]
pub struct PusTcSourceProviderDynamic(pub Sender<Vec<u8>>);
impl ReceivesEcssPusTc for PusTcSourceProviderDynamic {
type Error = SendError<Vec<u8>>;
fn pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
self.0.send(pus_tc.raw_data().to_vec())?;
Ok(())
}
}
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 TmtcTask {
pub fn new(tc_args: TcArgs, pus_receiver: PusReceiver) -> Self {
impl TcSourceTaskStatic {
pub fn new(
shared_tc_pool: SharedTcPool,
tc_receiver: Receiver<StoreAddr>,
pus_receiver: PusReceiver,
) -> Self {
Self {
tc_args,
shared_tc_pool,
tc_receiver,
tc_buf: [0; 4096],
pus_receiver,
}
}
pub fn periodic_operation(&mut self) {
//while self.poll_tc() {}
self.poll_tc();
}
pub fn poll_tc(&mut self) -> bool {
match self.tc_args.tc_receiver.try_recv() {
match self.tc_receiver.try_recv() {
Ok(addr) => {
let pool = self
.tc_args
.tc_source
.tc_store
.shared_tc_pool
.pool
.read()
.expect("locking tc pool failed");
let data = pool.read(&addr).expect("reading pool failed");
self.tc_buf[0..data.len()].copy_from_slice(data);
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(addr, pus_tc.service(), &pus_tc)
.handle_tc_packet(
satrs_core::pus::TcInMemory::StoreAddr(addr),
pus_tc.service(),
&pus_tc,
)
.ok();
true
}
@ -144,3 +157,51 @@ impl TmtcTask {
}
}
}
// 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, _)) => {
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: {:x?}", tc);
true
}
},
Err(e) => match e {
TryRecvError::Empty => false,
TryRecvError::Disconnected => {
warn!("tmtc thread: sender disconnected");
false
}
},
}
}
}

View File

@ -1,24 +1,83 @@
use std::{net::SocketAddr, sync::mpsc::Receiver};
use std::{
net::{SocketAddr, UdpSocket},
sync::mpsc::Receiver,
};
use log::{info, warn};
use satrs_core::{
hal::std::udp_server::{ReceiveResult, UdpTcServer},
pool::{SharedPool, StoreAddr},
pool::{PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr},
tmtc::CcsdsError,
};
use crate::tmtc::MpscStoreAndSendError;
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
pub tm_rx: Receiver<StoreAddr>,
pub tm_store: SharedPool,
pub trait UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
impl UdpTmtcServer {
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.send_tm_to_udp_client(&recv_addr);
self.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
}
}
@ -32,7 +91,7 @@ impl UdpTmtcServer {
true
}
CcsdsError::CustomError(e) => {
warn!("mpsc store and send error {e:?}");
warn!("mpsc custom error {e:?}");
true
}
},
@ -44,33 +103,113 @@ impl UdpTmtcServer {
},
}
}
}
fn send_tm_to_udp_client(&mut self, recv_addr: &SocketAddr) {
while let Ok(addr) = self.tm_rx.try_recv() {
let store_lock = self.tm_store.write();
if store_lock.is_err() {
warn!("Locking TM store failed");
continue;
}
let mut store_lock = store_lock.unwrap();
let pg = store_lock.read_with_guard(addr);
let read_res = pg.read();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
if buf.len() > 9 {
let service = buf[7];
let subservice = buf[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = self.udp_tc_server.socket.send_to(buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
#[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);
}
}
}