1 Commits

Author SHA1 Message Date
11b66c8bf7 Finish PUS service optimizations
Some checks failed
Rust/sat-rs/pipeline/pr-main There was a failure building this commit
- 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:46:05 +01:00
54 changed files with 1283 additions and 7135 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://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS documentation website](https://absatsw.irs.uni-stuttgart.de/sat-rs.html).
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).
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,13 +14,8 @@ 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.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
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
# SSH stuff to allow deployment to doc server
RUN adduser --uid 114 jenkins

View File

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

View File

@ -1,991 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.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

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

View File

@ -1,312 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.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

@ -1,835 +0,0 @@
%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

@ -1,348 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0"/>
<node id="n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="174.17919999999998" width="273.1071999999999" x="1000.3040000000003" y="433.61760000000004"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.380859375" x="12.06892812499973" xml:space="preserve" y="7.247609374999911">Static Pool<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.45580882479480683" nodeRatioY="-0.45838992615076934" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1141.8400000000001" y="536.1087687499992"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.142578125" x="16.9287109375" xml:space="preserve" y="21.015625">1 x 128 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="90.0" width="164.73919999999998" x="1113.1776" y="451.01919999999996"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="164.73919999999998" x="0.0" xml:space="preserve" y="0.0">Group 1</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="15" leftF="14.739199999999983" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 1</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n2:">
<node id="n2::n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="60.0" x="1142.9168" y="466.01919999999996"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="28.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1142.9168" y="466.01919999999996"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="20.746093749999773" xml:space="preserve" y="4.862813159989173">2 x 64 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="-1.1657341758564144E-15" nodeRatioY="-0.41895311400018" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
</node>
<node id="n3" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="89.99999999999994" width="150.0" x="997.9168" y="451.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="150.0" x="0.0" xml:space="preserve" y="0.0">Group 2</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="997.9168" y="451.01919999999996"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 2</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n3:">
<node id="n3::n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1092.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1072.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3::n2">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1032.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3::n3">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1052.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3::n4">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="20.0" x="1012.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3::n5">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1012.9168" y="466.0192"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="14.814773932043863" xml:space="preserve" y="4.392671444094276">6 x 16 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="-0.04942766514963404" nodeRatioY="-0.426788809265095" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
</node>
<node id="n4" yfiles.foldertype="group">
<data key="d4" xml:space="preserve"/>
<data key="d5"/>
<data key="d6">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry height="90.0" width="150.0" x="997.9168" y="521.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="150.0" x="0.0" xml:space="preserve" y="0.0">Group 3</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
<y:GroupNode>
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
<y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
<y:Shape type="roundrectangle"/>
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed" id="n4:">
<node id="n4::n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1012.9168" y="536.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="13.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4::n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1042.9168" y="536.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="13.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4::n2">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="30.0" x="1072.9168" y="536.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="13.0" y="28.0">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4::n3">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="120.0" x="1012.9168" y="536.1759843749999"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFFFFF" fontFamily="Dialog" fontSize="12" fontStyle="plain" height="17.96875" horizontalTextPosition="center" iconTextGap="4" lineColor="#000000" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.5078125" x="20.74609375" xml:space="preserve" y="4.392671444094276">4 x 32 bytes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="-0.426788809265095" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
</node>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

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

View File

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

View File

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

View File

@ -2,16 +2,9 @@
- [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,5 +1,3 @@
<div id="communication-chapter"/>
# Communication with sat-rs based software
Communication is a vital topic for remote system which are usually not (directly)
@ -17,16 +15,10 @@ 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 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).
2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol
to always deliver complete packets.
# Working with telemetry and telecommands (TMTC)

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

View File

@ -1,6 +1,6 @@
[package]
name = "satrs-core"
version = "0.1.0-alpha.3"
version = "0.1.0-alpha.1"
edition = "2021"
rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
@ -15,6 +15,8 @@ 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"
@ -71,11 +73,10 @@ features = ["all"]
optional = true
[dependencies.spacepackets]
version = "0.9.0"
version = "0.7.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,7 +5,6 @@ use spacepackets::cfdp::ChecksumType;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
use std::path::Path;
#[cfg(feature = "std")]
pub use stdmod::*;
@ -99,8 +98,7 @@ 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, dir_path: &str, all: bool) -> Result<(), FilestoreError>;
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError>;
fn remove_dir(&self, file_path: &str, all: bool) -> Result<(), FilestoreError>;
fn read_data(
&self,
@ -112,16 +110,7 @@ pub trait VirtualFilestore {
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>;
fn filename_from_full_path(path: &str) -> Option<&str>
where
Self: Sized,
{
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str>;
fn is_file(&self, path: &str) -> bool;
@ -134,10 +123,6 @@ 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,
@ -170,7 +155,7 @@ pub mod stdmod {
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Err(FilestoreError::FileDoesNotExist);
return Ok(());
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
@ -181,7 +166,7 @@ pub mod stdmod {
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path) {
return Err(FilestoreError::FileDoesNotExist);
return Ok(());
}
if !self.is_file(file_path) {
return Err(FilestoreError::IsNotFile);
@ -193,14 +178,6 @@ 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);
@ -255,11 +232,24 @@ 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() {
@ -277,10 +267,7 @@ pub mod stdmod {
) -> Result<bool, FilestoreError> {
match checksum_type {
ChecksumType::Modular => {
if self.calc_modular_checksum(file_path)? == expected_checksum {
return Ok(true);
}
Ok(false)
todo!();
}
ChecksumType::Crc32 => {
let mut digest = CRC_32.digest();
@ -303,29 +290,6 @@ 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)]
@ -333,13 +297,8 @@ 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]
@ -353,10 +312,11 @@ 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_file_exists() {
fn test_basic_native_fs_exists() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
@ -365,32 +325,7 @@ mod tests {
.unwrap();
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
}
#[test]
fn test_basic_native_fs_dir_exists() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let dir_path = tmpdir.path().join("testdir");
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
NATIVE_FS
.create_dir(dir_path.to_str().expect("getting str for file failed"))
.unwrap();
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
assert!(NATIVE_FS.is_dir(dir_path.as_path().to_str().unwrap()));
}
#[test]
fn test_basic_native_fs_remove_file() {
let tmpdir = tempdir().expect("creating tmpdir failed");
let file_path = tmpdir.path().join("test.txt");
NATIVE_FS
.create_file(file_path.to_str().expect("getting str for file failed"))
.expect("creating file failed");
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
NATIVE_FS
.remove_file(file_path.to_str().unwrap())
.expect("removing file failed");
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
#[test]
@ -410,6 +345,7 @@ 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]
@ -429,342 +365,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);
}
#[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");
}
fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed");
}
}

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,49 +34,47 @@ pub trait Executable: Send {
/// # Arguments
///
/// * `executable`: Executable task
/// * `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]
/// * `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_single<T: Executable<Error = E> + Send + 'static + ?Sized, E: Send + 'static>(
pub fn exec_sched_single<
T: Executable<Error = E> + Send + 'static + ?Sized,
E: Error + Send + 'static,
>(
mut executable: Box<T>,
task_freq: Option<Duration>,
op_code: i32,
mut termination: Option<BusReader<()>>,
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
) -> JoinHandle<Result<OpResult, E>> {
let mut cycle_count = 0;
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) => (),
}
}
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
thread::spawn(move || loop {
if let Some(ref mut terminator) = termination {
match terminator.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
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);
}
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);
}
}
if let Some(freq) = task_freq {
thread::sleep(freq);
}
})
}
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
thread::sleep(freq);
})
}
/// This function allows executing multiple tasks as long as the tasks implement the
@ -88,56 +86,55 @@ pub fn exec_sched_single<T: Executable<Error = E> + Send + 'static + ?Sized, E:
/// * `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: Send + 'static>(
task_name: &'static str,
pub fn exec_sched_multi<
T: Executable<Error = E> + Send + 'static + ?Sized,
E: Error + Send + 'static,
>(
mut executable_vec: Vec<Box<T>>,
task_freq: Option<Duration>,
op_code: i32,
mut termination: Option<BusReader<()>>,
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
) -> JoinHandle<Result<OpResult, E>> {
let mut cycle_counts = vec![0; executable_vec.len()];
let mut removal_flags = vec![false; executable_vec.len()];
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) => (),
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);
}
Err(TryRecvError::Empty) => (),
}
for (idx, executable) in executable_vec.iter_mut().enumerate() {
match executable.exec_type() {
ExecutionType::OneShot => {
executable.periodic_op(op_code)?;
}
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;
}
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)]
@ -297,8 +294,7 @@ 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);
@ -323,8 +319,7 @@ 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();
@ -361,13 +356,11 @@ 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);
@ -393,8 +386,7 @@ 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");
@ -426,13 +418,11 @@ 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");
@ -459,8 +449,7 @@ 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");
@ -496,13 +485,11 @@ mod tests {
let task_vec: Vec<Box<dyn Executable<Error = ExampleError>>> =
vec![cycled_task, periodic_task_0, periodic_task_1];
let jh = exec_sched_multi(
"multi-task-name",
task_vec,
Some(Duration::from_millis(20)),
expected_op_code,
Some(terminator.add_rx()),
)
.expect("thread creation failed");
);
thread::sleep(Duration::from_millis(60));
terminator.broadcast(());
let thread_res = jh.join().expect("Periodic Task failed");

View File

@ -1,21 +1,13 @@
//! # Pool implementation providing memory pools for packet storage.
//!
//! This module provides generic abstractions for memory pools which provide a storage
//! machanism for variable sized data like Telemetry and Telecommand (TMTC) packets. The core
//! abstraction for this is the [PoolProvider] trait.
//!
//! It also contains the [StaticMemoryPool] as a concrete implementation which can be used to avoid
//! dynamic run-time allocations for the storage of TMTC packets.
//!
//! # Example for the [StaticMemoryPool]
//!
//! ```
//! use satrs_core::pool::{PoolProvider, StaticMemoryPool, StaticPoolConfig};
//! use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig};
//!
//! // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
//! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)], false);
//! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
//! let mut local_pool = StaticMemoryPool::new(pool_cfg);
//! let mut read_buf: [u8; 16] = [0; 16];
//! let mut addr;
//! {
//! // Add new data to the pool
@ -28,25 +20,25 @@
//!
//! {
//! // Read the store data back
//! let res = local_pool.read(&addr, &mut read_buf);
//! let res = local_pool.read(&addr);
//! assert!(res.is_ok());
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 4);
//! assert_eq!(read_buf[0], 42);
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 4);
//! assert_eq!(buf_read_back[0], 42);
//! // Modify the stored data
//! let res = local_pool.modify(&addr, |buf| {
//! buf[0] = 12;
//! });
//! let res = local_pool.modify(&addr);
//! assert!(res.is_ok());
//! let buf_read_back = res.unwrap();
//! buf_read_back[0] = 12;
//! }
//!
//! {
//! // Read the modified data back
//! let res = local_pool.read(&addr, &mut read_buf);
//! let res = local_pool.read(&addr);
//! assert!(res.is_ok());
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 4);
//! assert_eq!(read_buf[0], 12);
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 4);
//! assert_eq!(buf_read_back[0], 12);
//! }
//!
//! // Delete the stored data
@ -54,21 +46,21 @@
//!
//! // Get a free element in the pool with an appropriate size
//! {
//! let res = local_pool.free_element(12, |buf| {
//! buf[0] = 7;
//! });
//! let res = local_pool.free_element(12);
//! assert!(res.is_ok());
//! addr = res.unwrap();
//! let (tmp, mut_buf) = res.unwrap();
//! addr = tmp;
//! mut_buf[0] = 7;
//! }
//!
//! // Read back the data
//! {
//! // Read the store data back
//! let res = local_pool.read(&addr, &mut read_buf);
//! let res = local_pool.read(&addr);
//! assert!(res.is_ok());
//! let read_bytes = res.unwrap();
//! assert_eq!(read_bytes, 12);
//! assert_eq!(read_buf[0], 7);
//! let buf_read_back = res.unwrap();
//! assert_eq!(buf_read_back.len(), 12);
//! assert_eq!(buf_read_back[0], 7);
//! }
//! ```
#[cfg(feature = "alloc")]
@ -78,7 +70,6 @@ use core::fmt::{Display, Formatter};
use delegate::delegate;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
@ -160,8 +151,6 @@ pub enum StoreError {
InvalidStoreId(StoreIdError, Option<StoreAddr>),
/// Valid subpool and packet index, but no data is stored at the given address
DataDoesNotExist(StoreAddr),
ByteConversionError(spacepackets::ByteConversionError),
LockError,
/// Internal or configuration errors
InternalError(u32),
}
@ -184,22 +173,10 @@ impl Display for StoreError {
StoreError::InternalError(e) => {
write!(f, "internal error: {e}")
}
StoreError::ByteConversionError(e) => {
write!(f, "store error: {e}")
}
StoreError::LockError => {
write!(f, "lock error")
}
}
}
}
impl From<ByteConversionError> for StoreError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversionError(value)
}
}
#[cfg(feature = "std")]
impl Error for StoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
@ -210,61 +187,41 @@ impl Error for StoreError {
}
}
/// Generic trait for pool providers which provide memory pools for variable sized data.
///
/// It specifies a basic API to [Self::add], [Self::modify], [Self::read] and [Self::delete] data
/// in the store at its core. The API was designed so internal optimizations can be performed
/// more easily and that is is also possible to make the pool structure [Sync] without the whole
/// pool structure being wrapped inside a lock.
pub trait PoolProvider {
/// Generic trait for pool providers where the data can be modified and read in-place. This
/// generally means that a shared pool structure has to be wrapped inside a lock structure.
pub trait PoolProviderMemInPlace {
/// Add new data to the pool. The provider should attempt to reserve a memory block with the
/// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can
/// be used to access the data stored in the pool
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError>;
/// The provider should attempt to reserve a free memory block with the appropriate size first.
/// It then executes a user-provided closure and passes a mutable reference to that memory
/// block to the closure. This allows the user to write data to the memory block.
/// The function should yield a [StoreAddr] which can be used to access the data stored in the
/// pool.
fn free_element<W: FnMut(&mut [u8])>(
&mut self,
len: usize,
writer: W,
) -> Result<StoreAddr, StoreError>;
/// The provider should attempt to reserve a free memory block with the appropriate size and
/// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access
/// the data stored in the pool
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>;
/// Modify data added previously using a given [StoreAddr]. The provider should use the store
/// address to determine if a memory block exists for that address. If it does, it should
/// call the user-provided closure and pass a mutable reference to the memory block
/// to the closure. This allows the user to modify the memory block.
fn modify<U: FnMut(&mut [u8])>(
&mut self,
addr: &StoreAddr,
updater: U,
) -> Result<(), StoreError>;
/// Modify data added previously using a given [StoreAddr] by yielding a mutable reference
/// to it
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>;
/// The provider should copy the data from the memory block to the user-provided buffer if
/// it exists.
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, StoreError>;
/// Read data by yielding a read-only reference given a [StoreAddr]
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>;
/// Delete data inside the pool given a [StoreAddr].
/// Delete data inside the pool given a [StoreAddr]
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>;
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError>;
/// Retrieve the length of the data at the given store address.
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError>;
#[cfg(feature = "alloc")]
fn read_as_vec(&self, addr: &StoreAddr) -> Result<alloc::vec::Vec<u8>, StoreError> {
let mut vec = alloc::vec![0; self.len_of_data(addr)?];
self.read(addr, &mut vec)?;
Ok(vec)
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
if !self.has_element_at(addr)? {
return Err(StoreError::DataDoesNotExist(*addr));
}
Ok(self.read(addr)?.len())
}
}
/// Extension trait which adds guarded pool access classes.
pub trait PoolProviderWithGuards: PoolProvider {
/// This function behaves like [PoolProvider::read], but consumes the provided address
pub trait PoolProviderMemInPlaceWithGuards: PoolProviderMemInPlace {
/// This function behaves like [PoolProviderMemInPlace::read], but consumes the provided address
/// and returns a RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
@ -274,7 +231,7 @@ pub trait PoolProviderWithGuards: PoolProvider {
/// manual deletion is necessary when returning from a processing function prematurely.
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard<Self>;
/// This function behaves like [PoolProvider::modify], but consumes the provided
/// This function behaves like [PoolProviderMemInPlace::modify], but consumes the provided
/// address and returns a RAII conformant guard object.
///
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
@ -285,16 +242,15 @@ pub trait PoolProviderWithGuards: PoolProvider {
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self>;
}
pub struct PoolGuard<'a, MemProvider: PoolProvider + ?Sized> {
pub struct PoolGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> {
pool: &'a mut MemProvider,
pub addr: StoreAddr,
no_deletion: bool,
deletion_failed_error: Option<StoreError>,
}
/// This helper object can be used to safely access pool data without worrying about memory
/// leaks.
impl<'a, MemProvider: PoolProvider> PoolGuard<'a, MemProvider> {
/// This helper object
impl<'a, MemProvider: PoolProviderMemInPlace> PoolGuard<'a, MemProvider> {
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
Self {
pool,
@ -304,13 +260,8 @@ impl<'a, MemProvider: PoolProvider> PoolGuard<'a, MemProvider> {
}
}
pub fn read(&self, buf: &mut [u8]) -> Result<usize, StoreError> {
self.pool.read(&self.addr, buf)
}
#[cfg(feature = "alloc")]
pub fn read_as_vec(&self) -> Result<alloc::vec::Vec<u8>, StoreError> {
self.pool.read_as_vec(&self.addr)
pub fn read(&self) -> Result<&[u8], StoreError> {
self.pool.read(&self.addr)
}
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
@ -320,7 +271,7 @@ impl<'a, MemProvider: PoolProvider> PoolGuard<'a, MemProvider> {
}
}
impl<MemProvider: PoolProvider + ?Sized> Drop for PoolGuard<'_, MemProvider> {
impl<MemProvider: PoolProviderMemInPlace + ?Sized> Drop for PoolGuard<'_, MemProvider> {
fn drop(&mut self) {
if !self.no_deletion {
if let Err(e) = self.pool.delete(self.addr) {
@ -330,24 +281,24 @@ impl<MemProvider: PoolProvider + ?Sized> Drop for PoolGuard<'_, MemProvider> {
}
}
pub struct PoolRwGuard<'a, MemProvider: PoolProvider + ?Sized> {
pub struct PoolRwGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> {
guard: PoolGuard<'a, MemProvider>,
}
impl<'a, MemProvider: PoolProvider> PoolRwGuard<'a, MemProvider> {
impl<'a, MemProvider: PoolProviderMemInPlace> PoolRwGuard<'a, MemProvider> {
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
Self {
guard: PoolGuard::new(pool, addr),
}
}
pub fn update<U: FnMut(&mut [u8])>(&mut self, updater: &mut U) -> Result<(), StoreError> {
self.guard.pool.modify(&self.guard.addr, updater)
pub fn modify(&mut self) -> Result<&mut [u8], StoreError> {
self.guard.pool.modify(&self.guard.addr)
}
delegate!(
to self.guard {
pub fn read(&self, buf: &mut [u8]) -> Result<usize, StoreError>;
pub fn read(&self) -> Result<&[u8], StoreError>;
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
/// is dropped.
pub fn release(&mut self);
@ -357,11 +308,13 @@ impl<'a, MemProvider: PoolProvider> PoolRwGuard<'a, MemProvider> {
#[cfg(feature = "alloc")]
mod alloc_mod {
use super::{PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticPoolAddr};
use super::{
PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard,
StaticPoolAddr,
};
use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError};
use alloc::vec;
use alloc::vec::Vec;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::sync::{Arc, RwLock};
@ -376,24 +329,16 @@ mod alloc_mod {
///
/// # Parameters
///
/// * `cfg` - Vector of tuples which represent a subpool. The first entry in the tuple specifies
/// the number of memory blocks in the subpool, the second entry the size of the blocks
/// * `spill_to_higher_subpools` - Specifies whether data will be spilled to higher subpools
/// if the next fitting subpool is full. This is useful to ensure the pool remains useful
/// for all data sizes as long as possible. However, an undesirable side-effect might be
/// the chocking of larger subpools by underdimensioned smaller subpools.
/// * `cfg`: Vector of tuples which represent a subpool. The first entry in the tuple specifies the
/// number of memory blocks in the subpool, the second entry the size of the blocks
#[derive(Clone)]
pub struct StaticPoolConfig {
cfg: Vec<(NumBlocks, usize)>,
spill_to_higher_subpools: bool,
}
impl StaticPoolConfig {
pub fn new(cfg: Vec<(NumBlocks, usize)>, spill_to_higher_subpools: bool) -> Self {
StaticPoolConfig {
cfg,
spill_to_higher_subpools,
}
pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self {
StaticPoolConfig { cfg }
}
pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> {
@ -411,21 +356,16 @@ mod alloc_mod {
/// Pool implementation providing sub-pools with fixed size memory blocks.
///
/// This is a simple memory pool implementation which pre-allocates all subpools using a given
/// pool configuration. After the pre-allocation, no dynamic memory allocation will be
/// performed during run-time. This makes the implementation suitable for real-time
/// applications and embedded environments.
///
/// The subpool bucket sizes only denote the maximum possible data size being stored inside
/// them and the pool implementation will still track the size of the data stored inside it.
/// The implementation will generally determine the best fitting subpool for given data to
/// add. Currently, the pool does not support spilling to larger subpools if the closest
/// fitting subpool is full. This might be added in the future.
/// This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool
/// configuration. After the pre-allocation, no dynamic memory allocation will be performed
/// during run-time. This makes the implementation suitable for real-time applications and
/// embedded environments. The pool implementation will also track the size of the data stored
/// inside it.
///
/// Transactions with the [pool][StaticMemoryPool] are done using a generic
/// [address][StoreAddr] type. Adding any data to the pool will yield a store address.
/// Modification and read operations are done using a reference to a store address. Deletion
/// will consume the store address.
/// [address][StoreAddr] type.
/// Adding any data to the pool will yield a store address. Modification and read operations are
/// done using a reference to a store address. Deletion will consume the store address.
pub struct StaticMemoryPool {
pool_cfg: StaticPoolConfig,
pool: Vec<Vec<u8>>,
@ -482,17 +422,7 @@ mod alloc_mod {
}
fn reserve(&mut self, data_len: usize) -> Result<StaticPoolAddr, StoreError> {
let mut subpool_idx = self.find_subpool(data_len, 0)?;
if self.pool_cfg.spill_to_higher_subpools {
while let Err(StoreError::StoreFull(_)) = self.find_empty(subpool_idx) {
if (subpool_idx + 1) as usize == self.sizes_lists.len() {
return Err(StoreError::StoreFull(subpool_idx));
}
subpool_idx += 1;
}
}
let subpool_idx = self.find_subpool(data_len, 0)?;
let (slot, size_slot_ref) = self.find_empty(subpool_idx)?;
*size_slot_ref = data_len;
Ok(StaticPoolAddr {
@ -546,7 +476,7 @@ mod alloc_mod {
}
}
impl PoolProvider for StaticMemoryPool {
impl PoolProviderMemInPlace for StaticMemoryPool {
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError> {
let data_len = data.len();
if data_len > POOL_MAX_SIZE {
@ -557,11 +487,7 @@ mod alloc_mod {
Ok(addr.into())
}
fn free_element<W: FnMut(&mut [u8])>(
&mut self,
len: usize,
mut writer: W,
) -> Result<StoreAddr, StoreError> {
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> {
if len > POOL_MAX_SIZE {
return Err(StoreError::DataTooLarge(len));
}
@ -569,40 +495,25 @@ mod alloc_mod {
let raw_pos = self.raw_pos(&addr).unwrap();
let block =
&mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len];
writer(block);
Ok(addr.into())
Ok((addr.into(), block))
}
fn modify<U: FnMut(&mut [u8])>(
&mut self,
addr: &StoreAddr,
mut updater: U,
) -> Result<(), StoreError> {
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> {
let addr = StaticPoolAddr::from(*addr);
let curr_size = self.addr_check(&addr)?;
let raw_pos = self.raw_pos(&addr).unwrap();
let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap()
[raw_pos..raw_pos + curr_size];
updater(block);
Ok(())
Ok(block)
}
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, StoreError> {
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> {
let addr = StaticPoolAddr::from(*addr);
let curr_size = self.addr_check(&addr)?;
if buf.len() < curr_size {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: curr_size,
}
.into());
}
let raw_pos = self.raw_pos(&addr).unwrap();
let block =
&self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size];
//block.copy_from_slice(&src);
buf[..curr_size].copy_from_slice(block);
Ok(curr_size)
Ok(block)
}
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> {
@ -629,21 +540,9 @@ mod alloc_mod {
}
Ok(true)
}
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
let addr = StaticPoolAddr::from(*addr);
self.validate_addr(&addr)?;
let pool_idx = addr.pool_idx as usize;
let size_list = self.sizes_lists.get(pool_idx).unwrap();
let size = size_list[addr.packet_idx as usize];
Ok(match size {
STORE_FREE => 0,
_ => size,
})
}
}
impl PoolProviderWithGuards for StaticMemoryPool {
impl PoolProviderMemInPlaceWithGuards for StaticMemoryPool {
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self> {
PoolRwGuard::new(self, addr)
}
@ -657,29 +556,30 @@ mod alloc_mod {
#[cfg(test)]
mod tests {
use crate::pool::{
PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticMemoryPool,
StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError, POOL_MAX_SIZE,
PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard,
StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError,
POOL_MAX_SIZE,
};
use std::vec;
fn basic_small_pool() -> StaticMemoryPool {
// 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)], false);
let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
StaticMemoryPool::new(pool_cfg)
}
#[test]
fn test_cfg() {
// Values where number of buckets is 0 or size is too large should be removed
let mut pool_cfg = StaticPoolConfig::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)], false);
let mut pool_cfg = StaticPoolConfig::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]);
pool_cfg.sanitize();
assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]);
// Entries should be ordered according to bucket size
pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)], false);
pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
pool_cfg.sanitize();
assert_eq!(*pool_cfg.cfg(), vec![(32, 3), (16, 6), (8, 12)]);
// Unstable sort is used, so order of entries with same block length should not matter
pool_cfg = StaticPoolConfig::new(vec![(12, 12), (14, 16), (10, 12)], false);
pool_cfg = StaticPoolConfig::new(vec![(12, 12), (14, 16), (10, 12)]);
pool_cfg.sanitize();
assert!(
*pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)]
@ -694,14 +594,13 @@ mod tests {
for (i, val) in test_buf.iter_mut().enumerate() {
*val = i as u8;
}
let mut other_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
// Read back data and verify correctness
let res = local_pool.read(&addr, &mut other_buf);
let res = local_pool.read(&addr);
assert!(res.is_ok());
let read_len = res.unwrap();
assert_eq!(read_len, 16);
for (i, &val) in other_buf.iter().enumerate() {
let buf_read_back = res.unwrap();
assert_eq!(buf_read_back.len(), 16);
for (i, &val) in buf_read_back.iter().enumerate() {
assert_eq!(val, i as u8);
}
}
@ -711,10 +610,8 @@ mod tests {
let mut local_pool = basic_small_pool();
let test_buf: [u8; 12] = [0; 12];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let res = local_pool
.read(&addr, &mut [0; 12])
.expect("Read back failed");
assert_eq!(res, 12);
let res = local_pool.read(&addr).expect("Read back failed");
assert_eq!(res.len(), 12);
}
#[test]
@ -725,13 +622,10 @@ mod tests {
// Delete the data
let res = local_pool.delete(addr);
assert!(res.is_ok());
let mut writer = |buf: &mut [u8]| {
assert_eq!(buf.len(), 12);
};
// Verify that the slot is free by trying to get a reference to it
let res = local_pool.free_element(12, &mut writer);
let res = local_pool.free_element(12);
assert!(res.is_ok());
let addr = res.unwrap();
let (addr, buf_ref) = res.unwrap();
assert_eq!(
addr,
u64::from(StaticPoolAddr {
@ -739,6 +633,7 @@ mod tests {
packet_idx: 0
})
);
assert_eq!(buf_ref.len(), 12);
}
#[test]
@ -752,34 +647,29 @@ mod tests {
{
// Verify that the slot is free by trying to get a reference to it
local_pool
.modify(&addr, &mut |buf: &mut [u8]| {
buf[0] = 0;
buf[1] = 0x42;
})
.expect("Modifying data failed");
let res = local_pool.modify(&addr).expect("Modifying data failed");
res[0] = 0;
res[1] = 0x42;
}
local_pool
.read(&addr, &mut test_buf)
.expect("Reading back data failed");
assert_eq!(test_buf[0], 0);
assert_eq!(test_buf[1], 0x42);
assert_eq!(test_buf[2], 2);
assert_eq!(test_buf[3], 3);
let res = local_pool.read(&addr).expect("Reading back data failed");
assert_eq!(res[0], 0);
assert_eq!(res[1], 0x42);
assert_eq!(res[2], 2);
assert_eq!(res[3], 3);
}
#[test]
fn test_consecutive_reservation() {
let mut local_pool = basic_small_pool();
// Reserve two smaller blocks consecutively and verify that the third reservation fails
let res = local_pool.free_element(8, |_| {});
let res = local_pool.free_element(8);
assert!(res.is_ok());
let addr0 = res.unwrap();
let res = local_pool.free_element(8, |_| {});
let (addr0, _) = res.unwrap();
let res = local_pool.free_element(8);
assert!(res.is_ok());
let addr1 = res.unwrap();
let res = local_pool.free_element(8, |_| {});
let (addr1, _) = res.unwrap();
let res = local_pool.free_element(8);
assert!(res.is_err());
let err = res.unwrap_err();
assert_eq!(err, StoreError::StoreFull(1));
@ -799,7 +689,6 @@ mod tests {
pool_idx: 0,
}
.into(),
&mut [],
);
assert!(res.is_err());
assert!(matches!(
@ -831,7 +720,7 @@ mod tests {
packet_idx: 0,
}
.into();
let res = local_pool.read(&addr, &mut []);
let res = local_pool.read(&addr);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(matches!(
@ -848,7 +737,7 @@ mod tests {
packet_idx: 1,
};
assert_eq!(addr.raw(), 0x00020001);
let res = local_pool.read(&addr.into(), &mut []);
let res = local_pool.read(&addr.into());
assert!(res.is_err());
let err = res.unwrap_err();
assert!(matches!(
@ -870,7 +759,7 @@ mod tests {
#[test]
fn test_data_too_large_1() {
let mut local_pool = basic_small_pool();
let res = local_pool.free_element(POOL_MAX_SIZE + 1, |_| {});
let res = local_pool.free_element(POOL_MAX_SIZE + 1);
assert!(res.is_err());
assert_eq!(
res.unwrap_err(),
@ -882,7 +771,7 @@ mod tests {
fn test_free_element_too_large() {
let mut local_pool = basic_small_pool();
// Try to request a slot which is too large
let res = local_pool.free_element(20, |_| {});
let res = local_pool.free_element(20);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), StoreError::DataTooLarge(20));
}
@ -924,7 +813,7 @@ mod tests {
let test_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let mut rw_guard = PoolRwGuard::new(&mut local_pool, addr);
rw_guard.update(&mut |_| {}).expect("modify failed");
let _ = rw_guard.modify().expect("modify failed");
drop(rw_guard);
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
}
@ -935,7 +824,7 @@ mod tests {
let test_buf: [u8; 16] = [0; 16];
let addr = local_pool.add(&test_buf).expect("Adding data failed");
let mut rw_guard = local_pool.modify_with_guard(addr);
rw_guard.update(&mut |_| {}).expect("modify failed");
let _ = rw_guard.modify().expect("modify failed");
drop(rw_guard);
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
}
@ -951,93 +840,13 @@ mod tests {
let addr1 = local_pool.add(&test_buf_1).expect("Adding data failed");
let addr2 = local_pool.add(&test_buf_2).expect("Adding data failed");
let addr3 = local_pool.add(&test_buf_3).expect("Adding data failed");
local_pool
.modify(&addr0, |buf| {
assert_eq!(buf, test_buf_0);
})
.expect("Modifying data failed");
local_pool
.modify(&addr1, |buf| {
assert_eq!(buf, test_buf_1);
})
.expect("Modifying data failed");
local_pool
.modify(&addr2, |buf| {
assert_eq!(buf, test_buf_2);
})
.expect("Modifying data failed");
local_pool
.modify(&addr3, |buf| {
assert_eq!(buf, test_buf_3);
})
.expect("Modifying data failed");
}
#[test]
fn test_spills_to_higher_subpools() {
let pool_cfg = StaticPoolConfig::new(vec![(2, 8), (2, 16)], true);
let mut local_pool = StaticMemoryPool::new(pool_cfg);
local_pool.free_element(8, |_| {}).unwrap();
local_pool.free_element(8, |_| {}).unwrap();
let mut in_larger_subpool_now = local_pool.free_element(8, |_| {});
assert!(in_larger_subpool_now.is_ok());
let generic_addr = in_larger_subpool_now.unwrap();
let pool_addr = StaticPoolAddr::from(generic_addr);
assert_eq!(pool_addr.pool_idx, 1);
assert_eq!(pool_addr.packet_idx, 0);
assert!(local_pool.has_element_at(&generic_addr).unwrap());
in_larger_subpool_now = local_pool.free_element(8, |_| {});
assert!(in_larger_subpool_now.is_ok());
let generic_addr = in_larger_subpool_now.unwrap();
let pool_addr = StaticPoolAddr::from(generic_addr);
assert_eq!(pool_addr.pool_idx, 1);
assert_eq!(pool_addr.packet_idx, 1);
assert!(local_pool.has_element_at(&generic_addr).unwrap());
}
#[test]
fn test_spillage_fails_as_well() {
let pool_cfg = StaticPoolConfig::new(vec![(1, 8), (1, 16)], true);
let mut local_pool = StaticMemoryPool::new(pool_cfg);
local_pool.free_element(8, |_| {}).unwrap();
local_pool.free_element(8, |_| {}).unwrap();
let should_fail = local_pool.free_element(8, |_| {});
assert!(should_fail.is_err());
if let Err(err) = should_fail {
assert_eq!(err, StoreError::StoreFull(1));
} else {
panic!("unexpected store address");
}
}
#[test]
fn test_spillage_works_across_multiple_subpools() {
let pool_cfg = StaticPoolConfig::new(vec![(1, 8), (1, 12), (1, 16)], true);
let mut local_pool = StaticMemoryPool::new(pool_cfg);
local_pool.free_element(8, |_| {}).unwrap();
local_pool.free_element(12, |_| {}).unwrap();
let in_larger_subpool_now = local_pool.free_element(8, |_| {});
assert!(in_larger_subpool_now.is_ok());
let generic_addr = in_larger_subpool_now.unwrap();
let pool_addr = StaticPoolAddr::from(generic_addr);
assert_eq!(pool_addr.pool_idx, 2);
assert_eq!(pool_addr.packet_idx, 0);
assert!(local_pool.has_element_at(&generic_addr).unwrap());
}
#[test]
fn test_spillage_fails_across_multiple_subpools() {
let pool_cfg = StaticPoolConfig::new(vec![(1, 8), (1, 12), (1, 16)], true);
let mut local_pool = StaticMemoryPool::new(pool_cfg);
local_pool.free_element(8, |_| {}).unwrap();
local_pool.free_element(12, |_| {}).unwrap();
local_pool.free_element(16, |_| {}).unwrap();
let should_fail = local_pool.free_element(8, |_| {});
assert!(should_fail.is_err());
if let Err(err) = should_fail {
assert_eq!(err, StoreError::StoreFull(2));
} else {
panic!("unexpected store address");
}
let tm0_raw = local_pool.modify(&addr0).expect("Modifying data failed");
assert_eq!(tm0_raw, test_buf_0);
let tm1_raw = local_pool.modify(&addr1).expect("Modifying data failed");
assert_eq!(tm1_raw, test_buf_1);
let tm2_raw = local_pool.modify(&addr2).expect("Modifying data failed");
assert_eq!(tm2_raw, test_buf_2);
let tm3_raw = local_pool.modify(&addr3).expect("Modifying data failed");
assert_eq!(tm3_raw, test_buf_3);
}
}

View File

@ -318,31 +318,10 @@ mod alloc_mod {
/// [Clone].
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
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;
}
pub trait EcssTmSender: EcssTmSenderCore + Downcast + DynClone {}
/// Blanket implementation for all types which implement [EcssTmSenderCore] and are clonable.
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
}
}
impl<T> EcssTmSender for T where T: EcssTmSenderCore + Clone + 'static {}
dyn_clone::clone_trait_object!(EcssTmSender);
impl_downcast!(EcssTmSender);
@ -390,7 +369,7 @@ mod alloc_mod {
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod std_mod {
use crate::pool::{PoolProvider, PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr};
use crate::pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr};
use crate::pus::verification::{
StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
};
@ -399,7 +378,7 @@ pub mod std_mod {
EcssTmSenderCore, EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper,
TryRecvTmtcError,
};
use crate::tmtc::tm_helper::SharedTmPool;
use crate::tmtc::tm_helper::SharedTmStore;
use crate::ChannelId;
use alloc::boxed::Box;
use alloc::vec::Vec;
@ -441,14 +420,14 @@ pub mod std_mod {
}
#[derive(Clone)]
pub struct MpscTmInSharedPoolSender {
pub struct MpscTmInStoreSender {
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmPool,
shared_tm_store: SharedTmStore,
sender: mpsc::Sender<StoreAddr>,
}
impl EcssChannel for MpscTmInSharedPoolSender {
impl EcssChannel for MpscTmInStoreSender {
fn id(&self) -> ChannelId {
self.id
}
@ -458,7 +437,7 @@ pub mod std_mod {
}
}
impl MpscTmInSharedPoolSender {
impl MpscTmInStoreSender {
pub fn send_direct_tm(&self, tm: PusTmCreator) -> Result<(), EcssTmtcError> {
let addr = self.shared_tm_store.add_pus_tm(&tm)?;
self.sender
@ -467,7 +446,7 @@ pub mod std_mod {
}
}
impl EcssTmSenderCore for MpscTmInSharedPoolSender {
impl EcssTmSenderCore for MpscTmInStoreSender {
fn send_tm(&self, tm: PusTmWrapper) -> Result<(), EcssTmtcError> {
match tm {
PusTmWrapper::InStore(addr) => {
@ -479,11 +458,11 @@ pub mod std_mod {
}
}
impl MpscTmInSharedPoolSender {
impl MpscTmInStoreSender {
pub fn new(
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmPool,
shared_tm_store: SharedTmStore,
sender: mpsc::Sender<StoreAddr>,
) -> Self {
Self {
@ -582,7 +561,7 @@ pub mod std_mod {
pub struct CrossbeamTmInStoreSender {
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmPool,
shared_tm_store: SharedTmStore,
sender: crossbeam_channel::Sender<StoreAddr>,
}
@ -590,7 +569,7 @@ pub mod std_mod {
pub fn new(
id: ChannelId,
name: &'static str,
shared_tm_store: SharedTmPool,
shared_tm_store: SharedTmStore,
sender: crossbeam_channel::Sender<StoreAddr>,
) -> Self {
Self {
@ -789,15 +768,12 @@ pub mod std_mod {
.shared_tc_store
.write()
.map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?;
let tc_size = tc_pool
.len_of_data(&addr)
.map_err(|e| PusPacketHandlingError::EcssTmtc(EcssTmtcError::Store(e)))?;
if tc_size > self.pus_buf.len() {
return Err(PusPacketHandlingError::PusPacketTooLarge(tc_size));
}
let tc_guard = tc_pool.read_with_guard(addr);
// TODO: Proper error handling.
tc_guard.read(&mut self.pus_buf[0..tc_size]).unwrap();
let tc_raw = tc_guard.read().unwrap();
if tc_raw.len() > self.pus_buf.len() {
return Err(PusPacketHandlingError::PusPacketTooLarge(tc_raw.len()));
}
self.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw);
Ok(())
}
}
@ -950,18 +926,19 @@ pub mod tests {
use spacepackets::CcsdsPacket;
use crate::pool::{
PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig, StoreAddr,
PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig,
StoreAddr,
};
use crate::pus::verification::RequestId;
use crate::tmtc::tm_helper::SharedTmPool;
use crate::tmtc::tm_helper::SharedTmStore;
use super::verification::{
TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
};
use super::{
EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver,
MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
PusPacketHandlingError, PusServiceHelper, TcInMemory,
MpscTmAsVecSender, MpscTmInStoreSender, PusPacketHandlerResult, PusPacketHandlingError,
PusServiceHelper, TcInMemory,
};
pub const TEST_APID: u16 = 0x101;
@ -1005,7 +982,7 @@ pub mod tests {
pus_buf: [u8; 2048],
tm_buf: [u8; 2048],
tc_pool: SharedStaticMemoryPool,
tm_pool: SharedTmPool,
tm_pool: SharedTmStore,
tc_sender: mpsc::Sender<EcssTcAndToken>,
tm_receiver: mpsc::Receiver<StoreAddr>,
verification_handler: VerificationReporterWithSender,
@ -1017,25 +994,21 @@ pub mod tests {
///
/// The PUS service handler is instantiated with a [EcssTcInStoreConverter].
pub fn new() -> (Self, PusServiceHelper<EcssTcInSharedStoreConverter>) {
let pool_cfg = StaticPoolConfig::new(vec![(16, 16), (8, 32), (4, 64)], false);
let pool_cfg = StaticPoolConfig::new(vec![(16, 16), (8, 32), (4, 64)]);
let tc_pool = StaticMemoryPool::new(pool_cfg.clone());
let tm_pool = StaticMemoryPool::new(pool_cfg);
let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool));
let shared_tm_pool = SharedTmPool::new(tm_pool);
let shared_tm_pool = SharedTmStore::new(tm_pool);
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
let (tm_tx, tm_rx) = mpsc::channel();
let verif_sender = MpscTmInSharedPoolSender::new(
0,
"verif_sender",
shared_tm_pool.clone(),
tm_tx.clone(),
);
let verif_sender =
MpscTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tm_tx.clone());
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
let verification_handler =
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
let test_srv_tm_sender =
MpscTmInSharedPoolSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx);
MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx);
let test_srv_tc_receiver = MpscTcReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx);
let in_store_converter =
EcssTcInSharedStoreConverter::new(shared_tc_pool.clone(), 2048);
@ -1079,9 +1052,9 @@ 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.0.read().unwrap();
let tm_raw = tm_pool.read_as_vec(&tm_addr).unwrap();
self.tm_buf[0..tm_raw.len()].copy_from_slice(&tm_raw);
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
self.tm_buf[0..tm_raw.len()].copy_from_slice(tm_raw);
PusTmReader::new(&self.tm_buf, 7).unwrap().0
}
@ -1097,9 +1070,9 @@ 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.0.read().unwrap();
let tm_raw = tm_pool.read_as_vec(&tm_addr).unwrap();
let tm = PusTmReader::new(&tm_raw, 7).unwrap().0;
let tm_pool = self.tm_pool.shared_pool.read().unwrap();
let tm_raw = tm_pool.read(&tm_addr).unwrap();
let tm = PusTmReader::new(tm_raw, 7).unwrap().0;
assert_eq!(PusPacket::service(&tm), 1);
assert_eq!(PusPacket::subservice(&tm), subservice);
assert_eq!(tm.apid(), TEST_APID);

View File

@ -8,15 +8,13 @@ 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, WritablePusPacket};
use spacepackets::time::{
CcsdsTimeProvider, TimeReader, TimeWriter, TimestampError, UnixTimestamp,
};
use spacepackets::{ByteConversionError, CcsdsPacket};
use spacepackets::ecss::{PusError, PusPacket};
use spacepackets::time::{CcsdsTimeProvider, TimeReader, TimestampError, UnixTimestamp};
use spacepackets::CcsdsPacket;
#[cfg(feature = "std")]
use std::error::Error;
use crate::pool::{PoolProvider, StoreError};
use crate::pool::{PoolProviderMemInPlace, StoreError};
#[cfg(feature = "alloc")]
pub use alloc_mod::*;
@ -156,9 +154,8 @@ pub enum ScheduleError {
StoreError(StoreError),
TcDataEmpty,
TimestampError(TimestampError),
WrongSubservice(u8),
WrongService(u8),
ByteConversionError(ByteConversionError),
WrongSubservice,
WrongService,
}
impl Display for ScheduleError {
@ -174,29 +171,26 @@ impl Display for ScheduleError {
} => {
write!(
f,
"time margin too short, current time: {current_time:?}, time margin: {time_margin:?}, release time: {release_time:?}"
"Error: time margin too short, current time: {current_time:?}, time margin: {time_margin:?}, release time: {release_time:?}"
)
}
ScheduleError::NestedScheduledTc => {
write!(f, "nested scheduling is not allowed")
write!(f, "Error: nested scheduling is not allowed")
}
ScheduleError::StoreError(e) => {
write!(f, "pus scheduling: {e}")
write!(f, "Store Error: {e}")
}
ScheduleError::TcDataEmpty => {
write!(f, "empty TC data field")
write!(f, "Error: empty Tc Data field")
}
ScheduleError::TimestampError(e) => {
write!(f, "pus scheduling: {e}")
write!(f, "Timestamp Error: {e}")
}
ScheduleError::WrongService(srv) => {
write!(f, "pus scheduling: wrong service number {srv}")
ScheduleError::WrongService => {
write!(f, "Error: Service not 11.")
}
ScheduleError::WrongSubservice(subsrv) => {
write!(f, "pus scheduling: wrong subservice number {subsrv}")
}
ScheduleError::ByteConversionError(e) => {
write!(f, "pus scheduling: {e}")
ScheduleError::WrongSubservice => {
write!(f, "Error: Subservice not 4.")
}
}
}
@ -204,45 +198,32 @@ impl Display for ScheduleError {
impl From<PusError> for ScheduleError {
fn from(e: PusError) -> Self {
Self::PusError(e)
ScheduleError::PusError(e)
}
}
impl From<StoreError> for ScheduleError {
fn from(e: StoreError) -> Self {
Self::StoreError(e)
ScheduleError::StoreError(e)
}
}
impl From<TimestampError> for ScheduleError {
fn from(e: TimestampError) -> Self {
Self::TimestampError(e)
}
}
impl From<ByteConversionError> for ScheduleError {
fn from(e: ByteConversionError) -> Self {
Self::ByteConversionError(e)
ScheduleError::TimestampError(e)
}
}
#[cfg(feature = "std")]
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,
}
}
}
impl Error for ScheduleError {}
/// Generic trait for scheduler objects which are able to schedule ECSS PUS C packets.
pub trait PusSchedulerProvider {
pub trait PusSchedulerInterface {
type TimeProvider: CcsdsTimeProvider + TimeReader;
fn reset(&mut self, store: &mut (impl PoolProvider + ?Sized)) -> Result<(), StoreError>;
fn reset(
&mut self,
store: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<(), StoreError>;
fn is_enabled(&self) -> bool;
@ -265,15 +246,13 @@ pub trait PusSchedulerProvider {
fn insert_wrapped_tc<TimeProvider>(
&mut self,
pus_tc: &(impl IsPusTelecommand + PusPacket + GenericPusTcSecondaryHeader),
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
if PusPacket::service(pus_tc) != 11 {
return Err(ScheduleError::WrongService(PusPacket::service(pus_tc)));
return Err(ScheduleError::WrongService);
}
if PusPacket::subservice(pus_tc) != 4 {
return Err(ScheduleError::WrongSubservice(PusPacket::subservice(
pus_tc,
)));
return Err(ScheduleError::WrongSubservice);
}
if pus_tc.user_data().is_empty() {
return Err(ScheduleError::TcDataEmpty);
@ -291,7 +270,7 @@ pub trait PusSchedulerProvider {
&mut self,
time_stamp: UnixTimestamp,
tc: &[u8],
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
let check_tc = PusTcReader::new(tc)?;
if PusPacket::service(&check_tc.0) == 11 && PusPacket::subservice(&check_tc.0) == 4 {
@ -310,38 +289,10 @@ pub trait PusSchedulerProvider {
}
}
/// 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::*;
use crate::pool::{PoolProvider, StoreAddr, StoreError};
use crate::pool::{PoolProviderMemInPlace, StoreAddr, StoreError};
use alloc::collections::btree_map::{Entry, Range};
use alloc::collections::BTreeMap;
use alloc::vec;
@ -356,19 +307,6 @@ 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>),
@ -377,7 +315,7 @@ pub mod alloc_mod {
/// This is the core data structure for scheduling PUS telecommands with [alloc] support.
///
/// It is assumed that the actual telecommand data is stored in a separate TC pool offering
/// a [crate::pool::PoolProvider] API. This data structure just tracks the store
/// a [crate::pool::PoolProviderMemInPlace] API. This data structure just tracks the store
/// addresses and their release times and offers a convenient API to insert and release
/// telecommands and perform other functionality specified by the ECSS standard in section 6.11.
/// The time is tracked as a [spacepackets::time::UnixTimestamp] but the only requirement to
@ -411,8 +349,6 @@ pub mod alloc_mod {
/// * `time_margin` - This time margin is used when inserting new telecommands into the
/// schedule. If the release time of a new telecommand is earlier than the time margin
/// added to the current time, it will not be inserted into the schedule.
/// * `tc_buf_size` - Buffer for temporary storage of telecommand packets. This buffer
/// should be large enough to accomodate the largest expected TC packets.
pub fn new(init_current_time: UnixTimestamp, time_margin: Duration) -> Self {
PusScheduler {
tc_map: Default::default(),
@ -476,7 +412,7 @@ pub mod alloc_mod {
&mut self,
time_stamp: UnixTimestamp,
tc: &[u8],
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
let check_tc = PusTcReader::new(tc)?;
if PusPacket::service(&check_tc.0) == 11 && PusPacket::subservice(&check_tc.0) == 4 {
@ -499,7 +435,7 @@ pub mod alloc_mod {
pub fn insert_wrapped_tc_cds_short(
&mut self,
pus_tc: &PusTc,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
self.insert_wrapped_tc::<cds::TimeProvider>(pus_tc, pool)
}
@ -509,7 +445,7 @@ pub mod alloc_mod {
pub fn insert_wrapped_tc_cds_long(
&mut self,
pus_tc: &PusTc,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<TcInfo, ScheduleError> {
self.insert_wrapped_tc::<cds::TimeProvider<DaysLen24Bits>>(pus_tc, pool)
}
@ -525,7 +461,7 @@ pub mod alloc_mod {
pub fn delete_by_time_filter<TimeProvider: CcsdsTimeProvider + Clone>(
&mut self,
time_window: TimeWindow<TimeProvider>,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<u64, (u64, StoreError)> {
let range = self.retrieve_by_time_filter(time_window);
let mut del_packets = 0;
@ -555,7 +491,7 @@ pub mod alloc_mod {
/// the last deletion will be supplied in addition to the number of deleted commands.
pub fn delete_all(
&mut self,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<u64, (u64, StoreError)> {
self.delete_by_time_filter(TimeWindow::<cds::TimeProvider>::new_select_all(), pool)
}
@ -613,7 +549,7 @@ pub mod alloc_mod {
pub fn delete_by_request_id_and_from_pool(
&mut self,
req_id: &RequestId,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<bool, StoreError> {
if let DeletionResult::WithStoreDeletion(v) =
self.delete_by_request_id_internal_with_store_deletion(req_id, pool)
@ -645,7 +581,7 @@ pub mod alloc_mod {
fn delete_by_request_id_internal_with_store_deletion(
&mut self,
req_id: &RequestId,
pool: &mut (impl PoolProvider + ?Sized),
pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> DeletionResult {
let mut idx_found = None;
for time_bucket in &mut self.tc_map {
@ -675,8 +611,7 @@ pub mod alloc_mod {
/// Utility method which calls [Self::telecommands_to_release] and then calls a releaser
/// closure for each telecommand which should be released. This function will also delete
/// the telecommands from the holding store after calling the release closure if the user
/// returns [true] from the release closure. A buffer must be provided to hold the
/// telecommands for the release process.
/// returns [true] from the release closure.
///
/// # Arguments
///
@ -686,55 +621,18 @@ pub mod alloc_mod {
/// note that returning false might lead to memory leaks if the TC is not cleared from
/// the store in some other way.
/// * `tc_store` - The holding store of the telecommands.
/// * `tc_buf` - Buffer to hold each telecommand being released.
pub fn release_telecommands_with_buffer<R: FnMut(bool, &TcInfo, &[u8]) -> bool>(
&mut self,
releaser: R,
tc_store: &mut (impl PoolProvider + ?Sized),
tc_buf: &mut [u8],
) -> Result<u64, (u64, StoreError)> {
self.release_telecommands_internal(releaser, tc_store, Some(tc_buf))
}
/// This functions is almost identical to [Self::release_telecommands_with_buffer] but does
/// not require a user provided TC buffer because it will always use the
/// [PoolProvider::read_as_vec] API to read the TC packets.
///
/// However, this might also perform frequent allocations for all telecommands being
/// released.
pub fn release_telecommands<R: FnMut(bool, &TcInfo, &[u8]) -> bool>(
&mut self,
releaser: R,
tc_store: &mut (impl PoolProvider + ?Sized),
) -> Result<u64, (u64, StoreError)> {
self.release_telecommands_internal(releaser, tc_store, None)
}
fn release_telecommands_internal<R: FnMut(bool, &TcInfo, &[u8]) -> bool>(
&mut self,
mut releaser: R,
tc_store: &mut (impl PoolProvider + ?Sized),
mut tc_buf: Option<&mut [u8]>,
tc_store: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<u64, (u64, StoreError)> {
let tcs_to_release = self.telecommands_to_release();
let mut released_tcs = 0;
let mut store_error = Ok(());
for tc in tcs_to_release {
for info in tc.1 {
let should_delete = match tc_buf.as_mut() {
Some(buf) => {
tc_store
.read(&info.addr, buf)
.map_err(|e| (released_tcs, e))?;
releaser(self.enabled, info, buf)
}
None => {
let tc = tc_store
.read_as_vec(&info.addr)
.map_err(|e| (released_tcs, e))?;
releaser(self.enabled, info, &tc)
}
};
let tc = tc_store.read(&info.addr).map_err(|e| (released_tcs, e))?;
let should_delete = releaser(self.enabled, info, tc);
released_tcs += 1;
if should_delete {
let res = tc_store.delete(info.addr);
@ -759,17 +657,16 @@ pub mod alloc_mod {
pub fn release_telecommands_no_deletion<R: FnMut(bool, &TcInfo, &[u8])>(
&mut self,
mut releaser: R,
tc_store: &(impl PoolProvider + ?Sized),
tc_buf: &mut [u8],
tc_store: &(impl PoolProviderMemInPlace + ?Sized),
) -> Result<Vec<TcInfo>, (Vec<TcInfo>, StoreError)> {
let tcs_to_release = self.telecommands_to_release();
let mut released_tcs = Vec::new();
for tc in tcs_to_release {
for info in tc.1 {
tc_store
.read(&info.addr, tc_buf)
let tc = tc_store
.read(&info.addr)
.map_err(|e| (released_tcs.clone(), e))?;
releaser(self.is_enabled(), info, tc_buf);
releaser(self.is_enabled(), info, tc);
released_tcs.push(*info);
}
}
@ -783,7 +680,7 @@ pub mod alloc_mod {
}
}
impl PusSchedulerProvider for PusScheduler {
impl PusSchedulerInterface for PusScheduler {
type TimeProvider = cds::TimeProvider;
/// This will disable the scheduler and clear the schedule as specified in 6.11.4.4.
@ -792,7 +689,10 @@ pub mod alloc_mod {
/// The holding store for the telecommands needs to be passed so all the stored telecommands
/// can be deleted to avoid a memory leak. If at last one deletion operation fails, the error
/// will be returned but the method will still try to delete all the commands in the schedule.
fn reset(&mut self, store: &mut (impl PoolProvider + ?Sized)) -> Result<(), StoreError> {
fn reset(
&mut self,
store: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<(), StoreError> {
self.enabled = false;
let mut deletion_ok = Ok(());
for tc_lists in &mut self.tc_map {
@ -850,13 +750,14 @@ pub mod alloc_mod {
mod tests {
use super::*;
use crate::pool::{
PoolProvider, StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreAddr, StoreError,
PoolProviderMemInPlace, StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreAddr,
StoreError,
};
use alloc::collections::btree_map::Range;
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
use spacepackets::ecss::WritablePusPacket;
use spacepackets::time::{cds, TimeWriter, UnixTimestamp};
use spacepackets::{PacketId, PacketSequenceCtrl, PacketType, SequenceFlags, SpHeader};
use spacepackets::SpHeader;
use std::time::Duration;
use std::vec::Vec;
#[allow(unused_imports)]
@ -934,7 +835,7 @@ mod tests {
}
#[test]
fn test_enable_api() {
fn basic() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
assert!(scheduler.is_enabled());
@ -945,8 +846,8 @@ mod tests {
}
#[test]
fn test_reset() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
fn 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));
@ -956,7 +857,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(100),
TcInfo::new(tc_info_0.addr, tc_info_0.request_id),
TcInfo::new(tc_info_0.addr.clone(), tc_info_0.request_id),
)
.unwrap();
@ -965,7 +866,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(200),
TcInfo::new(tc_info_1.addr, tc_info_1.request_id),
TcInfo::new(tc_info_1.addr.clone(), tc_info_1.request_id),
)
.unwrap();
@ -974,7 +875,7 @@ mod tests {
scheduler
.insert_unwrapped_and_stored_tc(
UnixTimestamp::new_only_seconds(300),
TcInfo::new(tc_info_2.addr(), tc_info_2.request_id()),
TcInfo::new(tc_info_2.addr().clone(), tc_info_2.request_id()),
)
.unwrap();
@ -1049,7 +950,7 @@ mod tests {
}
#[test]
fn test_time_update() {
fn time() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let time = UnixTimestamp::new(1, 2).unwrap();
@ -1063,7 +964,7 @@ mod tests {
expected_store_addrs: Vec<StoreAddr>,
counter: &mut usize,
) {
assert!(enabled);
assert_eq!(enabled, true);
assert!(expected_store_addrs.contains(store_addr));
*counter += 1;
}
@ -1073,13 +974,13 @@ mod tests {
expected_store_addrs: Vec<StoreAddr>,
counter: &mut usize,
) {
assert!(!enabled);
assert_eq!(enabled, false);
assert!(expected_store_addrs.contains(store_addr));
*counter += 1;
}
#[test]
fn test_request_id() {
fn request_id() {
let src_id_to_set = 12;
let apid_to_set = 0x22;
let seq_count = 105;
@ -1097,8 +998,8 @@ mod tests {
);
}
#[test]
fn test_release_telecommands() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
fn release_basic() {
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));
@ -1123,9 +1024,8 @@ mod tests {
// test 1: too early, no tcs
scheduler.update_time(UnixTimestamp::new_only_seconds(99));
let mut tc_buf: [u8; 128] = [0; 128];
scheduler
.release_telecommands_with_buffer(&mut test_closure_1, &mut pool, &mut tc_buf)
.release_telecommands(&mut test_closure_1, &mut pool)
.expect("deletion failed");
// test 2: exact time stamp of tc, releases 1 tc
@ -1147,7 +1047,7 @@ mod tests {
scheduler.update_time(UnixTimestamp::new_only_seconds(206));
released = scheduler
.release_telecommands_with_buffer(&mut test_closure_2, &mut pool, &mut tc_buf)
.release_telecommands(&mut test_closure_2, &mut pool)
.expect("deletion failed");
assert_eq!(released, 1);
// TC is deleted.
@ -1164,7 +1064,7 @@ mod tests {
#[test]
fn release_multi_with_same_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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));
@ -1193,10 +1093,9 @@ mod tests {
// test 1: too early, no tcs
scheduler.update_time(UnixTimestamp::new_only_seconds(99));
let mut tc_buf: [u8; 128] = [0; 128];
let mut released = scheduler
.release_telecommands_with_buffer(&mut test_closure, &mut pool, &mut tc_buf)
.release_telecommands(&mut test_closure, &mut pool)
.expect("deletion failed");
assert_eq!(released, 0);
@ -1222,7 +1121,7 @@ mod tests {
#[test]
fn release_with_scheduler_disabled() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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));
@ -1246,13 +1145,11 @@ mod tests {
true
};
let mut tc_buf: [u8; 128] = [0; 128];
// test 1: too early, no tcs
scheduler.update_time(UnixTimestamp::new_only_seconds(99));
scheduler
.release_telecommands_with_buffer(&mut test_closure_1, &mut pool, &mut tc_buf)
.release_telecommands(&mut test_closure_1, &mut pool)
.expect("deletion failed");
// test 2: exact time stamp of tc, releases 1 tc
@ -1292,7 +1189,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 32] = [0; 32];
let tc_info_0 = ping_tc_to_store(&mut pool, &mut buf, 0, None);
@ -1306,9 +1203,8 @@ mod tests {
assert!(pool.has_element_at(&tc_info_0.addr()).unwrap());
let mut read_buf: [u8; 64] = [0; 64];
pool.read(&tc_info_0.addr(), &mut read_buf).unwrap();
let check_tc = PusTcReader::new(&read_buf).expect("incorrect Pus tc raw data");
let data = pool.read(&tc_info_0.addr()).unwrap();
let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data");
assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None));
assert_eq!(scheduler.num_scheduled_telecommands(), 1);
@ -1329,9 +1225,8 @@ mod tests {
.release_telecommands(&mut test_closure, &mut pool)
.unwrap();
let read_len = pool.read(&addr_vec[0], &mut read_buf).unwrap();
let check_tc = PusTcReader::new(&read_buf).expect("incorrect Pus tc raw data");
assert_eq!(read_len, check_tc.1);
let data = pool.read(&addr_vec[0]).unwrap();
let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data");
assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None));
}
@ -1340,7 +1235,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 32] = [0; 32];
let tc = scheduled_tc(UnixTimestamp::new_only_seconds(100), &mut buf);
@ -1354,9 +1249,8 @@ mod tests {
assert!(pool.has_element_at(&info.addr).unwrap());
let read_len = pool.read(&info.addr, &mut buf).unwrap();
let check_tc = PusTcReader::new(&buf).expect("incorrect Pus tc raw data");
assert_eq!(read_len, check_tc.1);
let data = pool.read(&info.addr).unwrap();
let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data");
assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None));
assert_eq!(scheduler.num_scheduled_telecommands(), 1);
@ -1373,15 +1267,12 @@ mod tests {
false
};
let mut tc_buf: [u8; 64] = [0; 64];
scheduler
.release_telecommands_with_buffer(&mut test_closure, &mut pool, &mut tc_buf)
.release_telecommands(&mut test_closure, &mut pool)
.unwrap();
let read_len = pool.read(&addr_vec[0], &mut buf).unwrap();
let check_tc = PusTcReader::new(&buf).expect("incorrect PUS tc raw data");
assert_eq!(read_len, check_tc.1);
let data = pool.read(&addr_vec[0]).unwrap();
let check_tc = PusTcReader::new(data).expect("incorrect PUS tc raw data");
assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None));
}
@ -1390,7 +1281,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 32] = [0; 32];
let tc = wrong_tc_service(UnixTimestamp::new_only_seconds(100), &mut buf);
@ -1399,9 +1290,7 @@ mod tests {
assert!(err.is_err());
let err = err.unwrap_err();
match err {
ScheduleError::WrongService(wrong_service) => {
assert_eq!(wrong_service, 12);
}
ScheduleError::WrongService => {}
_ => {
panic!("unexpected error")
}
@ -1413,7 +1302,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 32] = [0; 32];
let tc = wrong_tc_subservice(UnixTimestamp::new_only_seconds(100), &mut buf);
@ -1422,9 +1311,7 @@ mod tests {
assert!(err.is_err());
let err = err.unwrap_err();
match err {
ScheduleError::WrongSubservice(wrong_subsrv) => {
assert_eq!(wrong_subsrv, 5);
}
ScheduleError::WrongSubservice => {}
_ => {
panic!("unexpected error")
}
@ -1435,7 +1322,7 @@ mod tests {
fn insert_wrapped_tc_faulty_app_data() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let tc = invalid_time_tagged_cmd();
let insert_res = scheduler.insert_wrapped_tc::<cds::TimeProvider>(&tc, &mut pool);
assert!(insert_res.is_err());
@ -1450,7 +1337,7 @@ mod tests {
fn insert_doubly_wrapped_time_tagged_cmd() {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 64] = [0; 64];
let tc = double_wrapped_time_tagged_tc(UnixTimestamp::new_only_seconds(50), &mut buf);
let insert_res = scheduler.insert_wrapped_tc::<cds::TimeProvider>(&tc, &mut pool);
@ -1486,7 +1373,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)]));
let mut buf: [u8; 32] = [0; 32];
@ -1510,7 +1397,7 @@ mod tests {
#[test]
fn test_store_error_propagation_release() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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];
@ -1545,7 +1432,7 @@ mod tests {
#[test]
fn test_store_error_propagation_reset() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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];
@ -1569,7 +1456,7 @@ mod tests {
#[test]
fn test_delete_by_req_id_simple_retrieve_addr() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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];
@ -1588,7 +1475,7 @@ mod tests {
#[test]
fn test_delete_by_req_id_simple_delete_all() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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];
@ -1600,14 +1487,14 @@ 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!(del_res.unwrap());
assert_eq!(del_res.unwrap(), true);
assert!(!pool.has_element_at(&tc_info_0.addr()).unwrap());
assert_eq!(scheduler.num_scheduled_telecommands(), 0);
}
#[test]
fn test_delete_by_req_id_complex() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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];
@ -1654,7 +1541,7 @@ mod tests {
let mut scheduler =
PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(1, 64)], false));
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(1, 64)]));
let mut buf: [u8; 32] = [0; 32];
// Store is full after this.
@ -1693,7 +1580,7 @@ mod tests {
#[test]
fn test_time_window_retrieval_select_all() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 tc_info_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1724,7 +1611,7 @@ mod tests {
#[test]
fn test_time_window_retrieval_select_from_stamp() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 _ = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1755,7 +1642,7 @@ mod tests {
#[test]
fn test_time_window_retrieval_select_to_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 tc_info_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1786,7 +1673,7 @@ mod tests {
#[test]
fn test_time_window_retrieval_select_from_time_to_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 _ = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1821,7 +1708,7 @@ mod tests {
#[test]
fn test_deletion_all() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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));
insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1848,7 +1735,7 @@ mod tests {
#[test]
fn test_deletion_from_start_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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));
insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1869,7 +1756,7 @@ mod tests {
#[test]
fn test_deletion_to_end_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 cmd_0_to_delete = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1891,7 +1778,7 @@ mod tests {
#[test]
fn test_deletion_from_start_time_to_end_time() {
let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)], false));
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 cmd_out_of_range_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50);
@ -1917,110 +1804,4 @@ 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)], false));
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 mut tc_buf: [u8; 64] = [0; 64];
let tc_info_vec = scheduler
.release_telecommands_no_deletion(&mut test_closure_1, &pool, &mut tc_buf)
.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

@ -1,6 +1,6 @@
use super::scheduler::PusSchedulerProvider;
use super::scheduler::PusSchedulerInterface;
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
use crate::pool::PoolProvider;
use crate::pool::PoolProviderMemInPlace;
use crate::pus::{PusPacketHandlerResult, PusPacketHandlingError};
use alloc::string::ToString;
use spacepackets::ecss::{scheduling, PusPacket};
@ -16,13 +16,13 @@ use spacepackets::time::cds::TimeProvider;
/// telecommands when applicable.
pub struct PusService11SchedHandler<
TcInMemConverter: EcssTcInMemConverter,
PusScheduler: PusSchedulerProvider,
Scheduler: PusSchedulerInterface,
> {
pub service_helper: PusServiceHelper<TcInMemConverter>,
scheduler: PusScheduler,
scheduler: Scheduler,
}
impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerProvider>
impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerInterface>
PusService11SchedHandler<TcInMemConverter, Scheduler>
{
pub fn new(service_helper: PusServiceHelper<TcInMemConverter>, scheduler: Scheduler) -> Self {
@ -42,7 +42,7 @@ impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerProvider>
pub fn handle_one_tc(
&mut self,
sched_tc_pool: &mut (impl PoolProvider + ?Sized),
sched_tc_pool: &mut (impl PoolProviderMemInPlace + ?Sized),
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
@ -173,7 +173,7 @@ mod tests {
use crate::pool::{StaticMemoryPool, StaticPoolConfig};
use crate::pus::tests::TEST_APID;
use crate::pus::{
scheduler::{self, PusSchedulerProvider, TcInfo},
scheduler::{self, PusSchedulerInterface, TcInfo},
tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness},
verification::{RequestId, TcStateAccepted, VerificationToken},
EcssTcInSharedStoreConverter,
@ -201,7 +201,7 @@ mod tests {
impl Pus11HandlerWithStoreTester {
pub fn new() -> Self {
let test_scheduler = TestScheduler::default();
let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)], false);
let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)]);
let sched_tc_pool = StaticMemoryPool::new(pool_cfg.clone());
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
Self {
@ -232,12 +232,12 @@ mod tests {
inserted_tcs: VecDeque<TcInfo>,
}
impl PusSchedulerProvider for TestScheduler {
impl PusSchedulerInterface for TestScheduler {
type TimeProvider = cds::TimeProvider;
fn reset(
&mut self,
_store: &mut (impl crate::pool::PoolProvider + ?Sized),
_store: &mut (impl crate::pool::PoolProviderMemInPlace + ?Sized),
) -> Result<(), crate::pool::StoreError> {
self.reset_count += 1;
Ok(())

View File

@ -15,11 +15,11 @@
//! ```
//! use std::sync::{Arc, mpsc, RwLock};
//! use std::time::Duration;
//! use satrs_core::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
//! use satrs_core::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig};
//! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
//! use satrs_core::seq_count::SeqCountProviderSimple;
//! use satrs_core::pus::MpscTmInSharedPoolSender;
//! use satrs_core::tmtc::tm_helper::SharedTmPool;
//! use satrs_core::pus::MpscTmInStoreSender;
//! use satrs_core::tmtc::tm_helper::SharedTmStore;
//! use spacepackets::ecss::PusPacket;
//! use spacepackets::SpHeader;
//! use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader};
@ -28,12 +28,12 @@
//! const EMPTY_STAMP: [u8; 7] = [0; 7];
//! const TEST_APID: u16 = 0x02;
//!
//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)], false);
//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
//! let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
//! let shared_tm_store = SharedTmPool::new(tm_pool);
//! let shared_tm_store = SharedTmStore::new(tm_pool);
//! let tm_store = shared_tm_store.clone_backing_pool();
//! let (verif_tx, verif_rx) = mpsc::channel();
//! let sender = MpscTmInSharedPoolSender::new(0, "Test Sender", shared_tm_store, verif_tx);
//! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx);
//! let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
//! let mut reporter = VerificationReporterWithSender::new(&cfg , Box::new(sender));
//!
@ -56,7 +56,9 @@
//! {
//! let mut rg = tm_store.write().expect("Error locking shared pool");
//! let store_guard = rg.read_with_guard(addr);
//! tm_len = store_guard.read(&mut tm_buf).expect("Error reading TM slice");
//! let slice = store_guard.read().expect("Error reading TM slice");
//! tm_len = slice.len();
//! tm_buf[0..tm_len].copy_from_slice(slice);
//! }
//! let (pus_tm, _) = PusTmReader::new(&tm_buf[0..tm_len], 7)
//! .expect("Error reading verification TM");
@ -1323,15 +1325,15 @@ mod std_mod {
#[cfg(test)]
mod tests {
use crate::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
use crate::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig};
use crate::pus::tests::CommonTmInfo;
use crate::pus::verification::{
EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone,
VerificationReporter, VerificationReporterCfg, VerificationReporterWithSender,
VerificationToken,
};
use crate::pus::{EcssChannel, MpscTmInSharedPoolSender, PusTmWrapper};
use crate::tmtc::tm_helper::SharedTmPool;
use crate::pus::{EcssChannel, MpscTmInStoreSender, PusTmWrapper};
use crate::tmtc::tm_helper::SharedTmStore;
use crate::ChannelId;
use alloc::boxed::Box;
use alloc::format;
@ -1484,11 +1486,10 @@ mod tests {
#[test]
fn test_mpsc_verif_send_sync() {
let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)], false));
let shared_tm_store = SharedTmPool::new(pool);
let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)]));
let shared_tm_store = SharedTmStore::new(pool);
let (tx, _) = mpsc::channel();
let mpsc_verif_sender =
MpscTmInSharedPoolSender::new(0, "verif_sender", shared_tm_store, tx);
let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx);
is_send(&mpsc_verif_sender);
}
@ -1545,7 +1546,7 @@ mod tests {
let mut sender = TestSender::default();
let fail_code = EcssEnumU16::new(2);
let fail_params = FailParams::new(Some(stamp_buf.as_slice()), &fail_code, None);
b.vr.acceptance_failure(tok, &sender, fail_params)
b.vr.acceptance_failure(tok, &mut sender, fail_params)
.expect("Sending acceptance success failed");
acceptance_fail_check(&mut sender, tok.req_id, stamp_buf);
}
@ -1680,10 +1681,12 @@ mod tests {
);
let accepted_token =
b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
.expect("Sending acceptance success failed");
b.vr.start_failure(accepted_token, &mut sender, fail_params)
.expect("Start failure failure");
let empty =
b.vr.start_failure(accepted_token, &mut sender, fail_params)
.expect("Start failure failure");
assert_eq!(empty, ());
start_fail_check(&mut sender, tok.req_id, fail_data_raw);
}
@ -1775,23 +1778,23 @@ mod tests {
let mut sender = TestSender::default();
let accepted_token = b
.rep()
.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
.expect("Sending acceptance success failed");
let started_token = b
.rep()
.start_success(accepted_token, &sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
.expect("Sending start success failed");
b.rep()
.step_success(
&started_token,
&sender,
&mut sender,
Some(&EMPTY_STAMP),
EcssEnumU8::new(0),
)
.expect("Sending step 0 success failed");
b.vr.step_success(
&started_token,
&sender,
&mut sender,
Some(&EMPTY_STAMP),
EcssEnumU8::new(1),
)
@ -2135,15 +2138,14 @@ mod tests {
}
#[test]
// TODO: maybe a bit more extensive testing, all I have time for right now
fn test_seq_count_increment() {
let pool_cfg =
StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)], false);
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
let shared_tm_store = SharedTmPool::new(tm_pool);
let shared_tm_store = SharedTmStore::new(tm_pool);
let shared_tm_pool = shared_tm_store.clone_backing_pool();
let (verif_tx, verif_rx) = mpsc::channel();
let sender =
MpscTmInSharedPoolSender::new(0, "Verification Sender", shared_tm_store, verif_tx);
let sender = MpscTmInStoreSender::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));
@ -2172,9 +2174,9 @@ mod tests {
{
let mut rg = shared_tm_pool.write().expect("Error locking shared pool");
let store_guard = rg.read_with_guard(addr);
tm_len = store_guard
.read(&mut tm_buf)
.expect("Error reading TM slice");
let slice = store_guard.read().expect("Error reading TM slice");
tm_len = slice.len();
tm_buf[0..tm_len].copy_from_slice(slice);
}
let (pus_tm, _) =
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");

View File

@ -8,38 +8,39 @@ pub use std_mod::*;
#[cfg(feature = "std")]
pub mod std_mod {
use crate::pool::{PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr};
use crate::pool::{
PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr,
};
use crate::pus::EcssTmtcError;
use spacepackets::ecss::tm::PusTmCreator;
use spacepackets::ecss::WritablePusPacket;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct SharedTmPool(pub SharedStaticMemoryPool);
pub struct SharedTmStore {
pub shared_pool: SharedStaticMemoryPool,
}
impl SharedTmPool {
impl SharedTmStore {
pub fn new(shared_pool: StaticMemoryPool) -> Self {
Self(Arc::new(RwLock::new(shared_pool)))
Self {
shared_pool: Arc::new(RwLock::new(shared_pool)),
}
}
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.0.clone()
}
pub fn shared_pool(&self) -> &SharedStaticMemoryPool {
&self.0
}
pub fn shared_pool_mut(&mut self) -> &mut SharedStaticMemoryPool {
&mut self.0
self.shared_pool.clone()
}
pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result<StoreAddr, EcssTmtcError> {
let mut pg = self.0.write().map_err(|_| EcssTmtcError::StoreLock)?;
let addr = pg.free_element(pus_tm.len_written(), |buf| {
pus_tm
.write_to_bytes(buf)
.expect("writing PUS TM to store failed");
})?;
let mut pg = self
.shared_pool
.write()
.map_err(|_| EcssTmtcError::StoreLock)?;
let (addr, buf) = pg.free_element(pus_tm.len_written())?;
pus_tm
.write_to_bytes(buf)
.expect("writing PUS TM to store failed");
Ok(addr)
}
}

View File

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

View File

@ -1,14 +1,15 @@
//#[cfg(feature = "crossbeam")]
#[cfg(feature = "crossbeam")]
pub mod crossbeam_test {
use hashbrown::HashMap;
use satrs_core::pool::{
PoolProvider, PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig,
PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, StaticMemoryPool,
StaticPoolConfig,
};
use satrs_core::pus::verification::{
FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender,
};
use satrs_core::pus::CrossbeamTmInStoreSender;
use satrs_core::tmtc::tm_helper::SharedTmPool;
use satrs_core::tmtc::tm_helper::SharedTmStore;
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
use spacepackets::ecss::tm::PusTmReader;
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, WritablePusPacket};
@ -35,14 +36,13 @@ pub mod crossbeam_test {
// each reporter have an own sequence count provider.
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
// Shared pool object to store the verification PUS telemetry
let pool_cfg =
StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)], false);
let shared_tm_pool = SharedTmPool::new(StaticMemoryPool::new(pool_cfg.clone()));
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_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_pool.clone(), tx.clone());
CrossbeamTmInStoreSender::new(0, "verif_sender", shared_tm_store.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();
@ -59,21 +59,15 @@ pub mod crossbeam_test {
let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
req_id_0 = RequestId::new(&pus_tc_0);
let addr = tc_guard
.free_element(pus_tc_0.len_written(), |buf| {
pus_tc_0.write_to_bytes(buf).unwrap();
})
.unwrap();
let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_0.write_to_bytes(buf).unwrap();
tx_tc_0.send(addr).unwrap();
let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap();
let tc_header = PusTcSecondaryHeader::new_simple(5, 1);
let pus_tc_1 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
req_id_1 = RequestId::new(&pus_tc_1);
let addr = tc_guard
.free_element(pus_tc_0.len_written(), |buf| {
pus_tc_1.write_to_bytes(buf).unwrap();
})
.unwrap();
let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap();
pus_tc_1.write_to_bytes(buf).unwrap();
tx_tc_1.send(addr).unwrap();
}
let verif_sender_0 = thread::spawn(move || {
@ -85,7 +79,9 @@ pub mod crossbeam_test {
{
let mut tc_guard = shared_tc_pool_0.write().unwrap();
let pg = tc_guard.read_with_guard(tc_addr);
tc_len = pg.read(&mut tc_buf).unwrap();
let buf = pg.read().unwrap();
tc_len = buf.len();
tc_buf[0..tc_len].copy_from_slice(buf);
}
let (_tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
@ -121,7 +117,9 @@ pub mod crossbeam_test {
{
let mut tc_guard = shared_tc_pool_1.write().unwrap();
let pg = tc_guard.read_with_guard(tc_addr);
tc_len = pg.read(&mut tc_buf).unwrap();
let buf = pg.read().unwrap();
tc_len = buf.len();
tc_buf[0..tc_len].copy_from_slice(buf);
}
let (tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
let token = reporter_with_sender_1.add_tc(&tc);
@ -147,13 +145,13 @@ pub mod crossbeam_test {
.recv_timeout(Duration::from_millis(50))
.expect("Packet reception timeout");
let tm_len;
let shared_tm_store = shared_tm_pool.clone_backing_pool();
let shared_tm_store = shared_tm_store.clone_backing_pool();
{
let mut rg = shared_tm_store.write().expect("Error locking shared pool");
let store_guard = rg.read_with_guard(verif_addr);
tm_len = store_guard
.read(&mut tm_buf)
.expect("Error reading TM slice");
let slice = store_guard.read().expect("Error reading TM slice");
tm_len = slice.len();
tm_buf[0..tm_len].copy_from_slice(slice);
}
let (pus_tm, _) =
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");

View File

@ -26,7 +26,3 @@ path = "../satrs-core"
[dependencies.satrs-mib]
# version = "0.1.0-alpha.1"
path = "../satrs-mib"
[features]
dyn_tmtc = []
default = ["dyn_tmtc"]

View File

@ -3,32 +3,10 @@ sat-rs example
This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
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.
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
You can run the application using `cargo run`. The `simpleclient` binary target sends a
ping telecommand and then verifies the telemetry generated by the example application.
It can be run like this:
@ -39,7 +17,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).
@ -68,7 +46,31 @@ as Python code. For example, you can use the following command to send a ping li
the `simpleclient`:
```sh
./main.py -p /test/ping
./main.py -s test -o ping
```
You can also simply call the script without any arguments to view the command tree.
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.

View File

@ -1,117 +0,0 @@
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::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT};
use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::Duration;

View File

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

View File

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

View File

@ -1,187 +0,0 @@
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,12 +1,17 @@
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;
pub mod config;
use satrs_mib::res_code::{ResultU16, ResultU16Info};
use satrs_mib::resultcode;
pub type Apid = u16;
@ -57,3 +62,96 @@ 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,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +1,17 @@
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::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
};
use satrs_core::spacepackets::ecss::tc::PusTcReader;
use satrs_core::spacepackets::ecss::PusPacket;
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 satrs_example::{tmtc_err, TargetIdWithApid};
use std::collections::HashMap;
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 }
}
use std::sync::mpsc::Sender;
pub struct PusService8ActionHandler<TcInMemConverter: EcssTcInMemConverter> {
service_helper: PusServiceHelper<TcInMemConverter>,
@ -203,11 +141,11 @@ impl<TcInMemConverter: EcssTcInMemConverter> PusService8ActionHandler<TcInMemCon
}
}
pub struct Pus8Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub(crate) pus_8_handler: PusService8ActionHandler<TcInMemConverter>,
pub struct Pus8Wrapper {
pub(crate) pus_8_handler: PusService8ActionHandler<EcssTcInSharedStoreConverter>,
}
impl<TcInMemConverter: EcssTcInMemConverter> Pus8Wrapper<TcInMemConverter> {
impl Pus8Wrapper {
pub fn handle_next_packet(&mut self) -> bool {
match self.pus_8_handler.handle_one_tc() {
Ok(result) => match result {

View File

@ -1,85 +1,12 @@
use std::sync::mpsc;
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};
use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult};
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 }
pub struct Pus5Wrapper {
pub pus_5_handler: PusService5EventHandler<EcssTcInSharedStoreConverter>,
}
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> {
impl Pus5Wrapper {
pub fn handle_next_packet(&mut self) -> bool {
match self.pus_5_handler.handle_one_tc() {
Ok(result) => match result {

View File

@ -1,73 +1,15 @@
use crate::requests::{Request, RequestWithToken};
use log::{error, warn};
use satrs_core::hk::{CollectionIntervalFactor, HkRequest};
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
use satrs_core::pus::verification::{
FailParams, StdVerifReporterWithSender, VerificationReporterWithSender,
};
use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender};
use satrs_core::pus::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender,
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
};
use satrs_core::spacepackets::ecss::{hk, PusPacket};
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 satrs_example::{hk_err, tmtc_err, TargetIdWithApid};
use std::collections::HashMap;
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 }
}
use std::sync::mpsc::Sender;
pub struct PusService3HkHandler<TcInMemConverter: EcssTcInMemConverter> {
psb: PusServiceHelper<TcInMemConverter>,
@ -205,11 +147,11 @@ impl<TcInMemConverter: EcssTcInMemConverter> PusService3HkHandler<TcInMemConvert
}
}
pub struct Pus3Wrapper<TcInMemConverter: EcssTcInMemConverter> {
pub(crate) pus_3_handler: PusService3HkHandler<TcInMemConverter>,
pub struct Pus3Wrapper {
pub(crate) pus_3_handler: PusService3HkHandler<EcssTcInSharedStoreConverter>,
}
impl<TcInMemConverter: EcssTcInMemConverter> Pus3Wrapper<TcInMemConverter> {
impl Pus3Wrapper {
pub fn handle_next_packet(&mut self) -> bool {
match self.pus_3_handler.handle_one_tc() {
Ok(result) => match result {

View File

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

View File

@ -1,52 +0,0 @@
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,89 +1,22 @@
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, VerificationReporterWithSender};
use satrs_core::pus::{
EcssTcAndToken, EcssTcInMemConverter, EcssTcInVecConverter, MpscTcReceiver, MpscTmAsVecSender,
MpscTmInSharedPoolSender, PusPacketHandlerResult, PusServiceHelper,
};
use satrs_core::pus::verification::FailParams;
use satrs_core::pus::{EcssTcInMemConverter, PusPacketHandlerResult};
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::config::{tmtc_err, TcReceiverId, TmSenderId, PUS_APID, TEST_EVENT};
use std::sync::mpsc::{self, Sender};
use satrs_example::{tmtc_err, TEST_EVENT};
use std::sync::mpsc::Sender;
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 struct Service17CustomWrapper {
pub pus17_handler: PusService17TestHandler<EcssTcInSharedStoreConverter>,
pub test_srv_event_sender: Sender<(EventU32, Option<Params>)>,
}
impl<TcInMemConverter: EcssTcInMemConverter> Service17CustomWrapper<TcInMemConverter> {
impl Service17CustomWrapper {
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_with_apid: TargetIdWithApid,
pub(crate) target_id: TargetIdWithApid,
pub(crate) request: Request,
}

View File

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

View File

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

View File

@ -1,15 +1,34 @@
use log::warn;
use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc};
use satrs_core::spacepackets::SpHeader;
use std::sync::mpsc::{self, Receiver, SendError, Sender, TryRecvError};
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
use thiserror::Error;
use crate::pus::PusReceiver;
use satrs_core::pool::{PoolProvider, SharedStaticMemoryPool, StoreAddr, StoreError};
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}")]
@ -21,48 +40,45 @@ pub enum MpscStoreAndSendError {
}
#[derive(Clone)]
pub struct SharedTcPool {
pub struct TcStore {
pub pool: SharedStaticMemoryPool,
}
impl SharedTcPool {
impl TcStore {
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 = pg.free_element(pus_tc.len_packed(), |buf| {
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
})?;
let (addr, buf) = pg.free_element(pus_tc.len_packed())?;
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
Ok(addr)
}
}
pub struct TmFunnel {
pub tm_funnel_rx: Receiver<StoreAddr>,
pub tm_server_tx: Sender<StoreAddr>,
}
#[derive(Clone)]
pub struct PusTcSourceProviderSharedPool {
pub struct PusTcSource {
pub tc_source: Sender<StoreAddr>,
pub shared_pool: SharedTcPool,
pub tc_store: TcStore,
}
impl PusTcSourceProviderSharedPool {
#[allow(dead_code)]
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
self.shared_pool.pool.clone()
}
}
impl ReceivesEcssPusTc for PusTcSourceProviderSharedPool {
impl ReceivesEcssPusTc for PusTcSource {
type Error = MpscStoreAndSendError;
fn pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
let addr = self.shared_pool.add_pus_tc(pus_tc)?;
let addr = self.tc_store.add_pus_tc(pus_tc)?;
self.tc_source.send(addr)?;
Ok(())
}
}
impl ReceivesCcsdsTc for PusTcSourceProviderSharedPool {
impl ReceivesCcsdsTc for PusTcSource {
type Error = MpscStoreAndSendError;
fn pass_ccsds(&mut self, _: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut pool = self.shared_pool.pool.write().expect("locking pool failed");
let mut pool = self.tc_store.pool.write().expect("locking pool failed");
let addr = pool.add(tc_raw)?;
drop(pool);
self.tc_source.send(addr)?;
@ -70,45 +86,16 @@ impl ReceivesCcsdsTc for PusTcSourceProviderSharedPool {
}
}
// 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>,
pub struct TmtcTask {
tc_args: TcArgs,
tc_buf: [u8; 4096],
pus_receiver: PusReceiver,
}
impl TcSourceTaskStatic {
pub fn new(
shared_tc_pool: SharedTcPool,
tc_receiver: Receiver<StoreAddr>,
pus_receiver: PusReceiver,
) -> Self {
impl TmtcTask {
pub fn new(tc_args: TcArgs, pus_receiver: PusReceiver) -> Self {
Self {
shared_tc_pool,
tc_receiver,
tc_args,
tc_buf: [0; 4096],
pus_receiver,
}
@ -119,15 +106,17 @@ impl TcSourceTaskStatic {
}
pub fn poll_tc(&mut self) -> bool {
match self.tc_receiver.try_recv() {
match self.tc_args.tc_receiver.try_recv() {
Ok(addr) => {
let pool = self
.shared_tc_pool
.tc_args
.tc_source
.tc_store
.pool
.read()
.expect("locking tc pool failed");
pool.read(&addr, &mut self.tc_buf)
.expect("reading pool failed");
let data = pool.read(&addr).expect("reading pool failed");
self.tc_buf[0..data.len()].copy_from_slice(data);
drop(pool);
match PusTcReader::new(&self.tc_buf) {
Ok((pus_tc, _)) => {
@ -157,51 +146,3 @@ impl TcSourceTaskStatic {
}
}
}
// 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,83 +1,24 @@
use std::{
net::{SocketAddr, UdpSocket},
sync::mpsc::Receiver,
};
use std::{net::SocketAddr, sync::mpsc::Receiver};
use log::{info, warn};
use satrs_core::{
hal::std::udp_server::{ReceiveResult, UdpTcServer},
pool::{PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr},
pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr},
tmtc::CcsdsError,
};
pub trait UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
use crate::tmtc::MpscStoreAndSendError;
pub struct StaticUdpTmHandler {
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
pub tm_rx: Receiver<StoreAddr>,
pub tm_store: SharedStaticMemoryPool,
}
impl UdpTmHandler for StaticUdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, &recv_addr: &SocketAddr) {
while let Ok(addr) = self.tm_rx.try_recv() {
let store_lock = self.tm_store.write();
if store_lock.is_err() {
warn!("Locking TM store failed");
continue;
}
let mut store_lock = store_lock.unwrap();
let pg = store_lock.read_with_guard(addr);
let read_res = pg.read_as_vec();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
let result = socket.send_to(&buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct DynamicUdpTmHandler {
pub tm_rx: Receiver<Vec<u8>>,
}
impl UdpTmHandler for DynamicUdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
while let Ok(tm) = self.tm_rx.try_recv() {
if tm.len() > 9 {
let service = tm[7];
let subservice = tm[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = socket.send_to(&tm, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
pub struct UdpTmtcServer<TmHandler: UdpTmHandler, SendError> {
pub udp_tc_server: UdpTcServer<CcsdsError<SendError>>,
pub tm_handler: TmHandler,
}
impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
UdpTmtcServer<TmHandler, SendError>
{
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.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
self.send_tm_to_udp_client(&recv_addr);
}
}
@ -91,7 +32,7 @@ impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
true
}
CcsdsError::CustomError(e) => {
warn!("mpsc custom error {e:?}");
warn!("mpsc store and send error {e:?}");
true
}
},
@ -103,113 +44,33 @@ impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
},
}
}
}
#[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);
fn send_tm_to_udp_client(&mut self, recv_addr: &SocketAddr) {
while let Ok(addr) = self.tm_rx.try_recv() {
let store_lock = self.tm_store.write();
if store_lock.is_err() {
warn!("Locking TM store failed");
continue;
}
let mut store_lock = store_lock.unwrap();
let pg = store_lock.read_with_guard(addr);
let read_res = pg.read();
if read_res.is_err() {
warn!("Error reading TM pool data");
continue;
}
let buf = read_res.unwrap();
if buf.len() > 9 {
let service = buf[7];
let subservice = buf[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
let result = self.udp_tc_server.socket.send_to(buf, recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}

View File

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

View File

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