24 Commits

Author SHA1 Message Date
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
44 changed files with 5857 additions and 904 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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: 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.2"
edition = "2021"
rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
@ -15,8 +15,6 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
[dependencies]
delegate = ">0.7, <=0.10"
paste = "1"
# TODO: Remove this as soon as the image including the description was moved to the satrs-book.
embed-doc-image = "0.1"
[dependencies.smallvec]
version = "1"
@ -73,10 +71,11 @@ features = ["all"]
optional = true
[dependencies.spacepackets]
version = "0.7.0"
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"

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

@ -318,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);
@ -378,7 +399,7 @@ pub mod std_mod {
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;
@ -420,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
}
@ -437,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
@ -446,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) => {
@ -458,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 {
@ -561,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>,
}
@ -569,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 {
@ -930,15 +951,15 @@ pub mod tests {
StoreAddr,
};
use crate::pus::verification::RequestId;
use crate::tmtc::tm_helper::SharedTmStore;
use crate::tmtc::tm_helper::SharedTmPool;
use super::verification::{
TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
};
use super::{
EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver,
MpscTmAsVecSender, MpscTmInStoreSender, PusPacketHandlerResult, PusPacketHandlingError,
PusServiceHelper, TcInMemory,
MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceHelper, TcInMemory,
};
pub const TEST_APID: u16 = 0x101;
@ -982,7 +1003,7 @@ pub mod tests {
pus_buf: [u8; 2048],
tm_buf: [u8; 2048],
tc_pool: SharedStaticMemoryPool,
tm_pool: SharedTmStore,
tm_pool: SharedTmPool,
tc_sender: mpsc::Sender<EcssTcAndToken>,
tm_receiver: mpsc::Receiver<StoreAddr>,
verification_handler: VerificationReporterWithSender,
@ -998,17 +1019,21 @@ pub mod tests {
let tc_pool = StaticMemoryPool::new(pool_cfg.clone());
let tm_pool = StaticMemoryPool::new(pool_cfg);
let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool));
let shared_tm_pool = SharedTmStore::new(tm_pool);
let 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 =
MpscTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tm_tx.clone());
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 =
MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx);
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);
@ -1052,7 +1077,7 @@ pub mod tests {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
let tm_addr = next_msg.unwrap();
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_pool = self.tm_pool.0.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
self.tm_buf[0..tm_raw.len()].copy_from_slice(tm_raw);
PusTmReader::new(&self.tm_buf, 7).unwrap().0
@ -1070,7 +1095,7 @@ pub mod tests {
let next_msg = self.tm_receiver.try_recv();
assert!(next_msg.is_ok());
let tm_addr = next_msg.unwrap();
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_pool = self.tm_pool.0.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
let tm = PusTmReader::new(tm_raw, 7).unwrap().0;
assert_eq!(PusPacket::service(&tm), 1);

View File

@ -8,9 +8,11 @@ use core::time::Duration;
use serde::{Deserialize, Serialize};
use spacepackets::ecss::scheduling::TimeWindowType;
use spacepackets::ecss::tc::{GenericPusTcSecondaryHeader, IsPusTelecommand, PusTcReader};
use spacepackets::ecss::{PusError, PusPacket};
use spacepackets::time::{CcsdsTimeProvider, TimeReader, TimestampError, UnixTimestamp};
use spacepackets::CcsdsPacket;
use spacepackets::ecss::{PusError, PusPacket, WritablePusPacket};
use spacepackets::time::{
CcsdsTimeProvider, TimeReader, TimeWriter, TimestampError, UnixTimestamp,
};
use spacepackets::{ByteConversionError, CcsdsPacket};
#[cfg(feature = "std")]
use std::error::Error;
@ -154,8 +156,9 @@ pub enum ScheduleError {
StoreError(StoreError),
TcDataEmpty,
TimestampError(TimestampError),
WrongSubservice,
WrongService,
WrongSubservice(u8),
WrongService(u8),
ByteConversionError(ByteConversionError),
}
impl Display for ScheduleError {
@ -171,26 +174,29 @@ impl Display for ScheduleError {
} => {
write!(
f,
"Error: time margin too short, current time: {current_time:?}, time margin: {time_margin:?}, release time: {release_time:?}"
"time margin too short, current time: {current_time:?}, time margin: {time_margin:?}, release time: {release_time:?}"
)
}
ScheduleError::NestedScheduledTc => {
write!(f, "Error: nested scheduling is not allowed")
write!(f, "nested scheduling is not allowed")
}
ScheduleError::StoreError(e) => {
write!(f, "Store Error: {e}")
write!(f, "pus scheduling: {e}")
}
ScheduleError::TcDataEmpty => {
write!(f, "Error: empty Tc Data field")
write!(f, "empty TC data field")
}
ScheduleError::TimestampError(e) => {
write!(f, "Timestamp Error: {e}")
write!(f, "pus scheduling: {e}")
}
ScheduleError::WrongService => {
write!(f, "Error: Service not 11.")
ScheduleError::WrongService(srv) => {
write!(f, "pus scheduling: wrong service number {srv}")
}
ScheduleError::WrongSubservice => {
write!(f, "Error: Subservice not 4.")
ScheduleError::WrongSubservice(subsrv) => {
write!(f, "pus scheduling: wrong subservice number {subsrv}")
}
ScheduleError::ByteConversionError(e) => {
write!(f, "pus scheduling: {e}")
}
}
}
@ -198,24 +204,39 @@ impl Display for ScheduleError {
impl From<PusError> for ScheduleError {
fn from(e: PusError) -> Self {
ScheduleError::PusError(e)
Self::PusError(e)
}
}
impl From<StoreError> for ScheduleError {
fn from(e: StoreError) -> Self {
ScheduleError::StoreError(e)
Self::StoreError(e)
}
}
impl From<TimestampError> for ScheduleError {
fn from(e: TimestampError) -> Self {
ScheduleError::TimestampError(e)
Self::TimestampError(e)
}
}
impl From<ByteConversionError> for ScheduleError {
fn from(e: ByteConversionError) -> Self {
Self::ByteConversionError(e)
}
}
#[cfg(feature = "std")]
impl Error for ScheduleError {}
impl Error for ScheduleError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ScheduleError::PusError(e) => Some(e),
ScheduleError::StoreError(e) => Some(e),
ScheduleError::TimestampError(e) => Some(e),
ScheduleError::ByteConversionError(e) => Some(e),
_ => None,
}
}
}
pub trait PusSchedulerInterface {
type TimeProvider: CcsdsTimeProvider + TimeReader;
@ -249,10 +270,12 @@ pub trait PusSchedulerInterface {
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
if PusPacket::service(pus_tc) != 11 {
return Err(ScheduleError::WrongService);
return Err(ScheduleError::WrongService(PusPacket::service(pus_tc)));
}
if PusPacket::subservice(pus_tc) != 4 {
return Err(ScheduleError::WrongSubservice);
return Err(ScheduleError::WrongSubservice(PusPacket::subservice(
pus_tc,
)));
}
if pus_tc.user_data().is_empty() {
return Err(ScheduleError::TcDataEmpty);
@ -289,6 +312,34 @@ pub trait PusSchedulerInterface {
}
}
/// Helper function to generate the application data for a PUS telecommand to insert an
/// activity into a time-based schedule according to ECSS-E-ST-70-41C 8.11.2.4
///
/// Please note that the N field is set to a [u16] unsigned bytefield with the value 1.
pub fn generate_insert_telecommand_app_data(
buf: &mut [u8],
release_time: &impl TimeWriter,
request: &impl WritablePusPacket,
) -> Result<usize, ScheduleError> {
let required_len = 2 + release_time.len_written() + request.len_written();
if required_len > buf.len() {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: required_len,
}
.into());
}
let mut current_len = 0;
let n = 1_u16;
buf[current_len..current_len + 2].copy_from_slice(&n.to_be_bytes());
current_len += 2;
current_len += release_time
.write_to_bytes(&mut buf[current_len..current_len + release_time.len_written()])?;
current_len +=
request.write_to_bytes(&mut buf[current_len..current_len + request.len_written()])?;
Ok(current_len)
}
#[cfg(feature = "alloc")]
pub mod alloc_mod {
use super::*;
@ -307,6 +358,19 @@ pub mod alloc_mod {
#[cfg(feature = "std")]
use std::time::SystemTimeError;
/// This function is similar to [generate_insert_telecommand_app_data] but returns the application
/// data as a [alloc::vec::Vec].
pub fn generate_insert_telecommand_app_data_as_vec(
release_time: &impl TimeWriter,
request: &impl WritablePusPacket,
) -> Result<alloc::vec::Vec<u8>, ScheduleError> {
let mut vec = alloc::vec::Vec::new();
vec.extend_from_slice(&1_u16.to_be_bytes());
vec.append(&mut release_time.to_vec()?);
vec.append(&mut request.to_vec()?);
Ok(vec)
}
enum DeletionResult {
WithoutStoreDeletion(Option<StoreAddr>),
WithStoreDeletion(Result<bool, StoreError>),
@ -757,7 +821,7 @@ mod tests {
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
use spacepackets::ecss::WritablePusPacket;
use spacepackets::time::{cds, TimeWriter, UnixTimestamp};
use spacepackets::SpHeader;
use spacepackets::{PacketId, PacketSequenceCtrl, PacketType, SequenceFlags, SpHeader};
use std::time::Duration;
use std::vec::Vec;
#[allow(unused_imports)]
@ -835,7 +899,7 @@ mod tests {
}
#[test]
fn basic() {
fn test_enable_api() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
assert!(scheduler.is_enabled());
@ -846,7 +910,7 @@ mod tests {
}
#[test]
fn reset() {
fn test_reset() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
@ -857,7 +921,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(100),
TcInfo::new(tc_info_0.addr.clone(), tc_info_0.request_id),
TcInfo::new(tc_info_0.addr, tc_info_0.request_id),
)
.unwrap();
@ -866,7 +930,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(200),
TcInfo::new(tc_info_1.addr.clone(), tc_info_1.request_id),
TcInfo::new(tc_info_1.addr, tc_info_1.request_id),
)
.unwrap();
@ -875,7 +939,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(300),
TcInfo::new(tc_info_2.addr().clone(), tc_info_2.request_id()),
TcInfo::new(tc_info_2.addr(), tc_info_2.request_id()),
)
.unwrap();
@ -950,7 +1014,7 @@ mod tests {
}
#[test]
fn time() {
fn test_time_update() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let time = UnixTimestamp::new(1, 2).unwrap();
@ -964,7 +1028,7 @@ mod tests {
expected_store_addrs: Vec<StoreAddr>,
counter: &mut usize,
) {
assert_eq!(enabled, true);
assert!(enabled);
assert!(expected_store_addrs.contains(store_addr));
*counter += 1;
}
@ -974,13 +1038,13 @@ mod tests {
expected_store_addrs: Vec<StoreAddr>,
counter: &mut usize,
) {
assert_eq!(enabled, false);
assert!(!enabled);
assert!(expected_store_addrs.contains(store_addr));
*counter += 1;
}
#[test]
fn request_id() {
fn test_request_id() {
let src_id_to_set = 12;
let apid_to_set = 0x22;
let seq_count = 105;
@ -998,7 +1062,7 @@ mod tests {
);
}
#[test]
fn release_basic() {
fn test_release_telecommands() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
@ -1290,7 +1354,9 @@ mod tests {
assert!(err.is_err());
let err = err.unwrap_err();
match err {
ScheduleError::WrongService => {}
ScheduleError::WrongService(wrong_service) => {
assert_eq!(wrong_service, 12);
}
_ => {
panic!("unexpected error")
}
@ -1311,7 +1377,9 @@ mod tests {
assert!(err.is_err());
let err = err.unwrap_err();
match err {
ScheduleError::WrongSubservice => {}
ScheduleError::WrongSubservice(wrong_subsrv) => {
assert_eq!(wrong_subsrv, 5);
}
_ => {
panic!("unexpected error")
}
@ -1487,7 +1555,7 @@ mod tests {
let del_res =
scheduler.delete_by_request_id_and_from_pool(&tc_info_0.request_id(), &mut pool);
assert!(del_res.is_ok());
assert_eq!(del_res.unwrap(), true);
assert!(del_res.unwrap());
assert!(!pool.has_element_at(&tc_info_0.addr()).unwrap());
assert_eq!(scheduler.num_scheduled_telecommands(), 0);
}
@ -1804,4 +1872,109 @@ mod tests {
assert!(!pool.has_element_at(&cmd_1_to_delete.addr()).unwrap());
assert!(pool.has_element_at(&cmd_out_of_range_1.addr()).unwrap());
}
#[test]
fn test_release_without_deletion() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut buf: [u8; 32] = [0; 32];
let tc_info_0 = ping_tc_to_store(&mut pool, &mut buf, 0, None);
scheduler
.insert_unwrapped_and_stored_tc(UnixTimestamp::new_only_seconds(100), tc_info_0)
.expect("insertion failed");
let tc_info_1 = ping_tc_to_store(&mut pool, &mut buf, 1, None);
scheduler
.insert_unwrapped_and_stored_tc(UnixTimestamp::new_only_seconds(200), tc_info_1)
.expect("insertion failed");
let mut i = 0;
let mut test_closure_1 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| {
common_check(
boolvar,
&tc_info.addr,
vec![tc_info_0.addr(), tc_info_1.addr()],
&mut i,
);
};
scheduler.update_time(UnixTimestamp::new_only_seconds(205));
let tc_info_vec = scheduler
.release_telecommands_no_deletion(&mut test_closure_1, &pool)
.expect("deletion failed");
assert_eq!(tc_info_vec[0], tc_info_0);
assert_eq!(tc_info_vec[1], tc_info_1);
}
#[test]
fn test_generic_insert_app_data_test() {
let time_writer = cds::TimeProvider::new_with_u16_days(1, 1);
let mut sph = SpHeader::new(
PacketId::const_new(PacketType::Tc, true, 0x002),
PacketSequenceCtrl::const_new(SequenceFlags::Unsegmented, 5),
0,
);
let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new_no_app_data(&mut sph, sec_header, true);
let mut buf: [u8; 64] = [0; 64];
let result = generate_insert_telecommand_app_data(&mut buf, &time_writer, &ping_tc);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2 + 7 + ping_tc.len_written());
let n = u16::from_be_bytes(buf[0..2].try_into().unwrap());
assert_eq!(n, 1);
let time_reader = cds::TimeProvider::from_bytes_with_u16_days(&buf[2..2 + 7]).unwrap();
assert_eq!(time_reader, time_writer);
let pus_tc_reader = PusTcReader::new(&buf[9..]).unwrap().0;
assert_eq!(pus_tc_reader, ping_tc);
}
#[test]
fn test_generic_insert_app_data_test_byte_conv_error() {
let time_writer = cds::TimeProvider::new_with_u16_days(1, 1);
let mut sph = SpHeader::new(
PacketId::const_new(PacketType::Tc, true, 0x002),
PacketSequenceCtrl::const_new(SequenceFlags::Unsegmented, 5),
0,
);
let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new_no_app_data(&mut sph, sec_header, true);
let mut buf: [u8; 16] = [0; 16];
let result = generate_insert_telecommand_app_data(&mut buf, &time_writer, &ping_tc);
assert!(result.is_err());
let error = result.unwrap_err();
if let ScheduleError::ByteConversionError(ByteConversionError::ToSliceTooSmall {
found,
expected,
}) = error
{
assert_eq!(found, 16);
assert_eq!(
expected,
2 + time_writer.len_written() + ping_tc.len_written()
);
} else {
panic!("unexpected error {error}")
}
}
#[test]
fn test_generic_insert_app_data_test_as_vec() {
let time_writer = cds::TimeProvider::new_with_u16_days(1, 1);
let mut sph = SpHeader::new(
PacketId::const_new(PacketType::Tc, true, 0x002),
PacketSequenceCtrl::const_new(SequenceFlags::Unsegmented, 5),
0,
);
let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
let ping_tc = PusTcCreator::new_no_app_data(&mut sph, sec_header, true);
let mut buf: [u8; 64] = [0; 64];
generate_insert_telecommand_app_data(&mut buf, &time_writer, &ping_tc).unwrap();
let vec = generate_insert_telecommand_app_data_as_vec(&time_writer, &ping_tc)
.expect("vec generation failed");
assert_eq!(&buf[..vec.len()], vec);
}
}

View File

@ -18,8 +18,8 @@
//! use satrs_core::pool::{PoolProviderMemInPlaceWithGuards, 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};
@ -30,10 +30,10 @@
//!
//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
//! let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
//! let shared_tm_store = SharedTmStore::new(tm_pool);
//! 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));
//!
@ -1332,8 +1332,8 @@ mod tests {
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;
@ -1487,9 +1487,10 @@ mod tests {
#[test]
fn test_mpsc_verif_send_sync() {
let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)]));
let shared_tm_store = SharedTmStore::new(pool);
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);
}
@ -2142,10 +2143,11 @@ mod tests {
fn test_seq_count_increment() {
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
let shared_tm_store = SharedTmStore::new(tm_pool);
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));

View File

@ -17,26 +17,26 @@ pub mod std_mod {
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct SharedTmStore {
pub shared_pool: SharedStaticMemoryPool,
}
pub struct SharedTmPool(pub SharedStaticMemoryPool);
impl SharedTmStore {
impl SharedTmPool {
pub fn new(shared_pool: StaticMemoryPool) -> Self {
Self {
shared_pool: Arc::new(RwLock::new(shared_pool)),
}
Self(Arc::new(RwLock::new(shared_pool)))
}
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.shared_pool.clone()
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
.shared_pool
.write()
.map_err(|_| EcssTmtcError::StoreLock)?;
let mut pg = self.0.write().map_err(|_| EcssTmtcError::StoreLock)?;
let (addr, buf) = pg.free_element(pus_tm.len_written())?;
pus_tm
.write_to_bytes(buf)

View File

@ -1,4 +1,4 @@
#[cfg(feature = "crossbeam")]
//#[cfg(feature = "crossbeam")]
pub mod crossbeam_test {
use hashbrown::HashMap;
use satrs_core::pool::{
@ -9,7 +9,7 @@ pub mod crossbeam_test {
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};
@ -37,12 +37,12 @@ pub mod crossbeam_test {
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
// Shared pool object to store the verification PUS telemetry
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let shared_tm_store = SharedTmStore::new(StaticMemoryPool::new(pool_cfg.clone()));
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();
@ -145,7 +145,7 @@ 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);

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.

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]

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

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

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

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

View File

@ -1,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,168 +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::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig};
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::{
EcssTcInSharedStoreConverter, MpscTcReceiver, MpscTmInStoreSender, PusServiceHelper,
};
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 = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let shared_tm_store = SharedTmStore::new(tm_pool);
let tm_store_event = shared_tm_store.clone();
let tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let tc_store = TcStore {
pool: Arc::new(RwLock::new(tc_pool)),
};
let sched_tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
]));
let seq_count_provider = CcsdsSimpleSeqCountProvider::new();
let 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();
@ -176,134 +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 = MpscTcReceiver::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(PusServiceHelper::new(
Box::new(test_srv_receiver),
Box::new(test_srv_tm_sender),
PUS_APID,
let pus_scheduler_service = create_scheduler_service_static(
shared_tm_pool.clone(),
tm_funnel_tx.clone(),
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
));
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(),
tm_funnel_tx.clone(),
);
let sched_srv_receiver = MpscTcReceiver::new(
TcReceiverId::PusSched as ChannelId,
"PUS_11_TC_RECV",
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(
PusServiceHelper::new(
Box::new(sched_srv_receiver),
Box::new(sched_srv_tm_sender),
PUS_APID,
verif_reporter.clone(),
EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048),
),
scheduler,
);
let mut pus_11_wrapper = Pus11Wrapper {
pus_11_handler,
sched_tc_pool,
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 = MpscTcReceiver::new(
TcReceiverId::PusEvent as ChannelId,
"PUS_5_TC_RECV",
verif_reporter.clone(),
shared_tc_pool.pool.clone(),
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_store.pool.clone(), 2048),
),
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 = 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_store.pool.clone(), 2048),
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 =
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_store.pool.clone(), 2048),
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())
@ -312,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())
@ -338,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,17 +1,79 @@
use crate::requests::{ActionRequest, Request, RequestWithToken};
use log::{error, warn};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::verification::{
FailParams, TcStateAccepted, VerificationReporterWithSender, VerificationToken,
};
use satrs_core::pus::{
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
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 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>,
@ -141,11 +203,11 @@ impl<TcInMemConverter: EcssTcInMemConverter> PusService8ActionHandler<TcInMemCon
}
}
pub struct Pus8Wrapper {
pub(crate) pus_8_handler: PusService8ActionHandler<EcssTcInSharedStoreConverter>,
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_one_tc() {
Ok(result) => match result {

View File

@ -1,12 +1,85 @@
use log::{error, warn};
use satrs_core::pus::event_srv::PusService5EventHandler;
use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult};
use std::sync::mpsc;
pub struct Pus5Wrapper {
pub pus_5_handler: PusService5EventHandler<EcssTcInSharedStoreConverter>,
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_one_tc() {
Ok(result) => match result {

View File

@ -1,15 +1,73 @@
use crate::requests::{Request, RequestWithToken};
use log::{error, warn};
use satrs_core::hk::{CollectionIntervalFactor, HkRequest};
use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, VerificationReporterWithSender,
};
use satrs_core::pus::{
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
};
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 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>,
@ -147,11 +205,11 @@ impl<TcInMemConverter: EcssTcInMemConverter> PusService3HkHandler<TcInMemConvert
}
}
pub struct Pus3Wrapper {
pub(crate) pus_3_handler: PusService3HkHandler<EcssTcInSharedStoreConverter>,
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_one_tc() {
Ok(result) => match result {

View File

@ -6,13 +6,14 @@ 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 {

View File

@ -1,36 +1,66 @@
use crate::tmtc::PusTcSource;
use std::sync::mpsc;
use std::time::Duration;
use log::{error, info, warn};
use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool};
use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StoreAddr};
use satrs_core::pus::scheduler::{PusScheduler, TcInfo};
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult};
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 struct Pus11Wrapper {
pub pus_11_handler: PusService11SchedHandler<EcssTcInSharedStoreConverter, PusScheduler>,
pub sched_tc_pool: StaticMemoryPool,
pub tc_source_wrapper: PusTcSource,
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, tc: &[u8]| -> bool {
if enabled {
// Transfer TC from scheduler TC pool to shared TC pool.
let released_tc_addr = self
.tc_source_wrapper
.tc_store
.pool
.write()
.expect("locking pool failed")
.add(tc)
.expect("adding TC to shared pool failed");
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
}
}
self.tc_source_wrapper
.tc_source
.send(released_tc_addr)
.expect("sending TC to TC source failed");
}
true
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 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
@ -71,3 +101,77 @@ 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,
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,
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,22 +1,89 @@
use log::{info, warn};
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::{EcssTcInMemConverter, PusPacketHandlerResult};
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_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::ChannelId;
use satrs_core::{events::EventU32, pus::EcssTcInSharedStoreConverter};
use satrs_example::{tmtc_err, TEST_EVENT};
use std::sync::mpsc::Sender;
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<EcssTcInSharedStoreConverter>,
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_one_tc();
if res.is_err() {

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,145 @@
use std::{
collections::HashMap,
sync::mpsc::{Receiver, Sender},
};
use satrs_core::{
pool::{PoolProviderMemInPlace, 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;
}
// This operation has to come last!
zero_copy_writer.finish();
}
}
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 tm_raw = pool_guard
.modify(&addr)
.expect("Reading TM from pool failed");
let zero_copy_writer = PusTmZeroCopyWriter::new(tm_raw, MIN_CDS_FIELD_LEN)
.expect("Creating TM zero copy writer failed");
self.common.apply_packet_processing(zero_copy_writer);
self.tm_server_tx
.send(addr)
.expect("Sending TM to server failed");
self.common.sync_tm_tcp_source.add_tm(tm_raw);
}
}
}
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,34 +1,15 @@
use log::warn;
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::{PoolProviderMemInPlace, 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}")]
@ -40,11 +21,11 @@ pub enum MpscStoreAndSendError {
}
#[derive(Clone)]
pub struct TcStore {
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())?;
@ -53,32 +34,34 @@ impl TcStore {
}
}
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)?;
@ -86,16 +69,45 @@ 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,
}
@ -106,12 +118,10 @@ impl TmtcTask {
}
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");
@ -146,3 +156,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,4 +1,7 @@
use std::{net::SocketAddr, sync::mpsc::Receiver};
use std::{
net::{SocketAddr, UdpSocket},
sync::mpsc::Receiver,
};
use log::{info, warn};
use satrs_core::{
@ -7,45 +10,17 @@ use satrs_core::{
tmtc::CcsdsError,
};
use crate::tmtc::MpscStoreAndSendError;
pub trait UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
pub struct StaticUdpTmHandler {
pub tm_rx: Receiver<StoreAddr>,
pub tm_store: SharedStaticMemoryPool,
}
impl UdpTmtcServer {
pub fn periodic_operation(&mut self) {
while self.poll_tc_server() {}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.send_tm_to_udp_client(&recv_addr);
}
}
fn poll_tc_server(&mut self) -> bool {
match self.udp_tc_server.try_recv_tc() {
Ok(_) => true,
Err(e) => match e {
ReceiveResult::ReceiverError(e) => match e {
CcsdsError::ByteConversionError(e) => {
warn!("packet error: {e:?}");
true
}
CcsdsError::CustomError(e) => {
warn!("mpsc store and send error {e:?}");
true
}
},
ReceiveResult::IoError(e) => {
warn!("IO error {e}");
false
}
ReceiveResult::NothingReceived => false,
},
}
}
fn send_tm_to_udp_client(&mut self, recv_addr: &SocketAddr) {
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() {
@ -67,10 +42,181 @@ impl UdpTmtcServer {
} else {
info!("Sending PUS TM");
}
let result = self.udp_tc_server.socket.send_to(buf, recv_addr);
let result = socket.send_to(buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct DynamicUdpTmHandler {
pub tm_rx: Receiver<Vec<u8>>,
}
impl UdpTmHandler for DynamicUdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
while let Ok(tm) = self.tm_rx.try_recv() {
if tm.len() > 9 {
let service = tm[7];
let subservice = tm[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = socket.send_to(&tm, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct UdpTmtcServer<TmHandler: UdpTmHandler, SendError> {
pub udp_tc_server: UdpTcServer<CcsdsError<SendError>>,
pub tm_handler: TmHandler,
}
impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
UdpTmtcServer<TmHandler, SendError>
{
pub fn periodic_operation(&mut self) {
while self.poll_tc_server() {}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
}
}
fn poll_tc_server(&mut self) -> bool {
match self.udp_tc_server.try_recv_tc() {
Ok(_) => true,
Err(e) => match e {
ReceiveResult::ReceiverError(e) => match e {
CcsdsError::ByteConversionError(e) => {
warn!("packet error: {e:?}");
true
}
CcsdsError::CustomError(e) => {
warn!("mpsc custom error {e:?}");
true
}
},
ReceiveResult::IoError(e) => {
warn!("IO error {e}");
false
}
ReceiveResult::NothingReceived => false,
},
}
}
}
#[cfg(test)]
mod tests {
use std::{
collections::VecDeque,
net::IpAddr,
sync::{Arc, Mutex},
};
use satrs_core::{
spacepackets::{
ecss::{tc::PusTcCreator, WritablePusPacket},
SpHeader,
},
tmtc::ReceivesTcCore,
};
use satrs_example::config::{OBSW_SERVER_ADDR, PUS_APID};
use super::*;
#[derive(Default, Debug, Clone)]
pub struct TestReceiver {
tc_vec: Arc<Mutex<VecDeque<Vec<u8>>>>,
}
impl ReceivesTcCore for TestReceiver {
type Error = CcsdsError<()>;
fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
self.tc_vec.lock().unwrap().push_back(tc_raw.to_vec());
Ok(())
}
}
#[derive(Default, Debug, Clone)]
pub struct TestTmHandler {
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
}
impl UdpTmHandler for TestTmHandler {
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
}
}
#[test]
fn test_basic() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TestReceiver::default();
let tc_queue = test_receiver.tc_vec.clone();
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler,
};
udp_dyn_server.periodic_operation();
assert!(tc_queue.lock().unwrap().is_empty());
assert!(tm_handler_calls.lock().unwrap().is_empty());
}
#[test]
fn test_transactions() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TestReceiver::default();
let tc_queue = test_receiver.tc_vec.clone();
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
let server_addr = udp_tc_server.socket.local_addr().unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler,
};
let mut sph = SpHeader::tc_unseg(PUS_APID, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true)
.to_vec()
.unwrap();
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
let client_addr = client.local_addr().unwrap();
client.connect(server_addr).unwrap();
client.send(&ping_tc).unwrap();
udp_dyn_server.periodic_operation();
{
let mut tc_queue = tc_queue.lock().unwrap();
assert!(!tc_queue.is_empty());
let received_tc = tc_queue.pop_front().unwrap();
assert_eq!(received_tc, ping_tc);
}
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
udp_dyn_server.periodic_operation();
assert!(tc_queue.lock().unwrap().is_empty());
// Still tries to send to the same client.
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
}
}