Compare commits
119 Commits
satrs-core
...
d017b9c179
Author | SHA1 | Date | |
---|---|---|---|
d017b9c179
|
|||
18a5095d0f | |||
66d9da23b3
|
|||
930da294ad | |||
729ef4be05
|
|||
0eb1a9cb08 | |||
5ccc50d9ec
|
|||
176a9f1612
|
|||
cb8405ca65 | |||
f68221a73f
|
|||
0fd70c08c2
|
|||
28da48ca6e | |||
292ba1f1cd
|
|||
8a5b81b67f | |||
414bda6751
|
|||
d1bc00f27c | |||
a891b947c7 | |||
6152c834d4 | |||
e2941d34ca
|
|||
4ace46e141
|
|||
134feeb1b4
|
|||
e2086391bc
|
|||
8bb13efe80 | |||
0109c6855d
|
|||
a09af65396 | |||
7cbe4f1170
|
|||
93fb38a9b7
|
|||
21961daba4
|
|||
b79b5d2009
|
|||
b27842c2bb
|
|||
21edd1dcff | |||
7ca4825bba | |||
0437e2b095
|
|||
4e43fb8fd7
|
|||
62c9d13cec
|
|||
5f227d1a20
|
|||
f3baa5247e | |||
0681f5847e | |||
aade7c51f2
|
|||
bf97a03730
|
|||
602aea3ec5
|
|||
c9a7f75ca4
|
|||
dada8a775d
|
|||
48b8c6891a
|
|||
d8acaaf580 | |||
1d19530349 | |||
c2bd862ba4
|
|||
c5054c323e
|
|||
7776847364
|
|||
4cf96ce0d5
|
|||
303a9ab581
|
|||
71ce43eca6
|
|||
42cb3f7e6b
|
|||
620ffbb131
|
|||
7f301a0771
|
|||
7e8be538e0
|
|||
28a8b18329
|
|||
2f07fdfe83
|
|||
9605dbb13a
|
|||
27c5e4d14e
|
|||
37c2f72cbc
|
|||
4e81fd2e16
|
|||
7615729af9
|
|||
fbd05a4a25
|
|||
0ecb718416 | |||
1b1bef2958
|
|||
bd6e1637e4
|
|||
5a55993452
|
|||
c766ab2d71
|
|||
a4346fd182 | |||
094a9f0956 | |||
51d3c9b6e8 | |||
774d9b5961 | |||
0b81198c03
|
|||
f6d2cfa042
|
|||
4a6c28724f | |||
8142ae9c38 | |||
da8858eae0 | |||
274ae654cd | |||
f7f1017fed
|
|||
2afb3de227 | |||
f9b94b29dc | |||
ca360d2d8d
|
|||
80305466e5
|
|||
bb3fd8fe74 | |||
e75a145b0e | |||
8cab8ab011 | |||
6a300f5b65
|
|||
922631022c
|
|||
157d904794
|
|||
62a9f58462 | |||
7654670967
|
|||
ef8417d9db | |||
40bf53d261
|
|||
7cfa4f9785
|
|||
183aca3219
|
|||
47b794e12f
|
|||
77c06718c9
|
|||
6bee0f35ff
|
|||
8f325138ff
|
|||
5a3b9fb46b
|
|||
7ca8d52368
|
|||
b458c2cb83 | |||
70e535e397 | |||
b13e9b59ac
|
|||
d21e98d2e5
|
|||
89fd44f752 | |||
466206e133
|
|||
e0b8280c41
|
|||
d20e205c32
|
|||
777630c499
|
|||
92ac91e194 | |||
c01c6d1504 | |||
a8b4519748 | |||
f8df716865
|
|||
a3311f102e | |||
26404cdfe1
|
|||
5aa2ec74ba | |||
075dc38434 |
21
README.md
21
README.md
@ -5,7 +5,9 @@ sat-rs
|
||||
|
||||
This is the repository of the sat-rs framework. Its primary goal is to provide re-usable components
|
||||
to write on-board software for remote systems like rovers or satellites. It is specifically written
|
||||
for the special requirements for these systems.
|
||||
for the special requirements for these systems. You can find an overview of the project and the
|
||||
link to the [more high-level sat-rs book](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
|
||||
at the [IRS documentation website](https://absatsw.irs.uni-stuttgart.de/sat-rs.html).
|
||||
|
||||
A lot of the architecture and general design considerations are based on the
|
||||
[FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
|
||||
@ -16,6 +18,10 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
|
||||
|
||||
This project currently contains following crates:
|
||||
|
||||
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
|
||||
Primary information resource in addition to the API documentation, hosted
|
||||
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read
|
||||
this first before delving into the example application and the API documentation.
|
||||
* [`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core):
|
||||
Core components of sat-rs.
|
||||
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-example):
|
||||
@ -37,3 +43,16 @@ Each project has its own `CHANGELOG.md`.
|
||||
packet protocol implementations. This repository is re-exported in the
|
||||
[`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core)
|
||||
crate.
|
||||
|
||||
# Coverage
|
||||
|
||||
Coverage was generated using [`grcov`](https://github.com/mozilla/grcov). If you have not done so
|
||||
already, install the `llvm-tools-preview`:
|
||||
|
||||
```sh
|
||||
rustup component add llvm-tools-preview
|
||||
cargo install grcov --locked
|
||||
```
|
||||
|
||||
After that, you can simply run `coverage.py` to test the `satrs-core` crate with coverage. You can
|
||||
optionally supply the `--open` flag to open the coverage report in your webbrowser.
|
||||
|
@ -14,8 +14,13 @@ RUN rustup install nightly && \
|
||||
rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \
|
||||
rustup component add rustfmt clippy
|
||||
|
||||
WORKDIR "/tmp"
|
||||
# RUN cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
RUN curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.34/mdbook-v0.4.34-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory /usr/local/bin
|
||||
RUN curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory /usr/local/bin
|
||||
RUN curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o mdbook-linkcheck.zip && \
|
||||
unzip mdbook-linkcheck.zip && \
|
||||
chmod +x mdbook-linkcheck && \
|
||||
cp mdbook-linkcheck /usr/local/bin
|
||||
|
||||
# SSH stuff to allow deployment to doc server
|
||||
RUN adduser --uid 114 jenkins
|
||||
|
2
automation/Jenkinsfile
vendored
2
automation/Jenkinsfile
vendored
@ -67,7 +67,7 @@ pipeline {
|
||||
sh 'mdbook build'
|
||||
sshagent(credentials: ['documentation-buildfix']) {
|
||||
// Deploy to Apache webserver
|
||||
sh 'rsync -r --delete book/ buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs'
|
||||
sh 'rsync -r --delete book/html/ buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs/book/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
coverage.py
Executable file
61
coverage.py
Executable file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
import webbrowser
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger()
|
||||
|
||||
|
||||
def generate_cov_report(open_report: bool, format: str, package: str):
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
os.environ["RUSTFLAGS"] = "-Cinstrument-coverage"
|
||||
os.environ["LLVM_PROFILE_FILE"] = "target/coverage/%p-%m.profraw"
|
||||
_LOGGER.info("Executing tests with coverage")
|
||||
os.system(f"cargo test -p {package}")
|
||||
|
||||
out_path = "./target/debug/coverage"
|
||||
if format == "lcov":
|
||||
out_path = "./target/debug/lcov.info"
|
||||
os.system(
|
||||
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
|
||||
f"-o {out_path}"
|
||||
)
|
||||
if format == "lcov":
|
||||
os.system(
|
||||
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
|
||||
"--legend ./target/debug/lcov.info"
|
||||
)
|
||||
if open_report:
|
||||
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
|
||||
webbrowser.open_new_tab(coverage_report_path)
|
||||
_LOGGER.info("Done")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate coverage report and optionally open it in a browser"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--open", action="store_true", help="Open the coverage report in a browser"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--package",
|
||||
choices=["satrs-core"],
|
||||
default="satrs-core",
|
||||
help="Choose project to generate coverage for",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["html", "lcov"],
|
||||
default="html",
|
||||
help="Choose report format (html or lcov)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
generate_cov_report(args.open, args.format, args.package)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.22-->
|
||||
<!--Created by yEd 3.23.2-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
@ -20,7 +20,7 @@
|
||||
<y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.936037063598633" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="150.1282958984375" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="24.177873611450195" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="168.3929901123047" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@ -31,7 +31,7 @@
|
||||
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
|
||||
<y:Fill color="#FFFF00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.21258544921875" x="58.393707275390625" xml:space="preserve" y="21.27395296096796">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="24.177873611450195" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="127.31723022460938" x="37.84138488769531" xml:space="preserve" y="17.911063194274846">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@ -39,10 +39,10 @@
|
||||
<node id="n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/>
|
||||
<y:Geometry height="60.0" width="92.24000000000001" x="607.36" y="413.23"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="13.4398193359375" xml:space="preserve" y="14.547905921936035">Event
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="10.039937133789067" xml:space="preserve" y="10.063962936401367">Event
|
||||
Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -54,7 +54,7 @@ Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
||||
<y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="10.719819335937473" xml:space="preserve" y="14.547905921936035">Event
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="2.199937133789035" xml:space="preserve" y="10.063962936401367">Event
|
||||
Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -63,10 +63,10 @@ Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
||||
<node id="n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/>
|
||||
<y:Geometry height="60.0" width="87.27999999999997" x="845.9410837438425" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="8.719819335937359" xml:space="preserve" y="14.547905921936035">Event
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="7.559937133789049" xml:space="preserve" y="10.063962936401367">Event
|
||||
Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -78,7 +78,7 @@ Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
||||
<y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="16.079819335937373" xml:space="preserve" y="14.547905921936035">Event
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.16012573242188" x="7.559937133788935" xml:space="preserve" y="10.063962936401367">Event
|
||||
Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -87,10 +87,10 @@ Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
|
||||
<node id="n6">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/>
|
||||
<y:Geometry height="60.0" width="145.19999999999993" x="734.2060377358491" y="620.26"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.78865051269531" x="16.605674743652344" xml:space="preserve" y="14.547905921936035">PUS Service 5
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="122.38423156738281" x="11.407884216308503" xml:space="preserve" y="10.063962936401367">PUS Service 5
|
||||
Event Reporting
|
||||
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
@ -100,10 +100,10 @@ Event Reporting
|
||||
<node id="n7">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/>
|
||||
<y:Geometry height="60.0" width="136.8599999999999" x="901.8" y="620.26"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.08859252929688" x="17.2757037353515" xml:space="preserve" y="14.547905921936035">PUS Service 19
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.78424072265625" x="13.037879638671825" xml:space="preserve" y="10.063962936401367">PUS Service 19
|
||||
Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -112,10 +112,10 @@ Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel>
|
||||
<node id="n8">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/>
|
||||
<y:Geometry height="60.0" width="97.27999999999997" x="787.1260377358491" y="733.8400000000001"/>
|
||||
<y:Fill color="#FFCC99" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.932403564453125" x="13.673798217773424" xml:space="preserve" y="14.547905921936035">Telemetry
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.872074127197266" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.57614135742188" x="9.351929321289049" xml:space="preserve" y="10.063962936401367">Telemetry
|
||||
Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
@ -124,10 +124,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
|
||||
<node id="n9">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/>
|
||||
<y:Geometry height="218.79999999999995" width="256.8800000000001" x="1068.6599999999999" y="575.0400000000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="143.6875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="181.591796875" x="8.373079774614325" xml:space="preserve" y="7.444138124199753">Subscriptions
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="190.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="240.7890625" x="10.203400059311889" xml:space="preserve" y="9.536167573623516">Subscriptions
|
||||
|
||||
1. Event Creator 0 subscribes
|
||||
for event 0
|
||||
@ -144,10 +144,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
|
||||
<edge id="e0" source="n4" target="n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/>
|
||||
<y:Path sx="15.418916256157559" sy="0.0" tx="-10.5" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="8.639817810058275" xml:space="preserve" y="29.00100609374465">event 1
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="4.48011932373015" xml:space="preserve" y="27.465240361497138">event 1
|
||||
(group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
@ -161,7 +161,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="25.334655000000453" xml:space="preserve" y="-40.972107505798476">event 0
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="24.477808328186484" xml:space="preserve" y="-43.213945007324355">event 0
|
||||
(group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
@ -173,7 +173,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
|
||||
<y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="5.6761352539062955" xml:space="preserve" y="27.551854405966765">event 2
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.23976135253906" x="5.6761352539062955" xml:space="preserve" y="26.10821814399378">event 2
|
||||
(group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
@ -187,8 +187,8 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.38468933105469" x="26.667665869801795" xml:space="preserve" y="43.287014528669715">event 3 (group 2)
|
||||
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="75.3599999999999" distanceToCenter="true" position="left" ratio="0.2967848459873102" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.94757080078125" x="8.646225134939186" xml:space="preserve" y="27.90167113105076">event 3 (group 2)
|
||||
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.12000000000057" distanceToCenter="true" position="left" ratio="0.18074782715730137" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@ -196,10 +196,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
|
||||
<edge id="e4" source="n1" target="n6">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/>
|
||||
<y:Path sx="-80.36000000000001" sy="0.0" tx="28.333962264150955" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="-105.378832397461" xml:space="preserve" y="15.634602566150534"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@ -207,10 +207,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
|
||||
<edge id="e5" source="n1" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/>
|
||||
<y:Path sx="32.38107215104537" sy="0.0" tx="-22.34892784895453" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" underlinedText="true" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="13.821211921553186" xml:space="preserve" y="16.782337120427428"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@ -219,11 +219,11 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Point x="658.6" y="536.6799999999998"/>
|
||||
<y:Point x="653.48" y="536.6799999999998"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.69230651855469" x="-131.99129340961497" xml:space="preserve" y="-45.45208675384538">event 1
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="51.47381591796875" x="-139.88417228306275" xml:space="preserve" y="-47.69392425537126">event 1
|
||||
event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
@ -232,10 +232,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
|
||||
<edge id="e7" source="n1" target="n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/>
|
||||
<y:Path sx="-35.69940886699487" sy="0.0" tx="-9.780492610837314" ty="1.5"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="46.14430236816406" x="-54.352158195608126" xml:space="preserve" y="-79.29459128622307">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.16780090332031" x="-57.86390746318625" xml:space="preserve" y="-80.0118020361142">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@ -243,10 +243,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
|
||||
<edge id="e8" source="n6" target="n8">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/>
|
||||
<y:Path sx="28.960000000000036" sy="0.0" tx="0.0" ty="-21.423522388059723"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="87.40060424804688" x="-100.50030212402339" xml:space="preserve" y="11.337896156311103">enabled Events
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="35.38786315917969" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="101.29963684082031" x="-107.4498329306548" xml:space="preserve" y="9.082203674316474">enabled Events
|
||||
as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
1821
images/events/event_man_arch.pdf
Normal file
1821
images/events/event_man_arch.pdf
Normal file
File diff suppressed because it is too large
Load Diff
BIN
images/events/event_man_arch.png
Normal file
BIN
images/events/event_man_arch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
991
images/satrs-example-dataflow/satrs-example-dataflow.graphml
Normal file
991
images/satrs-example-dataflow/satrs-example-dataflow.graphml
Normal file
@ -0,0 +1,991 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.23.2-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0" xml:space="preserve"/>
|
||||
<node id="n0" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="111.16238125000103" width="641.0000000000002" x="809.2454000000014" y="463.9111499999988"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="641.0000000000002" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="6" leftF="6.400000000000091" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 7</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n0:">
|
||||
<node id="n0::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="77.16238125000098" width="598.8461000000009" x="835.7654000000015" y="482.9111499999988"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="297.42305000000044" y="36.58119062500049">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n0::n1" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="54.0" width="157.5999999999999" x="830.6454000000015" y="503.13804374999916"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="157.5999999999999" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="8" rightF="7.67999999999995" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 5</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n0::n1:">
|
||||
<node id="n0::n1::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="845.6454000000015" y="522.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n0::n1::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="952.5654000000014" y="522.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n0::n2" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="56.25" width="171.68000000000018" x="965.5654000000014" y="502.88804374999916"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="171.68000000000018" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="7" rightF="7.039999999999964" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="1565.2374000000002" y="273.72953125000004"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 8</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n0::n2:">
|
||||
<node id="n0::n2::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="980.5654000000014" y="521.8880437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n0::n2::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="1102.2054000000016" y="524.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n0::n3" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="164.0" x="1271.2454000000016" y="509.13804374999916"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="164.0" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 8</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n0::n3:">
|
||||
<node id="n0::n3::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="1286.2454000000016" y="524.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n0::n3::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="1407.2454000000016" y="524.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n0::n4" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="178.3169499999999" x="1107.9284500000017" y="509.13804374999916"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="178.3169499999999" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="5" leftF="5.120000000000118" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 9</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n0::n4:">
|
||||
<node id="n0::n4::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="1128.0484500000018" y="524.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n0::n4::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="13.0" x="1258.2454000000016" y="524.1380437499992"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="4.5" y="8.0">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="415.0" width="739.300200000002" x="695.3113000000005" y="568.8694874999993"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="367.650100000001" y="205.5">
|
||||
<y:LabelModel>
|
||||
<y:SmartNodeLabelModel distance="4.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||
</y:ModelParameter>
|
||||
</y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="290.08000000000004" width="195.36000000000013" x="1210.809400000001" y="618.4077874999996"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="195.36000000000013" x="13.645244799999773" xml:space="preserve" y="10.027339123239472">PUS Stack<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4301533333333345" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46543250440140804" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="10" leftF="10.240000000000236" right="5" rightF="5.119999999999891" top="26" topF="26.431249999999977"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 1</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2:">
|
||||
<node id="n2::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="703.4877874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="128.39453125" x="10.802734375" xml:space="preserve" y="6.015625">PUS 3 Housekeeping<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="743.4877874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.529296875" x="33.2353515625" xml:space="preserve" y="6.015625">PUS 5 Events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="783.4877874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.9453125" x="31.52734375" xml:space="preserve" y="6.015625">PUS 8 Actions<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="863.4877874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.205078125" x="36.8974609375" xml:space="preserve" y="6.015625">PUS 17 Test<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="823.4877874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="116.8515625" x="16.57421875" xml:space="preserve" y="6.015625">PUS 11 Scheduling<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1236.0494000000012" y="660.8077874999996"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.255859375" x="19.3720703125" xml:space="preserve" y="-0.96875">PUS 1 Verification
|
||||
Reporter<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n3" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="105.13453125000012" width="155.4000000000001" x="711.4787000000007" y="632.7428312499998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="155.4000000000001" x="12.272399999999493" xml:space="preserve" y="3.249732314531343">Application
|
||||
Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4210270270270303" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46908977216245173" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="5" rightF="5.400000000000091" top="45" topF="45.13453125000012"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n3:">
|
||||
<node id="n3::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="726.4787000000007" y="692.8773624999999"/>
|
||||
<y:Fill color="#FFCC99" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.2216796875" x="25.38916015625" xml:space="preserve" y="4.8515625">ACS Task<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n4" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="227.60000000000014" width="310.3248000000002" x="882.3347000000007" y="682.1055312499999"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="310.3248000000002" x="23.48672560479929" xml:space="preserve" y="6.686613155747068">TMTC Components<y:LabelModel><y:SmartNodeLabelModel distance="0.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.42431566666666864" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.4706212075758038" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="5" bottomF="5.1200000000000045" left="0" leftF="0.0" right="5" rightF="5.324800000000096" top="26" topF="26.432000000000016"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 7</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n4:">
|
||||
<node id="n4::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="1052.3347000000008" y="788.14553125"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.93359375" x="27.033203125" xml:space="preserve" y="6.015625">TM Funnel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="902.3347000000007" y="859.58553125"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.42578125" x="23.787109375" xml:space="preserve" y="6.015625">UDP Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="1047.3347000000006" y="859.58553125"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.111328125" x="24.9443359375" xml:space="preserve" y="6.015625">TCP Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="902.3347000000007" y="788.14553125"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.001953125" x="27.4990234375" xml:space="preserve" y="6.015625">TC Source<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="897.3347000000007" y="723.5375312499999"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.904296875" x="18.0478515625" xml:space="preserve" y="6.015625">PUS Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="130.44000000000005" x="958.2147000000009" y="946.9855312500001"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.5302734375" x="42.95486328125003" xml:space="preserve" y="4.8515625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="127.03999999999996" x="1030.4707000000008" y="629.3055312499998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="109.9228515625" x="8.558574218749982" xml:space="preserve" y="4.8515625">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n7">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="40.0" width="120.0" x="896.8787000000008" y="621.6432750000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.1650390625" x="17.91748046875" xml:space="preserve" y="1.703125">Shared
|
||||
TMTC Pools<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n8" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="106.85641458333345" width="167.16000000000008" x="678.4854000000014" y="467.9111499999987"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="167.16000000000008" x="0.0" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 9</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n8:">
|
||||
<node id="n8::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="76.8564145833335" width="137.16000000000008" x="693.4854000000014" y="482.9111499999987"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="59.875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="118.25" x="9.455000000000041" xml:space="preserve" y="8.490707291666752">satrs-example
|
||||
Data Flow
|
||||
Diagram<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<edge id="n0::n1::e0" source="n0::n1::n0" target="n0::n1::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="4.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.5234375" x="22.12435476073938" xml:space="preserve" y="-29.312517773438344">TMTC<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="18.0" distanceToCenter="true" position="left" ratio="0.48378541003594444" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e0" source="n4::n1" target="n4::n3">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999983203125566" y="-22.719979003906246">
|
||||
<y:LabelModel>
|
||||
<y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/>
|
||||
</y:ModelParameter>
|
||||
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
|
||||
</y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e1" source="n4::n2" target="n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="5.000000000000227" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="none"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e2" source="n4::n0" target="n4::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="27.85279999999989" ty="1.470468750000009">
|
||||
<y:Point x="1112.3347000000008" y="829.09977125"/>
|
||||
<y:Point x="990.1875000000006" y="829.09977125"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e3" source="n4::n3" target="n4::n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-16.954559999999674" ty="3.7001374999999825">
|
||||
<y:Point x="962.3347000000007" y="839.58553125"/>
|
||||
<y:Point x="1090.380140000001" y="839.58553125"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="none"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e4" source="n4::n3" target="n4::n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="5.0" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e0" source="n2" target="n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="39.69774375000043" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e1" source="n2" target="n6">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-51.37930695586169" sy="-119.14225624999972" tx="60.74632526271894" ty="0.0"/>
|
||||
<y:LineStyle color="#993300" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e2" source="n3" target="n6">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-19.839999999999918" sy="0.0" tx="-38.399999999999864" ty="8.753668750000086">
|
||||
<y:Point x="769.3387000000008" y="593.0482125000003"/>
|
||||
<y:Point x="1055.5907000000009" y="593.0482125000003"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#993300" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n0::n2::e0" source="n0::n2::n1" target="n0::n2::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="-2.25" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#993300" type="line" width="4.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.171875" x="-76.64202462463709" xml:space="preserve" y="-31.1845177734383">Events<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="19.872000000000014" distanceToCenter="true" position="right" ratio="0.33295067336054324" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e3" source="n3" target="n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="52.53735479999898" sy="29.121903125000244" tx="0.1231654464985258" ty="139.1977687499998">
|
||||
<y:Point x="841.7160547999997" y="918.67033125"/>
|
||||
<y:Point x="1308.6125654464995" y="918.67033125"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#0000FF" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="none"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n0::n3::e0" source="n0::n3::n1" target="n0::n3::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#FF6600" type="line" width="4.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.6328125" x="-91.81636757812339" xml:space="preserve" y="-48.84101777343835">Function
|
||||
Interface<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="28.21599999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n4::n4" target="n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="-0.049743750000288856" tx="-82.89626674268379" ty="-24.959999999999923"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n3" target="n2::n5">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="7.053999999999746" sy="-52.567265625000005" tx="57.5920000000001" ty="0.0">
|
||||
<y:Point x="796.2327000000005" y="581.6432750000002"/>
|
||||
<y:Point x="1368.6414000000013" y="581.6432750000002"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999972949219227" y="-30.999879003906244">
|
||||
<y:LabelModel>
|
||||
<y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/>
|
||||
</y:ModelParameter>
|
||||
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
|
||||
</y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n2" target="n2::n5">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="47.144060319997834" ty="0.0">
|
||||
<y:Point x="1415.2947000000008" y="763.4477874999995"/>
|
||||
<y:Point x="1415.2947000000008" y="675.8077874999996"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n6" target="n2::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="39.02668799999979" sy="0.0" tx="0.0" ty="-8.276800000000094">
|
||||
<y:Point x="1133.0173880000004" y="750.2109874999995"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#993300" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n3" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="-38.78246875000036" tx="0.0" ty="4.884353124999166"/>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e9" source="n2" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-65.73714292000136" sy="-118.80000000000018" tx="35.35927123999659" ty="-20.0">
|
||||
<y:Point x="1242.7522570799997" y="601.6432750000002"/>
|
||||
<y:Point x="992.2379712399974" y="601.6432750000002"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e10" source="n2::n4" target="n4::n3">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="60.0" ty="0.0">
|
||||
<y:Point x="1415.2947000000008" y="838.4877874999996"/>
|
||||
<y:Point x="1415.2947000000008" y="926.9855312500001"/>
|
||||
<y:Point x="1042.3347000000008" y="926.9855312500001"/>
|
||||
<y:Point x="1042.3347000000008" y="803.14553125"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e11" source="n5" target="n4::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-45.90000000000032" sy="0.0" tx="15.199999999999932" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e12" source="n5" target="n4::n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="46.63667953667937" sy="0.0" tx="-37.26332046332027" ty="0.0"/>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="27.999968403867797" y="-27.220816406249924">
|
||||
<y:LabelModel>
|
||||
<y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/>
|
||||
</y:ModelParameter>
|
||||
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
|
||||
</y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e13" source="n3" target="n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="66.54799999999989" sy="0.0" tx="-35.74228584000252" ty="-3.0175312500000473">
|
||||
<y:Point x="855.7267000000006" y="770.8415312499999"/>
|
||||
<y:Point x="1076.5924141599983" y="770.8415312499999"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#008000" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e14" source="n4" target="n6">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="18.251857503227473" sy="-77.84525000000008" tx="-38.24174249677253" ty="3.238468750000152"/>
|
||||
<y:LineStyle color="#993300" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n0::n4::e0" source="n0::n4::n1" target="n0::n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#0000FF" type="line" width="4.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="82.4609375" x="-88.20577865560745" xml:space="preserve" y="-48.84101777343835">Other
|
||||
Messages<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="28.21599999999995" distanceToCenter="true" position="right" ratio="0.030113173151242907" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e15" source="n4::n4" target="n2::n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="-7.129743750000216" tx="0.0" ty="-0.10225624999964111">
|
||||
<y:Point x="1112.3347000000008" y="731.4077874999997"/>
|
||||
<y:Point x="1112.3347000000008" y="675.7055312499999"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e16" source="n4" target="n7">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-80.61839999999995" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#FF6600" type="line" width="2.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
954
images/satrs-example-dataflow/satrs-example-dataflow.pdf
Normal file
954
images/satrs-example-dataflow/satrs-example-dataflow.pdf
Normal file
@ -0,0 +1,954 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD>
|
||||
1 0 obj
|
||||
<<
|
||||
/Title ()
|
||||
/Author ()
|
||||
/Subject ()
|
||||
/Keywords ()
|
||||
/Creator (yExport 1.5)
|
||||
/Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5)
|
||||
/CreationDate (D:20240208153304+01'00')
|
||||
/ModDate (D:20240208153304+01'00')
|
||||
/Trapped /False
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 3 0 R
|
||||
/ViewerPreferences 4 0 R
|
||||
/OpenAction [5 0 R /Fit]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/FitWindow true
|
||||
/CenterWindow false
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Parent 3 0 R
|
||||
/Type /Page
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Length 7 0 R
|
||||
/Filter [/ASCII85Decode /FlateDecode]
|
||||
>>
|
||||
stream
|
||||
GauF[]96a;XrZ`X=6'nI']0G-Xc?4G)r9+Z>EKNG2R4%q0^eCF&Ru6"F&ec4BQ"pOe/h16:(d[p&-0iM
|
||||
R9opj8<89"/O*Y3#Ch."r:0gdGZOb<A+'"-pfj+Mq>To#iqpb<5Q:*ns8CKnJ,8':s.;OT9E+^)s8Q;e
|
||||
r9++WDnl>Ls7O/,aM7WLDu]Utnc/F"n6>@gGouoFD;Jt3gAG(frg14qpF?3tR.eLKCMf1E/rJd]h<YjS
|
||||
mGD7bARfQBp#S*PVrM^S]f.Y2^LOJIa,g8s^MJN%[*$]\r9NuN+9200PLa+)s7ij6s.&\UXn'$/;7*dX
|
||||
+oEqIa/dnh_Q<ct^&B*<rr#>I+0]Xq54JJRl^sr%oMRkoH[Tc(Fl@8Z9pkV2Z0MAfYO,X[qi%`Gd4bFb
|
||||
=KSY'48t^Vqiu:fHcg^5au[k`^K@Dm^]*8Q5%E@BWd<I+oq8Sqs6^S!NA^TG55<+UL]2)'-GIDO<*oE6
|
||||
)<?%%bJ-rqR7sfj>PcQ*g/,>on`NHfJ,ED%VO=X%VKLU@NFeI&ObCIOn'>/J?L+qW?ZCN,D&FCelSgfg
|
||||
o6]qNBj>G_ci!"Sph]Qid=+g?lO3Z4)eDqCCc0@ZC7fSIMUeJml=]@ri`D1E24RK,J_.nX#6t4/^9EZc
|
||||
rI;jB_O\\Z9ejMUa(J^[O+g0#S]bY\^LA;$9,QB$Yh.M$hJr1d%+3EYORT<EIPf8tZ/n`8e+u&&<blX9
|
||||
(OFIFkVZ(A57YuSV7]FdBA813CCtfJn]F1rmRunIPU;mQ([A2I?c/;f^0/XYqnb8VI%&r<S\b"9.;B>l
|
||||
5?G>*`353h&-(`NU[`NjB3j'nI.d/rQb]RjVh+%bd&DrNW"^J)TsgI$03"Cj"dNEOg&&Z93'YHW$L'WJ
|
||||
"(+@_FVHk"]p?q,95tqN^qm3Hk<!0_[O^U5^jh:7:53r#rm)dAZ0q%VYCATD[8*K*Ds73Z)U?Zh5*jf&
|
||||
?iQp:'R_k&aQZEh5PP*F?bX#tH+T.SbiANaMq;_@:lj'&>fljN7!e#ED5IDBBEJ]9bn@^riKja+$:5-m
|
||||
p@7540@QMqrVn6G&6GY,?qL3mp%!1k>R>1Q5!sTejfTF!G2.aRruJ0>hd<@(IOW\]m13-f'e/=t>W_q.
|
||||
g`3*g4')<p[o0u:a66[_aR\*#s36fiVk)-M/*W6r1-;IU]`7DJMAkg"^UD:Rk'Q'ReAO">?=.a:.lF:G
|
||||
D7i?&UX&2+0@@Mg5e4W!B<B[eU$U6G1=^@?nM1E^n9<fW]OG\7IEl3iX*8'PIf0`$7pfQphspIm<V:is
|
||||
k7R0$n*`>=J,_LC=J&\5=2p'ik7g+pGZfUEgFk-l\:<X2q08rIXhK;T,mb*0Ih+^+_tmJqmedst2<=un
|
||||
lpK5,4rUbu(!=l]SQP"^ojhFqa5$GND8#)WHN./b%=q]d^:[8KA9I5&R?ps?^)!1SC+QD+Fkbm.6hbUn
|
||||
]?T%hgh$iXhJB+u4O<m#rPNEsZ_i%AoC8B.[Ea+Rca%-gZT0u@5D2#IEg8!&IBB_CV4uN7e:$_98>lGq
|
||||
-lGHD$WN/sBAI)\\E\kLo#6C+gO61SMf_O_Y];u8U]/[@lu*Wuf_SV%::k.]R%PU=PZ_oC-;k-iOq1&F
|
||||
q<&,*eVn8)/Aj[SkKf:\-.k&$!Ptf$=hP`XZ5P-OnZ3]T0LUfmX#(\AVR$[m-X)SGIQ3ss@kg%U^OEp!
|
||||
=b"3Cq@=G](k4PNr\LcOeS+$C-SC1p/t>5B!A7$;ld0rWd2)=:.d:FW(et*!V@A'WDZ1VWBdPEqI3k#=
|
||||
SF;S"CWYcMH=Ut;;/s;Tp@7eJe0;]%F\r)YT2gF68EA5s]k>AHB!7oUp:A;ONYu@PcG??P:to\X26bIC
|
||||
*5.DL]]KKalNMDml'hL)cX8s!e8p)T_pbQ,[L(<<`'2%@.9fT\l*!q^2gf.`gpf^99V]oAXU[aI#**re
|
||||
%W087/RA-<R:Xm,kQc)!.qXh7"/hO>(EGF,S[CVV[5PD-=OapZq-Rf#LQ+PW51UbKprE:aG5AKC4!'QX
|
||||
HTiR9.oH.M<;PkSa:[eX:cP#Rs7Q96Ps9C6nDbkGCh/^((ZO<>E7LJN>KH))ksd0D)HQul4d!R%ZVq@6
|
||||
[G/UqE0RU<]Pq@dnE4>oOPS0m_e-#9dU*:.>6apsD,rMPki^ef8^(qc]M1q3KcDX_?9Pp20@4ZIO(5#S
|
||||
lVoBc3qdJ.hAn0WnB(q:(FH55YHLi2G0mkAb5(=P^"68EDE>JIYEMpPg$#0h`Qcl`p'emQ*9dN@G/Eta
|
||||
Y<@p+b$(gNm!L_2fd0ts+(of\4g`bqqX6L_dQ_$6Mf`Y8B^7/b?,lDs)M4;gbSuhE]%pt.E?6X31*UD$
|
||||
HLl?hI>OY,ZYF8!l!7jR]g`)g_LP?r^22hd*d"AOZQ%0)H#nk?*UDPhMJ#PH:`_Ro,o_ha1c)iIq4nH&
|
||||
qEm?SbYd]@qYsY2VCYu:/sb566b-Re/#4=\kPC:[d/%bJQbU4@r`Ai6J*h\FQSfgGfWt6]#>=DF<n;c@
|
||||
Irbklr)u0DX6XQ'UrV)C]lhnUa8aA4R<3eDA5BN/p@!KX^Ke7-YH#W#Vnf`JAtPg;2B>mH][AHL4Ki+j
|
||||
r3alA]rn;]IAPhD?J'(aBERAm`h0\HY/dgAd9\tP[U63!XBid%Jb/C`)1J\A^VQdg$m3Ug'bC(1^&A]>
|
||||
<4I9DJ)7I)kE#5$UW;(pBANG&\AB$-osFm/^VPi^9%Z82L<fO%&_EIg?.WECi*lClPIqGXc$\)[6W;(k
|
||||
@^i/]Ap%Ou=2^tBaLnK*Hn<UmLY"09:bbpiTLI*V*90'Vq0.3`X?n_Wj.mQn`gs;^*j'`"H@>Lj#:2`a
|
||||
CJ;Y"3d0UNa\Z7[IhO$.g/XU:b=!h4EXcM.S+J\o=u3[LDs03rAng;;B>bZMH=QG4)s"#1].`*##0e^r
|
||||
2gjo.edQqL4pP*JiNiS-PRJl1]euQLp>Q;l6g`7(<GSM[ZDt#DYGnn5m]`m*CT!e%=P_3)T!eKhA3I5$
|
||||
+u1f-l__]BDc6JP(Q\([n$pb,=3UtlE2iUN,\UJsY-_4>#6354&Jr]GTe/Z9mAmUp=(e,Z0[8#k57O'U
|
||||
RGF_Xm:HYhjqg/-n`tMrp1N<DB^Z=QcI1B!eW(Mq;>tsr4?<^e7-<1,=W/[lD-jSqTup\Mk?iRE]-o8*
|
||||
UENI=,<#&.M.84gn&^#=b^L.m[#9SRqhng!E(Ymr-1!ej<Kj$E_&f$$l09o6a-Nl+pi(%>5rtBV@t-SJ
|
||||
+8Tb'c3p>\KDH""^TamDXDOED>:R\ap[8gMYj+Y;Tc`Uj%_Y0#PA?EnSZC2_Y9"e8Ti/jC-uESb*\D_.
|
||||
B:c:o6gu.Ye!h2$AGjkfY,YcsA#ob*<KhqBH=%ZL+n<kJj;sS<ZRX^HPcIu>hBib9oX:rFd@:%CN^&p?
|
||||
[Lsea6.fapeDFa,_L>#RGJ%Lp3'LZX$=?W8+K6N'#'NiKK@7cA6YkmDPZbFt[uss>O+(BGMY.'Y(;Ph<
|
||||
S,@YW+pSiMSJ9#Fr&$]._56tr66%*5p3Z^crY@To=T9BhXVpk\?kjU.G497t0qO\fiHqBf\Ot\m^KXt?
|
||||
86*#&r#K-`.:`g)=S5IJo?q!D`<gmMqJ9ba^?MsQrDfQgAtj`1CWJV9VU'!"QS7?P22&`OU_,u@4.M]f
|
||||
X38"eB%eUjs7=ZV<jt\R=fGV.7$rJjJ*Uq!U5*rim&F%e7=Vp=dm!\rYE9U.CNLt>(Fthd7dEoJn=oC'
|
||||
9l[XKB"cg7WAuon1Ac.#R=dDK=b_!:"YlKs-q43L]e3u%S(]a"NM)5%ebW[7>fW#eY(>cRY(JZ0^P"O"
|
||||
g3qoo?QJJ#&WugSR%ponqFIaERgt:$'NE4CH$qg>$];,rj_bMG^>s+Dak^^=/h#D5bo0j6g9_&cUC>Kf
|
||||
>iGqIZ/sP'"\Q6]\B8Y=Y0=Jc`ii)5L$QfPZjt>!1f&bQMTDncqB:5KF$DPKgs;7:^OO:A%B`+rW3Dm?
|
||||
XML108kk9oLGcTE\ZkVUHYm">n-aC<d]C82cDNP0A0%+$E^jaPhX_,ibA2U?l!;Wj'e4fsPP1+lr;-F"
|
||||
q[iGXZ0MAZ6T`3;^:Q]*!llsO=W?i)<PQ!^;bIgMh\,ZCn<aY]VC[$jO+6jGGi+N/qWu"QOO]Wj%Ae]"
|
||||
c/pH\hH4_dl25;L.#$!nX='a&W1<?7T),6"nF7nCi(gKG'A'!,LuEnCh-:XY0e"T)d4]NZ)2b.IN0mqf
|
||||
"LNkRX*p6_15B.Z@\;SXXB"-A)LMEMNV2h3?g]`3;sa"q!65?W)?0ll@t2Knq\MA_HQ[-M;23@E2g<?!
|
||||
4`/mI'f.9l"ha`e4N-%F"+<U&[X9R?0ls:Ci>sdjHi@TT'/`-^DrD;LS,NFV+r@UNOc>FYK(Tt1+n_He
|
||||
kZ>k>%h/;a^Wd#Xq3/P_KGee=%mB[+^L0ImZ,="D[&mW:d6DDRMu:@(ZT^HR:u#a6=s36Ka&UhbJ!9NU
|
||||
T<luN9,mW5.)er.Os;m9:Z.BQdph]#2uG8j2/>.!&EUAmIa&4<Ad]@F-Q5TD*:(+,1GtTjAP;CKU^mb$
|
||||
+2n*g66sSESjYrX_C?R>\uBT<K@u/78WQUA0VBKp]aQKg\lJ"tb'\!6bW"dL9KrS&$J]Q]VNk,LN&nhV
|
||||
E:M8@4@dbMUW@KZ*=:UnQc@s-OlnL#..2?tUA,U5M%LRK`V3A9(aCcM)EG&Xjrcj)(-sqd4dtHcV#GhV
|
||||
ocGs;eZ0&CL\WINno$!6(FndQY!1<`lV4W'Q9qA$Rai_?M;Ej,mj.=Y)nt#jjInR!d*`CBCE[l4R;oVs
|
||||
!tF^OiQtWHbBV($2>I&mm-@Ko>Tq\K1Tj(m_>IVp3%"#N.s!rk]d;e&8U)tmdLK6,L>lC<97E`S#*E-b
|
||||
Dn<5uQFm8_J)AuU,Bn`%9"a!SZBaP23!n?RFXjPs4gN,#%q^d&U(hl#1BkqT:\D3i%pjcnYj;2c[QaDo
|
||||
FfE-X9>FX-:7cdkR-F-G:V*W`Q>]G=`bU&7Wn:M%-hCm0X..7-Bc-XW`SCd(,]i^-E/#Kh6SaFf4g6)o
|
||||
SlfqPb$3=YW7"9t=\87G0\]97ai!5mFrUA@L:1YocrSq0fWHiJpWNIYj<U[RY2`"_SM*2r[L@4E>k?Y:
|
||||
j=4pA_(iJAN#HBFGCSR4jfcaVs6q%=[_k*i*=#B5Km0'SGQT[A\!Qj^XImt^ERqOt;52S<Rff(j>Q`$S
|
||||
?BTWd$9<ucciAZW7/t5f,r+Za)I_50/bqSeVe%b,"?sY<(Zrof+3XKI/8[fMAUhYp72M#s3Tg<r\64bs
|
||||
F,9VjTbH!(=tF/[m][Mrdea1rhs9!0>1H8b3nFM]&FFF!g`]XU(=E\l-sF#=SjH^M=WXjh90pbNq(j9G
|
||||
bDI4@W<`:\[Y/6U[.kKdgBhtQL,$I9=Bm@%-/,2HL0M2@/Pq4%S)6`ing0>]"4nb:[O[uGCGRW`eG>Sa
|
||||
BN_heLfCgog@HV:ZIV/`Zeb412'^Y5An5P"dpQCIUsHoj=SRg^1Krjl.E[pD+9'H;I4*cTbWaHV2ojJp
|
||||
+#Ec*0>CMt8r"t`&p2dS]VNs[c]E_tr%tehb3Xa]RoV1g+)OiEhNDQ"V^,lrZ5:QI*p?N4>l\58BF8:L
|
||||
G\Q8@iMe<V<ms4&'6LNDAEFi5'I`/s/(!X#rj'u$=_);+c!q4ILod%ur]Y.4dT`P8Q:_[6BBf7AGt9ON
|
||||
?!t.tr(Xr\@Q#?QB9'MA>PYg6>PZF45/&8CD=@deEhr9+j945.Xa5'<C0J=7PL@_pV9X#AU%5U.b0P`=
|
||||
PN!nfK'-oakC_FiNnrA$%s@`I<p\+tefO8)QI[,3SZTrdZeCV<<dg2Jmd"\5[W8J0iVV*lCal:;1`qkK
|
||||
cB^4Xc!FMXf$#aOZC7!Soq;0h4de<TAmL]Ua*-.X9D^HuSZPI9+\.ZlgmsQpjQ4#Ka$$u[8@3c$\NAi<
|
||||
\3!nUQ,)5/OtY%&BBra1Hj$T.=1q\?V4EV5cB`"#]/*?>]/0"3G@s=X41kCirAQONrmf5h^GnS3(L-Xn
|
||||
N"boTEQ/;Ec8;6o48pT``!U\lSrYf$1ER&co$:/4*WL1f)]Rtt:7h$8;rkmS_L#9jepk_Hs.;8brB06A
|
||||
b9Z.uP`Jl!1YXHj#CGR-OTJja=Ct.cUD8#.4I;#gO*ie0l1Gdt(HiilGDj(Y)_D$<o?m,0"LFTJPYVEe
|
||||
pk=f*KBUcM-UpqBbKd#O72/L_a$JB75_K>OGf%N>eh:m$j6^aL2gj/MhrB>P3\dgK^JlHf?BMl*o.<d"
|
||||
(`C@IVo:Wih8N&8I!&SkHF0%uVuA<g7i(FACSYW&kV,Op:s!UHE!fqD2'&a4,bA7$ot.H9]aPpr`q_7O
|
||||
TCJc&N/"Z:Q/m3(/!*4C]M*8/Qe7k'?->+a#U\U.X*q3BMEm'YMGKi>-s*CBa7uJd'@a1**:0_AF4RH`
|
||||
6r%`2N(1S#8C?j"LJ/V<9#ui%"Y]]j`CjsiYs]poYi9XKbt#EA=IBMrs,NAd&U<%m2!4fY$g41+ATF$2
|
||||
bf%D#-2#@00Joq+%eLu$3J8,[<;jTaifYr?\_.Fj?C;cZPq/'YT]):Y5`s/3*95A[`Bh90>HCMYa*a7U
|
||||
U7?W..tS1WN@ZTTl,F<$%Xdtc#7Z_:+&oELo"iEma<b12KJ.H/%pIf+R7F6^p;R/+<s6((;^<DM5,`c1
|
||||
=\'-8i-XV;,=2k[GpiOg48&sN_:'XMVe=[-a9VAgJpbS&4G%sBk>N6U4P,It.T`V4bNsjBW<(?-_:p3U
|
||||
RScJ7+7fl.A=g)t"Sip[S^u)B$KV4-Dt%O6b$Z5TlcSoQ7JV9X^U4d%,!NYFC*9R(jmTlZA%n/s;/+7q
|
||||
UHB=<m'T#*?Z-c"/?95&#?ufS6+'F>GtZ,k_lR3a7qp,^/*Ltdj+BHAQt%.<Bju7jBqkOL(+FN+4+enD
|
||||
?gC%B4*':g-]8I$:.EoMQWY-#Mm>dsNX=3ZSgf_:&aTJ28sQ+%#H.],j9er+6_KStgN3@=ndda\e)EX\
|
||||
<oB/1-"Ij`W9Zaj0Zm.1l@^XE/&US0@!tF\9IY/if0T*+AKm[QSVU)=\U)+(HBdTJF7c."n^2i"`N!6=
|
||||
fU3MY[;R'lXd(Yk:<]$(5'QL'Xg/Bo8u]i@SZ@eWHr6//Mc+9"j!?[@Gs\U%8@`I%9$?lKBR1cWZs7n*
|
||||
Ee^c?K"lfUY2sCBhj(6Te8k4VbLMB6X'1W@Bm&U\*8DU3e;0n?IL2XHW*[a?UD^%mTrM?sdH[/bg&:Eq
|
||||
H/;67=YA$iaZP&2c^t'mPs>"KDE0kp@/&8cb6[qJT/4Lkm;I<hKGXTE_OY<r+#quDcRb@%GeRbHY0/=1
|
||||
G]H2J0@dnh]i#(EpIZHq0OJ.cK;X'mN`F`emrWA47rBP@WNltFL;fN4YLLDE]i(cKah:RWouejSls;-l
|
||||
VH8X,d49@R\hC6$HU4jbl/?Ztdm<Zt8tE6T-s]L&e-)!gY>ehlZZs,\H0NV42$1lM,n-uAIL32WG&Iu*
|
||||
;Wg-j]i(Rgr(a0)l1.H;Z'DAs1,C=sTQPOV7+4aX20t[&qfqZBG>+9G1VbLe%'LB>,P4U!?HgSCF`;K,
|
||||
eEO;P$2=eePregWIf;.WiEdsQU:aC`e&K)F@ZP=Y8WK*<lHt)pG$*XNH\&CrGTI9CRB=1pdJ^Dd3S*np
|
||||
6Z=_BD$.C8HIfSn/4/,V2D75*5g4E6?5)_WVB%@>gURF[$j0#eg+Dp1%+]"H)r&?qZe#B\eV&?"@r;Y[
|
||||
3F^\oq<]Iq3t5TaZjJ[I=XD_<GV&LRG^.)Nqu5cs9k-,HFll>AgMEIITK*e^7aSAq>[t7<Cp)7)>DDmi
|
||||
ZcpgBl(W(Pj$WGj#r]'U_-M+$b)3%LNZ?qm>onbESb2kJ[52VZpa6obp"@t[N=4'e,!Mb$VADYsFme`B
|
||||
W:V3%\B0Lp?O==)NKl507^:2BGLW-qJ^(JnMU>0CSf"(YJ'ZG5r^Qfi?4G,hFRu@7IWa"K0uan++H(t-
|
||||
+E61\:D/)raXM\Gj9X8j#GV!$+7=84g9%#RpuA>l[ncgu)DVOLGt)l@:lS5HY$l[uIHK>rdL*rgVFtDA
|
||||
0CtlE?leFS?;TZmBangIXNXDkqmpnM)Fl_!8E72q)(84G`fV$f"<=&Q\k85<L0c>>c95)E]%<mU[(40g
|
||||
o"BV7_7JG[c9_'M3:r\:iuHQSj.-,"LW3K.X,%[,'4XPopO6qOI;>?(c8CTF`RXH6-ng,i-VIW:j((TH
|
||||
9]K[F:/g36'KI)_UPOr%rna&t*1`EGcFSd0aYhA[GIq-`M%tK?I;'[1f/T[OF,e)'W,XX5dQ!sc1lgcQ
|
||||
Sas-u%\?b$[+!sS?*BA$GA%uOE$gS)L`4<Ri]O)2j62L<BcQ5>6K?-2cQi9:NMFllj.*@bf/TK41_0J,
|
||||
Y&Oq(Y&WY5UrF"0`Y%?.3H`?6_=*u]]l)MR`f\J:VlY\SJ%=aa9=H+)rPNp<2%I"q2%DJJ29r=2W,ZG)
|
||||
cp=;\'W/?13Eknu7?5s8>eB<W]RS/Q&lO`j@*"`kXc@ONg4<L#l8gq=cps@Rf<T$U;!Jaa7]ESc=l,=_
|
||||
d4.-#^`9J/BPZ*_Td>ASU\18nXM^4"^Q>3Zm!C@nn;]Y%Mgc`0BX+bbd5i[]3EkV3_fN0p8teE=GjIs\
|
||||
94$=_#LU_`Y8j\)8L-01*b8WS)+Zbk_NURiqjofmD_IO#P*3gF"2Z]=lQShlFA\'!@-jG<Oe[%1G=(qW
|
||||
EJ:%ULLK9oq*S-A:>tj)c3PWM][ZP,G&X8S'?IAud5hPK*i,N-k%>(JFl[3Vb\UI3L\:`0Mq5aAT4gmE
|
||||
++)ZYGs"iLBWN]6m6mAt$sb[F_o80=s)?7PNn/iDl(d!to0OPTVEJ;Qf:+i^r-FKC>cl.HQCN5C+*L)@
|
||||
@=s7-eP!MtEr/5<f;i3E54R1#]Y5G#BI,!sR'#A:W(">0a[TXnG5=Wjg@RUE73m'lAQGLHBl.-ALM1o'
|
||||
48A*<c%-pAoEE47g[#;3`'gd1HsL@1PX<O.it,2.m\Rdq?TdbK0V0RYMV"oYZobG12:S^un8p#8i\W8&
|
||||
g]l[1n]LUL(OgZ9).r8F=`Nrr*].&J8#MIVc9NEMmI1uAKcYL*W!Xc,"fK)Ie5j]?0'@kr]s!nXfqQ<Y
|
||||
3INrWJaG/o@GF@VMEG(>(Q5pe\]%X6H^[*MY(L@ll[/Yq<m5TV0k0313krb.eNN&G4-_2,h:Q(cL6XhK
|
||||
EP+%B2bRkKpLmW2EVuj/;7X#tf\b%1O#(G.J&Ws(LE,iG-2WE2k3\HNa&fD4`6(pns1`(#8q&%].1\+X
|
||||
2@%iLE*R8&+*S-/"TIpD4*(KY(Ra,h5=]>Bic05h99m_B#-4O@V-:d7q6.b+e/'^YhELRH8S?^OO*G,&
|
||||
r^pYo=0aa_Q)4=kO8+b4/(k=#G$S23jlufXXb/YUV4HHPZYa^)/SqKQq5ZDSA`T>N<o#gkgcH#s3AKgX
|
||||
)7A>(r0<_1_?duKH$T8PCECMSY>RU:\3X!GfDVEerKY@J$^k>6q1h#_]iYG!GOncI+r.\k\+%#R;Qf:"
|
||||
:Cdn24?ScFJ_M&b>i&0E)Z5l[7/8?%)>G`$_i7N`(0P]cDuHSmCe5m';I]AA1jK_YHdI/:Sj8qh]a)f!
|
||||
VpghVs0H]>o=Sl2RchLdlK;(Q&C9`/q_XkN1@Kdd%Pt__aLqh=#O>hgCd$e22tn@MS_H$j6.6n#]E`;p
|
||||
ZV*J"r%5SeD%`<ahK75>XgUs<.dd\5G+FJd1UM1$e:q/0a\QQUfq]Y4HdK-s:kQ4l&KGAk8P7a&C!EoC
|
||||
X40p)%]<3L%pF=eX148YH!7&iiD.):cq3+Z<*S2]5OI.,fl6B\ktgb6l`j)]s$NtHAJ_+`En<$*nu8eq
|
||||
?0".^McQ*g%iiuDi&G]tH0b,!n2Y[\!Vr5AK/U9(`D#MYNc]=i8'lV0[+:0JFJ8GZ-QIO^e>bW4$)Pj'
|
||||
*eE*ncu]nrZP72Npmo$SV0V15o,9AS/_S-BLGn7tUXV/;&%QoQ!OW!B#LjU_*o-VR+23Et:M4qOeMfSD
|
||||
=nm")GK\^b*5%?00M<S(i=ed*2REor5(ghMa'"TgIQn9CY'R#6KS2F]m7f^)Ohb=I?VRSp`g%?ImBG'm
|
||||
UW1h@a*>>T`^e@nRF?0,@#UJf0`2rn=6.<(?a3XUCT[Jk<3Dg7CT0\Q&B+p+(KX''EaqV^G"k1(29lQ#
|
||||
S]XZMM"MQ47L8o4f^`:]ID##Nj0c:Dg=Ls$8#tM]1#1!e\o?dk&)WGf*K$EF*j+uSXCHqu_e+=b%5Q6-
|
||||
G:#$jODEJs2'26o2Z%*ngME,PC2<Onlrnk;^T>'8n^r`n$\$X`l,JFC^u1Pr?YfE6SN^d'Z^GSLo4V=l
|
||||
TRMm(.pp]*<\H<leUL_gNQ9eUe)m0-`=/_97#<:h*qP.)m209L4%PBScUb=1pCB/gm,DK\:@6j,,sc^/
|
||||
q-[DOm<nLk[VrO^$](meA.bZ$7@PNKahVG@FVA?Anqt$+VW*Wq5U3=ILFf\_,g5CbpYHq/oon99*)3l(
|
||||
;X3W*]7\0n*6f*DoZ#TsjkB*:7k2armt27rFe]IfRB;U";C633+=a\]M4'E7B8k/AUPIZ;]#jrgCp_Kk
|
||||
K($f;p,^.2;0LDoP\0KQfcWF\s)\@n_.YdV2FC+dCF)&ji'*12.a*[Vl-ns]BlHJ7`7#d3Rg02D>$):g
|
||||
R:cJ)ipA)*-oFb@\hqREGcUG:QMG1V\kW1iNTf1@cHqq!17M^eoL<]/Wq[P0e%F[`(Z.N4gg1EN/:)pR
|
||||
2YIRQ(<42KpVbK`AFCEi("p2:X'p"*=8LO*L/q(/cFqr/"E%+%)62qe,OXma23`_s7fSZKd+_24om@ui
|
||||
YR](cUfOFuj/=[^<ps8-@)"ZIldRo4Y&Pd:7?')HcdXpagLsmQN6Tq5^W'`^d<I4.OXnH4MN$!"\u'J%
|
||||
ZlV*MkbP4mS,qlO`c4D+6VNKW?W,nmBEHdFFro9ncBt,7)sK%>*ZBg/EK+H]&@f`JM>!1U^:gOcBq/>I
|
||||
S#11;&Qb;aY&T8sEU8-:QRk3=F_Mi-.Es`q^j8bHK)UPFYuBRX$;F!L1Bt6!23+4dGE"BsooWsY+^(__
|
||||
+a/j_g=/(ank>$!L=*gpcB(4INPcf8Vm-3(EU:)i.,EB;(E\CO+^M$dVTG5_N[&9Y@VQnP[knTW3&Jq5
|
||||
>WGL<fR"I<EPUJ*EQI%?EU8,Uf/TF]1lgD=E+tiiXE8`%%ZNj:)Ma[uB?8BSUjfj3%]+^pLGDAj1)@8e
|
||||
nGBdhNOP^'./0la5]:?VC(+jI>JF"p%,)c%6fST88/c.@(>Gl"hbjC_\"_+&.D*q"2Kf/Upi-=s+3>sa
|
||||
l_F8HlpS4#("!ThnpUR/^VW0IrP4%`A0)OKR;Gj/JACJq+/R1.DDsT-$>P#q,/$K7I#YnhWd<Zt?_`@'
|
||||
@\`ttgO-dD?(Lg!n#@*aJPBr/R>dNrDo"3Mo(]:QHceHZ$i5eYDF`2``-=F3^SIK0(!b;IC6uM@TF]C9
|
||||
[[Uk.SBk9#9l?e5ek#ldD:pQ:UHi:SFqAg@K.&6V]!UA1*H)HLO`HYM@Z6H%H%IW\j!rs9)WWWBH1nY^
|
||||
0=57so(:"<dc&dKLV:7$r=#ZLm9isqY9qU`UT_V>g,ca:&Lfq4*p$SRQPNG%M7YcP.97e'\EE#P-C/*G
|
||||
Z4Xo[NV[USMpfDu2s0Bu]Wc=m*1jRLK21&,:i>ZT!*f->LT"6$+qRE(%%\@;h7U,9ek7*Z+kkn"P2XqF
|
||||
>1sUbYhKfOFQbQa"tNVA/WKqU*!on';0Cgi=JljS=-fYo(8(9bk:SbqPn'NSDT6?8-'R?i%4A,e]Cq_T
|
||||
TsaXUQe&rF,&Su@7eRs1c^iu!J`c-SAD+9*\?Fe\?g\l03G?-rp5#)u?17UC<AZp^gP%KIM7Ho*e_;)1
|
||||
b[$le"G'a>X\-gd9))sJlu+&nF"Q2<DjV;-NuIH>A:K.i[X=?`m?=R!Tf$BlJP+?aT]"4B2H&p)C"EtV
|
||||
]^b?rl%NM<P\>ND++'3"Ji79n9q_r<$dHJlQE+!^<:c=,.01@s3--IMSe_XfM42q=O!s4o4dO&>nB5>#
|
||||
%(&[,l^Gj6dX`HrVo^%+nJI7PG5<<-DO59^>kG0]0gq$BYfL_]-HC&m\M@J38@!#U6Q'i9oWZmlg#J'X
|
||||
Bg1mNRL6_mI%)D',0*k`;h(kXC>WBT+ni;GrHlTMhsk4V]pfkEl0>p+#-oQ(TtoR&4'?Cq>P6mYfDfL?
|
||||
l</Vs`f'NPkdG/6qO"8I2VrT8DoYkFp#!%>+%eX"s$(RM_4TSnIrJc7=dpdFp%fPNnKgX!`GSqUg^m'k
|
||||
]eloGj/k07:T@ie?G#08.dN5<1*g#s@)`iTbDu7Qm<85>i2bWdZ:FI3U;'E'h*!rJ5a/TVq6mMV;>HN^
|
||||
>-G/efWRd5h6E\%?9N@JP4aLb,kPmE5VYU`M,fkV%*EES>`nj4L"HHR`/"Ej];=OJ_G.-uN3&^k+7D;B
|
||||
G`<n;O5(FP7fAtcn3/T*^rkuJ4!5oV.L`EqdOgO!hH2bi,/*r"^(.F`dr'CUZ5m@c?'Js0AA>Kq3NmTr
|
||||
\\Dm'h0G'3csP?EpNZ$.e6h`03^%FRoIs8<dPS\oR<[*PIPdfP]H5c&;6Vk<p^5o>4elpm9"OPZI6H>m
|
||||
)^YH`Qm)-Yo<l"&hXgInT3DKN:ANZf:US?SZjai>PfVmNY;3p9/jWtdoboB#o19oITA(.@T)L@5P/Rht
|
||||
=W-d;:o`>Q0_tF8coCVg57TCFF84kTge[h0<8Inq1G:DB9AM:j,M/Ao_f$tCk)VM>Scqr)EM"o/fHGJ$
|
||||
GF4flm*+OaSfO@QKqc0:A]koIn(g:<?SHE<i<*\R?oCWnScI8aN`o5=)U%oM3/(3G^9B%L0Z+]=r3efQ
|
||||
WBA7DZhZEg.Qr0LemJ/'5r*;l4,//2dn;PIRA5W<.(/u(+hi\)Y;_<qr9Y\Hc&432^MNkG$ZeCBU;X6W
|
||||
URG1!)SgI)q:m;P":[Y(<qe%]_hjt_DR6oTnFps1j<>*g1WV\9VRO1djm^H8$KF<6pPg'Z'rUiKPt3is
|
||||
G10'8FuuO:nLNElbI8Hkm'=X,DR8[]]45kNN]$%fV42dN??0C12q/2YA#1_5XPUF8JM"Jh8%XluAp9Tp
|
||||
qUVq3"Pp*HF,c(Fd9/1+k5DR<WfE*O:YMiNDu+Fg0uJ4G-:q?eiZ9)SdOe&^Ot6Ccb#O=!XFQ)hU-pc'
|
||||
BeHNd7LMeYl1$4%@r;Wn<eg(5P^pd-)j3&JSVkfgI>@AI5NiP5=^h?B&H*k39$L/KQ8CNjHC*gs#rPN;
|
||||
P--RtW[#pGoc!-_XtgU!Ir4iZdK4>VU((Tp4?%D^Uj8(eGcHC@b)0bYa)$a5A2$2]Fn:#$`7;a&67uL/
|
||||
o:X]2Lt35Za/q;uAOjHpg\#nMQ=OB_(a&>pg#",JS,9<E"*'p1jI*B*1mt`VSre(&IQ;fni[79HcJk`2
|
||||
.G`:KZ<aB(T@&AWdQa:TU^Q<s45kCqoEZ^6Vanra2>spiaYPLO8oo2Z?UjLKDHZ/uQW6!aRru8^\+&QT
|
||||
jIWnO_>U5AI7"IN3!*9g45Er"A*9fHJtjq<p&!3Q":dL3p"qK?q!39F39m9Rn+a3JC.dRI<V<fg(,j,0
|
||||
e,U\pkd?L7R8%=lU<RjaJ_Y\W`[`)+V(s.%ZjnZ*HrZU=4ZZd0gfKoN6:2j1T+Ij)HL)*8,`;rq<]f\4
|
||||
Fb?if8s_\:jS]h5Mp)%[)<p^8SIYQIWPfj?Je.^,h%]<_`G3PHm7S=tF9TOMc!,WqAO#NFk>]"3fipI)
|
||||
0nRR(MnRr"?QNB%Dtg!#<].Kh)*Qt?-&VRL[WgAZ2Nu=6=mSP*Zs2lb^:nnio,\KL*&>XjKq>C1,A"J^
|
||||
#C_q;@ZR%2(O\mFcCGDRFi8PZ#$02\lOSC.QRB3\j3up9BJ371fr9@E0b*]Dh$0FW1>Dcp8^u\1$#)Op
|
||||
[1_5PT6;R:el1?5Rl4tn\+-=iBsKgNJ[-Km_EOQ]IYV2ZW3pi>h8d!A`5lRkchJ8f>-^i%`e:oAs1UI,
|
||||
4ufMgm1rMZ`.LMgm1qJCG1eJ7j-ihAj-j?#\CNU\fS-$nZM&L``tt?<KA'I4Y4m[fY>jo=X0!e38irYY
|
||||
gj-8%mNM+TEle7g2rdo'2RRWTE7R4#"oMdV5c8MVcW0kbFX,<i9,Es*i\-Dt;R8K!c0N6$Z<NuCUS]i<
|
||||
RH5_=0A1&4g0^W`/8)SW3=gC$(=8?fpB@!p(M*H7-2i#e\g,Tk^'Nc_/@jH02g:9u=8PVM"ZrXH>]_NM
|
||||
S&[4oX#.B39e*]u?#tG$>GTr.^q6=L@=JYrW_i-N0(:E/CTg8!3u203qA8#^cQ6]h5bf+QIJ;nEj]1$f
|
||||
P.fOX-c#+"6Zg.*g,L$b\(![bgD.0/jg-dKh&i*Giic73^Q3cEi:"q$C%N+bFj^(ob)uUZI5kMmc6l>q
|
||||
G_n6\f@p'0$>2b.KI/iOJI>\6L7A)JUoK\a2?r#_TZhD`+MW!*E.1lfp;jl'EmHhWW>,WoFJ-VjGj"\L
|
||||
0@39ZPgXo._MAEJ]X\Dri.??W>*ATTon.2,gZj=B/D?$8.l\i1NV#R.C3$F07CtUHDYK0](\?q:WMO?(
|
||||
ZI1*!g-ApVnFa\=3IUc'iO*[N<%Sj;;uD[R7d:/i9'RR2[M6k8-+`0Q2Y%KH9@h63/mfTh=\)[RW:_R^
|
||||
65>j.I^SpCnB5>%-+$=I]gQdBBaoLC_)$8HiPeZjLkc=,%\Bql8kPqf4`;71<J^9Z>bla0q1fq9/^Kbs
|
||||
*8>7o74Z=;G@A,43\5tCCa+kB22`^K261*qY#=YF;fYW$O"\XL=>>)B26lllZM*H"mF?]fb%5N&fWf=\
|
||||
)>H;_d^uFN>2j,HeFL!5MWQlWQ.Od=Z*UMJB)?a$2"4L/IYZH<H]`<sDb_a*9W>2^G?@aLS#j:!_06AB
|
||||
.Bcn_-=:8Q+ie^fpI/)*-q`;*m6Ha*W60U);I5\jiXN84.Bg&O.HOQY/%k^,Y>sKM*LH=H^X_D7O=TCZ
|
||||
rHB]orEct6FGi_9ebVidYL6T$j]1/!oLPQ;8i\W&*5@`&f.&kB@o`HRU`@mN*7kdYA%Pd,3jh2oTLuno
|
||||
*&NnEP=$b[2uiB,cc$r(arQt9o1P>=3i,'\7<(nEeI#l.>d`mkPh42<'W%@@Cf4T/iem]3%<p`G>8j
|
||||
B>15?mf0b&0Pn4W32TBV9IY?sab[V6i%i`f\lF5-ZA4CkWBHT"<^S48DN!=#?c[=,Z*U^%N7]RCQ]]j;
|
||||
CTi`M>CZTjlnZH3PS(>l)<#dE7bEXYYBYfB=T*F]C$"29[7jlWM<ZZ;p-tq&85$>hd%a!uNA<go)/4r>
|
||||
&<i6B`AJWN(8nVP^=)7DctX\`mFnHsm>O_.R\CJeVP.OR*'tORq1ilt.ah$/EGRDY3nlD-Bhd!dEVF5E
|
||||
];%B^J0T"NC%_2eJCc%k9qFbq8<]BRn.j0'gPEaQ7dg7S7QdP20I#GXQ)q+FiE5nPrtddN%#`-^nnsr'
|
||||
B_Hq"l\=jtb(jPHc1He!drX0b<k$k1Ya:h@HBfMeG9a0dBRGl(R^iWCD\fR#-s`!PH^cYdcb2X=SQkqe
|
||||
O!H%8DobaN[tg^$f/8K8+ab6%Ousg\IP?+_lCKZuACp7H@KqZ*?7fcD`Pg&rCH'_k9uVrpG0dYSmeQ*E
|
||||
kiqdhoQ:,]:'"Yf2YhE<pHGWJ6r%`^#Td4Lo(YtH\*KFpU/V=UFlclpm/7AfK[G$YH2Q*7eP1.QnPrC^
|
||||
4hBnTV0?I:GhsjOLd$)*339SA^&5[GJ$nM6-sLe?Pdngs_YW`rZ4H?q6$caa3fZBX'BBTEbtsuko4:\"
|
||||
_gI<)3g23U(AX0\"[_U=(LMj>6<6%'Ml\ba)hrPtPC0Y7ROGn-[l4u2W'&FEW`pN$hN%pGEQf#Es,O"l
|
||||
$7Tl%omJ)SH89e@]iCY6_YE`)[mkZ.X\j#M25Kt&hd_>jf;kF5A[XdM7OM7'H[bdj@\uRq^m2ir0*3KG
|
||||
d3_1aN@t_fg8Bc\2Qi/6Z(L_/j7"O(deEH.AZ2h=jA5!?6_=%$Te1%laAZ`C7:ojOGRK:9bC%-FaJ3Tj
|
||||
h@5Mt2t&D]e_Te!Zk&-q^LXL$[gO\MX.Z,3FOQQj0='o*o1E9WG3FUM<;ChbHmMM\,JVadb=*HW<a"a.
|
||||
(?0#Z<Jm>Da%Oq+RaITR'mWCT=aZqmU9U!:/^7Yt"Kof=r/MYnjA!#234%7W21KN\rqFF!:uUr[M&a2'
|
||||
8]2L6;9H.%;AD!\dqGJ6?KKCqi]1qQpH5;BR8(i]Au@(7?68XeU/_Gd12"jCO'N]NrsQ/``,.!S?G5o8
|
||||
?eXt5n%gjfZQnpOn^iD3NZYWX$fC$AhgKa]&Wst4'sFC^8W.CsT1R'ci`&:-el8.LTt@122Q#0+-'74?
|
||||
NqYNLbYK5dIXfD"LYq/lrs;iRm4FO3]k%i&\'3fTh#>/Ak<H.gIm;-0:9;_.=jFsWeSO\>4Rj-Jq6bi+
|
||||
=4^4Bc0e`[drt/M=jGKtNm91g,0p9:AsP6&frpTl#?@ss%3lAd4_!gcqs-82p\:*VCAXXkfb"(8B/?c7
|
||||
AW!-fb]:.9=qBniF+R_&QF"9(EKF`l'?(6.R@1s"MK3C^Z^ghkAi[1fb]7<F4j06<oIOY5iRnk]mlTr]
|
||||
:9=8`K-JBU$&T:@Q8A#<:,-dY#4mh:q(t2Q)-K@[\]W!:)pE'(1L<IVaC(K=Bk(HHZEYFXgE']ZRWsl]
|
||||
+_Ea<[i)W\=heFlEX!T/.>GJEjr5u.o+')9?KUVpb]-j!Et)_!E."70J;c39c)ch1VJ!N^RaP7l6g>9H
|
||||
bB"6_q880F3@WulA2t<MQLhPuB4mO:]iK/g,9s(Ia!j;t53q.kB=SMhaW6l<mTIgs4\BR6kfe'R2PTVB
|
||||
cM$[uonfO/&b>LC4NlD41*:@7f9ta50RD&c[eE]1/j'MA6gF%"&(=)9Dl/7eV@mEl=jD3ea`JO81aB>s
|
||||
mJRib;56c/%j@^DOb8l;e0l&tjc!OVB(O_,+bpT6di2P$\UpKgG\3j$8*r^t<bXHKVpf4%QF"88@f0j6
|
||||
I#FhuS(D*eZ^ck8bF,c+jij$Xjc"I)T&jcA383S@g8p''Zi;ZJ>M9+SL$_W"$$];42dMg4D!(jormQkl
|
||||
F_^EiL?W[J2m]#uOn<1lgC,I$LP+`s/8+O_B0"`"\V$=3CbOh9hX4.*c7EUT6cXF#M5K3!bAUZi*K]/+
|
||||
,jhIo"RRg5,S?i;=jE6VAiWfPR%:a.TV/c\6F*$P832!]1[*Y_j\1ZM/E^6PG^`c+erZ?Ep[1(CfR42%
|
||||
]#V"!P2d%,`,FeVFeB'uJ='nXHF48q,[j[E#p;a>]>l&E^kSL4ZF/F8rUajZh>:hKmG2R=noVSp:UINj
|
||||
"Wm=3i0T[iC2\YEkt4s(J/!4i_hEcMpEQu'1?EU4a/qVDm44$^K"H^^D(l/d.IYK4S<EWM+-,rTphue>
|
||||
_+^/jeYlj4a\J0R3K:QefmC5_XDX/Rk7?\G%HTrWrfT>[dY0CgHS+k4oVcb((VPMqNHnd;7^:cj(2KsV
|
||||
kjcT]8ZW_DG^m><BTT]t'JaGFLSWI1:XL-0T-@pR04nfCn]k%goPq(^eQr]q,R/Nb+Yd,cV_H]!GH<?$
|
||||
Aodg$Q"@#mD]!P(G`i0#fR7)imlPuW86UtDbcFfKEGaB`S[`\\=*^R*$^YK:8pXUJO7+f`SRi0bN\/JB
|
||||
>QIRUn5-R9\G=HgWjXdG)'"hQK5?gl;WOThdSAIFS+<?&jf@0nhEqJ]a%iq%XY`Z\2c.+4UhUARa-I0k
|
||||
W3PQ=3-M%!:q"N4[[?8W%D?4(11".^beSt38M[5499jW]7qZ-7Hcsm6Funn0UEaB0%_q@hUnHNbfd`23
|
||||
*8)g*eg'`_(@>79X2"p=*'9;O&B@ON*Tg)6XuLUUI^a\eq1itV`#^Fh/W;Z4dZ0[OKmlk,b,.QN>=Htj
|
||||
I$CHbMqKTBKCRBh?RFsS*n`$,h_Dl<OdoX7cB1SCp&A6Yq#q_W,P15e.`CK,EHbd(A+Se,>GBT5?`.S\
|
||||
ke'@T4+6qGS^?(pLZlRQ5PFUsm2WJ.nnY"d<^&q3=!o6gd(JIkkf^0sWOIsPnW$!&nFuD(IJLr6kem!p
|
||||
^np2`@^"T0k$uj7o>RPYqk(sKW)r5ZJ&M8EaMmB2bI<roNB6CCB9^..H[ZiF0!r"ni^'^%HIJV/os]0U
|
||||
`kD?R*uWrp`iVX[OS;SIo,17Ue/j7=e>X4CP]]5uHdf-3[]<aiF0$In7(FMp7lN2dc!;)@(1b<6AnNCH
|
||||
hZtOr&rEg^-d(^9(B%54M2hMlDtDq-F3HQ6\srDtbj00#j(:btX))@*U#WW+lY_X3Z2(NV]!Y9H=",q5
|
||||
9'<K:af`("Kde(L](584B4>O5ZYlQYO$R"']^ktHo=F=lGI'ZL0)3sPQJ84;AhfK-WQc8iBT"r'g-?YT
|
||||
p:)i#E[@Hnh=J2imHr-`mT:KtfBju\dj;*S0&(V@"FYN$GM?Q&=H?XeS9?^^[B<t\?95G;0;m7f]^@[!
|
||||
m<5W!6tf(Im-HCs\lh*MI8X/a:$]9/KSHRliUe4NF0kn/O0_62*+NPdG?^<n0lFQVYIhnh:hh-!lZR)i
|
||||
h>AS*-JG3'OjS=@pL%;d1.XIt]$]F'H/7O.cg,J$jaB*o3qKqFbL8VX)qcg6H/23,^Nq'\2)m4)$$ND!
|
||||
$(4_`P3a13l__lT]BHP@37`,8/m145Q@$WTRTnOjMguR>*e`jGYJ2?KZJORUlPZm.q&,fLa'SeLLo(#p
|
||||
IJX8S'\c:KLnt)Am)8TZ(H+M9<\1)P'G1AP13UT6bg+juV3no6`DV41dK$@N$@\sV$JLJA$GNNB$@8LM
|
||||
$Mk^HP\YZ**7taG7k=hYLfd7Wm/9g5`T1l+/7l';LntOR$aCD=4'L-Mr.q3-Q_GJRNEAecl-;S+`DUHD
|
||||
CP39m3I%BVSXf5a@it.]*YGt3@_1p##dD?I%'RUjKq5V9a?84#%XB@]H.Y^LK1AkYi1SVClBg`##>ZO6
|
||||
BjjYl_Q:@T]@#$@CA74CO-V1nr0DoS$npAAs+2J]dd5/7V&Tm2I(Af3K5#"G5I+.qo6/;l8C<SEoS",]
|
||||
NmbI@(H2f)B=eq)o<6jVH&jpl&PK[sO0Op9R8>H2:nA@oYs%,J/B-tuX]S1>O"6N*44C<68n,sZ/V1Zc
|
||||
HRraq8#b;TZ5$"e4(q,8`R^$N=NY08&5*%]=GeeA.;!"*`DSDdLnuZfR8Q.Z+K7_%Ak+Bu;*udu@f,Sq
|
||||
=GhA1[pgf7N88C_XH:itT)Yop8g+EgQ6KX1oNHmBVVaI[pcu*K9J/U^,s=5&^N%aYW@1MMe=BN[Z$*!X
|
||||
fYDD5&OM^+Q82R>=N[;,.[Dh<`JC?aQ?LR/P+6jB^dcA3c+7G3^Ic)FK-Do7U.aK!>JKEs=A$$*.$c'Q
|
||||
pkX4_[UL]JJ_l9\,Y=';UP@_AZ&$Y,$MiFd.>VL#LnuZ*L96@d`\4,PGY><+5u)5b'L+]"pW+C$mA?/E
|
||||
nKq"',enpG8[4LOI?$Bg(3LR6HJ!cmcDXW5[UL]"/mS%WG0-SBoMds\Z_+Le"\LJ7Osh>o1W8-$2-l5@
|
||||
jBLK$@dgmt^1Jfp)d>b?Y;K7$JS4i;09%?:g,4o\gC:qaPJ1r(jmr0H&Uim1.a=#](4N+Um0;C]MA-!S
|
||||
;'>;[EW2T*_(RZDhK@auTO4<jinXU1i.Jamh^#;*]lfqMoUa`%34HYJ$hsp-9^_q*b'tWoF\n6tEZs<0
|
||||
6-L95m$7EL%Ib+I4(N8Me;ebT\P4:[0:q^YX0ci*hF.6UpI%B;J+]Jn4VCYB"8CkrQ)uH>e_75a456a1
|
||||
fpNn89;6>mKf6WILk'?TU9+>c:E5<rcg:*%%ht_,6Rm@5<K5#*.GMgF^cY/.*kg[rd^VHW&a*WDg0cjh
|
||||
ka/e(,hbC@qcD+'&CD_rOMH.$7bk9I[ZPn%9ADs&.e33E;XhN;cI(RmS8XE/\1n23Q0^ebUgJm`fWJMV
|
||||
I3t_S>1Is1=c_"TQ<Lh34lj*K=2c?H;4sFhG0G\t>>JqiW*NOjqPn,$HB/t_;b<4RiA%t0V&F(.b."o?
|
||||
U/aW3dAeAodA$qpTs\m/)o)B/p_[VLnRQa:Aer,GHURA[4&gPK4)A]f>7eCWS8#`XZZ>Z,(f!ks2S!!M
|
||||
AurK8nsf$i1X5+N?K<@J8TjEXckXg0:>g#%DjO/R6,+\>J"(mYS;5s@3d1p6h/o"G9ch4>%H)Uu*Klb6
|
||||
hp0_)Y(o!I^LB.=]!*J:9=`bm[2Z_%I+@;&I]u>NVJZ!mLh"%iZW>+!3nX9f<Hj4!.U!@b^I>23]hZ85
|
||||
)<)(1[[lgUQ>kTI^>o2[p4D-i472G3XX)KJI1n]Hd04pGM97t^HNW*fA>c1MX[SFu6T;\+FdR'mT[bsM
|
||||
Y0Ct<gaq)pS9%4+idHZHWH_dI#12%GBSS`g^.U?ID3C>OS7@>fYO^UiSkeb0B/*FR*&20pQejH,!FGfG
|
||||
7\>*=`_,U"AF3&$!`sN*QBG/`SHaSt)IJ6/W[T8SKtd`IB>,InDR!8P6f9KNOT8^]hCae9[^Eb)b(\(h
|
||||
OiL'dMj8uNrpa241&pqdm'2_#5,aYfs.3eP3-V`;p`?D8lPk3LX$o6+`>8Eb'X;:WYl6_ucdVX8?e1r@
|
||||
\TgF^l_6g<C8FmO5O4=%G4Qt=ep5HW+`dCYY\A?jb^O:&CoUd);id?Dct?@bLm.NqoG?PRj!l(1?YVg+
|
||||
G)*FQUUo3n7mM9JL\C[_Q^W9#h4"o76)@>jd5c-Cn(19R^H8E`f6p/jFbXd"<&uj3S[)FDP5q]3Z3/&O
|
||||
(ACL^/OA[E;S+k&#A(+q2LK4H;ESh,BU:qV1T=a(?`/(TB&K`OdA-9`hsqVDen^5O:udECPq-r"Qfa3X
|
||||
(Oc8S13EE_e'%EM1HPd>Nm4.I:Mb=p-^_;g^mX`[Gm$hsP'B<_o+s.Bm<JhjY'R&u;FCesHrc2oAFeC6
|
||||
q=59-El8u092FlQmbcqq&+TD_f^f@[J_nTq5^[l6O+=q;49?Ug,4%I)#,f(CD/;)\9#]"5o*AN0]?DM8
|
||||
C%fe6!mW4c#k%NM8QDmoN)C=9Pe)da-Mq)U9g%D3YQfHcXhk"@pCOqDCTpB?0?1W7i^O>s8!J=>7"s#j
|
||||
7SVHnefE#jMGc*Bg-S3"Mq=e2a`b.,UPSE".()/o3`#'J0qO;X%QEY>=h[X#WAAaOMl"dtLf+U08hE]A
|
||||
Ol#_Vo=QrVYB]]om/b5D?/)`a-PW>6dR>*s/lCl=1>BIrD,Yb=3<Ji2HDH$`DghZTg58d$Q]<-EM(&&.
|
||||
D%QD;ETkYj,+?0=P[e9h]#ubL.!:k&hj>.oa@Hd$;/4.(/PYW8G^A/Xe1LImi5Fh<ej3jY?mdp8d+`M;
|
||||
<WdcO6Zb&B:'rrM''Tq0SZGI[b(V$3/R0LK@LrJbA?M2%NYJ<r&"U!ns,c*h>"ctLPhP5j;<k<4T1,`k
|
||||
p4SisiVMXhM,LPIN!'ea,^@'mK&?+nl_;mY@c./NPhMpN7[+fNU=-LI]C:i$SkD*mn^Pu[[cY6j7W*r"
|
||||
,F%#Gq7_YIL.f`=21O<X7e1@d;/1JY;<f/HnGYnAqAS78]R%)"`+8Cr4!s:Gqo'tkS^KQtqeK[)[9hBN
|
||||
^IA_J,6X%=\GJ]T[-LS(IT96[oLMjE($HQE7&^X!Le?EHhsn?gRY^_C7I1JnM^$)Zim7*l9\B8:9CZKG
|
||||
/ZsmVRiT!QCUSZR;U!:,TdsSQPB@V"g".n%mr5\H*a^e^NVCN@N$pQ&N5tk*^YjoG)d\D"DtXf@C>1tU
|
||||
Lf2u"ZkT@F<GO$uM"*o-Z*Q&T)bY<SnC_]e\e,l7=-#%B*i6U6mos_K2/qs4)f^re7B(.kSVg;Uni3An
|
||||
i/`[lRic)h0$u+DK<M:$+fj?=V,ahB'6BBQ[Tb'./5;4s)9AT&N]YQ+AOFYe,+2d-GLN3t$=>5T+a*Kj
|
||||
R;!pr,7_IgP^4/m'r;,"'W%+-<0Z,!H7DNMa[83Q=qRSH0-a(Pa\dl6:<KK/e:V"X.!<9bC:>Q\D;/'9
|
||||
Bk?*l)PW0fC-07Cr3e^c^)3^'R:X>^P`Sg5WEG]fl>;Zn`@*UY'.<c/96&reI(aju5>M-!rcXF5ejYqA
|
||||
?ReZ2-kV85%n\s3og4Nu%r-X&K>4n12@"XJFd(e&Qg0=M.Sfnal=ECi)L5`cak6m3h460^;/4HoHBd,-
|
||||
]>f3:.6\WGG#[0as4P(k8=@al%R]b5,4mljr)N$jaYt3+:sdri48%UGj?B<fTUMV)OG2hZ/(ZG5;V0:H
|
||||
]UsQXIliCFq>S2iSk8srERS&+erLJsdqJ6*YP)%Fs2#B(W5$<QcZk@tJL@\ArC,`.nWYL`D2R89#6q<j
|
||||
KU-G0-beUkR1s9Eb0r*X%SriVo)hJnMR:>)?hH5JrY&X%K'.'l#qFomGF/:50AQc"W!./SBU0.%O"3k!
|
||||
,l+0.'@8PCL$4G0=)#@L#l;\ES=jWH'>r.RQklB.'7J46W_P-Jr?bH^8Y]@OC\s.\`(co6[\]%&a^\E)
|
||||
cLS6CX41a1ae+nZ2P$Mp**qN;Z#2C7e'X*f<1A%T:/Ge2R(*IPp7*pV!k$^K\d:2lrnu*QJN1)XPr?'M
|
||||
G`O:qe\#TUFb36^1RCB$$*(udlNEJ,:/EgV22aTWf(-=!5'7MeX<.1f0nQnA[)b]ZmpY%m9q[oTI-ik(
|
||||
F;&?![(&(X:/G6pk@oq"SW$<ZAD/-U4aZrB:2B&t33@b_j0V;(R-`Rs1PRW#IRbX<[#%Vje<8_?C1$N.
|
||||
9<g:jge[X;XW**u-`rssjRGF4&/D9j=m]\s1Cm:3??*4D.&.mN]HI`sjT,N<b86XVZS2,=D8l2f3k:ig
|
||||
'RE42F+E<nb`[4Bqgl]e?V@7)la0+gNG0eH;S$"Tq,hPQ[ZN6>_*FjYI]tm%G,odsh4l*i@rat'W0=rF
|
||||
-uRr]9=EjDnX]R.A*V%loO%64e]i8Z_sjP;KpO@4/h93BV%(fU?\PC78L)bNl+3,pGd&Jt@e?*VHPQ:D
|
||||
=2R"sVL@j7hVYjc2[gYf8\7D$0g?N=1?f%Eg&i^`iQuJ#.+G%7N6(s*N[5Xk)tWo`qnXZioRg$'Ua7to
|
||||
=ONo6;JadX!-P3_N7/]nc&"B[647<rL?fYq=rQ"@S`";5I>TTb[J#?aa'.W/Gf:!'7SZO6),/qEioXCM
|
||||
rJP+h8XuQScdE+`\*)N-nQg-dD`hklf"Y]_4/Br,(FD)Rknk&2'7AMV^a!*lB2=*<"CIFsoFlK8[oi9$
|
||||
r?mU=I+NcD-$PQTo&^B4Dr/;/k5BYIjY-7*rcrqh#;7d#9\4N?))tr-2G_PNGEh\nfn8dSYj'$iT#tKq
|
||||
YgQHapN7NfHQ==)it9a=a^=<:B!`L10]T3[JU,9A]YEt<o//_G2f/%]n+VRK0Lt?ZqaF;Wh04/V4dG_Q
|
||||
Yk>rkd57Cll)>qc&H_8M=T">HA%V0.jB)!U1O>i5QrfqrK^f,Mi>+(!gN_Uq^&2&W)sg"U@5`C-K\/GQ
|
||||
^eO8<Rg*SQ$e]\j/RD%-D:A-;ZMEdS?<f@[6&YJ>d;tRJ[Y/!_[\J#jB(E.ZMX<P^<9"-oi#%ON*e2#r
|
||||
!jgj_(RR..M&2OrrG;!*1]!d]Qu*)coHGp-([pI/UROjp9C^4%eum]br`,F2mh+^*4dGd>q$F#/Y+:G7
|
||||
5b@TLZgG?)?=4i8EP0'qrWllGAO':B.da%d60=)q2VZbFQ$0Sf8ulJW"/-7t<)-X..Qs''YXdiT,]Z\W
|
||||
A+NYpgQb`a.]7PHX*Z=/V4K+e*?fRSBO9LnajJ$7ba(Iko`F`Hmc.N=d!XXo"OMRAJU*E6H?Rft"_R:F
|
||||
ik:$'W(H8Mj+D`Md5-&BTQ^j:R>kd(2muVm7:C/m,Rl]dRA+*p^Fb[:9Alm]Af3as`9sU8D0d[EZ1$%<
|
||||
amZa;E`S0;I<Z8Oe*OhBr02A,jK3UQV=U%"ZZYcHg$bsuq+irt$d=5A5kdnebRMlY6=6aqq0+;OQ(W>t
|
||||
Vp\=\*^ifc2#gU!:F19iYKRc&7%R04lCLUpZ.jN,JE"gufK>FZF:'aDKkl(l)lq9D=/mEi.Odm[<)VlN
|
||||
Y%e=Yo_)1A<A1ZS\o?-1l36<98Pa_Vf_*FSU0Xq(IsP(ka`C!Xjf.;Ia=9u^YWS2NJ);>.J&PLd/&$Ml
|
||||
Gi,n-(5Je#Vlm07^]PQ.!lkT5T#5=H`'+7RNu)%?WVsV6q]ih)iHX<=rt\tUehfBWd/(W-q?LZbqKQLI
|
||||
=s%ZaL=SS\ipBqLT^I.)TQ4aph!#TN=p&jcft=]k_:>qWc\kWc$Oo<`,2@IlUq5-t3LS7jq@9n3ksKHm
|
||||
J'bd6^.'p7CCP,8r@Q1Y>5t7EBmmW1.BS*tiRtV&O_]ARB`7PGjkB*pH>.fQ#.6XohfMbnKpF+"lW%+k
|
||||
GER^JdeYrt,H];`37Rdait5I%3;`Z)KecnY;J`trf6=%`eVhXUo3,7>hLB8<1V.ap%XrO9k<mktU'n@-
|
||||
6//erT^&_8DV+aEn'+ceK3!'C/["aa(STCrJ&gfP7NTpWVm%TchqB[CM!Sbj6L3ZL6tq'IQs)^NBc$`I
|
||||
om)FU>5t7<BmiPTZp&BmP?u;SLnXK5n#LQBesG/nT,[%J>s];Fc[&mnL$*Fom4t8=+ni5m=nYnY$_k#X
|
||||
phD7Z35(S\\H_&q<67=!VdT/e<bcoEC1i7,/"*]HZR^m\O0^?nF,.(P.4fu"L#&%t\d[QaMY+$rY/j)L
|
||||
l_oNCDS1c([Q)T1+/SYmJ'anbfbI>_na&l6,^$g,MH,RI=,?hA9(kD:Fa?!<?/^"q2be!<o#9&l3[GTn
|
||||
e_-3-f]WBrdu!?Qf!IFY41et*'-A-Es#9=#]gOIP!Aoc5#An^t29rAW=0'WLA?IG-NO&`pH@)W-NBM0n
|
||||
9D,$'jk1Ah/C1KSr;EJ@>1a!4,J02\DOY9hhJr4ICr_r^,_@r[:MA-Ci$Yn3ei:C2B_3,`3]U2',MQP$
|
||||
N7,Z[/b-g&,TUE<Vu/A07]gXkJXDQ/*950$GI[K0%fZGJHU`1MP[o\Jk!_:J49YnL8b%!`;-2d2f:;9l
|
||||
N8&-3?Ac'uPo87Df(JO]8t^LikqGq(09#c*Id,o#ee,U@.QmVde+G5/aj1nP6b)S_/"c7Ae/i9!m>S1o
|
||||
lpf&hY0*!tKVbqp_G-HL%aP/.=iZJ/U88X1cPE^jDU,1eBAN2X+?$eZq08HQ;8;>/bB_h`7D,?6/^pgh
|
||||
b.';;:.%U.'^7JDCLp3S^'P1LXS61tZPBUmC0^R0m?[ItWN$m2?$>dCiJFXan]V9WZ!A9F/FCg^/l1Ne
|
||||
Rtc'3`#-*U]Ztin%lnCDY*'Y/(]drU?#4<I]<l+(H:?[:NE`K:*'p+a*7CM@4oR2s-e>frZI;PdEB6fP
|
||||
%66OWB>+01)%A2q56JsjY!JQ;JO>@FViOa.iZH*@k7Z\,";b*'Vo1HRZ"2?a[(?ImebWP&C:W_JHNVi&
|
||||
@Q^,q3$VUp10,7hL0W?t.+U7b^0$.tMC6ehn0G^tSj\XXFgXE@*(EjOK4'@]"<FMWbm,^u^>&r#<kdjC
|
||||
cPJAC$?n7QP7G+ee6AW-GOdT?mGl1/WG\t?=Z;gq%I'0p7/FU(5CV9SiMI3Gl`6S&2>7qR:g=dKSTO%.
|
||||
q3i<Qi>,jef2).4p5kFeCI%[qop2gc+29B)MV9Z[c=Ue)H?@R/9CDn6K3iEnFBn:@9_hb;lB`k>N$$M1
|
||||
Q@LXoc(iB:p/ioicd'%M(]+!EF&5`6\].j=g&o?_0mMXhJ$$$LR\R2.9<-_fit:)h3-7FWSa6GkmDAYc
|
||||
_OM-.EqT/c0:gGslY?@V28sjK9.(j>m%rt'5M&WcP!/[15+$+TGQ2p5n!A&u>ag/V@=S<#8l&WMq:)1T
|
||||
(JW\#E8u88P-n4qUb^8[4h(hY>3P3bcecEdZktG<B'qhTnmU[P0hu>Whr),-Mm?!F)YiU8r#H9:g9L0s
|
||||
T9Ms3Ma"PonN>E&'[k:S%-caP<,\8;GJtH$<*BDPbSojW=s705?aH_Yj1_BQ&8c1?CHPTteW$amC/;qp
|
||||
G+6'%p=$+aY'%NrlE4-;2RST;j1k:ZI5$,L(L&88FXWff0$L3\Ms\rimYg*aS!l-*S,/^`8A[Wqr8kg>
|
||||
Y<1SYPV%JG#i0jJ(CHXTq[KBg-XbHNj]8jD7:`?&^N@d1gXWG,?d8-T>U\H[b?UGJho*IUKU34G5P=T@
|
||||
7n_![HIVTX@AWDBbD2(lSK(kKIOQq=g?m>go0X5a*aSCa`I2Y*:CT$c4rhQINBX%eFk9tajHoZ4i;iW;
|
||||
D8R:5lG=<5.96tmHjD5H?9Y`dZDNs!1V^Ns@V<Y=PNl3@O=q]M[U)-c9?h+X(s:,-BY!2k!PNk_=][6u
|
||||
ejh1/b*NW!"9.j7l,&btLTJoaDaaWtODVld%fS@J9qgP<W[N(%j5p]^Hrrc!HiN-f_\0Q"kV?XJ*:dM%
|
||||
jhi@(1:glL9E&+1IrKduQXG:+Qh&W[QQ][!bg\B/P.Gm&[G3fTe9dPDd[Z3D+`nMXJ_kM!?Ht*ciS&Q_
|
||||
oF@DuIXfE*YC$(a*#dq[i$%e&T!<uPruJ0>?]\Rc-hcpLA/CeQ^%%PLP)sG]]TfI-;Z4O?DKu]1;!gO(
|
||||
:h#GYZ+:TgB%&gH+hIsB`K?`d!F3g'rt7GlFurb\IC38hUj?PrnG[TBQJCM1G0sFta,<kh74o$u@JCeD
|
||||
48++\`-H2W3+t*dV-0dDdb$3`8--[8JLBk'j=_=U7k,sc4;<1.pD:,*gLOIc20=i)n@.Eko3]WN)o#jA
|
||||
pCCfJbQgS^hZ'GYn32-1JS28>fjZVkNq89F=+8C[SNLk=1^AX[a!NfpYm^Y[A!Q6c=Zn9-:]'ZT]4:VN
|
||||
G@p_(^H.F)o,[q+^T:Z-nQ6RBY?2[O`7$KB%:OY*QoTMue&V%C8)YoPAGrI&"ib22[+JG/kf-u`9=7j5
|
||||
@rQhh<QpPN`;/,G'0PF'hm10/p8n.X2^AZknT0l1E8tYEE\ZJ?*4H'A*-fA8+g1n:'JFVBI3Bio/fg1q
|
||||
bhNg&U&"ZBps/BQOs;Q)bnR*.eHOOiVVNLhm6\uqc\d7E;(bFme7E2[*4.io#J[*OfU^,S%Wr:>FWV2P
|
||||
ht"+ri3L5kdbl:=iD&AUSX1*F\,='`h.Nl(WK3HK`fm3-o@.gkT!8^%Mk_id,>*a$O*O=G;"?.UaPQ((
|
||||
lB&F<=]YiNcP!;0hb&?3PeQ,NXrk#SI)k<4IDY2grpUt$VWUn;-a\LuNRJAC](=n&XEi/=o60HE5/`H>
|
||||
LYef,/oW8Uho[f[e<d2IN19P"9@<X\.et#pfl(+#Ir'T>GT36'Zhm$O4,"1u<RI&$30\&u:hbK8CH&@@
|
||||
F"9c%jG5bJB9V_m'-I(`VNA?3jfoC$]tJNm--VV)"jP<Sb#eFpCQ.C/Z+>BmG0pS4rUU9_CaT.)4M#/9
|
||||
%?7UK\=Hd5lFW(bH\[*edJhG]r<rM&a8Wb^f&p>4^2Ep<G0aZ`f^m\"))'e`5PdssngJF;hrDDcWsAO,
|
||||
:@f_X=8&Z+1P'aO(<ejpQC5t)J@sNkV)5T/0Pb':r-8/N.oG9;JW!E.Wa`^<.[>HLI`LS(doO]EkF'_C
|
||||
dgV08ZO`!1C98K"4\4'3MHLbIHaon.Zkq`KdVT6"*omd%TbW[n&Z(,T;_AHuk*[%D,M^o*WAdjuFOjU@
|
||||
\:T)Y`sSloC>SB@B?sm%OQc?\lG!nmAd.2HC6Tc4*P^r+>jH8GT$6Nni>f]sQ^`JlrLl*+O\t*AIg*To
|
||||
i%MFh54-glpfU[f;g^8pKHl:7D7,C9k#'[pQSWr8;$M==jP`BX%ak!t?N$d\^_Sf2*`":^Xo:G`Ca]CZ
|
||||
Q\B](KDe,c_N4t6h/ptMmeX&N2LUhr?W$5[,?5-C'/J-l)//?k&?opNA9Ig%LRl*SpA2P^H03cN8Ie50
|
||||
:*\AYG:IfVda4AsB,:6eYa`RR,C#<WC6-gB461M&=-9,*)&^*SFRUfgW34I=^R+10k'NNB&aqZ9XZ)->
|
||||
RpoHWQ8*NiBK00X(2gfiGfQn(nR0_o>9n)V1K[&Zrdp'4ALq5s1egiO[r&\[]G0)9VQCik4bett]B_Lp
|
||||
8s>hD_"5e!GLV,[6+`lIbQD5Fdr05R0E2f3g.-]%n)hRr3dCr^m#*n^2VKW,BNCNa;1njTDtEK>2qD;?
|
||||
<5olR`nST3gW97TP,lQho5T9/Z#^B?^Z2AU[B`SSF0]h0##E$8atf^<\%`(/]PT"np<-)n=mXT0Dp=u8
|
||||
4k:XMRa]DXhcZuso-@`&(-d"]H_)6kqq]E!$kb?V2YR53!M?l;md*:b]p%=Vi"cOn)tjM+j/%Hq9AWF!
|
||||
K2P+GX^+EV@?'Kb"R[Bp;@FUsc)KENVheUl#JH2sd3<UI2?i>nfcPC[rj"b']'YP&M[@hHq9@nPSoSJ_
|
||||
pN1,s%7_5J(Y?gG[o:*T.s\a>N]l92IrIo?EOOctGKk[u[WIcMXV*)XDFdC#Y><^54dc7toCnSX=iCXN
|
||||
ZVJ_""6O</)]s"47G2;(Krmm!>%Eq2n63QRp,cm&D3]7EqbHXtW*-'JUFs5R]Xub^5stUZ%dSG*b*/,R
|
||||
Q@)iEFn;Zl;p!$-Nm?:iDAO>-hrr!<F!<L"P9UQc<VKFAe`VBP]d_GPngTNCO(1nD&q5JOZet*.m4#d<
|
||||
U>ElDn26hEn93f:GnH.g@FX<VZ'_<^5\[5NAlKX8Y[f,&7Hi]((M=`V=pZfbOKZ4s?'I8FI:MLAiPMqr
|
||||
:"iEC48RUo6sDHqLha(m-d/<,AHQT%(;2A<I9ENh%pE_D%-i"J;F+O5/3741RTXo;>;i'OAg"j`N]5X^
|
||||
[&bb7/A<Ytbke.LejEq`f#>cWh*U@AM,_s+#pUjEo.b;;WdG&m@j:W3\rEWdf6h3gPZ>#=`RE,#?8uFb
|
||||
@-?XBlWPObd>ho2HR4tU^3T<g')M0J!.WmNqeMr;]@:_A-Ka!,P<%K#9Re!;>."!jUt@\Thie[!qCIK,
|
||||
nJCRt*]W@b33uCso`'B0&*fb^[&)K&g/@;U;.0?HI+qQsMq_-Gd;>kkT!f@!^Z3M'3iD3g?^?gPmN#u-
|
||||
FbEt573Ts&ca7\6T.i)%E[^M-`W;Y;RCW+SA3:XpcE-tEQ_e0HKHA6m6%<D#n*RQ6/70ZOff9#fZWn>U
|
||||
@J0uX7GQGZRPLip49E%m\8s<<qdHjYd@i_%e8?f,VXN,2ERP1Xna"A6_(X3mka`WPeju,9,POZSr_Wa9
|
||||
#i)I<5P3,D$ea:JW7VhPBclW&eg=;M=0?F<6ncgU$Lu^2nn;+PVSTm.miJ=7rhd_LP.W3tgV)enbH$h"
|
||||
Y?gfq?Y4sj`d&1@_8%4,j)$^[4>u'EEP,\[0oF*i'c9</aqGk[&_!27]4!aSGop1FR0Il7FqA84Fn[)B
|
||||
G;lL2]7RcY6*#RFr!:(mT"]Z/6P&_]n(<%5I<3>=Y;&dEhoJ@Z\.=Q*F()eFo-SP&bfES0h'h?,Pfc<N
|
||||
%5B?U2X=E;TeX$.nYPmJS6t(DSBm[MYMV"nbFcT#0]#ecb+BJ:rB8&N(f@X2XToAdcYtH3qX*CdK,B`V
|
||||
3uc3YoHgP#E8n&='=E$9&'fP[&mq7'6b`>:F0!%^=:A9f9"PNIU*aDnbpEJj%da-rB&C8]\bo3/X@7\a
|
||||
@#a1$gQe:8-iR%4Y>_H&!jR[<QL@2J'(%;[Gh3g=n/FG`b*RM8GRsB'`n)BGTufLnr%`;E*QZf+&>XhF
|
||||
Jn'HMhGLHGY/2-.bq`MG'ZSmk#ES6$=ID+d!lYG!7C`J7'Q4e3:jiM42dc%/0`mu0X`r:_n_K<CnEnO@
|
||||
iuXq9mX"m;la8Lsq$TB_K=[bm3>K:ShN&#H`gEW#6i$JdO#BUIkHK]U44hghji*sKX7OnHE-V*ndtT#8
|
||||
:Pm-V'-f[80('ZI97qGql9!bX;*;OZIUd7SPl8U,U9>ZO\MQoqj^5S>\7oc.@r;@=XN.2`Nc`T4Q$sA)
|
||||
_Y5hUbC&-a4)c,jl!96`FN82!&(M3ISOu:rYH-14Xt0$#h9E.!#7\#Sfsr5&,nTb++.sNAOEg6ciWu!f
|
||||
9ka+>pB;uZrq[aJVWOOr`_AXDZ<*"D+suNMrUJ(V?]KS6-D+QVDq#f?@5BlJUV6NMRfWqBC;\aLhFFIt
|
||||
Rb3+BdohC>U]*@#(WP6HMe1SA[1l360f'TMWLUH_-hfKV#L.%E)&;bkEH)4.ER$Wn1/sdM(aE6I$VOIt
|
||||
PWUXJa8BN3]`Wn&RA*Q2ntmgAaH68anMH8<qAYq4dT;#1b2<nt#%(:\e23Gg^O<h@1U-8c0nQ$K$h?k$
|
||||
0>u,a4P`b)>ZBe?F/]B\nuR3-,gQ'QBdJPoQfrFr>:<#'lEbe9BNp;P^^V0Wr$AY!e8FH,YHN-4VCgWa
|
||||
r>cpsmF8HjQ2]W%kRd+kIOFZ'JU7"QU@4H.k>RUQr4E/DEai`oPSJ:D*be<^W\V_F-ke?(M_^df;2]\$
|
||||
qT*ila8>AV%]c'^)N5ABnh'7o>c>J9.Z/./D1B#FMC4^1Y'M5Z06+PJ[L_-slKncr$98\t.^U%5rHQcb
|
||||
61H3RAM/Q49*q2km!RM%#mDF*r?]":1]%Xr,62V?CGt`W?dWafZ#W9mF7Ls@m-hUHbNgWPP],c\*OZ&_
|
||||
4iLKb1SGj9l#1QtefVQ)X0lg<BK,.:W-^UE5?J^*o;u:M.O?%:_P:FJiDmT'M:ohJPd1MR/PjSBphPXC
|
||||
2Ul[$S<mj28scml&\b":&10o:q%b<e%<FDKMsYgL1[KD,P(1<^DR'1&c=cMl`gBY]Ss`<re*9EUla472
|
||||
`O@_j7Hkh7-`qQVQ?:e;(LkT^1MBsW1QiS@F/sVfY0Z"u]'S+8n?`sMN4:_;qLPun)%1*FM;.Pc>e#tb
|
||||
Zq_(%V2())oHWWNSV&?aZ%3k@'`*]9)#h6(B@?*aIjN^i1L;1]S!\-56A?GS=.*<f(NjHPVa-5eJ\j-j
|
||||
l1l\:qRp$(N.EXQNk=J,ESLEDY6<]'>COVt?bn;pg99E;IdC-T5%"1%S1OCLT^9^Jjl"dpS=!XL=)aXT
|
||||
lrO[Wr<dHAep`>C9&HfXCVQUtK@-$-m8n8=HL+]MBB4Mq&RjqQZ6/t/1V.t!C,`SG[f>3fi%BVKo7$(.
|
||||
KX>(u0"ahc>l,8Dl["OtV+"#A9<QlV^Oggo]bD(FV$LU\lrO[?lea]*qiqEd0J8e8q\<PrnQY*08;sA%
|
||||
q\;sJ]+.]..s+p&$^0N$qH`7DQSWq]b!<"s,t8s,*>@J<?[;:91,<?n@&3O#Qg268V5dgdU9*B]Qg2*4
|
||||
,t)Z]rbB4"V:tB\#r0A#Q`=kZbZ>7VFc"2kV:qO,;Q\8fWRduu@/Y<9lV_To]#:1[?)mk6/oaRBI?&Y[
|
||||
,&NhtlaSs@PRNfFFfC8M913g./oeUQ?+T4fT0$o?rl/ZQp3*10i4o9OmGq-N?.qE='.kQ\/3Ajd>Bl\)
|
||||
IRH6%K,4I\+$'^^^Drj"Op1]32tGq&"i]Q/`gG;c^GXYnp%[*JN3etRD)BEqq>"O7K_;`E(sVKKqgOg^
|
||||
6gb?>ohQ*225V5R"Q7ZAZWc&anbIl!YX^@&DpNK-@(C8?aXuPc"_ma:A<LkIa!@DY\UnAJ^V@OE*po_c
|
||||
a,,`-R"U6@[*F9q$U/R_D&Dqq_&Zk[K/^q[s3,Q:E>a)Eb!@]>4iefOZ^Jt?[dqJWjZ;G7R_)n)*)RtB
|
||||
FmI_`H"f?tl27h66[MrXm#::YrPrT+pSlnE3_teWnKqqi7T',iVrJ"Y]:Jh;IJ$%bED"WR?4Tu5^H`Xj
|
||||
)3%6Cdap-ZqFG_l]I?NRl?6s9.+mo,`H/c=l>q:A;L0>!cbL<kQltg1j&cPaO38t<.Uoh9qRh><K%,H5
|
||||
4"EqP#tt%H9h@dceJuj1Wd"S.&e`0;pDK*Bg$L[F[<q+%8XY,Iki5#nl?6ZrqV-:FN(0R?N)[Op*MQh9
|
||||
8REbIdA$qp6#fO@&"+#qN&1phrdnEF=T90gRH\mn>VadqH4S-Al#O@GVVB[.1=PWZ(J2RP8<->T?Hb?#
|
||||
PdY(n94gr_[9bW;]6N;UbSa#qR1[jU$:AG#9?I[tFd_2i.5U<&V1SYgRGhgYMNLeK8-bJ!;Md.r$;tIW
|
||||
o49`1m9iQ-N%d6`oW^IO^-9=+`S3'$LN@pRA1:)=0@4)i7aP>'-:qPJC?]L-4?.[Z.X?VD9]TB7QlLuj
|
||||
P'blbo/=?Uc=`1d($r-I$qI'jN)[Op*MQh98REbIdA$qp6#kY0N?c6n.)%u+_r$K&V.0CIXST+97fg:s
|
||||
MI%kE];dZ_MV)-@rHG@>=,Z@=>#ZStmCoU$Km+Mb4HIpa:R6@f\Dnp>K.HG,fq-YtHK(!,KFf%;P)`Kf
|
||||
PL*7[Y3Idfg4lPUc(^gd@6I$C3Ng/@J_1J-DCk:`SSadI]#r;f3"k*TgK@e[ZCBCGG+Ckme\aPJ^Z\P(
|
||||
;hjQjd-32QUJd&XgK?hiTkE-^+Y%WX72?'==mD`"c\NT$edSO$3Gj%7jd^7pmngb5dCVaB<9nO2Y)B/o
|
||||
'LPeB[8Ebk8!K#QG!2+MX]f"'+$'+-<R,#*'@WL&OOK]OEZXkrB'8B[?#Y>MNFZCm22,1>U$H@20=;E8
|
||||
)9ke^ef44*38ZT`!^;u%cKdP]6!.<eIm!P]jEr8$a\LM1NjlAjictV$5W3h0?-P[rJdLbmc3GASBbq1J
|
||||
4i)JciL-gTJ7EUS6lW*?f%o7BX1OB/8B79V:Ygd#ZMr/6V(9+,YRE,/S@,uq5jdgRCQEOuXfds]jG8!u
|
||||
?auNtH-jZT`N#;ZM-Q2C6//jnf<X^7qKinpJV*pJ#bKpj^1^/V>XOspgC'DUOL92e\^;#P>)F#0<KSC0
|
||||
J05+W%XFm0r5+knXgt`ANMc<KY;>eo2mu4\ZD=Gb;BG`@D_J7tkA;!!Mq11?C!@&*\fkU2RWa6_g>:rm
|
||||
DFr&E4".A6N8oX$dJ,"J+1"+8J#)%n6)i?oB8*Af^C9E@n[CgKah+MgVD[L\^n0O$hiQMe/,>W#G*g<F
|
||||
4_4'!QhHQA/6.,?gr2.I<a7]+l6Q+al0mSUA1'WW%BeL3<A@`+csh#(_s&+beQD"KAf>AG\`t,Tm=Nq=
|
||||
"a3>,f!,_Jk0CbR!hF#]b//#jMK*^BKk6(@KPhUufOu\Y*Tg"AnhF^mU_SQmQ(N\7;f<q(r/I!',G'D*
|
||||
\iK)d,;YOW?A%PZdWuh3SC,G!FbU*)bI,b@g4?u4+'fca"31aRce\E6'gc=rM&%Ps4g7jDK8[#;$'2>h
|
||||
N5"A%P:Sj.a(N_A[m2hM#'onM48o*QGFY'UpGj=W4hGZ%\Lk5JN4ii!\jk^Np&$ncCaoCFgJ\#?\"t(V
|
||||
B(kEFES+/>V/*ct)`Mt)>]1Bud:5U^"9W.66e6&VaVl5O3k8-#=W#5&Y%-8W`%T5:[H"r^Yt6aXV<M^S
|
||||
Cm=IR($#78)oo2L(:8+1OUhn<m76tg+fE.^@n9ptfD5iMrW^B<qC`f#s8LlP$@VnR;TX8R$9m_OF]07m
|
||||
i<R-#@7B*Igj'Z&>4qa"i@/!MqV3J.;2Zu%N'MXZ=oQk#6AtTLIeKsggjS]6;)\Zt?$#C%*D"Fd1V*FM
|
||||
L@RIqSJDPhr2YQmp)XAE%M?]or0JD&^GiNO^.Kl#O.:e\I6hjGQ+gPMr<2h:[d4"/s2M8*Z'#26:U6Zf
|
||||
??k_-Pr$22RBtUdkDCu8Z]"%Q-4M,H]T?!=]*HSam/WZH;Jd,kG8D*MiCohi:"2e"\$TNr[)aQb+[K0J
|
||||
DM(0XbE_l$47;_rgUE)'-NuXP7%M#W%P(IflB59f,<g0W#l77OA(3Nkkh9S[^Xc+K;?Q.&N+X[5O7fK#
|
||||
`/GnCkW-D:&N68b;19Wklf[3Cl=dLJ/8>Se+$@CBaE28f!Y7ZR,?jqQiaeW=";W5+R!IXu$4;3t4)J6h
|
||||
XXIdr^GKkj>sDTX"8<Q7W?_r-%\[hXBXpR[6#JJ=`b$NGM9K)l?:C't=)gm[HShF.]tZrT>:uh=,Fpgo
|
||||
E&tWCm2i/<mNMi=2tjURhkbam0XLY4hR\^!5266)iQB2bGXmEKMi-G/h(17EWo$RWrn$:gA*kdMm4ah"
|
||||
R`mrH8GnJH6ZUl=SRVWdf`=:F4(RHGTXQUq8]EEW55i/>bNdGU#O;#.O8>`?QMi,hfMXf5e[fV=G0VYM
|
||||
A;I[(V$/UeIFKqY%Ft=FjDXljYn]`hi>$'k4iHLCD9NHQL_B0AO",'fO1Rf7.W;H12dVb4@I)UhgG>8"
|
||||
>rRB_^$<al[cCS/\t;%B=\cOU7'rs=HC"FlXd(]A?WK_?U"JX%;5G7"P:8\\OXcIef.dOH^[0Teqg4fE
|
||||
?!eVVoY!ai_4/.LMF2@]i7D['SkQVMeX_8-#OK461Q\IQAJHR:Ae,dRNIt[C_TSQgV"#;l?J]c:AHm)H
|
||||
V%<$W<ejU9T+d>NccXtUP3eMd,';[G`Uj!WMbJDW*!M*'F#<khN`>HV3p<V>-2,D/Ggi#YcApFbq%2cY
|
||||
K_#Xklh$;U+Hp+PkqiV#L\.Jt@ee%'MXrLo7BY"(b\rC3U"0[&beUp13T)HnYK32d-0TAr^kbF&[sSaq
|
||||
-%'n-#'-4(caNckO28>r_"9@)>/CA?N]r=9oJ4lr\'2#r,HC!)B!RJ_<f5:6pX9I,$EiQ[`F`Edd3:Pm
|
||||
Dm\6t<cdS?n<Df")%TMrhXMIjFhY%A',^V*O6=5iI$JcKnKA#r7Y$^0fD+R;n^a)$`02:LAn$/M,\5IX
|
||||
COMnmS6KUO3qDeIa*=%0bSqUW3PFJIn$k*%/'\tMqUp/.;E:UJ1s(rg/8S"Nr*+1#YV$D;OL'^l&TeYf
|
||||
'tt[Nr'nCLIe(@>\,dt[+fup:nW3?t(!)VpONRa_HspPF2K(`LrX/r7o_^At9JmB*cD4#>9Brg8,'NLX
|
||||
>ne0oBj_VmP?&XCCoNWH71il$McT\EA9gmaD!H?CE=:BNfcTc^>8FYfONT]*fYtc^+k6G''8L8fK3LM0
|
||||
m#R7Bl>OaUNQ,0_;LZjS0NGm'WPgn":;McSh<0/o#0e.A5(]DZ#JK)co!qt`RA&3%&u)+4MrtpO;C-nn
|
||||
UH-chXOB5?,5"TC[!456C#c8R$2$j1.IJ%\]Ao4cd`LD4es=$bUYd0M,O.,SDeM+i``kH$s/"ojmhM,_
|
||||
<MpsdoUmid0$\R^Cl[8QR#I#dAN"+"]4El^D$jHY'<X[2bT$d>$I^d%?d^^!Z6%gWS%+.1p=1/!WB<J.
|
||||
h+nJ=QgYO59(.`=Ek>W(h:"SF*s8XN>i8-.:V4V$/[H=X\+K8u\'4JE=_?r];t-[QKApd:9h+bgohY!Q
|
||||
ATOpeRaTM(;@.](_-X;5*;Kj[07PmFY`\S7i%KLYp6Bs"of_co:@E:"VmV`fqBaH\c[33XcrXkVN+@th
|
||||
"iZ2-5:aG"CV2UiTWa5FlB,$7VCs@320N(_5Cl0XXr.*>LF(lGT#S1;cPj1;gAH+M@Xl3]4=OEla4A/V
|
||||
=VGEf@0s"[Kf].NAVMM8A1tM?@,^!!/=hJ>iL"m6[fhB1GY$#%*.#n[s,PnJ=P^B(Vm"hI*d_2iV>&QG
|
||||
A1L,sl+Jr'9jI.hF^_'+Un+a6X2[+AZXhlJ5A;"gHJGb8b_ttX?huLrqh&m8JZ$fQ[WXbLQ7RQ9Qk:n3
|
||||
ZWKJF;M=t_LMO"Y:Z617Il9o'*9jhmfJ-PihO3T4Q@5=Z4-a2$c:!^tEY$0mn9Utol\Jm'6nO<h7K2pI
|
||||
bWOaXq:9c"I`WUW\Yci7$M0aZIFdQK.+\59>gI.RnlK2a]6XFeei,`:>3ie`RsDU.8D,Bm__[-VNj*4U
|
||||
UJ/PafjU/64gYIiJipBu<]_9In%?,IpL22<>.OK!]4bk$*\%$0bhMmQ+3K]4];:s=(9o<jE+YbK(b5"l
|
||||
dRnCPnTB">3`1uffTWp>QY_"RHZAhOlhP+ir(r)Nn0u,Y3p=!tV)D3@rdMQR+/^XOs34%`3d)qQDs@1Z
|
||||
>q-'0ahjp.L/`i@GTWnuIb?_d.`,g;EmHkZB=IL:&+CL8Fh#,SlmnX=G=b6@FgtC-PcIHr5a%"bcZm<j
|
||||
pV8Bbj7i"E;UKGf/eud?j\jKPII@@oVX.4.7P't\'0YCm%i]X$2G29S;UN/,V&lTAYB>&g(c)Mupmb]_
|
||||
2/rL(h=_6C,L#eVs2($94Q?0\]Z`XQ0S0Z@2._O^5b4JiGWZD\jd+WC_`_agB\nnLr?+<&MOF)!"8?SC
|
||||
=)O8EaS]GEn[goYbKYIO[dJ4u8ql1/ZtPd1Nq!q2l8oLPl1oOn["9@jJMh-X@C6'B.)iu]p#QWao&.^h
|
||||
=TsM,HrX>7:eD4hRh_<c=@^qG@iq+hEcu!>QLImWC\AKV4D39H['.?g:s!Sq]GBWZ#l_\u>R.7"^kM/,
|
||||
Uh'4<%_c$<*`#X*VB(U/>jG7Q:1$8E()bEiXoFR149[3o]S=Xa7OA#aej8Qm!`/Y!$ubQb?;.]Jn/^9L
|
||||
c%.e=-XCs+]LdH,4-d5EWmpt9<EL?uMBolJ=NT.oEtU`akBpI>HYuMpQ0`@9Lk[/8Z6=KfEJB5nCLtmP
|
||||
%c+j_6<-fD9n(q.^[:[6B$CNM;jPm>[S-FP*^[IX)OF`JUFZ5K<a#sS9F7JH:,^`PUC1oT46q<g)10%)
|
||||
A`qag*STSD3qoe:8A9LEL!o0T6H"uYTlF+7-bY@M09tPohG/OAB"SR617Ts!GMim@l8_6S)GMT)X_^m?
|
||||
*g^lA[%.-N0,M&LY"1XB0083n["1LBk0a9G0"bZEh]5`-l.rMnb#MFQ8RT3g>doeI>>cl#93i3nB9.H9
|
||||
qL)$#H2H^"5tk<[K`8W[OaIk]hi?Yna$,(#^Z0S<=!aXAYl`F5&c]-<PlC]trX3@Hp?l3#WR,P\m<BCT
|
||||
4o^Q&q1#[Oe3Dj"^T.?GhXm.730Rf0b[YNClD,=7k1;NsDW&S,Lto8pNqSCJ)pIg<GK9?>^3F>Rn;Cm`
|
||||
;`L.qs)t%F^M69UapZ+c50mG<q7?+pI7@'Y1=uA)kp$d@IW"agg,:oV,T(8ak.dDPNK'"u$Fi&)GP]V/
|
||||
6r-o]PGX3KrG9$8<YY;DRjpOBY@VNWl!=9MgSe=N!W)Y/Mj+EUEJ:8PAWH$DdP#[9^QItB;m+3RC1#5q
|
||||
"m8%kN/UeeZYHN8B[j&EB%6["3ooY!1/]R=%WVp=`&L'22^dIZ%#eR<d@2pmkXd]'A7WHnG<!3EM;ED`
|
||||
Or4pG4j61bOJRgB`3G>J>+s4Nfq6(lq4*kl`a\HC\]T)[/B]a][T\66hSPhT=8js4H@,JE<12^j^RFA1
|
||||
:lc7K>J7enUKn,MA%Ye\q1MthTS$:i]',\-rISO!ZLR!oO.s&l?Rj:()&Nr5MB4,%DL*hRp+[\mZN/r2
|
||||
0`7&\]Br<?U,0U6N(Z$r9uOQk>Yj[)g'gHMC,KgghmK/^:\"]/)X/B`%pc\`Ns*IM<-jfKek."S."]U?
|
||||
UQ*pY$D`Jt:iS24=c?r`<a$bia35pNCKnl+@[u<`%B`$<gOdCSX0=;g\<;Km]$^5=@`'(MN[Cq1@q@CC
|
||||
\ru)ppm&t?6U']J4?[t\-TKtWWU"7=hR_nU?]p=71t6^R4mJ5<0nN(o68!^g6oZp=.\>efJ`"5Sgh<3.
|
||||
j5R/qmYEfnPTkhOhL'LCe,Sd_'T)X[F`DPf/kj2r8?q-J$Y,BtA6%Xb;kTB2hnDDVc3S?-g?&itNBY#;
|
||||
D>AN71%(G\b%4]EM#>Ra4Opll<<G3<`DS'UMFC8sEW&6!C?+TNbSY0Kk`Sbn(DJ4J@PgEBGI7N3p7hBZ
|
||||
*fP`te1uubR[kF)\2FmG*&;*:*2rP)\$mumqni4sRkg\IK\tY?l/UAK#?Sp-09eF)h@?[&H-IbE^>mA+
|
||||
k2p7Bof!@4^):DPNoPEbhE*o)IrCr@=u,6>9TBQjZolso8=L.c:VYSY/>h[;-c`sc)'^p(QM%>`h>#Xl
|
||||
`]u14RWGi/CMqoQ_:kj[Q4i/C\Af^\+'T87=AR%t6@UW>Ol@l/CmG_t6<pAohN81Wp\*b.FSL<['-j?3
|
||||
YE7Jsmk-4ojLaCjIUPbIqMuG(9<+nna?:&P18WDi)WdWX?M&X-,L?)S?W,T\afVUd@Q@XGkW'aPP$f55
|
||||
I8I,_f=<ahocU_>KGgR8T5\`RHu5#V(M>:Hgq9i?(RlK*\fG(@#6)@,S55Xm57L7!QT7=="k7pS$SB48
|
||||
^1_2qjsL0roFOCY7P!O=c,;d$ZHU.ue;/f"i^IAj@Io@L)R;XKGN8U>i6>RBc:;A[:*0.I.(Rs`631C*
|
||||
9f%]61p07SAg35B3/2(:UWMS/'6C.ZhOX0`j9e#s_7>@BII]*gEhjKoW.,9uXChno1o55h[kq#mj3'E?
|
||||
"!-2*h3(urL1J&;`[RsTMT66"TbWFUnKstM1I0:Wd`c?KJg]_hVLtc?+YY+>]5*q!nD;ZcgXlbT_"9fU
|
||||
CFmKno=Cg0db.tW1fB?Rd8N*3JM'q8,h&[H0):ZA2=\+@:>?L+>gcJEN.qUs]J,)'XFnbR!(r"2T'Wjj
|
||||
l6&\]A:sPVg1"-CF^D3*7chM]G08K))e>)%j1UG8[#g"op(4$X0V"Hd^mc*4jdTiWo]`7]OP'!/05LgB
|
||||
e3ch%kn6t)'W0Vh9.(_5Vi_K9qtkKHaA*X9/)dhsI<)-b^)*r"YH4Uk4PCf3<Lpm=_p4(Na/k;oG>\m1
|
||||
7J;E6gcE^4r%1Yc8Jc-/7?J^!,#-+")p;'UPs+GmEn@X$3K8*6SOk2o&<R4^1js4;.cP?$esp9nj=WVA
|
||||
NpJm9;D4rX"^lD6<1th<6FZ%[M';+OFQ<g*8I@=CZ!QohL?oVGq`%o@emE]CQ$2:fZI=XfDqnS>Tk^<I
|
||||
"k.$1g$`.VEP-ei$_MV9mi?cTr\!fgdoD\5#>\D"2jo.phL;nAeGaI#o29^?eoUbCe)F**ruH$V;fAmr
|
||||
n]b#:(mBG8<r6V[72YFlOj$&iG_RKr,o-DTh1u1$9=G:M[64<#0Q&Jg%q\AlIg.A==GA*PokdX<`;LLo
|
||||
.rqr`\@;M$1F-feX0E8#q=A63Utujc5.I(nBc%)Nmhid,!h@/0F5c`'J+"EDHiNqPChJ4jk`MF.N2oIF
|
||||
dbfZr&r(\KI+iSqn;^;*b+pkA@d?n:^b,7c`5Qq,`0iQhj7t54'I-rNh\4ll/<'(V&Q[a]NZL9*)V*+F
|
||||
54SG8j[G=kI%L*$^#RS:`_6N$>MReNi5&)mL@)'Y_t_mo`K1MAP`-c*"O;o`e"PF+?35R7T%7ahM_nrl
|
||||
k?GJAG=,bbZj(H^^:G8i)K;`bcXLBmI@Bk9hMYp[9;'N:&]-=I3*X-SN`jK]Mgap0Zdnfl0+k$%_)#0*
|
||||
^I%WF*Ko:%`%UbkM%4b@h[:pMCLHU:guB*/FO>[jXNDEjRd(Kul'm%uYr<,4=PN``p$.o>Da+'P.rV8Q
|
||||
T[gf"%A&Ac*ER$M0+J"E7O=&%Ytp2l.ahcD:Vsh2:kI&f[prB4e1\X$`S[`h@dh9;D9`gXHr/\ll2;>0
|
||||
]s$"<P=UEIJIZL#c07r/9'%9W(:Q8qr,#KO*nC(L*:MN?IdfHt*']g%7iXa<g)_4uGaCf<5Hs6lSf[B0
|
||||
*ff^d=ol7=Ggi^[SN#GYj#_WqcLIGc#K6.mHj5Ctj1Ds9jS0c[.8lF<FcPqfic4YcE'-W(XdQd8E]iT*
|
||||
31a)iN[G"$j%8.GnW'1#mHG?*5Nm%ta,e(Y3ld<Jo$04DBMVEq*qXj`a1oX*:2?K/*Qdi"pfPZ.^%5co
|
||||
*8`i[:3&JE7;lUT4FcT8chAK@k?"57fd#Zop@iMg7h`!BB@l-f?8+M^5Bt=%Nd$L0a$9?g*074SobdKG
|
||||
\!\;\Yp##A0P?*ah.58Lo\!hrZbai3L+Ff__g<V=S2Z=s:"7MI&c<:555i/S7D\>p&._Ms')DU_!I@?9
|
||||
'm`#Y!=:&UFTk(GA8/4<?3X(!=':('HfW9F90DDRdk<H(jmP?Si)cIAlqpUKVXK/sME0J)YUXJXL42L[
|
||||
_OcImDfRM==fBW[D!SopAdre<Ecd:+31J)Y^GO(M=3TZ-M=QF/eL@GC'Pg2QDeF79d2":FkeA*ZWlKHV
|
||||
Vn[pm[1K#M4`V5V!pl]@07BXDn"Biul;V5RS:V:JK&TGhAb#cA<YgpZ6RY<mmsaXr[UhV\Y7\6W(j7,V
|
||||
Yj^+U(%P"uq>)E[/Dc$Lf"p%U6,7(NBDW%K97%gIhW;17<-)A_76=D$Hf8JV/Q)1tm<N5A]s(@>1$9Kn
|
||||
KZt\NTZ'a"Z%d6`C*Qp3-80Nr<0ecWU9E%g>LFI*`2GZjQ-<kKk@?js/Dc\&#d'354AKDga*84CmLrsf
|
||||
$]OU%;I/<m/7jcY1*<(,2n.,@NPKR(okSqr3A&T-^!#ppF&@mWk@kd*XdDC)6Ru#?(a#A[+4'<Af=8)*
|
||||
9#p`)D=;^)GJ*Sp&Jt/IP!dL>R!8mA2gnAbr*(o8)'!GAd!$eVA?Ui>R>K<O5>aWSk=MI!^oFZjm$uJp
|
||||
a)eq&"n!A.e_&V9T$._Mf;@a2M%9dbr,4\83F]L>2&UnWOFD$\fu<2CMJU:RMBX^]=jNIZV=&G_lul<<
|
||||
GNo<7?@,N;:.B:BhAgZ`8X1k6KraN+8(K]Gm,?m5!=U9f(GZ[;B^8n8!I*gk94.DeaC^GZ=rI7"i2RD1
|
||||
n/g"$PNFjZ'Eo'`5RO+YiXC"uB9OlV@d(i>b1Jl2%3rkh#tg;:MnCeq7So.p1a==hrJ3Z+don^mqIQP[
|
||||
+7IIgo'o`MKR0F+"N?',NiHG]-J?dn6%A>jV&&BmRpq<"@V(%ML>usA[F%XG?YZ2Jn*P,?]0CnAlHr=5
|
||||
A>;p,2,`3b!tIu`Ki%)q2@%T?Oh<@t'%/N;C2TFG`KpY4eiA4/)bKK-$VFckIAJ",p/6CX14.u[BA)!&
|
||||
e+LoXfcKUi]^4g^rA._B,!,Zk`c50oqP>`!^%\Bk'?s(E.[lKO(O[A>QXtp7\)[5rC_$*sZHQVl23DRU
|
||||
!-Qlu6MCbA@0c%`ZB=e9i3/d^lmqa"lT6NOY1`&@.qb$2)/lrgI(RLi[*7mtZ@ulP.+Z\<<2=]t+1KI9
|
||||
A*0Kf3R=[Xq?oU*MnZ!3m\8/u4Z26GN%]!i^ts'5!Z+TiinUmJXNl"3W-%$b!-!!P!%>C\J"cENFq:05
|
||||
m>YdJV,^\#0B?n!lWI2=TVVaTgsQcA9-A24#8N*PETjrXgH1]I!)32j2SUS3TWm%T<QATCd!HUL4u8BE
|
||||
1BZp$WtkWu4h)P9okD_hIod^=7a)`]/_S2BElK]H*Y^'%89+.%*75P@%]8$1a`sK@4prK*B$G9d/tHf*
|
||||
/EHfOT5r%'4Bm3(Ts]?(IfkLDXMeR-U.\jlkpolj(7<?1E"*.5T"r=oNH25tqAk2ZG2VjRZ$!%dmAspL
|
||||
ijmWdn^WV"e;4M]JVm'd%*;/;K7lOm-EQlgOL"]g%jpDe0;WZ>_Ro@28$a@c8CS0A.J[qE`\t#$kH?%d
|
||||
P(W$`1rK_'c+:+KnaYZ^E>56+*:?$M[L,-r\.n/$3-PV/I7&mI9IN@.J:8pBediVT^05sH%IrOknHNQD
|
||||
6nBBKNuht?ga.oZ/ZFmS+VfP].8cX3DL>$p^lXG]W,IcT[:ju'bu@YiEXL;5o-2>d<+Ij2De0q1Q\raP
|
||||
/?)IS3!W?dOE2Z^$`.0#`UY>ZbPZ1kn0J7]S!5JLQ]d6-E9'p%31"OUF5KlXQ'5H&ZDs(/C!E:%D?.&D
|
||||
=)4(;L/_\0RNiP_=<=,N'Al)*hXaA]Y,DWu1*8<KU2Z'qjej.2T$2<mcTa-ZO7O"qbj1DpbF(mK'H6(r
|
||||
*=%F$\"otQbVM(oak3n'Fo1gsk03doft&H:^b^.39W9=+)$^+$YiIb<FrWL]W%cLrp`=qb9-!TQoC'3:
|
||||
ci9LGY0",J0.3lAiPj9P(S9X&Ac6DnV1[1-7INXP+^P2'gUQWsb?93cS^q\Xe!a<F(Q)KPSul1YgLO3_
|
||||
Z;\mJZ!PtsHa-T:ZP$,aY_'E6LD%*MI[/5Sop<,GVF7i0)6Ri(AO%%X]m7a5[VB*&poG`L,F<$8Gf`R4
|
||||
L))7)(['M8fudUQdX0)I7<FsCQ'q7Ss!@N2DnV%USQ<3aXi_Q]K_K;\@D1N%gf!#tr+-OIML+K.n_I*j
|
||||
Ru?aEj#`OR=j[W.FnZqD]1;lgf3@OKX*&gTA4C7&?+p"GI2JaupVcr[HI.a5muO+ir"jSgcU;A*r,!;:
|
||||
)Z4HN?M*qPRl20g,HOs;*hEdG%D!h-=Ad_ODAeaIE3ba,[9dOG)P(AF)b$g/l76,o]DK65^Qc(e,k].F
|
||||
TbEkfQ7>M*[==#,Tj*E':qB+QS4A(ao&\cbiEM4hObN+k056YpnQMQHB>ckTJ\ef[s,Q.8^UqC]97$Cs
|
||||
]'(gIs1eR!oa].hO!H.lOtt>kIlk)h.UkUANoTqbndaPAhm^A4_j?Qm2u-8eYj%)Mbt@n^n.'9?ruE>h
|
||||
5U&*]I)l!Qr8'NdpIpJsm$LP/G5`J*O3F<]iI8T39jI)Wr''W#,NIZg(h(kBZeK3t>=bDsPFsjhrGXo<
|
||||
p`AsSId\<g7[WFQM@o!X+!Ya;"(ORcL(V^9M&/YRNb`#ds,OdJm*@bBoGFui(U_?1_27o%?T<Q'^VEQ`
|
||||
G`EC%p]BPU!Qpt30E`7shlm_r/@I#RZPqh.$s$F2hH)4QO-7a!6Yd9lq8-EEIE`^-@B,?0:ugB=^h5di
|
||||
Zi1+<MWHdS$6tKJ\>c*1ZY'AR$RmW0rJ]E3*&6'3mHZQCD@;;%okMaD$s5*@8AX&,Q@+R7opA%C`\kM<
|
||||
SFMJ>^t"%/2b]eklmLTa56hE&Y]'K"X6O"P0'%W$GqS0>66=33GD5'F8DX`:S#Eq.2+MmL4^+%Qc2Ph`
|
||||
^UtDg$CYe5S-G#,paU[MJLYT1"?pn>rXp:ofmeiUS55MR(<h]!;$e-W(a]iE]9\M7lcj6g)fbDY!1Jp9
|
||||
X*1o%:Slr%7&3<"ZW2FXrJSuECX53,QTTh6^XnDP#6ejg;FEKV>Z`dg3A[,olJqUVhjR!Z\C6IZ\j+YR
|
||||
`n-u.WKf8'28bmQ1-b)mERaW?)&J/J(Hrg$[/-@RAm!Boj-Xi]#gSC8B0P?Kn+%?'RRPNsden/]\"o-u
|
||||
]I(lX>&3sSB?"^&2)O@D<srVF1T22+k>sIb03.Bf*B-Cfk=UU.NY/]1`*iJ&)0Yhf.?@;V(\5YG@=d'q
|
||||
jIKsF_R&hi?`d6F_Y0k;:<?PlZ$YFU*F[N:Y"N]UpuVM972amg$&gAnhgaX">YU5;9iSt%mn?V,H8.k#
|
||||
GpG3B+&Z<Q]qdV;r8K4ne.l98<nSSQa]_tGQ%PNHZS9N'`%Q*l)"6RDP-1l\EagEA7`]ZC2iu$[YL_ej
|
||||
A.f?a[lnD,:p8M3Uqio#i#csgdXP)i%[\bc285RKWfo99:.W?N7G]&;XlSiWcNphV11ti1:2>@WZF^lC
|
||||
2e<?`L>#&7<Bl)1CQ.uP5P8F-ZJJDjpP#l1-N8198I[f^NoTrM]1)8i]?f0C]XsP4s6#HUa26_bQ`r^S
|
||||
NrAd'EhDsLp[.GC3>OVm(\7k!Q<E90Y;HoA#Ng1%$fci:TpKLKZjVt$,+=Yt];!0F@>h7g&GM*:i-%Ph
|
||||
$DB+@\6G[^Dlk@^j-L2kK?ehK`JLe8R#W3oFl%@aI)a'^:@IuNHlG"6+Ln,j?<KZ`7@Np-E<h6eDB3s9
|
||||
o\$A^>@:>mj'0`6@&%Z9ZmNLg^J)uJQHf0pX-t4)aAJj(rgZ2@r`960/g_P53ES4P/XCa\)oE75CpDc]
|
||||
KPN(*)ZdpN1o'OjT7p%n._[YMQ56FhVJ+o1fRNj_c4#2@p0.XqC1_S#H@ukuJ!9`u(`u8\E'5dt\1o"+
|
||||
bcqJC^md3UmJ"14?qJgZApuIm:J^6nA+-OMbUafl%hp6�Xq<Tel5q$9I1J!aff3@uDFCKe!6km+u*D
|
||||
Y8@f?pY5K@G"5?[*'8o,e4.Z_U4GB]%LI'>#;q_t%Q\[m<T\>S/F$s%B!oN2Lfpeal/qeqe,"l-,25i)
|
||||
2q&E\?UXs4oLO&)rq[U!`lAZ9Nq/WKrZ^L7,O(]DZ1eVD@5eS.)&eJ+ju$0-8b%8_N&un/$A3'PQ]4l$
|
||||
HEThMq,]Z<bLn+5BMm%\]4+<ZKO&en*;c7A]^PXT!@9I1%`Y,nOemm(pR1%k]^IPrf1BuFm@s>?>+rnT
|
||||
N.sAp#ibb.`KS=GUIJ\!,+fn4P]?9pCGk])ZYgt[:MdEhjV,K0JI:j=?eAi'3`B'SjHqgS`J%I@6C1Y@
|
||||
%4Ci\BDdK"gL.A,BW<I@$<L]uh%%qk^rf25=Gb1uNgP0rn73-ZdR3L5"<)ah+gb4?Ga!7.@O/^bWah,?
|
||||
j8A:Z/sD(K%`T"70G7rjCQ<tWU#+K8?"qAjNo^G.UmRm71^R4@,1RH1nhP7u&r:a/KI=H?Ga`pZG9n%#
|
||||
*r"U_Sa-'=hq<D.@d6JC/5pb>s';rAVFgF2b]U_RC#/rC[co_V(`ge1ieud,(M[+!![AjPI`^,P0tfs.
|
||||
\"@:@B.a_CS*Lj)7%05q<EIZGhoHCgnEk1.@[6[T([UKR1J3Fn6rW?_?>qi7';]f4o#LsZra+2kS?#'f
|
||||
f^le:&m$_>Rks`&ofimULYeqO;,/b"F-7kd2nbF42hAn4^?_\E5of.=9'XVrHsrO==JUH(:HZ-R9QjTW
|
||||
6LmG%h#l>Wc0qD9il-6hd[lqP"L&4\I3<]+_"hj-cG1F9?VGKspOK$9oF-l`Ch?-=^MVcXW3gq%j2-Kd
|
||||
F*$8RXe3_)'d_Gt7#$H&nqJD3fB(Z!hQ`f#C&b8J*&L1ON&@(jV93-l52432Lp&P_A.+EM3rM3m)fMo6
|
||||
+m.;!Y!IoJ4HkLc#q,l@_k_DZ5/7_`34;#G<@NK3@2sT&rsJ]\S#OIPMq^a@8:qt4n..N/f%M%R:SSL9
|
||||
h?^uG<6C*cX,3,q;lW/3,Z7n-Wu/ZHNoF);ZqbYMIQ8hr[$(Z)3!/<Q6!A6@%qNaIJ,KT#ks)O1`Morq
|
||||
+3+1jf3EJ<?r:7+ah%b)Q?N;XqOtR+AJ6k3#kGgpO+JufHe@&,>%g/bbMnEn=c#(YBL/5tK9U3rYhL0S
|
||||
1tNBWh5^,1hf34n4Muf9[rjhQ2''WAoibk"#.625RPa>Finu&k::,#4S:e-iXcp+IfeNfoHT?2uhSZ_1
|
||||
cNjl\A%og3rHaqtT@"E!rkY^q;6LQb\0>MKJ)a"WqP&-KX?8IcJtaYl!?Z]=%mAQR8Q+st9KB]h7s*c'
|
||||
Ed'64Y2o!hhHS-.hIU`<bBb!X=kDY>hrX98&n#Y.:6]dC0r[ach1`I&/pDMA#UXbtE^[V%Y%*g14&K$U
|
||||
mOf8[OPF-2]^0Z_D9a6bY4_;NZIc[`0ZRAnBk7>[akbLHEF%ubXXM?J-,-52Fl[Sp1$.igZstP/Q_^jh
|
||||
>?9p+\\#dsjPmSG;k1n[Ef0t#gSH[nZcM9O\\"'odnK0jLJ[D&pkQ?DAA>\1?/%kWEgLo5>ja4t&fnNp
|
||||
2Ku'`Snj$"WZ4=8jR/JdR;-<t@K.WPK885f_kk&gh)._2b$F1DQb/=XF,^Ur8mLd>.+%)Def(4:h1M$)
|
||||
bM><d7[r2@gDi_hm`tLbo=)>)-+$:U;=<l74)*O!WO12)GB]lhXLg_a/pA>J'\LAbq2&C7q"*t,jY!_J
|
||||
]A25%[&)Qc?E[#iDL!i_GB`5AJm>dQr&o:F&'fX2jq2X#=l57p[8<;1En=rWEd&&JAM/0KoCpRWHPO/6
|
||||
4K<\NLIY?hKnm)7-2p\bHiNoUrI%Yg`0lL0;%R;ZGs`UAiCc:QK(o_3.pj,@&&7UZ*oD=ba,>Ks&FMdf
|
||||
j/%Ig!`Y\LG"EH@](g\B)(qG:bcFa8AEo([Z`'sZP:,s9!"H=f`^MXH-X*"RJuIDRe-Et2/Y!#DAcn/o
|
||||
90#Oc<<=DuDYt,oep-Z5\W))]1<s&dJj,1nC)7ncn^dLRg,A/fh"j;_SuOdSO^^-141JK']U0WgSmIA-
|
||||
><^qMoTkOfht0D/[-6IX>bV!Ih2B[GX0i6WQ?;*XI84C"HXVh_6Q[nSEoFZ@G/f[P*I8A8hT+%tFlPmY
|
||||
Q#AUqMF(A#,OUc/^V5X"d7!<mFcm7U;S:U_$0!I(5pY98ni,UJGL:mu3I^j]b4)p@otWP$,6hU-37Tc^
|
||||
I"^r(R(b/RQ;*Bkml$7iRhnic,CZ$uorRn"O!<.V1N.E77u?c'lE>Slcgc"7n,/D2r5FdZHol??.ks(%
|
||||
;/AN_i,SF#W*b?n(-3ZcL?0&_CqRl`pW@\!?\Rr,`nS&D2+%+V@taF/"qE*'n\4$VlK'kA0nj88_XcLa
|
||||
duS8$ccO4D[qH28Mr#5W7f)/@3-EK*`=Q\;[g_.G,1u>7.S_k(2T.di]bb>Fo\BSR7l\7!P4/+]G"9C\
|
||||
Xc6VBaHJX]ZfF`s+Y^or^0;^_:s6o!h^WXl+hioL>G1KAJnA78KBN)FC=\Q=?72mY`H;L/G8jYH#71tj
|
||||
o\BRGXqf@'-//ubB?2srs"3mPQ`HC!KY7`>[?Gmp,0>baQ;jY_4(^>lWJkJrA@b8`(29)#pfm,Ve+o#O
|
||||
\%ip<qM61jYB^(Qle(Fde_9]QPn+s3O$uXCZ-$N+2'8PB!\"mR(r"c"-!V?RoXUT,\[t<&L?RIcbPle_
|
||||
DUEsV;VkPrT'"iiV[L`@TWigQ^uRmO9;O2mJ`mQ:i\<(&3Sph.b!nepeapJ7P#a6eSFh^3G@+NDoSut2
|
||||
Q+gJ#KDTP=C5FZ6Or0#f3k'K6ggV("1[WE%P>UL'rPIO*"Ua-=+sq#BBSJ(!lA'`mLqT%LU6sNn8<Kn5
|
||||
C9<iIUBjZi+aDclI0`)c';D?@8a8Km_"&Yee*nO!Ro[IqVL.DZL==a#@nD*nk&T%^`d4b2`eVut!5q&H
|
||||
c2?/V.O9^P?@<gC4]Y@g1&aq_XB`SfDt+)fI?jM>cIql8q=#fk)6lJqdaq]m,Vrsaj)*V-!#j:SB^I$(
|
||||
"?qJds+mFtXhfd3@BBK&\\_Q6er.nn(WEGLN'gt%HN^rA`V7oO+2?$ta.a"8#P=L1=R/,3O7qkR,'U@=
|
||||
!,qEW@[<Gj."&KhS>!kS1S31Z"`B^=<N6neC]=$?5HVFQ/KY%gAM5,,Yb]cVX%o2>61Q0%?4g?+Rd5gl
|
||||
1Pk5OZ$ZiIm@naJT#$24?Ak#iF%hD1_sQmM3mf:!P#8knC\SHmb2o@ur!iTLAu&7hC;*FOlSX/HUZ&9P
|
||||
q;_"oB>gW<EfK1CX5hSk5N`.CLS^n.i<s#M@h5SDP*3G.-"e+Aq[6'b1A]Va0&qVGgo]fsI(+o0%H$O\
|
||||
Pp9r3CiN%Y)%i9sgo\<jP2*0>P'6PT_fWFnh*R4YX`!Z"mag%Dc&[+5jK?31E`]r9gf4k2=_g#q=Riqg
|
||||
<3Aga!4c=BA?t8Kj.K]GQLu,.AqBdn,9:E/nOq![Z9L#"/\YDKgo\D@M@mSJ-d_m9[8uf<@rL,bjgts?
|
||||
9(#a;iKa?2j<$gJ<qTR82_a<1FmF?&o!R7n[hb<DM)5]6$Zqr-Gslb]VT1@=kB1APP`aa4gpP=2EHK:X
|
||||
]Dma&FkN`og<sX/DLqA@].I`O;FK3XR2JhGB7]nhrg%=4`TsrWG8Xh9j=XqY?1-:C]d)U@:rqJXILo)%
|
||||
3rTbI'[cO@392\:Wa5$nQdT/Zg\mGM0dan+n'<sh!"'d[BVea,;KB7WNY5Weo/brMF+"8'gHqJbIl?HX
|
||||
58'rBDZ)<dp"]CB1p#U*oB=dbr#`J3ps8^%D^0BT$U"7P396=<m`u'nNhah?N?c0kU/uR<Ks0\4]ltI*
|
||||
h^j,X9"#GrR-)M99!%l)\FK9^SbKM-6Lc!,;%70OPBj8I9:;]E7^qU'_6Ht6]GrYlnmH!Xc<ed"RA"mN
|
||||
UbM3L\V_-XD^[@:h(k!O.Z/t#X?=N!ARdi8p;.2r.cXTMPDsRf3=7%1^I!m4*W*4D#`E/*H04LZ911)]
|
||||
qPt](5o_'QI62MP"@du;d@9#^So0f_REX@.8EMb>,=F2,07uD/EF04S0B\W*U3"q#c!JW2cI*WW(0M@L
|
||||
$aI0ppG2(TdKC-M_:/iQ]cH,[7f3nLDY7>l-KX#V(?/32n&YW1Lo*UjKgbBQ>#>:G:h)]Sc;gY6ZGOdk
|
||||
N;">DeW/^R:]AAEWT@FJGF..X$62pA#Z4+^q1YodqYae3iNDjlgr-H]Q(*#=4k's@LEh/SYs=LV)[g.m
|
||||
N2TgD^=p`ugf^1WUN2Wnb55'PGC@h1HF@hunj[ED%@./SihdTch7P[VO^tXZ<8N]l54+;f2a7T."$6n,
|
||||
'Zh-Q:L:,\AlL7kml#]NDWNXG2Igls5lmCU1H6f/)t=p#1"%V!6mHWkg3XcW6S551g4!hZ:l:`42O`GT
|
||||
NphrrHD8sA8g.Ko\aO&gCLoBA":orMZ,M"cMI\P`k'A*0Q\$V$]@/V+k0+cJRAf_d]ZJNi9*SU'f#&I_
|
||||
`V]*:Vg#:V[IUslTr9+/OM=aGmC5db+O<%/>T3o*SUMZ>Lsj*p8;o=f_rjnCWq[4uN;$(-Tr@P-6=u@X
|
||||
)qfHPo5]83VGfU>%pa/G#VjuCWT_abQ/%$0oYRjWe1?]eP[$1(0"gHJGf5.P9(Q@Z]VEkor8KK.GF&)G
|
||||
M*lSZCWI4O,]CgFO:JsTaeb9;[-3g2Hst%Z6H\6#edl_gN\.h`E(qgKHdR7c`qUk*&(:>mQ7N&rgHj(o
|
||||
quSOk+E-+<D"YHMCXnF2`N^mK]8J=(,B$*PL`;5-HnM>)P"5/f7]@!gD4J_.FZ,=b#X(d=8fhj*H,Hg>
|
||||
10ZjpY4i,@oMna[4j'@Qrm:C*ES^+R9R,>I`KuI'PbBC@_q^9t,$)e0JZRLgOh2\L,Nul8'-fS=rX)NN
|
||||
H>cVW?_*-_<f\Z"]YBS9)]*msQ+&CDQO,A`nQs1Sl`5G4C9U3g]YBIe)ZQX'WqQ]<oMna[4V@p:qNBi?
|
||||
"-H"qE:Sh`77m'1PsliMp1&h<7kpD8J!/iTa6?nk_BOg]Mh7W[*%AJ*7k;IaPXWDt=US<Pp+5PnncinO
|
||||
81KNa+D"CUm,OKFCqm5Zk5P9?<YgD6]#MmeJ.22#3K-mCr%5=W*D8d9"[k)C_^jJ!RlG+NTs"bpef9!R
|
||||
9NhP9j.@4R7;WbqW1+For-I.:]%>R:1[9"#/i"s5,PuUo3^=*#n$ij>`bo%8]X)+gA0l?5$Y@$YZT+q>
|
||||
ZHoeuri(%MNAHDPn(D;:45=`<MGIg?L7E.W\F\ZJ!B"M\I.rY2cLq)sefFS2UIs@)8+\<oieTN>eFFGW
|
||||
m<Q_BXQ$tjVIi]+Dt?<Le_57<*qAdQl7HUeij2!rmkaQt$Pf<\eF`*q5Kr>+/d6KPb;!lRIfhXTWG21Y
|
||||
@f-Q.lDJGq7V>bB4D/MFpQ`E=^^:PH`JO]VZt?5kA@D(rRE#+r/,9YYZ(!2DP&cZ-_h/b0QS`;=n*RDB
|
||||
r2\=ohBV;Rn#spqFnV!s,=X_]SR$>6<iVh@?o4i237qTcGN`Y6@heMG]sC?96]qR!6W`q$+*ZM@EeU4J
|
||||
+*r/gbUP69-oEQ.r3<?T6iX5UDY(,:NG7XL_@6I/j(?CnEdBfZVKMnb@i#$VY('K0'u;oF<kq$ZNO&[e
|
||||
FdoVA14iPWEOF7=0et2VPb;6NJe51_rat#0hX^:i&V62^!qFA)%FYMH]^KrWfNMOC\)$)#[>p@q_U.6b
|
||||
`(7(ib)82+e@LibM-7N]pGY3-K]Q,KpJp<YO":mc3ZhKK!\6nHSlT8mAoZ-bKjSsC`bbC.n^a:(1#C23
|
||||
]^9o&$=!7<SSmoW))9+%-,QFgR3f_FLm]P5OKq7W7[=>X1:\qio["70)j)EJeA$6?]=lWU(:tQ5B.(tE
|
||||
Z:#O0R]Oi.@<3k7*.f^R:]Blf%%1.7/#pRM\L*W:^8[`@T=5JF:.4Ls(?E`A;mJ&*M%R+!AkKM4F]%U=
|
||||
YAgeUpK3[=#g=o)de2Cr/Hg\oQAB2Rs&i.fg>#77lnFQ_g`g6^^B8[gJ,UJcoDbVapn.$0hg1/9WsR7K
|
||||
qfD6^[^9e#X8mERgN=-5F[YgR<CDKYZ<_F?])>i8Q_Leb:18RT$F`8CQTGGAO2ug-X`D(`bJWFKm>)8H
|
||||
*jP^5pUA8&nee!#R'\>N("[SXc3Su(n^7'r`6g77PR8\M+kd^.ZB]W_9CmV#*TAJ:-ql)P^D,f%hBdZD
|
||||
Is"&,H&lq/3Vp?fTuKU4(#:OuMYop^mlU75>"-Sob$0WF_j&Y>&`IS"qr!2G<j/IT.'j9Iak<uf.MUGu
|
||||
DlCfpeMmq@(\:R-4WpIPjJT6)DJHRhgCh9&X;)H<X\bSf<BK]@d3Bh.pZkU_+&?H:\R0UI:6BM5j`NqI
|
||||
]eU[WAOV`Y**_V3WGD6h.Vj$.rmGf+=*.ZVYj/L&:;3(0/E?Q_deR3u<AUElq0[&DiQ(kA?4X(<B'8JK
|
||||
k<HbQ=i:_Q_>/uK"j7clhdX_@6bQbUW8bn?Yk_<L;L]NM[M;h--MkTid2]&!pCQl<JW)#>iN?K'cgmoO
|
||||
m8UMt28RFmMiNO8U'I0RZsP.&bWY:Se>Z`#m4N$&ImkCE+]hr\A]-W4$QGiak1ZcDTk#!'7U?+"KpcUP
|
||||
f'--=[tef9'!#?-(V\mDS"l;I"@Y<AHmLdCRHCYk.8D32+hb#KO0.#gfuPUeVP59aiMam;UtuI-$]$XH
|
||||
)L8I+f5GeAaaGL7D2S*'PKag[8.7p6*8o&@e9IG;=de$acI<d,bk@Uil]ch?>1F@9lJns[p"rhuS`FRo
|
||||
i^Q&o1<>7K-1,fP$hM)3guZHMml.8<pE`aIDUQmB`=(?oXY.noT+G%(m'WIIqb<OLA".:EM!"i)'u&t]
|
||||
rQWKRo+h8Yc/f)aaBV*Z97G(3P/2'WiZuLb_e+6:,j?Z2q;)=ZE,DB`QDl$RM!Q:f:TWD=T&n3V/cm3D
|
||||
H_"3%0pkY-"F=^TmIY:\.cf#M/Sj);+^]KJPu*0rX5aGpAZ(cd_:GHQ15Q_.#uL>\e<i0!?eVT'$F^IV
|
||||
eoRpoH>0^WTA$EBR[H=rRW.EU0'80sP+32!YTKZeK5HV`bppE0K8Js%O6?s3<c[-e^sS3$>kUC$ka])/
|
||||
-A;,I0ac@\p/2pC!^i)3V@db/c5DkA,9Jc?`'\c)cO@61c@a?P!tP!?52uOXX?2Mt=<>farDJq%:r"['
|
||||
6]k@9&I*tN(67ooK7Jn>lj7a4o]/09jHi>/$E',>1dYX=ir#.6E`hf'$t'!6>[iX5WE-G'l[&"S5%rK,
|
||||
jS.ZgT,S;Q:28:RJNK]6@Rch^R@6*NN+ls.;Q.M&L$[iVA#CU-4D1_:&Ppt'NKsMgLD?Z0/3oe<A.qc$
|
||||
Agr+GQOf_:=/V8!-oB+`-A<?@)+rgGE4sb(=$<o`8"gOH4aka^UL*67[EJ-Gk+f>+Ibb?,iL1>YA)jmD
|
||||
'7i>*SmR\94U9t]jsf*F?D[1^'09MVB1er[*aT^$#69psTK`0^o?94;8#3V.*cK#gr^l,SR>;buOe<lL
|
||||
>NNE:k).=Qj2jNMRdbu&^\eXY\&C/g_,Z#lmtGM>eSjJlBe!.qlj1-i5Bp(nL*N7mNml^1[3`g'N9i\C
|
||||
AS;0?T"_'C3_^T"Q]Lk.5;Q"C?_*H0@`!@Oo=SVf@tg1W-Zu0/[@32LOCaGdo8IUi(rLkX4<nAHV.di%
|
||||
+a[-_;,=c0R=dOL\PIY<WgkR5?@^@:02U<TWEkV_We<gHeVD;4W3aB!@[3t.Z%b+SDs_H<+F=^b^r#c?
|
||||
_j"HF)S'!4.cu7qr+9IbC",5oIq/86UpJeCUA['-ROlDFo[^&(alkdtQ%>U[Z\A^/XPR,cEce%_n$-Jk
|
||||
8[WdQ,tPumkg2E'8"m6"G:0ll*:L!s(<S%ifY_K2[iMN6B>/!fo9;(841,rjZI>KWq>*K*1M#6q;m8#i
|
||||
jFb3m5#j7Ok3'b6!eY[^"7OSDQA[)F<Yu*N3a,r=h]XKpdGZ7=.^"]P'*`]g`Z-P\QYu-;$4J;*XY>l5
|
||||
e$Xc22"sWn3-MDX;+AL3_CXbgC5#f\9;iRB,3i??b*F9ubj>4hj^ZR,E"osD_,6-&("We&dGE3@O_Ek'
|
||||
$::F$*)eAdk.%e8=O'4TrNg3$:b2%2d<Wa7<7Zu&GjRUs$`,8ZJq^\"9.N]S!P)6M%\ZpJn..O:27u:,
|
||||
bi0j42=TUP<7;0_mQF`^(.r7TKpaSCM%R*fAkq(&/i-Y"='B$Kl4_bVbaINq@;OAD>%+)6d*W2]rptfO
|
||||
]2b:3$ca5L_V]p^>1W<6fUoq8L`#LENPO_:B"bVL%I[EFs2uo@?[Y=nRFB1[D`m)5EHS7Sp'-HCi8W1L
|
||||
@pn_sCJ=Ks^?)Vl[s`NU--&@"Qj6s<^?"O599M0G\(,8PY@ColSE.8_Ye2s)ICXOrU=d&<IE@\(,>!'!
|
||||
WFM*%B9tZM[>*FQ?TN@%Z?(9+$I9iTICX(4LEp$Wjil^%d*Y>(13;3:CJr$L'uu.`8_2gKd3XY":p2$6
|
||||
<qkE^c[eEG32Rs(S^nc,aVsT2MJP38fU#)+@pdprghgCRLQ8i]`5fA5fnm=[QR#&<@p_9P:m*s9k=aL$
|
||||
`bf'$55+)5oIEb$D#(C]gQV@+Wr0Qd")"oKXioGX),N?Ef922VP%;h\!Yf4+IC\ECVBcaDLEq0;T5.uc
|
||||
DP(+/Hd_)o#ij/e:o@rrOi>C)qptV;cXdWrQoFWaZuXFKQ=i^ih/.(A]h'1U^%Ij,k[mU+ABa;tP't\X
|
||||
k#10"!kUK@5B'.*s8>$N2Hds:'67a->%]ash0hu9o+=^M3Qu;=_En<@g;T8&\Aq^R9=EB.3p+uQR5R4]
|
||||
FmrX61IBiR`Tr)DSiu]>(T2T*e#CW&VD]V'NSVA_Nljs"C8(BWcbEt>rnh;"5ntCC"1'VenD`g[<7^.8
|
||||
OEH>b>-k44F6fbE-FehhNA0WBg[-\2dM>]#ABuU7_fK"Z>*6K%=U_MLA53Nfo1"m=<R>?.B&*]'h;3?\
|
||||
RC.[^*6JK&pR55i]arnE+<(T51cjs]Z+m&\rB?7]><,T!;Oe5lqq+H9d8ag;X#5H[T^QQcK0sl;l6MrB
|
||||
*37qLE2NEULt]=]-OC(s4+#bN/eb!_(sUQYV1e1GVfh4:aBO:$oa&'db2V3jTl)>8:gg&BTgNbX`-`g-
|
||||
-.B3sHi$e9!I_AV=t+gJ#<@_@,ndogC#qmtWn*$qL?HGd<t8j>&][3U4o&<)\_ALLNd9ZLFe"0#!tT(;
|
||||
ms+gPqt#'mqMb'kGiSd>NkY['DciI;#PR>a2kE3NdE;>WHtm6aq0.5ZZ)g`<>#dG2\9bP;e5:O$#4Q5(
|
||||
[B6(S=,naM)o%W9C!8.TC,hM+Ic%/>g`>A,Tln?*iN7rMql7N>jOp[C`kFW6@B_<QCf(ArV-.Pl(ap'X
|
||||
)6?H22[rfheT[h2J_>p,U$A=PMY\F\aH!X2euGn`<OF/Z[VGj)cfKQX@OO,bpC^KPbp<eu8,P-!h?@au
|
||||
@sGG21?gaY$`W#"ZNHIFft"SQX^8,.S5p#[>]fk?@>_Y99EpK&X>g0X2YHuYG^D%'.,l3\cTk&Yj`ndt
|
||||
ie][bm,7V*WR?1t,I0h)p,T9VQT(`_:5nO0U^U0c#7n^f'HHO$8fmq4D//><S4`c#*:)+7^A\0M`Wu]J
|
||||
D,AaM6)aN%&oIGe4#f;9W%l:\PTA`9.Vc5rLfuZaW9,t?-`F=i&`'d.?7Wh(+&onm\aUJ4qAd9?O^ie<
|
||||
*KS.PNTtA5T5sS*DsA25\o4Kq`QIG7H3<p"K1i7\&b2\<j'JinD^+:thQrSrW]I%dD.j+cCA46*o!!Bs
|
||||
W@JT3*tu[#>CB-7jti,[@Qsf"2tGIP=`HAlK!AKqG3_L1<8D;VM66_Y(p1<$NU"1$n^Xh13B_k24rF7V
|
||||
e"[S]:!t#*`$*+8`EW9`[C9[K/cKS$a'Y2.pjk)Yg#@i9Sr@Z<cH!]+Pej!j@ou_e<gPXYdd_P>mT@fM
|
||||
@t4L8=j#l6rYQ%`;3gM8pcEFfW`,sUe\?(b/dBX_^VJ2,fCSHU"g(UTrYOF?]:/\Ha511`rf6jEN&ImP
|
||||
ftjXtqaF#GpP(gP0Id'0m1VXoTATAZ]1M$%?f,?(#3ph"CbC3T4SV@/O)N\-TIRi+:!l2+d?[7&De6nV
|
||||
*%$*\C+I/_AV!CIMKZ(=B=L002j(4mJJuXI.6j<\7B3!k*BdlW01-CD=n@<5GoOKt`7'V[]ABo/WS4?R
|
||||
.`qQUC-p48X.O'E1u"/43j7*[PS0+ub4TWLmsTR,Ep]N['^D:A:`'G#n]OONS"+u%iYA+D)p46jl=\\.
|
||||
*iS$kUp81F[HUiH*Hk81p\M4lTk\e06RqD2hu#iu/t1dWo?Y61.*a^%&lA[%61KX^F`B]Y5NWga&=tKK
|
||||
ZF=Gi:*E?NM8_=O72aU]$m)S$#su/[q=W<gY)u<@^^9RQE3-)8[`X7B]tAnHZ]Ir3")jH0ASu;WP[C&]
|
||||
3pm]L75FsZc)`hnL0^@]>*Gn=Sa%>9<3sIrWf>n:XkTs4Vg0M$E8_Y[=AQIS*)];Q^Z2&Y7>;%dWU)!^
|
||||
NSLLV*'hg8pR'n4`e`s>;TkMToC+:i&'sOS=@ArZVH#OrBn^=i1C.XiF+26KjsGRLlKslpnJU:X:V")H
|
||||
-9,Hts,I>0.PU0e)PT>26Cm[@4f;>t_>=VNc@J2kDFmPo7eL9%`Y1P+\_fAj*K8dpPe+q84/^peBl:I$
|
||||
.E0*A"4,,:,W'bT.e">Ud!C*!TV%g*_kd>,gO(D"ZWWHX/[4rbb%NgYQW@X9/WY(]>(>KuRJj!SKp17c
|
||||
aJ]Hj-_*R8iPdfOD`HUorkIL:2R"`l*@$Y_Q1DaE6k.UJ__c?JFfck*Pfd*<QI,iA;]\L-,s8^ghHT"W
|
||||
NUJ0cH3VOQr&33=?*BrOmF$%!3I+edoIt?W;?fnNR='8q9Z*HQEh\=@*J@Wd?[j[Z)tTqj.+^2"KL5YX
|
||||
m'QIF]OSG2Qk.be]DB++lNk$WU"QK-P-Q0pB%(T#(s3r@7PK/=BJiD_(QGBO.oVjeQhggkZ!A]8E=fd-
|
||||
HBpOiFTlF1HQ-Y'f?G2U':"`&>uaAAmHC:On"gLHl,G-h'Q^3h<MEZR]Vhpq;"%[h;mk9=HEA[CCj,\c
|
||||
*_nA35SZ+,N27$P/X2eE/b;jXU8DMQ294B0s-m+kO18=JWLai9]+:g/FH:&-kTujDf1S;.Dlmp+Drt![
|
||||
fWrNN1qK[a[C!''"8Lm9W9q;`N[<[E3'!<QB+)<tig?#;o2rRMP-`KV<c1jFY>@eM?ab_>dN;-#10<&U
|
||||
RUT#!;>W>=85<B!7"*t"`.?"#(2-.fj'.!fC[?!+HGcA)a)6ad[n6Nr:X??;$uc)F1,Sl7#nf.obLWof
|
||||
a8Cirq.fTu-fqAlR[!o\C;u$4BrA?H*D;'2.PEB8f>5Ub)-S.YXaD5,PZGTpnrsS]*b[E\[Y,Fc?V1^e
|
||||
WEl+&5[J>\.-"s]rTIU%Ja=-Z>*3GSZp73GC=31&pQ(n-ZQ(P>'NuW`[(oB`A_&DqHo!NnhRdcW_`J>[
|
||||
:gCW?X09qQ)FnIlm'7U7+R^QL6'6V3JNRikOf+eqT_+>J2auu)54Q>(pXeFJf)c1)[GIE.['$2.1qtD2
|
||||
CHYiD]>fpJ&7%U1"I9f7Q57ge8dgk/b?nGG,cq6k=A_e554>hM':4]ZFnne2IKU*c$Y8$fF=gmqh4Xu\
|
||||
oh,ZPFtI+:9h,SI1=?5LatAqp&>aEkln&nEOTF'9eQT7^T`n'>fVA\.b?ei_EN6C]c,[u18a\rZpWp[E
|
||||
^Z%>i+XF1kflRPAed%2X'\tSJA_pV%%$t1PW'9)(?3.M.gTD)SJ%tsc*p!.VI(S*Xn+>/)Y>/\;<KEI:
|
||||
*W(:SH2ENgPd5>4k3J@<ZGte6[:<Y<H;[R&]'1`&B6fJ/,/pl>\ZWTRY7^EF$3:m-6dO.1I&Uc'/\&H?
|
||||
IUG]_,#Q!ZXl/7-H&'5F4aU1_@bK=`;p`$eaeu:*N9M$plli_*lY2qHq3t?)6/PO/.UYGuI.j(]T%,3%
|
||||
"sV;W]3;]-Z8<ZiK6,Y1DVl8Tl"b?nr)@$Y_r9YQ5/0Md:sA=Tg=gGJQ^]K`c3k)8eG&qU?/4SE?2We@
|
||||
,nD9KV27"=3gsl(aBP@Una9hYhJ9F6aJ[k->heG!11sjmcDAUH/Qm_dD],n2g<-*VfJCLKh5-XN[#E-r
|
||||
n)DY#LE0Cd_o)(J&!8$%,GlRY@/>j79CVie9L!R#?()4EYMH;kAQJMM!;3AX87iO,DLEpWYKI'WL2BhE
|
||||
_E7>]8%E<hO3Gh\mRLl4g#GSGm!`JpRXSd7jJoT5GMJ6q\6cg2eQS6I2tho3bf"k]%^ip>JR>kK%_L<P
|
||||
lNX(rIcQ.:O6jAJ_YMJ/p[__c,Pu<(O)-sOLWA\-C:UonbI;BhAL!@]aYs[n7s5B9+8N`;P.=e?#5uW'
|
||||
CJ\9J#\>-UIl@PMAS6fh/iuEF#VB,57aKX<*SBQ67/%ofA`uWrk%j]94%Hb^6Wa;q7bfU6XJs[gjm=rO
|
||||
S#-h?&'PWniB+-df#ntHhIfDQP<2jHmk>n\>'%BZ/BY^,JF%*W_]K,+fCA7QDpldL2^TMBUA5\E'?>!l
|
||||
2-G=J,HdV05.1+>NG2(LD6;\n;7$Y$Cjo"=Xo3er6&Rt.2i6hO0#+qu;NgjI^OLGcqVNBIfej-9r#\_C
|
||||
nULQi7YmX7dmSO?:/-FtYWr/Js2S'es1U\jQD#1AG=Fui4.aTMCl>:q(Z!=^9"`K?pfkb<<n&NUX;h#(
|
||||
Xh'-LRj6aLhkq3CF,&[t.rKP?^4<\S8+Jk.rj!\0/$p!BI.6.%]u!EYiKm4Y@&S>mdKCW(^Ye9a`_C?H
|
||||
bVQX)`cBYP0k/.jG\$<@2sr(i+Nr,3OV$6V6oX@MUL>Q/S]N#ZUX'$f-<\A[a1Q\'(qVW\2:kDBqi@h0
|
||||
L2hDV66EgbhS1\d&]Zs,C$jcGH0a5IVKlu/DggZZp=3p:0ntc9TRn7hf,Zci9l[ua>84urV;.#7OEd:r
|
||||
_6N5O3fG4QGhuVroZuQ0\,,!)(Q*dod>Iht5o8=Q_,p2%Q\N/%'kri)<Q:#Uh_(OcWk+$N[,WD]3\eeB
|
||||
_knte\s"dm<63s])D/6Z<0o)Y>d,V:1-%(5o#iA6H'2!pVM/8jpOR,AC=jMcOCr6H4P>X-T,YA@=HV@W
|
||||
;"`T@i:-11.bY"Dj;4opmPKU?-!D\J4YEdCh%T=mSkB6!j#)\0?Z'JV3:_dt[1P_6b2?m/j.Vuh%0?=?
|
||||
Z0KO::0^LQ0Z1J$p1pgEh?ms&8(EJU<!K0b7oo#F*.9!BGcI[LR2J'`L-BY/Bh#guc/!j'O:]W4-SfR]
|
||||
PSmO=36"n<ol$5n8&"$Z?[+AfP$pp:>7$9>XQ!]XThZ;)gl5g'OdS_PFWejPd^lnBO.[r5EE[@]!-5IJ
|
||||
JZb,X!8B4"WIDbo110ccIX/OB"qT8s.Q;QC'P8r_.(KSR5Cn,D<XuDW'2Rb$fWnKuL`&Ds_kOPG-@-Zu
|
||||
r^2Gq#G4T>HcL6odX%-+D)-$RpHZCg?05ii^b'(-F.a>l5M_(^HnQ+n)d8KoV&rY^^CSV=Du\2K>IiLp
|
||||
c6GGqHgV^=p`&'E,V8JYhkK8^=*T?P%^tV>2Th4M-_q8aBQnmP.t?g;Ua%=7[)-eeIBf2!@pU(*=X-0p
|
||||
ATQe2i9%[K<CU$0?jZaO*mVQ4/kOrY1GWmY(YBB1W;q)(?Ke,*<c4pi;nk'U!r),`47TJ!c/A8_7Ail:
|
||||
YP_:^'OdU+=WB.0J8#R5/N,1GbkeG!<'AQOibst444BMLS?!(pdi9L.(Tui?4u+qG4`c9h[T7jTF.HCY
|
||||
T:bn[B"$5loc;(<7jckjO%ecI[kg,[#82YU%RmOHNj\HkM):`CZ6)8BraZ/:j([9SZi&E4,ddgSXJ#(h
|
||||
W`IHlh+8(9C0lK/bar8l#NFS+ek[f$A,7[,K(ER6%HN")T,BmCX%h9AKm)N+J#pR_*WCe=J,DC"s+OaY
|
||||
&!Tcl20g.S;r/uo\g4[=<>Si^Pj5p[mrN$HWRhQ=8j8-`.5XV-4mTWdr(Xh9OF$DQce$VDZ,ccjlaCbq
|
||||
2ehKaE74P)1L:U7O:,.XhCA\8]2&;KclZ/NUR9Q)Pdt;OW^MsXh.%8\%H4`jX).7MXM]n'6dr/1At"Ie
|
||||
jA#>?gN?39J;lb^2R+r(.Pk1.78tPpX?O)fKIJjKkHVEGh8e@n82B,OPP=X/C`S_f8$)kLAmaL#R$95J
|
||||
1ckN\R.DAcMW"RR#8PoNNYA`nBEsVin'6'iF[M3k1^4D';#f/WA%AZQLMcJGfK,3\gQ^=%iP#'0,!MC&
|
||||
.6&T(b9-ZB)7QLp^TEILPC44c4&I0?9D722-n]d3/uUA_n5rI84Vf?<>,P#P`j&/d;sTCY*C@WfeU^].
|
||||
DT@gSg$h[DO8a3[CeEj7kha*ml`EQlnqKN3kkST<ot=q@N\aJm_KL>l(SC(Bj0j,Za9@S4?-[S6>Whe8
|
||||
>O(hDHPW-kRr$W*q4uo9@mFh01'9gD7):p_8E)Vk?#fljP"k^uK.$=lnYCKl>fZgJ:MM`C3h8X]$p'%u
|
||||
'pU%[76tg#T(>CBCpnW;#_!7a7eKLtL0umgW`+Ko=S2V:$N^rK]2^khe!)A0:0Q%[?1&G0b]r"-rEa"?
|
||||
Mm&!ce0CZ[pi]A:QtLl3IsX9G9qK%e?aZ0'5J0Ki-0fEM+cXKG3E#mRkRd4M^@")YB?eo#S%`8!W9oO:
|
||||
`HLO3Z"aWO0FE:O>Wj_g')2@A_^kTu*'!o6n[sA?*UA(P6KV./+K%^;.m82XZ2(Y/%B4(9T@2PUX3\P"
|
||||
oS@c&IpjjU6n\rB2Bt4Op4J"@iqr$TT+SF0n+[f0SK*3AcpZ1P6suo)"+>XWpk"pfq)=>srHn#.oZ1iG
|
||||
dWdLQT5`6r4+2OjEaM:_Op-\MjcW!rP-)Dh+72<4a-(c8Ar*fARF0_b-HcrMTlDs_-JP:=B;o='N","\
|
||||
L2AQ_5uTPh0uQlc'(-B^mmng5S:,NjmT/#9_7C!=mt_Bi*0%FJUk>^@F;W]O7Omq'UJl--W6[l'8mYk>
|
||||
o=(t!n2.nd\1:=EmhAd4`c0["Hi-`CN=*4t#4K3A*eTLc48oBOUKSUW]&:9o/L:"]^6QfQ;*iu^''k"/
|
||||
qEdJd@2,!u?ZoBkp%NL?E,ISEY;0l=T$?'>ZbJmJ>90UVL#RVN#B@$;VmM=r$$u?e3-Y/PX86YSTqXS?
|
||||
^i5!V_kOJ;IkWL[$0,4%Tg>.cf8^;_\+%1O_sJ;BJ(EsJ.!mR"i'qL$300tWhHQ6e`P5T]/,(*i)MRpp
|
||||
]%qLiO'13K+EsWAs2(Um^V)=n)RVaV^aun!%fMK-Bq:i'b;5O*02RU(=SB:SWI/W1n'jP:^H+Mfg>XU5
|
||||
SdG@$o&ffT([l`5%On@H^>hlOZ'6iMeoNO1i>D8F>:6Q?ar.BOp?1QX_bNkcgW9dY6;.;ma.&Kji6'!L
|
||||
@V$\:WK!j?6>ltpZ_Br1W%fe:I[PW9`:EtI"o72FOs';O7Ob%7ifGm@,S^1l`hA1fP1^';]n[*qW`+a;
|
||||
EXL[qrOOG<]dGDE<#fhB"GVT-S^[87-7^Kc"tW:!`5QpkTQ3kf5^7Y3SkP$foA8HZo^pZ7a,]mb'%/5s
|
||||
noMZ6(9R5&q(K93NVAH-4+L\g^/R79jSa5&N/@J/O-K)rFPp5k9'h[=n>7ie^,!+&oD%<O=u3OY"Cq.%
|
||||
_s<aqR-)6FE!Z]/$O+M'`DQQ0EuT>e_2A]?=IiBm7jYZIr='fT#hk3f!r:chXOLAB?>9UkonCDC*]`fj
|
||||
3[TpRQf<"u2s?p<MG!/r;t-[QKAt2:YNL7?<R1-f>E-(ecg>KF&ncZ5=!k(hZqdKuF>un-'Y!>?_2J/i
|
||||
3)r*S(H]U,a0dRD*etC\G^NAG`7dLr9WTe:iVBJe$4[[!MWHL^a8+T0j#_'?EbS1%JiA*Ln]CSn9^*('
|
||||
i=*caJO:+7q022Os,3;=[)VT)a$\FnmZ`=Umi0DSTm#:1fp\j.OJBWGXDQZ\]93N^SrM6A_*(iXAbXYW
|
||||
aH\k8gqe3Wb*K<-mlVK?3B1[.8::cV=gr%+YKWWR$a'4R&+Gl#A3S9TRXB:_pQ\bkZ^1%EDq^en$K".n
|
||||
QSN:nWhe&gs"[2T%oSs2D4>1dAu![;,>$K/D"cGWVf(S:J46S2gLPAlbLnb;%l\D0clQNNOsb"`@0eFK
|
||||
5q6Rj[m\-Fc[1j[h0F902d"L7>Gm1pq$(/1-_#%WdaZL62_"9]_(0VVjm5OtRI>T+fC>l5;dlGG.fp[Z
|
||||
9;8$U/#!FEd:V^19^Xj6B4&/eIhph#`7i:cVVP5"ZdNZT*"<po3O\4=`V-Q.$89^8\A],_/lcW5M>erK
|
||||
M)66[d&/Y_;4=.1<H@muKDt2<%G>*i6+qQX_kb=V)ZsjrPgGkoXP&lN=8.tu,gMHO"C)$`$f%QGm@K//
|
||||
qq&[NEh0=rkfB*oIhUQn2KkDR-+8qiIBcP)n@.H7;-N)9T7=oOr86[cQl5IffmPlmlRCUfjt52QER<3<
|
||||
q9%'"$eSS_D.Zb,m9]J3E94El;E;Q'0P!b&'5S_`f<YbgDUAee@V;gZRAL9R6]aBa>=Y;:'d/F%GLf*n
|
||||
&EIA0*,-=k)lW;?YE!eWpZ&SA!F$2M=!(bThcXC5qO?l=Q2V',@fhu$\fb\'YU+53RD6`*X.a,'D=kIY
|
||||
:!3UHe?'VJVm@l+L#<[VN8)($ZMfq8p+e6q(r@/oojLuF=b6kl$m^^^qWlKIoN*Q+?2ui*[6Z06(9I6(
|
||||
Z<4+M9:DC/O0h)edR@9+W?r"K)RdSL#mFTCD9W8T"$]?'*tEtf+XYR_%Rg0f*N^2KT:sJk\+kjEdb"fq
|
||||
Cgmq@4@iK=/o7@US>+(0@gK_#f`D)DR)_YpSf>a_.P:Y*;-"X=`:0bZ=F2AjZpPWV'MjA[YI'i/VZiS+
|
||||
.DETa=B.[KgUV:_HS\_Kp`83l>MFL\hChl;fTO&t\F^553lcnqQ9qDAMoiUh,Bp13/#SLQ@1=AX6W*r0
|
||||
J\m&A[[&$+`FRlM#kRJcQ"gqEg4R6u&$C<t:516";W/%+EWonn$k9?i97H>#EE@\rf3.:*6oA=uDe>J>
|
||||
*E)?WT*ecr4M6D#d,DpUfI+)D^XZJ*G@ncD@Q!Yg3&AP=Ia)qbpBrK9+66>:ETIMVP>3IFB??BFnEg,o
|
||||
]]eU>2n\0:Hd-lC3"j<69rU0FGFW#RQ#i2J8JooGK-O)!ja"t64=G:$g.<rfD];<3=^5m)l[T-a1+'H9
|
||||
2J&Q,plN9#Z6VebG#k!>pQg--RG>@P=N3L>I22a%EOM-X39X7RY?`"<LBAb,$?O9'Ak?nsp*s0_>r/"M
|
||||
X"7[q?Bj=9q\l#;G4Yr50DVtA6isX6V>GU!Sk8=[aQ\u9@T>7F2:MFMChjIT)!pM,oa(AK21noTp%8q,
|
||||
=K>/;[tu[WDMGla<=dY?1;Ut6UU@n.NHJ!6SfBal8]f@i.pXPubt,):i+'i]JhrLsECB_Gn7He>/2/O1
|
||||
EG$l_Dh%aFf'.*\#Za=>*osnaZfY"kq1WIXJN$f0UFZjmR;A@]IIDrkl3<7`W946nC'AA'JdXYp&lGN\
|
||||
6Y+NMMbK:#7UL."bC2G94K/T+j0+\[:OLP#ci;"KQ$o7q!SP@=gs0ZD5JZ:)ai)fkXFc=f0uknWS[-PP
|
||||
dOIlPI*\6b6M>5l*)\:R>4>NbO^'j^3hJ.^AG^5NQ@W%pAGSXCq]P5];6d@^nY$5\J&gcp&FRJ:4d2aS
|
||||
b50,#K"+g>WOTZ1dAItbaCpd?29*L&=0#AB<UhcW(S3*Q]n]X#>e7+Lc54jSZY07kUNW?o[etuCjj2,K
|
||||
DY`lZS'Q<%e4i-E/QTJ";iqfhZ5C]GojguSZaK5Unums0Q!=4rRWZfg0$XJLs+>"\4/V+Hf5:?8osFY_
|
||||
\h3r^]a69?VNj0-InNS\^Lch<U0J%S9?Y'b>+VnG>J2V<`ZMhSIh6H.a0bbiakJCs?<fOW5LV*l*,55W
|
||||
r9G^&f:dE#>*n";Xa/q'A,02/jN@o8e`esm;Sc:Qe&lk#Q@GH1X0he-E7$7eRgmK].Jb]#o.6-RZk$On
|
||||
[iL:smjhQnLle`A`AHU*U.bH(bPt#=`#dKTOB_\hHT=GsO+h7DIo+G^dZRi6/Q$--nuqq)q(iHP=T2;Y
|
||||
61\Xb93*'srjQlM0ta"7Q@B>oVRM35nio)_OZcdAMISh$J"qK&bF_(2/n5`KFX3'.mV:!!#Aa!a7E=ts
|
||||
<,g$qTb+&)rX\f+/`/fgX<_KT[V7\ce1u!,bk:/VEL_&EL5"90E8D2:l&CW1IUK9<e#C#Wg9>g&YEWH_
|
||||
l_[bO(g,_%2ek<WP*ir'ScJ]9L[Ra\3[eiIDN5=@a(Cuc[<%NpH(YJ(-(;s-PW^N*LWK4^Gaa?'3oYt"
|
||||
54oq"'8Ub7??Fle'n([UV\CiJfXPH[Dra!f(>fK:(GZ3kXN-o\SQf6LSu>^m:;KdBh`6Gd_Cl;Y2ntM(
|
||||
lT=?_6l+^IN7liR&,,EQ9_)j=\".8O60_N%!7R:h(YjX'%5Xt/%UfD##QK<EHgF4ADE_3Wc(;C2?sOBW
|
||||
2c?^nRi2r[&YQYH@h!kCZsCo0n(Ou??D'5tVHU%d]ue.K21(BQ)1KWeNU^uN#UWAPhVYNGraUore=;^K
|
||||
o,?$?lm:r>J"qGJ]?#Oi'GkY_5\%cn3=9I9m>:`!^j%RbYGHF<"1X65V>e8L`BlN$mU2d[X8VHN7Pl'2
|
||||
llLg_Bi360^e._s6't.DJBMQeDN5ck.6E8*"@qlGE(geZba`Nr&>ne[pE7]>r[Bs6.B08P==`[<TW,J'
|
||||
.uKq1#@VfD0AV77[;P4pR"P)84U@)'g2QC"KEn&=3UKAnKAbDFcJS,Oh1IE#e+=d,mT'YXhOQ=`6*SuB
|
||||
nqC4^ZdKLR#r=)U].SSFE?7I4nS9s(Bmb4<k-5/-*g005gWMTt%t\;;&10R'nfTVUrTN++.J;mjR"bL8
|
||||
#@TO"/a5Q?]!r"(@!WA(dt9=ZB:'`nWEl-C&8%iRah?/^SXdfU)01:+A)ae*ZkiD"GP5,s3'@OG?>+Wr
|
||||
?:\*V<^l"Mh1It?4:(ZQ$4k!A#aTfK=F'=Z&8(J*C)UKs/9],tX7&A'\*(HZ0tQEe*X%B`-69HcB(X_h
|
||||
-r-TDiPEQrgK'Mo4qU0r)d\]AY@"J%b9EX9VNmO:a"j&u]6D_?@b.CH547KY/rj<a>u@tpHN^sBDj+]@
|
||||
cgNOiYF4KdWqIro$I?irqo[Z]/+"?YmEcm6J7.qIdGA^rZuaTgWR&;&CDbIc5oJ70&gApm^?HsSZ[EH^
|
||||
f"j5:`L?,qWh(/u4?kR4^ep@Sin2;XhB^>Qfu<K`=A^ujKpWo'hs.?T3PHf-amlK.n$tp7B^*J1E/#:5
|
||||
0gcdbE?<U(MQ^Ks-<fFVq]*TMMH`mq%pUdVph2NKs,N.i@25C>.gEMf;2%0Z?HZDGs2)Vn>5'\_7XR>o
|
||||
L=Ws2GP0_fY]-]K^[+4Fl^YT2(R>57L!S?B2.ZT\a*PW1glp/$5Pb\VZXff$lDJ"a:k6m,A,1S02mHLp
|
||||
4*EGMOhKQE6&KJ`4a)^!>[l9O=%_'Orq`s!^*p9"DR[aq>tI3lfr0W18,\mHPo<l3W<cH@R1L3:7H9`e
|
||||
MoL)kjfcB*Mq_Dk/Mq-.p%RZBNlH2f8*X9$bCAM4QrJ)8m1cAiIa"/'i8R4),PX89jsU#Hopl`l\:P?h
|
||||
@cQfKp0"ss,=lh*b7Ws.(aE.s+t$6uhJ9f0_nR+<B$#9fU3L'Nc\c)hH:tnP<F4^d39l>AJ]4D!ZI`PB
|
||||
C=PlFbFjLE)cX[UK7`=7PJ3hP;HgIk\D(+W*qqtd>r\X9Sq"fNr9K>k3i*j:@-rJG7a1N-YLjGj>\In]
|
||||
7r2k#7[UZM.t%iY&KBH/,@7qVfb/DaiYb.kG3?NiNb7B=RB.F6@6[,c5"Mte7r8-"f8_hpG/Wo-G/VR"
|
||||
mT+_ug(@OB<pYVOe^tI<s6&K$ZdI<K>)T"l]i9p;XBd3*.jsV1m"c+9p7WLH+ROse@n2Jb`fA^.>H]&%
|
||||
Ag;K;et]PV[f4FE:FVUAlN";PnAG`3<0e^$'LBW1.Z$Ns,C]1oGVgoh5WdtjL\jh^k*KOICoG])^lB6G
|
||||
NubL!&j'Q]i7\dtpgu[keRcb%jMYh(%*HMK(d(J0@I#^^g6()$5PrdN6?^ph'Zi^h@c>c6";$1\_C/b?
|
||||
ChEo@^DB5I7G:\:\\AlG(43$hGr0%o=&_jq83-VgkKU<'r+04,kOZAL*rl)s?ff\4pP:'`rXsXmNK7_m
|
||||
($e&-<e%useoY^Qh!e)Nj"t9KTpRTP9emI01XM?MB/DN-=TsJ8c3T+Sb?4>%7A[ZmhU:6WR7J>6j=ZpC
|
||||
^lk>TDc'Lj*tRo9g\?#&"m;d\^.k,"Tkp@*Tp3m9n;P-G`WNlQ)84>d]PaCV%r/blIGl'_Q>K=+H\k^\
|
||||
E9P!!Ho5%NeTQD<oDFL/rC&R]XoH()/$g*2)D$haX0,_,Bp6d%6$UAfJaK,)F-;/qHi-G_Sdb-'UL`'Z
|
||||
JXtBkL3JS='uYrMQ0;R50;ZoC;.eR)Du&=]Dk:7%m9V,8*-c3`7-_1*=pO:L]Pt"CD].`NbA@"&]eFBR
|
||||
XK7$mmjJ@BIW@?S@Ti]^Ho7bA^K(q0Cp_T8'h4'W7ecfo?'NXY<,c9K3QK5L(kWb?$`n.:]1(I^p):++
|
||||
i1!N+#P'k'.?8Lb<_b.TCk,Wb[ILpk6_s.pgIi5r/Z\NSNA^#KGi6PfCPGP5S%Ra;:F9!8hEb9[-U*qR
|
||||
A+T'4]OcAoi&\"H$Yc\"=rXn[p81/ohL$tXM,)&GI1so*l%-.p>I-mrG31pr]U3c`r7/fX^[Us'5<]%$
|
||||
?^ua`["35=e<%%p;5>o^^dKtk'\NdtV_li":u\qFgrnAP5DW+Mc3i(mJa'S.ltiWuCC'%3)1tb?]G/Rr
|
||||
KU$$=L#EB\Z4<nYlf%Vt+-*bdf]hSF^Z3VPNR.FYM%3F]*N@j4lQW<Fk7gFUG5>3+CA1=<8ZAgj$qY;+
|
||||
0!u3J8_e=<lVa3r/s"NMa:mZ5]@.QE\]bG]7r!6I'TZ9a7S.@h5tdoDP9@pt9'qWiBJ-\5\^Wg1;Yra-
|
||||
;N=WtUpadr$%KiSG,_Tb.S4LCCCR9!)4_I>S(ATbC>7jb&TZQ"2Yn`-H_hc^ld7!\W9-`fGP\Qi[6*DS
|
||||
-++c(e94Nbh<F<ke@%/rHqE_Ndr\1KhDeD*D4QB7e98F5e396tgG*85TB$2KmEUt4;OciK0"%,?a5q@1
|
||||
[%^f?pI=IEXUXu\Uim7fMc./g8k_kE?79S<[<ln][JX9(19LEkkh%&d[:U\l4&8a@C3LXL\CBq;kMgNM
|
||||
=$D\pmP*Ci'c0PKCA1:Oj&3KBmkJK(:/)7@V"R%_pg^p2mM*N]FU[B.';P19Gaf#]k=bc59BH[,J,U8X
|
||||
Q)">kj3..dN5NmOLHK38&"9+,bi]r<r&aW8c+i'eDY\k0:Lc(r=cClC5Ocr:G`k8:bdECHWhI_c_2g%@
|
||||
4#2@;]mQ7A.P<U;]p+r7#)g)KBs:Tcca;n[e/jIPH*sko(OWT4SmO7-]JTH$fAN*4LteS(&j>Lb7=U#R
|
||||
/M_d3K:[=kC@1k!Zi'afO]/JI<oZX=aa4luldCF#([Xr!,'cdj/ME^>B?#QU3%(pXRKpe;KI`\V02(K7
|
||||
_fnXV2_1t7ZS0fQ8QMb!bM?1.6W.]CM*6\n^YatAMuM7`1g]#`8)@Un)@_2\,NX7_*V`X4&pr[R]]i(H
|
||||
UR83cKccghUi]`MS+.C#3N!;WAGWYD[hOO-S]C>\N:M/`h:6m;^UY`a<OcVT"if@S)r62&q1$q#bDttu
|
||||
&_t]6bSN75pg/93p!$cQ*=NL+0dfNYKmu>PWii4pRD:M(D<.c.l;44r(>3N4MVWHIn%:<B^'4mNqW"0&
|
||||
9Q"+<hFGU%'fn@Yd+4#,1%cN=!I3,8*(+=f>Pf<(hY/?tMDK(QoT.Rkq4%QKaYSO"@<Uq]9[9]jCKi:Y
|
||||
%.1%)P!R)'Ym5#^Y1e5mqa\s7aGVo#VTsh<aXkXWg05lCId<)h4iI*V)eu.MCAS4D1\kq9m_k=r'Km^R
|
||||
XFK'JW9gOn'^b0(AQA!4+=G_!/oIk-0@="rL\Ui!5OP06REA^tbXt9&FSm1>C@LQX=P'0[bdOIAC<YC7
|
||||
3j+Ec);!cN?iM@\<p!1$SaM%LKR$(D%;dQ6%[./u7H6.n=DAn[6N9P!ULk[Te:]V<GWnGM^8NdcXM'<>
|
||||
G!nDGY,-?<cr\0H!05p_rYcXN+kF#aKk,@!ag/eI*T9+8A9J<T7L)Op$O':Q(M>bUcDCbL2YS4!lg:[+
|
||||
4"cJZBmAZs#LA7`&]Xas^93:D?0E:Xme*8tUf@25kT(h@oZF.thN>Z8>I\R8En\R=9ng2ARr]!n)eaF*
|
||||
QS@DVim,B&_7*\u$h>JK]d#uqj?'^Ak/f^L*?eULcEb;&VI!BWXab]8H'%&Tmmmqk)ndI6+/J9q[^.:e
|
||||
dTU)Q,gdQ#HJ)@K]d"D-S0>eMH8p5qDDa?`7liKYi2iS1,l<Gm^mu@H4-N*(V\ZT0(t$#QrIuJ924-;>
|
||||
YjGGYCo,PtK=B]j]WX=f/%m1\pJX%[CMhu\WOl?(7dRf^UAaaGNbN.<*_Lkg_f<ZsR*L?(]";g,dW"Ys
|
||||
_oFl1MS)"AjQ=b$/C2CJ0mG1_8uf893mGKTKJliDN.i0e`Y!F&a:[1MCE(kiHWtrsBqc[A>"$Ej]=cE,
|
||||
Z&s6uM2Z*9m3R(*;/a+sHg5gd*26c+P!;qZAW&23V8Rd.^nL&6_Dni2#lVRg%(dHiP%%(#1WCM=GWV"h
|
||||
U6VXeLa_fA<bZ2@+Er@p=F`AdX3]`*)qnh+JLXfu4.hqK^BJMD_gXIgh$foTRDh\nk0e[l=BGUt#QC,W
|
||||
>GdVELp-o?p]fH.;X:C8`:m\@=)#AZD.K%i>>WD2IPmhY.t_ea$^&9=U_hd_pHC!6fB^.XkLmQNJ"E>H
|
||||
<WUq3:,[a017(Gg?39Bh`mq&EgIcoA@uk)cbu"le3f4MFS=TJu43c7m;/b$R!Iq-8,B&^O#hut5%""aI
|
||||
p,$9*8IimC+Xo.JK-\MK.\T._YOLpofkA%U1$8X_dQF-p47rWO)XL+E_??WPL>MC%K=^MdqF4`l.[VC2
|
||||
6DiudpB'#X>8Z5^.`EcEDn#-&Vlr6'@C1eFU)k/oXsLPB`A;lFQ8N(?@CM;B8\f,RA'[2Q\?VUuN\0+&
|
||||
U$O.'h9U"oq\d(+d.6j)4+]\u2pQZ^Vpak^P#^1;0@+;d_Gt0A"2'a.CR<mZ>9)Airo>#t@:;=7$-Y7X
|
||||
:M5Zujk1#!Yq/[\dakWb43h!Nnm%!:V_B\u]5nA'PJ"hFGri^h/^Ia6LRN)5.p!3`T"Si\5\\\.!j4"K
|
||||
G_Y>5l/R!2Z]d'*QD]fqc\6(Y?K=Mr:$OlET#)C=Wo8tLH$3@3DL$CRBka9tBG&DXm+D6XnoW0WgQiU"
|
||||
/=e=f7a[+AXSia@]K87T@<jWJR5AaS23!p"O=X2d,XsEY3\=.5:7((qOU4=>kKtK4=B_;eBA^SsFLd(d
|
||||
\M,V]Kh\?8YYQ1pg-/;uB1EN4cp6YV\8Bp`=UrW]_\!KW"^a&kj7H"c,KmFb"K,Iq`[C4`H$8b!_X\D,
|
||||
jk6o\Q)6L^['2Z>;44bDahHBn`SqY:TUmuppU.3QY4VqAFIptibHk!P(c(/CFXul8Nog)Q=p;>2T]^uO
|
||||
/=;cfn"Z*B\*6K*7qQu]ouMM7FnK/5e7/'#&Se$sT6i8Mp(5_2'3Q@&"(ROWV9^D^66'i\07c"PB')e!
|
||||
@rRnRAn5^FKtV\BYfjm$E):3b)�]mY/=2&TW2ma/lC@`l5/:7&KFm;%lW1]No%g&FYr-QA(4Ajp8"P
|
||||
UW`AA@eJ'PpMVaWMt_/":Ik@&e=#1._/Ek1o8W33r>V$(o[7tO)a1^ATDOu*L5h_V:)s?7-dh4Oj6.#A
|
||||
^NUZuZ>.!]XjX!.4W$Ou'^[@`><U=+dt#Tk.Qse(_,"sSQ`Bu+Bj#;uWQmWn3/6c_g4k5e/U/hqOO#(3
|
||||
%<._RT0st$bP1K*rp_)ponT/H4PSO4Lib$u7mHtAq>YsAlO-Xs^&E&31R*%jXTIKtWlWLF/BamMpQr)(
|
||||
5Gm?l=&ds0(t[ueQ2rZ,n8`TuHJX=)^*Gl==#tF$=e3,&*1cofEZ"KZORCb0OnaY-jr'AlDE7>Qi^qu*
|
||||
2?"K?9:"P^"t,bMb8l4/q?'q+h'da_3?c`G0cKbgd6PpG.#e1p5J\(I@5sj'7iJDD(o>7naEmWqShR+L
|
||||
4S*l`,@7rr7@^po,2jT&+%;:m]t@%1J[WcS7b(K6F32![)a8/F5Kg@X\,rsF<fSKL16hs0Z;s!Hn."6p
|
||||
s1^,2j)B=?l$SWTGtcK;R$tK=eS\l3hSuC&WG<c[GcJqbcZh'FVJO:ON`H[&/P3o0gVWErP:REsFLJOP
|
||||
JCf[`jnkLQmFO4.bL\UabL\V,j\N>IHl+Foh0IVSB72Htbl;[Ps5Q$;lYZW^B9O!0eruWJ.94iBmXgI_
|
||||
f";(c,cu?)8"a'mo.J1X6Omh:6PU#>K6ar\+W"RG#?l37MI`@%5sr]W[H!9W?bYr)\aBEg7h-<P\2#K_
|
||||
nub%qF7?"%f=7Ps^LAl+7a;u.;1gY+/:P&U,-@#\C)p]#4lVn^;c`Ef,_p],B;k_gA9h:NQM!FPl1cMs
|
||||
>p3>lf^_joXJ,ocje6-7Zhd+VW`?@S23Ls%eRb^mr)p<f2Iqp&I_5:%s*nl=Krd`,ASTH+?'qtPoX0+s
|
||||
3F@>#J7TCd[*QsU]^AV,q7-D4IJn$Dcoi0_)7'k43jW%eRuq\Ti3mRbomMf<o'SB^6e?q9X=$DPMN"Lb
|
||||
.d*2j>!W8,QqN`>4]S(M;gEu`$[=I1X8FtgIC)%>YD`P(?"^QNB58a3gh%/K)&\lEfM$a27]phnR#PLA
|
||||
edLYI*EGPbor[C+oN8CD9^H?1bQ>56o&+;[h(8MYD65Y'P5_`:e4.of;<;M/m<eeL?>;`NXn2Vkd;ncg
|
||||
^FQct5Q1V3q__PeI!TG<%eKL=@?\TrmIZHF_/,lXEaLVWk6CN><8KkoHkn`<h7c)oZQ"JG*eAT7nX]4,
|
||||
b=B2i8Hru<rI^<(QAkl6<F\;@F[qnUPK\SRD,L:faQ\V_]U!sAkM,%,r'.sZBtrn8f09>g$M(.%5X/_m
|
||||
biJHkQ[^#-rpoVe;>LlA~>
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj
|
||||
68467
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Parent null
|
||||
/Type /Pages
|
||||
/MediaBox [0.0000 0.0000 798.00 551.00]
|
||||
/Resources 8 0 R
|
||||
/Kids [5 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
[/PDF /Text /ImageC]
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/S /Transparency
|
||||
/CS /DeviceRGB
|
||||
/I true
|
||||
/K false
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Alpha1
|
||||
<<
|
||||
/ca 1.0000
|
||||
/CA 1.0000
|
||||
/BM /Normal
|
||||
/AIS false
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/ProcSet 9 0 R
|
||||
/ExtGState 11 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 12
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000315 00000 n
|
||||
0000069210 00000 n
|
||||
0000000445 00000 n
|
||||
0000000521 00000 n
|
||||
0000000609 00000 n
|
||||
0000069186 00000 n
|
||||
0000069664 00000 n
|
||||
0000069380 00000 n
|
||||
0000069419 00000 n
|
||||
0000069521 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 12
|
||||
/Root 2 0 R
|
||||
/Info 1 0 R
|
||||
>>
|
||||
startxref
|
||||
69737
|
||||
%%EOF
|
312
images/satrs-example-structure/satrs-example-structure.graphml
Normal file
312
images/satrs-example-structure/satrs-example-structure.graphml
Normal file
@ -0,0 +1,312 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.23.2-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0" xml:space="preserve"/>
|
||||
<node id="n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="372.44000000000005" width="681.2" x="808.8000000000001" y="142.0"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="296.09375" x="28.614190924657578" xml:space="preserve" y="13.404178370786525">satrs-example Component Structure<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.4579944349315068" nodeRatioY="-0.46400983146067415" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="311.8450000000001" width="155.39999999999998" x="823.9300000000002" y="187.59499999999997"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="155.39999999999998" x="12.272399999999493" xml:space="preserve" y="9.639200000000272">Application
|
||||
Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4210270270270303" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46908977216245173" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="207" bottomF="206.71046874999996" left="0" leftF="0.0" right="5" rightF="5.399999999999977" top="45" topF="45.13453125000012"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n1:">
|
||||
<node id="n1::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="838.9300000000002" y="247.7295312500001"/>
|
||||
<y:Fill color="#FFCC99" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.2216796875" x="25.38916015625" xml:space="preserve" y="4.8515625">ACS Task<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n2" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="355.9183000000014" width="539.4029243565859" x="971.9147999999997" y="158.5217000000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="content" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="false" width="4.0" x="267.7014621782929" y="0.0"/>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="1.4779288903810084E-12" left="0" leftF="1.1368683772161603E-13" right="0" rightF="0.0" top="14" topF="14.073299999999762"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 4</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2:">
|
||||
<node id="n2::n0" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="311.84500000000014" width="490.5852000000002" x="986.9147999999998" y="187.59499999999997"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="490.5852000000002" x="18.817724356585472" xml:space="preserve" y="8.551700000000238">Generic Components<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.46164229096885634" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.4725770815629552" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="57" bottomF="56.71046875000002" left="7" leftF="7.372800000000552" right="193" rightF="193.19119999999975" top="28" topF="27.68500000000006"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 2</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2::n0:">
|
||||
<node id="n2::n0::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="125.0" x="1144.3088000000002" y="284.00476562500006"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.255859375" x="24.3720703125" xml:space="preserve" y="4.8515625">TM Funnel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="125.0" x="1009.3300000000002" y="397.7295312500001"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.830078125" x="20.5849609375" xml:space="preserve" y="4.8515625">UDP Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="120.0" x="1144.3300000000002" y="335.7295312500001"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="81.1298828125" x="19.43505859375" xml:space="preserve" y="4.8515625">TCP Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="125.0" x="1009.3300000000002" y="337.7295312500001"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.1689453125" x="24.91552734375" xml:space="preserve" y="4.8515625">TC Source<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="125.0" x="1009.2876000000003" y="230.28000000000003"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="109.9228515625" x="7.53857421875" xml:space="preserve" y="4.8515625">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="125.0" x="1009.2876000000003" y="284.00476562500006"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.2216796875" x="13.88916015625" xml:space="preserve" y="4.8515625">PUS Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n0::n6">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="40.0" width="125.0" x="1144.3088000000002" y="230.28000000000003"/>
|
||||
<y:Fill color="#CCFFFF" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.1650390625" x="20.41748046875" xml:space="preserve" y="1.703125">Shared
|
||||
TMTC Pools<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n2::n1" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="284.0" width="180.0" x="1279.3300000000002" y="208.7295312500001"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="node_width" borderDistance="5.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="22.625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="180.0" x="12.572399999999789" xml:space="preserve" y="9.817168750000121">PUS Stack<y:LabelModel><y:SmartNodeLabelModel distance="5.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.4301533333333345" labelRatioY="-0.5" nodeRatioX="0.5" nodeRatioY="-0.46543250440140804" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="24" topF="24.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" xml:space="preserve" y="0.0">Folder 1</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2::n1:">
|
||||
<node id="n2::n1::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="247.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.255859375" x="19.3720703125" xml:space="preserve" y="6.015625">PUS 1 Verification<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="287.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="128.39453125" x="10.802734375" xml:space="preserve" y="6.015625">PUS 3 Housekeeping<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="327.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.529296875" x="33.2353515625" xml:space="preserve" y="6.015625">PUS 5 Events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="367.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.9453125" x="31.52734375" xml:space="preserve" y="6.015625">PUS 8 Actions<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1::n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="447.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.205078125" x="36.8974609375" xml:space="preserve" y="6.015625">PUS 17 Test<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1::n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="30.0" width="150.0" x="1294.3300000000002" y="407.7295312500001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="116.8515625" x="16.57421875" xml:space="preserve" y="6.015625">PUS 11 Scheduling<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
835
images/satrs-example-structure/satrs-example-structure.pdf
Normal file
835
images/satrs-example-structure/satrs-example-structure.pdf
Normal file
@ -0,0 +1,835 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD>
|
||||
1 0 obj
|
||||
<<
|
||||
/Title ()
|
||||
/Author ()
|
||||
/Subject ()
|
||||
/Keywords ()
|
||||
/Creator (yExport 1.5)
|
||||
/Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5)
|
||||
/CreationDate (D:20240208115813+01'00')
|
||||
/ModDate (D:20240208115813+01'00')
|
||||
/Trapped /False
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 3 0 R
|
||||
/ViewerPreferences 4 0 R
|
||||
/OpenAction [5 0 R /Fit]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/FitWindow true
|
||||
/CenterWindow false
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Parent 3 0 R
|
||||
/Type /Page
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Length 7 0 R
|
||||
/Filter [/ASCII85Decode /FlateDecode]
|
||||
>>
|
||||
stream
|
||||
GauF[afZr%OsN$$?ZCN;:U+hm2(CkA#-&YV>R3Uj"qa0Zrt!2qelkQ>N_hnSoNp?Fgi_i8SMXM?UQLkW
|
||||
e+;HuHN!Fgq#t!&ci8Ckq"<6gqs44=YJ4\WB8(gBo-Zd&HGAJ&?[qt=s8;3_IbsZ<ro;?)qR?Kf^]*VM
|
||||
I(YXBr:b^<oABT>J,1\ql0j-"j4!&gj+$$s5&'UKp.sCidLNY[7oB&2WpB]A_uDL$^\V;XRH0!YUA=Iu
|
||||
%Z:8Umghk_1;<)ME\HfAn5&"L'8_D*5B/gc\%hpGs()lA%t"(7M#VbsVm$-fq+9l_qVV<VBmm2JkuL@&
|
||||
[aTHYT>Eb#n(&Q(k<ErKs0ocBorhgHn8:?1G^liTn".;bW8DYY?OPZ(F0W2;3'V=#m<e$"eb]#BNb3K7
|
||||
R\S9FIQYq7s71ehq_D-_UUD7D^Xj+(BBsa_VmUo5MDlhOANF#6T?Z9*Gb:/42h009W6Q>FT>n3tT5N]B
|
||||
2'"Qe2]"sdc^+%2*)H38D+AqtIu!]8IcnkYjmKW<Q(P>El*q2#K`?4E:[?L)Y4eFZSX9A"(i^"/Zu5d+
|
||||
[PqQ,<Bb2C-DRQ6kFXm.GM/!SG4^1DL\IK+n!t!"]$n8C8#3Uk*sc@^=6h[5%7D\>X`i-s!Z)e[qHeEq
|
||||
6Td_IHQP0B9:X(Op2k::Mnce\8%[bB(epHns2=+T45Pm3POJG\"T#$9=s"MICEALZf2kM0N'8MZRt_tF
|
||||
^ApFhhMohoGKcgZoK`Z%H%1=Ai:#h"\A6u*ZT9$$3ut^UH3;daFNKp5AS`X'4T)KMi=4XYP;M[BBmHc&
|
||||
d=>1.FaH9C,u/+oON<ad/#Bgn59,Z.?'+`N+`kn*!Pci0KX$iD[6^gmNQ1'$cTY\:%:(J`@mJW_BQ[Il
|
||||
ouiCd)m*GoX(ffIBUiCFR)H$iI0P#=[<aU50WGj0_1O8:fSd4NbeTeTmI*pIXPmgK:!W*li6)Tecj$Ir
|
||||
n-2tkZ_edrf;sJpiku'`LU4CW!c[#NN]-[GiP!M8a/.O^IFR#)jHkLp-%qg!5<nB(dj[f0:8>h4^l[$;
|
||||
nV5r*b6>+C@s4DSIDr^dmaW/@;N=Ul8::XNN]&WgN-=HOLO-:tec^:6A"_?n_+N'WOGa;@k,`91E`RLZ
|
||||
LH[Ncfa0h,]#;S73Fb+=l&lpRhZ<+-]n[>'^jc+OILMp.dZ/h6pe`LWm]Ns6%O#6iZ^,9_>UMXUl`b/7
|
||||
qj02lQV#h^neSNDZTT#)Y4`idH[W!p*fR#4HEQV0V.DN`G`\!Z&OY2@ib(TAl^A8e>@%7HAN$?u6_Z!0
|
||||
NI@hB&&A[V&Q3PR?8%kr?)47'5tb4A\(,1'S"uiMO,\nTghqlEc,Zt#7Y2E$7bh>U];BmDADQX&^s0Ul
|
||||
o0!&0[rLlQ&"f7ZI8Jh;FUsq&'%]s72l4MtZYWSB,55R?fam#iYE2"4+#l9/BiuOB@"n"(I7#AnF+oR)
|
||||
gH!%p-$sF%Af6.R5!5=g2?#dFTDaL55<]%'Sc1i<k/2>&I5lj$?#.dEc\H7idL7QA>J;0V6;u"eHaL3r
|
||||
Hp>'rdsWJqT[>f/Z'sok6dDa'UGcP9A3PBZBNBA%j.NA8!2#\CDJ)0'i@t91'0"D$L8QG+dBc;8gDk'6
|
||||
^nibODG5ZkBL#1Pd:Q]?B$0Sm\hJ%[J]`45Fh*9AP?#%/)'R+jY=`!3jps7/>r.9!0.bHL_$RG[T)Teo
|
||||
-TqD6Q$U^Em'l0XTLplu)8g9/ci:hJk<E7#aRao.bO^4\BBSc%*oqABci5,4rke1&+1.^4)rBm!8"TaV
|
||||
[g(1Y6'pd7o^Co;5GQ?ie_p6fA'\gI8USnHq*-\lcSKD6K4k4Ik<C>q"$HU9i.DTio:PXJr221_O6?Oj
|
||||
Q1iims/1WhLAd[Mo!6QIhO_KAr-84I]KQ9tDU73FKCB0UQ$%BO_XdUP]M!SM'=NOb^O<"cT"jr-K]fN[
|
||||
_P=F9hk'ji;]Djp81Dmb5s*X6Ue;'@s$:LUk>)e`s1o`Ik^G?Nrs'k&NthL$5BMIk0@/PP$NuB&UB-,4
|
||||
5Nriso-[i-q&;'Cji'?l".PjMiPi<+&]sW/32-$9r+uMYfCAcur^khfAM3>\om_]ra&pO8g8,$[`sh_o
|
||||
o),=!i;#TBRaLE+I-VTXnoL;TmZQX3IT<I!*</-G;<R6?:X&!o2t!]\FFU(TdMiG`a,M6`AlI<7[b:()
|
||||
5%oLk+?+(eK0;E-eohF6>klOt:9c-bqEC;4;/a_KbHna/L.8k(L0Zp)?p+>-aY$bR4?ftA5>TDNO1ta/
|
||||
le9\"iosoqg##3n`-46f]=O$W)0/["1jKU1BS!Q72s$"+*tS@E"P3^pKb)GJ^&D/?ErNR0L>8qJ[lQj>
|
||||
Hm_%(eb,TGSRYAA5O\!D"5W54I$NQc;8T8]m,4TC+WfJ(p/?/NG<a;b<2"em2&L%@G&;^4LCE4!2ubju
|
||||
mT>J2j;_1XfaLgGr[#Y@A(_;K=Jq>8X3Kj1F,JH+R`m@d=0r`f`jaE2?-k"EKZto"Q[2?J^bVCnGu0*`
|
||||
/!q?s%WWb;Sc5+;lr7E.>kW#&)nhagQTr0s?E;/<*e(e(Q-]%.qJ<\NmGTl`mI$KMGX;'tUQON<GQ=Q7
|
||||
q>GWu%52>/=1.MS^AbbE(t")'AHZG>-&_]ZSL-2(7hBi))Se>\E+:H%G\_k:Il'D57M(73B)+*D+p`p8
|
||||
4V7ho.eSEY47+36N;"tI#r90BC#+k6O!&6He!:Y50\dgYO4H6H@AP$'*/NH@?q(R]FE5N8Y"0Er&<=<a
|
||||
)h,\`+;eub&HqQ<<Wp7Z;,.810]N-J1h]l-`oo5%d6C+F<DMk:CU==hkuNif(fhp/.,C],HOlnX6=>is
|
||||
fIo$sYKqBIPgT4$r5.06lh$;`7UWp-Ds7jeTJRl!(\S,$@#lH**5YWkR'd&Kkk;gU1a)[*3u(\\?Eima
|
||||
LaR_rjufaZCDL8-3=V2/KCMpD=L+!R?FWWsa&j3F#g<R+i=5b^.'8T!$5uk'0S85I!j9fFZm0JPZ[P=X
|
||||
IB9-mN=tkNL=%;_,mYsA3I5Ku,j0IViQ]E?[s,sXEAq$F34fmCi1!C*K-n7+LNRd>MJ]q;@[PQfB10A=
|
||||
/*aB4G92eWP`EE_[Z23@6fTr,^X/op*mD%:g>9DKLRr2ejP5K&f+kp(2n/]H2UAV_Gh@6M'o_!]W:Z`O
|
||||
B:lSTCYi5c)2r_A!\MtcXg&q:gSma07p6V[^U_#-ID]L[&88p"&O,7FE6E1pN/s@d95]qM^*J'HnNd\*
|
||||
I!HqhKb<D-;9g$9K1e8-0gdk5#N^)f0+UXZ[gr2q"1iEl;oeZhO65edmC[C?2?!Lf1VR7A5qCIE4Y)UK
|
||||
Q\DD@rE(%<VAEQd`:Is36]KeQrdF8G:Wb*;nSA;5-2$3</.X)\ZWT'5a4FPP>QS\\m:s)(R03k@lGT,&
|
||||
NW-^Z!C$N&G4u'J!C'&u&A8@E80smc+qH"0N$>YY[UWfrd#h^_[U8Ql>mGW0^!L&qL7s[>l]Nps\,qCG
|
||||
38V$Icnr.aB00qW_ZBT[etRJofeD$mr9AB:;a$j4A:MKtE\qO5ZD@J+clm3ZWC.+g$IX78jpNt)&_m!k
|
||||
r&T)T%!WjKg6<R3#2PH@GU2N>e@lngO,+<YLR*fBTLAW0$S5Pq[qH6;?S1g`]gkNbS.m>W2PTuC#LrD&
|
||||
ko31W6\4.!6+:u!1A3[)23(/.(o_rj3h5r)n3GQ+:lmiOl0B%mh,9HVK[n([N".+Xn%tbeq"uMjo?SAd
|
||||
4/E[3@'5$()q_d7ENFs*oiMgpL@r2[`.k[_IHI9+LsnKAe@,(:@/CidQYc'J2a1j.d(WQ!>Pi9O+u[A!
|
||||
Qq:[=MW)A*&E\j0T(Wo(^F<4%rge.3`1)*A=:oK>WDJTr<3?t@NHOpC3jcft/?X0^l9G'=H,u4#iba%=
|
||||
Yp'[r'ldVg&T@%j6_C_,[b'bQ]DkA;[<_XE[Z>U6Li$rgilFTE*%PJc'9QP$.7ch-07uW>7Ju9ik>CUX
|
||||
ECK4R<lFt`fZ9t3er+gUL^f\Q&K?_?a(>+qp@DaACnk:o9X[]8U9B@^d:^a/PehQ5o@^+J,'Jtf?a1f(
|
||||
pYTOmE(-75DLW[3D(#ojg#CB8kJ<IDWAA(L21lDBhAsi-E.0Y)CYe@,)"agaXjT!BT,"e8XOP'bK=NDL
|
||||
=Os(gZApj;[L8A<P8"q*:LU>?oZM*HM>(oY+29jbdn;hZ\NKfo_UoRC>$i2I8aQdEb_/O#W+>3JHS6d\
|
||||
>7sC?dT<J)@LHQJ]p[JM5]9c3PjodjNL>25[!nZl3igEN?]f7GCHn,*E35UHq-m<t4n`RS*g&TnHCK8$
|
||||
(;iiOM;CBrD!$`BQPK+JpC?Z*]pC5akVi-J\@#._LMVa*VOc:hLf(JNWRE"P)FO/:Z]F.N&^E)UZFI].
|
||||
^KlRA@Pj1K?]NS@=du[c\ptE9P)nNE?cUmO1p#]gm]ZhW"Vu9Z4+=`"E=D5j`nRg7hM=7ahP2.cK)H-o
|
||||
@SYsg-FO#/rEGc5#q\.Np+WrKXgU7Yqs;B@rIpu\;j+NWCr/A4WpsDjK?lTn.Oq,^U>Sc'(WBZ%s70Np
|
||||
UV6%fi0aU&moB?lda.(QcNTBZ_\7T1"N4Njb$+L`DqHLfPu`i,U3+V8i^%aZg$/+8(.j\h110o9p2%g5
|
||||
Il5%uB9.kC^Vh*dQ9Xc_`eqM9kH.)toLK\/l0B$j$dtdUJ45P[6dKSO"hTKp^tS9R'`iRKid0SJeP92q
|
||||
@TsiN[^<Y[(Y2e`XMl5Ef\t#*Dg,42fC\ch\6j*]6:_[gipc>Y6-d=PNYSiR<lhmd2op*(ooZV#R<Z_*
|
||||
He@9^e[#J:.3d6#[+K$i]XG+*br`D52=8$)ikZ(IfsKmNZjIBF+g$i_@lu9m4>.]7'?9f`(LkG[*lL_P
|
||||
N,K0m<SR:L=#OatP+R<q[>:8SCWqm$Y:>RLnJ-!\+oi,ln<@bf,S]8@53'c+5Pr7$^=D[:Yp'/YhTRZ%
|
||||
hD7Wt9X@p+_3=@>CeTf0fs=DT`hBF`2h>HWf[4*m:em(tfi28`+oo%b"M.b>^imf_^Q#G+]X\15'l,:[
|
||||
!!mAZ^Ca9;`'FDfru4uA@)GcJ"Q2SiNA-6+X?V:\HYZEu(*4nf,-<Ra.">ScSe$:/o[WdL?Coc$Si=gC
|
||||
qm-g4^Z_paJnNI%6.C%.4T_9`_Z&f5M?)k@L]FADd(\eKFL+G(h7R5<@hA^h+X-o^&gIB<4>#UOf6l6[
|
||||
HETY>s#SfagHc]aM`YZ\Yu5ccP2q%O*"_T/*<FgiapBeu<Plbg?LKbq8A)&c`=n%-#g'X`A%f!O_PcF@
|
||||
A#7JSEZNII_5=3HZ>^^V[/*>`9@6&o%4WN`7hkS:^$,o^k<qAABD+Hm^V_jKPiQmJkaB0FR5rR`bT8T$
|
||||
40'BDGS(eO;#Sg*1VHk]RD'$S9Ke!rV>eUnHZ8u0=6j@0jI&00LuTX?%j(;c*%cGJJYKotm[;$IiU6<N
|
||||
o$$`+;$fAu"`O7O-r/+4T9$\HplbLo%slanq=TC=F:8!]8)]j>3[4X]nSFALV"Yr0_H5]'WCN-NHq?D4
|
||||
kLL*#aF@on0e^6]ht)6G[Y.D:L^o1?F#<Zg7Z-p]3fnh^-1iT6GgR<Aa-e&p3kka>1-Sg8IpmU(J`2+u
|
||||
U8KeuG[:RO=Rp[+:iR2@^k8G:g_4pY]"duR[8?\H?[7',HNB.r4<6ppSk^DS!lAS4)7/g`A`r(IT9u4#
|
||||
A,'H>ibPoJm.Fra*3WlHb004<&%boc_2]@"2skKe5Q4C2-efL7@fBcZB_M\MCOS"u<O1O[aM<`q5/3JL
|
||||
rAXEujFa8!>e`I9Sg`ZPpNKIKda:8TVl#+5fp]\QeI'!\_+,#ngg,E<9'[AV*n1pYo_ta%Kac0RA3=.j
|
||||
M>=%>qG%WH4FPq@698jOR\0S_>Lq!3>9?Je,O5lbqCV$p-0ljf]cG$>A71HblBh'U9#s'WnpIhbIgFO?
|
||||
cd[&Y-c\-Kf&3J5WmDuh(C^u*$P)\TjI:Y?;2+_fUQC$i$5m3f5nl]Ug.PJM+q!Q_FF&lg1gAYjla2sP
|
||||
8XpOf+6rAb)37*Fh9_q6fnlZfO,s;?oF\JAXk5hE@f+N0m6`D4QW@oUXk5hE@XDh.p<nic6S$`S<idsV
|
||||
=?j0%I;-[sdYd]b/(P65=?j0%rQpGp6S$^uX]Sq6Z+dRhHQ?:p^9jUTRQ=^-5."dGBuTeoMX<b]g9IWP
|
||||
(#)A3[bnH$F8=?8LVPX]T)XV7`?3EU@\.TO`[M-_n4S*5@\+`'_Ilc<=pplnZu4-EId!"`i5:u[HbJo0
|
||||
1]*iEPt_L8ef)m:[`S0\;[s<Vh`OUm!pPq:i%t@-Y(#E(+.rSO3@*gW&'1RB4'rjq5^j_YoELK]a0)6i
|
||||
I.#(`rkM,\bF]^BoeU^1-u?<<Z(W`0cfX\:[3k)'N)c2JR/JP"jc1'BnL\9=c&pc'/oRC94j1L)NE2Xl
|
||||
>sQmGlpIMm^h=?<.Rol+q7+H'BXj6"YsrVhdfl2>39o!#'=2BC!PREd)rFc&Gn5c03/Tp^BQs'`S%&Pd
|
||||
lGM0`a<G5+(4NZ_27*%lhfa/'=)3'5-0R+BoKehYm="2Vr4AMSoD?[)$_20j,+eKFI_6!-jUB,m4OP<L
|
||||
Q4s,%O0:>"a3aC969cQoPqmIOiCq'lNF-g$3OC9`9EF7XB:4<6dE6OS(%:5k/W%5fC3>*&R??#0IsW4u
|
||||
km\]dn<ZV=DKL=4?&W,K-k+t$V#R/i7I-1d^/2eo;W%U@TJ&pjRQus`qFd'JKP*e!WXJIU?a2C3H:P%-
|
||||
ek#;g8;AJf8ZdO(2JJ*Q$[M%SfRs8S%%AsE>DLJo7iXR.lp'L6$,EhQ@t2P@\*%$gr!6&cM=tGc2,:kK
|
||||
kct4RU*%E&&;L^^+#_"0!fp%mnn\3[A(-pE9c-M8#No4B0t.HSa!M@in^"\(,S'\@q+htgiLUS4KXm/'
|
||||
rrkN\2]_Gc)i1D[80u5nS6P.hmnTN!jNl+Ql]ekg>VoqJc=[AF>YB#iP`IX5c03&iqd$PArCZ"+_oCI"
|
||||
+qDB2%aH'/b76cX^[(-,\c"/8Ji\A4>drTZg4\?@?VuKUF[f(j4G?]*/9=SP=3O4S>)2]LK3$V%@7F=1
|
||||
9Q"8j5o_+Ujh(Nn0@SYlhO(9BW5m`^/?L+1Q'NUWY+&Lhe@8La9e/1egD(IoekT@j9="h6P87sDb.4@"
|
||||
,R`Bp.e5mK,fZIIhs!"i%iG@2*.d:&_oXfdIcB+D:QH2Q#em(S*ET-[i4=\R.\88#,EDH!UFW[qWDftN
|
||||
_HpdqJ#5BiWcVfe"^eBA#'iK)28@MDX.=CtY7D&5f](CNhL6L3],NtcTiX?']&eeFWlU2g5jH2YHV!H#
|
||||
fmZG5*u$^hrbT2JUKb>>DOl"MNpIm<[P7^HF1r%6n?sbj'[]&&"^Sj]Z08B9Ph']pA&-`c\8Xic:IJd;
|
||||
#:lSPoB&rt!?M[6E,dQ[a:2P*G:%U8`*#4BV[L-\^8\o[fRo\klUC8F^lRh*\S1^N/`g(f%buag(#O/e
|
||||
iirPX^,+g$c$PrpUY5qnH7-_5II2r"R_Grp?CTuL\RB@dB/q^LJj2RPP'b\>.*6`&TUookUA)Hs-?1Rh
|
||||
Z?buLb=#*n.h[(J_U8cO[6J,'<^$J>j`qEdo.W#=o.[O)cQdMG$_E-ILs/"1]\AHZRoTRgl;qXL4b%E2
|
||||
g<@<:_Xkht;'3^Ia=29k10R=q*^nt4#`#=_'e=Uj6bi8!rda*U]]S6%AnY"q@Aee))s/Y[+tPf%$p0`Z
|
||||
"%559e)K"fVfL4s`p?Z"o#M0c]^/VCl.E*C8b\[<)sWc\b.5kSREit1\Ug:aP+<5Gn`9Q/5i=H)ggBor
|
||||
Z;]>@*HoS%$ThoJ?$8hMJt4Z=eqZ0_HO%Q(eZRKF?KD?T00<`d2Sr4/#&0uAo1PM,hm%8&lN;be8q)&\
|
||||
L>s<Z7l@qV6i$:BF%OeIhO=3dkDk_>YBq*-$LGA^n&lZp=Xu=$6<5)U]MV%/D'pTIPk^]Rn_J]8f:9%2
|
||||
mij<$e8Pk8eBeVBe9ZOiUDafY;&f%^A;`@o]D:;6#ti&R'p[m?)K)762'_T[2.,oD?h9$jXblQZm$.il
|
||||
D^:\CSc=5F=(2I$Tfmu#/UaHgWkI<Mf^`kQceXoCn@VqkU(J@.NX4dQ[8M*=C5lph"P/'KWbt7Z[9'*#
|
||||
&N/%,8pgm;(bt'0#WqcQXG=j%)nIW5NHJR0Z3kJt9).54"aN@FX`Mn(1hKF$p.`+HcJC'T[sTr\c]td&
|
||||
?cCe^,s_FeTlKE]a*)Nkin%[l.a46X(CdoV+qDFqabtYA7CYj_]b4c?Po,*S3>T?PN2Na`EQ=gX`T`rI
|
||||
#U8HZT++#OUW'f`f&4L!.hQ?s=(6tGkNaWX>'hR!ro6bpJ+!S/-Wt_rra(&+@CD"BB!_E4QKEBM>kid)
|
||||
D\92bs*F\(a4Yq?`S9L1EM$.O?R:%5eaPD(^9A9Zj=KXHdMI(LJ"EimZbZ82qtmnfbMAAFrQr]jK-u"V
|
||||
pQq`6EUneBTCMhtfQAt=A*+3S>q+T*8(uE*pOYP,Ar*os<E%_.mB2T.b^D1`43U_A_Q?Upl&A^Sq>-TI
|
||||
.t1!KIC%qIZXqTilk$5R&@KlrE"<\GD?;$e7J4jjO^>`qnI9^f(VZ@:/?=)]4c<)6"i<uA-(S(q1cN&`
|
||||
FNC&>M&Xj$9%g>'.%'%lX>=3JGZWX;)SrV0QetQfrUdUZIGo@NZ:,)VS]0:!but:,,A:9eZSUo]XVRpr
|
||||
?P[\bY4qe+RdjDK"FENN'^(;MS:H'VrFu>G2_ugg?d4&ALjJ\i-E$&\&B8=W;(G,9?\/dN]qCP&$4<e'
|
||||
2go\$R/cNoN3Hi'pgW8B*&>CoJ&@EeC][1?L>NarR2LJ.k)gNcB'#,Bd3*:fMW+B07qRM=@n,dgm/Y7e
|
||||
.J8,2N&^o%AB)LY4(LfO1It3-i@cW9HuI(7^Ijo&O!kb0Do`>WUK3><1.+*Ub`GG]),cPOo9q>Vi.OR]
|
||||
bQolXkd=H10@H'pDiXq!K(G*K-`MUuHt6hgbh^VOb!*QkOM,nBD[OHbaV4"Q;,Ec-!f-?5jBfPZGai%d
|
||||
kHd[C$MHDhL8.Tr7]KgI%d1NuP.h81W^f!rJ+-LJD)CoISUUAt5u+o5)^C3W;XsQYXQTWG[g+q,>`+"+
|
||||
%70<G1?.m04TC92r:S(n,Ms>d,d(8B`q,htoPs[9?M9B;2kLCP,OA4@[:[9:g?i8`^c^=q29=t-4Y@f*
|
||||
EF.AUR`Tbs;bMiDF_&f6fL*)nNkW9[mX3-$O8/.ZcHi)&]bYb@o"2_k`-9/QpR(TrSC(]V2uƪ#>;
|
||||
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<CZ,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
|
274
images/satrs-structure/satrs-structure.graphml
Normal file
274
images/satrs-structure/satrs-structure.graphml
Normal file
@ -0,0 +1,274 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.23.2-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0" xml:space="preserve"/>
|
||||
<node id="n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="115.8239999999999" width="93.73760000000004" x="680.0096000000001" y="128.20672000000008"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.060546875" x="4.0" xml:space="preserve" y="7.633644259571071">spacepackets<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.5" nodeRatioY="-0.4340927246548981" offsetX="4.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="38.232" width="93.73760000000004" x="680.0096000000001" y="253.37632000000008"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="30.056640625" x="31.840479687499965" xml:space="preserve" y="10.131624999999985">cfdp<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="163.40159999999997" width="284.4319999999997" x="783.8771200000003" y="128.20672000000008"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.896484375" x="12.842478099131654" xml:space="preserve" y="10.580257051368562">sat-rs core<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.45484868756282143" nodeRatioY="-0.4352499788780002" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="75.46560000000002" width="82.73087999999973" x="985.5782400000003" y="128.20672000000008"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.91796875" x="5.868388937197551" xml:space="preserve" y="7.831944999999905">HAL<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.42906652344085205" nodeRatioY="-0.39621834319213123" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="57.03359999999975" x="998.4268800000001" y="165.02472000000006"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="41.763671875" x="7.634964062499762" xml:space="preserve" y="1.015625">TCP/IP<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000003" y="236.42112000000003"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.63671875" x="17.56564062500013" xml:space="preserve" y="1.015625">PUS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="212.21056000000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.62890625" x="9.069546875000015" xml:space="preserve" y="1.015625">Events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n7">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999995" y="260.6316800000001"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="40.708984375" x="19.796867812500068" xml:space="preserve" y="1.015625">Power<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n8">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000003" y="163.78943999999998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="42.947265625" x="9.910367187500128" xml:space="preserve" y="1.015625">Modes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n9">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4944" y="164.06207999999998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="45.232421875" x="8.767789062500015" xml:space="preserve" y="1.7148249999999905">CCSDS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n10">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4943999999999" y="198.76255999999998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="35.1953125" x="13.786343750000128" xml:space="preserve" y="1.7148249999999905">ECSS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n11">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="93.73760000000004" x="867.20384" y="163.78943999999998"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.62890625" x="20.05434687500008" xml:space="preserve" y="1.015625">Thermal<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n12">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="188.43504"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="89.494140625" x="2.1217296874999647" xml:space="preserve" y="1.015625">Housekeeping<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n13">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="128.20672000000008"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="63.765625" x="22.802665047002165" xml:space="preserve" y="16.015624999999986">sat-rs MIB<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n14">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="212.21056000000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="73.22265625" x="10.257471875000078" xml:space="preserve" y="1.015625">Parameters<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n15">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="80.30272000000025" x="969.0342399999994" y="236.42112000000003"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.25390625" x="25.52440687500018" xml:space="preserve" y="1.015625">Pool<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n16">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="260.6316800000001"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.392578125" x="28.172510937499965" xml:space="preserve" y="1.015625">TMTC<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n17">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000002" y="260.6316800000001"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="32.01953125" x="15.374234375000242" xml:space="preserve" y="1.015625">FDIR<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n18">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="184.90752000000006"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.505859375" x="18.932547859502165" xml:space="preserve" y="16.015625">sat-rs Book<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n19">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999997" y="212.21056000000002"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.22265625" x="5.040031875000068" xml:space="preserve" y="1.015625">Subsystem<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n20">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="236.42112000000003"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.044921875" x="22.84633906250008" xml:space="preserve" y="1.015625">Actions<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n21">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="188.0"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="43.404296875" x="9.681851562500015" xml:space="preserve" y="1.015625">Health<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n22">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="241.60832000000005"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="93.701171875" x="7.834891609502165" xml:space="preserve" y="16.015625">sat-rs Example<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
607
images/satrs-structure/satrs-structure.pdf
Normal file
607
images/satrs-structure/satrs-structure.pdf
Normal file
@ -0,0 +1,607 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD>
|
||||
1 0 obj
|
||||
<<
|
||||
/Title ()
|
||||
/Author ()
|
||||
/Subject ()
|
||||
/Keywords ()
|
||||
/Creator (yExport 1.5)
|
||||
/Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5)
|
||||
/CreationDate (D:20240129115539+01'00')
|
||||
/ModDate (D:20240129115539+01'00')
|
||||
/Trapped /False
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 3 0 R
|
||||
/ViewerPreferences 4 0 R
|
||||
/OpenAction [5 0 R /Fit]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/FitWindow true
|
||||
/CenterWindow false
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Parent 3 0 R
|
||||
/Type /Page
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Length 7 0 R
|
||||
/Filter [/ASCII85Decode /FlateDecode]
|
||||
>>
|
||||
stream
|
||||
GauF[bH</%%P0O,YLl(tZJLY97g9BH@!gh:YlW`<fOajJs+$:F5t\]Qi,fnlDGR9B.l3M[OVCAa<;r/S
|
||||
s1ZqITDp7Q@,C;HT<I_@J,N`(]c[1N=0M5I_*<tQb0OG`:]1.`YJ:(g^]';W49+\tp[`n6l[R)bs5j:V
|
||||
pn[ob^]2Kls82iohuAuAh;A8"+.jVS:]D/Zl]X6!A,i@L-gaHos1]u[s/G>al/DYSN;a2iOfS98U1f!=
|
||||
RtII8p?rM\oeCm\q;md^YJ<A_CRW1frY*?Ds8)&2s0">ss1^_p*lS)&qPUOsiH_9m#Q9^5NV,E+^N*Jl
|
||||
8q&0LBDj^"iuc=<4Ub5JJpdP2a7K?-l[S`2fFOVocO[[<D<G96I-"CX%@b,Jp"*mN[0J)=[_BaV^LdPI
|
||||
"/ntfZG4-R17UCFrYfSPRMdMLT\:DLc`ZqVQ.*<n258NM17s!oB[=t2CB8X+G5dp>kkRK:B!:],iKhj%
|
||||
Ldo7**tfH;*ap-_!Id4NBNTbrOjEl.&l@_sqc=lT"]Vq8=5^YUM)BjU:Dh@KmRI3uh!K<?rLBRdJ0#%&
|
||||
R\J!AF.p!Es.><Hr6"FmUPRcD!cLt]=oYs._FCke,E?oZ>"^*_Uf%Fi9f-[Ehd'ni7%huWD>pDG-p4V-
|
||||
5Bo*ri)T;blTk<pib6cSl2OHO54h@sd/WKhoPaW6DK<\e-M]@]=.Be*AKM7_4+#WCNVHf"_ak^\*PZ1i
|
||||
.\*J%S\L"c5p&SVXHJ2pgN%:[-fEKBQef^rl-Q9]A/i\IH&4@`jL)?\EQD!5dC[cC@XM3S#2+6U;1\6!
|
||||
UjSJ8F1<3$i)V:J'rVQ3HQ^a`P\@Ie-R*RmA@^>WUs%83ao.''CA[>!?bRV!1nKt,Zh)Dln%_(*e!X`4
|
||||
\bq$GHf-T-rtF":`ukh-p)@S?E(7GK^ej4\GKq@$1A7C?d[6Y@^F?H9-!m^;oDO'YjE+7.T_[qWEC2U<
|
||||
+'Z*h-7XSXc)3]T/=t=[e'q*>GA4Ma<u7[UHUO:qrjp&FB[j&EDA1?h__SXmrDug0Z%o(3qGl/!HbMIl
|
||||
bLD89nGD/,,FOdic3:CF8i8$5&JS+IH?2n-E#!VZg8#+gY;U(][Q)3pc'7Peh=do<o;h/_H6a1=(EVX6
|
||||
]fDborO^Ni?GF(m:$N=uOS@@LP`J;9dp)@>kRcVdQ1V,OTR(_/f-ig]V)F"@p^NaOj8#'ESS/Yt78:+[
|
||||
[qEkMeZ_7Wdd$G:EZrYDi+Y!.2\U?2`cmWRCr*H=cqn(6/#8ldII,P"FA^'IfVc.1KRtsI^%_qDcSk(^
|
||||
Brf*ENtS6X*p''_I0_r/=1mei"3M)=T%<;d#=H^ac5/\7FPLQ-/5rD/CN__S380K=qt7L)`XEmP2#;`g
|
||||
P"s5!3N\]3Ldg&p-L.+d`-7mY$k:M)I%`CiXO`s*a73b'S<;W>BurAf3)t"HKZl!m@.0+.8d#+KGh+f+
|
||||
'pRCnV%Zgt^.**s$`1Qar6j@Qjb?B0NMP%'VtEp`_t>Qo-u1YT4L,B"_IslmHNQmi/^p=deDCp?Fir4@
|
||||
`Ukc2UG[h>?+>Vhn'g30M#J2o>$)Rpl`k>BXB00n;[pL)_9It?O//tK\rs09S19EQHjlR-Z,gEdS]9?C
|
||||
X(3@iEb!(,4fE?1N/o'F0?\e+9`()sI8D"Oo*:Q(EhAC;dG((O9o4L'2KW_]_m6Mi6EBJASIUgK$iFZb
|
||||
[MEV3>g@PkF3o*r0NinUUBO88IhsOKUP[PjGrP-MFBHORhgYfpTX09&'E*(M:?2-<6gl<-Yc"n+%anQ>
|
||||
[-2p@f]\$2"6g9LQFc!?7l-7`q9gMYUD>.kR;$lbqV]R=)nmqkmCNe;r?_T`LAGdEj=M=4LB,O$d!
|
||||
XA@2)cf"%=DoXk%bM.m)eO!<,%PC=J-!6s/_.&/CS%lf0-(2"Ar<6JUn70,`%!;7%%NZV)rH@@KVqLAl
|
||||
C[I_%CfsPgY>DYSja$BXU&EuL7Cua*&$<c;&!,mj/kPgtL3n/1L:$51dr52goih/20C.'thVH+#j5!J;
|
||||
.t<=ZOg=g?`AIsMWo<>Js&\\+a1D+]*VAifrR%Rn_@*\_)s?;HU]"pPV[d[&UJCh5E.bi3Dh\^;hlWT_
|
||||
^G.[:I*ic,0Z1?-rlu<M/Zt+Kh-n3FTHtO3BNSLI#)>$V\).$M*P+tu^roO#U"`(Ff$T+"[9)?@-2nP=
|
||||
kNUNY-/1]TUEqXHLi1kOP;98k@Dn*`iBXE`U("/<>Ep9UIh,^f*8NL+=8u^tP\R("GZTp]S!o%3gF%/_
|
||||
.c13DojDKYduY*oS>g6#XE@h*Q!qpA2<sr*+U=)ST6"6_O(sq%9C=4W4Zr;\KB;25B,,,5GZTH$b;gX1
|
||||
U#(HH(#P:#'u.=.Sa$f``;AjUjJ,SQ7S:kMl/6`Y;i+PM;\7g9HbQH?'L@k*'c=ZDokK2Q@n8+-f3@Q^
|
||||
NRdggj(2AYmVV%,&q2T>)n!34XDSR4]3u/7IJU#d#mJ;%iQK0/n8mU$^5i)oEkO'`GFFlVrH1iqUJ^@H
|
||||
<kKkB`-7"&HMjckW#jde"iY9IfF5Ho#9U-YekDQ`2Y;VWXVQ4a26g6TXr]E1_iu+f^2m!:b2+CT9O$(A
|
||||
IWFoB_1f6Mn*31d2MV4eP^2b3VcCGS=0`(=LsOb\Q0U0<X4jNLl!6;in*4:aPV5'`gXJt+gq>5YmS3Kn
|
||||
5-]H9:N*U`RT4*N*?cdgNc[ii/R0MK@h6/Ho:aKh'og_\%&u2O6h@H%,d33a<S;bdhGoF*HG0s(/'%:b
|
||||
m*0r<6d\6pY&&QD9oi@Y7J*aShEu4nGNEpdn*4<7Q3Wnu@<:'gs#m<%D'A?eh.2Wqr6d^YqL`h04R"]S
|
||||
C!Bo[klhn5'lV4>^&5$Y<tE`*DVior&p;CiB,(NEG?'jIloJdpj,i>==d.D5e7BjO[-eq"K$AUDSfDN+
|
||||
[cW3#OfalZbpld:8RJ5g8+4S\<><.B(cpW$/TEA;GNIJjX-UkeoWB72r<qZGQ@60=GL_lFFVnC+eAK0]
|
||||
<73oSZ9U5kM\8O^r1KCbIrL_"D'MqpOPZ6If?0-<?29u^Y5Go74oJIH?.BU6YG]-Q$i=V3F[\\_kht<_
|
||||
;i*Qi.7ipMl0I2kA,!-pI!^0@p*JD/U,T2[H<2\D*%$m+XbABPd*W8tGImFf18]-]lClnTf2s,bAZm$^
|
||||
LW@rh]5eVX*jP=T04e9;l$V*rX1I'j1[jVn;*I+uP']m`GT4o=>NIT-X>h5*P+5/:Y+1+9Y$V$#Np#-o
|
||||
r;<<pN8kch-N4<f1'/KHQHLj3_mWigKnlk<Zo"EKH*7Z/BIt4]T._2OeNNXCpAR3"k_$6^s6sFmK9+F7
|
||||
o*7JYiIg:_%snUY)LV,6If.)i3@j1e#Hc_gmu:LCXj)(6bX1Na<,qs-_.SPIcoV/7Q]:$@G7U0urh2K'
|
||||
r57AY-7XN.p(+8Aa$6pP*riGp!p;?/N:j9L7[JQ$_["`H5O;PgkT<!e*rjTLqhL3r+534)5O>\"grAdp
|
||||
2_Yd#l-VKi@@%/b1=>3-WjQpHUOUqB*^\6CpkR-<oFG=,".AAD0gtaHHhS,>IFl+"l^31S,N_Q,nDJcn
|
||||
_qoc:g[r(:i:[\.oj.8bhWr3Hlk-:nIqY',raD.)8H7$sgASojs!]2`Bp1^Tqd>cEOaYjINN:J#k</d$
|
||||
LFN31:\]<l'JiBI?2LqgZV:p3B)3FohU\Hpk=EnC[(O$n^cscT2E9"rgM^rLL(_%?ft8ntolTcYU;oS.
|
||||
YV2&"Rc\dHnaY<')ZSm`nqiodrqn,JrI7#n"#Dd@<[]1;YVIur(`j8J46Ai7-6@6POJ7!,4@XNfd4C=`
|
||||
ott>OHJ*nFGuOjIc+V*ebi>X\E@W9OY\ZU0Z%dWgA7Pg@7`L/,DR;?Z#1:";1@/'W+^PP)B_4M%H_77M
|
||||
F48bkrA%no@-h-LbuT/;TI^V#GP\rDCkd+.TaV>YWb"!SBKBE#S!?35A8pO0'Rus6*jHs"8R^5]*M]%3
|
||||
jl)tm<o:%(U_:!mB[Me(qmFGgUX6VYUf,.47Dj.kHp>F;\SY%YQ$Sb^NR*?_7S\HeDr*kXPn3FkW1\W$
|
||||
XkdP<&"p%3fhN?cCB[[9bbNK'l+=jn#J5Hh72`jc:qAD;^u(Zm<q*/LhH;!,]4YH?hV;U8PU#<j0d@q,
|
||||
6D&a@:Ic9?L'MuUD7%HP1Tbg3UI^Ck)rZ;*bt@e27L&'0m8/kfE?-iaDq?HtqN\78O_YCO[qE_DCDREp
|
||||
K5VX0>"^IHn^9)JaLM*Xq-hDulNlcD2Oa-W7b_%":,^fRp?2Hs2O_T@+3V/f"N#X1S8DMiLd+MM0]K%c
|
||||
Lh\=m!]r?th&\Fk`$QcJjqeRRqN";U.6]ggl`e[p]7]Y,$Ih4ZrD:u4AmG4qTJGcb3i]p/+l(^W]a;];
|
||||
mrjc!lW;2ZhBR'=pD8Wq_e@Q"O`j4`7>GdGL,%[UN2XB;f\'WZf4,SVLLJT"\CX)dl:LDj(n//@d5nPr
|
||||
br8AV1U?S)37cbY/$oJ/Ke`#,m]f+IJ5-ci$9a#rXWA2K(=mAehTQa9MXMlp*uQ4a_hCs4UY=AU?%J^_
|
||||
'#!%6?1pRWETiL3Dbj>)gkcUHiASIHS'bPENTCbp7ppfgN8uP0gMQ(kr%.`^L7X+T:hOl-%A:P)HEl+Z
|
||||
@AECG(<9h]3f%#m4H\Xs_ru(8[s2!A)sI)V*qE#t2APs6NouTToRDYhHh%"A5Q)Vq`X<FKT]\+c/H/P?
|
||||
+q8N]$<3SDGVC??/@_E6kdqj`J'8.-SCi=F2ILWofUkF)2Z8JBjSo-+``C@q@Z$Z$Rhnp[[D7=[s.D[r
|
||||
duMR+!sUM]Yh.9`N"q"GNf96;`_EfMo1&5G"I(<Eg7d_HKe6l7<m"N*aCWS8@16+RAoaQY$_>I#I$]fb
|
||||
0CP%!8Gru8dqDMB(B9XeLZCN^U/GG27DZo5qE82MYsHWf&c=i&_,`";S;lo.liFLe.pJW9B[8G>?Xi2.
|
||||
R;F.d&ZgFb[je"q'9%9dG>mZ^NudC%4`]TtGKfiD^R8LPq0.70nmCM4jZVV7k5FsQcUXa2NDshR%e3h%
|
||||
8\Z_^W^f!r\YITG+j&cZ%u.40\3G**-['4uk<ErMGjI6Hbr%L*e(m-d_^cImE)*Fo:iTXW^<toU3mEYA
|
||||
<Wk[YH`NseH+Rt&P4Kaul;9#N`=*)PUAR_XRt'%$d=5/Y>4tKbXT!s5CGanJO]"8#*F*eJQ)IiUK%)ib
|
||||
q;MgcM[+_RX;L%U#KOBKc8Q8;T9&q]OjL>?%+X8rcAqPr@N<+`o-PZ3f6>uoKAL86815l=/;\,NN;Ig)
|
||||
(B%;`WSSjlZ1lj]gde-jl)f2O^1H*!UgIC\).,$7s3Fa@>ST8]IO6<f>NsEN&)jb(p&$3:LM&qSC7^jD
|
||||
/J9$?b5CG=Q'(rP2M`OP<=^OEB7:?=@&61#H'kBHC5f%N%&A=)SFN[i3bm2?l`fJm=XGAcXTQlr`cpA)
|
||||
(m=(Vr)fW6I>83^N`PQ3FeVIRFDO+@%"VF+#=Yum!8<AU@:c_CQ\b!k0&!Y1Ue>>OTFW0Y]@_W/HJ:Oa
|
||||
6^889k7pKM3F)^4MH5W\>/6Bp*mBq*7r1.@hd[I!f_iOF6qQ^ZH*Kc5GJ.1*\tih65UCl'4'aD;g0n[>
|
||||
c(31@1c@_D0`_^cj(QCBYXLaWrtA`t?aYO54rZZ6<ce+%pbB:RLId4YA?fA`d6F1&7d,2BhHKc@k&b>(
|
||||
")"C-L\XF9DY:^)S\CVg%Db[6mab&HkAT?@crq<r.HKE\XH;]=-4`52pq^'&jRP<:(eZWQBhnc$K&M0!
|
||||
VB=;_N:J)\`Z-cB@E1Bn`*Z*i(X+L%N3Uo)R8]D#qnE5QXNDV;V!qngXYe26+sol<<9t)M=".]OG^\lo
|
||||
dsW``cPIZ<)CJ8a%HET8faj9;;MR,VV86JIgWYbRCi.ru>VMZ]+ed*ml,eED*$P][Ln/bB*/8&i]Z.Xg
|
||||
_hea_No#ZD*k\t4XS7s6DiAgsgE'O/brV^\ODB]*iUskJA9rW+p18:coK+bgoI%BB#F=8$hCp%Q(A=#'
|
||||
*^okU.N7ECr^[>PBUAF#NoAA;Yk=cVdF\p4C8_\dc3g=k7QocH7M9Q=g5T"XZ]_;nd&NZ%agkKc&lC?%
|
||||
L:$\#(GR6s3J4(e\gS$cF(=S2?T<+%[k__Zho[a?:=s/f@HHeLV7"UKVHqMMo-X@5f'2uVH&s8[Iu!(3
|
||||
N\b3\0S]e#<edp*8IRbOQ,>F4c\)(hgIn25S2dk4WKPf+5)jt9HSI9-WF&ot6RO9<R&rAok1$`q)X>J1
|
||||
_E;Jm372V_c6YndHjlQRYoI=W,]IquY>pO;0r.&(TT'W2Ipsru#Fd>j.$>N0T3Q$>X.?g%gh`)uEoFHH
|
||||
F^GLuShr9mU[RCOn^!1@JapB,j6FR;$J.LH.l/sd4!If#m2BbF6Yk]fb\@^Kq"SZPNF+&T5&nrQqC/C&
|
||||
#5s$8k(C6X14"qIs"d#2Go+7\#@gQik</d$p&+F#a-2a0<.6R3g?,=Bg/jN>'!_H<\sFJ(k!H,o_3cKN
|
||||
p1A;Tcg"Jj=rVPJnZ.D/'/q(WV>:\2Bd_):UjZnpa3h1E(I$U>27KEto)-El@t4Ll%n=_O:J-tU,h=d=
|
||||
;/@AWYWZ\ob!n0Kgp8@WQU[I+@FAnU]61"fP4Xs@__$W+K:!7E%]#W(1A1"%5Kp'=0"N^pfR8S]f:s!o
|
||||
EkQ3tW$s\AqP,Cp[DP[k%Q;R7_TJQY/_a(NTlna`TeB9/9*Tg$ZDcOB99ePpN`h7"gm:"QFKiHXdt0uQ
|
||||
Vc;kQ0V[[C6s6<@I>OkO3Zg[qik[/;h[d`l=i*XplumpT&iKQuZk@,6ZXjfPRl6=A"ekE3%\EUd/E'6(
|
||||
jK`++KJHNg93=qrXROfud]`?#,#)/)Ek`i]L:LU(KZ0M!-8AJtq\$n7],tB(Y^?a>h8>R-4RFe/6F.9<
|
||||
K]k(+krki%<jpa*[,<L%o2orhXJSZTPb-^VLS,Q6M435KkkR^GY-<;1ROHMS,q=>En0L2pP.j?3kn!V'
|
||||
Kf3N;&fOL>&g])@o-Hd@q@`,T\\jafX:3b"B9da)?77`qX#7N\9?N"9Rp&,A99dC/47jh(k.:<%hf_A\
|
||||
h5Sth4)5U%9pB5`-!VtIQ`0JLkY&S\$g`VFdsB[Bs*aBu%Gg*a,A:f4cV##rBd7A_()DgBS>KO=lBDP(
|
||||
Yj8BWRsW(iodi9pgK%UMO!/;F0!7/VV]j`ZWuV<FQ-[T?Yl<q4%R\FSgQQ[iJ$ibse9b_2]V<//2E$T8
|
||||
><1,lTSq9m,6aHVOEG*"ZmaRH\CBcdFKY)*GL*Rb)tV+;[Y-<qcm]Kg'N-_DFPQPDp$h_f/\O#:Q']J@
|
||||
MBt5<M/Uo^ZAq+':'Wh&ZmOKM1ER>'St;\erQ.!8k$!/ak.#/DF98I`cKeV'>IqIYdC\;<HNqNK%Ujpt
|
||||
i9JIqg#QhQoO%MjNZW_q+Htj+'j"Tk/0X&lVXs.AQ>MQOm^A^rk%>=X363Q/GXM&oVK(UOmi$oUh+gV.
|
||||
(3<t;!p;H7pMf,N30(Dp:\-N-6?7Jj8drILXc#BiL@sIQqC@NLF+:N)S&<!fkV1hJ"-qL`Vr1;,(WAJr
|
||||
Z&>S40t5@%TG+GRnVjIAW%hap-lP)VM@g,4=IXWCA(gA?*\&e/=d&jYaCRjUGM-^$Wn#gNc,H0rP<%N3
|
||||
m+?fqH6!mq.*aTI^\'XclYlVj,QC5TLj\)40&;;72gn!^hWZ,nB4+l6ME3sHkfpHgNJemc,.m1:V;@RA
|
||||
9$b<D11_mETK4(GZSfB$CK94+4bKo=+SQ:,/!Z\lO1[!<9TQ8+H3V7F$!=uSs,"W,nFo/PHNoe2Wu=>5
|
||||
rCfWA)>*2X3G#'<?;Jn?2ie@]U-j.PC>>qlh+2i_FR=N@a?5+3]d<pFTY$MaBCM?SGQoC>^*b[o0"bT>
|
||||
oGKZYJSE`)SarF?]mu^!L`(3NZpYUZlU[CdrR/1,QQ^j&7Ang"P/kmc*?5XpVaWVQ51/4,q6i]scSItZ
|
||||
cS:a2+5faApu*9A8[5\FhiZeUoK-4LF%;;9P*31qXth0DI9l05;*rfTQD&j\Z*89GTVc;=TQ`U:4SqE;
|
||||
em+:-dM<kj,L0/(T_X`h>3-2"VUhMiI*]O?RYa"?LOY^HEq/%nO;a+SG>ljBCq&=6"D6Qh"!(f,ec#u)
|
||||
bm/"eNAT43'8p1#^+f6#n<rrtX,T<(Z\%nTj_3oOjn6:5\frW/abeJn/B[HbZ^gk;/PuEQ*3HOIe%%"*
|
||||
s780rJpS[4ic"PoD1fnl-U%Kds5BR-hE!l_Gd_("jW>i3,mA1FHKA\l)?0p,q$1Qum2pT%s,H2+0p7'j
|
||||
htt?<55P/5om?_E00XILr$=/FqSE,s1hu&(5*U?OlZmp;Bfe.sn9g;]U#gNY@=O7+@3kDuB[D3f(Od.H
|
||||
fCS+aHe7_m7le>2A472=4X+53L!1TfiqN\*mh<_pr^RISN3`pD'@rXCV0'aBJX<T*s,$^Ms3.10jtjGk
|
||||
`r>09a$0gl^r==i#JS6Irsb"Oom<>+RLMnI_\8FX-<uq_qVYuu8%U3*6gTjcm&=NbqjH^%!OMO/Lnp2M
|
||||
*[ZO%L70?6ergM=Wtd,BQT>`U!6pK@+2AZkd/6[W.Q_D#rs@K>](haG-K3jQN)[j$6FTCEGgO>)/'T*0
|
||||
n^adB>Mu%^3^X``RS]8E6,YC-^6:HF@"a(`H6Y^6^;R;er]A9J#A.aSLYaSr@]2=#^!g]?j9$Ht1b'76
|
||||
^[g'/d;`;CPX33-TgNa--<U&4aDHq*U3)@X:$rtd.sdGa<U0NW5o5R$>I3CD@j<"WIofV/d<2?0Z?>%%
|
||||
rCr:o/gKA#+48`2!V4INd':MSC^$_?^_Fk:^%#p(Zts_sT/TBeT<4cO/_'bH95aQL1JcE<Y@9FGHGnf?
|
||||
hVZQTkkFitj%ZnFf$PG1FnBg2&A;EZ"W1EuIQ>q(%=k&",fqm\b>F3PYQ^<jbm&7>^Y=&-07K2JVS#DM
|
||||
-0"l'k#[L<qhBYh$Ym>HpsOJriJjLd*Nm+*;GeT^-'N9*r.3W$4MEd/-g-EZXkAE':O<!R?XIWP5K0P;
|
||||
_3]=Gie=XT3hP+dW5GNVipZ#.Z[!%Qg:<<_Vh$[__W"M-.6c:+K2;0@iXYWPI&d/MTAJTA%CaM<BrEtB
|
||||
LAJLV<;dH%>2\T+;mY;`Soj3Cn>XbPTk!Skh@uUI]>:EA<Um&7,X,4XK24AAi7""KI&ZXbidYLLb`+;Q
|
||||
WCSH4rKhmKiLdnkfUr2^rqt[7gLV?O.h/IG^CMCJ`7dE>B#SE'"^BQp9:>B6e[u6hY8S>7-\0$0'M0iX
|
||||
0!&*\Gp<R%Wb6mFWYTNI_EiRYnr.Yf.S>:\O"c1LDt2V?H[NPKCjE&jHTo3j#_BC)g`/i!kRA=2M[aCX
|
||||
ZZn"oJ-t1%nm.n*TlB1DZJOs'OjeLZALN"ME(<>[K$+r."hI//r"GHHp.pYA'Dm%SL#(Lr08^I8<]Lj+
|
||||
`kKZNZ%$GcGG(q+EGO;k_2)CN!XtOUJ<$>6Y_U\s,OXMu-In"@Uj*QiFF=rE=7N=1)9(GWU"DjJp@6YN
|
||||
5(;M5<?Uf%IGr"K(%0?np1ek;)QDQ&gGg)_HH#CdOVZ7JUD:'(GdDmI\)Fsq<M(H.aO0'?g\:Vck8QK?
|
||||
`g;Y,44Zu3gInbL$`:tT-Xl1aN/JAD,XPsVT)hs#%:e9,T`uXo4mG$(6=eCtS(V!bB,aA;%5gatiZ:]:
|
||||
+6&ORWY#*F+E!jL*U89:<jbdH2#j`?[kji'mn0l@>B^OUX,KEQcEkGDRdM$o3-]5kG?bJt*V[QGUPqt8
|
||||
^C4l"[EpBEdS?2i.!Ig)WCRo/I00pBCM-Lji]1,b`h9tFd!Hb(W<dMbNiIn^7Vf=Z!_T7]"r^W$SE.[b
|
||||
$5`]!m?^ja8fQ3M'f"#Is5;^"m_g.qap]!O`De+J928'.l`ontFj;aR>5\tgYD<KS.h3'm?)GA>Dn7@K
|
||||
Z=<jCnDs+nR'#Kf'55H^3A__Q2H6VLp$BKI"NBNiPQTOL-3!%scfG"0JNMtlELfF,4QrMFfLg#3=]NIT
|
||||
C8Zq+<F;gU<EOD.6I]n5;1$?tPdJ<t=.hF/rU_C+f`I1g<WsBTrL_il7;oU)>`BJ2d9\*M]t&$,BCiAq
|
||||
@q>#qhYQ>P:CtSka[J9lZ=&KL78noXK)*4]r84%f=HN&8[0N<CA;'a@i$Fm)KDu%?m($jG4R`S.VKB$)
|
||||
5B5bi+fk$,XPF0NmjIi.rX2kU7W"H\r/IdAdr<61nVnb#F;ukL>?R]6T_ct,49>oR(Gfgt?hjU`=pR76
|
||||
AHseqP<)1$C$(;@I_2k!h9reSj4&T&PNr;%V/X92l=FVNQmN&R+p6LZ+R>_7^Y/*iVj/6;Y:q&rnQFtk
|
||||
;1"YN#V#[h@8<+cW6"fGXRl74RMbi]'5)Ru1W(aOBEQ@W6]!taN?2jnHXk(LE5^Njj#q<L8]/DcK#mU0
|
||||
h_9A['=@uU86CqK+aPD5[D"UinNa`9Nb+fTl/Ue#;haO(hR#BaHCq0X$c6[G*8;C0'rbf!V.#L+1[s5g
|
||||
1pg8bAG/;/P%hbhr.Fq\b7^Q!2#&*8bHg:iBrl51k5#caV"3&tQW<5RJDu:]kak'goGtB?\+52h8^*hq
|
||||
<iLBr4)4:e1[)9(9Wo5]GI.9`[!2OaF(+)7It<hFG,_Ldq;dN4M=/6K.o4&cYEG;t`%trCF(/tAiM\))
|
||||
i$8Z"omo["c8_ru3st]kCS>9N[o5%n-IYH*=@J4Mb)M-grW1&aYke&!%]uMh5B08rhQp=$c`lX7.Xsgo
|
||||
(Ks\qTO^8[;0faCXq4Xf&mSU;$30L6jXba/b%RFaZpu&!=P]PMePRSGAh_\O\E=Yj>[a.lVLEjaqd:LS
|
||||
!u_-S-1Nh#5Q:t+>%nJd/7\,cqHs=q:F\SOPFB^qJ']NWb[kP2dJ)l1nC91a3Ia5)'))nCgY+;E0!o0p
|
||||
HaV=(A="#Pbsq]b%NX]1BfFU_N.7R._'_Dd_rq'8PIkAoq1)-oFV]A2Pq-@SG^_bRE%p/OR31FCah4<@
|
||||
r`!bQ.\8qd2KD8jkSZau?<W+1L8)(q2.:r`e(X$]rN267baWC5g'jIu([@UP,)g?+l<;"Z%4pNcdgkmf
|
||||
(UG"f%LO,SgUS-r08nPcBP`ql%;d5VY_GkFZpEQ#)/<i#XnV0.ZZBuO8]^ibX-&8*(D:#,40jR.Jqt/&
|
||||
oU`FJ\0a#*`jq*i?+t?bB[^*cI<<l*[*"PdKAU5[L/)]8(MEEVbnI&G2ap)BmI]E&(01P?;Joq<Cq0H&
|
||||
oVWsZ&$`^@s7]^C<0_.a/lUc3X^>4+(J-f'UXCbkY+(Ne)]4uSU1j/B19BA5Got&``0S)&(E'QZgcW#b
|
||||
okR`\.O1B?p;s5U3cq@Fc7A72dFjU^BYtS2-u@tfFJ5eDX2Sh]q5(&*K"iN*jP&.'4pBX`;s&+6I;[T-
|
||||
25-`-bgku/"u?B<f8J<M6Wr0*3FoPmqXMueJ*.((JQjJD83CURPPMYIIf_93J,U,Yo7*)aJQf]e)[*Vp
|
||||
3hD17!jWI$NTo11)]Gg7n//Ba2JWu!rkPtsLPR%8)euIGnX*+'(283Ua7P?MOf9,R7H(iH^\h.<ZJ)r]
|
||||
*.djNkkek;7ZG*$33Wfnn\8Re6WQ@a4rd)-clkcG#T60[F7&Lh9YQ>sH@]OY_H<!"c&%?.r4>m))#r+m
|
||||
9?qlp]B(h<89@<lr,Zge/DJ5BM1*&:@V,uuq!?*ROlor/Ok2A9DbGI5`2YrRjfmdV*/BL\B7^S$hLmZE
|
||||
i.8:#X>^a\nF+ZF'/0?c'?c[8kS]"!19&#Vhon-\=6lW1Y;^;'q#50MmhG38>HSPLG4L@:hnVq/ni*!3
|
||||
VXIi5^oms-d%d8l7GmPm`KM]V%ea7=XWX:]\%1C5QPI&1!NE@q4m7iT(c=qac,kUaqX-d@Nj!FmNBN_L
|
||||
].]'1R%8B-VI/$6ZruhYI+NCLbS'!?)R;5N4UMlC,)jqES$#!Nh2;d<eY!P$'GS4DqgC5nln>2[-<,#e
|
||||
g_a&'@-3@NB^U%*)Z%>4rHngQ>j^q=rVGiC6@<^cE!"(Q(mk**mrge`qSLj/[*N*.Z*$i9guilDe\h1f
|
||||
AgOaS4Hme)PE\'pkrZF2PbJog3[&Ee+6c_,aZ?QMb$4AthF+GrZB_PRIZZRd125AGAo_GN"7oEJ;lj]j
|
||||
5O3M`re`?db,X.E5g!"ScCp?r]PlYerpL\>\QYn(`F+FY`hpD;U![:53+G]q@?6(`<\6#r<rC4YoQbl:
|
||||
QL68a$,!r6F")@g0%N%qNNo>/>*A[+F(HUQ2[p!"VE9PY*)h.QO)efpHG#Z\)7\8h9.U1]N%)h_\gTb(
|
||||
Xb8m!UCUi;HTK.Ce[dFUIrh2&?^06fmh$'X*.^,k7sjo91,>[fV$5"@YHouYq!FT.3.dqGYc3)6YHJfG
|
||||
ReQ_'-fq`/?c.@.Lqb^0qpO!;KO8kM%ZOb!T@`ASY$2:dn_6:ud5l[S&%l$g*IT+"5d&YDV<k)IS.p6-
|
||||
Ho=c\\6iK"%hFOuq:5=2jN0bjrbC.o0dA42JA7i6[!VfO@?*_s<ET^Po-TJ<KtN*;q+pA's4r^PAhN:*
|
||||
o8@`DljQ)J3NYJ,.uN;MrOU`dQh_:jO'Cg3YWm*TDJeO8n2VK[?G_?:;Wi=6s.9)eM7l.R6Mq\8LtKO^
|
||||
mMKKTL+LMt(7kt08>hl5jVkT"U.8.N#@.m&11^BHUSaEPA(G>_2[:SIqQ<o*-qkeT_t-;J20DZTIu@bs
|
||||
6!KEVlE(=d`1&/]I=7I(;025S0In%rmjCs(=1C^.G]BPFHoL8^?PSjU,>ms-n+TQH9RUJjQf8jse=#F:
|
||||
rVi\'4f4d;\YVCi1N=eth`rmt@I@h+RtE^9qJK,,^P_EOkRma$kjMtL@qFjl.rYFZ2fUD;HrmetkuT[e
|
||||
:9$gE]YBIe)Lk[1eTZWUqGgBa4V@p:qL5I`5h#W^<<Fr!$DftC.2t-.ECc)(s+uM6'?$=7^m4)=mJI3q
|
||||
K9pHVRZj$"E9<O5]%Gcrq/</Wn!H2$k".mXS^-I8V9t.l3PDj(';AVc(V=7C[]p>Mc4G_)(2oMjGDl"m
|
||||
r)Y]KXjeRm>5c]PZU(?pX,iK&I[gnJWY\XOM>NqgP+gb('miF#8+?C`BbR)!SV)9Lf^I!>G2=!a)4PcI
|
||||
LVVU5oARS!H^/BR<bYLH=c\k`g'*V)9'c,;4`]&tNf!'kek.@7:.5*)dAhZ`]W][/hBQ_Jp"(G%4rUhD
|
||||
Us_W)K7DLP`*[WoZ:?SEnuO9Y1h?!ph`'EIU$1R$$Z#ZM@!?0qh-q^hFO0I3)m)`T]@`g8OMA%R%8pms
|
||||
GUmP[9V-[6b9]l0A)^!p&n54$Z\R`bCBRKJIK($ap$2M*qOH=]6fhCj)Llml9=F$`ZoDWI/=mn\b%cl*
|
||||
L+TH:T2o!NOg_@$<8ccM(>=rI*//[;k`J[tB:@-^k4B/O')%^YV0f%*L+BA=afnWl*%@.Ai\Fba?J/Os
|
||||
m*=rFc)$V&d#d$B=Jqgt8%+?Mih+jZA,\7C,J`BU`dOn`9_/l3#%m3]ZMr<(XO&_@Y%P2cObl-ep"([-
|
||||
c7b_q!%0,^YpdM&+lTN`5B1rh31T<61=>Ohs6)Rg;%EWU$2>]jE7^0F&^8+UleS[tb".$DLrrP!h8dGs
|
||||
RrQH`$P:Vi)c$ig/'m%si]@ff+5Md%$fu]@C;QW]RHl!O;KLg>*K`qC(!Ge<n=6Api/&7o[Q5kHg&"H8
|
||||
AO#S>W=<PFF3haCAIO%XZpn+*T'n;ho4YKKr!Yf$@\eWccR,l>nLF3(+ha^&]'nl@C6IDIk('870l9$`
|
||||
fLHa[R;98)/LKR`HWR:+lJF1X+4f(@4c;=kL;I8^W"j?N1fbe";4IAt\?9oLA,VH0:?R0KWu1*YcDoGt
|
||||
KkaA':FEo9ZPQT?-c$ssHA,D=62Ia'B2oC<PVraWg)AC'Sa9/&QJa>`X@8*IZ7Yn*s3@_CZhLoL'B@H9
|
||||
EDO!Hs%WCcNE(gIiso%c*+>W86J6S#-"WXRZ`#=[G?&qNB"DePg-M#$YB>ScAFeU)ToB9d:@"4#<Q,=!
|
||||
4k9:6LV>j\j2Wf1Eq%I0IsYhJ`kHO9)#8+,q(j#j"CBgZpZFcG5Q>qOAi0'.Wj2Ic\F?l@<6_oZGLB==
|
||||
M5J93[u42G)u"=gN]Q^mpW9a)\@Xf68'IGuF1SU3&DfE?Frs:ZZm;sFE(Go`>kd+H:>X*^,TU<n^>f^p
|
||||
M2T"*9h8lDGNu=Gs4,41ZI@nW7=UZ]Q(2,EADZ][GS\YEH4TE4rDuf%.BGg$'WMN::;;'YGj/PIbdtQ1
|
||||
nIB9aSXikPm<8baPr6LJ:5Lc_m.&S^`6J8t7i^hud@(nOR^e%)ODH>r&5TB21N=IE-JiN#EC(pIcs+$N
|
||||
Iei6/@;=u;q:mX<F@rKZ7?8!aMuG'$$h#&,ej:;Xa6Z(s78'Z0]*$e><_i,%gZEGRAj-12ou&lIZ#b<7
|
||||
0"]-n9\aTI\$V?IEBFn?SDsuq[WdaE'nF3llb\+O\i;lLY5QZ9RC@9pj"p-O(<8N-)/r90pG^4ara"]3
|
||||
s&F'OH6f*=(ZO*<N)h%"DG1fdSE$/4jERT\o_UJPIODl\UooX99;Y-EN\A'0<ZKG-\\mr2h/SH.M>"26
|
||||
IRi-.]qKT!3[lT?pcPJkSE"[7D^FYNQQ."6.-N6VNqYKSnbuhWGlLca]_=rZ]q%qf(N*QZeKZ="pl]>k
|
||||
43&/g3Z#gu]ufdtr[,[(q?A?Yrqg*R,+.eHB&;CX(t6[@.=8VFcZ/7Q0kOhiDf+?eSr+cLqG??hm9H!o
|
||||
3k5!<CIe'p\[-[Q*Oi`ZI1b;e,i0VenDXm%=f3J>Au0@[47&?I,OYaoN6'c5/`<puH(UC`#%e;.U<",4
|
||||
@*uQ"]QnqS^M5)e]K'J"$4CblffY``pHaH/,_:6a+jW'_dc@a&I+mRd'1p"#%3hLrdJnT64;7Rc'b2T&
|
||||
mmF9>=3_cK=lj>f?;B$)SVoOi2agRg^]+WLjj)V..d^']0ng?U-Z?LFXQ28;-?ADUjZ2knO*oM\msf#@
|
||||
H,\#:XaK?,Fkq!2Fh5A&1c21]1fI?CiiQjI1S7Z@9t-QjY&<cGilp?c*L6nsSSo]V*VO;jr`1S(El:&j
|
||||
khAm5<1UZBZo!gh-,7&U;2aA0n4Hn+/kM_q\#0LAJ]F7EeuW*EFXDLH*)5$!LA%HB`jOf`hpZ<$]tuRF
|
||||
VHAP-)&8l)2,NFOD!J1G)g%*hiVakKjYudCpZbcin'8&oaU$(fT"LPFpWlQ5c^m8D-9m:[@eahkm]BN3
|
||||
,p1XT\W1sMV8$12DA65$e5lRLa;p<_@0Ftbf7L3rnX_,bX3-qUhH<dg@ubLea10N"2G/h\[U[jdMiu9<
|
||||
''eV<<o-(d'I+;]`_a=)F-F1$"KPcD270+<*q`:O%;IKk]-OjLSg@\;)BVGmib<%*EU9_ulQ1&S<JB+^
|
||||
^8e[Zl1AenYG5+:iZp,,%Fp#f<J@u?d9:5VE`HgWqg;BuYHl0J@\2P7<Q1*(.L.W`L97!jICJ9Q<T04$
|
||||
^8e\Mb>kCD$?N9u4rRd.Ras!l.SX*8X%F>l$XZn/SA\Hn-JOQ]8de:QX,:Di.W)[a[RpE\Y>rPiI'+se
|
||||
`H0jK#b.AbV=AF-De7H[XX^I"3q-.Gn]hKD.P89W20kD^bh&lSfN4?(?,N.:]2-aQKu_]gWks[bI9!Nn
|
||||
7]\NEn#K(0%\R!pa+m*M<3K<`7$DFe;=.#Y.ZSmV3B6V5`OBY`BrphGX%I0q6eSL!EAXgJXjSQj<J=N<
|
||||
SlLK2FS`#sFQ'7J['/erb!XW8@o]e&Tef^a8%tu_k,QjmmoO-#9&%stic;t;`FE"T2d3mW[>WmVD6tY#
|
||||
Wd&X8<u%Vl<u'=(>ZG,,je#":/4o5hRLi!="M)NRPiNLuo7%Q6@9.Zj&YIsbo.?`/pugT'(olOj]GRuC
|
||||
3ktCohE4pTc[XkXFA$OY4;7=CX(1kt'754SbFP2J8;Qm95'SL5Jd:NKgZljpq1c1kbCXqLp[dHEJnfqG
|
||||
\:KBNS)m1"8$:;J3QIIEIDLVE_7;LMK8#YE9\p:f1rG<;6Ml#ia9^B<IsdGG*WL##Dd0uFl#!Ik\a\b!
|
||||
H6J<NmaK=@k!I7MFNDGi>9oJSTq+sdk/(4$l'o$I5BU5@2_@\UAk[@fc@8ka+aDRgTd%lG"3^,>)]!Lk
|
||||
mfD0?Lgt;fZm)S[#X/^9*W>`E?ZAOCQXrq[j;UN"eKP?H4.a)RQ>`Wd0:q&qh=#+>YGP&&Q9BP=d=%)8
|
||||
8G$etQ"Qjj$X\O)lFXR(^qQj2#W):Z*-s!OZ"]t:k&s):cgQJkZfqqRfbWUfZPKf1:j?Q1Tdol@ZBdCQ
|
||||
EH`Jh08$/-:hbr7+/=-;FB]uP4@Pb&1u-",ICfeS%WIJ?V*+C&VH`884)K#bD>JJD,[^*][0mIP6$@M9
|
||||
AYD";534u:F:lPOLoEIXrfEH^)3ls7;^/O=V9/1[XZV/K(Z*NYKENF@16AMo"tmPeOS'ekUMZYr5*c(P
|
||||
GiG4%A67PCjZHf\pc'8>qYS,!S#9gJ3Kna3\T/e>E2&rjh'4))3o8JVVFtHEMWetZZiF`S$ZukE</);g
|
||||
>HXS5!MKF`Uj,P+;nW.5EN7;,WYI%(d&$N,m7!AY>69(f1hY*5+PO&[EH9@OU3,-qc$!9s"3%U#1C#Q8
|
||||
QEnd#&q:Sb#'jg:'OD!_)eoV'U+nEd\?][DfM5\JV;%k!i_mmFM%h<JYB?W"pSB4Z8s:>mRF?#]Mjg:k
|
||||
d2QjtUIUTjZd^E];Z^7b<(%k4eh)m1.JI")Mpknr5.S%>*DG_`X^@`@=Ju+2*654%>,$28`bDA+lPdh;
|
||||
kgQYR[VM!sY>DtrGWXt)\&RCKH#MfXXjT2?+Z)"cldrbgW@2-7)TRtUS:fcrA*_N"%R&UBE@T<.]>g9*
|
||||
Tet%=`C&[!1p1heF<i_=9@WO^3"6`q8)lF?.r(T?\M@(C3-(#=Ql;l6Y&BOUg;?G@=<>CE)MdKi?/mKF
|
||||
Y,$''CCsEQjNok$Y5-I)Wm)cJ;e73'LdCB#j#D4A;fTR7CE1lmOiFYOgm?$c$7b(LSE8H23^ruXNO*nZ
|
||||
:9Bc<6Pn?i0=k!?[,dO<AkGU#ZS47snTPA#dh9\6)Fq5k1_02VCRdij1,KJMe48Ou<Z@.?&RD2Pl>R2"
|
||||
NDhnr`U_c5`s@ZGd-E;lBcL,JP/aIL.WdLBS^_`/338+t3e\qqNlE6U]leB*Q,Z+kph1Y<m$>Y2NBrQq
|
||||
dP$+50s/8r$9SE;X/<C><'0f4COc<ss2,93`@R_(^tt#d6q4R<4[HuNdC<!=\XU2b=7d1'oD6-rHnqS.
|
||||
DWYK4bO:)V`LR?[QThZh;?t4EgeN_CiM`iOao+;ki\fMUbu<!S\U`^+.<:(GSlJ6!nCkNRO]?l?.5Qh6
|
||||
%"%g<X4?d9oBMaQNgf5@3"Cg?mXBQAaI^rRT6M,PKs\QBLY9!5m+,4VW&&eFfE,MF(mB,i$:`ep2pIHQ
|
||||
KGMsM[*r2@qm6eTAKu$>[5c\Y-$7Dq6brHoRPaA)NqQ`!$R>Cc-Ul,Y%n8])p4`oq#tb@>7"`1Ego9@+
|
||||
=5>mZCr^D9>]F<#`bkr`[q?6*3RtL-c]K<V\WA7eaEh*Z\RVO>1TTD[/nAAO6K2_HbY3D0/&Rjkg9g0&
|
||||
ofG_W[>>rcLnP\9f?Wk\Z'U@!_)rcl/9^;"JVMH,S;0VZ[](k@lD$j9b(U7_[1R=,4$W8IWks\!VVue`
|
||||
O0K#V#7p2Pn">KnMJ]&0qjtKlp!q<WSdPiCP+r6G]c"Gio8t^*!Cs!2Gai^s2]!m]L%.g[e4ojHAMB/'
|
||||
'NZ>a-H.jVaq7mV%;^E/S_!'&T6ALAn[9+WL"s6]Q+<"7@E&<%,`:7GpIG7W'ePn?)4CpgU=lC*b/;Pf
|
||||
fP0M2(mQ]&0KVpV+L6`&bh14'29G[;MS,rh;*t3Geh*1do`fU%mFN#&;+$OPnd&*c`[H(>f@lfPo8,8A
|
||||
6AakiZ*YL"Hb#G0cQY<dO>I][>`N**d&KSg!7AN+1@mCrD]RUH^^^pSXR,k^B=5'DqH(m3p,VV9riKhZ
|
||||
r*`&t[8d(ob\3uY[r(dee^%KfPa0CY]JA%2fkYsVg-p;3lnch7SIq.T4#(C`Vbh;uiVP^8rj,aI97&>(
|
||||
9\7cSh2%NAZ-Qeqmrd<gYKMr'H#N"48tKSN]&e"U.Q6(C%'h!G:o68/\1Y+re3u=(53oMJ<mJ8b)K/uL
|
||||
D$I@>pBt\@%NONN^>&Y,0u!!X''%0l%u`1=lt_"%1QF1R`V;<B'KQEX@o4pubr[JB%[?-6^8t!\N\<'3
|
||||
UZ`NbnMq'cSA)&p`Z]oVZeXFt,]\/p+d')%,a$i&=!Wo34XlL>\W'LCk&#=,m'9nfaUpUk2Fg>ZYr$)G
|
||||
2$L+1LG!qq)chf!4G/G&=>T]pGp]0R*isr]b-@h>kUsu6`R%H0l'-<8`8(OUBI)HaBaoMmq7s*3;-B'3
|
||||
UATikc]!7MGg8m#%Bg\`>)QSp7fQZEHac9l"qnOWL2-(-"_unL^o/%AVA"&QD;<<MoGRcmn+[\_nMjXZ
|
||||
'dut-LNF*l>DT3pc>fRofBi<NR4-*I3`<0#ft"6>.'4:#86MdfQt/2"qKqkBB+a+"m0g0?K4?1mi([DE
|
||||
[aLH.4+DB6B.-i2PDcAZ9TRflqg^X^'Qg=U]6.O]W5!<64;DV;`VEj$mbU>ZSR&PoV\Z`BbVH0N:R9@F
|
||||
WL806.rTNqSCHU\ZhSce$DgLV<ZY3f-$5;d/CCgRf)Rj`nSH<1-pu[)R1c@.:TAkV8(P!=n=28PXP[es
|
||||
%=\J^eT8ILQE&Iki?Wh[[5=(+EN1F5o.bC)\@aERm.p4\*$<HID_:g3s6i]eL[Vs9D3(kr*54V2+RSst
|
||||
T/`GB)df5$Q?Qnpm4pS(NdhP1W]`5-s7-DTkILG:i'M'1T)Za;Qcj^Prd/R24cfSkiqU3:rO!EeDf;/C
|
||||
GXf%iCcgXa?trpGXZ#h2c0/AUfbcWL[W1Jh+d7_[06RuGdC)R?XB!22#^GW/6WWN9h^p:X_XK#pF[i4q
|
||||
<Oo$IC7X^pco:f>Q*n94F!"D#ZG,JsDJ+%uko#5[)hcqr6<AYjEF;X^@\iPMM[oO4TFuPb'YN]-I8de"
|
||||
LY]1G#nu(cl<7#Ml>Cqh(d_<-AfhXn7D[C;9:*%4:l$FC,kgZhM.Kq8"sG:tF$rgFtglB*7%a4VnJ
|
||||
R.Xt\Ok+4ugl9dPRCdD-h9:,?IPip%W#btFr>!msBX@J)H"Q3cJQ@Y,#sAV&hXi,5(\\(GX,RG?gSlZS
|
||||
B%m+m]QJOhf\X+/?YbjDE;/>Fg2Y4qh6RtV:97p<';M>(S%?"7RWU*0Z_FG.(</>-W"!15'h\BC;*`>3
|
||||
[P0:FLq"p`9Z[(Rb3P(94<*tU54R#-rj>OlFgq/;gP`CO[2aDLN[q]!?B[J)nhH7g0`K;[2L/ls2^e"2
|
||||
rAATC%c(-9$5%3>=Nd[]&\p2t7ZH$t@`C>Z+$27O_MjWJ*r.s$pH(&YYQ"Sq[ital],<Q.2Jes40SIo-
|
||||
Ri0\EFkKn^b2n-5eqK38_`=@?l'uGM`\pW(g2GuXi+?$ML7>^JB<<Hr&hSfWaq&D+aq@RJA;NfREuqC%
|
||||
7]SB%@.,%9'_)h7+o]C3kb-EL)FtfrDi]*#b"]'sN@mE,.U!GQa*#oKRS1tEnegAWM4,@,A>kSSH,g70
|
||||
*LsDh)egB86^dn2a&j;;>:4JPi9c(fe6P1P];6_[UWggL#A5!mOU4CLTePIEN:L%Z4%[$hZQ7,hS..ie
|
||||
e5,pNFQk<%aBJ;gs5ceVm<)TD+(UnOhV2,OiI!&RJO<Q60(JpA8'/Lb\2+mbM6&nZjrRq$)](E5qjMo_
|
||||
09==q<gha$H@"m(J^&6qH`Kp0Sm9+i3f#EH(K.b#k9\P`(51KUZ$?t.A,A@7AH(eH0tO\dnkI4Rr/!ic
|
||||
LAksYk."T2hHB-E"\*^Do"=tie@M-T/?e]o$AmPFfnW8C3;S=@<Sg*V[jjWaN;FMEWaG&sk,O%^C?9c'
|
||||
-kWQ<RWM`VE$Ijr^9$uHX0BR0a#n,m,,P1DZ@Zc,+[AD1*DT$cHo`>op-5(f&]kIg?n=?9643V9HS,pU
|
||||
@;3;XG5gbNdN6:K-_lKtFseQAk^Pc/D55SVK3$hK@_Y7*Q"tO0U.E^)o,SXBTm9GfJ(\%4VrPbKZRf\C
|
||||
R:>&Jb;K2Vk!g>V)>e?sH,f^8/roLI^:3\,k05<+,oDGVK8*V3MKW(RUGT^Ar'QL-Cgm/:*P3,774b"e
|
||||
dN4H#fUAAj[&>m,,V4\n<='n[eh$/oeC2PR2?%31%tRU3`%lY.Obgs@;d9,JQ$2cidGFj%d68=^2cKda
|
||||
(d0WVKRXJ$&rbf31'c"B@aR!fR+^k3,1nN^lW`he(Y8>C<"o%A:N"#(3;U[_oVe+@+&&ZZPFQ]&fe+W5
|
||||
/._/HSoKAF89nOUWXM?LCY7>Vo1SnBEgE`?>%MDC7TN0rUqA+^KMnuM^*Mg:\.;"WGO/5Q!oGWlT%?R`
|
||||
03EO\Pd@U/96j*6<@Gm=T7<Y!YA;rPP\*I3Au4pWnuQXJ8pMK%d\;s>*@Z2+.G'tc8U3dJg`tp+\phta
|
||||
rm(!gXM6R(2NDVo*M@^SP&eOL6coc<5q_9E+dDO6s$4^70;o=/f83HuH-,K4%fVVR;+`jPTl=/rD0.DN
|
||||
RJ4=KPUdC7Xc-!?c-1UmOE.,?#V")YkX`k&elS4/=8s089^_^R7Ll:hB`I263JEE::,Z[ZFLQND,A"Be
|
||||
AMV,DO<FeVI5B!d]Y)ds>?G-"QH'^c,Klnb19t18)WR1Tng#Hs+X9'"ahl-_4>O3UCP@]u`^qF#doSJl
|
||||
!pb?LA>BY.OjMf2'pOj/>UCjDKfAm"7W`-rA;'0fLh"RuOnVBadFl9rOePq`AV>9MKG;59^1:b[T"1Ot
|
||||
Nf7qdj2_LBI$YH.&B`(PIjcPI8OGt5NrUrPr!o&*hO].+,Wmj'aE5-T.4CAqWQRgMjCP`>#MS(bg>ar_
|
||||
o=C*I_EECj87>5V,9qF4J$cO+NK%/p"*^kXQ1lVRi3W"Eq*4Ck>cB0V`Ju?,WK5kgl81c7eZKK]fn0mo
|
||||
n>-R7HeG"XQZ[/Mgj8=_$9._K%Por4Z^=ISkl:lbF@]LTb4aUc7/$7Yn2d0^^9X,JZ:?Mnm-g&[h>DJ#
|
||||
[D%^dFGSS(W2.Li.#=hCrd)nWa2-O(&c=h;0p4b+U[Ee!b)0q><#f-d&mJ#I&?V#!4irnq\T]j_X3o4^
|
||||
XS[7V><Et?doZ$lH6a1=f8((6d1hdUNHO&!8)KJo)6us$Q[0q5U])</`6gtgGUIaB]9^ukL[4e.R_`'&
|
||||
O)FfWi\9-*o+/elUMsNp5^]ZBWnchtNp!T!\io=cFF2MW@rrufs2;rACe,4?[,ndWdg*Lm+od,`r-W"d
|
||||
-_Lq3)lmc2jBYR$$k7F6p]IXd)T)3^G`RWA4kmllZ.A'b0]SnY4;Q-5U#D:t`1F%@Q$*;i[MN+Ocd&$B
|
||||
Xa$Z;`n)21[n>M%lCN[[)!Bo8Del&ULg4jm*tpN^Zeub5]JNXC$GG!0D83LC&Q@_s`dRai0mBG<fG/[V
|
||||
en5\/c[1,WQ+a`I?bhqeOd^*bnSPTuHLlq7:@@OVh/Y=me#W@^-L&2t]&n=3,?ZI!Z`hi&1QtVp]A1*l
|
||||
?VRf+e.:9=Ar%&ta^9ACcndk6=0Z^nGKP,s?g85648LJGZ*e[W.m5]%ff[QVml$M[r2MT2H?@W&'M#kI
|
||||
Cc>mN3Q?6G+iL0rVTdoJq*s)!r_#4qB"aY`='6KY9TD[6HF7A=FZ?.-BZ,`6;PrZJL+'4[k5dE(6et1N
|
||||
@%;B<q(&%+,tr]CrR]8:IM_0Xd0jQ!RoRuRS]<$PL3e!\)(=mZ8lD-W'm;DJ]BI+=/b@0dXjeC1IQJ!%
|
||||
:';I&>%<t!d[Na41F^!#kAf&:2*>pQ.5f]mk^RjCijPmFHQ:[=kFUIX+&b#@g9iY0&^aD</EShpa#cch
|
||||
EkRldFc*mP4&PiSm6mpDY!q"#ULQ9PB[oj.[1heDF;Lq;R]XcqJs$J$f02K.f9t^o:Fped],gR7D#<lU
|
||||
diJ;`=3bS$r"F<9f(5NdTE!5!l8dmZMJOiXnS?!q3FNH,)aSLW$%n360+=7i?ml%aNZXeaXA+hN7b!9X
|
||||
,<Tac24R^Z2ZR325+aEPPeW&Z"Ut!*9/8<QcpJ0W*eZ\;$X9jNhk\qjEP"a/3dCh?J]hFC.T"qh$OHQ7
|
||||
k^NUSg$+ZMD0[sK^Vc4BL)*Tss6Z<cn3Cd.n$#X-5u&5*3c]s1ck*m_5Bbk'"It&/+GDn=--B?iLEk3Y
|
||||
SlUgjkGs2,>+L2ImoiUL:_D]2S;']8`D'"q1^aN`QWUE4In-tY7k_*L7h;#0m/8"gkCCtAX#939(5aiR
|
||||
pab[-$^4j*'i9/L)u4[ufD65#Fr5Muh2Q^7rT8$a,r/F[X8V\"kp?hVSF]31ZBT4aZ4:/:8J*d`c:amG
|
||||
#B#I%%4GM\0Inc[bJqE&AA,N"F1CO"kg+*3QT58q$uJKQCV<n-^<=A'cMNHrUjsESrX+Y$Je9[7K)7tl
|
||||
DYh`4TlHqX"^5#Y&1@pkNOMbqgnk-&UU=l$FulQ,(HA"4Q9:<W^FR%JC^oo@05R0qYKat1hsBgf]$S?G
|
||||
9o#7*psLI1TtakQ^;XA,+*=m.`=kHXFM^0f\YQ!QINL[t(MA-S'9Bq%1a#5QUd"H-%c)Mm;(h-e7N/X,
|
||||
[g][TDAdbeX^.BnFs>@ICQ3#mK9]Ro6n(nJlL^AEitMaF^,Vl\.d"<&"6UWNUI!.2f_[oMV5s#S3B61@
|
||||
o0sgo6PbR'fd4jb_U=<$elP\m=#G$l=nAk<8*eO%``6IB$FkLHr"@R8NA`#_Ar-E/ae.\,R6(,ZPk3_F
|
||||
Jhku1#Yn1&Fb<R@qO]C>T19,kA+eETB@eKiBO(DB`X4:?:)-395,M)ZgEXK(j1O?Z>fSu^X7X6aFik(:
|
||||
=6Ja/s+uk]s2&@h3YL''p)L.L*2OC-nC?Sc9ep8!U7pQ0qt(!cl;pNLSj0`VFs+nsEBkuLh*hf%`RhW=
|
||||
f#Bn?K/u/P^h<RJ>l)HqI]fq_R.UFSQk/F&QRKd>+7-Gcckj-4E>KpUYD4d4kO"2890*j'=Ik@)7s5cV
|
||||
bJSt$Uk/:.):n^%[U,,-+[nnG[edULX_tsm,$MUH1p=9j=9QbXf3r?_5WekLrLVc4V'FkWBWUA]0qtP^
|
||||
*"u?coo',:%5jJcNG84rN"MC$Q_I2FX_;o^KfY>sIF5I7r6MR6&a9ME=5o7aHi?""/\0`30-M=L6qdL`
|
||||
kiiue^-pf&Z!8Y-FS?Na6s[:aXKI2P=6ho]`Ik%;W=h)W-(^[eZE7b"s,b#]>1WgC#O>+[&p6g%j`m*W
|
||||
&Zk%SA^ONNSo+$EWnVmk-0c7Y_3\iWm=7US^+LqPMA1Gl0@>sL$YY5ZU5Q7'$,%cpU:KY69;V&\1KE^`
|
||||
Xlsnuoj)'5=6jtC,987f02]dU]0kEWWRU$`3"<Lr.po$^<<cg)\lArAU:O>O?UFCO0;0H07]UF%ab$SZ
|
||||
[c"/i8an%kep"K]M(\?F.Qp`+&qTtmNiTleq\-\sju@_pn[ElPn[ElPof%7)N@'%0l4u1'$.238Z\@MU
|
||||
/mk=_ff![fMT_=t$=?-PARe87Rsp&h-"!CXV#Xjh1?U]18R&_0=t^>l?!GPupYeb9/'+%gQcbZ)oaC/A
|
||||
2RH6$,&X%nYu.MK;8#mUGNd\[]q+m.5!$>Xn`pRlc/!mYHB85bgQht'#$oOG`92L6s!RO>fEaQTm5)Th
|
||||
?$Gsd8Q*6WDO\E1rPanI<MPj$mRnef-<o;[2g,Q7\nnk[":6.:8?C(mkjkT&!+q%l%GPW=eD+LD>kBT?
|
||||
l)NpYeN$RC>4n`Sp,[M!cZm/g`cmL=R>&BC&JqqV#A<kPgG+/:XcR&c,eK>=Sn<$7E2s/Vcg]Ia(.7#'
|
||||
d>lg;l+:fQZctL)A)'6Lmk<)_a7qLN,^W9k1[;e@kNe'"3;`@kkb?Jt[7h$=G=M2i(t4R76A#E0nLSfP
|
||||
c"t'E\j;BH)<JZ@[:!W_rX8DnTFFh)K8<S9=%Iar`B!luWinD"2'DgfiHD0oLrcPXjDg@*Z/4j=.t]3J
|
||||
UCU$iYTAP["qOIi;ipuK$LdfcLM*UoPNUq!Q1-e4<JCDI:[HF-?2YJ/K!8ng:EU5cjF7-e]SW0o]!96B
|
||||
M=%[*m=t,7Yb)+Lfh^b'R#L"JZSf1\:_M(n"QGt#_Nkt&^AL\Pg%fiuDHc^?c,ef3*cV>>?_["MnaXh'
|
||||
@QgJXl%h8FB*Vf$Nh]t1:H2L#Rmg!J6r5)!3nL-5cTl,r_O2dCph#*Ulea;b7"RHj8+RelilILLU0M`2
|
||||
Z,3Td^l&$aGX&HopKold@XEa(lK54fCd;GsPmF_;9WXu#eIC8N;rK<)CU*_MoogOa9m9;P4S^;XUU+JN
|
||||
,!4o'Eq8,OKB3`PSA1c;ektH9:m'jNrU5:Hfq)Y=,-[V$61;GR?eS%"ZTd=>67.IGT%agVlGLGp-ZO#b
|
||||
O`EQ#</PqBZ.%FAn;7%YK>#_?S][A+)hrP,D\YL?@sOe[n;IHEko.DF\P69(h1KK_#1\UcdEmd]A@\_:
|
||||
laFCDgn)r>[YOP*$5*sQ24&"88q*BrpR-)`*Ygmc[ZH&XZ%Yk(+qNF2[#J#E65QK+#%&3%\\KVDM)hAl
|
||||
ZJ2'rk*M:($c&;KL@(S3SN+2kl^&d<D'pNoToX$?W@)#7!?)aR%`d&oh8qHLKrpi/!(UYWB]_9/->-+Y
|
||||
`X^I66(%n`aN^m`LAGF;c8^pIVr'6jlK?uZ1tJ^mqu?BGc*LjFDtPo&P@MYd$?<'oHLil;E(S=gj"iZO
|
||||
UsV-PJ?"CE:p*'R!S2ZLaj2e8%iSGn!__aI`pqXEX2<jfP0(+e>&hG31TV6VKmF:&X9*6'Ti,b[\<-ge
|
||||
#L8h/$\/-h1Nf0h@tIat'lo[KSd%QQhp(p):6KTt9XjWGC<Y(7Fh3sV`@@D*Wg-=MHkLNi(!b(QIFOX0
|
||||
q]'DrhAu?Z5Hd_G6C[r9M2UYD1BgHUIe2K-BR-*Yl`T>.O,Z`S#EkCf0PW>Yr0<R<KsG^*4R/7JfCGkX
|
||||
2I)<<%ba*MhXK<u'r_bmniHLV>$gA9Uuh!fK]TO:b6P'(LVV:%D`X#$\9fL.HslJBI,P`_a3=:SQM7^R
|
||||
eFFGH5?`Ll9h&<81Po"U*4=_qdINbUcd<).Pa9@8W,ubL*G]9MM9;5Qb<'_rn!I;8hX%E,EQYL+/W]^E
|
||||
Y2qUkM581opL>0p+*<X2^MqIWL/-GrKoqZ+e`P9M36flW"/0%*E]q.)c4X;%FDIch#A^/U5f7]Nr8nSS
|
||||
BmE(o:7Gn?nig5@YdF='W^@(U!6P`%o(G/DT"Y(B($uQIH0]I=0)(j7lCfFVeuQ3hc<i^4eGIYa`g`s;
|
||||
Q4H_*7;kA%jZ8t[/m14YUZr17[86rUP>\>*<GD>J`a;QYWq_Bh(2-)XEspgB1ErAi6Ie-b&`h<?e2;Nu
|
||||
IRX&N43o$%kiJJS8nii^nhOunrdg!m;)h%p?'W>8O,KZlZHp5L=QSaZrDHq+TM0DJ;M@7'C=#\WPBFhO
|
||||
/Ucc&k<a$Y3cTj7%oMm*Z'Lm>(m!bD'?Bm$288F`Mt+n%Nq&h:9T2bN>9cZ^6!hl3lQ,UHC,5Y\P#`JC
|
||||
+)CfMp_)1;NUJ"?WY5leM<!-9?sf2tq+p@X1b'UX?_pU+DtkfAb(T#\d[OfS[7[3#?U#M;e)fRrSj`2$
|
||||
%"En"GRR/U\G"tr;U[@qXb=-.aW&Q$1t'(9?Ai;=]DpeoOF3(@#M69#F(\b@7eb&0V0&p;PMFp>l**Wa
|
||||
_fS1Q71^Z7MX'eNh*c[sQ5eH;jsO+ThaKsSAeZ"ckc.g(B-h^`PX0+]'B@"<1H`k=fqM*Q)NsM48O9@:
|
||||
H5N\).VJ-a/_!K'hO<6r6f#BA*Y'cY/<9+^3EfR^q)PcIqFb"Y9:4BcH-s*#FhR/`_+:<0Cs(!`BZKPY
|
||||
8\K4c'Go,07:F6R;JE(iVq!>3=Ia/Y6Zd52jr\VCjm%hho;p+o.%ctcf!cPn+r0r+am5"i'fr$&f?@MF
|
||||
.+bJ&o!QVlf;Kc=CS8(ncTrF`n]#<Y>;;uQ'c^M,[!2OqW`,1c-h@EA]+ufa2<I+WYEo*CkPr=l&[d6=
|
||||
c`)ok[Ll96Oe7Q(>#TO:nsil\XC:ml.\Z=[&)EltfX725+*_,>!h;1o^I,/YoB[g-nSB5L+iML$&fg;;
|
||||
WQpsm(dDs;pr%r:I/XWrPlpLNpsEfF2(pQDfX4a+pV1YX/QReakGTdFROEGeY^J)$>$\n1SuRccX(sG\
|
||||
G4F-YrD,"G]Bd5oB;JW:/Z/C#dUI>eCs('\.DCtX8s])W:0.%D\FEW8D4q`Gh//CrMHrC;QD(rD/Z)`*
|
||||
]X7kIYQigkrLlj@?ZTP!\lW(ZO_Z\le7)DiP#IjO?WsPZ09d6Y7"k&*%IJRXZ.aG\NESF4[@e;9[.6PA
|
||||
eNkXZ(39tZ(45Q^:D7ksH`k<rYL$adqmU[ebTj0;->S[h@RfltrRYPYF]#dlc_2-+EKb%@HG?gh/ZpZE
|
||||
ldqsI>"j-oKGJq-T4a5Gb:00k>BhSVa&C%coOiD%<^]o1;uDP$0Nnt^9gGm^W$NNK>*Ii6[>6S.`/;rB
|
||||
)N!Wll_`d`8t?4SXCoT@jjrO`r5,-bo_^'H>HMf;fhAPcX&fn>$7(+P[[N2TL99%tm15%3_36I_hhBY@
|
||||
d5@V&:.3(*U-<b[YSH$$[!`GFL!?]3chqcPVs@&f]JqNH'ij"I2[7P^\\6gE>"ga;Vo>)eMTsUZ&Z<"b
|
||||
;JqtFKUl:BXj-6/4.Xa81LZ!WeeeW_4G=OlG"FWep,UcQhprW/7#:q3NP6Vf0Xg,kh88bgg97`Loldp+
|
||||
YrGcW[9E]HUL;+C@ooInVnFS3L8<:)N%d%Ke#)S&J=CO-(Y4Rd,LJps=-_L/95?iVYE4+1dG)1_g,A@i
|
||||
9'_Ea]2ih?rR@i)#06U+-#2p=\l<^X^0A-(0hA/+2J`#!e+QdrVsS$0gD\RVm"ikA)npVYrOMoS%iH>H
|
||||
SX=&dB\h]EBH8-3lJt=7[ak1mn[m_@iGl6\Dkuh+Xu[$6fs$Z/D0QJGnJ\!R1q`V>`:[i+^DkpR^?JpG
|
||||
JuLK5@)hImFM[X4T^[&0,#*tX@Qp:'Qg$hCD:c%',9fa+;tA$FZo)2*F[C-iDm=HAA.feon=CE'N[p\3
|
||||
\gTJl`8=^dD1bJ#hmDYU0E(HcpMT?&6+)TBqu&Y-/B:<V-ZBmqk<H49s.B%FpZCm7/%Xi[n_f9!S1IM3
|
||||
_KGuO(s#mK8S)Q:1&EV,3"4G[!t7?YUg<LMDZk=nbYmm.-9*HgX*\aD=)f$_oEZ[trd%i@2*P^NNQXGB
|
||||
/N4F:eJl:gkNfq20AHR21k:tBQ)mptH\,l\hF,(NTJOG7>1m>)IhU?4ik)KRj&]*@-$k7!kG,eU)&P*F
|
||||
aV+l=Hf@f2O7s=bGlM?iB"<EB;r!B.@*<L*#3%W9-=X`X4.Y_e)ib#ql5BojNC^Gs!\a'RhddTrs6cbS
|
||||
`inrZ'CX?"S?TbtQ'i`,^]E<'eB0]dIcYt/o%*<2BS@up?Dl`FoTts.8c6gc%UC!^dp\)fU_8%8'YNZ,
|
||||
I8_+7&&7p`F,XJ#Ch06][ui&&E$d$0$7G>EO-\ng-q5>lODX6#DId5QG7PGYX.?)feK^Y>nG^FQ".P@#
|
||||
[:1KCc)tFDml(+SJ$n)q&ip_FpLs2rlOSG'kTKkE.ZHgC2gn8;S63=HgobBO1R+qD0mhS"<HFWs<fL`C
|
||||
n9]CbkYRdO4K^W^j8*Z_U4-+4WZI"Te_d#DcAnFGrd&YL:&jVmNMftCiGkR??XiQ8h@u%:]8$mp7AVWM
|
||||
5RLiiI8N+O*%/:+L@e--4e,WbH$hqW30-j_aa'E?N*o8=ON70o\4tjD<-5hXVm8h<`U*'PESD>@>3da\
|
||||
\`d,m]1(G3^RALmargT,YL+'S(.ZO9*]heT'<jtm_2H%%pr;'V2m2=S5)SBm^qLHVo:O[-s(D.E")1:R
|
||||
^Q&$L[k2$<rY,2#J!8cuE=f72iH64Sj:hMQfIL42!:V(T2Vh]*E,G4hfUn5^qo&=omNu/^jRd-V(SaJS
|
||||
rlRW?)ck?B$Sg?kme-P]7uoFnM>.16WEmZjfIUr0iU<2RHnZ&=TBCpSLtVf(I#UsLAMM%g:HEggfP-jQ
|
||||
_=nIf7^-9uJ)^'IpE%*>kn3BcYeRD/Q/ft)e#7IZN+a^+NZ,d;"H)Z"*aUG@]Tn&[+:o(GQP\sh=mV3*
|
||||
Ok54;c77\nX;GP(nM0.90Nk*.KtE3L;HCtNPa<8$-@)#>1\YPWVJZ[dGBmEGpL!p#/5GXsdd7P009N!`
|
||||
jg9'(PBe4D_lS`NLe8="$;SEobtb2*/E^%Fi23(EJtP`pNE>o'"<+"YibWhd]5mT]Hb:bV>;>^2M"FI<
|
||||
Zn`+N)g<],BVY'G-gicmF@J4Td@Dh;o86AkRs8bhfD4Qrgem$S.5.21qpdE'?][DhSb;R+?R!qQ@MI\1
|
||||
W%0nZRP!n(NT&ckS.8#@SR\'8o&B,Qo+WP[NJ.RCI?P=M_[=#qp#eR05LdKZ?2Ej^2;Xl;dM^[pVQp='
|
||||
^g$WFRJ0g4igCeXq*2*jL\KsF+)S7iX)3iqMY4im.Mo2E.*dMamil'A"s2j"5A64DWi@Td`j[@sL+HrR
|
||||
qgSYnH?5)O.\ld."4i$QGNb>1s4)\c;r/aZ_#$3jpYd/88'K2nQm:-3N&`B3Rio/s[Q*6]R1T"2*<'<H
|
||||
pP5?nDo9)l?LNNl#_`l@a.&X,.f:n2n"_L2S'W#f,M^7NdJHT/*TMu:IZ4j_[QT^fNEVU0i#&_<HpQ).
|
||||
iqnYXM4BCcrW_6?6d'Z(h89o2-:<dHL3I<3AmFkt)+49t7?R,>S<qb][BfP&Kl0o:GS<"(ebNXg$GZ:R
|
||||
>\3mX=,s*rDPtcsYK@m,af=G;ooT\0,&,-:B9CIdr,ZiLK9>@nHmh)h`BJNP(t@T)fmORGVfrP\65kQr
|
||||
_2B#eqEOR;90'_H2srlWS^.CBp\#W/m@3j!)R_efM;*Wc3U?%ZR:#$rC;cenS,u8bb8T?!=)j2h;NKME
|
||||
%M`'9(a?HDTfMhXERm$jTOij>nduooat6M$N:YjSmdXY<2<-i_T(!V;4n0HJ5HS1tE4Y12/m>jZ<4GoJ
|
||||
I/Xf"o7'iPLf&q\c2-<&K@s7]Ac[,+=OD!0Wft;4X"(8G4c>LgMub18$7J!QZZ]sE[)19dMe>5UO>hYf
|
||||
Q&FQJ-6/-Jice8'6qC=]0$N$$Lj5c@Ze(.<,ki5E=*5>T?lp;*ZrZW/n%129[L-9GdmZ3^cS&:W0f2l:
|
||||
.,eM"'U>"7E/cnE\:_C5<.Z`_EOqVoY?Z>c%TS(!;n9WSd-C:Bi-FX/:)hH"Up7@0CEcA:V;-<B)K<C(
|
||||
CFAK8=1;!.EZ,Q,<d=dR^cV/2lp'dSk]a&!M,Q(-Wqd'O\Z1G.d7&P'SYCcP$T,dCVKL@aLdT`Hh&G6i
|
||||
jPP*Mg/kqTGog%.*;'s:4'D6J-MjcAMX#-R=qV__Oop2cLN??&/^brML5[ONU7tR"2s&"ZEqFaQZM`C)
|
||||
a[VJiAGljHHSqEshO!*jOZAfWcL4.>CO<HAU3Yf@gTmP^-f/4(0db3hhYK[+Mjbo(%^2-u%kUWP89`F]
|
||||
;ApfPQ&CMFX/K&8Bg0s2`OR+1e!G<JcRY!30;A_R-U3kL<?;NS6f##*',`ZdegA,TWie-`)H9X)B*_Le
|
||||
L>4ZXND_kX4Rr#/-_<gseJ[VEhZocW8,_Y($eGbbhFNdkg8*Wdn;9>aF_)?X1XqKZ5W,Q7[qM)?2,!Nd
|
||||
<NP5):N\b@I+3oR^"/1KAij^U=VWDZ,Hn&p#+8UQB-c"I!uL3BDX2566YYPP)ln]ai2V(^b-c"]NUlef
|
||||
=__3PesB=[=#J!ec^IA87;L9X=As1MU$Zs]Q`uLDE`g^8e/;9Y%&k/#nFW.tH?uU.:m#:4eRkR]HZc:H
|
||||
P-+`H,P@T"UD#IbDftHa3e^j&FQja=>$%Q)H;X=oA*F+a4qg76^CNdk$ru.errs^DnT1<%^cB$O,[=<(
|
||||
CV,f`,-=tI>a31#XD=&*^'E&-oH!lQ55dMTW9940DFkSDZu)-i<ANV'G&`&9p=(*UG/5^7ANta:h(J<h
|
||||
:N>V<#Q:rDlo4?=l0+]L<A&=*>ZJ<?[M88X_3m6C`Uf^(c_GMXQ?74K/on,7*8/l</e:43*1[MAalaaG
|
||||
IZfaM&TpU2UFf&(X'JHB?eIjF'sAC\A0F9$PpP`.)Q1*)2mPY0??89+,@C6*g%E2190Ct8@u^jG8b$p:
|
||||
PoRrt[rdd?@o?UC$6F?#'Q$0Om6]@(2>]Gl&!c1nrPA(UAjj5(B)P?FrbTfNAorjPj@]c607g,QiCd/%
|
||||
&F_!Gg\C=cC3SLE^JmQL1hUqO/A6q`9rGB[DlDes^O:rBodoI+>$\V8K>Nr\bh=XKX25E_[kD1/j,Yj%
|
||||
l&j#Q5\7(@Q`+fjad8rD)o@q,`L--@lB%fgKqTY/Zpq0on0Nb)QO(5lmra#73i%*t*NF6=;ft_7.sc&F
|
||||
I96HteQUet83s*49@WP/DNTpnL!o?]e?I>p[T>Q)7!5O=^-m+E9YQ1++`42"b!rH!p^ZThA\hBUP):`d
|
||||
p"PfE?0"YO[SQZ2Jc*-?PJ4Da02]>^Uf/M>*FGOac"$.=^Yqh!1uV),\!ZnNFHQN,Mq^\LBj-NM;2[D[
|
||||
^6!^K@@%G48Gp_Xq\#%rq%AiNf.g!gc8AE_^'.?HFA1;1j\&,paHr".O=tV>(KE)fma8(9RbQuKQHH=F
|
||||
%<_#B&%QMok8'>r&>;(lH^j"Y.pC2d6^ZU9A[ajW'5^.NR:pU)]?.%R?VROYk*?VE5I/$lh]LnU\OVTO
|
||||
F3]TjEa,t@FJS\fZXY,\3T*L&,V?Jbg-"mF+O34<GbOB&?f6<FQZs+-Is8i;r)M92;ND#.<W*q(SSNTk
|
||||
EKMgsYG_&GUY:ncH;]89=,hBaC6u!,'u#\_+ibo=8qYK4aUqj\1Y\4oJJGY>jD,7'#3Q:NA"2n]MiN`O
|
||||
8K,Z#BiEcqV,l)liihG(g9WB5%&g,ZQ8FKn8D\:YB?ADone/_**P[c4\ds.TGqjqK0W`ccRH?[ll$r&g
|
||||
HTkD7X,8g3Zas3-31t]5],nd<g5@LB=kc)t)R(s-HZc:@(J(s:KJU":hopfIX1GJTZB4j^0'U&?%tI1&
|
||||
?_VK4Ip]<!iSp?pdJJ.DUPDn:m_cr:l/BH3J!LrC`A`'id_C.V_>Qo&l/bFlo_lKQ%ZU=SDdO8fGC>BL
|
||||
Wk1_KSBpUA!S-,>lQ=%j\pJ$QJt\G8I)7hA+oT"h'g9,.*S<$7+XeBI6n`a@Z=hG-(<WiL-I">r28+=#
|
||||
55LaTUIhWUApj?I6h]%u>^H4S2grHS(>-9FI&,L@mI#IEp9s,,kB=HA5jNIhT5N^7E2b1FDs+PLhn=8S
|
||||
bN8i'Z:E3Z,9;fRB1MYD(0^<4(E`8m42^G<nG"CaFlT.YDd`&ohfKn]+0s1!FXiAu>O=@t?PF"l+8K"R
|
||||
pi\g,2i>e:.Yi<1"Z&A*rWB;G\jfNXWIgo?VK3]kHVr1u$94;6E\8F$lT:0rK4?7!a4H+59NdD:nTjk"
|
||||
k]"VU8&I9<a4H*Jn!>BRLSA_`MW49=Rb*TA>ZmQJ1F>#pV%W\"jO$`s*efAR7B.:eo/g8=Y%YoBqs/r3
|
||||
H^E<.@IN)<bNnnKE1Y,=p"^h<5O--8O8f=^Z[-S;'qT84lG;qA[olWo^c:)!A>$V\c]0$/hEZ7>UXri,
|
||||
.s^M>]2`OLE_&YS.RU.ZZ07JA\TuVqX`O$F(H*oUF[""(I(S`uB-n6q'@2#.0$uu+DKMu/p^bg+I!o(/
|
||||
s-ODji5&>nQJ(s'.-V03#+@Y'a"t+RYu3m=l*k<l<E1%h.'[p+d85\R=6S,m>tftHgo]k>>qE2B+J*t%
|
||||
N(UAKk0t:"#,F:9bU-NH/'C%\Yo_1.Vj:qbhW($$#FBF6Q_.Zn1TGNd]T&OkS`a7`;0\7=]K(!BY:G5d
|
||||
1P$L?s7UQb.Fj<"rH(oun*jS2ospMPe""cpS]J2Y2t)7J/RZogC6-r2r2`jB=6e'.U'l.1%8G@WW]/pO
|
||||
A&g"GK/_,uUSURmofS.2H8m-YI2JJc4/oYfR/uJ88T.TkX,s*U>'QFOTF6cOK>L#=5<#L4f?G<M?fYWW
|
||||
bpC<W\tm*Fh2!m8.hXD]E3c(b;C80^&cN3,><m@tI;V[8e6-tB>HQne%#kQSn4h/%p@I6q>akGbSU5bK
|
||||
(S(,'NE*\hZhSb&3&eEp<&M:-5p"p;EA=nqc+18F^,<P]"\630B2Et2U>D,o@`DPkE;a6`KX[P:rWPh-
|
||||
,nb54X]?FBEYp+9Fs2lE##bD<a"a0VW&6eYS?)>W5B.JeS]ZdiLM'IcWk+ErWUoCQ2C=tqC`%/Me)uH`
|
||||
\MV5C[t].klBPup:a$hql.TAMfVqB5;,QAF?d<mqFVu2UIUbTXQ$MXNi=7lg"Z_ip\99b.a_SG/Rbc7H
|
||||
NMoVDr;,hfdDBohGCN\Kg<EDkUU>BD3kSTuUEI:O)IrWob4E40OcZ;U(LI$f+?u7#piel6eUJ<F"/8:J
|
||||
X6KTUPFOfT[;)L`;fmiaMG!`qTI>,.dhd)b4nd@h>?*Y-8$V`T<6X^(C0Br(&a6%"SRgXXDe7P'ABCiV
|
||||
rV;?MJe>3G@EOa]jiCK0EFnbjo_AFl2=$C/pIrN9Z=[.TY(?e>9T!*N>b$Hd*Te`/cABhQS9*[.=<i&?
|
||||
YQEG;N$M38jq/;__/iFD>aWp:r[$T*rS0ndablLHkB?<;:Q+qd.St%gg:W?P(355]ALVWd=b70YS1S*g
|
||||
fc#3;YMtr0(S%1`^7tTg51/SMHBKfqA&Eg;NK2(G0`cC9&W.gl+en)I'dJDc^5PO*PTa*7:rK5\rc"*5
|
||||
P\*-O&W$6-U<sR*aEb-,OXM9.F<a?U_N)t'RU;7L]4XNTd\84DDe4<A9+d$FBQW0?6'GOjXZcq80g?A3
|
||||
%N1.P8aXQ4#G][t`5P\nA@W6r)BD?\g6+Bb)q!l0n`8o/Kjh$U2chh^o^7s$'hL0nU<t"QWG(q3(pD1t
|
||||
oUXp2DrKPdltH#sS!`_iI+"eb4-#&idA%G;,$<MbBW5[[o-E\Z[At!SDN%&/FK8J7bf`L4"Q./-W\<Gr
|
||||
)V5a%[+#=8`em6Hq9?:-Se4t4_tGPu4aNl:oT1fBfN9a`@VkYVM+1jqHu;$CHnFq6U^K1$h3cURCRN_H
|
||||
kq-.?N^fkkNgh+sF4@1\n(,#:h,Pr5N#Cs<dHK-gN(3OmAMj7aIShpO]_$f87a\/]<X_F#iZ,O'X\#$F
|
||||
QPi]?aYU,J479DuLr)hTT0F=U9Diq:U3D..$g-o5Zj]_-8Zi3GfZ*&sFC;7b[VBJlVtr@%%\1C7hg[AD
|
||||
oJr5,gMpZEU[<L#Mc$nR0'?N.X8XD@l?#KA*h?&RFF#!s(\:$^>,NUSf;6rAHc8$/GAHu/&WNLC58eO1
|
||||
YL4a,&"V##1e4J8^6`tg`]$-JoK^i1JMFq39BgQTj>o=b\"CJj!jmt_7ujoKV_c3^k]HK'3ZU6Y6Ds"?
|
||||
odlZZ3)Jr5N,pH6oK\_cO=n_&mPm6Jq;*`pc3RS=FP\ORf&f]/UXRaTQSrXSFJKU3S[ZJq\^Z^H;CXma
|
||||
L69:"kttYG>u-VYf6]i^Wtj(IC8&"XI_;0#>Q-8Bh@2:PgTApN([6$P'+CEQaNl_u:=h.qdi"?rFLpVD
|
||||
F?9!LKhduI6`6tDU!jlHD4N;i8>l&;S9;DQCT5d6P')'nl1Dbt)r--Fr3G3@?2#\BdriiR7Ji]]VmCSE
|
||||
g9(BfXS\dAW>2M`WA1L'DuE"nY+&eXg'\l]Wg$SU7;OS:fDc:=T2l0Q=[=sol-L^8*+LV8<,>S$07t5.
|
||||
m&LdoUX.C!EU<pQp.O#PEU`VYc*W\=/kXQ=QScXdN<5M"flbdU93,EAY1rFE,O=K1pt>h97c5#>>OQ\s
|
||||
\0oe%))o\/+tRWFmpA2l@=$su0iouK9l2/F:D3EEh)ACodeO0E1;)c&dk\Su;9HS7\P<h,L`Kc!VFCfZ
|
||||
VQ<u0+a7=5m0-Nq:t,^$?9AO!HU-)Fi]em@\oP'pQQ<4%LX%LMk[6qJ1LP"&dZ-LYh59%;bY-Kh^_Tr7
|
||||
'!5YPE:(Ut1Sa[:>^<h#%Jmi*^/oFpYBF0=5LWUn[4]9<;jK8`o-2)r2J@hUZGPDg7r*R"Dl&^BSB9p/
|
||||
2j%kd8/u:6Q8lDJd,rMMA.O]#;5ga*C("R:1D@a8(\8>GHSL:3D)e4%]tN.'28Xc/][kt">-caF*O9',
|
||||
-HNA+3q+(aft"=rlhoFse<)?N)O%a]c;TU:IH%X=U*eqA`RMa@hK!F8r<%P(;"6X`%W-nnBYS'I4pU5@
|
||||
XjiN0Kj>?PeUmN5\=XA6b?!S4Dl8_.;.&D(;HQqaG#\1JY=!.,^BsAuOn\Z<]L?EPrMfaV3;P#(rpb_H
|
||||
o^B%ZXSt@$?2!`lSVnOGcVO5;]p-O)a<7K9r@=$Uqc@<JZ@>@3?[:IEC-FQ1_+3hW0&.C(=!H?2>`$i`
|
||||
jpk%l=qe9D:ZP+NC=iB#a)3lBMUG"s@/mk$X7S19[.-d-d'+jD?#P3-Ii]K.`)IKiXb)CgfZ[m/s,R+6
|
||||
VQubnA&l*fW[Q+\`r%`>a*][blu*aHht<E!kJ=VG1Ond142TE>=NCS`nY%*"7X`8Q`4et6aL_V$h&(u8
|
||||
YPJF#ZNoCV-\k5_bV)NgIi\IVp),hMa:=CIGlJkkj=KBq+I^N!_"dZD08tbc[,g9#BV2;nSS)RZGfD1$
|
||||
TXK)nOR8a)ouHkh$9<[!M#`Ci^f7`P'":TnanG"d\:5OF<CXQElXETk[7FF9e6^sKNo!1g,hhGYQG_^q
|
||||
acI-q@7#R(m?N&8Dp0+a\l""3`;AkfT!ZP!o%m`oS.?:8i+s:TEq*\H"X*uf`L!uZVjTE,f4(+?#T4o=
|
||||
h,f&RhcrLlb-K)i<r50+mprl@_V>ho>XIa=='S(:lSOq>ns(K,#=75>E!\ND`@9VMVHRQ.$Hcd5oo324
|
||||
%:=i9_:H]XJ'W%\T+I?=")\"?K:>Ud1GdV[!Dd;?M#+<"9'72,^DJ5F@r$mu0LJT>N'0KSJRTBHj:MqA
|
||||
bTeg#5#48)"4T;4oi+.0Y?M>qT>@:M"r#n9*di\;7(*3B9&qH>33R1W@k'<\F#%_,IrI5na->J\s,&!"
|
||||
YIF&s;D+*Gr/4q?,oZLkKMTosruJ`NogJ=J`=p3H't?dF]Dal%?E1sTiPFZbo7sEBmIn@m0G\O//M._o
|
||||
j<a%4@WU;&#Fep?Y:jCi\]$5<jqc9Kc7jbK2p\?pIX,WY\(>DCJ(AP9FBQ67Qi(;7ceTNtB>@&KjDQ,L
|
||||
Esq(&)L$5tka2(/*mY9U/FO..m/=I8$/Y7O]Ho0g2uG^lL=gEZTj)2T*R1>f"6;FG4B)pm=eOu0n759$
|
||||
rGn#^?dH+ooiC<S[7E>Zm0sN;%nPt7-#9`7M,[."kX^L!XneYCA;cCUL'Y+ErZupVk?8@AX/)O,8V/Vd
|
||||
Ac;r*5I@S0<Pl#d$8,fcL0Hmq&VRW"0#P:^R&5+tBA4<b>8eQ20G]f*[XmsYnZ%*NH)O1mV9DB8In@)L
|
||||
V30n]a_93GAs6n9WR>[,cnoZAbBJGTnF7G-4lkM8A)'m)m[T!%Dj)6t*6/6)N%ks+qU?sBn4f$ZaE+t7
|
||||
?$gMN7tiAQNM##6jcQ2U+4d!K3r#R/U(:>r4hg($ceR8I&m'OZVCKfu5do[lN#(EjDrf!>mF<,V\0,0p
|
||||
YZB?rbQX(q>CM/7^\:Z14JJ>2XCgb&aIn(AEEK$\5bRWNPC=XW\JcuH1P44P)']C`7[&"+F`n&FQ-:N$
|
||||
*m4bu$RhuOe[J/A4Y<#Eh_*%T/KDt75n:F`%\J74qVCn'.MC>"[>iB=JKu%?gm^cV!bib!HZ9#JXcc;*
|
||||
G1hc-!=ebPh4'fb>u\p2InJ/q4&X`T_TJt0J?gD71:M0HP'net-<n4.bI21"2s,lDgh#t#NQ)TL^#`$/
|
||||
6nN55=WIi^l=0)pjBk\sV7`/XQM/")8M600A"Jb$]"&n>rS532n5+rkZThtEH4RD]di^[UfcR_^9[;S<
|
||||
))02Zmku"WN&aR-Z)o7;JZn='=_Ba]-r\e(SbY7Hm4\*&1EN&VjR1[s7rRA_Uh6^hUMOJVYKZMcSPmR-
|
||||
(E03'0]=LPdRVMKP0"$ASB4,K*G^[!MR:ijIea6%jml/&Rh!'#n6qYV_If!Y_bpFe]__hU-SM:cf^*WL
|
||||
KI?\?YW.S"gKX[kNr!]rhf25nF&i5\n)phKqS!o@qJ>U^Nq[BC2L`GFb2+1a0oC!`kl4^HJTkGP/Et#<
|
||||
!B4<d,"l0Qjs<'9Z]GJMJP^/<9t(Zoe@(ol8f,nL4PU?ckZ(Z$@X]:I9^b!0.WQc<c[c@?[mAec_E,r/
|
||||
4?25Co3gr54d8b%H\BW/[SMWU+j1lY>AmIkgRjIK?/@8:l$(G]J%<,O1A=kI+8pj$';bE)!9X9^98W*Q
|
||||
)(?IL0Q[MJ8[L9EoBL[/SST,[NW+pBZV;LgD7%?\5UQrEDu4M&V)[8M0+2=#Nu;##hXQ2GH/hJA:UX]p
|
||||
^ODS8TAhZ^8*k"Hp(B?[rh,5?X?i.<nD<-G_\,b\S!e.9`t.l>4_R<Lp^Ttuks!==pra)Q5KU.KP"atA
|
||||
'BHTXG0XUDBP1ItBg:4jA=Fs[$?.4b@rNY&N`ZN.j3W)Q>?YGpk;V8NYk>hF0oI[3Sd0P>Y40=V<R0Ys
|
||||
[.A8b`!<O7jK,O&D(+'nCs%Wqq'LI7V.Af]Waso$=YC/p:eKes`:4OBHb*'ph-MXfY;\J(V(d0Q;dU.Z
|
||||
IDdN2Nq:)P.imuB49\93pd%.>XOPbCb4R4_auRd_*F@FLr0@41jh:&7K@$UqA5!O3#r]puND[f?R5HJS
|
||||
7cdM'r'3Sq(@"ih_C<fAi'^n/=2FZHhBfI4;O30pPW9Vp8/bf&@,,OI_2[K^aWc!"'WdX+A)i]A-J)@h
|
||||
GZ/HlNU\dS=^r_b32J]aD2F)_/tAN.rb1[s:5_mdPXO\<SP*u)IMf?'J")6!a.lKNFY]KT%g;d0XP3jq
|
||||
j(n*Qc_\YdpX@K0Lbst:L!O*6Qch<ke.Gr6(!!9mK?5fMmr!urb[[q5P&=^Ekhe2.nnFA<Sa`'#XT+<U
|
||||
rdTI%ERV9G,cU]a0G;?fC%H%g^n"i7UAc\"F-F&Q_%4'n[s+Ql>^7*M5Vl#EcHcAiba_?'Y;e\GdAP_B
|
||||
r,luN<:p>(C,02dHr:&s'9[Wg;C6V(`nL6t2QEGmpV(p7p`lV>U]1j+_)i/&WW"u6r4h+."!bb>3dVY'
|
||||
rT.5XoP>Tt[,3Q<GeGia!B)F_(UqthK8^OsRCh9[NohCbD3/N\r0@GEC8"qBHKCh7=dAO#%eQuap-^F<
|
||||
&Wd34,M<`7U.1>ZDo,S*kh!N,1["`6GML/sgu(E4Y2J%U=tAJ@fa'\>6go<pWN[*8Y9;TH3c9oqc)\Lb
|
||||
CXm@62Qi77\%Y#T!)W3X(c,;7IJ7(n8fm#Znbjua_i1<AdrIA,..L^g?,ioENa;Z<(eE,mI>m%VZMeq8
|
||||
j*XK-[nf'1b(.S2]iF/`XK,P>$XZiE11Vq`C$iJF\%:E`L:ag3(G&0u(S]s"iZ$j*8Un[oZ>g,h/Z7.K
|
||||
#(3`d[&WGTIs4CYDoB9l<G2@q;0SFh(N$*]+(%De,AZ+Q^Goi3jW_G25tQ1idfM*Vq5XD'MpZ6LDq'=,
|
||||
'g.T>64@I5TFA!ZoYatX3t9D3h!nLVQSQX6ZJ48r,k$&EU?<aUV[K=XP4NrMh"HTf,*lU^8ZCu8Q6C3S
|
||||
o_a5pH-PlT9KqBS7Uq8c`:@9J3?'PDCB#tKnrH](l0<@Z2O]$:]]@M++(jJ$7u*pg+[:0aCiGRk&8k!O
|
||||
N6Q_3pIsH>>e$uiVoAr.mDHF#1n06kC2"1,`4&tUDZNbQS4f"oB.\![Y,WAq0(eXdk/(+ol<%%bDrI/6
|
||||
>qsI9n]%$K*+DBI^,Oq6;=@cTQ&XOBP"VS<$a$(kAC;<SGBn-*_Dla//T`\[JFg9aAWb.p929"[c70LV
|
||||
$R98NnH*BCI+!ugW@ps.e\>AZ(<oMB`Fc55nFSCM896@=&g:jrAm@un<m@OFLq6[*-i:Kb]#>_[\ao>X
|
||||
,ME]M@R/8sE[&s?H#'C3IK^e/=ir,)j1YRrXa3U8>+>0Ja%3d?P8Vb'Gg@lrg-s^?1U\QNMaf-fPF`.o
|
||||
;oX3D,\FToQIe7]b0h,HDD.!B+smBSne&Y/bePg<4r>:\j4hNnbZWH;fZD2fU.`qM?T/`:@,Ri#i\&jK
|
||||
)<&W+I>g>B44iJ]<pXRh0kR]R8T%C_Ln+FeFCtA:*uOt8*_7pBUq0r=A:..%JtY8?-&_Rb[@>g$PMTJ"
|
||||
G&1*;2P$,(R<_W0?DAQ&_0pI*[0ufMX]rI7*_EJiHb*Gp^#lSA!cGekILgI+#ii!%N0q^JjTRl[?0h@U
|
||||
GhQaB?KBQ]?[+M&q=K[^I*0XEM)iBK$:aN&oQf/qbaCbEXha&+d+s^"40/.ZRH)3_en"/<4,3L>q-cbV
|
||||
^<";E1sJ4LCmV^4?*j/<L]!;mp+%b-/A9(km)R]#M0u#0fl'pi$SO8q7f@P$bZ@AR='BmaLcMLj7c]-n
|
||||
\fu32ItEN'H2Pj@q,4nho!0gbcTAOt=??+UE;F*L]4QK![0thD`n9oYN?^:()FJnlQ4Y>0pmSPLD,(Vd
|
||||
fi!bha(ICaCZ5epHBu*lfdHAYF,k!@8iEqhH11b_eNX!Q.0nV[_;cb0Ui?f8dT^a8h&&A9kbD9M?c(I0
|
||||
7-8?"q%C5?6Y*RB2SPP#9=+%k/.M(,)qiAr?7f1EA81ShW'nn47,=S?<]`sNgXD.hY6i[En1X$!Cd`4#
|
||||
M7T3-?-hVM1`"h=$Vh,_`V*UNpT/5L4=D$!!K!c2<W\\<j\'DXqZ9qEOQH%(P<G*'#[_&9Eqp?[%HD-!
|
||||
@S6baHDtP,='sQZ>3pI/OAbidS!3YsDO*0,`TOld?U?`"$.Y=pTdRsKn7^I@Yl(=LUP7s/rNb?R]e-a'
|
||||
.qpl8p2PHda[Np3B&nSlM1b`]VQh"b'!2shFI&dE8ToaK2gpNfCaZ)-g)IbZB9Rb#NEH`D>fh]66tUgD
|
||||
p_'"B8+Ir]j+ZPe(tk68\EZXjn,=Wbgsb\66smfH:-,7OBf]rNp!0C?fjskqH>[VlZgo^!7q`>-&hj+C
|
||||
dk')*VdpD27R$@o_)3q$q2QQO$0:jZ%<nE*7K%EM(3U_%(3^e&(-1.HNnn%2D76cDJ8_bb)cUgU[TAB]
|
||||
fcN/uZ4-<b)qNa_k?Im1>pNIUqIY$'okJc[lohWLrT0lgh7lI@f(!P&UFFO!M4^!<pCWT<b7:j+I3<RK
|
||||
gAcoB(MMUJY4hTn^\tiXYQ';^f)?`d6`gmQI+9!L^6C87,j`r2r"hQk8NJSV(4)-='3[RJ1jNL)HafN`
|
||||
m$T./c0csoNZFb]NmI'-lh:),])=ecj7mt<WpUS=l8_Mknp0d"3(d'%Xl762HM`&Ych.!Z@Q1^V/S'A"
|
||||
i%2=lFNoaslRU;.B#rlY/<[b<hU^`U8M8uj"dZn>3,+m8J$7PRPhp:d?&I>.o&qp:__,Zp[T^a7CHGKO
|
||||
1Angtbl_0;]p6@a]'@Gq'+TMqft)sRf6fVX%NG!/pR85.,hCdm"shLMUUScTPs2V\iU;oboD!Z.^AU4>
|
||||
>YF^Xa'ql9[LB,KSrf]a4P;rX-@%MOomp6^Zb!'HNBNi'B%QG,[?=WBeUtc@fpd+Y00XMJh<M\Y*7XdA
|
||||
g\@j4Ec&PNNit&g\;:0KDh=OQ2Cd\h=D-u0\]dQ!h,;jbh/0?"DL>=tHRZ(6mF[RVZWi@P[!1c3BoB@1
|
||||
mX3D5j41>]]\_/Q^dY>lrUfQ`DlWe"_gWZ:34lg:p;!Sd.l_qF;Yq!kTD]#hYM"+5O775`Y=X)NJ*hA[
|
||||
I-R?ng!u\f7i]7te'q.Z$BIt`iO_8!X7$gPK(&F*gNS#@RBMsY>\Xc^1S<Ht]uAha]kSHU1q_buod7,p
|
||||
m_*(uD6tE]AI3P+r&[(Tkl#Y]n((>@Z`Om7Y!,()/_o/41&(Fh;2qE=^Y7j2F$e]*9;L=OKuH+>Oim8;
|
||||
5!srD).ED+#7Yaq$d2`*04j>4\LU_:ne[GZi>-\tm.9S'lJ&sDLX$0;$TX_?\9fUB%c.(H-O[n<B`ri*
|
||||
g,+E)?aJ==fjV>o*^n89\>6'geE?A"J\mmW&"]iOhu*8mDYpuS#OQhQpV+6L$Af.S=i\9V/eC0iH.USN
|
||||
QY)FW?;cdV*d`m[M:,s"0c`9EkPBjWWH;Dt]&ZM@&+1"^H'/C1Rd@[M_rp@6,,;ecdJN[Fn7J!C/VW).
|
||||
N<cTUdJo$AiB6ID^hXoW*jG;'(p^fIPK)+r_i?iAB,MGrnafX.Rr->WHM5P2T'>sdVn]%C8"\IOi!Q"J
|
||||
GK<UTg4S`THI>@F;P*kcTKRJNH:;_+BHbsS\CMYb:@5eI*BmO.l=W;.;ML]7fGTE\::+^0/"ZGKiD)$m
|
||||
^U-20NFm#@>-3:-oVJJ*I'lVebF..EoD2&sC/;TU8DP:BUshrHk7?aS7]/<q7pu*ce)(b7hFjDV*U$GQ
|
||||
//b)KpSS8E$KJF7d9D%$-q*Ih2BeKD[ed4(%6IHD\jp\BpA]\DqJSUlIGF;kVO)f3prC]fhpqc(a$9Oo
|
||||
f73b>^ZUZms6l$@VeZ8'pHS]V?iT6fpTE+4"O=(<bknC"k9$q.mCrSSO8o,qn_\fn*uFoMFo~>
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj
|
||||
40412
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Parent null
|
||||
/Type /Pages
|
||||
/MediaBox [0.0000 0.0000 537.00 194.00]
|
||||
/Resources 8 0 R
|
||||
/Kids [5 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
[/PDF /Text /ImageC]
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/S /Transparency
|
||||
/CS /DeviceRGB
|
||||
/I true
|
||||
/K false
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Alpha1
|
||||
<<
|
||||
/ca 1.0000
|
||||
/CA 1.0000
|
||||
/BM /Normal
|
||||
/AIS false
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/ProcSet 9 0 R
|
||||
/ExtGState 11 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 12
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000315 00000 n
|
||||
0000041155 00000 n
|
||||
0000000445 00000 n
|
||||
0000000521 00000 n
|
||||
0000000609 00000 n
|
||||
0000041131 00000 n
|
||||
0000041609 00000 n
|
||||
0000041325 00000 n
|
||||
0000041364 00000 n
|
||||
0000041466 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 12
|
||||
/Root 2 0 R
|
||||
/Info 1 0 R
|
||||
>>
|
||||
startxref
|
||||
41682
|
||||
%%EOF
|
12
satrs-book/README.md
Normal file
12
satrs-book/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
sat-rs book
|
||||
=========
|
||||
|
||||
High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||
|
||||
## Building
|
||||
|
||||
If you have not done so, install `mdbook` using `cargo install mdbook --locked`.
|
||||
|
||||
```sh
|
||||
mdbook build
|
||||
```
|
@ -4,3 +4,6 @@ language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The sat-rs book"
|
||||
|
||||
[output.html]
|
||||
[output.linkcheck]
|
||||
|
@ -2,9 +2,16 @@
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Design](./design.md)
|
||||
|
||||
# Basic concepts and components
|
||||
|
||||
- [Communication with Space Systems](./communication.md)
|
||||
- [Working with Constrained Systems](./constrained-systems.md)
|
||||
- [Actions](./actions.md)
|
||||
- [Modes and Health](./modes-and-health.md)
|
||||
- [Housekeeping Data](./housekeeping.md)
|
||||
- [Events](./events.md)
|
||||
|
||||
# Example project
|
||||
|
||||
- [The satrs-example application](./example.md)
|
||||
|
@ -1,3 +1,5 @@
|
||||
<div id="communication-chapter"/>
|
||||
|
||||
# Communication with sat-rs based software
|
||||
|
||||
Communication is a vital topic for remote system which are usually not (directly)
|
||||
@ -15,10 +17,16 @@ it is still centered around small packets. `sat-rs` provides support for these E
|
||||
standards and also attempts to fill the gap to the internet protocol by providing the following
|
||||
components.
|
||||
|
||||
1. [UDP TMTC Server](https://docs.rs/satrs-core/0.1.0-alpha.0/satrs_core/hal/host/udp_server/index.html#).
|
||||
1. [UDP TMTC Server](https://docs.rs/satrs-core/0.1.0-alpha.0/satrs_core/hal/host/udp_server/index.html).
|
||||
UDP is already packet based which makes it an excellent fit for exchanging space packets.
|
||||
2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol
|
||||
to always deliver complete packets.
|
||||
2. [TCP TMTC Server Components](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/index.html).
|
||||
TCP is a stream based protocol, so the framework provides building blocks to parse telemetry
|
||||
from an arbitrary bytestream. Two concrete implementations are provided:
|
||||
- [TCP spacepackets server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
|
||||
to parse tightly packed CCSDS Spacepackets.
|
||||
- [TCP COBS server](https://docs.rs/satrs-core/0.1.0-alpha.1/satrs_core/hal/std/tcp_server/struct.TcpTmtcInCobsServer.html)
|
||||
to parse generic frames wrapped with the
|
||||
[COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).
|
||||
|
||||
# Working with telemetry and telecommands (TMTC)
|
||||
|
||||
|
@ -18,7 +18,7 @@ running out of memory (something even Rust can not protect from) or heap fragmen
|
||||
A huge candidate for heap allocations is the TMTC and handling. TC, TMs and IPC data are all
|
||||
candidates where the data size might vary greatly. The regular solution for host systems
|
||||
might be to send around this data as a `Vec<u8>` until it is dropped. `sat-rs` provides
|
||||
another solution to avoid run-time allocations by offering and recommendng pre-allocated static
|
||||
another solution to avoid run-time allocations by offering pre-allocated static
|
||||
pools.
|
||||
|
||||
These pools are split into subpools where each subpool can have different page sizes.
|
||||
|
@ -13,4 +13,4 @@ event components recommended by this framework do not really need this service.
|
||||
The following images shows how the flow of events could look like in a system where components
|
||||
can generate events, and where other system components might be interested in those events:
|
||||
|
||||

|
||||

|
||||
|
152
satrs-book/src/example.md
Normal file
152
satrs-book/src/example.md
Normal file
@ -0,0 +1,152 @@
|
||||
# sat-rs Example Application
|
||||
|
||||
The `sat-rs` framework includes a monolithic example application which can be found inside
|
||||
the [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||
subdirectory of the repository. The primary purpose of this example application is to show how
|
||||
the various components of the sat-rs framework could be used as part of a larger on-board
|
||||
software application.
|
||||
|
||||
## Structure of the example project
|
||||
|
||||
The example project contains components which could also be expected to be part of a production
|
||||
On-Board Software. A structural diagram of the example application is given to provide
|
||||
a brief high-level view of the components used inside the example application:
|
||||
|
||||

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

|
||||
|
||||
It should be noted that an arrow coming out of a component group refers to multiple components
|
||||
in that group. An explanation for important component groups will be given.
|
||||
|
||||
#### TMTC component group
|
||||
|
||||
This groups is the primary interface for clients to communicate with the on-board software
|
||||
using a standardized TMTC protocol. The example uses the
|
||||
[ECSS PUS protocol](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
|
||||
In the future, this might be extended with the
|
||||
[CCSDS File Delivery Protocol](https://public.ccsds.org/Pubs/727x0b5.pdf).
|
||||
|
||||
A client can connect to the UDP or TCP server to send these PUS packets to the on-board software.
|
||||
These servers then forward the telecommads to a centralized TC source component using a dedicated
|
||||
message abstraction.
|
||||
|
||||
This TC source component then demultiplexes the message and forwards it to the relevant components.
|
||||
Right now, it forwards all PUS requests to the respective PUS service handlers using the PUS
|
||||
receiver component. The individual PUS services are running in a separate thread. In the future,
|
||||
additional forwarding to components like a CFDP handler might be added as well. It should be noted
|
||||
that PUS11 commands might contain other PUS commands which should be scheduled in the future.
|
||||
These wrapped commands are forwarded to the PUS11 handler. When the schedule releases those
|
||||
commands, it forwards the released commands to the TC source again. This allows the scheduler
|
||||
and the TC source to run in separate threads and keeps them cleanly separated.
|
||||
|
||||
All telemetry generated by the on-board software is sent to a centralized TM funnel. This component
|
||||
also performs a demultiplexing step to forward all telemetry to the relevant TM recipients.
|
||||
In the example case, this is the last UDP client, or a connected TCP client. In the future,
|
||||
forwarding to a persistent telemetry store and a simulated communication component might be
|
||||
added here as well. The centralized TM funnel also takes care of some packet processing steps which
|
||||
need to be applied for each ECSS PUS packet, for example CCSDS specific APID incrementation and
|
||||
PUS specific message counter incrementation.
|
||||
|
||||
#### Application Group
|
||||
|
||||
The application components generally do not receive raw PUS packets directly, even though
|
||||
this is certainly possible. Instead, they receive internalized messages from the PUS service
|
||||
handlers. For example, instead of receiving a PUS 8 Action Telecommand directly, an application
|
||||
component will receive a special `ActionRequest` message type reduced to the basic important
|
||||
information required to execute a request. These special requests are denoted by the blue arrow
|
||||
in the diagram.
|
||||
|
||||
It should be noted that the arrow pointing towards the event manager points in both directions.
|
||||
This is because the application components might be interested in events generated by other
|
||||
components as well. This mechanism is oftentimes used to implement the FDIR functionality on system
|
||||
and component level.
|
||||
|
||||
#### Shared components and functional interfaces
|
||||
|
||||
It should be noted that sometimes, a functional interface is used instead of a message. This
|
||||
is used for the generation of verification telemetry. The verification reporter is a clonable
|
||||
component which generates and sends PUS1 verification telemetry directly to the TM funnel. This
|
||||
introduces a loose coupling to the PUS standard but was considered the easiest solution for
|
||||
a project which utilizes PUS as the main communication protocol. In the future, a generic
|
||||
verification abstraction might be introduced to completely decouple the application layer from
|
||||
PUS.
|
||||
|
||||
The same concept is applied if the backing store of TMTC packets are shared pools. Every
|
||||
component which needs to read telecommands inside that shared pool or generate new telemetry
|
||||
into that shared pool will received a clonable shared handle to that pool.
|
||||
|
||||
The same concept could be extended to power or thermal handling. For example, a shared power helper
|
||||
component might be used to retrieve power state information and send power switch commands through
|
||||
a functional interface. The actual implementation of the functional interface might still use
|
||||
shared memory and/or messages, but the functional interface makes using and testing the interaction
|
||||
with these components easier.
|
Binary file not shown.
Before Width: | Height: | Size: 70 KiB |
BIN
satrs-book/src/images/events/event_man_arch.png
Normal file
BIN
satrs-book/src/images/events/event_man_arch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
BIN
satrs-book/src/images/satrs-example/satrs-example-dataflow.png
Normal file
BIN
satrs-book/src/images/satrs-example/satrs-example-dataflow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 254 KiB |
BIN
satrs-book/src/images/satrs-example/satrs-example-structure.png
Normal file
BIN
satrs-book/src/images/satrs-example/satrs-example-structure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
@ -21,3 +21,9 @@ A lot of the architecture and general design considerations are based on the
|
||||
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/)
|
||||
and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
|
||||
|
||||
# Getting started with the example
|
||||
|
||||
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
||||
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
||||
the example application.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "satrs-core"
|
||||
version = "0.1.0-alpha.1"
|
||||
version = "0.1.0-alpha.2"
|
||||
edition = "2021"
|
||||
rust-version = "1.61"
|
||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||
@ -15,8 +15,6 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
|
||||
[dependencies]
|
||||
delegate = ">0.7, <=0.10"
|
||||
paste = "1"
|
||||
# TODO: Remove this as soon as the image including the description was moved to the satrs-book.
|
||||
embed-doc-image = "0.1"
|
||||
|
||||
[dependencies.smallvec]
|
||||
version = "1"
|
||||
@ -73,11 +71,11 @@ features = ["all"]
|
||||
optional = true
|
||||
|
||||
[dependencies.spacepackets]
|
||||
version = "0.7.0-beta.2"
|
||||
version = "0.9.0"
|
||||
default-features = false
|
||||
# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git"
|
||||
# rev = "79d26e1a6"
|
||||
# branch = ""
|
||||
# rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7"
|
||||
# branch = "main"
|
||||
|
||||
[dependencies.cobs]
|
||||
git = "https://github.com/robamu/cobs.rs.git"
|
||||
@ -91,6 +89,7 @@ zerocopy = "0.7"
|
||||
once_cell = "1.13"
|
||||
serde_json = "1"
|
||||
rand = "0.8"
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies.postcard]
|
||||
version = "1"
|
||||
@ -123,4 +122,4 @@ doc-images = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "doc_cfg"]
|
||||
rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"]
|
||||
|
@ -1,259 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.22-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0" xml:space="preserve"/>
|
||||
<node id="n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.936037063598633" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="150.1282958984375" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
|
||||
<y:Fill color="#FFFF00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.21258544921875" x="58.393707275390625" xml:space="preserve" y="21.27395296096796">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="13.4398193359375" xml:space="preserve" y="14.547905921936035">Event
|
||||
Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="10.719819335937473" xml:space="preserve" y="14.547905921936035">Event
|
||||
Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="8.719819335937359" xml:space="preserve" y="14.547905921936035">Event
|
||||
Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
|
||||
<y:Fill color="#FF9900" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="16.079819335937373" xml:space="preserve" y="14.547905921936035">Event
|
||||
Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.78865051269531" x="16.605674743652344" xml:space="preserve" y="14.547905921936035">PUS Service 5
|
||||
Event Reporting
|
||||
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n7">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.08859252929688" x="17.2757037353515" xml:space="preserve" y="14.547905921936035">PUS Service 19
|
||||
Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n8">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/>
|
||||
<y:Fill color="#FFCC99" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.932403564453125" x="13.673798217773424" xml:space="preserve" y="14.547905921936035">Telemetry
|
||||
Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n9">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/>
|
||||
<y:Fill hasColor="false" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="143.6875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="181.591796875" x="8.373079774614325" xml:space="preserve" y="7.444138124199753">Subscriptions
|
||||
|
||||
1. Event Creator 0 subscribes
|
||||
for event 0
|
||||
2. Event Creator 1 subscribes
|
||||
for event group 2
|
||||
3. PUS Service 5 handler
|
||||
subscribes for all events
|
||||
4. PUS Service 19 handler
|
||||
subscribes for all events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.4602795077105583" nodeRatioY="-0.45641605313700395" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<edge id="e0" source="n4" target="n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="8.639817810058275" xml:space="preserve" y="29.00100609374465">event 1
|
||||
(group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e1" source="n2" target="n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="11.93999999999994" tx="-83.5" ty="0.0">
|
||||
<y:Point x="832.0" y="455.16999999999996"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="25.334655000000453" xml:space="preserve" y="-40.972107505798476">event 0
|
||||
(group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e2" source="n3" target="n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="5.6761352539062955" xml:space="preserve" y="27.551854405966765">event 2
|
||||
(group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e3" source="n5" target="n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-6.275467980295616" sy="0.0" tx="57.5" ty="8.5">
|
||||
<y:Point x="1149.8845320197042" y="545.1799999999998"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.38468933105469" x="26.667665869801795" xml:space="preserve" y="43.287014528669715">event 3 (group 2)
|
||||
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="75.3599999999999" distanceToCenter="true" position="left" ratio="0.2967848459873102" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n1" target="n6">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n1" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203"><<all events>><y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n1" target="n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Point x="658.6" y="536.6799999999998"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.69230651855469" x="-131.99129340961497" xml:space="preserve" y="-45.45208675384538">event 1
|
||||
event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n1" target="n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="46.14430236816406" x="-54.352158195608126" xml:space="preserve" y="-79.29459128622307">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n6" target="n8">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="87.40060424804688" x="-100.50030212402339" xml:space="preserve" y="11.337896156311103">enabled Events
|
||||
as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
Binary file not shown.
Before Width: | Height: | Size: 70 KiB |
File diff suppressed because it is too large
Load Diff
770
satrs-core/src/cfdp/filestore.rs
Normal file
770
satrs-core/src/cfdp/filestore.rs
Normal file
@ -0,0 +1,770 @@
|
||||
use alloc::string::{String, ToString};
|
||||
use core::fmt::Display;
|
||||
use crc::{Crc, CRC_32_CKSUM};
|
||||
use spacepackets::cfdp::ChecksumType;
|
||||
use spacepackets::ByteConversionError;
|
||||
#[cfg(feature = "std")]
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "std")]
|
||||
pub use stdmod::*;
|
||||
|
||||
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FilestoreError {
|
||||
FileDoesNotExist,
|
||||
FileAlreadyExists,
|
||||
DirDoesNotExist,
|
||||
Permission,
|
||||
IsNotFile,
|
||||
IsNotDirectory,
|
||||
ByteConversion(ByteConversionError),
|
||||
Io {
|
||||
raw_errno: Option<i32>,
|
||||
string: String,
|
||||
},
|
||||
ChecksumTypeNotImplemented(ChecksumType),
|
||||
}
|
||||
|
||||
impl From<ByteConversionError> for FilestoreError {
|
||||
fn from(value: ByteConversionError) -> Self {
|
||||
Self::ByteConversion(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FilestoreError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
FilestoreError::FileDoesNotExist => {
|
||||
write!(f, "file does not exist")
|
||||
}
|
||||
FilestoreError::FileAlreadyExists => {
|
||||
write!(f, "file already exists")
|
||||
}
|
||||
FilestoreError::DirDoesNotExist => {
|
||||
write!(f, "directory does not exist")
|
||||
}
|
||||
FilestoreError::Permission => {
|
||||
write!(f, "permission error")
|
||||
}
|
||||
FilestoreError::IsNotFile => {
|
||||
write!(f, "is not a file")
|
||||
}
|
||||
FilestoreError::IsNotDirectory => {
|
||||
write!(f, "is not a directory")
|
||||
}
|
||||
FilestoreError::ByteConversion(e) => {
|
||||
write!(f, "filestore error: {e}")
|
||||
}
|
||||
FilestoreError::Io { raw_errno, string } => {
|
||||
write!(
|
||||
f,
|
||||
"filestore generic IO error with raw errno {:?}: {}",
|
||||
raw_errno, string
|
||||
)
|
||||
}
|
||||
FilestoreError::ChecksumTypeNotImplemented(checksum_type) => {
|
||||
write!(f, "checksum {:?} not implemented", checksum_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for FilestoreError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
FilestoreError::ByteConversion(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl From<std::io::Error> for FilestoreError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::Io {
|
||||
raw_errno: value.raw_os_error(),
|
||||
string: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VirtualFilestore {
|
||||
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
||||
|
||||
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
||||
|
||||
/// Truncating a file means deleting all its data so the resulting file is empty.
|
||||
/// This can be more efficient than removing and re-creating a file.
|
||||
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
||||
|
||||
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError>;
|
||||
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError>;
|
||||
|
||||
fn read_data(
|
||||
&self,
|
||||
file_path: &str,
|
||||
offset: u64,
|
||||
read_len: u64,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(), FilestoreError>;
|
||||
|
||||
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>;
|
||||
|
||||
fn filename_from_full_path(path: &str) -> Option<&str>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Convert the path string to a Path
|
||||
let path = Path::new(path);
|
||||
|
||||
// Extract the file name using the file_name() method
|
||||
path.file_name().and_then(|name| name.to_str())
|
||||
}
|
||||
|
||||
fn is_file(&self, path: &str) -> bool;
|
||||
|
||||
fn is_dir(&self, path: &str) -> bool {
|
||||
!self.is_file(path)
|
||||
}
|
||||
|
||||
fn exists(&self, path: &str) -> bool;
|
||||
|
||||
/// This special function is the CFDP specific abstraction to verify the checksum of a file.
|
||||
/// This allows to keep OS specific details like reading the whole file in the most efficient
|
||||
/// manner inside the file system abstraction.
|
||||
///
|
||||
/// The passed verification buffer argument will be used by the specific implementation as
|
||||
/// a buffer to read the file into. It is recommended to use common buffer sizes like
|
||||
/// 4096 or 8192 bytes.
|
||||
fn checksum_verify(
|
||||
&self,
|
||||
file_path: &str,
|
||||
checksum_type: ChecksumType,
|
||||
expected_checksum: u32,
|
||||
verification_buf: &mut [u8],
|
||||
) -> Result<bool, FilestoreError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod stdmod {
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{BufReader, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NativeFilestore {}
|
||||
|
||||
impl VirtualFilestore for NativeFilestore {
|
||||
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
||||
if self.exists(file_path) {
|
||||
return Err(FilestoreError::FileAlreadyExists);
|
||||
}
|
||||
File::create(file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
||||
if !self.exists(file_path) {
|
||||
return Err(FilestoreError::FileDoesNotExist);
|
||||
}
|
||||
if !self.is_file(file_path) {
|
||||
return Err(FilestoreError::IsNotFile);
|
||||
}
|
||||
fs::remove_file(file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
||||
if !self.exists(file_path) {
|
||||
return Err(FilestoreError::FileDoesNotExist);
|
||||
}
|
||||
if !self.is_file(file_path) {
|
||||
return Err(FilestoreError::IsNotFile);
|
||||
}
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError> {
|
||||
fs::create_dir(dir_path).map_err(|e| FilestoreError::Io {
|
||||
raw_errno: e.raw_os_error(),
|
||||
string: e.to_string(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError> {
|
||||
if !self.exists(dir_path) {
|
||||
return Err(FilestoreError::DirDoesNotExist);
|
||||
}
|
||||
if !self.is_dir(dir_path) {
|
||||
return Err(FilestoreError::IsNotDirectory);
|
||||
}
|
||||
if !all {
|
||||
fs::remove_dir(dir_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
fs::remove_dir_all(dir_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_data(
|
||||
&self,
|
||||
file_name: &str,
|
||||
offset: u64,
|
||||
read_len: u64,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(), FilestoreError> {
|
||||
if buf.len() < read_len as usize {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: read_len as usize,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
if !self.exists(file_name) {
|
||||
return Err(FilestoreError::FileDoesNotExist);
|
||||
}
|
||||
if !self.is_file(file_name) {
|
||||
return Err(FilestoreError::IsNotFile);
|
||||
}
|
||||
let mut file = File::open(file_name)?;
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
file.read_exact(&mut buf[0..read_len as usize])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError> {
|
||||
if !self.exists(file) {
|
||||
return Err(FilestoreError::FileDoesNotExist);
|
||||
}
|
||||
if !self.is_file(file) {
|
||||
return Err(FilestoreError::IsNotFile);
|
||||
}
|
||||
let mut file = OpenOptions::new().write(true).open(file)?;
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
file.write_all(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_file(&self, path: &str) -> bool {
|
||||
let path = Path::new(path);
|
||||
path.is_file()
|
||||
}
|
||||
|
||||
fn exists(&self, path: &str) -> bool {
|
||||
let path = Path::new(path);
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn checksum_verify(
|
||||
&self,
|
||||
file_path: &str,
|
||||
checksum_type: ChecksumType,
|
||||
expected_checksum: u32,
|
||||
verification_buf: &mut [u8],
|
||||
) -> Result<bool, FilestoreError> {
|
||||
match checksum_type {
|
||||
ChecksumType::Modular => {
|
||||
if self.calc_modular_checksum(file_path)? == expected_checksum {
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
ChecksumType::Crc32 => {
|
||||
let mut digest = CRC_32.digest();
|
||||
let file_to_check = File::open(file_path)?;
|
||||
let mut buf_reader = BufReader::new(file_to_check);
|
||||
loop {
|
||||
let bytes_read = buf_reader.read(verification_buf)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
digest.update(&verification_buf[0..bytes_read]);
|
||||
}
|
||||
if digest.finalize() == expected_checksum {
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
ChecksumType::NullChecksum => Ok(true),
|
||||
_ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeFilestore {
|
||||
pub fn calc_modular_checksum(&self, file_path: &str) -> Result<u32, FilestoreError> {
|
||||
let mut checksum: u32 = 0;
|
||||
let file = File::open(file_path)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut buffer = [0; 4];
|
||||
|
||||
loop {
|
||||
let bytes_read = buf_reader.read(&mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
// Perform padding directly in the buffer
|
||||
(bytes_read..4).for_each(|i| {
|
||||
buffer[i] = 0;
|
||||
});
|
||||
|
||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
||||
}
|
||||
Ok(checksum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, path::Path, println};
|
||||
|
||||
use super::*;
|
||||
use alloc::format;
|
||||
use tempfile::tempdir;
|
||||
|
||||
const EXAMPLE_DATA_CFDP: [u8; 15] = [
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||
];
|
||||
|
||||
const NATIVE_FS: NativeFilestore = NativeFilestore {};
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_filestore_create() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result =
|
||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
||||
assert!(result.is_ok());
|
||||
let path = Path::new(&file_path);
|
||||
assert!(path.exists());
|
||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_fs_file_exists() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_fs_dir_exists() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let dir_path = tmpdir.path().join("testdir");
|
||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
assert!(NATIVE_FS.is_dir(dir_path.as_path().to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_fs_remove_file() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.expect("creating file failed");
|
||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.remove_file(file_path.to_str().unwrap())
|
||||
.expect("removing file failed");
|
||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_fs_write() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
||||
println!("{}", file_path.to_str().unwrap());
|
||||
let write_data = "hello world\n";
|
||||
NATIVE_FS
|
||||
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
|
||||
.expect("writing to file failed");
|
||||
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
|
||||
assert_eq!(read_back, write_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_native_fs_read() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
||||
println!("{}", file_path.to_str().unwrap());
|
||||
let write_data = "hello world\n";
|
||||
NATIVE_FS
|
||||
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
|
||||
.expect("writing to file failed");
|
||||
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
|
||||
assert_eq!(read_back, write_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_file() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.expect("creating file failed");
|
||||
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
|
||||
assert_eq!(fs::read(file_path.clone()).unwrap(), [1, 2, 3, 4]);
|
||||
NATIVE_FS
|
||||
.truncate_file(file_path.to_str().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(file_path.clone()).unwrap(), []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_dir() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let dir_path = tmpdir.path().join("testdir");
|
||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
NATIVE_FS
|
||||
.remove_dir(dir_path.to_str().unwrap(), false)
|
||||
.unwrap();
|
||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_file() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.expect("creating file failed");
|
||||
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
|
||||
let read_buf: &mut [u8] = &mut [0; 4];
|
||||
NATIVE_FS
|
||||
.read_data(file_path.to_str().unwrap(), 0, 4, read_buf)
|
||||
.unwrap();
|
||||
assert_eq!([1, 2, 3, 4], read_buf);
|
||||
NATIVE_FS
|
||||
.write_data(file_path.to_str().unwrap(), 4, &[5, 6, 7, 8])
|
||||
.expect("writing to file failed");
|
||||
NATIVE_FS
|
||||
.read_data(file_path.to_str().unwrap(), 2, 4, read_buf)
|
||||
.unwrap();
|
||||
assert_eq!([3, 4, 5, 6], read_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_which_does_not_exist() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::FileDoesNotExist = error {
|
||||
assert_eq!(error.to_string(), "file does not exist");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_already_exists() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result =
|
||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
||||
assert!(result.is_ok());
|
||||
let result =
|
||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::FileAlreadyExists = error {
|
||||
assert_eq!(error.to_string(), "file already exists");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_file_with_dir_api() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::IsNotDirectory = error {
|
||||
assert_eq!(error.to_string(), "is not a directory");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_dir_remove_all() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let dir_path = tmpdir.path().join("test");
|
||||
NATIVE_FS
|
||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let file_path = dir_path.as_path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let result = NATIVE_FS.remove_dir(dir_path.to_str().unwrap(), true);
|
||||
assert!(result.is_ok());
|
||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_dir_with_file_api() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test");
|
||||
NATIVE_FS
|
||||
.create_dir(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::IsNotFile = error {
|
||||
assert_eq!(error.to_string(), "is not a file");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_dir_which_does_not_exist() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test");
|
||||
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::DirDoesNotExist = error {
|
||||
assert_eq!(error.to_string(), "directory does not exist");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_file_which_does_not_exist() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::FileDoesNotExist = error {
|
||||
assert_eq!(error.to_string(), "file does not exist");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_file_which_does_not_exist() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::FileDoesNotExist = error {
|
||||
assert_eq!(error.to_string(), "file does not exist");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_file_on_directory() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test");
|
||||
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
|
||||
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::IsNotFile = error {
|
||||
assert_eq!(error.to_string(), "is not a file");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_byte_conversion_error_when_reading() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 2, &mut []);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::ByteConversion(byte_conv_error) = error {
|
||||
if let ByteConversionError::ToSliceTooSmall { found, expected } = byte_conv_error {
|
||||
assert_eq!(found, 0);
|
||||
assert_eq!(expected, 2);
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
format!("filestore error: {}", byte_conv_error)
|
||||
);
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_file_on_dir() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let dir_path = tmpdir.path().join("test");
|
||||
NATIVE_FS
|
||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
let result = NATIVE_FS.read_data(dir_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::IsNotFile = error {
|
||||
assert_eq!(error.to_string(), "is not a file");
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_file_non_existing() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::FileDoesNotExist = error {
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_file_on_dir() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test");
|
||||
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
|
||||
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::IsNotFile = error {
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_extraction() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("test.txt");
|
||||
NATIVE_FS
|
||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
||||
.unwrap();
|
||||
NativeFilestore::filename_from_full_path(file_path.to_str().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modular_checksum() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
||||
fs::write(file_path.as_path(), EXAMPLE_DATA_CFDP).expect("writing test file failed");
|
||||
// Kind of re-writing the modular checksum impl here which we are trying to test, but the
|
||||
// numbers/correctness were verified manually using calculators, so this is okay.
|
||||
let mut checksum: u32 = 0;
|
||||
let mut buffer: [u8; 4] = [0; 4];
|
||||
for i in 0..3 {
|
||||
buffer = EXAMPLE_DATA_CFDP[i * 4..(i + 1) * 4].try_into().unwrap();
|
||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
||||
}
|
||||
buffer[0..3].copy_from_slice(&EXAMPLE_DATA_CFDP[12..15]);
|
||||
buffer[3] = 0;
|
||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
||||
let mut verif_buf: [u8; 32] = [0; 32];
|
||||
let result = NATIVE_FS.checksum_verify(
|
||||
file_path.to_str().unwrap(),
|
||||
ChecksumType::Modular,
|
||||
checksum,
|
||||
&mut verif_buf,
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_checksum_impl() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
||||
// The file to check does not even need to exist, and the verification buffer can be
|
||||
// empty: the null checksum is always yields the same result.
|
||||
let result = NATIVE_FS.checksum_verify(
|
||||
file_path.to_str().unwrap(),
|
||||
ChecksumType::NullChecksum,
|
||||
0,
|
||||
&mut [],
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checksum_not_implemented() {
|
||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
||||
// The file to check does not even need to exist, and the verification buffer can be
|
||||
// empty: the null checksum is always yields the same result.
|
||||
let result = NATIVE_FS.checksum_verify(
|
||||
file_path.to_str().unwrap(),
|
||||
ChecksumType::Crc32Proximity1,
|
||||
0,
|
||||
&mut [],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error {
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
format!("checksum {:?} not implemented", cksum_type)
|
||||
);
|
||||
} else {
|
||||
panic!("unexpected error");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
//! This module contains the implementation of the CFDP high level classes as specified in the
|
||||
//! CCSDS 727.0-B-5.
|
||||
use core::{cell::RefCell, fmt::Debug, hash::Hash};
|
||||
|
||||
use crc::{Crc, CRC_32_CKSUM};
|
||||
use hashbrown::HashMap;
|
||||
use spacepackets::{
|
||||
cfdp::{
|
||||
pdu::{FileDirectiveType, PduError, PduHeader},
|
||||
ChecksumType, PduType, TransmissionMode,
|
||||
ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode,
|
||||
},
|
||||
util::UnsignedByteField,
|
||||
};
|
||||
@ -14,6 +19,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod dest;
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod filestore;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod source;
|
||||
pub mod user;
|
||||
@ -24,7 +31,27 @@ pub enum EntityType {
|
||||
Receiving,
|
||||
}
|
||||
|
||||
/// Generic abstraction for a check timer which has different functionality depending on whether
|
||||
pub enum TimerContext {
|
||||
CheckLimit {
|
||||
local_id: UnsignedByteField,
|
||||
remote_id: UnsignedByteField,
|
||||
entity_type: EntityType,
|
||||
},
|
||||
NakActivity {
|
||||
expiry_time_seconds: f32,
|
||||
},
|
||||
PositiveAck {
|
||||
expiry_time_seconds: f32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Generic abstraction for a check timer which is used by 3 mechanisms of the CFDP protocol.
|
||||
///
|
||||
/// ## 1. Check limit handling
|
||||
///
|
||||
/// The first mechanism is the check limit handling for unacknowledged transfers as specified
|
||||
/// in 4.6.3.2 and 4.6.3.3 of the CFDP standard.
|
||||
/// For this mechanism, the timer has different functionality depending on whether
|
||||
/// the using entity is the sending entity or the receiving entity for the unacknowledged
|
||||
/// transmission mode.
|
||||
///
|
||||
@ -35,30 +62,40 @@ pub enum EntityType {
|
||||
/// For the receiving entity, this timer determines the expiry period for incrementing a check
|
||||
/// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
|
||||
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard.
|
||||
pub trait CheckTimerProvider {
|
||||
///
|
||||
/// ## 2. NAK activity limit
|
||||
///
|
||||
/// The timer will be used to perform the NAK activity check as specified in 4.6.4.7 of the CFDP
|
||||
/// standard. The expiration period will be provided by the NAK timer expiration limit of the
|
||||
/// remote entity configuration.
|
||||
///
|
||||
/// ## 3. Positive ACK procedures
|
||||
///
|
||||
/// The timer will be used to perform the Positive Acknowledgement Procedures as specified in
|
||||
/// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer
|
||||
/// interval of the remote entity configuration.
|
||||
pub trait CheckTimer: Debug {
|
||||
fn has_expired(&self) -> bool;
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
/// A generic trait which allows CFDP entities to create check timers which are required to
|
||||
/// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2
|
||||
/// and 4.6.3.3. The [CheckTimerProvider] provides more information about the purpose of the
|
||||
/// check timer.
|
||||
/// and 4.6.3.3. The [CheckTimer] documentation provides more information about the purpose of the
|
||||
/// check timer in the context of CFDP.
|
||||
///
|
||||
/// This trait also allows the creation of different check timers depending on
|
||||
/// the ID of the local entity, the ID of the remote entity for a given transaction, and the
|
||||
/// type of entity.
|
||||
/// This trait also allows the creation of different check timers depending on context and purpose
|
||||
/// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or
|
||||
/// other factors.
|
||||
#[cfg(feature = "alloc")]
|
||||
pub trait CheckTimerCreator {
|
||||
fn get_check_timer_provider(
|
||||
local_id: &UnsignedByteField,
|
||||
remote_id: &UnsignedByteField,
|
||||
entity_type: EntityType,
|
||||
) -> Box<dyn CheckTimerProvider>;
|
||||
fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box<dyn CheckTimer>;
|
||||
}
|
||||
|
||||
/// Simple implementation of the [CheckTimerProvider] trait assuming a standard runtime.
|
||||
/// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime.
|
||||
/// It also assumes that a second accuracy of the check timer period is sufficient.
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Debug)]
|
||||
pub struct StdCheckTimer {
|
||||
expiry_time_seconds: u64,
|
||||
start_time: std::time::Instant,
|
||||
@ -75,7 +112,7 @@ impl StdCheckTimer {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl CheckTimerProvider for StdCheckTimer {
|
||||
impl CheckTimer for StdCheckTimer {
|
||||
fn has_expired(&self) -> bool {
|
||||
let elapsed_time = self.start_time.elapsed();
|
||||
if elapsed_time.as_secs() > self.expiry_time_seconds {
|
||||
@ -83,24 +120,322 @@ impl CheckTimerProvider for StdCheckTimer {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.start_time = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// This structure models the remote entity configuration information as specified in chapter 8.3
|
||||
/// of the CFDP standard.
|
||||
|
||||
/// Some of the fields which were not considered necessary for the Rust implementation
|
||||
/// were omitted. Some other fields which are not contained inside the standard but are considered
|
||||
/// necessary for the Rust implementation are included.
|
||||
///
|
||||
/// ## Notes on Positive Acknowledgment Procedures
|
||||
///
|
||||
/// The `positive_ack_timer_interval_seconds` and `positive_ack_timer_expiration_limit` will
|
||||
/// be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending
|
||||
/// entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU).
|
||||
/// Once the expected ACK response has not been received for that interval, as counter will be
|
||||
/// incremented and the timer will be reset. Once the counter exceeds the
|
||||
/// `positive_ack_timer_expiration_limit`, a Positive ACK Limit Reached fault will be declared.
|
||||
///
|
||||
/// ## Notes on Deferred Lost Segment Procedures
|
||||
///
|
||||
/// This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After
|
||||
/// issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is
|
||||
/// reset when missing segments or missing metadata is received. The timer will be deactivated if
|
||||
/// all missing data is received. If the timer expires, a new NAK sequence will be issued and a
|
||||
/// counter will be incremented, which can lead to a NAK Limit Reached fault being declared.
|
||||
///
|
||||
/// ## Fields
|
||||
///
|
||||
/// * `entity_id` - The ID of the remote entity.
|
||||
/// * `max_packet_len` - This determines of all PDUs generated for that remote entity in addition
|
||||
/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs.
|
||||
/// * `max_file_segment_len` The maximum file segment length which determines the maximum size
|
||||
/// of file data PDUs in addition to the `max_packet_len` attribute. If this field is set
|
||||
/// to None, the maximum file segment length will be derived from the maximum packet length.
|
||||
/// If this has some value which is smaller than the segment value derived from
|
||||
/// `max_packet_len`, this value will be picked.
|
||||
/// * `closure_requested_by_default` - If the closure requested field is not supplied as part of
|
||||
/// the Put Request, it will be determined from this field in the remote configuration.
|
||||
/// * `crc_on_transmission_by_default` - If the CRC option is not supplied as part of the Put
|
||||
/// Request, it will be determined from this field in the remote configuration.
|
||||
/// * `default_transmission_mode` - If the transmission mode is not supplied as part of the
|
||||
/// Put Request, it will be determined from this field in the remote configuration.
|
||||
/// * `disposition_on_cancellation` - Determines whether an incomplete received file is discard on
|
||||
/// transaction cancellation. Defaults to False.
|
||||
/// * `default_crc_type` - Default checksum type used to calculate for all file transmissions to
|
||||
/// this remote entity.
|
||||
/// * `check_limit` - This timer determines the expiry period for incrementing a check counter
|
||||
/// after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
|
||||
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to
|
||||
/// 2, so the check limit timer may expire twice.
|
||||
/// * `positive_ack_timer_interval_seconds`- See the notes on the Positive Acknowledgment
|
||||
/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to
|
||||
/// 10 seconds.
|
||||
/// * `positive_ack_timer_expiration_limit` - See the notes on the Positive Acknowledgment
|
||||
/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice.
|
||||
/// * `immediate_nak_mode` - Specifies whether a NAK sequence should be issued immediately when a
|
||||
/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True.
|
||||
/// * `nak_timer_interval_seconds` - See the notes on the Deferred Lost Segment Procedure inside
|
||||
/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds.
|
||||
/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside
|
||||
/// the class documentation. Defaults to 2, so the timer may expire two times.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct RemoteEntityConfig {
|
||||
pub entity_id: UnsignedByteField,
|
||||
pub max_packet_len: usize,
|
||||
pub max_file_segment_len: usize,
|
||||
pub closure_requeted_by_default: bool,
|
||||
pub closure_requested_by_default: bool,
|
||||
pub crc_on_transmission_by_default: bool,
|
||||
pub default_transmission_mode: TransmissionMode,
|
||||
pub default_crc_type: ChecksumType,
|
||||
pub positive_ack_timer_interval_seconds: f32,
|
||||
pub positive_ack_timer_expiration_limit: u32,
|
||||
pub check_limit: u32,
|
||||
pub disposition_on_cancellation: bool,
|
||||
pub immediate_nak_mode: bool,
|
||||
pub nak_timer_interval_seconds: f32,
|
||||
pub nak_timer_expiration_limit: u32,
|
||||
}
|
||||
|
||||
impl RemoteEntityConfig {
|
||||
pub fn new_with_default_values(
|
||||
entity_id: UnsignedByteField,
|
||||
max_file_segment_len: usize,
|
||||
max_packet_len: usize,
|
||||
closure_requested_by_default: bool,
|
||||
crc_on_transmission_by_default: bool,
|
||||
default_transmission_mode: TransmissionMode,
|
||||
default_crc_type: ChecksumType,
|
||||
) -> Self {
|
||||
Self {
|
||||
entity_id,
|
||||
max_file_segment_len,
|
||||
max_packet_len,
|
||||
closure_requested_by_default,
|
||||
crc_on_transmission_by_default,
|
||||
default_transmission_mode,
|
||||
default_crc_type,
|
||||
check_limit: 2,
|
||||
positive_ack_timer_interval_seconds: 10.0,
|
||||
positive_ack_timer_expiration_limit: 2,
|
||||
disposition_on_cancellation: false,
|
||||
immediate_nak_mode: true,
|
||||
nak_timer_interval_seconds: 10.0,
|
||||
nak_timer_expiration_limit: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RemoteEntityConfigProvider {
|
||||
fn get_remote_config(&self, remote_id: &UnsignedByteField) -> Option<&RemoteEntityConfig>;
|
||||
/// Retrieve the remote entity configuration for the given remote ID.
|
||||
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig>;
|
||||
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig>;
|
||||
/// Add a new remote configuration. Return [true] if the configuration was
|
||||
/// inserted successfully, and [false] if a configuration already exists.
|
||||
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool;
|
||||
/// Remote a configuration. Returns [true] if the configuration was removed successfully,
|
||||
/// and [false] if no configuration exists for the given remote ID.
|
||||
fn remove_config(&mut self, remote_id: u64) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Default)]
|
||||
pub struct StdRemoteEntityConfigProvider {
|
||||
remote_cfg_table: HashMap<u64, RemoteEntityConfig>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
|
||||
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig> {
|
||||
self.remote_cfg_table.get(&remote_id)
|
||||
}
|
||||
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> {
|
||||
self.remote_cfg_table.get_mut(&remote_id)
|
||||
}
|
||||
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool {
|
||||
self.remote_cfg_table
|
||||
.insert(cfg.entity_id.value(), *cfg)
|
||||
.is_some()
|
||||
}
|
||||
fn remove_config(&mut self, remote_id: u64) -> bool {
|
||||
self.remote_cfg_table.remove(&remote_id).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait introduces some callbacks which will be called when a particular CFDP fault
|
||||
/// handler is called.
|
||||
///
|
||||
/// It is passed into the CFDP handlers as part of the [DefaultFaultHandler] and the local entity
|
||||
/// configuration and provides a way to specify custom user error handlers. This allows to
|
||||
/// implement some CFDP features like fault handler logging, which would not be possible
|
||||
/// generically otherwise.
|
||||
///
|
||||
/// For each error reported by the [DefaultFaultHandler], the appropriate fault handler callback
|
||||
/// will be called depending on the [FaultHandlerCode].
|
||||
pub trait UserFaultHandler {
|
||||
fn notice_of_suspension_cb(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
cond: ConditionCode,
|
||||
progress: u64,
|
||||
);
|
||||
|
||||
fn notice_of_cancellation_cb(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
cond: ConditionCode,
|
||||
progress: u64,
|
||||
);
|
||||
|
||||
fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
|
||||
|
||||
fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
|
||||
}
|
||||
|
||||
/// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP
|
||||
/// standard.
|
||||
///
|
||||
/// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler
|
||||
/// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used
|
||||
/// to select the error handling inside the CFDP handler itself in addition to dispatching to a
|
||||
/// user-provided callback function provided by the [UserFaultHandler].
|
||||
///
|
||||
/// Some note on the provided default settings:
|
||||
///
|
||||
/// - Checksum failures will be ignored by default. This is because for unacknowledged transfers,
|
||||
/// cancelling the transfer immediately would interfere with the check limit mechanism specified
|
||||
/// in chapter 4.6.3.3.
|
||||
/// - Unsupported checksum types will also be ignored by default. Even if the checksum type is
|
||||
/// not supported the file transfer might still have worked properly.
|
||||
///
|
||||
/// For all other faults, the default fault handling operation will be to cancel the transaction.
|
||||
/// These defaults can be overriden by using the [Self::set_fault_handler] method.
|
||||
/// Please note that in any case, fault handler overrides can be specified by the sending CFDP
|
||||
/// entity.
|
||||
pub struct DefaultFaultHandler {
|
||||
handler_array: [FaultHandlerCode; 10],
|
||||
// Could also change the user fault handler trait to have non mutable methods, but that limits
|
||||
// flexbility on the user side..
|
||||
user_fault_handler: RefCell<Box<dyn UserFaultHandler + Send>>,
|
||||
}
|
||||
|
||||
impl DefaultFaultHandler {
|
||||
fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option<usize> {
|
||||
Some(match conditon_code {
|
||||
ConditionCode::PositiveAckLimitReached => 0,
|
||||
ConditionCode::KeepAliveLimitReached => 1,
|
||||
ConditionCode::InvalidTransmissionMode => 2,
|
||||
ConditionCode::FilestoreRejection => 3,
|
||||
ConditionCode::FileChecksumFailure => 4,
|
||||
ConditionCode::FileSizeError => 5,
|
||||
ConditionCode::NakLimitReached => 6,
|
||||
ConditionCode::InactivityDetected => 7,
|
||||
ConditionCode::CheckLimitReached => 8,
|
||||
ConditionCode::UnsupportedChecksumType => 9,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_fault_handler(
|
||||
&mut self,
|
||||
condition_code: ConditionCode,
|
||||
fault_handler: FaultHandlerCode,
|
||||
) {
|
||||
let array_idx = Self::condition_code_to_array_index(condition_code);
|
||||
if array_idx.is_none() {
|
||||
return;
|
||||
}
|
||||
self.handler_array[array_idx.unwrap()] = fault_handler;
|
||||
}
|
||||
|
||||
pub fn new(user_fault_handler: Box<dyn UserFaultHandler + Send>) -> Self {
|
||||
let mut init_array = [FaultHandlerCode::NoticeOfCancellation; 10];
|
||||
init_array
|
||||
[Self::condition_code_to_array_index(ConditionCode::FileChecksumFailure).unwrap()] =
|
||||
FaultHandlerCode::IgnoreError;
|
||||
init_array[Self::condition_code_to_array_index(ConditionCode::UnsupportedChecksumType)
|
||||
.unwrap()] = FaultHandlerCode::IgnoreError;
|
||||
Self {
|
||||
handler_array: init_array,
|
||||
user_fault_handler: RefCell::new(user_fault_handler),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fault_handler(&self, condition_code: ConditionCode) -> FaultHandlerCode {
|
||||
let array_idx = Self::condition_code_to_array_index(condition_code);
|
||||
if array_idx.is_none() {
|
||||
return FaultHandlerCode::IgnoreError;
|
||||
}
|
||||
self.handler_array[array_idx.unwrap()]
|
||||
}
|
||||
|
||||
pub fn report_fault(
|
||||
&self,
|
||||
transaction_id: TransactionId,
|
||||
condition: ConditionCode,
|
||||
progress: u64,
|
||||
) -> FaultHandlerCode {
|
||||
let array_idx = Self::condition_code_to_array_index(condition);
|
||||
if array_idx.is_none() {
|
||||
return FaultHandlerCode::IgnoreError;
|
||||
}
|
||||
let fh_code = self.handler_array[array_idx.unwrap()];
|
||||
let mut handler_mut = self.user_fault_handler.borrow_mut();
|
||||
match fh_code {
|
||||
FaultHandlerCode::NoticeOfCancellation => {
|
||||
handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress);
|
||||
}
|
||||
FaultHandlerCode::NoticeOfSuspension => {
|
||||
handler_mut.notice_of_suspension_cb(transaction_id, condition, progress);
|
||||
}
|
||||
FaultHandlerCode::IgnoreError => {
|
||||
handler_mut.ignore_cb(transaction_id, condition, progress);
|
||||
}
|
||||
FaultHandlerCode::AbandonTransaction => {
|
||||
handler_mut.abandoned_cb(transaction_id, condition, progress);
|
||||
}
|
||||
}
|
||||
fh_code
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IndicationConfig {
|
||||
pub eof_sent: bool,
|
||||
pub eof_recv: bool,
|
||||
pub file_segment_recv: bool,
|
||||
pub transaction_finished: bool,
|
||||
pub suspended: bool,
|
||||
pub resumed: bool,
|
||||
}
|
||||
|
||||
impl Default for IndicationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
eof_sent: true,
|
||||
eof_recv: true,
|
||||
file_segment_recv: true,
|
||||
transaction_finished: true,
|
||||
suspended: true,
|
||||
resumed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalEntityConfig {
|
||||
pub id: UnsignedByteField,
|
||||
pub indication_cfg: IndicationConfig,
|
||||
pub default_fault_handler: DefaultFaultHandler,
|
||||
}
|
||||
|
||||
/// The CFDP transaction ID of a CFDP transaction consists of the source entity ID and the sequence
|
||||
/// number of that transfer which is also determined by the CFDP source entity.
|
||||
#[derive(Debug, Eq, Copy, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct TransactionId {
|
||||
source_id: UnsignedByteField,
|
||||
@ -121,23 +456,38 @@ impl TransactionId {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for TransactionId {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.source_id.value().hash(state);
|
||||
self.seq_num.value().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TransactionId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.source_id.value() == other.source_id.value()
|
||||
&& self.seq_num.value() == other.seq_num.value()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum TransactionStep {
|
||||
Idle = 0,
|
||||
TransactionStart = 1,
|
||||
ReceivingFileDataPdus = 2,
|
||||
SendingAckPdu = 3,
|
||||
TransferCompletion = 4,
|
||||
SendingFinishedPdu = 5,
|
||||
ReceivingFileDataPdusWithCheckLimitHandling = 3,
|
||||
SendingAckPdu = 4,
|
||||
TransferCompletion = 5,
|
||||
SendingFinishedPdu = 6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum State {
|
||||
Idle = 0,
|
||||
BusyClass1Nacked = 2,
|
||||
BusyClass2Acked = 3,
|
||||
Busy = 1,
|
||||
Suspended = 2,
|
||||
}
|
||||
|
||||
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
|
||||
@ -248,8 +598,8 @@ mod tests {
|
||||
pdu::{
|
||||
eof::EofPdu,
|
||||
file_data::FileDataPdu,
|
||||
metadata::{MetadataGenericParams, MetadataPdu},
|
||||
CommonPduConfig, FileDirectiveType, PduHeader,
|
||||
metadata::{MetadataGenericParams, MetadataPduCreator},
|
||||
CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket,
|
||||
},
|
||||
PduType,
|
||||
};
|
||||
@ -272,7 +622,8 @@ mod tests {
|
||||
let dest_file_name = "hello-dest.txt";
|
||||
let src_lv = Lv::new_from_str(src_file_name).unwrap();
|
||||
let dest_lv = Lv::new_from_str(dest_file_name).unwrap();
|
||||
let metadata_pdu = MetadataPdu::new(pdu_header, metadata_params, src_lv, dest_lv, None);
|
||||
let metadata_pdu =
|
||||
MetadataPduCreator::new_no_opts(pdu_header, metadata_params, src_lv, dest_lv);
|
||||
metadata_pdu
|
||||
.write_to_bytes(&mut buf)
|
||||
.expect("writing metadata PDU failed");
|
||||
|
@ -1,10 +1,10 @@
|
||||
use spacepackets::{
|
||||
cfdp::{
|
||||
pdu::{
|
||||
file_data::RecordContinuationState,
|
||||
file_data::SegmentMetadata,
|
||||
finished::{DeliveryCode, FileStatus},
|
||||
},
|
||||
tlv::msg_to_user::MsgToUserTlv,
|
||||
tlv::{msg_to_user::MsgToUserTlv, WritableTlv},
|
||||
ConditionCode,
|
||||
},
|
||||
util::UnsignedByteField,
|
||||
@ -30,13 +30,44 @@ pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> {
|
||||
pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>],
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedMetadataRecvdParams {
|
||||
pub id: TransactionId,
|
||||
pub source_id: UnsignedByteField,
|
||||
pub file_size: u64,
|
||||
pub src_file_name: alloc::string::String,
|
||||
pub dest_file_name: alloc::string::String,
|
||||
pub msgs_to_user: alloc::vec::Vec<alloc::vec::Vec<u8>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
|
||||
fn from(value: MetadataReceivedParams) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<&MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
|
||||
fn from(value: &MetadataReceivedParams) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
source_id: value.source_id,
|
||||
file_size: value.file_size,
|
||||
src_file_name: value.src_file_name.into(),
|
||||
dest_file_name: value.dest_file_name.into(),
|
||||
msgs_to_user: value.msgs_to_user.iter().map(|tlv| tlv.to_vec()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileSegmentRecvdParams<'seg_meta> {
|
||||
pub id: TransactionId,
|
||||
pub offset: u64,
|
||||
pub length: usize,
|
||||
pub rec_cont_state: Option<RecordContinuationState>,
|
||||
pub segment_metadata: Option<&'seg_meta [u8]>,
|
||||
pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>,
|
||||
}
|
||||
|
||||
pub trait CfdpUser {
|
||||
|
@ -30,6 +30,12 @@ impl PacketIdLookup for [u16] {
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketIdLookup for &[u16] {
|
||||
fn validate(&self, packet_id: u16) -> bool {
|
||||
self.binary_search(&packet_id).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl PacketIdLookup for Vec<PacketId> {
|
||||
fn validate(&self, packet_id: u16) -> bool {
|
||||
@ -49,6 +55,12 @@ impl PacketIdLookup for [PacketId] {
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketIdLookup for &[PacketId] {
|
||||
fn validate(&self, packet_id: u16) -> bool {
|
||||
self.binary_search(&PacketId::from(packet_id)).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// This function parses a given buffer for tightly packed CCSDS space packets. It uses the
|
||||
/// [PacketId] field of the CCSDS packets to detect the start of a CCSDS space packet and then
|
||||
/// uses the length field of the packet to extract CCSDS packets.
|
||||
@ -101,7 +113,7 @@ pub fn parse_buffer_for_ccsds_space_packets<E>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use spacepackets::{
|
||||
ecss::{tc::PusTcCreator, SerializablePusPacket},
|
||||
ecss::{tc::PusTcCreator, WritablePusPacket},
|
||||
PacketId, SpHeader,
|
||||
};
|
||||
|
||||
|
@ -1,49 +0,0 @@
|
||||
pub enum FsrcGroupIds {
|
||||
Tmtc = 0,
|
||||
}
|
||||
|
||||
pub struct FsrcErrorRaw {
|
||||
pub group_id: u8,
|
||||
pub unique_id: u8,
|
||||
pub group_name: &'static str,
|
||||
pub info: &'static str,
|
||||
}
|
||||
|
||||
pub trait FsrcErrorHandler {
|
||||
fn error(&mut self, e: FsrcErrorRaw);
|
||||
fn error_with_one_param(&mut self, e: FsrcErrorRaw, _p1: u32) {
|
||||
self.error(e);
|
||||
}
|
||||
fn error_with_two_params(&mut self, e: FsrcErrorRaw, _p1: u32, _p2: u32) {
|
||||
self.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
impl FsrcErrorRaw {
|
||||
pub const fn new(
|
||||
group_id: u8,
|
||||
unique_id: u8,
|
||||
group_name: &'static str,
|
||||
info: &'static str,
|
||||
) -> Self {
|
||||
FsrcErrorRaw {
|
||||
group_id,
|
||||
unique_id,
|
||||
group_name,
|
||||
info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct SimpleStdErrorHandler {}
|
||||
|
||||
#[cfg(feature = "use_std")]
|
||||
impl FsrcErrorHandler for SimpleStdErrorHandler {
|
||||
fn error(&mut self, e: FsrcErrorRaw) {
|
||||
println!(
|
||||
"Received error from group {} with ID ({},{}): {}",
|
||||
e.group_name, e.group_id, e.unique_id, e.info
|
||||
);
|
||||
}
|
||||
}
|
@ -2,23 +2,13 @@
|
||||
//!
|
||||
//! This module provides components to perform event routing. The most important component for this
|
||||
//! task is the [EventManager]. It receives all events and then routes them to event subscribers
|
||||
//! where appropriate.
|
||||
#![cfg_attr(feature = "doc-images",
|
||||
cfg_attr(all(),
|
||||
doc = ::embed_doc_image::embed_image!("event_man_arch", "images/event_man_arch.png"
|
||||
)))]
|
||||
#![cfg_attr(
|
||||
not(feature = "doc-images"),
|
||||
doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \
|
||||
to enable."
|
||||
)]
|
||||
//! One common use case for satellite systems is to offer a light-weight publish-subscribe mechanism
|
||||
//! and IPC mechanism for software and hardware events which are also packaged as telemetry (TM) or
|
||||
//! can trigger a system response.
|
||||
//! where appropriate. One common use case for satellite systems is to offer a light-weight
|
||||
//! publish-subscribe mechanism and IPC mechanism for software and hardware events which are also
|
||||
//! packaged as telemetry (TM) or can trigger a system response.
|
||||
//!
|
||||
//! The following graph shows how the event flow for such a setup could look like:
|
||||
//!
|
||||
//! ![Event flow][event_man_arch]
|
||||
//! It is recommended to read the
|
||||
//! [sat-rs book chapter](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/events.html)
|
||||
//! about events first:
|
||||
//!
|
||||
//! The event manager has a listener table abstracted by the [ListenerTable], which maps
|
||||
//! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId].
|
||||
|
@ -1,13 +1,13 @@
|
||||
//! Task scheduling module
|
||||
use alloc::string::String;
|
||||
use bus::BusReader;
|
||||
use std::boxed::Box;
|
||||
use std::error::Error;
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
use std::vec;
|
||||
use std::vec::Vec;
|
||||
use std::{io, thread};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum OpResult {
|
||||
@ -34,47 +34,49 @@ pub trait Executable: Send {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executable`: Executable task
|
||||
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks
|
||||
/// * `op_code`: Operation code which is passed to the executable task [operation call][Executable::periodic_op]
|
||||
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks.
|
||||
/// If [None] is passed, no sleeping will be performed.
|
||||
/// * `op_code`: Operation code which is passed to the executable task
|
||||
/// [operation call][Executable::periodic_op]
|
||||
/// * `termination`: Optional termination handler which can cancel threads with a broadcast
|
||||
pub fn exec_sched_single<
|
||||
T: Executable<Error = E> + Send + 'static + ?Sized,
|
||||
E: Error + Send + 'static,
|
||||
>(
|
||||
pub fn exec_sched_single<T: Executable<Error = E> + Send + 'static + ?Sized, E: Send + 'static>(
|
||||
mut executable: Box<T>,
|
||||
task_freq: Option<Duration>,
|
||||
op_code: i32,
|
||||
mut termination: Option<BusReader<()>>,
|
||||
) -> JoinHandle<Result<OpResult, E>> {
|
||||
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
|
||||
let mut cycle_count = 0;
|
||||
thread::spawn(move || loop {
|
||||
if let Some(ref mut terminator) = termination {
|
||||
match terminator.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => {
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
}
|
||||
match executable.exec_type() {
|
||||
ExecutionType::OneShot => {
|
||||
executable.periodic_op(op_code)?;
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
ExecutionType::Infinite => {
|
||||
executable.periodic_op(op_code)?;
|
||||
}
|
||||
ExecutionType::Cycles(cycles) => {
|
||||
executable.periodic_op(op_code)?;
|
||||
cycle_count += 1;
|
||||
if cycle_count == cycles {
|
||||
return Ok(OpResult::Ok);
|
||||
thread::Builder::new()
|
||||
.name(String::from(executable.task_name()))
|
||||
.spawn(move || loop {
|
||||
if let Some(ref mut terminator) = termination {
|
||||
match terminator.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => {
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
|
||||
thread::sleep(freq);
|
||||
})
|
||||
match executable.exec_type() {
|
||||
ExecutionType::OneShot => {
|
||||
executable.periodic_op(op_code)?;
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
ExecutionType::Infinite => {
|
||||
executable.periodic_op(op_code)?;
|
||||
}
|
||||
ExecutionType::Cycles(cycles) => {
|
||||
executable.periodic_op(op_code)?;
|
||||
cycle_count += 1;
|
||||
if cycle_count == cycles {
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(freq) = task_freq {
|
||||
thread::sleep(freq);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// This function allows executing multiple tasks as long as the tasks implement the
|
||||
@ -86,55 +88,56 @@ pub fn exec_sched_single<
|
||||
/// * `task_freq`: Optional frequency of task. Required for periodic and fixed cycle tasks
|
||||
/// * `op_code`: Operation code which is passed to the executable task [operation call][Executable::periodic_op]
|
||||
/// * `termination`: Optional termination handler which can cancel threads with a broadcast
|
||||
pub fn exec_sched_multi<
|
||||
T: Executable<Error = E> + Send + 'static + ?Sized,
|
||||
E: Error + Send + 'static,
|
||||
>(
|
||||
pub fn exec_sched_multi<T: Executable<Error = E> + Send + 'static + ?Sized, E: Send + 'static>(
|
||||
task_name: &'static str,
|
||||
mut executable_vec: Vec<Box<T>>,
|
||||
task_freq: Option<Duration>,
|
||||
op_code: i32,
|
||||
mut termination: Option<BusReader<()>>,
|
||||
) -> JoinHandle<Result<OpResult, E>> {
|
||||
) -> Result<JoinHandle<Result<OpResult, E>>, io::Error> {
|
||||
let mut cycle_counts = vec![0; executable_vec.len()];
|
||||
let mut removal_flags = vec![false; executable_vec.len()];
|
||||
thread::spawn(move || loop {
|
||||
if let Some(ref mut terminator) = termination {
|
||||
match terminator.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => {
|
||||
removal_flags.iter_mut().for_each(|x| *x = true);
|
||||
|
||||
thread::Builder::new()
|
||||
.name(String::from(task_name))
|
||||
.spawn(move || loop {
|
||||
if let Some(ref mut terminator) = termination {
|
||||
match terminator.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => {
|
||||
removal_flags.iter_mut().for_each(|x| *x = true);
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
}
|
||||
for (idx, executable) in executable_vec.iter_mut().enumerate() {
|
||||
match executable.exec_type() {
|
||||
ExecutionType::OneShot => {
|
||||
executable.periodic_op(op_code)?;
|
||||
removal_flags[idx] = true;
|
||||
}
|
||||
ExecutionType::Infinite => {
|
||||
executable.periodic_op(op_code)?;
|
||||
}
|
||||
ExecutionType::Cycles(cycles) => {
|
||||
executable.periodic_op(op_code)?;
|
||||
cycle_counts[idx] += 1;
|
||||
if cycle_counts[idx] == cycles {
|
||||
for (idx, executable) in executable_vec.iter_mut().enumerate() {
|
||||
match executable.exec_type() {
|
||||
ExecutionType::OneShot => {
|
||||
executable.periodic_op(op_code)?;
|
||||
removal_flags[idx] = true;
|
||||
}
|
||||
ExecutionType::Infinite => {
|
||||
executable.periodic_op(op_code)?;
|
||||
}
|
||||
ExecutionType::Cycles(cycles) => {
|
||||
executable.periodic_op(op_code)?;
|
||||
cycle_counts[idx] += 1;
|
||||
if cycle_counts[idx] == cycles {
|
||||
removal_flags[idx] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut removal_iter = removal_flags.iter();
|
||||
executable_vec.retain(|_| !*removal_iter.next().unwrap());
|
||||
removal_iter = removal_flags.iter();
|
||||
cycle_counts.retain(|_| !*removal_iter.next().unwrap());
|
||||
removal_flags.retain(|&i| !i);
|
||||
if executable_vec.is_empty() {
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
|
||||
thread::sleep(freq);
|
||||
})
|
||||
let mut removal_iter = removal_flags.iter();
|
||||
executable_vec.retain(|_| !*removal_iter.next().unwrap());
|
||||
removal_iter = removal_flags.iter();
|
||||
cycle_counts.retain(|_| !*removal_iter.next().unwrap());
|
||||
removal_flags.retain(|&i| !i);
|
||||
if executable_vec.is_empty() {
|
||||
return Ok(OpResult::Ok);
|
||||
}
|
||||
let freq = task_freq.unwrap_or_else(|| panic!("No task frequency specified"));
|
||||
thread::sleep(freq);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -294,7 +297,8 @@ mod tests {
|
||||
Some(Duration::from_millis(100)),
|
||||
expected_op_code,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
let thread_res = jhandle.join().expect("One Shot Task failed");
|
||||
assert!(thread_res.is_ok());
|
||||
assert_eq!(thread_res.unwrap(), OpResult::Ok);
|
||||
@ -319,7 +323,8 @@ mod tests {
|
||||
Some(Duration::from_millis(100)),
|
||||
op_code_inducing_failure,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
let thread_res = jhandle.join().expect("One Shot Task failed");
|
||||
assert!(thread_res.is_err());
|
||||
let error = thread_res.unwrap_err();
|
||||
@ -356,11 +361,13 @@ mod tests {
|
||||
assert_eq!(task.task_name(), ONE_SHOT_TASK_NAME);
|
||||
}
|
||||
let jhandle = exec_sched_multi(
|
||||
"multi-task-name",
|
||||
task_vec,
|
||||
Some(Duration::from_millis(100)),
|
||||
expected_op_code,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
let thread_res = jhandle.join().expect("One Shot Task failed");
|
||||
assert!(thread_res.is_ok());
|
||||
assert_eq!(thread_res.unwrap(), OpResult::Ok);
|
||||
@ -386,7 +393,8 @@ mod tests {
|
||||
Some(Duration::from_millis(100)),
|
||||
expected_op_code,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
let thread_res = jh.join().expect("Cycles Task failed");
|
||||
assert!(thread_res.is_ok());
|
||||
let data = shared.lock().expect("Locking Mutex failed");
|
||||
@ -418,11 +426,13 @@ mod tests {
|
||||
let task_vec: Vec<Box<dyn Executable<Error = ExampleError>>> =
|
||||
vec![one_shot_task, cycled_task_0, cycled_task_1];
|
||||
let jh = exec_sched_multi(
|
||||
"multi-task-name",
|
||||
task_vec,
|
||||
Some(Duration::from_millis(100)),
|
||||
expected_op_code,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
let thread_res = jh.join().expect("Cycles Task failed");
|
||||
assert!(thread_res.is_ok());
|
||||
let data = shared.lock().expect("Locking Mutex failed");
|
||||
@ -449,7 +459,8 @@ mod tests {
|
||||
Some(Duration::from_millis(20)),
|
||||
expected_op_code,
|
||||
Some(terminator.add_rx()),
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
thread::sleep(Duration::from_millis(40));
|
||||
terminator.broadcast(());
|
||||
let thread_res = jh.join().expect("Periodic Task failed");
|
||||
@ -485,11 +496,13 @@ mod tests {
|
||||
let task_vec: Vec<Box<dyn Executable<Error = ExampleError>>> =
|
||||
vec![cycled_task, periodic_task_0, periodic_task_1];
|
||||
let jh = exec_sched_multi(
|
||||
"multi-task-name",
|
||||
task_vec,
|
||||
Some(Duration::from_millis(20)),
|
||||
expected_op_code,
|
||||
Some(terminator.add_rx()),
|
||||
);
|
||||
)
|
||||
.expect("thread creation failed");
|
||||
thread::sleep(Duration::from_millis(60));
|
||||
terminator.broadcast(());
|
||||
let thread_res = jh.join().expect("Periodic Task failed");
|
||||
|
@ -1,4 +1,3 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec;
|
||||
use cobs::encode;
|
||||
use delegate::delegate;
|
||||
@ -29,7 +28,6 @@ impl<TmError, TcError: 'static> TcpTcParser<TmError, TcError> for CobsTcParser {
|
||||
current_write_idx: usize,
|
||||
next_write_idx: &mut usize,
|
||||
) -> Result<(), TcpTmtcError<TmError, TcError>> {
|
||||
// Reader vec full, need to parse for packets.
|
||||
conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets(
|
||||
&mut tc_buffer[..current_write_idx],
|
||||
tc_receiver.upcast_mut(),
|
||||
@ -111,11 +109,23 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for CobsTmSender {
|
||||
///
|
||||
/// The [TCP integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs)
|
||||
/// test also serves as the example application for this module.
|
||||
pub struct TcpTmtcInCobsServer<TmError, TcError: 'static> {
|
||||
generic_server: TcpTmtcGenericServer<TmError, TcError, CobsTmSender, CobsTcParser>,
|
||||
pub struct TcpTmtcInCobsServer<
|
||||
TmError,
|
||||
TcError: 'static,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
> {
|
||||
generic_server:
|
||||
TcpTmtcGenericServer<TmError, TcError, TmSource, TcReceiver, CobsTmSender, CobsTcParser>,
|
||||
}
|
||||
|
||||
impl<TmError: 'static, TcError: 'static> TcpTmtcInCobsServer<TmError, TcError> {
|
||||
impl<
|
||||
TmError: 'static,
|
||||
TcError: 'static,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
> TcpTmtcInCobsServer<TmError, TcError, TmSource, TcReceiver>
|
||||
{
|
||||
/// Create a new TCP TMTC server which exchanges TMTC packets encoded with
|
||||
/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).
|
||||
///
|
||||
@ -128,9 +138,9 @@ impl<TmError: 'static, TcError: 'static> TcpTmtcInCobsServer<TmError, TcError> {
|
||||
/// forwarded to this TC receiver.
|
||||
pub fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
|
||||
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
|
||||
) -> Result<Self, TcpTmtcError<TmError, TcError>> {
|
||||
tm_source: TmSource,
|
||||
tc_receiver: TcReceiver,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
Ok(Self {
|
||||
generic_server: TcpTmtcGenericServer::new(
|
||||
cfg,
|
||||
@ -177,7 +187,7 @@ mod tests {
|
||||
ServerConfig,
|
||||
},
|
||||
};
|
||||
use alloc::{boxed::Box, sync::Arc};
|
||||
use alloc::sync::Arc;
|
||||
use cobs::encode;
|
||||
|
||||
use super::TcpTmtcInCobsServer;
|
||||
@ -202,11 +212,11 @@ mod tests {
|
||||
addr: &SocketAddr,
|
||||
tc_receiver: SyncTcCacher,
|
||||
tm_source: SyncTmSource,
|
||||
) -> TcpTmtcInCobsServer<(), ()> {
|
||||
) -> TcpTmtcInCobsServer<(), (), SyncTmSource, SyncTcCacher> {
|
||||
TcpTmtcInCobsServer::new(
|
||||
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
|
||||
Box::new(tm_source),
|
||||
Box::new(tc_receiver),
|
||||
tm_source,
|
||||
tc_receiver,
|
||||
)
|
||||
.expect("TCP server generation failed")
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! Generic TCP TMTC servers with different TMTC format flavours.
|
||||
use alloc::vec;
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use alloc::vec::Vec;
|
||||
use core::time::Duration;
|
||||
use socket2::{Domain, Socket, Type};
|
||||
use std::io::Read;
|
||||
@ -134,20 +134,29 @@ pub trait TcpTmSender<TmError, TcError> {
|
||||
pub struct TcpTmtcGenericServer<
|
||||
TmError,
|
||||
TcError,
|
||||
TmHandler: TcpTmSender<TmError, TcError>,
|
||||
TcHandler: TcpTcParser<TmError, TcError>,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
TmSender: TcpTmSender<TmError, TcError>,
|
||||
TcParser: TcpTcParser<TmError, TcError>,
|
||||
> {
|
||||
base: TcpTmtcServerBase<TmError, TcError>,
|
||||
tc_handler: TcHandler,
|
||||
tm_handler: TmHandler,
|
||||
pub(crate) listener: TcpListener,
|
||||
pub(crate) inner_loop_delay: Duration,
|
||||
pub(crate) tm_source: TmSource,
|
||||
pub(crate) tm_buffer: Vec<u8>,
|
||||
pub(crate) tc_receiver: TcReceiver,
|
||||
pub(crate) tc_buffer: Vec<u8>,
|
||||
tc_handler: TcParser,
|
||||
tm_handler: TmSender,
|
||||
}
|
||||
|
||||
impl<
|
||||
TmError: 'static,
|
||||
TcError: 'static,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
TmSender: TcpTmSender<TmError, TcError>,
|
||||
TcParser: TcpTcParser<TmError, TcError>,
|
||||
> TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>
|
||||
> TcpTmtcGenericServer<TmError, TcError, TmSource, TcReceiver, TmSender, TcParser>
|
||||
{
|
||||
/// Create a new generic TMTC server instance.
|
||||
///
|
||||
@ -165,25 +174,38 @@ impl<
|
||||
cfg: ServerConfig,
|
||||
tc_parser: TcParser,
|
||||
tm_sender: TmSender,
|
||||
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
|
||||
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
|
||||
) -> Result<TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>, std::io::Error> {
|
||||
tm_source: TmSource,
|
||||
tc_receiver: TcReceiver,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
// Create a TCP listener bound to two addresses.
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
|
||||
socket.set_reuse_address(cfg.reuse_addr)?;
|
||||
#[cfg(unix)]
|
||||
socket.set_reuse_port(cfg.reuse_port)?;
|
||||
let addr = (cfg.addr).into();
|
||||
socket.bind(&addr)?;
|
||||
socket.listen(128)?;
|
||||
Ok(Self {
|
||||
base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?,
|
||||
tc_handler: tc_parser,
|
||||
tm_handler: tm_sender,
|
||||
listener: socket.into(),
|
||||
inner_loop_delay: cfg.inner_loop_delay,
|
||||
tm_source,
|
||||
tm_buffer: vec![0; cfg.tm_buffer_size],
|
||||
tc_receiver,
|
||||
tc_buffer: vec![0; cfg.tc_buffer_size],
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve the internal [TcpListener] class.
|
||||
pub fn listener(&mut self) -> &mut TcpListener {
|
||||
self.base.listener()
|
||||
&mut self.listener
|
||||
}
|
||||
|
||||
/// Can be used to retrieve the local assigned address of the TCP server. This is especially
|
||||
/// useful if using the port number 0 for OS auto-assignment.
|
||||
pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
|
||||
self.base.local_addr()
|
||||
self.listener.local_addr()
|
||||
}
|
||||
|
||||
/// This call is used to handle the next connection to a client. Right now, it performs
|
||||
@ -205,20 +227,20 @@ impl<
|
||||
let mut connection_result = ConnectionResult::default();
|
||||
let mut current_write_idx;
|
||||
let mut next_write_idx = 0;
|
||||
let (mut stream, addr) = self.base.listener.accept()?;
|
||||
let (mut stream, addr) = self.listener.accept()?;
|
||||
stream.set_nonblocking(true)?;
|
||||
connection_result.addr = Some(addr);
|
||||
current_write_idx = next_write_idx;
|
||||
loop {
|
||||
let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]);
|
||||
let read_result = stream.read(&mut self.tc_buffer[current_write_idx..]);
|
||||
match read_result {
|
||||
Ok(0) => {
|
||||
// Connection closed by client. If any TC was read, parse for complete packets.
|
||||
// After that, break the outer loop.
|
||||
if current_write_idx > 0 {
|
||||
self.tc_handler.handle_tc_parsing(
|
||||
&mut self.base.tc_buffer,
|
||||
self.base.tc_receiver.as_mut(),
|
||||
&mut self.tc_buffer,
|
||||
&mut self.tc_receiver,
|
||||
&mut connection_result,
|
||||
current_write_idx,
|
||||
&mut next_write_idx,
|
||||
@ -229,10 +251,10 @@ impl<
|
||||
Ok(read_len) => {
|
||||
current_write_idx += read_len;
|
||||
// TC buffer is full, we must parse for complete packets now.
|
||||
if current_write_idx == self.base.tc_buffer.capacity() {
|
||||
if current_write_idx == self.tc_buffer.capacity() {
|
||||
self.tc_handler.handle_tc_parsing(
|
||||
&mut self.base.tc_buffer,
|
||||
self.base.tc_receiver.as_mut(),
|
||||
&mut self.tc_buffer,
|
||||
&mut self.tc_receiver,
|
||||
&mut connection_result,
|
||||
current_write_idx,
|
||||
&mut next_write_idx,
|
||||
@ -245,8 +267,8 @@ impl<
|
||||
// both UNIX and Windows.
|
||||
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
|
||||
self.tc_handler.handle_tc_parsing(
|
||||
&mut self.base.tc_buffer,
|
||||
self.base.tc_receiver.as_mut(),
|
||||
&mut self.tc_buffer,
|
||||
&mut self.tc_receiver,
|
||||
&mut connection_result,
|
||||
current_write_idx,
|
||||
&mut next_write_idx,
|
||||
@ -254,14 +276,14 @@ impl<
|
||||
current_write_idx = next_write_idx;
|
||||
|
||||
if !self.tm_handler.handle_tm_sending(
|
||||
&mut self.base.tm_buffer,
|
||||
self.base.tm_source.as_mut(),
|
||||
&mut self.tm_buffer,
|
||||
&mut self.tm_source,
|
||||
&mut connection_result,
|
||||
&mut stream,
|
||||
)? {
|
||||
// No TC read, no TM was sent, but the client has not disconnected.
|
||||
// Perform an inner delay to avoid burning CPU time.
|
||||
thread::sleep(self.base.inner_loop_delay);
|
||||
thread::sleep(self.inner_loop_delay);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@ -271,8 +293,8 @@ impl<
|
||||
}
|
||||
}
|
||||
self.tm_handler.handle_tm_sending(
|
||||
&mut self.base.tm_buffer,
|
||||
self.base.tm_source.as_mut(),
|
||||
&mut self.tm_buffer,
|
||||
&mut self.tm_source,
|
||||
&mut connection_result,
|
||||
&mut stream,
|
||||
)?;
|
||||
@ -280,47 +302,6 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TcpTmtcServerBase<TmError, TcError> {
|
||||
pub(crate) listener: TcpListener,
|
||||
pub(crate) inner_loop_delay: Duration,
|
||||
pub(crate) tm_source: Box<dyn TmPacketSource<Error = TmError>>,
|
||||
pub(crate) tm_buffer: Vec<u8>,
|
||||
pub(crate) tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
|
||||
pub(crate) tc_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<TmError, TcError> TcpTmtcServerBase<TmError, TcError> {
|
||||
pub(crate) fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
|
||||
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
// Create a TCP listener bound to two addresses.
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
|
||||
socket.set_reuse_address(cfg.reuse_addr)?;
|
||||
socket.set_reuse_port(cfg.reuse_port)?;
|
||||
let addr = (cfg.addr).into();
|
||||
socket.bind(&addr)?;
|
||||
socket.listen(128)?;
|
||||
Ok(Self {
|
||||
listener: socket.into(),
|
||||
inner_loop_delay: cfg.inner_loop_delay,
|
||||
tm_source,
|
||||
tm_buffer: vec![0; cfg.tm_buffer_size],
|
||||
tc_receiver,
|
||||
tc_buffer: vec![0; cfg.tc_buffer_size],
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn listener(&mut self) -> &mut TcpListener {
|
||||
&mut self.listener
|
||||
}
|
||||
|
||||
pub(crate) fn local_addr(&self) -> std::io::Result<SocketAddr> {
|
||||
self.listener.local_addr()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
@ -88,16 +88,31 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for SpacepacketsTmSender {
|
||||
/// [spacepackets::PacketId]s as part of the server configuration for that purpose.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// The [TCP server integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/tcp_servers.rs)
|
||||
/// also serves as the example application for this module.
|
||||
pub struct TcpSpacepacketsServer<TmError, TcError: 'static> {
|
||||
generic_server:
|
||||
TcpTmtcGenericServer<TmError, TcError, SpacepacketsTmSender, SpacepacketsTcParser>,
|
||||
pub struct TcpSpacepacketsServer<
|
||||
TmError,
|
||||
TcError: 'static,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
> {
|
||||
generic_server: TcpTmtcGenericServer<
|
||||
TmError,
|
||||
TcError,
|
||||
TmSource,
|
||||
TcReceiver,
|
||||
SpacepacketsTmSender,
|
||||
SpacepacketsTcParser,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<TmError: 'static, TcError: 'static> TcpSpacepacketsServer<TmError, TcError> {
|
||||
/// Create a new TCP TMTC server which exchanges CCSDS space packets.
|
||||
impl<
|
||||
TmError: 'static,
|
||||
TcError: 'static,
|
||||
TmSource: TmPacketSource<Error = TmError>,
|
||||
TcReceiver: ReceivesTc<Error = TcError>,
|
||||
> TcpSpacepacketsServer<TmError, TcError, TmSource, TcReceiver>
|
||||
{
|
||||
///
|
||||
/// ## Parameter
|
||||
///
|
||||
@ -110,10 +125,10 @@ impl<TmError: 'static, TcError: 'static> TcpSpacepacketsServer<TmError, TcError>
|
||||
/// parsing. This mechanism is used to have a start marker for finding CCSDS packets.
|
||||
pub fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: Box<dyn TmPacketSource<Error = TmError>>,
|
||||
tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
|
||||
tm_source: TmSource,
|
||||
tc_receiver: TcReceiver,
|
||||
packet_id_lookup: Box<dyn PacketIdLookup + Send>,
|
||||
) -> Result<Self, TcpTmtcError<TmError, TcError>> {
|
||||
) -> Result<Self, std::io::Error> {
|
||||
Ok(Self {
|
||||
generic_server: TcpTmtcGenericServer::new(
|
||||
cfg,
|
||||
@ -158,7 +173,7 @@ mod tests {
|
||||
use alloc::{boxed::Box, sync::Arc};
|
||||
use hashbrown::HashSet;
|
||||
use spacepackets::{
|
||||
ecss::{tc::PusTcCreator, SerializablePusPacket},
|
||||
ecss::{tc::PusTcCreator, WritablePusPacket},
|
||||
PacketId, SpHeader,
|
||||
};
|
||||
|
||||
@ -179,11 +194,11 @@ mod tests {
|
||||
tc_receiver: SyncTcCacher,
|
||||
tm_source: SyncTmSource,
|
||||
packet_id_lookup: HashSet<PacketId>,
|
||||
) -> TcpSpacepacketsServer<(), ()> {
|
||||
) -> TcpSpacepacketsServer<(), (), SyncTmSource, SyncTcCacher> {
|
||||
TcpSpacepacketsServer::new(
|
||||
ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
|
||||
Box::new(tm_source),
|
||||
Box::new(tc_receiver),
|
||||
tm_source,
|
||||
tc_receiver,
|
||||
Box::new(packet_id_lookup),
|
||||
)
|
||||
.expect("TCP server generation failed")
|
||||
|
@ -19,7 +19,7 @@ use std::vec::Vec;
|
||||
///
|
||||
/// ```
|
||||
/// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
/// use spacepackets::ecss::SerializablePusPacket;
|
||||
/// use spacepackets::ecss::WritablePusPacket;
|
||||
/// use satrs_core::hal::std::udp_server::UdpTcServer;
|
||||
/// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
|
||||
/// use spacepackets::SpHeader;
|
||||
@ -144,7 +144,7 @@ mod tests {
|
||||
use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
||||
use crate::tmtc::ReceivesTcCore;
|
||||
use spacepackets::ecss::tc::PusTcCreator;
|
||||
use spacepackets::ecss::SerializablePusPacket;
|
||||
use spacepackets::ecss::WritablePusPacket;
|
||||
use spacepackets::SpHeader;
|
||||
use std::boxed::Box;
|
||||
use std::collections::VecDeque;
|
||||
|
@ -20,9 +20,10 @@ extern crate downcast_rs;
|
||||
#[cfg(any(feature = "std", test))]
|
||||
extern crate std;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
|
||||
pub mod cfdp;
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
|
||||
pub mod event_man;
|
||||
|
@ -1,23 +1,14 @@
|
||||
//! # Pool implementation providing pre-allocated sub-pools with fixed size memory blocks
|
||||
//! # Pool implementation providing memory pools for packet storage.
|
||||
//!
|
||||
//! This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool
|
||||
//! configuration. After the pre-allocation, no dynamic memory allocation will be performed
|
||||
//! during run-time. This makes the implementation suitable for real-time applications and
|
||||
//! embedded environments. The pool implementation will also track the size of the data stored
|
||||
//! inside it.
|
||||
//!
|
||||
//! Transactions with the [pool][LocalPool] are done using a special [address][StoreAddr] type.
|
||||
//! Adding any data to the pool will yield a store address. Modification and read operations are
|
||||
//! done using a reference to a store address. Deletion will consume the store address.
|
||||
//!
|
||||
//! # Example
|
||||
//! # Example for the [StaticMemoryPool]
|
||||
//!
|
||||
//! ```
|
||||
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider};
|
||||
//! use satrs_core::pool::{PoolProvider, StaticMemoryPool, StaticPoolConfig};
|
||||
//!
|
||||
//! // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
|
||||
//! let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]);
|
||||
//! let mut local_pool = LocalPool::new(pool_cfg);
|
||||
//! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
|
||||
//! let mut local_pool = StaticMemoryPool::new(pool_cfg);
|
||||
//! let mut read_buf: [u8; 16] = [0; 16];
|
||||
//! let mut addr;
|
||||
//! {
|
||||
//! // Add new data to the pool
|
||||
@ -30,25 +21,25 @@
|
||||
//!
|
||||
//! {
|
||||
//! // Read the store data back
|
||||
//! let res = local_pool.read(&addr);
|
||||
//! let res = local_pool.read(&addr, &mut read_buf);
|
||||
//! assert!(res.is_ok());
|
||||
//! let buf_read_back = res.unwrap();
|
||||
//! assert_eq!(buf_read_back.len(), 4);
|
||||
//! assert_eq!(buf_read_back[0], 42);
|
||||
//! let read_bytes = res.unwrap();
|
||||
//! assert_eq!(read_bytes, 4);
|
||||
//! assert_eq!(read_buf[0], 42);
|
||||
//! // Modify the stored data
|
||||
//! let res = local_pool.modify(&addr);
|
||||
//! let res = local_pool.modify(&addr, |buf| {
|
||||
//! buf[0] = 12;
|
||||
//! });
|
||||
//! assert!(res.is_ok());
|
||||
//! let buf_read_back = res.unwrap();
|
||||
//! buf_read_back[0] = 12;
|
||||
//! }
|
||||
//!
|
||||
//! {
|
||||
//! // Read the modified data back
|
||||
//! let res = local_pool.read(&addr);
|
||||
//! let res = local_pool.read(&addr, &mut read_buf);
|
||||
//! assert!(res.is_ok());
|
||||
//! let buf_read_back = res.unwrap();
|
||||
//! assert_eq!(buf_read_back.len(), 4);
|
||||
//! assert_eq!(buf_read_back[0], 12);
|
||||
//! let read_bytes = res.unwrap();
|
||||
//! assert_eq!(read_bytes, 4);
|
||||
//! assert_eq!(read_buf[0], 12);
|
||||
//! }
|
||||
//!
|
||||
//! // Delete the stored data
|
||||
@ -56,43 +47,46 @@
|
||||
//!
|
||||
//! // Get a free element in the pool with an appropriate size
|
||||
//! {
|
||||
//! let res = local_pool.free_element(12);
|
||||
//! let res = local_pool.free_element(12, |buf| {
|
||||
//! buf[0] = 7;
|
||||
//! });
|
||||
//! assert!(res.is_ok());
|
||||
//! let (tmp, mut_buf) = res.unwrap();
|
||||
//! addr = tmp;
|
||||
//! mut_buf[0] = 7;
|
||||
//! addr = res.unwrap();
|
||||
//! }
|
||||
//!
|
||||
//! // Read back the data
|
||||
//! {
|
||||
//! // Read the store data back
|
||||
//! let res = local_pool.read(&addr);
|
||||
//! let res = local_pool.read(&addr, &mut read_buf);
|
||||
//! assert!(res.is_ok());
|
||||
//! let buf_read_back = res.unwrap();
|
||||
//! assert_eq!(buf_read_back.len(), 12);
|
||||
//! assert_eq!(buf_read_back[0], 7);
|
||||
//! let read_bytes = res.unwrap();
|
||||
//! assert_eq!(read_bytes, 12);
|
||||
//! assert_eq!(read_buf[0], 7);
|
||||
//! }
|
||||
//! ```
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
|
||||
pub use alloc_mod::*;
|
||||
use core::fmt::{Display, Formatter};
|
||||
use delegate::delegate;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spacepackets::ByteConversionError;
|
||||
#[cfg(feature = "std")]
|
||||
use std::error::Error;
|
||||
|
||||
type NumBlocks = u16;
|
||||
pub type StoreAddr = u64;
|
||||
|
||||
/// Simple address type used for transactions with the local pool.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct StoreAddr {
|
||||
pub struct StaticPoolAddr {
|
||||
pub(crate) pool_idx: u16,
|
||||
pub(crate) packet_idx: NumBlocks,
|
||||
}
|
||||
|
||||
impl StoreAddr {
|
||||
impl StaticPoolAddr {
|
||||
pub const INVALID_ADDR: u32 = 0xFFFFFFFF;
|
||||
|
||||
pub fn raw(&self) -> u32 {
|
||||
@ -100,7 +94,22 @@ impl StoreAddr {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StoreAddr {
|
||||
impl From<StaticPoolAddr> for StoreAddr {
|
||||
fn from(value: StaticPoolAddr) -> Self {
|
||||
((value.pool_idx as u64) << 16) | value.packet_idx as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreAddr> for StaticPoolAddr {
|
||||
fn from(value: StoreAddr) -> Self {
|
||||
Self {
|
||||
pool_idx: ((value >> 16) & 0xff) as u16,
|
||||
packet_idx: (value & 0xff) as u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StaticPoolAddr {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@ -144,6 +153,8 @@ pub enum StoreError {
|
||||
InvalidStoreId(StoreIdError, Option<StoreAddr>),
|
||||
/// Valid subpool and packet index, but no data is stored at the given address
|
||||
DataDoesNotExist(StoreAddr),
|
||||
ByteConversionError(spacepackets::ByteConversionError),
|
||||
LockError,
|
||||
/// Internal or configuration errors
|
||||
InternalError(u32),
|
||||
}
|
||||
@ -166,10 +177,22 @@ impl Display for StoreError {
|
||||
StoreError::InternalError(e) => {
|
||||
write!(f, "internal error: {e}")
|
||||
}
|
||||
StoreError::ByteConversionError(e) => {
|
||||
write!(f, "store error: {e}")
|
||||
}
|
||||
StoreError::LockError => {
|
||||
write!(f, "lock error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ByteConversionError> for StoreError {
|
||||
fn from(value: ByteConversionError) -> Self {
|
||||
Self::ByteConversionError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl Error for StoreError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
@ -180,39 +203,176 @@ impl Error for StoreError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic trait for pool providers where the data can be modified and read in-place. This
|
||||
/// generally means that a shared pool structure has to be wrapped inside a lock structure.
|
||||
pub trait PoolProvider {
|
||||
/// Add new data to the pool. The provider should attempt to reserve a memory block with the
|
||||
/// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can
|
||||
/// be used to access the data stored in the pool
|
||||
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError>;
|
||||
|
||||
/// The provider should attempt to reserve a free memory block with the appropriate size first.
|
||||
/// It then executes a user-provided closure and passes a mutable reference to that memory
|
||||
/// block to the closure. This allows the user to write data to the memory block.
|
||||
/// The function should yield a [StoreAddr] which can be used to access the data stored in the
|
||||
/// pool.
|
||||
fn free_element<W: FnMut(&mut [u8])>(
|
||||
&mut self,
|
||||
len: usize,
|
||||
writer: W,
|
||||
) -> Result<StoreAddr, StoreError>;
|
||||
|
||||
/// Modify data added previously using a given [StoreAddr]. The provider should use the store
|
||||
/// address to determine if a memory block exists for that address. If it does, it should
|
||||
/// call the user-provided closure and pass a mutable reference to the memory block
|
||||
/// to the closure. This allows the user to modify the memory block.
|
||||
fn modify<U: FnMut(&mut [u8])>(
|
||||
&mut self,
|
||||
addr: &StoreAddr,
|
||||
updater: U,
|
||||
) -> Result<(), StoreError>;
|
||||
|
||||
/// The provider should copy the data from the memory block to the user-provided buffer if
|
||||
/// it exists.
|
||||
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, StoreError>;
|
||||
|
||||
/// Delete data inside the pool given a [StoreAddr].
|
||||
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>;
|
||||
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError>;
|
||||
|
||||
/// Retrieve the length of the data at the given store address.
|
||||
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError>;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
fn read_as_vec(&self, addr: &StoreAddr) -> Result<alloc::vec::Vec<u8>, StoreError> {
|
||||
let mut vec = alloc::vec![0; self.len_of_data(addr)?];
|
||||
self.read(addr, &mut vec)?;
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PoolProviderWithGuards: PoolProvider {
|
||||
/// This function behaves like [PoolProvider::read], but consumes the provided address
|
||||
/// and returns a RAII conformant guard object.
|
||||
///
|
||||
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
|
||||
/// given address will be deleted automatically when the guard is dropped.
|
||||
/// This can prevent memory leaks. Users can read the data and release the guard
|
||||
/// if the data in the store is valid for further processing. If the data is faulty, no
|
||||
/// manual deletion is necessary when returning from a processing function prematurely.
|
||||
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard<Self>;
|
||||
|
||||
/// This function behaves like [PoolProvider::modify], but consumes the provided
|
||||
/// address and returns a RAII conformant guard object.
|
||||
///
|
||||
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
|
||||
/// given address will be deleted automatically when the guard is dropped.
|
||||
/// This can prevent memory leaks. Users can read (and modify) the data and release the guard
|
||||
/// if the data in the store is valid for further processing. If the data is faulty, no
|
||||
/// manual deletion is necessary when returning from a processing function prematurely.
|
||||
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self>;
|
||||
}
|
||||
|
||||
pub struct PoolGuard<'a, MemProvider: PoolProvider + ?Sized> {
|
||||
pool: &'a mut MemProvider,
|
||||
pub addr: StoreAddr,
|
||||
no_deletion: bool,
|
||||
deletion_failed_error: Option<StoreError>,
|
||||
}
|
||||
|
||||
/// This helper object
|
||||
impl<'a, MemProvider: PoolProvider> PoolGuard<'a, MemProvider> {
|
||||
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
addr,
|
||||
no_deletion: false,
|
||||
deletion_failed_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, buf: &mut [u8]) -> Result<usize, StoreError> {
|
||||
self.pool.read(&self.addr, buf)
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn read_as_vec(&self) -> Result<alloc::vec::Vec<u8>, StoreError> {
|
||||
self.pool.read_as_vec(&self.addr)
|
||||
}
|
||||
|
||||
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
|
||||
/// is dropped.
|
||||
pub fn release(&mut self) {
|
||||
self.no_deletion = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl<MemProvider: PoolProvider + ?Sized> Drop for PoolGuard<'_, MemProvider> {
|
||||
fn drop(&mut self) {
|
||||
if !self.no_deletion {
|
||||
if let Err(e) = self.pool.delete(self.addr) {
|
||||
self.deletion_failed_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolRwGuard<'a, MemProvider: PoolProvider + ?Sized> {
|
||||
guard: PoolGuard<'a, MemProvider>,
|
||||
}
|
||||
|
||||
impl<'a, MemProvider: PoolProvider> PoolRwGuard<'a, MemProvider> {
|
||||
pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self {
|
||||
Self {
|
||||
guard: PoolGuard::new(pool, addr),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<U: FnMut(&mut [u8])>(&mut self, updater: &mut U) -> Result<(), StoreError> {
|
||||
self.guard.pool.modify(&self.guard.addr, updater)
|
||||
}
|
||||
|
||||
delegate!(
|
||||
to self.guard {
|
||||
pub fn read(&self, buf: &mut [u8]) -> Result<usize, StoreError>;
|
||||
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
|
||||
/// is dropped.
|
||||
pub fn release(&mut self);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
mod alloc_mod {
|
||||
use super::{PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticPoolAddr};
|
||||
use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use delegate::delegate;
|
||||
use spacepackets::ByteConversionError;
|
||||
#[cfg(feature = "std")]
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub type ShareablePoolProvider = Box<dyn PoolProvider + Send + Sync>;
|
||||
#[cfg(feature = "std")]
|
||||
pub type SharedPool = Arc<RwLock<ShareablePoolProvider>>;
|
||||
pub type SharedStaticMemoryPool = Arc<RwLock<StaticMemoryPool>>;
|
||||
|
||||
type PoolSize = usize;
|
||||
const STORE_FREE: PoolSize = PoolSize::MAX;
|
||||
pub const POOL_MAX_SIZE: PoolSize = STORE_FREE - 1;
|
||||
|
||||
/// Configuration structure of the [local pool][LocalPool]
|
||||
/// Configuration structure of the [static memory pool][StaticMemoryPool]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `cfg`: Vector of tuples which represent a subpool. The first entry in the tuple specifies the
|
||||
/// number of memory blocks in the subpool, the second entry the size of the blocks
|
||||
#[derive(Clone)]
|
||||
pub struct PoolCfg {
|
||||
pub struct StaticPoolConfig {
|
||||
cfg: Vec<(NumBlocks, usize)>,
|
||||
}
|
||||
|
||||
impl PoolCfg {
|
||||
impl StaticPoolConfig {
|
||||
pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self {
|
||||
PoolCfg { cfg }
|
||||
StaticPoolConfig { cfg }
|
||||
}
|
||||
|
||||
pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> {
|
||||
@ -228,135 +388,30 @@ mod alloc_mod {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolGuard<'a> {
|
||||
pool: &'a mut LocalPool,
|
||||
pub addr: StoreAddr,
|
||||
no_deletion: bool,
|
||||
deletion_failed_error: Option<StoreError>,
|
||||
}
|
||||
|
||||
/// This helper object
|
||||
impl<'a> PoolGuard<'a> {
|
||||
pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
addr,
|
||||
no_deletion: false,
|
||||
deletion_failed_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Result<&[u8], StoreError> {
|
||||
self.pool.read(&self.addr)
|
||||
}
|
||||
|
||||
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
|
||||
/// is dropped.
|
||||
pub fn release(&mut self) {
|
||||
self.no_deletion = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PoolGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if !self.no_deletion {
|
||||
if let Err(e) = self.pool.delete(self.addr) {
|
||||
self.deletion_failed_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolRwGuard<'a> {
|
||||
guard: PoolGuard<'a>,
|
||||
}
|
||||
|
||||
impl<'a> PoolRwGuard<'a> {
|
||||
pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self {
|
||||
Self {
|
||||
guard: PoolGuard::new(pool, addr),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify(&mut self) -> Result<&mut [u8], StoreError> {
|
||||
self.guard.pool.modify(&self.guard.addr)
|
||||
}
|
||||
|
||||
delegate!(
|
||||
to self.guard {
|
||||
pub fn read(&self) -> Result<&[u8], StoreError>;
|
||||
/// Releasing the pool guard will disable the automatic deletion of the data when the guard
|
||||
/// is dropped.
|
||||
pub fn release(&mut self);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
pub trait PoolProvider {
|
||||
/// Add new data to the pool. The provider should attempt to reserve a memory block with the
|
||||
/// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can
|
||||
/// be used to access the data stored in the pool
|
||||
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError>;
|
||||
|
||||
/// The provider should attempt to reserve a free memory block with the appropriate size and
|
||||
/// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access
|
||||
/// the data stored in the pool
|
||||
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>;
|
||||
|
||||
/// Modify data added previously using a given [StoreAddr] by yielding a mutable reference
|
||||
/// to it
|
||||
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>;
|
||||
|
||||
/// This function behaves like [Self::modify], but consumes the provided address and returns a
|
||||
/// RAII conformant guard object.
|
||||
///
|
||||
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
|
||||
/// given address will be deleted automatically when the guard is dropped.
|
||||
/// This can prevent memory leaks. Users can read (and modify) the data and release the guard
|
||||
/// if the data in the store is valid for further processing. If the data is faulty, no
|
||||
/// manual deletion is necessary when returning from a processing function prematurely.
|
||||
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard;
|
||||
|
||||
/// Read data by yielding a read-only reference given a [StoreAddr]
|
||||
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>;
|
||||
|
||||
/// This function behaves like [Self::read], but consumes the provided address and returns a
|
||||
/// RAII conformant guard object.
|
||||
///
|
||||
/// Unless the guard [PoolRwGuard::release] method is called, the data for the
|
||||
/// given address will be deleted automatically when the guard is dropped.
|
||||
/// This can prevent memory leaks. Users can read the data and release the guard
|
||||
/// if the data in the store is valid for further processing. If the data is faulty, no
|
||||
/// manual deletion is necessary when returning from a processing function prematurely.
|
||||
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard;
|
||||
|
||||
/// Delete data inside the pool given a [StoreAddr]
|
||||
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>;
|
||||
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError>;
|
||||
|
||||
/// Retrieve the length of the data at the given store address.
|
||||
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
|
||||
if !self.has_element_at(addr)? {
|
||||
return Err(StoreError::DataDoesNotExist(*addr));
|
||||
}
|
||||
Ok(self.read(addr)?.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pool implementation providing sub-pools with fixed size memory blocks. More details in
|
||||
/// the [module documentation][crate::pool]
|
||||
pub struct LocalPool {
|
||||
pool_cfg: PoolCfg,
|
||||
/// Pool implementation providing sub-pools with fixed size memory blocks.
|
||||
///
|
||||
/// This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool
|
||||
/// configuration. After the pre-allocation, no dynamic memory allocation will be performed
|
||||
/// during run-time. This makes the implementation suitable for real-time applications and
|
||||
/// embedded environments. The pool implementation will also track the size of the data stored
|
||||
/// inside it.
|
||||
///
|
||||
/// Transactions with the [pool][StaticMemoryPool] are done using a generic
|
||||
/// [address][StoreAddr] type.
|
||||
/// Adding any data to the pool will yield a store address. Modification and read operations are
|
||||
/// done using a reference to a store address. Deletion will consume the store address.
|
||||
pub struct StaticMemoryPool {
|
||||
pool_cfg: StaticPoolConfig,
|
||||
pool: Vec<Vec<u8>>,
|
||||
sizes_lists: Vec<Vec<PoolSize>>,
|
||||
}
|
||||
|
||||
impl LocalPool {
|
||||
/// Create a new local pool from the [given configuration][PoolCfg]. This function will sanitize
|
||||
/// the given configuration as well.
|
||||
pub fn new(mut cfg: PoolCfg) -> LocalPool {
|
||||
impl StaticMemoryPool {
|
||||
/// Create a new local pool from the [given configuration][StaticPoolConfig]. This function
|
||||
/// will sanitize the given configuration as well.
|
||||
pub fn new(mut cfg: StaticPoolConfig) -> StaticMemoryPool {
|
||||
let subpools_num = cfg.sanitize();
|
||||
let mut local_pool = LocalPool {
|
||||
let mut local_pool = StaticMemoryPool {
|
||||
pool_cfg: cfg,
|
||||
pool: Vec::with_capacity(subpools_num),
|
||||
sizes_lists: Vec::with_capacity(subpools_num),
|
||||
@ -372,39 +427,39 @@ mod alloc_mod {
|
||||
local_pool
|
||||
}
|
||||
|
||||
fn addr_check(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
|
||||
fn addr_check(&self, addr: &StaticPoolAddr) -> Result<usize, StoreError> {
|
||||
self.validate_addr(addr)?;
|
||||
let pool_idx = addr.pool_idx as usize;
|
||||
let size_list = self.sizes_lists.get(pool_idx).unwrap();
|
||||
let curr_size = size_list[addr.packet_idx as usize];
|
||||
if curr_size == STORE_FREE {
|
||||
return Err(StoreError::DataDoesNotExist(*addr));
|
||||
return Err(StoreError::DataDoesNotExist(StoreAddr::from(*addr)));
|
||||
}
|
||||
Ok(curr_size)
|
||||
}
|
||||
|
||||
fn validate_addr(&self, addr: &StoreAddr) -> Result<(), StoreError> {
|
||||
fn validate_addr(&self, addr: &StaticPoolAddr) -> Result<(), StoreError> {
|
||||
let pool_idx = addr.pool_idx as usize;
|
||||
if pool_idx >= self.pool_cfg.cfg.len() {
|
||||
return Err(StoreError::InvalidStoreId(
|
||||
StoreIdError::InvalidSubpool(addr.pool_idx),
|
||||
Some(*addr),
|
||||
Some(StoreAddr::from(*addr)),
|
||||
));
|
||||
}
|
||||
if addr.packet_idx >= self.pool_cfg.cfg[addr.pool_idx as usize].0 {
|
||||
return Err(StoreError::InvalidStoreId(
|
||||
StoreIdError::InvalidPacketIdx(addr.packet_idx),
|
||||
Some(*addr),
|
||||
Some(StoreAddr::from(*addr)),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve(&mut self, data_len: usize) -> Result<StoreAddr, StoreError> {
|
||||
fn reserve(&mut self, data_len: usize) -> Result<StaticPoolAddr, StoreError> {
|
||||
let subpool_idx = self.find_subpool(data_len, 0)?;
|
||||
let (slot, size_slot_ref) = self.find_empty(subpool_idx)?;
|
||||
*size_slot_ref = data_len;
|
||||
Ok(StoreAddr {
|
||||
Ok(StaticPoolAddr {
|
||||
pool_idx: subpool_idx,
|
||||
packet_idx: slot,
|
||||
})
|
||||
@ -422,7 +477,7 @@ mod alloc_mod {
|
||||
Err(StoreError::DataTooLarge(req_size))
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: &StoreAddr, data: &[u8]) -> Result<(), StoreError> {
|
||||
fn write(&mut self, addr: &StaticPoolAddr, data: &[u8]) -> Result<(), StoreError> {
|
||||
let packet_pos = self.raw_pos(addr).ok_or(StoreError::InternalError(0))?;
|
||||
let subpool = self
|
||||
.pool
|
||||
@ -449,13 +504,13 @@ mod alloc_mod {
|
||||
Err(StoreError::StoreFull(subpool))
|
||||
}
|
||||
|
||||
fn raw_pos(&self, addr: &StoreAddr) -> Option<usize> {
|
||||
fn raw_pos(&self, addr: &StaticPoolAddr) -> Option<usize> {
|
||||
let (_, size) = self.pool_cfg.cfg.get(addr.pool_idx as usize)?;
|
||||
Some(addr.packet_idx as usize * size)
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolProvider for LocalPool {
|
||||
impl PoolProvider for StaticMemoryPool {
|
||||
fn add(&mut self, data: &[u8]) -> Result<StoreAddr, StoreError> {
|
||||
let data_len = data.len();
|
||||
if data_len > POOL_MAX_SIZE {
|
||||
@ -463,10 +518,14 @@ mod alloc_mod {
|
||||
}
|
||||
let addr = self.reserve(data_len)?;
|
||||
self.write(&addr, data)?;
|
||||
Ok(addr)
|
||||
Ok(addr.into())
|
||||
}
|
||||
|
||||
fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> {
|
||||
fn free_element<W: FnMut(&mut [u8])>(
|
||||
&mut self,
|
||||
len: usize,
|
||||
mut writer: W,
|
||||
) -> Result<StoreAddr, StoreError> {
|
||||
if len > POOL_MAX_SIZE {
|
||||
return Err(StoreError::DataTooLarge(len));
|
||||
}
|
||||
@ -474,34 +533,44 @@ mod alloc_mod {
|
||||
let raw_pos = self.raw_pos(&addr).unwrap();
|
||||
let block =
|
||||
&mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len];
|
||||
Ok((addr, block))
|
||||
writer(block);
|
||||
Ok(addr.into())
|
||||
}
|
||||
|
||||
fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> {
|
||||
let curr_size = self.addr_check(addr)?;
|
||||
let raw_pos = self.raw_pos(addr).unwrap();
|
||||
fn modify<U: FnMut(&mut [u8])>(
|
||||
&mut self,
|
||||
addr: &StoreAddr,
|
||||
mut updater: U,
|
||||
) -> Result<(), StoreError> {
|
||||
let addr = StaticPoolAddr::from(*addr);
|
||||
let curr_size = self.addr_check(&addr)?;
|
||||
let raw_pos = self.raw_pos(&addr).unwrap();
|
||||
let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap()
|
||||
[raw_pos..raw_pos + curr_size];
|
||||
Ok(block)
|
||||
updater(block);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard {
|
||||
PoolRwGuard::new(self, addr)
|
||||
}
|
||||
|
||||
fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> {
|
||||
let curr_size = self.addr_check(addr)?;
|
||||
let raw_pos = self.raw_pos(addr).unwrap();
|
||||
fn read(&self, addr: &StoreAddr, buf: &mut [u8]) -> Result<usize, StoreError> {
|
||||
let addr = StaticPoolAddr::from(*addr);
|
||||
let curr_size = self.addr_check(&addr)?;
|
||||
if buf.len() < curr_size {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: curr_size,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let raw_pos = self.raw_pos(&addr).unwrap();
|
||||
let block =
|
||||
&self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size];
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard {
|
||||
PoolGuard::new(self, addr)
|
||||
//block.copy_from_slice(&src);
|
||||
buf[..curr_size].copy_from_slice(block);
|
||||
Ok(curr_size)
|
||||
}
|
||||
|
||||
fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> {
|
||||
let addr = StaticPoolAddr::from(addr);
|
||||
self.addr_check(&addr)?;
|
||||
let block_size = self.pool_cfg.cfg.get(addr.pool_idx as usize).unwrap().1;
|
||||
let raw_pos = self.raw_pos(&addr).unwrap();
|
||||
@ -514,7 +583,8 @@ mod alloc_mod {
|
||||
}
|
||||
|
||||
fn has_element_at(&self, addr: &StoreAddr) -> Result<bool, StoreError> {
|
||||
self.validate_addr(addr)?;
|
||||
let addr = StaticPoolAddr::from(*addr);
|
||||
self.validate_addr(&addr)?;
|
||||
let pool_idx = addr.pool_idx as usize;
|
||||
let size_list = self.sizes_lists.get(pool_idx).unwrap();
|
||||
let curr_size = size_list[addr.packet_idx as usize];
|
||||
@ -523,35 +593,57 @@ mod alloc_mod {
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn len_of_data(&self, addr: &StoreAddr) -> Result<usize, StoreError> {
|
||||
let addr = StaticPoolAddr::from(*addr);
|
||||
self.validate_addr(&addr)?;
|
||||
let pool_idx = addr.pool_idx as usize;
|
||||
let size_list = self.sizes_lists.get(pool_idx).unwrap();
|
||||
let size = size_list[addr.packet_idx as usize];
|
||||
Ok(match size {
|
||||
STORE_FREE => 0,
|
||||
_ => size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolProviderWithGuards for StaticMemoryPool {
|
||||
fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard<Self> {
|
||||
PoolRwGuard::new(self, addr)
|
||||
}
|
||||
|
||||
fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard<Self> {
|
||||
PoolGuard::new(self, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pool::{
|
||||
LocalPool, PoolCfg, PoolGuard, PoolProvider, PoolRwGuard, StoreAddr, StoreError,
|
||||
StoreIdError, POOL_MAX_SIZE,
|
||||
PoolGuard, PoolProvider, PoolProviderWithGuards, PoolRwGuard, StaticMemoryPool,
|
||||
StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError, POOL_MAX_SIZE,
|
||||
};
|
||||
use std::vec;
|
||||
|
||||
fn basic_small_pool() -> LocalPool {
|
||||
fn basic_small_pool() -> StaticMemoryPool {
|
||||
// 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes
|
||||
let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]);
|
||||
LocalPool::new(pool_cfg)
|
||||
let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]);
|
||||
StaticMemoryPool::new(pool_cfg)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cfg() {
|
||||
// Values where number of buckets is 0 or size is too large should be removed
|
||||
let mut pool_cfg = PoolCfg::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]);
|
||||
let mut pool_cfg = StaticPoolConfig::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]);
|
||||
pool_cfg.sanitize();
|
||||
assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]);
|
||||
// Entries should be ordered according to bucket size
|
||||
pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]);
|
||||
pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
|
||||
pool_cfg.sanitize();
|
||||
assert_eq!(*pool_cfg.cfg(), vec![(32, 3), (16, 6), (8, 12)]);
|
||||
// Unstable sort is used, so order of entries with same block length should not matter
|
||||
pool_cfg = PoolCfg::new(vec![(12, 12), (14, 16), (10, 12)]);
|
||||
pool_cfg = StaticPoolConfig::new(vec![(12, 12), (14, 16), (10, 12)]);
|
||||
pool_cfg.sanitize();
|
||||
assert!(
|
||||
*pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)]
|
||||
@ -566,13 +658,14 @@ mod tests {
|
||||
for (i, val) in test_buf.iter_mut().enumerate() {
|
||||
*val = i as u8;
|
||||
}
|
||||
let mut other_buf: [u8; 16] = [0; 16];
|
||||
let addr = local_pool.add(&test_buf).expect("Adding data failed");
|
||||
// Read back data and verify correctness
|
||||
let res = local_pool.read(&addr);
|
||||
let res = local_pool.read(&addr, &mut other_buf);
|
||||
assert!(res.is_ok());
|
||||
let buf_read_back = res.unwrap();
|
||||
assert_eq!(buf_read_back.len(), 16);
|
||||
for (i, &val) in buf_read_back.iter().enumerate() {
|
||||
let read_len = res.unwrap();
|
||||
assert_eq!(read_len, 16);
|
||||
for (i, &val) in other_buf.iter().enumerate() {
|
||||
assert_eq!(val, i as u8);
|
||||
}
|
||||
}
|
||||
@ -582,8 +675,10 @@ mod tests {
|
||||
let mut local_pool = basic_small_pool();
|
||||
let test_buf: [u8; 12] = [0; 12];
|
||||
let addr = local_pool.add(&test_buf).expect("Adding data failed");
|
||||
let res = local_pool.read(&addr).expect("Read back failed");
|
||||
assert_eq!(res.len(), 12);
|
||||
let res = local_pool
|
||||
.read(&addr, &mut [0; 12])
|
||||
.expect("Read back failed");
|
||||
assert_eq!(res, 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -594,18 +689,20 @@ mod tests {
|
||||
// Delete the data
|
||||
let res = local_pool.delete(addr);
|
||||
assert!(res.is_ok());
|
||||
let mut writer = |buf: &mut [u8]| {
|
||||
assert_eq!(buf.len(), 12);
|
||||
};
|
||||
// Verify that the slot is free by trying to get a reference to it
|
||||
let res = local_pool.free_element(12);
|
||||
let res = local_pool.free_element(12, &mut writer);
|
||||
assert!(res.is_ok());
|
||||
let (addr, buf_ref) = res.unwrap();
|
||||
let addr = res.unwrap();
|
||||
assert_eq!(
|
||||
addr,
|
||||
StoreAddr {
|
||||
u64::from(StaticPoolAddr {
|
||||
pool_idx: 2,
|
||||
packet_idx: 0
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(buf_ref.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -619,29 +716,34 @@ mod tests {
|
||||
|
||||
{
|
||||
// Verify that the slot is free by trying to get a reference to it
|
||||
let res = local_pool.modify(&addr).expect("Modifying data failed");
|
||||
res[0] = 0;
|
||||
res[1] = 0x42;
|
||||
local_pool
|
||||
.modify(&addr, &mut |buf: &mut [u8]| {
|
||||
buf[0] = 0;
|
||||
buf[1] = 0x42;
|
||||
})
|
||||
.expect("Modifying data failed");
|
||||
}
|
||||
|
||||
let res = local_pool.read(&addr).expect("Reading back data failed");
|
||||
assert_eq!(res[0], 0);
|
||||
assert_eq!(res[1], 0x42);
|
||||
assert_eq!(res[2], 2);
|
||||
assert_eq!(res[3], 3);
|
||||
local_pool
|
||||
.read(&addr, &mut test_buf)
|
||||
.expect("Reading back data failed");
|
||||
assert_eq!(test_buf[0], 0);
|
||||
assert_eq!(test_buf[1], 0x42);
|
||||
assert_eq!(test_buf[2], 2);
|
||||
assert_eq!(test_buf[3], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consecutive_reservation() {
|
||||
let mut local_pool = basic_small_pool();
|
||||
// Reserve two smaller blocks consecutively and verify that the third reservation fails
|
||||
let res = local_pool.free_element(8);
|
||||
let res = local_pool.free_element(8, |_| {});
|
||||
assert!(res.is_ok());
|
||||
let (addr0, _) = res.unwrap();
|
||||
let res = local_pool.free_element(8);
|
||||
let addr0 = res.unwrap();
|
||||
let res = local_pool.free_element(8, |_| {});
|
||||
assert!(res.is_ok());
|
||||
let (addr1, _) = res.unwrap();
|
||||
let res = local_pool.free_element(8);
|
||||
let addr1 = res.unwrap();
|
||||
let res = local_pool.free_element(8, |_| {});
|
||||
assert!(res.is_err());
|
||||
let err = res.unwrap_err();
|
||||
assert_eq!(err, StoreError::StoreFull(1));
|
||||
@ -655,10 +757,14 @@ mod tests {
|
||||
fn test_read_does_not_exist() {
|
||||
let local_pool = basic_small_pool();
|
||||
// Try to access data which does not exist
|
||||
let res = local_pool.read(&StoreAddr {
|
||||
packet_idx: 0,
|
||||
pool_idx: 0,
|
||||
});
|
||||
let res = local_pool.read(
|
||||
&StaticPoolAddr {
|
||||
packet_idx: 0,
|
||||
pool_idx: 0,
|
||||
}
|
||||
.into(),
|
||||
&mut [],
|
||||
);
|
||||
assert!(res.is_err());
|
||||
assert!(matches!(
|
||||
res.unwrap_err(),
|
||||
@ -684,11 +790,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_invalid_pool_idx() {
|
||||
let local_pool = basic_small_pool();
|
||||
let addr = StoreAddr {
|
||||
let addr = StaticPoolAddr {
|
||||
pool_idx: 3,
|
||||
packet_idx: 0,
|
||||
};
|
||||
let res = local_pool.read(&addr);
|
||||
}
|
||||
.into();
|
||||
let res = local_pool.read(&addr, &mut []);
|
||||
assert!(res.is_err());
|
||||
let err = res.unwrap_err();
|
||||
assert!(matches!(
|
||||
@ -700,12 +807,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_invalid_packet_idx() {
|
||||
let local_pool = basic_small_pool();
|
||||
let addr = StoreAddr {
|
||||
let addr = StaticPoolAddr {
|
||||
pool_idx: 2,
|
||||
packet_idx: 1,
|
||||
};
|
||||
assert_eq!(addr.raw(), 0x00020001);
|
||||
let res = local_pool.read(&addr);
|
||||
let res = local_pool.read(&addr.into(), &mut []);
|
||||
assert!(res.is_err());
|
||||
let err = res.unwrap_err();
|
||||
assert!(matches!(
|
||||
@ -727,7 +834,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_data_too_large_1() {
|
||||
let mut local_pool = basic_small_pool();
|
||||
let res = local_pool.free_element(POOL_MAX_SIZE + 1);
|
||||
let res = local_pool.free_element(POOL_MAX_SIZE + 1, |_| {});
|
||||
assert!(res.is_err());
|
||||
assert_eq!(
|
||||
res.unwrap_err(),
|
||||
@ -739,7 +846,7 @@ mod tests {
|
||||
fn test_free_element_too_large() {
|
||||
let mut local_pool = basic_small_pool();
|
||||
// Try to request a slot which is too large
|
||||
let res = local_pool.free_element(20);
|
||||
let res = local_pool.free_element(20, |_| {});
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err(), StoreError::DataTooLarge(20));
|
||||
}
|
||||
@ -781,7 +888,7 @@ mod tests {
|
||||
let test_buf: [u8; 16] = [0; 16];
|
||||
let addr = local_pool.add(&test_buf).expect("Adding data failed");
|
||||
let mut rw_guard = PoolRwGuard::new(&mut local_pool, addr);
|
||||
let _ = rw_guard.modify().expect("modify failed");
|
||||
rw_guard.update(&mut |_| {}).expect("modify failed");
|
||||
drop(rw_guard);
|
||||
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
|
||||
}
|
||||
@ -792,7 +899,7 @@ mod tests {
|
||||
let test_buf: [u8; 16] = [0; 16];
|
||||
let addr = local_pool.add(&test_buf).expect("Adding data failed");
|
||||
let mut rw_guard = local_pool.modify_with_guard(addr);
|
||||
let _ = rw_guard.modify().expect("modify failed");
|
||||
rw_guard.update(&mut |_| {}).expect("modify failed");
|
||||
drop(rw_guard);
|
||||
assert!(!local_pool.has_element_at(&addr).expect("Invalid address"));
|
||||
}
|
||||
@ -808,13 +915,25 @@ mod tests {
|
||||
let addr1 = local_pool.add(&test_buf_1).expect("Adding data failed");
|
||||
let addr2 = local_pool.add(&test_buf_2).expect("Adding data failed");
|
||||
let addr3 = local_pool.add(&test_buf_3).expect("Adding data failed");
|
||||
let tm0_raw = local_pool.modify(&addr0).expect("Modifying data failed");
|
||||
assert_eq!(tm0_raw, test_buf_0);
|
||||
let tm1_raw = local_pool.modify(&addr1).expect("Modifying data failed");
|
||||
assert_eq!(tm1_raw, test_buf_1);
|
||||
let tm2_raw = local_pool.modify(&addr2).expect("Modifying data failed");
|
||||
assert_eq!(tm2_raw, test_buf_2);
|
||||
let tm3_raw = local_pool.modify(&addr3).expect("Modifying data failed");
|
||||
assert_eq!(tm3_raw, test_buf_3);
|
||||
local_pool
|
||||
.modify(&addr0, |buf| {
|
||||
assert_eq!(buf, test_buf_0);
|
||||
})
|
||||
.expect("Modifying data failed");
|
||||
local_pool
|
||||
.modify(&addr1, |buf| {
|
||||
assert_eq!(buf, test_buf_1);
|
||||
})
|
||||
.expect("Modifying data failed");
|
||||
local_pool
|
||||
.modify(&addr2, |buf| {
|
||||
assert_eq!(buf, test_buf_2);
|
||||
})
|
||||
.expect("Modifying data failed");
|
||||
local_pool
|
||||
.modify(&addr3, |buf| {
|
||||
assert_eq!(buf, test_buf_3);
|
||||
})
|
||||
.expect("Modifying data failed");
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ impl EventReporterBase {
|
||||
Ok(PusTmCreator::new(
|
||||
&mut sp_header,
|
||||
sec_header,
|
||||
Some(&buf[0..current_idx]),
|
||||
&buf[0..current_idx],
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ pub mod heapless_mod {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum EventRequest<Event: GenericEvent = EventU32> {
|
||||
Enable(Event),
|
||||
Disable(Event),
|
||||
|
@ -1,85 +1,64 @@
|
||||
use crate::events::EventU32;
|
||||
use crate::pool::{SharedPool, StoreAddr};
|
||||
use crate::pus::event_man::{EventRequest, EventRequestWithToken};
|
||||
use crate::pus::verification::{
|
||||
StdVerifReporterWithSender, TcStateAccepted, TcStateToken, VerificationToken,
|
||||
};
|
||||
use crate::pus::{
|
||||
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult,
|
||||
PusPacketHandlingError, PusServiceBase, PusServiceHandler,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use crate::pus::verification::TcStateToken;
|
||||
use crate::pus::{PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError};
|
||||
use spacepackets::ecss::event::Subservice;
|
||||
use spacepackets::ecss::tc::PusTcReader;
|
||||
use spacepackets::ecss::PusPacket;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct PusService5EventHandler {
|
||||
psb: PusServiceBase,
|
||||
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
|
||||
|
||||
pub struct PusService5EventHandler<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub service_helper: PusServiceHelper<TcInMemConverter>,
|
||||
event_request_tx: Sender<EventRequestWithToken>,
|
||||
}
|
||||
|
||||
impl PusService5EventHandler {
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusService5EventHandler<TcInMemConverter> {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_store: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
service_handler: PusServiceHelper<TcInMemConverter>,
|
||||
event_request_tx: Sender<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
Self {
|
||||
psb: PusServiceBase::new(
|
||||
tc_receiver,
|
||||
shared_tc_store,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler,
|
||||
),
|
||||
service_helper: service_handler,
|
||||
event_request_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusServiceHandler for PusService5EventHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase {
|
||||
&mut self.psb
|
||||
}
|
||||
fn psb(&self) -> &PusServiceBase {
|
||||
&self.psb
|
||||
}
|
||||
|
||||
fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
self.copy_tc_to_buf(addr)?;
|
||||
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
|
||||
pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
let tc = self
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
|
||||
let subservice = tc.subservice();
|
||||
let srv = Subservice::try_from(subservice);
|
||||
if srv.is_err() {
|
||||
return Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
token,
|
||||
ecss_tc_and_token.token,
|
||||
));
|
||||
}
|
||||
let handle_enable_disable_request = |enable: bool, stamp: [u8; 7]| {
|
||||
if tc.user_data().len() < 4 {
|
||||
return Err(PusPacketHandlingError::NotEnoughAppData(
|
||||
"At least 4 bytes event ID expected".into(),
|
||||
"at least 4 bytes event ID expected".into(),
|
||||
));
|
||||
}
|
||||
let user_data = tc.user_data();
|
||||
let event_u32 = EventU32::from(u32::from_be_bytes(user_data[0..4].try_into().unwrap()));
|
||||
let start_token = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_success(token, Some(&stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&stamp))
|
||||
.map_err(|_| PartialPusHandlingError::Verification);
|
||||
let partial_error = start_token.clone().err();
|
||||
let mut token: TcStateToken = token.into();
|
||||
let mut token: TcStateToken = ecss_tc_and_token.token.into();
|
||||
if let Ok(start_token) = start_token {
|
||||
token = start_token.into();
|
||||
}
|
||||
@ -107,7 +86,7 @@ impl PusServiceHandler for PusService5EventHandler {
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
};
|
||||
let mut partial_error = None;
|
||||
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
|
||||
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
|
||||
match srv.unwrap() {
|
||||
Subservice::TmInfoReport
|
||||
| Subservice::TmLowSeverityReport
|
||||
@ -123,7 +102,8 @@ impl PusServiceHandler for PusService5EventHandler {
|
||||
}
|
||||
Subservice::TcReportDisabledList | Subservice::TmDisabledEventsReport => {
|
||||
return Ok(PusPacketHandlerResult::SubserviceNotImplemented(
|
||||
subservice, token,
|
||||
subservice,
|
||||
ecss_tc_and_token.token,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -131,3 +111,170 @@ impl PusServiceHandler for PusService5EventHandler {
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use delegate::delegate;
|
||||
use spacepackets::ecss::event::Subservice;
|
||||
use spacepackets::util::UnsignedEnum;
|
||||
use spacepackets::{
|
||||
ecss::{
|
||||
tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||
tm::PusTmReader,
|
||||
},
|
||||
SequenceFlags, SpHeader,
|
||||
};
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
|
||||
use crate::pus::event_man::EventRequest;
|
||||
use crate::pus::tests::SimplePusPacketHandler;
|
||||
use crate::pus::verification::RequestId;
|
||||
use crate::{
|
||||
events::EventU32,
|
||||
pus::{
|
||||
event_man::EventRequestWithToken,
|
||||
tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness, TEST_APID},
|
||||
verification::{TcStateAccepted, VerificationToken},
|
||||
EcssTcInSharedStoreConverter, PusPacketHandlerResult, PusPacketHandlingError,
|
||||
},
|
||||
};
|
||||
|
||||
use super::PusService5EventHandler;
|
||||
|
||||
const TEST_EVENT_0: EventU32 = EventU32::const_new(crate::events::Severity::INFO, 5, 25);
|
||||
|
||||
struct Pus5HandlerWithStoreTester {
|
||||
common: PusServiceHandlerWithSharedStoreCommon,
|
||||
handler: PusService5EventHandler<EcssTcInSharedStoreConverter>,
|
||||
}
|
||||
|
||||
impl Pus5HandlerWithStoreTester {
|
||||
pub fn new(event_request_tx: Sender<EventRequestWithToken>) -> Self {
|
||||
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
|
||||
Self {
|
||||
common,
|
||||
handler: PusService5EventHandler::new(srv_handler, event_request_tx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusTestHarness for Pus5HandlerWithStoreTester {
|
||||
delegate! {
|
||||
to self.common {
|
||||
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
|
||||
fn read_next_tm(&mut self) -> PusTmReader<'_>;
|
||||
fn check_no_tm_available(&self) -> bool;
|
||||
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl SimplePusPacketHandler for Pus5HandlerWithStoreTester {
|
||||
delegate! {
|
||||
to self.handler {
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn event_test(
|
||||
test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler),
|
||||
subservice: Subservice,
|
||||
expected_event_req: EventRequest,
|
||||
event_req_receiver: mpsc::Receiver<EventRequestWithToken>,
|
||||
) {
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(5, subservice as u8);
|
||||
let mut app_data = [0; 4];
|
||||
TEST_EVENT_0
|
||||
.write_to_be_bytes(&mut app_data)
|
||||
.expect("writing test event failed");
|
||||
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &app_data, true);
|
||||
let token = test_harness.send_tc(&ping_tc);
|
||||
let request_id = token.req_id();
|
||||
test_harness.handle_one_tc().unwrap();
|
||||
test_harness.check_next_verification_tm(1, request_id);
|
||||
test_harness.check_next_verification_tm(3, request_id);
|
||||
// Completion TM is not generated for us.
|
||||
assert!(test_harness.check_no_tm_available());
|
||||
let event_request = event_req_receiver
|
||||
.try_recv()
|
||||
.expect("no event request received");
|
||||
assert_eq!(expected_event_req, event_request.request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enabling_event_reporting() {
|
||||
let (event_request_tx, event_request_rx) = mpsc::channel();
|
||||
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
|
||||
event_test(
|
||||
&mut test_harness,
|
||||
Subservice::TcEnableEventGeneration,
|
||||
EventRequest::Enable(TEST_EVENT_0),
|
||||
event_request_rx,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabling_event_reporting() {
|
||||
let (event_request_tx, event_request_rx) = mpsc::channel();
|
||||
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
|
||||
event_test(
|
||||
&mut test_harness,
|
||||
Subservice::TcDisableEventGeneration,
|
||||
EventRequest::Disable(TEST_EVENT_0),
|
||||
event_request_rx,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_tc_queue() {
|
||||
let (event_request_tx, _) = mpsc::channel();
|
||||
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
if let PusPacketHandlerResult::Empty = result {
|
||||
} else {
|
||||
panic!("unexpected result type {result:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sending_custom_subservice() {
|
||||
let (event_request_tx, _) = mpsc::channel();
|
||||
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(5, 200);
|
||||
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
|
||||
test_harness.send_tc(&ping_tc);
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result {
|
||||
assert_eq!(subservice, 200);
|
||||
} else {
|
||||
panic!("unexpected result type {result:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sending_invalid_app_data() {
|
||||
let (event_request_tx, _) = mpsc::channel();
|
||||
let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx);
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(5, Subservice::TcEnableEventGeneration as u8);
|
||||
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &[0, 1, 2], true);
|
||||
test_harness.send_tc(&ping_tc);
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_err());
|
||||
let result = result.unwrap_err();
|
||||
if let PusPacketHandlingError::NotEnoughAppData(string) = result {
|
||||
assert_eq!(string, "at least 4 bytes event ID expected");
|
||||
} else {
|
||||
panic!("unexpected result type {result:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,12 +55,6 @@ impl<'tm> From<PusTmCreator<'tm>> for PusTmWrapper<'tm> {
|
||||
}
|
||||
}
|
||||
|
||||
pub type TcAddrWithToken = (StoreAddr, TcStateToken);
|
||||
|
||||
/// Generic abstraction for a telecommand being sent around after is has been accepted.
|
||||
/// The actual telecommand is stored inside a pre-allocated pool structure.
|
||||
pub type AcceptedTc = (StoreAddr, VerificationToken<TcStateAccepted>);
|
||||
|
||||
/// Generic error type for sending something via a message queue.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum GenericSendError {
|
||||
@ -200,11 +194,75 @@ pub trait EcssTcSenderCore: EcssChannel {
|
||||
fn send_tc(&self, tc: PusTcCreator, token: Option<TcStateToken>) -> Result<(), EcssTmtcError>;
|
||||
}
|
||||
|
||||
pub struct ReceivedTcWrapper {
|
||||
pub store_addr: StoreAddr,
|
||||
/// A PUS telecommand packet can be stored in memory using different methods. Right now,
|
||||
/// storage inside a pool structure like [crate::pool::StaticMemoryPool], and storage inside a
|
||||
/// `Vec<u8>` are supported.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TcInMemory {
|
||||
StoreAddr(StoreAddr),
|
||||
#[cfg(feature = "alloc")]
|
||||
Vec(alloc::vec::Vec<u8>),
|
||||
}
|
||||
|
||||
impl From<StoreAddr> for TcInMemory {
|
||||
fn from(value: StoreAddr) -> Self {
|
||||
Self::StoreAddr(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<alloc::vec::Vec<u8>> for TcInMemory {
|
||||
fn from(value: alloc::vec::Vec<u8>) -> Self {
|
||||
Self::Vec(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic structure for an ECSS PUS Telecommand and its correspoding verification token.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EcssTcAndToken {
|
||||
pub tc_in_memory: TcInMemory,
|
||||
pub token: Option<TcStateToken>,
|
||||
}
|
||||
|
||||
impl EcssTcAndToken {
|
||||
pub fn new(tc_in_memory: impl Into<TcInMemory>, token: impl Into<TcStateToken>) -> Self {
|
||||
Self {
|
||||
tc_in_memory: tc_in_memory.into(),
|
||||
token: Some(token.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic abstraction for a telecommand being sent around after is has been accepted.
|
||||
pub struct AcceptedEcssTcAndToken {
|
||||
pub tc_in_memory: TcInMemory,
|
||||
pub token: VerificationToken<TcStateAccepted>,
|
||||
}
|
||||
|
||||
impl From<AcceptedEcssTcAndToken> for EcssTcAndToken {
|
||||
fn from(value: AcceptedEcssTcAndToken) -> Self {
|
||||
EcssTcAndToken {
|
||||
tc_in_memory: value.tc_in_memory,
|
||||
token: Some(value.token.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EcssTcAndToken> for AcceptedEcssTcAndToken {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: EcssTcAndToken) -> Result<Self, Self::Error> {
|
||||
if let Some(TcStateToken::Accepted(token)) = value.token {
|
||||
return Ok(AcceptedEcssTcAndToken {
|
||||
tc_in_memory: value.tc_in_memory,
|
||||
token,
|
||||
});
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TryRecvTmtcError {
|
||||
Error(EcssTmtcError),
|
||||
@ -231,7 +289,7 @@ impl From<StoreError> for TryRecvTmtcError {
|
||||
|
||||
/// Generic trait for a user supplied receiver object.
|
||||
pub trait EcssTcReceiverCore: EcssChannel {
|
||||
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError>;
|
||||
fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError>;
|
||||
}
|
||||
|
||||
/// Generic trait for objects which can receive ECSS PUS telecommands. This trait is
|
||||
@ -260,10 +318,31 @@ mod alloc_mod {
|
||||
/// [Clone].
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
|
||||
pub trait EcssTmSender: EcssTmSenderCore + Downcast + DynClone {}
|
||||
pub trait EcssTmSender: EcssTmSenderCore + Downcast + DynClone {
|
||||
// Remove this once trait upcasting coercion has been implemented.
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
|
||||
fn upcast(&self) -> &dyn EcssTmSenderCore;
|
||||
// Remove this once trait upcasting coercion has been implemented.
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
|
||||
fn upcast_mut(&mut self) -> &mut dyn EcssTmSenderCore;
|
||||
}
|
||||
|
||||
/// Blanket implementation for all types which implement [EcssTmSenderCore] and are clonable.
|
||||
impl<T> EcssTmSender for T where T: EcssTmSenderCore + Clone + 'static {}
|
||||
impl<T> EcssTmSender for T
|
||||
where
|
||||
T: EcssTmSenderCore + Clone + 'static,
|
||||
{
|
||||
// Remove this once trait upcasting coercion has been implemented.
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
|
||||
fn upcast(&self) -> &dyn EcssTmSenderCore {
|
||||
self
|
||||
}
|
||||
// Remove this once trait upcasting coercion has been implemented.
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/65991
|
||||
fn upcast_mut(&mut self) -> &mut dyn EcssTmSenderCore {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(EcssTmSender);
|
||||
impl_downcast!(EcssTmSender);
|
||||
@ -309,21 +388,23 @@ mod alloc_mod {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
|
||||
pub mod std_mod {
|
||||
use crate::pool::{SharedPool, StoreAddr};
|
||||
use crate::pool::{PoolProvider, PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr};
|
||||
use crate::pus::verification::{
|
||||
StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
|
||||
};
|
||||
use crate::pus::{
|
||||
EcssChannel, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender, EcssTmSenderCore,
|
||||
EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper, ReceivedTcWrapper,
|
||||
TcAddrWithToken, TryRecvTmtcError,
|
||||
EcssChannel, EcssTcAndToken, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender,
|
||||
EcssTmSenderCore, EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper,
|
||||
TryRecvTmtcError,
|
||||
};
|
||||
use crate::tmtc::tm_helper::SharedTmStore;
|
||||
use crate::tmtc::tm_helper::SharedTmPool;
|
||||
use crate::ChannelId;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
use crossbeam_channel as cb;
|
||||
use spacepackets::ecss::tc::PusTcReader;
|
||||
use spacepackets::ecss::tm::PusTmCreator;
|
||||
use spacepackets::ecss::PusError;
|
||||
use spacepackets::time::cds::TimeProvider;
|
||||
@ -335,6 +416,9 @@ pub mod std_mod {
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::verification::VerificationReporterWithSender;
|
||||
use super::{AcceptedEcssTcAndToken, TcInMemory};
|
||||
|
||||
impl From<mpsc::SendError<StoreAddr>> for EcssTmtcError {
|
||||
fn from(_: mpsc::SendError<StoreAddr>) -> Self {
|
||||
Self::Send(GenericSendError::RxDisconnected)
|
||||
@ -357,14 +441,14 @@ pub mod std_mod {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MpscTmInStoreSender {
|
||||
pub struct MpscTmInSharedPoolSender {
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
shared_tm_store: SharedTmStore,
|
||||
shared_tm_store: SharedTmPool,
|
||||
sender: mpsc::Sender<StoreAddr>,
|
||||
}
|
||||
|
||||
impl EcssChannel for MpscTmInStoreSender {
|
||||
impl EcssChannel for MpscTmInSharedPoolSender {
|
||||
fn id(&self) -> ChannelId {
|
||||
self.id
|
||||
}
|
||||
@ -374,7 +458,7 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
impl MpscTmInStoreSender {
|
||||
impl MpscTmInSharedPoolSender {
|
||||
pub fn send_direct_tm(&self, tm: PusTmCreator) -> Result<(), EcssTmtcError> {
|
||||
let addr = self.shared_tm_store.add_pus_tm(&tm)?;
|
||||
self.sender
|
||||
@ -383,7 +467,7 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
impl EcssTmSenderCore for MpscTmInStoreSender {
|
||||
impl EcssTmSenderCore for MpscTmInSharedPoolSender {
|
||||
fn send_tm(&self, tm: PusTmWrapper) -> Result<(), EcssTmtcError> {
|
||||
match tm {
|
||||
PusTmWrapper::InStore(addr) => {
|
||||
@ -395,11 +479,11 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
impl MpscTmInStoreSender {
|
||||
impl MpscTmInSharedPoolSender {
|
||||
pub fn new(
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
shared_tm_store: SharedTmStore,
|
||||
shared_tm_store: SharedTmPool,
|
||||
sender: mpsc::Sender<StoreAddr>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@ -411,13 +495,13 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MpscTcInStoreReceiver {
|
||||
pub struct MpscTcReceiver {
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
receiver: mpsc::Receiver<TcAddrWithToken>,
|
||||
receiver: mpsc::Receiver<EcssTcAndToken>,
|
||||
}
|
||||
|
||||
impl EcssChannel for MpscTcInStoreReceiver {
|
||||
impl EcssChannel for MpscTcReceiver {
|
||||
fn id(&self) -> ChannelId {
|
||||
self.id
|
||||
}
|
||||
@ -427,26 +511,22 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
impl EcssTcReceiverCore for MpscTcInStoreReceiver {
|
||||
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> {
|
||||
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e {
|
||||
impl EcssTcReceiverCore for MpscTcReceiver {
|
||||
fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
|
||||
self.receiver.try_recv().map_err(|e| match e {
|
||||
TryRecvError::Empty => TryRecvTmtcError::Empty,
|
||||
TryRecvError::Disconnected => {
|
||||
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
|
||||
}
|
||||
})?;
|
||||
Ok(ReceivedTcWrapper {
|
||||
store_addr,
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MpscTcInStoreReceiver {
|
||||
impl MpscTcReceiver {
|
||||
pub fn new(
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
receiver: mpsc::Receiver<TcAddrWithToken>,
|
||||
receiver: mpsc::Receiver<EcssTcAndToken>,
|
||||
) -> Self {
|
||||
Self { id, name, receiver }
|
||||
}
|
||||
@ -459,8 +539,8 @@ pub mod std_mod {
|
||||
#[derive(Clone)]
|
||||
pub struct MpscTmAsVecSender {
|
||||
id: ChannelId,
|
||||
sender: mpsc::Sender<Vec<u8>>,
|
||||
name: &'static str,
|
||||
sender: mpsc::Sender<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<mpsc::SendError<Vec<u8>>> for EcssTmtcError {
|
||||
@ -502,7 +582,7 @@ pub mod std_mod {
|
||||
pub struct CrossbeamTmInStoreSender {
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
shared_tm_store: SharedTmStore,
|
||||
shared_tm_store: SharedTmPool,
|
||||
sender: crossbeam_channel::Sender<StoreAddr>,
|
||||
}
|
||||
|
||||
@ -510,7 +590,7 @@ pub mod std_mod {
|
||||
pub fn new(
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
shared_tm_store: SharedTmStore,
|
||||
shared_tm_store: SharedTmPool,
|
||||
sender: crossbeam_channel::Sender<StoreAddr>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@ -545,23 +625,23 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CrossbeamTcInStoreReceiver {
|
||||
pub struct CrossbeamTcReceiver {
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
receiver: cb::Receiver<TcAddrWithToken>,
|
||||
receiver: cb::Receiver<EcssTcAndToken>,
|
||||
}
|
||||
|
||||
impl CrossbeamTcInStoreReceiver {
|
||||
impl CrossbeamTcReceiver {
|
||||
pub fn new(
|
||||
id: ChannelId,
|
||||
name: &'static str,
|
||||
receiver: cb::Receiver<TcAddrWithToken>,
|
||||
receiver: cb::Receiver<EcssTcAndToken>,
|
||||
) -> Self {
|
||||
Self { id, name, receiver }
|
||||
}
|
||||
}
|
||||
|
||||
impl EcssChannel for CrossbeamTcInStoreReceiver {
|
||||
impl EcssChannel for CrossbeamTcReceiver {
|
||||
fn id(&self) -> ChannelId {
|
||||
self.id
|
||||
}
|
||||
@ -571,17 +651,13 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
impl EcssTcReceiverCore for CrossbeamTcInStoreReceiver {
|
||||
fn recv_tc(&self) -> Result<ReceivedTcWrapper, TryRecvTmtcError> {
|
||||
let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e {
|
||||
impl EcssTcReceiverCore for CrossbeamTcReceiver {
|
||||
fn recv_tc(&self) -> Result<EcssTcAndToken, TryRecvTmtcError> {
|
||||
self.receiver.try_recv().map_err(|e| match e {
|
||||
cb::TryRecvError::Empty => TryRecvTmtcError::Empty,
|
||||
cb::TryRecvError::Disconnected => {
|
||||
TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected))
|
||||
}
|
||||
})?;
|
||||
Ok(ReceivedTcWrapper {
|
||||
store_addr,
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -596,8 +672,12 @@ pub mod std_mod {
|
||||
InvalidSubservice(u8),
|
||||
#[error("not enough application data available: {0}")]
|
||||
NotEnoughAppData(String),
|
||||
#[error("PUS packet too large, does not fit in buffer: {0}")]
|
||||
PusPacketTooLarge(usize),
|
||||
#[error("invalid application data")]
|
||||
InvalidAppData(String),
|
||||
#[error("invalid format of TC in memory: {0:?}")]
|
||||
InvalidTcInMemoryFormat(TcInMemory),
|
||||
#[error("generic ECSS tmtc error: {0}")]
|
||||
EcssTmtc(#[from] EcssTmtcError),
|
||||
#[error("invalid verification token")]
|
||||
@ -634,42 +714,129 @@ pub mod std_mod {
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for handlers which can handle PUS TC packets. Right now, the verification
|
||||
/// reporter is constrained to the [StdVerifReporterWithSender] and the service handler
|
||||
/// relies on TMTC packets being exchanged via a [SharedPool].
|
||||
pub trait EcssTcInMemConverter {
|
||||
fn cache_ecss_tc_in_memory(
|
||||
&mut self,
|
||||
possible_packet: &TcInMemory,
|
||||
) -> Result<(), PusPacketHandlingError>;
|
||||
|
||||
fn tc_slice_raw(&self) -> &[u8];
|
||||
|
||||
fn convert_ecss_tc_in_memory_to_reader(
|
||||
&mut self,
|
||||
possible_packet: &TcInMemory,
|
||||
) -> Result<PusTcReader<'_>, PusPacketHandlingError> {
|
||||
self.cache_ecss_tc_in_memory(possible_packet)?;
|
||||
Ok(PusTcReader::new(self.tc_slice_raw())?.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converter structure for PUS telecommands which are stored inside a `Vec<u8>` structure.
|
||||
/// Please note that this structure is not able to convert TCs which are stored inside a
|
||||
/// [SharedStaticMemoryPool].
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EcssTcInVecConverter {
|
||||
pub pus_tc_raw: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EcssTcInMemConverter for EcssTcInVecConverter {
|
||||
fn cache_ecss_tc_in_memory(
|
||||
&mut self,
|
||||
tc_in_memory: &TcInMemory,
|
||||
) -> Result<(), PusPacketHandlingError> {
|
||||
self.pus_tc_raw = None;
|
||||
match tc_in_memory {
|
||||
super::TcInMemory::StoreAddr(_) => {
|
||||
return Err(PusPacketHandlingError::InvalidTcInMemoryFormat(
|
||||
tc_in_memory.clone(),
|
||||
));
|
||||
}
|
||||
super::TcInMemory::Vec(vec) => {
|
||||
self.pus_tc_raw = Some(vec.clone());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tc_slice_raw(&self) -> &[u8] {
|
||||
if self.pus_tc_raw.is_none() {
|
||||
return &[];
|
||||
}
|
||||
self.pus_tc_raw.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Converter structure for PUS telecommands which are stored inside
|
||||
/// [SharedStaticMemoryPool] structure. This is useful if run-time allocation for these
|
||||
/// packets should be avoided. Please note that this structure is not able to convert TCs which
|
||||
/// are stored as a `Vec<u8>`.
|
||||
pub struct EcssTcInSharedStoreConverter {
|
||||
shared_tc_store: SharedStaticMemoryPool,
|
||||
pus_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EcssTcInSharedStoreConverter {
|
||||
pub fn new(shared_tc_store: SharedStaticMemoryPool, max_expected_tc_size: usize) -> Self {
|
||||
Self {
|
||||
shared_tc_store,
|
||||
pus_buf: alloc::vec![0; max_expected_tc_size],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> {
|
||||
// Keep locked section as short as possible.
|
||||
let mut tc_pool = self
|
||||
.shared_tc_store
|
||||
.write()
|
||||
.map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?;
|
||||
let tc_size = tc_pool
|
||||
.len_of_data(&addr)
|
||||
.map_err(|e| PusPacketHandlingError::EcssTmtc(EcssTmtcError::Store(e)))?;
|
||||
if tc_size > self.pus_buf.len() {
|
||||
return Err(PusPacketHandlingError::PusPacketTooLarge(tc_size));
|
||||
}
|
||||
let tc_guard = tc_pool.read_with_guard(addr);
|
||||
// TODO: Proper error handling.
|
||||
tc_guard.read(&mut self.pus_buf[0..tc_size]).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl EcssTcInMemConverter for EcssTcInSharedStoreConverter {
|
||||
fn cache_ecss_tc_in_memory(
|
||||
&mut self,
|
||||
tc_in_memory: &TcInMemory,
|
||||
) -> Result<(), PusPacketHandlingError> {
|
||||
match tc_in_memory {
|
||||
super::TcInMemory::StoreAddr(addr) => {
|
||||
self.copy_tc_to_buf(*addr)?;
|
||||
}
|
||||
super::TcInMemory::Vec(_) => {
|
||||
return Err(PusPacketHandlingError::InvalidTcInMemoryFormat(
|
||||
tc_in_memory.clone(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tc_slice_raw(&self) -> &[u8] {
|
||||
self.pus_buf.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PusServiceBase {
|
||||
pub tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
pub shared_tc_store: SharedPool,
|
||||
pub tm_sender: Box<dyn EcssTmSender>,
|
||||
pub tm_apid: u16,
|
||||
/// The verification handler is wrapped in a [RefCell] to allow the interior mutability
|
||||
/// pattern. This makes writing methods which are not mutable a lot easier.
|
||||
pub verification_handler: RefCell<StdVerifReporterWithSender>,
|
||||
pub pus_buf: [u8; 2048],
|
||||
pub pus_size: usize,
|
||||
}
|
||||
|
||||
impl PusServiceBase {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_store: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_receiver,
|
||||
shared_tc_store,
|
||||
tm_apid,
|
||||
tm_sender,
|
||||
verification_handler: RefCell::new(verification_handler),
|
||||
pus_buf: [0; 2048],
|
||||
pus_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn get_current_timestamp(
|
||||
&self,
|
||||
partial_error: &mut Option<PartialPusHandlingError>,
|
||||
) -> [u8; 7] {
|
||||
let mut time_stamp: [u8; 7] = [0; 7];
|
||||
@ -684,48 +851,73 @@ pub mod std_mod {
|
||||
time_stamp
|
||||
}
|
||||
|
||||
pub fn get_current_timestamp_ignore_error(&self) -> [u8; 7] {
|
||||
#[cfg(feature = "std")]
|
||||
pub fn get_current_timestamp_ignore_error() -> [u8; 7] {
|
||||
let mut dummy = None;
|
||||
self.get_current_timestamp(&mut dummy)
|
||||
Self::get_current_timestamp(&mut dummy)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PusServiceHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase;
|
||||
fn psb(&self) -> &PusServiceBase;
|
||||
fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
|
||||
/// This is a high-level PUS packet handler helper.
|
||||
///
|
||||
/// It performs some of the boilerplate acitivities involved when handling PUS telecommands and
|
||||
/// it can be used to implement the handling of PUS telecommands for certain PUS telecommands
|
||||
/// groups (for example individual services).
|
||||
///
|
||||
/// This base class can handle PUS telecommands backed by different memory storage machanisms
|
||||
/// by using the [EcssTcInMemConverter] abstraction. This object provides some convenience
|
||||
/// methods to make the generic parts of TC handling easier.
|
||||
pub struct PusServiceHelper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub common: PusServiceBase,
|
||||
pub tc_in_mem_converter: TcInMemConverter,
|
||||
}
|
||||
|
||||
fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> {
|
||||
// Keep locked section as short as possible.
|
||||
let psb_mut = self.psb_mut();
|
||||
let mut tc_pool = psb_mut
|
||||
.shared_tc_store
|
||||
.write()
|
||||
.map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?;
|
||||
let tc_guard = tc_pool.read_with_guard(addr);
|
||||
let tc_raw = tc_guard.read().unwrap();
|
||||
psb_mut.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw);
|
||||
Ok(())
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusServiceHelper<TcInMemConverter> {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: VerificationReporterWithSender,
|
||||
tc_in_mem_converter: TcInMemConverter,
|
||||
) -> Self {
|
||||
Self {
|
||||
common: PusServiceBase {
|
||||
tc_receiver,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler: RefCell::new(verification_handler),
|
||||
},
|
||||
tc_in_mem_converter,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_next_packet(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
match self.psb().tc_receiver.recv_tc() {
|
||||
Ok(ReceivedTcWrapper { store_addr, token }) => {
|
||||
/// This function can be used to poll the internal [EcssTcReceiver] object for the next
|
||||
/// telecommand packet. It will return `Ok(None)` if there are not packets available.
|
||||
/// In any other case, it will perform the acceptance of the ECSS TC packet using the
|
||||
/// internal [VerificationReporterWithSender] object. It will then return the telecommand
|
||||
/// and the according accepted token.
|
||||
pub fn retrieve_and_accept_next_packet(
|
||||
&mut self,
|
||||
) -> Result<Option<AcceptedEcssTcAndToken>, PusPacketHandlingError> {
|
||||
match self.common.tc_receiver.recv_tc() {
|
||||
Ok(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token,
|
||||
}) => {
|
||||
if token.is_none() {
|
||||
return Err(PusPacketHandlingError::InvalidVerificationToken);
|
||||
}
|
||||
let token = token.unwrap();
|
||||
let accepted_token = VerificationToken::<TcStateAccepted>::try_from(token)
|
||||
.map_err(|_| PusPacketHandlingError::InvalidVerificationToken)?;
|
||||
self.handle_one_tc(store_addr, accepted_token)
|
||||
Ok(Some(AcceptedEcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: accepted_token,
|
||||
}))
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvTmtcError::Error(e) => Err(PusPacketHandlingError::EcssTmtc(e)),
|
||||
TryRecvTmtcError::Empty => Ok(PusPacketHandlerResult::Empty),
|
||||
TryRecvTmtcError::Empty => Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -746,10 +938,34 @@ pub(crate) fn source_buffer_large_enough(cap: usize, len: usize) -> Result<(), E
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator};
|
||||
pub mod tests {
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
use std::sync::{mpsc, RwLock};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec;
|
||||
use spacepackets::ecss::tc::PusTcCreator;
|
||||
use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator, PusTmReader};
|
||||
use spacepackets::ecss::{PusPacket, WritablePusPacket};
|
||||
use spacepackets::CcsdsPacket;
|
||||
|
||||
use crate::pool::{
|
||||
PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig, StoreAddr,
|
||||
};
|
||||
use crate::pus::verification::RequestId;
|
||||
use crate::tmtc::tm_helper::SharedTmPool;
|
||||
|
||||
use super::verification::{
|
||||
TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
|
||||
};
|
||||
use super::{
|
||||
EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver,
|
||||
MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
|
||||
PusPacketHandlingError, PusServiceHelper, TcInMemory,
|
||||
};
|
||||
|
||||
pub const TEST_APID: u16 = 0x101;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub(crate) struct CommonTmInfo {
|
||||
pub subservice: u8,
|
||||
@ -759,12 +975,23 @@ pub(crate) mod tests {
|
||||
pub time_stamp: [u8; 7],
|
||||
}
|
||||
|
||||
pub trait PusTestHarness {
|
||||
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
|
||||
fn read_next_tm(&mut self) -> PusTmReader<'_>;
|
||||
fn check_no_tm_available(&self) -> bool;
|
||||
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
|
||||
}
|
||||
|
||||
pub trait SimplePusPacketHandler {
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
|
||||
}
|
||||
|
||||
impl CommonTmInfo {
|
||||
pub fn new_from_tm(tm: &PusTmCreator) -> Self {
|
||||
let mut time_stamp = [0; 7];
|
||||
time_stamp.clone_from_slice(&tm.timestamp()[0..7]);
|
||||
Self {
|
||||
subservice: tm.subservice(),
|
||||
subservice: PusPacket::subservice(tm),
|
||||
apid: tm.apid(),
|
||||
msg_counter: tm.msg_counter(),
|
||||
dest_id: tm.dest_id(),
|
||||
@ -772,4 +999,196 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common fields for a PUS service test harness.
|
||||
pub struct PusServiceHandlerWithSharedStoreCommon {
|
||||
pus_buf: [u8; 2048],
|
||||
tm_buf: [u8; 2048],
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
tm_pool: SharedTmPool,
|
||||
tc_sender: mpsc::Sender<EcssTcAndToken>,
|
||||
tm_receiver: mpsc::Receiver<StoreAddr>,
|
||||
verification_handler: VerificationReporterWithSender,
|
||||
}
|
||||
|
||||
impl PusServiceHandlerWithSharedStoreCommon {
|
||||
/// This function generates the structure in addition to the PUS service handler
|
||||
/// [PusServiceHandler] which might be required for a specific PUS service handler.
|
||||
///
|
||||
/// The PUS service handler is instantiated with a [EcssTcInStoreConverter].
|
||||
pub fn new() -> (Self, PusServiceHelper<EcssTcInSharedStoreConverter>) {
|
||||
let pool_cfg = StaticPoolConfig::new(vec![(16, 16), (8, 32), (4, 64)]);
|
||||
let tc_pool = StaticMemoryPool::new(pool_cfg.clone());
|
||||
let tm_pool = StaticMemoryPool::new(pool_cfg);
|
||||
let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool));
|
||||
let shared_tm_pool = SharedTmPool::new(tm_pool);
|
||||
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
|
||||
let (tm_tx, tm_rx) = mpsc::channel();
|
||||
|
||||
let verif_sender = MpscTmInSharedPoolSender::new(
|
||||
0,
|
||||
"verif_sender",
|
||||
shared_tm_pool.clone(),
|
||||
tm_tx.clone(),
|
||||
);
|
||||
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
let verification_handler =
|
||||
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
|
||||
let test_srv_tm_sender =
|
||||
MpscTmInSharedPoolSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx);
|
||||
let test_srv_tc_receiver = MpscTcReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx);
|
||||
let in_store_converter =
|
||||
EcssTcInSharedStoreConverter::new(shared_tc_pool.clone(), 2048);
|
||||
(
|
||||
Self {
|
||||
pus_buf: [0; 2048],
|
||||
tm_buf: [0; 2048],
|
||||
tc_pool: shared_tc_pool,
|
||||
tm_pool: shared_tm_pool,
|
||||
tc_sender: test_srv_tc_tx,
|
||||
tm_receiver: tm_rx,
|
||||
verification_handler: verification_handler.clone(),
|
||||
},
|
||||
PusServiceHelper::new(
|
||||
Box::new(test_srv_tc_receiver),
|
||||
Box::new(test_srv_tm_sender),
|
||||
TEST_APID,
|
||||
verification_handler,
|
||||
in_store_converter,
|
||||
),
|
||||
)
|
||||
}
|
||||
pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
|
||||
let token = self.verification_handler.add_tc(tc);
|
||||
let token = self
|
||||
.verification_handler
|
||||
.acceptance_success(token, Some(&[0; 7]))
|
||||
.unwrap();
|
||||
let tc_size = tc.write_to_bytes(&mut self.pus_buf).unwrap();
|
||||
let mut tc_pool = self.tc_pool.write().unwrap();
|
||||
let addr = tc_pool.add(&self.pus_buf[..tc_size]).unwrap();
|
||||
drop(tc_pool);
|
||||
// Send accepted TC to test service handler.
|
||||
self.tc_sender
|
||||
.send(EcssTcAndToken::new(addr, token))
|
||||
.expect("sending tc failed");
|
||||
token
|
||||
}
|
||||
|
||||
pub fn read_next_tm(&mut self) -> PusTmReader<'_> {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
let tm_addr = next_msg.unwrap();
|
||||
let tm_pool = self.tm_pool.0.read().unwrap();
|
||||
let tm_raw = tm_pool.read_as_vec(&tm_addr).unwrap();
|
||||
self.tm_buf[0..tm_raw.len()].copy_from_slice(&tm_raw);
|
||||
PusTmReader::new(&self.tm_buf, 7).unwrap().0
|
||||
}
|
||||
|
||||
pub fn check_no_tm_available(&self) -> bool {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
if let TryRecvError::Empty = next_msg.unwrap_err() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
let tm_addr = next_msg.unwrap();
|
||||
let tm_pool = self.tm_pool.0.read().unwrap();
|
||||
let tm_raw = tm_pool.read_as_vec(&tm_addr).unwrap();
|
||||
let tm = PusTmReader::new(&tm_raw, 7).unwrap().0;
|
||||
assert_eq!(PusPacket::service(&tm), 1);
|
||||
assert_eq!(PusPacket::subservice(&tm), subservice);
|
||||
assert_eq!(tm.apid(), TEST_APID);
|
||||
let req_id =
|
||||
RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
|
||||
assert_eq!(req_id, expected_request_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PusServiceHandlerWithVecCommon {
|
||||
current_tm: Option<alloc::vec::Vec<u8>>,
|
||||
tc_sender: mpsc::Sender<EcssTcAndToken>,
|
||||
tm_receiver: mpsc::Receiver<alloc::vec::Vec<u8>>,
|
||||
verification_handler: VerificationReporterWithSender,
|
||||
}
|
||||
|
||||
impl PusServiceHandlerWithVecCommon {
|
||||
pub fn new() -> (Self, PusServiceHelper<EcssTcInVecConverter>) {
|
||||
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
|
||||
let (tm_tx, tm_rx) = mpsc::channel();
|
||||
|
||||
let verif_sender = MpscTmAsVecSender::new(0, "verififcatio-sender", tm_tx.clone());
|
||||
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
let verification_handler =
|
||||
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
|
||||
let test_srv_tm_sender = MpscTmAsVecSender::new(0, "test-sender", tm_tx);
|
||||
let test_srv_tc_receiver = MpscTcReceiver::new(0, "test-receiver", test_srv_tc_rx);
|
||||
let in_store_converter = EcssTcInVecConverter::default();
|
||||
(
|
||||
Self {
|
||||
current_tm: None,
|
||||
tc_sender: test_srv_tc_tx,
|
||||
tm_receiver: tm_rx,
|
||||
verification_handler: verification_handler.clone(),
|
||||
},
|
||||
PusServiceHelper::new(
|
||||
Box::new(test_srv_tc_receiver),
|
||||
Box::new(test_srv_tm_sender),
|
||||
TEST_APID,
|
||||
verification_handler,
|
||||
in_store_converter,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
|
||||
let token = self.verification_handler.add_tc(tc);
|
||||
let token = self
|
||||
.verification_handler
|
||||
.acceptance_success(token, Some(&[0; 7]))
|
||||
.unwrap();
|
||||
// Send accepted TC to test service handler.
|
||||
self.tc_sender
|
||||
.send(EcssTcAndToken::new(
|
||||
TcInMemory::Vec(tc.to_vec().expect("pus tc conversion to vec failed")),
|
||||
token,
|
||||
))
|
||||
.expect("sending tc failed");
|
||||
token
|
||||
}
|
||||
|
||||
pub fn read_next_tm(&mut self) -> PusTmReader<'_> {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
self.current_tm = Some(next_msg.unwrap());
|
||||
PusTmReader::new(self.current_tm.as_ref().unwrap(), 7)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn check_no_tm_available(&self) -> bool {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
if let TryRecvError::Empty = next_msg.unwrap_err() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) {
|
||||
let next_msg = self.tm_receiver.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
let next_msg = next_msg.unwrap();
|
||||
let tm = PusTmReader::new(next_msg.as_slice(), 7).unwrap().0;
|
||||
assert_eq!(PusPacket::service(&tm), 1);
|
||||
assert_eq!(PusPacket::subservice(&tm), subservice);
|
||||
assert_eq!(tm.apid(), TEST_APID);
|
||||
let req_id =
|
||||
RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
|
||||
assert_eq!(req_id, expected_request_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,141 +1,130 @@
|
||||
use crate::pool::{SharedPool, StoreAddr};
|
||||
use crate::pus::scheduler::PusScheduler;
|
||||
use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken};
|
||||
use crate::pus::{
|
||||
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
|
||||
PusServiceHandler,
|
||||
};
|
||||
use spacepackets::ecss::tc::PusTcReader;
|
||||
use super::scheduler::PusSchedulerInterface;
|
||||
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
|
||||
use crate::pool::PoolProvider;
|
||||
use crate::pus::{PusPacketHandlerResult, PusPacketHandlingError};
|
||||
use alloc::string::ToString;
|
||||
use spacepackets::ecss::{scheduling, PusPacket};
|
||||
use spacepackets::time::cds::TimeProvider;
|
||||
use std::boxed::Box;
|
||||
|
||||
/// This is a helper class for [std] environments to handle generic PUS 11 (scheduling service)
|
||||
/// packets. This handler is constrained to using the [PusScheduler], but is able to process
|
||||
/// the most important PUS requests for a scheduling service.
|
||||
/// packets. This handler is able to handle the most important PUS requests for a scheduling
|
||||
/// service which provides the [PusSchedulerInterface].
|
||||
///
|
||||
/// Please note that this class does not do the regular periodic handling like releasing any
|
||||
/// telecommands inside the scheduler. The user can retrieve the wrapped scheduler via the
|
||||
/// [Self::scheduler] and [Self::scheduler_mut] function and then use the scheduler API to release
|
||||
/// telecommands when applicable.
|
||||
pub struct PusService11SchedHandler {
|
||||
psb: PusServiceBase,
|
||||
scheduler: PusScheduler,
|
||||
pub struct PusService11SchedHandler<
|
||||
TcInMemConverter: EcssTcInMemConverter,
|
||||
Scheduler: PusSchedulerInterface,
|
||||
> {
|
||||
pub service_helper: PusServiceHelper<TcInMemConverter>,
|
||||
scheduler: Scheduler,
|
||||
}
|
||||
|
||||
impl PusService11SchedHandler {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_store: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
scheduler: PusScheduler,
|
||||
) -> Self {
|
||||
impl<TcInMemConverter: EcssTcInMemConverter, Scheduler: PusSchedulerInterface>
|
||||
PusService11SchedHandler<TcInMemConverter, Scheduler>
|
||||
{
|
||||
pub fn new(service_helper: PusServiceHelper<TcInMemConverter>, scheduler: Scheduler) -> Self {
|
||||
Self {
|
||||
psb: PusServiceBase::new(
|
||||
tc_receiver,
|
||||
shared_tc_store,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler,
|
||||
),
|
||||
service_helper,
|
||||
scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduler_mut(&mut self) -> &mut PusScheduler {
|
||||
pub fn scheduler_mut(&mut self) -> &mut Scheduler {
|
||||
&mut self.scheduler
|
||||
}
|
||||
|
||||
pub fn scheduler(&self) -> &PusScheduler {
|
||||
pub fn scheduler(&self) -> &Scheduler {
|
||||
&self.scheduler
|
||||
}
|
||||
}
|
||||
|
||||
impl PusServiceHandler for PusService11SchedHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase {
|
||||
&mut self.psb
|
||||
}
|
||||
fn psb(&self) -> &PusServiceBase {
|
||||
&self.psb
|
||||
}
|
||||
|
||||
fn handle_one_tc(
|
||||
pub fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
sched_tc_pool: &mut (impl PoolProvider + ?Sized),
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
self.copy_tc_to_buf(addr)?;
|
||||
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
|
||||
let subservice = tc.subservice();
|
||||
let std_service = scheduling::Subservice::try_from(subservice);
|
||||
if std_service.is_err() {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
let tc = self
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
|
||||
let subservice = PusPacket::subservice(&tc);
|
||||
let standard_subservice = scheduling::Subservice::try_from(subservice);
|
||||
if standard_subservice.is_err() {
|
||||
return Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
token,
|
||||
subservice,
|
||||
ecss_tc_and_token.token,
|
||||
));
|
||||
}
|
||||
let mut partial_error = None;
|
||||
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
|
||||
match std_service.unwrap() {
|
||||
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
|
||||
match standard_subservice.unwrap() {
|
||||
scheduling::Subservice::TcEnableScheduling => {
|
||||
let start_token = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&time_stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
|
||||
.expect("Error sending start success");
|
||||
|
||||
self.scheduler.enable();
|
||||
if self.scheduler.is_enabled() {
|
||||
self.psb
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&time_stamp))
|
||||
.expect("Error sending completion success");
|
||||
} else {
|
||||
panic!("Failed to enable scheduler");
|
||||
return Err(PusPacketHandlingError::Other(
|
||||
"failed to enabled scheduler".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
scheduling::Subservice::TcDisableScheduling => {
|
||||
let start_token = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&time_stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
|
||||
.expect("Error sending start success");
|
||||
|
||||
self.scheduler.disable();
|
||||
if !self.scheduler.is_enabled() {
|
||||
self.psb
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&time_stamp))
|
||||
.expect("Error sending completion success");
|
||||
} else {
|
||||
panic!("Failed to disable scheduler");
|
||||
return Err(PusPacketHandlingError::Other(
|
||||
"failed to disable scheduler".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
scheduling::Subservice::TcResetScheduling => {
|
||||
let start_token = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&time_stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
|
||||
.expect("Error sending start success");
|
||||
|
||||
let mut pool = self
|
||||
.psb
|
||||
.shared_tc_store
|
||||
.write()
|
||||
.expect("Locking pool failed");
|
||||
|
||||
self.scheduler
|
||||
.reset(pool.as_mut())
|
||||
.reset(sched_tc_pool)
|
||||
.expect("Error resetting TC Pool");
|
||||
|
||||
self.psb
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&time_stamp))
|
||||
@ -143,31 +132,30 @@ impl PusServiceHandler for PusService11SchedHandler {
|
||||
}
|
||||
scheduling::Subservice::TcInsertActivity => {
|
||||
let start_token = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&time_stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
|
||||
.expect("error sending start success");
|
||||
|
||||
let mut pool = self
|
||||
.psb
|
||||
.shared_tc_store
|
||||
.write()
|
||||
.expect("locking pool failed");
|
||||
// let mut pool = self.sched_tc_pool.write().expect("locking pool failed");
|
||||
self.scheduler
|
||||
.insert_wrapped_tc::<TimeProvider>(&tc, pool.as_mut())
|
||||
.insert_wrapped_tc::<TimeProvider>(&tc, sched_tc_pool)
|
||||
.expect("insertion of activity into pool failed");
|
||||
|
||||
self.psb
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&time_stamp))
|
||||
.expect("sending completion success failed");
|
||||
}
|
||||
_ => {
|
||||
// Treat unhandled standard subservices as custom subservices for now.
|
||||
return Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
token,
|
||||
subservice,
|
||||
ecss_tc_and_token.token,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -176,9 +164,192 @@ impl PusServiceHandler for PusService11SchedHandler {
|
||||
partial_error,
|
||||
));
|
||||
}
|
||||
Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
token,
|
||||
))
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pool::{StaticMemoryPool, StaticPoolConfig};
|
||||
use crate::pus::tests::TEST_APID;
|
||||
use crate::pus::{
|
||||
scheduler::{self, PusSchedulerInterface, TcInfo},
|
||||
tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness},
|
||||
verification::{RequestId, TcStateAccepted, VerificationToken},
|
||||
EcssTcInSharedStoreConverter,
|
||||
};
|
||||
use alloc::collections::VecDeque;
|
||||
use delegate::delegate;
|
||||
use spacepackets::ecss::scheduling::Subservice;
|
||||
use spacepackets::ecss::tc::PusTcSecondaryHeader;
|
||||
use spacepackets::ecss::WritablePusPacket;
|
||||
use spacepackets::time::TimeWriter;
|
||||
use spacepackets::SpHeader;
|
||||
use spacepackets::{
|
||||
ecss::{tc::PusTcCreator, tm::PusTmReader},
|
||||
time::cds,
|
||||
};
|
||||
|
||||
use super::PusService11SchedHandler;
|
||||
|
||||
struct Pus11HandlerWithStoreTester {
|
||||
common: PusServiceHandlerWithSharedStoreCommon,
|
||||
handler: PusService11SchedHandler<EcssTcInSharedStoreConverter, TestScheduler>,
|
||||
sched_tc_pool: StaticMemoryPool,
|
||||
}
|
||||
|
||||
impl Pus11HandlerWithStoreTester {
|
||||
pub fn new() -> Self {
|
||||
let test_scheduler = TestScheduler::default();
|
||||
let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)]);
|
||||
let sched_tc_pool = StaticMemoryPool::new(pool_cfg.clone());
|
||||
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
|
||||
Self {
|
||||
common,
|
||||
handler: PusService11SchedHandler::new(srv_handler, test_scheduler),
|
||||
sched_tc_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusTestHarness for Pus11HandlerWithStoreTester {
|
||||
delegate! {
|
||||
to self.common {
|
||||
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
|
||||
fn read_next_tm(&mut self) -> PusTmReader<'_>;
|
||||
fn check_no_tm_available(&self) -> bool;
|
||||
fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TestScheduler {
|
||||
reset_count: u32,
|
||||
enabled: bool,
|
||||
enabled_count: u32,
|
||||
disabled_count: u32,
|
||||
inserted_tcs: VecDeque<TcInfo>,
|
||||
}
|
||||
|
||||
impl PusSchedulerInterface for TestScheduler {
|
||||
type TimeProvider = cds::TimeProvider;
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
_store: &mut (impl crate::pool::PoolProvider + ?Sized),
|
||||
) -> Result<(), crate::pool::StoreError> {
|
||||
self.reset_count += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn enable(&mut self) {
|
||||
self.enabled_count += 1;
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
fn disable(&mut self) {
|
||||
self.disabled_count += 1;
|
||||
self.enabled = false;
|
||||
}
|
||||
|
||||
fn insert_unwrapped_and_stored_tc(
|
||||
&mut self,
|
||||
_time_stamp: spacepackets::time::UnixTimestamp,
|
||||
info: crate::pus::scheduler::TcInfo,
|
||||
) -> Result<(), crate::pus::scheduler::ScheduleError> {
|
||||
self.inserted_tcs.push_back(info);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generic_subservice_send(
|
||||
test_harness: &mut Pus11HandlerWithStoreTester,
|
||||
subservice: Subservice,
|
||||
) {
|
||||
let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap();
|
||||
let tc_header = PusTcSecondaryHeader::new_simple(11, subservice as u8);
|
||||
let enable_scheduling = PusTcCreator::new(&mut reply_header, tc_header, &[0; 7], true);
|
||||
let token = test_harness.send_tc(&enable_scheduling);
|
||||
|
||||
let request_id = token.req_id();
|
||||
test_harness
|
||||
.handler
|
||||
.handle_one_tc(&mut test_harness.sched_tc_pool)
|
||||
.unwrap();
|
||||
test_harness.check_next_verification_tm(1, request_id);
|
||||
test_harness.check_next_verification_tm(3, request_id);
|
||||
test_harness.check_next_verification_tm(7, request_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scheduling_enabling_tc() {
|
||||
let mut test_harness = Pus11HandlerWithStoreTester::new();
|
||||
test_harness.handler.scheduler_mut().disable();
|
||||
assert!(!test_harness.handler.scheduler().is_enabled());
|
||||
generic_subservice_send(&mut test_harness, Subservice::TcEnableScheduling);
|
||||
assert!(test_harness.handler.scheduler().is_enabled());
|
||||
assert_eq!(test_harness.handler.scheduler().enabled_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scheduling_disabling_tc() {
|
||||
let mut test_harness = Pus11HandlerWithStoreTester::new();
|
||||
test_harness.handler.scheduler_mut().enable();
|
||||
assert!(test_harness.handler.scheduler().is_enabled());
|
||||
generic_subservice_send(&mut test_harness, Subservice::TcDisableScheduling);
|
||||
assert!(!test_harness.handler.scheduler().is_enabled());
|
||||
assert_eq!(test_harness.handler.scheduler().disabled_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_scheduler_tc() {
|
||||
let mut test_harness = Pus11HandlerWithStoreTester::new();
|
||||
generic_subservice_send(&mut test_harness, Subservice::TcResetScheduling);
|
||||
assert_eq!(test_harness.handler.scheduler().reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_activity_tc() {
|
||||
let mut test_harness = Pus11HandlerWithStoreTester::new();
|
||||
let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap();
|
||||
let mut sec_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
let ping_tc = PusTcCreator::new(&mut reply_header, sec_header, &[], true);
|
||||
let req_id_ping_tc = scheduler::RequestId::from_tc(&ping_tc);
|
||||
let stamper = cds::TimeProvider::from_now_with_u16_days().expect("time provider failed");
|
||||
let mut sched_app_data: [u8; 64] = [0; 64];
|
||||
let mut written_len = stamper.write_to_bytes(&mut sched_app_data).unwrap();
|
||||
let ping_raw = ping_tc.to_vec().expect("generating raw tc failed");
|
||||
sched_app_data[written_len..written_len + ping_raw.len()].copy_from_slice(&ping_raw);
|
||||
written_len += ping_raw.len();
|
||||
reply_header = SpHeader::tm_unseg(TEST_APID, 1, 0).unwrap();
|
||||
sec_header = PusTcSecondaryHeader::new_simple(11, Subservice::TcInsertActivity as u8);
|
||||
let enable_scheduling = PusTcCreator::new(
|
||||
&mut reply_header,
|
||||
sec_header,
|
||||
&sched_app_data[..written_len],
|
||||
true,
|
||||
);
|
||||
let token = test_harness.send_tc(&enable_scheduling);
|
||||
|
||||
let request_id = token.req_id();
|
||||
test_harness
|
||||
.handler
|
||||
.handle_one_tc(&mut test_harness.sched_tc_pool)
|
||||
.unwrap();
|
||||
test_harness.check_next_verification_tm(1, request_id);
|
||||
test_harness.check_next_verification_tm(3, request_id);
|
||||
test_harness.check_next_verification_tm(7, request_id);
|
||||
let tc_info = test_harness
|
||||
.handler
|
||||
.scheduler_mut()
|
||||
.inserted_tcs
|
||||
.pop_front()
|
||||
.unwrap();
|
||||
assert_eq!(tc_info.request_id(), req_id_ping_tc);
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,45 @@
|
||||
use crate::pool::{SharedPool, StoreAddr};
|
||||
use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken};
|
||||
use crate::pus::{
|
||||
EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult,
|
||||
PusPacketHandlingError, PusServiceBase, PusServiceHandler, PusTmWrapper,
|
||||
PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError, PusTmWrapper,
|
||||
};
|
||||
use spacepackets::ecss::tc::PusTcReader;
|
||||
use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
|
||||
use spacepackets::ecss::PusPacket;
|
||||
use spacepackets::SpHeader;
|
||||
use std::boxed::Box;
|
||||
|
||||
use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper};
|
||||
|
||||
/// This is a helper class for [std] environments to handle generic PUS 17 (test service) packets.
|
||||
/// This handler only processes ping requests and generates a ping reply for them accordingly.
|
||||
pub struct PusService17TestHandler {
|
||||
psb: PusServiceBase,
|
||||
pub struct PusService17TestHandler<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub service_helper: PusServiceHelper<TcInMemConverter>,
|
||||
}
|
||||
|
||||
impl PusService17TestHandler {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_store: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
) -> Self {
|
||||
Self {
|
||||
psb: PusServiceBase::new(
|
||||
tc_receiver,
|
||||
shared_tc_store,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler,
|
||||
),
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusService17TestHandler<TcInMemConverter> {
|
||||
pub fn new(service_helper: PusServiceHelper<TcInMemConverter>) -> Self {
|
||||
Self { service_helper }
|
||||
}
|
||||
|
||||
pub fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusServiceHandler for PusService17TestHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase {
|
||||
&mut self.psb
|
||||
}
|
||||
fn psb(&self) -> &PusServiceBase {
|
||||
&self.psb
|
||||
}
|
||||
|
||||
fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
self.copy_tc_to_buf(addr)?;
|
||||
let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?;
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
let tc = self
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
|
||||
if tc.service() != 17 {
|
||||
return Err(PusPacketHandlingError::WrongService(tc.service()));
|
||||
}
|
||||
if tc.subservice() == 1 {
|
||||
let mut partial_error = None;
|
||||
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
|
||||
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
|
||||
let result = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&time_stamp))
|
||||
.start_success(ecss_tc_and_token.token, Some(&time_stamp))
|
||||
.map_err(|_| PartialPusHandlingError::Verification);
|
||||
let start_token = if let Ok(result) = result {
|
||||
Some(result)
|
||||
@ -70,11 +48,13 @@ impl PusServiceHandler for PusService17TestHandler {
|
||||
None
|
||||
};
|
||||
// Sequence count will be handled centrally in TM funnel.
|
||||
let mut reply_header = SpHeader::tm_unseg(self.psb.tm_apid, 0, 0).unwrap();
|
||||
let mut reply_header =
|
||||
SpHeader::tm_unseg(self.service_helper.common.tm_apid, 0, 0).unwrap();
|
||||
let tc_header = PusTmSecondaryHeader::new_simple(17, 2, &time_stamp);
|
||||
let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, None, true);
|
||||
let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, &[], true);
|
||||
let result = self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.tm_sender
|
||||
.send_tm(PusTmWrapper::Direct(ping_reply))
|
||||
.map_err(PartialPusHandlingError::TmSend);
|
||||
@ -84,7 +64,8 @@ impl PusServiceHandler for PusService17TestHandler {
|
||||
|
||||
if let Some(start_token) = start_token {
|
||||
if self
|
||||
.psb
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&time_stamp))
|
||||
@ -98,121 +79,194 @@ impl PusServiceHandler for PusService17TestHandler {
|
||||
partial_error,
|
||||
));
|
||||
};
|
||||
return Ok(PusPacketHandlerResult::RequestHandled);
|
||||
} else {
|
||||
return Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
ecss_tc_and_token.token,
|
||||
));
|
||||
}
|
||||
Ok(PusPacketHandlerResult::CustomSubservice(
|
||||
tc.subservice(),
|
||||
token,
|
||||
))
|
||||
Ok(PusPacketHandlerResult::RequestHandled)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pool::{LocalPool, PoolCfg, SharedPool};
|
||||
use crate::pus::test::PusService17TestHandler;
|
||||
use crate::pus::verification::{
|
||||
RequestId, StdVerifReporterWithSender, VerificationReporterCfg,
|
||||
use crate::pus::tests::{
|
||||
PusServiceHandlerWithSharedStoreCommon, PusServiceHandlerWithVecCommon, PusTestHarness,
|
||||
SimplePusPacketHandler, TEST_APID,
|
||||
};
|
||||
use crate::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender, PusServiceHandler};
|
||||
use crate::tmtc::tm_helper::SharedTmStore;
|
||||
use crate::pus::verification::RequestId;
|
||||
use crate::pus::verification::{TcStateAccepted, VerificationToken};
|
||||
use crate::pus::{
|
||||
EcssTcInSharedStoreConverter, EcssTcInVecConverter, PusPacketHandlerResult,
|
||||
PusPacketHandlingError,
|
||||
};
|
||||
use delegate::delegate;
|
||||
use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader};
|
||||
use spacepackets::ecss::tm::PusTmReader;
|
||||
use spacepackets::ecss::{PusPacket, SerializablePusPacket};
|
||||
use spacepackets::ecss::PusPacket;
|
||||
use spacepackets::{SequenceFlags, SpHeader};
|
||||
use std::boxed::Box;
|
||||
use std::sync::{mpsc, RwLock};
|
||||
use std::vec;
|
||||
|
||||
const TEST_APID: u16 = 0x101;
|
||||
use super::PusService17TestHandler;
|
||||
|
||||
#[test]
|
||||
fn test_basic_ping_processing() {
|
||||
let mut pus_buf: [u8; 64] = [0; 64];
|
||||
let pool_cfg = PoolCfg::new(vec![(16, 16), (8, 32), (4, 64)]);
|
||||
let tc_pool = LocalPool::new(pool_cfg.clone());
|
||||
let tm_pool = LocalPool::new(pool_cfg);
|
||||
let tc_pool_shared = SharedPool::new(RwLock::new(Box::new(tc_pool)));
|
||||
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
|
||||
let tm_pool_shared = shared_tm_store.clone_backing_pool();
|
||||
let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel();
|
||||
let (tm_tx, tm_rx) = mpsc::channel();
|
||||
let verif_sender =
|
||||
MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store.clone(), tm_tx.clone());
|
||||
let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
let mut verification_handler =
|
||||
StdVerifReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
|
||||
let test_srv_tm_sender = MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_store, tm_tx);
|
||||
let test_srv_tc_receiver = MpscTcInStoreReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx);
|
||||
let mut pus_17_handler = PusService17TestHandler::new(
|
||||
Box::new(test_srv_tc_receiver),
|
||||
tc_pool_shared.clone(),
|
||||
Box::new(test_srv_tm_sender),
|
||||
TEST_APID,
|
||||
verification_handler.clone(),
|
||||
);
|
||||
struct Pus17HandlerWithStoreTester {
|
||||
common: PusServiceHandlerWithSharedStoreCommon,
|
||||
handler: PusService17TestHandler<EcssTcInSharedStoreConverter>,
|
||||
}
|
||||
|
||||
impl Pus17HandlerWithStoreTester {
|
||||
pub fn new() -> Self {
|
||||
let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new();
|
||||
let pus_17_handler = PusService17TestHandler::new(srv_handler);
|
||||
Self {
|
||||
common,
|
||||
handler: pus_17_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusTestHarness for Pus17HandlerWithStoreTester {
|
||||
delegate! {
|
||||
to self.common {
|
||||
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
|
||||
fn read_next_tm(&mut self) -> PusTmReader<'_>;
|
||||
fn check_no_tm_available(&self) -> bool;
|
||||
fn check_next_verification_tm(
|
||||
&self,
|
||||
subservice: u8,
|
||||
expected_request_id: RequestId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SimplePusPacketHandler for Pus17HandlerWithStoreTester {
|
||||
delegate! {
|
||||
to self.handler {
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Pus17HandlerWithVecTester {
|
||||
common: PusServiceHandlerWithVecCommon,
|
||||
handler: PusService17TestHandler<EcssTcInVecConverter>,
|
||||
}
|
||||
|
||||
impl Pus17HandlerWithVecTester {
|
||||
pub fn new() -> Self {
|
||||
let (common, srv_handler) = PusServiceHandlerWithVecCommon::new();
|
||||
Self {
|
||||
common,
|
||||
handler: PusService17TestHandler::new(srv_handler),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusTestHarness for Pus17HandlerWithVecTester {
|
||||
delegate! {
|
||||
to self.common {
|
||||
fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted>;
|
||||
fn read_next_tm(&mut self) -> PusTmReader<'_>;
|
||||
fn check_no_tm_available(&self) -> bool;
|
||||
fn check_next_verification_tm(
|
||||
&self,
|
||||
subservice: u8,
|
||||
expected_request_id: RequestId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SimplePusPacketHandler for Pus17HandlerWithVecTester {
|
||||
delegate! {
|
||||
to self.handler {
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ping_test(test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler)) {
|
||||
// Create a ping TC, verify acceptance.
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, None, true);
|
||||
let token = verification_handler.add_tc(&ping_tc);
|
||||
let token = verification_handler
|
||||
.acceptance_success(token, None)
|
||||
.unwrap();
|
||||
let tc_size = ping_tc.write_to_bytes(&mut pus_buf).unwrap();
|
||||
let mut tc_pool = tc_pool_shared.write().unwrap();
|
||||
let addr = tc_pool.add(&pus_buf[..tc_size]).unwrap();
|
||||
drop(tc_pool);
|
||||
// Send accepted TC to test service handler.
|
||||
test_srv_tc_tx.send((addr, token.into())).unwrap();
|
||||
let result = pus_17_handler.handle_next_packet();
|
||||
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
|
||||
let token = test_harness.send_tc(&ping_tc);
|
||||
let request_id = token.req_id();
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_ok());
|
||||
// We should see 4 replies in the TM queue now: Acceptance TM, Start TM, ping reply and
|
||||
// Completion TM
|
||||
let mut next_msg = tm_rx.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
let mut tm_addr = next_msg.unwrap();
|
||||
let tm_pool = tm_pool_shared.read().unwrap();
|
||||
let tm_raw = tm_pool.read(&tm_addr).unwrap();
|
||||
let (tm, _) = PusTmReader::new(&tm_raw, 0).unwrap();
|
||||
assert_eq!(tm.service(), 1);
|
||||
assert_eq!(tm.subservice(), 1);
|
||||
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
|
||||
assert_eq!(req_id, token.req_id());
|
||||
|
||||
// Acceptance TM
|
||||
next_msg = tm_rx.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
tm_addr = next_msg.unwrap();
|
||||
let tm_raw = tm_pool.read(&tm_addr).unwrap();
|
||||
// Is generated with CDS short timestamp.
|
||||
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap();
|
||||
assert_eq!(tm.service(), 1);
|
||||
assert_eq!(tm.subservice(), 3);
|
||||
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
|
||||
assert_eq!(req_id, token.req_id());
|
||||
test_harness.check_next_verification_tm(1, request_id);
|
||||
|
||||
// Start TM
|
||||
test_harness.check_next_verification_tm(3, request_id);
|
||||
|
||||
// Ping reply
|
||||
next_msg = tm_rx.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
tm_addr = next_msg.unwrap();
|
||||
let tm_raw = tm_pool.read(&tm_addr).unwrap();
|
||||
// Is generated with CDS short timestamp.
|
||||
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap();
|
||||
let tm = test_harness.read_next_tm();
|
||||
assert_eq!(tm.service(), 17);
|
||||
assert_eq!(tm.subservice(), 2);
|
||||
assert!(tm.user_data().is_empty());
|
||||
|
||||
// TM completion
|
||||
next_msg = tm_rx.try_recv();
|
||||
assert!(next_msg.is_ok());
|
||||
tm_addr = next_msg.unwrap();
|
||||
let tm_raw = tm_pool.read(&tm_addr).unwrap();
|
||||
// Is generated with CDS short timestamp.
|
||||
let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap();
|
||||
assert_eq!(tm.service(), 1);
|
||||
assert_eq!(tm.subservice(), 7);
|
||||
let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed");
|
||||
assert_eq!(req_id, token.req_id());
|
||||
test_harness.check_next_verification_tm(7, request_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_ping_processing_using_store() {
|
||||
let mut test_harness = Pus17HandlerWithStoreTester::new();
|
||||
ping_test(&mut test_harness);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_ping_processing_using_vec() {
|
||||
let mut test_harness = Pus17HandlerWithVecTester::new();
|
||||
ping_test(&mut test_harness);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_tc_queue() {
|
||||
let mut test_harness = Pus17HandlerWithStoreTester::new();
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
if let PusPacketHandlerResult::Empty = result {
|
||||
} else {
|
||||
panic!("unexpected result type {result:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sending_unsupported_service() {
|
||||
let mut test_harness = Pus17HandlerWithStoreTester::new();
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(3, 1);
|
||||
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
|
||||
test_harness.send_tc(&ping_tc);
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
if let PusPacketHandlingError::WrongService(num) = error {
|
||||
assert_eq!(num, 3);
|
||||
} else {
|
||||
panic!("unexpected error type {error}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sending_custom_subservice() {
|
||||
let mut test_harness = Pus17HandlerWithStoreTester::new();
|
||||
let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(17, 200);
|
||||
let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true);
|
||||
test_harness.send_tc(&ping_tc);
|
||||
let result = test_harness.handle_one_tc();
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result {
|
||||
assert_eq!(subservice, 200);
|
||||
} else {
|
||||
panic!("unexpected result type {result:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,11 @@
|
||||
//! ```
|
||||
//! use std::sync::{Arc, mpsc, RwLock};
|
||||
//! use std::time::Duration;
|
||||
//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider, SharedPool};
|
||||
//! use satrs_core::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
|
||||
//! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
|
||||
//! use satrs_core::seq_count::SeqCountProviderSimple;
|
||||
//! use satrs_core::pus::MpscTmInStoreSender;
|
||||
//! use satrs_core::tmtc::tm_helper::SharedTmStore;
|
||||
//! use satrs_core::pus::MpscTmInSharedPoolSender;
|
||||
//! use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
//! use spacepackets::ecss::PusPacket;
|
||||
//! use spacepackets::SpHeader;
|
||||
//! use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader};
|
||||
@ -28,18 +28,18 @@
|
||||
//! const EMPTY_STAMP: [u8; 7] = [0; 7];
|
||||
//! const TEST_APID: u16 = 0x02;
|
||||
//!
|
||||
//! let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
//! let tm_pool = LocalPool::new(pool_cfg.clone());
|
||||
//! let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
|
||||
//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
//! let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
|
||||
//! let shared_tm_store = SharedTmPool::new(tm_pool);
|
||||
//! let tm_store = shared_tm_store.clone_backing_pool();
|
||||
//! let (verif_tx, verif_rx) = mpsc::channel();
|
||||
//! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx);
|
||||
//! let sender = MpscTmInSharedPoolSender::new(0, "Test Sender", shared_tm_store, verif_tx);
|
||||
//! let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
//! let mut reporter = VerificationReporterWithSender::new(&cfg , Box::new(sender));
|
||||
//!
|
||||
//! let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
|
||||
//! let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
//! let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true);
|
||||
//! let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
|
||||
//! let init_token = reporter.add_tc(&pus_tc_0);
|
||||
//!
|
||||
//! // Complete success sequence for a telecommand
|
||||
@ -56,9 +56,7 @@
|
||||
//! {
|
||||
//! let mut rg = tm_store.write().expect("Error locking shared pool");
|
||||
//! let store_guard = rg.read_with_guard(addr);
|
||||
//! let slice = store_guard.read().expect("Error reading TM slice");
|
||||
//! tm_len = slice.len();
|
||||
//! tm_buf[0..tm_len].copy_from_slice(slice);
|
||||
//! tm_len = store_guard.read(&mut tm_buf).expect("Error reading TM slice");
|
||||
//! }
|
||||
//! let (pus_tm, _) = PusTmReader::new(&tm_buf[0..tm_len], 7)
|
||||
//! .expect("Error reading verification TM");
|
||||
@ -87,7 +85,7 @@ use delegate::delegate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spacepackets::ecss::tc::IsPusTelecommand;
|
||||
use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
|
||||
use spacepackets::ecss::{EcssEnumeration, PusError, SerializablePusPacket};
|
||||
use spacepackets::ecss::{EcssEnumeration, PusError, WritablePusPacket};
|
||||
use spacepackets::{CcsdsPacket, PacketId, PacketSequenceCtrl};
|
||||
use spacepackets::{SpHeader, MAX_APID};
|
||||
|
||||
@ -208,6 +206,8 @@ impl WasAtLeastAccepted for TcStateAccepted {}
|
||||
impl WasAtLeastAccepted for TcStateStarted {}
|
||||
impl WasAtLeastAccepted for TcStateCompleted {}
|
||||
|
||||
/// Token wrapper to model all possible verification tokens. These tokens are used to
|
||||
/// enforce the correct order for the verification steps when doing verification reporting.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum TcStateToken {
|
||||
None(VerificationToken<TcStateNone>),
|
||||
@ -353,7 +353,7 @@ impl<'src_data, State, SuccessOrFailure> VerificationSendable<'src_data, State,
|
||||
}
|
||||
|
||||
pub fn len_packed(&self) -> usize {
|
||||
self.pus_tm.as_ref().unwrap().len_packed()
|
||||
self.pus_tm.as_ref().unwrap().len_written()
|
||||
}
|
||||
|
||||
pub fn pus_tm(&self) -> &PusTmCreator<'src_data> {
|
||||
@ -877,7 +877,7 @@ impl VerificationReporterCore {
|
||||
PusTmCreator::new(
|
||||
sp_header,
|
||||
tm_sec_header,
|
||||
Some(&src_data_buf[0..source_data_len]),
|
||||
&src_data_buf[0..source_data_len],
|
||||
true,
|
||||
)
|
||||
}
|
||||
@ -1323,15 +1323,15 @@ mod std_mod {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pool::{LocalPool, PoolCfg};
|
||||
use crate::pool::{PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig};
|
||||
use crate::pus::tests::CommonTmInfo;
|
||||
use crate::pus::verification::{
|
||||
EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone,
|
||||
VerificationReporter, VerificationReporterCfg, VerificationReporterWithSender,
|
||||
VerificationToken,
|
||||
};
|
||||
use crate::pus::{EcssChannel, MpscTmInStoreSender, PusTmWrapper};
|
||||
use crate::tmtc::tm_helper::SharedTmStore;
|
||||
use crate::pus::{EcssChannel, MpscTmInSharedPoolSender, PusTmWrapper};
|
||||
use crate::tmtc::tm_helper::SharedTmPool;
|
||||
use crate::ChannelId;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
@ -1438,6 +1438,7 @@ mod tests {
|
||||
fn base_tc_init(app_data: Option<&[u8]>) -> (PusTcCreator, RequestId) {
|
||||
let mut sph = SpHeader::tc_unseg(TEST_APID, 0x34, 0).unwrap();
|
||||
let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
let app_data = app_data.unwrap_or(&[]);
|
||||
let pus_tc = PusTcCreator::new(&mut sph, tc_header, app_data, true);
|
||||
let req_id = RequestId::new(&pus_tc);
|
||||
(pus_tc, req_id)
|
||||
@ -1446,12 +1447,11 @@ mod tests {
|
||||
fn base_init(api_sel: bool) -> (TestBase<'static>, VerificationToken<TcStateNone>) {
|
||||
let mut reporter = base_reporter();
|
||||
let (tc, req_id) = base_tc_init(None);
|
||||
let init_tok;
|
||||
if api_sel {
|
||||
init_tok = reporter.add_tc_with_req_id(req_id);
|
||||
let init_tok = if api_sel {
|
||||
reporter.add_tc_with_req_id(req_id)
|
||||
} else {
|
||||
init_tok = reporter.add_tc(&tc);
|
||||
}
|
||||
reporter.add_tc(&tc)
|
||||
};
|
||||
(TestBase { vr: reporter, tc }, init_tok)
|
||||
}
|
||||
|
||||
@ -1474,7 +1474,7 @@ mod tests {
|
||||
time_stamp: EMPTY_STAMP,
|
||||
},
|
||||
additional_data: None,
|
||||
req_id: req_id.clone(),
|
||||
req_id: *req_id,
|
||||
};
|
||||
let mut service_queue = sender.service_queue.borrow_mut();
|
||||
assert_eq!(service_queue.len(), 1);
|
||||
@ -1484,11 +1484,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_mpsc_verif_send_sync() {
|
||||
let pool = LocalPool::new(PoolCfg::new(vec![(8, 8)]));
|
||||
let tm_store = Box::new(pool);
|
||||
let shared_tm_store = SharedTmStore::new(tm_store);
|
||||
let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)]));
|
||||
let shared_tm_store = SharedTmPool::new(pool);
|
||||
let (tx, _) = mpsc::channel();
|
||||
let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx);
|
||||
let mpsc_verif_sender =
|
||||
MpscTmInSharedPoolSender::new(0, "verif_sender", shared_tm_store, tx);
|
||||
is_send(&mpsc_verif_sender);
|
||||
}
|
||||
|
||||
@ -1504,7 +1504,7 @@ mod tests {
|
||||
fn test_basic_acceptance_success() {
|
||||
let (b, tok) = base_init(false);
|
||||
let mut sender = TestSender::default();
|
||||
b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
|
||||
b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
|
||||
.expect("Sending acceptance success failed");
|
||||
acceptance_check(&mut sender, &tok.req_id);
|
||||
}
|
||||
@ -1545,7 +1545,7 @@ mod tests {
|
||||
let mut sender = TestSender::default();
|
||||
let fail_code = EcssEnumU16::new(2);
|
||||
let fail_params = FailParams::new(Some(stamp_buf.as_slice()), &fail_code, None);
|
||||
b.vr.acceptance_failure(tok, &mut sender, fail_params)
|
||||
b.vr.acceptance_failure(tok, &sender, fail_params)
|
||||
.expect("Sending acceptance success failed");
|
||||
acceptance_fail_check(&mut sender, tok.req_id, stamp_buf);
|
||||
}
|
||||
@ -1604,7 +1604,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_basic_acceptance_failure_with_fail_data() {
|
||||
let (b, tok) = base_init(false);
|
||||
let mut sender = TestSender::default();
|
||||
let sender = TestSender::default();
|
||||
let fail_code = EcssEnumU8::new(10);
|
||||
let fail_data = EcssEnumU32::new(12);
|
||||
let mut fail_data_raw = [0; 4];
|
||||
@ -1614,7 +1614,7 @@ mod tests {
|
||||
&fail_code,
|
||||
Some(fail_data_raw.as_slice()),
|
||||
);
|
||||
b.vr.acceptance_failure(tok, &mut sender, fail_params)
|
||||
b.vr.acceptance_failure(tok, &sender, fail_params)
|
||||
.expect("Sending acceptance success failed");
|
||||
let cmp_info = TmInfo {
|
||||
common: CommonTmInfo {
|
||||
@ -1680,12 +1680,10 @@ mod tests {
|
||||
);
|
||||
|
||||
let accepted_token =
|
||||
b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
|
||||
b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
|
||||
.expect("Sending acceptance success failed");
|
||||
let empty =
|
||||
b.vr.start_failure(accepted_token, &mut sender, fail_params)
|
||||
.expect("Start failure failure");
|
||||
assert_eq!(empty, ());
|
||||
b.vr.start_failure(accepted_token, &mut sender, fail_params)
|
||||
.expect("Start failure failure");
|
||||
start_fail_check(&mut sender, tok.req_id, fail_data_raw);
|
||||
}
|
||||
|
||||
@ -1777,31 +1775,27 @@ mod tests {
|
||||
let mut sender = TestSender::default();
|
||||
let accepted_token = b
|
||||
.rep()
|
||||
.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP))
|
||||
.acceptance_success(tok, &sender, Some(&EMPTY_STAMP))
|
||||
.expect("Sending acceptance success failed");
|
||||
let started_token = b
|
||||
.rep()
|
||||
.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
|
||||
.start_success(accepted_token, &sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
|
||||
.expect("Sending start success failed");
|
||||
let mut empty = b
|
||||
.rep()
|
||||
b.rep()
|
||||
.step_success(
|
||||
&started_token,
|
||||
&mut sender,
|
||||
&sender,
|
||||
Some(&EMPTY_STAMP),
|
||||
EcssEnumU8::new(0),
|
||||
)
|
||||
.expect("Sending step 0 success failed");
|
||||
assert_eq!(empty, ());
|
||||
empty =
|
||||
b.vr.step_success(
|
||||
&started_token,
|
||||
&mut sender,
|
||||
Some(&EMPTY_STAMP),
|
||||
EcssEnumU8::new(1),
|
||||
)
|
||||
.expect("Sending step 1 success failed");
|
||||
assert_eq!(empty, ());
|
||||
b.vr.step_success(
|
||||
&started_token,
|
||||
&sender,
|
||||
Some(&EMPTY_STAMP),
|
||||
EcssEnumU8::new(1),
|
||||
)
|
||||
.expect("Sending step 1 success failed");
|
||||
assert_eq!(sender.service_queue.borrow().len(), 4);
|
||||
step_success_check(&mut sender, tok.req_id);
|
||||
}
|
||||
@ -1817,16 +1811,12 @@ mod tests {
|
||||
.helper
|
||||
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
|
||||
.expect("Sending start success failed");
|
||||
let mut empty = b
|
||||
.helper
|
||||
b.helper
|
||||
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(0))
|
||||
.expect("Sending step 0 success failed");
|
||||
assert_eq!(empty, ());
|
||||
empty = b
|
||||
.helper
|
||||
b.helper
|
||||
.step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(1))
|
||||
.expect("Sending step 1 success failed");
|
||||
assert_eq!(empty, ());
|
||||
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
|
||||
assert_eq!(sender.service_queue.borrow().len(), 4);
|
||||
step_success_check(sender, tok.req_id);
|
||||
@ -2121,10 +2111,8 @@ mod tests {
|
||||
let started_token =
|
||||
b.vr.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0]))
|
||||
.expect("Sending start success failed");
|
||||
let empty =
|
||||
b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP))
|
||||
.expect("Sending completion success failed");
|
||||
assert_eq!(empty, ());
|
||||
b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP))
|
||||
.expect("Sending completion success failed");
|
||||
completion_success_check(&mut sender, tok.req_id);
|
||||
}
|
||||
|
||||
@ -2139,11 +2127,9 @@ mod tests {
|
||||
.helper
|
||||
.start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0]))
|
||||
.expect("Sending start success failed");
|
||||
let empty = b
|
||||
.helper
|
||||
b.helper
|
||||
.completion_success(started_token, Some(&EMPTY_STAMP))
|
||||
.expect("Sending completion success failed");
|
||||
assert_eq!(empty, ());
|
||||
let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap();
|
||||
completion_success_check(sender, tok.req_id);
|
||||
}
|
||||
@ -2151,18 +2137,19 @@ mod tests {
|
||||
#[test]
|
||||
// TODO: maybe a bit more extensive testing, all I have time for right now
|
||||
fn test_seq_count_increment() {
|
||||
let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
let tm_pool = Box::new(LocalPool::new(pool_cfg.clone()));
|
||||
let shared_tm_store = SharedTmStore::new(tm_pool);
|
||||
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
let tm_pool = StaticMemoryPool::new(pool_cfg.clone());
|
||||
let shared_tm_store = SharedTmPool::new(tm_pool);
|
||||
let shared_tm_pool = shared_tm_store.clone_backing_pool();
|
||||
let (verif_tx, verif_rx) = mpsc::channel();
|
||||
let sender = MpscTmInStoreSender::new(0, "Verification Sender", shared_tm_store, verif_tx);
|
||||
let sender =
|
||||
MpscTmInSharedPoolSender::new(0, "Verification Sender", shared_tm_store, verif_tx);
|
||||
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
let mut reporter = VerificationReporterWithSender::new(&cfg, Box::new(sender));
|
||||
|
||||
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
|
||||
let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true);
|
||||
let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
|
||||
let init_token = reporter.add_tc(&pus_tc_0);
|
||||
|
||||
// Complete success sequence for a telecommand
|
||||
@ -2185,9 +2172,9 @@ mod tests {
|
||||
{
|
||||
let mut rg = shared_tm_pool.write().expect("Error locking shared pool");
|
||||
let store_guard = rg.read_with_guard(addr);
|
||||
let slice = store_guard.read().expect("Error reading TM slice");
|
||||
tm_len = slice.len();
|
||||
tm_buf[0..tm_len].copy_from_slice(slice);
|
||||
tm_len = store_guard
|
||||
.read(&mut tm_buf)
|
||||
.expect("Error reading TM slice");
|
||||
}
|
||||
let (pus_tm, _) =
|
||||
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");
|
||||
|
@ -21,7 +21,7 @@
|
||||
//! use satrs_core::tmtc::ccsds_distrib::{CcsdsPacketHandler, CcsdsDistributor};
|
||||
//! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
|
||||
//! use spacepackets::{CcsdsPacket, SpHeader};
|
||||
//! use spacepackets::ecss::SerializablePusPacket;
|
||||
//! use spacepackets::ecss::WritablePusPacket;
|
||||
//! use spacepackets::ecss::tc::{PusTc, PusTcCreator};
|
||||
//!
|
||||
//! #[derive (Default)]
|
||||
@ -226,7 +226,7 @@ pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::tmtc::ccsds_distrib::{CcsdsDistributor, CcsdsPacketHandler};
|
||||
use spacepackets::ecss::tc::PusTcCreator;
|
||||
use spacepackets::ecss::SerializablePusPacket;
|
||||
use spacepackets::ecss::WritablePusPacket;
|
||||
use spacepackets::CcsdsPacket;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@ -244,9 +244,10 @@ pub(crate) mod tests {
|
||||
&buf[0..size]
|
||||
}
|
||||
|
||||
type SharedPacketQueue = Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>;
|
||||
pub struct BasicApidHandlerSharedQueue {
|
||||
pub known_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>,
|
||||
pub unknown_packet_queue: Arc<Mutex<VecDeque<(u16, Vec<u8>)>>>,
|
||||
pub known_packet_queue: SharedPacketQueue,
|
||||
pub unknown_packet_queue: SharedPacketQueue,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -268,11 +269,11 @@ pub(crate) mod tests {
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut vec = Vec::new();
|
||||
vec.extend_from_slice(tc_raw);
|
||||
Ok(self
|
||||
.known_packet_queue
|
||||
self.known_packet_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back((sp_header.apid(), vec)))
|
||||
.push_back((sp_header.apid(), vec));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_unknown_apid(
|
||||
@ -282,11 +283,11 @@ pub(crate) mod tests {
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut vec = Vec::new();
|
||||
vec.extend_from_slice(tc_raw);
|
||||
Ok(self
|
||||
.unknown_packet_queue
|
||||
self.unknown_packet_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back((sp_header.apid(), vec)))
|
||||
.push_back((sp_header.apid(), vec));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,7 @@
|
||||
//! routing without the overhead and complication of using message queues. However, it also requires
|
||||
#[cfg(feature = "alloc")]
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spacepackets::{ByteConversionError, SpHeader};
|
||||
use spacepackets::SpHeader;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod ccsds_distrib;
|
||||
@ -24,40 +22,6 @@ pub use pus_distrib::{PusDistributor, PusServiceProvider};
|
||||
|
||||
pub type TargetId = u32;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct AddressableId {
|
||||
pub target_id: TargetId,
|
||||
pub unique_id: u32,
|
||||
}
|
||||
|
||||
impl AddressableId {
|
||||
pub fn from_raw_be(buf: &[u8]) -> Result<Self, ByteConversionError> {
|
||||
if buf.len() < 8 {
|
||||
return Err(ByteConversionError::FromSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: 8,
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
target_id: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
|
||||
unique_id: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
|
||||
if buf.len() < 8 {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: 8,
|
||||
});
|
||||
}
|
||||
buf[0..4].copy_from_slice(&self.target_id.to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&self.unique_id.to_be_bytes());
|
||||
Ok(8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic trait for object which can receive any telecommands in form of a raw bytestream, with
|
||||
/// no assumptions about the received protocol.
|
||||
///
|
||||
|
@ -18,7 +18,7 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use spacepackets::ecss::SerializablePusPacket;
|
||||
//! use spacepackets::ecss::WritablePusPacket;
|
||||
//! use satrs_core::tmtc::pus_distrib::{PusDistributor, PusServiceProvider};
|
||||
//! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
|
||||
//! use spacepackets::SpHeader;
|
||||
|
@ -8,34 +8,38 @@ pub use std_mod::*;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod std_mod {
|
||||
use crate::pool::{ShareablePoolProvider, SharedPool, StoreAddr};
|
||||
use crate::pool::{PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr};
|
||||
use crate::pus::EcssTmtcError;
|
||||
use spacepackets::ecss::tm::PusTmCreator;
|
||||
use spacepackets::ecss::SerializablePusPacket;
|
||||
use spacepackets::ecss::WritablePusPacket;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharedTmStore {
|
||||
pool: SharedPool,
|
||||
}
|
||||
pub struct SharedTmPool(pub SharedStaticMemoryPool);
|
||||
|
||||
impl SharedTmStore {
|
||||
pub fn new(backing_pool: ShareablePoolProvider) -> Self {
|
||||
Self {
|
||||
pool: Arc::new(RwLock::new(backing_pool)),
|
||||
}
|
||||
impl SharedTmPool {
|
||||
pub fn new(shared_pool: StaticMemoryPool) -> Self {
|
||||
Self(Arc::new(RwLock::new(shared_pool)))
|
||||
}
|
||||
|
||||
pub fn clone_backing_pool(&self) -> SharedPool {
|
||||
self.pool.clone()
|
||||
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
|
||||
self.0.clone()
|
||||
}
|
||||
pub fn shared_pool(&self) -> &SharedStaticMemoryPool {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn shared_pool_mut(&mut self) -> &mut SharedStaticMemoryPool {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result<StoreAddr, EcssTmtcError> {
|
||||
let mut pg = self.pool.write().map_err(|_| EcssTmtcError::StoreLock)?;
|
||||
let (addr, buf) = pg.free_element(pus_tm.len_packed())?;
|
||||
pus_tm
|
||||
.write_to_bytes(buf)
|
||||
.expect("writing PUS TM to store failed");
|
||||
let mut pg = self.0.write().map_err(|_| EcssTmtcError::StoreLock)?;
|
||||
let addr = pg.free_element(pus_tm.len_written(), |buf| {
|
||||
pus_tm
|
||||
.write_to_bytes(buf)
|
||||
.expect("writing PUS TM to store failed");
|
||||
})?;
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
@ -59,7 +63,7 @@ impl PusTmWithCdsShortHelper {
|
||||
&'a mut self,
|
||||
service: u8,
|
||||
subservice: u8,
|
||||
source_data: Option<&'a [u8]>,
|
||||
source_data: &'a [u8],
|
||||
seq_count: u16,
|
||||
) -> PusTmCreator {
|
||||
let time_stamp = TimeProvider::from_now_with_u16_days().unwrap();
|
||||
@ -71,7 +75,7 @@ impl PusTmWithCdsShortHelper {
|
||||
&'a mut self,
|
||||
service: u8,
|
||||
subservice: u8,
|
||||
source_data: Option<&'a [u8]>,
|
||||
source_data: &'a [u8],
|
||||
stamper: &TimeProvider,
|
||||
seq_count: u16,
|
||||
) -> PusTmCreator {
|
||||
@ -83,7 +87,7 @@ impl PusTmWithCdsShortHelper {
|
||||
&'a self,
|
||||
service: u8,
|
||||
subservice: u8,
|
||||
source_data: Option<&'a [u8]>,
|
||||
source_data: &'a [u8],
|
||||
seq_count: u16,
|
||||
) -> PusTmCreator {
|
||||
let mut reply_header = SpHeader::tm_unseg(self.apid, seq_count, 0).unwrap();
|
||||
@ -91,3 +95,33 @@ impl PusTmWithCdsShortHelper {
|
||||
PusTmCreator::new(&mut reply_header, tc_header, source_data, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use spacepackets::{ecss::PusPacket, time::cds::TimeProvider, CcsdsPacket};
|
||||
|
||||
use super::PusTmWithCdsShortHelper;
|
||||
|
||||
#[test]
|
||||
fn test_helper_with_stamper() {
|
||||
let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123);
|
||||
let stamper = TimeProvider::new_with_u16_days(0, 0);
|
||||
let tm = pus_tm_helper.create_pus_tm_with_stamper(17, 1, &[1, 2, 3, 4], &stamper, 25);
|
||||
assert_eq!(tm.service(), 17);
|
||||
assert_eq!(tm.subservice(), 1);
|
||||
assert_eq!(tm.user_data(), &[1, 2, 3, 4]);
|
||||
assert_eq!(tm.seq_count(), 25);
|
||||
assert_eq!(tm.timestamp(), [64, 0, 0, 0, 0, 0, 0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_helper_from_now() {
|
||||
let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123);
|
||||
let tm = pus_tm_helper.create_pus_tm_timestamp_now(17, 1, &[1, 2, 3, 4], 25);
|
||||
assert_eq!(tm.service(), 17);
|
||||
assert_eq!(tm.subservice(), 1);
|
||||
assert_eq!(tm.user_data(), &[1, 2, 3, 4]);
|
||||
assert_eq!(tm.seq_count(), 25);
|
||||
assert_eq!(tm.timestamp().len(), 7);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
use core::mem::size_of;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spacepackets::ecss::{Ptc, RealPfc, UnsignedPfc};
|
||||
use spacepackets::ecss::{PfcReal, PfcUnsigned, Ptc};
|
||||
use spacepackets::time::cds::TimeProvider;
|
||||
use spacepackets::time::{CcsdsTimeProvider, TimeWriter};
|
||||
|
||||
@ -64,7 +64,7 @@ impl TestMgmHkWithIndividualValidity {
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = Ptc::Real as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = RealPfc::Float as u8;
|
||||
buf[curr_idx] = PfcReal::Float as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.val.to_be_bytes());
|
||||
curr_idx += size_of::<f32>();
|
||||
@ -75,7 +75,7 @@ impl TestMgmHkWithIndividualValidity {
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = Ptc::UnsignedInt as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = UnsignedPfc::TwoBytes as u8;
|
||||
buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = 3;
|
||||
curr_idx += 1;
|
||||
@ -100,7 +100,7 @@ impl TestMgmHkWithGroupValidity {
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = Ptc::Real as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = RealPfc::Float as u8;
|
||||
buf[curr_idx] = PfcReal::Float as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx..curr_idx + size_of::<f32>()].copy_from_slice(&self.temp.to_be_bytes());
|
||||
curr_idx += size_of::<f32>();
|
||||
@ -109,7 +109,7 @@ impl TestMgmHkWithGroupValidity {
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = Ptc::UnsignedInt as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = UnsignedPfc::TwoBytes as u8;
|
||||
buf[curr_idx] = PfcUnsigned::TwoBytes as u8;
|
||||
curr_idx += 1;
|
||||
buf[curr_idx] = 3;
|
||||
for val in self.mgm_vals {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use satrs_core::pool::{LocalPool, PoolCfg, PoolGuard, PoolProvider, StoreAddr};
|
||||
use satrs_core::pool::{PoolGuard, PoolProvider, StaticMemoryPool, StaticPoolConfig, StoreAddr};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
@ -9,8 +9,8 @@ const DUMMY_DATA: [u8; 4] = [0, 1, 2, 3];
|
||||
|
||||
#[test]
|
||||
fn threaded_usage() {
|
||||
let pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]);
|
||||
let shared_pool = Arc::new(RwLock::new(LocalPool::new(pool_cfg)));
|
||||
let pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]);
|
||||
let shared_pool = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
|
||||
let shared_clone = shared_pool.clone();
|
||||
let (tx, rx): (Sender<StoreAddr>, Receiver<StoreAddr>) = mpsc::channel();
|
||||
let jh0 = thread::spawn(move || {
|
||||
@ -25,8 +25,10 @@ fn threaded_usage() {
|
||||
addr = rx.recv().expect("Receiving store address failed");
|
||||
let mut pool_access = shared_clone.write().unwrap();
|
||||
let pg = PoolGuard::new(pool_access.deref_mut(), addr);
|
||||
let read_res = pg.read().expect("Reading failed");
|
||||
assert_eq!(read_res, DUMMY_DATA);
|
||||
let mut read_buf: [u8; 4] = [0; 4];
|
||||
let read_bytes = pg.read(&mut read_buf).expect("Reading failed");
|
||||
assert_eq!(read_buf, DUMMY_DATA);
|
||||
assert_eq!(read_bytes, 4);
|
||||
}
|
||||
let pool_access = shared_clone.read().unwrap();
|
||||
assert!(!pool_access.has_element_at(&addr).expect("Invalid address"));
|
||||
|
@ -1,15 +1,17 @@
|
||||
#[cfg(feature = "crossbeam")]
|
||||
//#[cfg(feature = "crossbeam")]
|
||||
pub mod crossbeam_test {
|
||||
use hashbrown::HashMap;
|
||||
use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider};
|
||||
use satrs_core::pool::{
|
||||
PoolProvider, PoolProviderWithGuards, StaticMemoryPool, StaticPoolConfig,
|
||||
};
|
||||
use satrs_core::pus::verification::{
|
||||
FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender,
|
||||
};
|
||||
use satrs_core::pus::CrossbeamTmInStoreSender;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmStore;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader};
|
||||
use spacepackets::ecss::tm::PusTmReader;
|
||||
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, SerializablePusPacket};
|
||||
use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, WritablePusPacket};
|
||||
use spacepackets::SpHeader;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
@ -33,13 +35,13 @@ pub mod crossbeam_test {
|
||||
// each reporter have an own sequence count provider.
|
||||
let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap();
|
||||
// Shared pool object to store the verification PUS telemetry
|
||||
let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
let shared_tm_store = SharedTmStore::new(Box::new(LocalPool::new(pool_cfg.clone())));
|
||||
let shared_tc_pool_0 = Arc::new(RwLock::new(LocalPool::new(pool_cfg)));
|
||||
let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]);
|
||||
let shared_tm_pool = SharedTmPool::new(StaticMemoryPool::new(pool_cfg.clone()));
|
||||
let shared_tc_pool_0 = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg)));
|
||||
let shared_tc_pool_1 = shared_tc_pool_0.clone();
|
||||
let (tx, rx) = crossbeam_channel::bounded(10);
|
||||
let sender =
|
||||
CrossbeamTmInStoreSender::new(0, "verif_sender", shared_tm_store.clone(), tx.clone());
|
||||
CrossbeamTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tx.clone());
|
||||
let mut reporter_with_sender_0 =
|
||||
VerificationReporterWithSender::new(&cfg, Box::new(sender));
|
||||
let mut reporter_with_sender_1 = reporter_with_sender_0.clone();
|
||||
@ -54,17 +56,23 @@ pub mod crossbeam_test {
|
||||
let mut tc_guard = shared_tc_pool_0.write().unwrap();
|
||||
let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap();
|
||||
let tc_header = PusTcSecondaryHeader::new_simple(17, 1);
|
||||
let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true);
|
||||
let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
|
||||
req_id_0 = RequestId::new(&pus_tc_0);
|
||||
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap();
|
||||
pus_tc_0.write_to_bytes(&mut buf).unwrap();
|
||||
let addr = tc_guard
|
||||
.free_element(pus_tc_0.len_written(), |buf| {
|
||||
pus_tc_0.write_to_bytes(buf).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
tx_tc_0.send(addr).unwrap();
|
||||
let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap();
|
||||
let tc_header = PusTcSecondaryHeader::new_simple(5, 1);
|
||||
let pus_tc_1 = PusTcCreator::new(&mut sph, tc_header, None, true);
|
||||
let pus_tc_1 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true);
|
||||
req_id_1 = RequestId::new(&pus_tc_1);
|
||||
let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap();
|
||||
pus_tc_1.write_to_bytes(&mut buf).unwrap();
|
||||
let addr = tc_guard
|
||||
.free_element(pus_tc_0.len_written(), |buf| {
|
||||
pus_tc_1.write_to_bytes(buf).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
tx_tc_1.send(addr).unwrap();
|
||||
}
|
||||
let verif_sender_0 = thread::spawn(move || {
|
||||
@ -76,21 +84,17 @@ pub mod crossbeam_test {
|
||||
{
|
||||
let mut tc_guard = shared_tc_pool_0.write().unwrap();
|
||||
let pg = tc_guard.read_with_guard(tc_addr);
|
||||
let buf = pg.read().unwrap();
|
||||
tc_len = buf.len();
|
||||
tc_buf[0..tc_len].copy_from_slice(buf);
|
||||
tc_len = pg.read(&mut tc_buf).unwrap();
|
||||
}
|
||||
let (_tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
|
||||
let accepted_token;
|
||||
|
||||
let token = reporter_with_sender_0.add_tc_with_req_id(req_id_0);
|
||||
accepted_token = reporter_with_sender_0
|
||||
let accepted_token = reporter_with_sender_0
|
||||
.acceptance_success(token, Some(&FIXED_STAMP))
|
||||
.expect("Acceptance success failed");
|
||||
|
||||
// Do some start handling here
|
||||
let started_token;
|
||||
started_token = reporter_with_sender_0
|
||||
let started_token = reporter_with_sender_0
|
||||
.start_success(accepted_token, Some(&FIXED_STAMP))
|
||||
.expect("Start success failed");
|
||||
// Do some step handling here
|
||||
@ -116,9 +120,7 @@ pub mod crossbeam_test {
|
||||
{
|
||||
let mut tc_guard = shared_tc_pool_1.write().unwrap();
|
||||
let pg = tc_guard.read_with_guard(tc_addr);
|
||||
let buf = pg.read().unwrap();
|
||||
tc_len = buf.len();
|
||||
tc_buf[0..tc_len].copy_from_slice(buf);
|
||||
tc_len = pg.read(&mut tc_buf).unwrap();
|
||||
}
|
||||
let (tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap();
|
||||
let token = reporter_with_sender_1.add_tc(&tc);
|
||||
@ -144,13 +146,13 @@ pub mod crossbeam_test {
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("Packet reception timeout");
|
||||
let tm_len;
|
||||
let shared_tm_store = shared_tm_store.clone_backing_pool();
|
||||
let shared_tm_store = shared_tm_pool.clone_backing_pool();
|
||||
{
|
||||
let mut rg = shared_tm_store.write().expect("Error locking shared pool");
|
||||
let store_guard = rg.read_with_guard(verif_addr);
|
||||
let slice = store_guard.read().expect("Error reading TM slice");
|
||||
tm_len = slice.len();
|
||||
tm_buf[0..tm_len].copy_from_slice(slice);
|
||||
tm_len = store_guard
|
||||
.read(&mut tm_buf)
|
||||
.expect("Error reading TM slice");
|
||||
}
|
||||
let (pus_tm, _) =
|
||||
PusTmReader::new(&tm_buf[0..tm_len], 7).expect("Error reading verification TM");
|
||||
@ -158,8 +160,7 @@ pub mod crossbeam_test {
|
||||
RequestId::from_bytes(&pus_tm.source_data()[0..RequestId::SIZE_AS_BYTES])
|
||||
.expect("reading request ID from PUS TM source data failed");
|
||||
if !verif_map.contains_key(&req_id) {
|
||||
let mut content = Vec::new();
|
||||
content.push(pus_tm.subservice());
|
||||
let content = vec![pus_tm.subservice()];
|
||||
verif_map.insert(req_id, content);
|
||||
} else {
|
||||
let content = verif_map.get_mut(&req_id).unwrap();
|
||||
|
@ -28,7 +28,7 @@ use satrs_core::{
|
||||
tmtc::{ReceivesTcCore, TmPacketSourceCore},
|
||||
};
|
||||
use spacepackets::{
|
||||
ecss::{tc::PusTcCreator, SerializablePusPacket},
|
||||
ecss::{tc::PusTcCreator, WritablePusPacket},
|
||||
PacketId, SpHeader,
|
||||
};
|
||||
use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec};
|
||||
@ -94,8 +94,8 @@ fn test_cobs_server() {
|
||||
tm_source.add_tm(&INVERTED_PACKET);
|
||||
let mut tcp_server = TcpTmtcInCobsServer::new(
|
||||
ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024),
|
||||
Box::new(tm_source),
|
||||
Box::new(tc_receiver.clone()),
|
||||
tm_source,
|
||||
tc_receiver.clone(),
|
||||
)
|
||||
.expect("TCP server generation failed");
|
||||
let dest_addr = tcp_server
|
||||
@ -176,8 +176,8 @@ fn test_ccsds_server() {
|
||||
packet_id_lookup.insert(TEST_PACKET_ID_0);
|
||||
let mut tcp_server = TcpSpacepacketsServer::new(
|
||||
ServerConfig::new(AUTO_PORT_ADDR, Duration::from_millis(2), 1024, 1024),
|
||||
Box::new(tm_source),
|
||||
Box::new(tc_receiver.clone()),
|
||||
tm_source,
|
||||
tc_receiver.clone(),
|
||||
Box::new(packet_id_lookup),
|
||||
)
|
||||
.expect("TCP server generation failed");
|
||||
|
1
satrs-example/.gitignore
vendored
Normal file
1
satrs-example/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/output.log
|
@ -17,11 +17,16 @@ zerocopy = "0.6"
|
||||
csv = "1"
|
||||
num_enum = "0.7"
|
||||
thiserror = "1"
|
||||
derive-new = "0.5"
|
||||
|
||||
[dependencies.satrs-core]
|
||||
# version = "0.1.0-alpha.0"
|
||||
# version = "0.1.0-alpha.1"
|
||||
path = "../satrs-core"
|
||||
|
||||
|
||||
[dependencies.satrs-mib]
|
||||
# version = "0.1.0-alpha.1"
|
||||
path = "../satrs-mib"
|
||||
|
||||
[features]
|
||||
dyn_tmtc = []
|
||||
default = ["dyn_tmtc"]
|
||||
|
@ -3,10 +3,32 @@ sat-rs example
|
||||
|
||||
This crate contains an example application which simulates an on-board software.
|
||||
It uses various components provided by the sat-rs framework to do this. As such, it shows how
|
||||
a more complex real on-board software could be built from these components.
|
||||
The application opens a UDP server on port 7301 to receive telecommands.
|
||||
a more complex real on-board software could be built from these components. It is recommended to
|
||||
read the dedicated
|
||||
[example chapters](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
|
||||
the sat-rs book.
|
||||
|
||||
You can run the application using `cargo run`. The `simpleclient` binary target sends a
|
||||
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
|
||||
|
||||
You can run the application using `cargo run`.
|
||||
|
||||
# Features
|
||||
|
||||
The example has the `dyn_tmtc` feature which is enabled by default. With this feature enabled,
|
||||
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
|
||||
stores.
|
||||
|
||||
You can run the application without this feature using
|
||||
|
||||
```sh
|
||||
cargo run --no-default-features
|
||||
```
|
||||
|
||||
# Interacting with the sat-rs example
|
||||
|
||||
## Simple Client
|
||||
|
||||
The `simpleclient` binary target sends a
|
||||
ping telecommand and then verifies the telemetry generated by the example application.
|
||||
It can be run like this:
|
||||
|
||||
@ -17,7 +39,7 @@ cargo run --bin simpleclient
|
||||
This repository also contains a more complex client using the
|
||||
[Python tmtccmd](https://github.com/robamu-org/tmtccmd) module.
|
||||
|
||||
# Using the tmtccmd Python client
|
||||
## <a id="tmtccmd"></a> Using the tmtccmd Python client
|
||||
|
||||
The python client requires a valid installation of the
|
||||
[tmtccmd package](https://github.com/robamu-org/tmtccmd).
|
||||
@ -46,8 +68,7 @@ as Python code. For example, you can use the following command to send a ping li
|
||||
the `simpleclient`:
|
||||
|
||||
```sh
|
||||
./main.py -s test -o ping
|
||||
./main.py -p /test/ping
|
||||
```
|
||||
|
||||
You can also simply call the script without any arguments to view a list of services (`-s` flag)
|
||||
and corresponding op codes (`-o` flag) for each service.
|
||||
You can also simply call the script without any arguments to view the command tree.
|
||||
|
1
satrs-example/pyclient/.gitignore
vendored
1
satrs-example/pyclient/.gitignore
vendored
@ -6,3 +6,4 @@ __pycache__
|
||||
!/.idea/runConfigurations
|
||||
|
||||
/seqcnt.txt
|
||||
/.tmtc-history.txt
|
||||
|
@ -4,7 +4,11 @@ import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
|
||||
from spacepackets.ecss.tc import PacketId, PacketType
|
||||
|
||||
EXAMPLE_PUS_APID = 0x02
|
||||
EXAMPLE_PUS_PACKET_ID_TM = PacketId(PacketType.TM, True, EXAMPLE_PUS_APID)
|
||||
TM_PACKET_IDS = [EXAMPLE_PUS_PACKET_ID_TM]
|
||||
|
||||
|
||||
class EventSeverity(enum.IntEnum):
|
||||
@ -40,10 +44,6 @@ class AcsHkIds(enum.IntEnum):
|
||||
MGM_SET = 1
|
||||
|
||||
|
||||
class HkOpCodes:
|
||||
GENERATE_ONE_SHOT = ["0", "oneshot"]
|
||||
|
||||
|
||||
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
|
||||
byte_string = bytearray(struct.pack("!I", target_id))
|
||||
byte_string.extend(struct.pack("!I", unique_id))
|
||||
|
@ -4,6 +4,8 @@ import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
from prompt_toolkit.history import History
|
||||
from prompt_toolkit.history import FileHistory
|
||||
|
||||
import tmtccmd
|
||||
from spacepackets.ecss import PusTelemetry, PusVerificator
|
||||
@ -11,16 +13,16 @@ from spacepackets.ecss.pus_17_test import Service17Tm
|
||||
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
|
||||
from spacepackets.ccsds.time import CdsShortTimestamp
|
||||
|
||||
from tmtccmd import CcsdsTmtcBackend, TcHandlerBase, ProcedureParamsWrapper
|
||||
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
|
||||
from tmtccmd.core.base import BackendRequest
|
||||
from tmtccmd.pus import VerificationWrapper
|
||||
from tmtccmd.tm import CcsdsTmHandler, SpecificApidHandlerBase
|
||||
from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
|
||||
from tmtccmd.com import ComInterface
|
||||
from tmtccmd.config import (
|
||||
CmdTreeNode,
|
||||
default_json_path,
|
||||
SetupParams,
|
||||
HookBase,
|
||||
TmtcDefinitionWrapper,
|
||||
params_to_procedure_conversion,
|
||||
)
|
||||
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
|
||||
@ -30,7 +32,7 @@ from tmtccmd.logging.pus import (
|
||||
RawTmtcTimedLogWrapper,
|
||||
TimedLogWhen,
|
||||
)
|
||||
from tmtccmd.tc import (
|
||||
from tmtccmd.tmtc import (
|
||||
TcQueueEntryType,
|
||||
ProcedureWrapper,
|
||||
TcProcedureType,
|
||||
@ -39,13 +41,12 @@ from tmtccmd.tc import (
|
||||
DefaultPusQueueHelper,
|
||||
QueueWrapper,
|
||||
)
|
||||
from tmtccmd.util import FileSeqCountProvider, PusFileSeqCountProvider
|
||||
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
|
||||
from tmtccmd.util.obj_id import ObjectIdDictT
|
||||
|
||||
|
||||
import pus_tc
|
||||
import tc_definitions
|
||||
from common import EXAMPLE_PUS_APID, EventU32
|
||||
from common import EXAMPLE_PUS_APID, TM_PACKET_IDS, EventU32
|
||||
|
||||
_LOGGER = logging.getLogger()
|
||||
|
||||
@ -54,25 +55,29 @@ class SatRsConfigHook(HookBase):
|
||||
def __init__(self, json_cfg_path: str):
|
||||
super().__init__(json_cfg_path=json_cfg_path)
|
||||
|
||||
def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
|
||||
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
|
||||
from tmtccmd.config.com import (
|
||||
create_com_interface_default,
|
||||
create_com_interface_cfg_default,
|
||||
)
|
||||
|
||||
assert self.cfg_path is not None
|
||||
cfg = create_com_interface_cfg_default(
|
||||
com_if_key=com_if_key,
|
||||
json_cfg_path=self.cfg_path,
|
||||
space_packet_ids=None,
|
||||
space_packet_ids=TM_PACKET_IDS,
|
||||
)
|
||||
assert cfg is not None
|
||||
return create_com_interface_default(cfg)
|
||||
|
||||
def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
|
||||
return tc_definitions.tc_definitions()
|
||||
def get_command_definitions(self) -> CmdTreeNode:
|
||||
"""This function should return the root node of the command definition tree."""
|
||||
return pus_tc.create_cmd_definition_tree()
|
||||
|
||||
def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int):
|
||||
_LOGGER.info("Mode operation hook was called")
|
||||
pass
|
||||
def get_cmd_history(self) -> Optional[History]:
|
||||
"""Optionlly return a history class for the past command paths which will be used
|
||||
when prompting a command path from the user in CLI mode."""
|
||||
return FileHistory(".tmtc-history.txt")
|
||||
|
||||
def get_object_ids(self) -> ObjectIdDictT:
|
||||
from tmtccmd.config.objects import get_core_object_ids
|
||||
@ -94,15 +99,12 @@ class PusHandler(SpecificApidHandlerBase):
|
||||
|
||||
def handle_tm(self, packet: bytes, _user_args: any):
|
||||
try:
|
||||
tm_packet = PusTelemetry.unpack(
|
||||
packet, time_reader=CdsShortTimestamp.empty()
|
||||
)
|
||||
pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty())
|
||||
except ValueError as e:
|
||||
_LOGGER.warning("Could not generate PUS TM object from raw data")
|
||||
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
|
||||
raise e
|
||||
service = tm_packet.service
|
||||
dedicated_handler = False
|
||||
service = pus_tm.service
|
||||
if service == 1:
|
||||
tm_packet = Service1Tm.unpack(
|
||||
data=packet, params=UnpackParams(CdsShortTimestamp.empty(), 1, 2)
|
||||
@ -119,8 +121,7 @@ class PusHandler(SpecificApidHandlerBase):
|
||||
else:
|
||||
self.verif_wrapper.log_to_console(tm_packet, res)
|
||||
self.verif_wrapper.log_to_file(tm_packet, res)
|
||||
dedicated_handler = True
|
||||
if service == 3:
|
||||
elif service == 3:
|
||||
_LOGGER.info("No handling for HK packets implemented")
|
||||
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
|
||||
pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty())
|
||||
@ -128,8 +129,8 @@ class PusHandler(SpecificApidHandlerBase):
|
||||
if len(pus_tm.source_data) < 8:
|
||||
raise ValueError("No addressable ID in HK packet")
|
||||
json_str = pus_tm.source_data[8:]
|
||||
dedicated_handler = True
|
||||
if service == 5:
|
||||
_LOGGER.info(json_str)
|
||||
elif service == 5:
|
||||
tm_packet = PusTelemetry.unpack(
|
||||
packet, time_reader=CdsShortTimestamp.empty()
|
||||
)
|
||||
@ -138,11 +139,10 @@ class PusHandler(SpecificApidHandlerBase):
|
||||
_LOGGER.info(f"Received event packet. Event: {event_u32}")
|
||||
if event_u32.group_id == 0 and event_u32.unique_id == 0:
|
||||
_LOGGER.info("Received test event")
|
||||
if service == 17:
|
||||
elif service == 17:
|
||||
tm_packet = Service17Tm.unpack(
|
||||
packet, time_reader=CdsShortTimestamp.empty()
|
||||
)
|
||||
dedicated_handler = True
|
||||
if tm_packet.subservice == 2:
|
||||
self.file_logger.info("Received Ping Reply TM[17,2]")
|
||||
_LOGGER.info("Received Ping Reply TM[17,2]")
|
||||
@ -153,17 +153,14 @@ class PusHandler(SpecificApidHandlerBase):
|
||||
_LOGGER.info(
|
||||
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
|
||||
)
|
||||
if tm_packet is None:
|
||||
else:
|
||||
_LOGGER.info(
|
||||
f"The service {service} is not implemented in Telemetry Factory"
|
||||
)
|
||||
tm_packet = PusTelemetry.unpack(
|
||||
packet, time_reader=CdsShortTimestamp.empty()
|
||||
)
|
||||
self.raw_logger.log_tm(tm_packet)
|
||||
if not dedicated_handler and tm_packet is not None:
|
||||
pass
|
||||
# self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet)
|
||||
self.raw_logger.log_tm(pus_tm)
|
||||
|
||||
|
||||
class TcHandler(TcHandlerBase):
|
||||
@ -195,22 +192,18 @@ class TcHandler(TcHandlerBase):
|
||||
log_entry = entry_helper.to_log_entry()
|
||||
_LOGGER.info(log_entry.log_str)
|
||||
|
||||
def queue_finished_cb(self, helper: ProcedureWrapper):
|
||||
if helper.proc_type == TcProcedureType.DEFAULT:
|
||||
def_proc = helper.to_def_procedure()
|
||||
_LOGGER.info(
|
||||
f"Queue handling finished for service {def_proc.service} and "
|
||||
f"op code {def_proc.op_code}"
|
||||
)
|
||||
def queue_finished_cb(self, info: ProcedureWrapper):
|
||||
if info.proc_type == TcProcedureType.DEFAULT:
|
||||
def_proc = info.to_def_procedure()
|
||||
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
|
||||
|
||||
def feed_cb(self, helper: ProcedureWrapper, wrapper: FeedWrapper):
|
||||
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
|
||||
q = self.queue_helper
|
||||
q.queue_wrapper = wrapper.queue_wrapper
|
||||
if helper.proc_type == TcProcedureType.DEFAULT:
|
||||
def_proc = helper.to_def_procedure()
|
||||
service = def_proc.service
|
||||
op_code = def_proc.op_code
|
||||
pus_tc.pack_pus_telecommands(q, service, op_code)
|
||||
if info.proc_type == TcProcedureType.DEFAULT:
|
||||
def_proc = info.to_def_procedure()
|
||||
assert def_proc.cmd_path is not None
|
||||
pus_tc.pack_pus_telecommands(q, def_proc.cmd_path)
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -1,50 +1,85 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from spacepackets.ccsds import CdsShortTimestamp
|
||||
from spacepackets.ecss import PusTelecommand
|
||||
from tmtccmd.config import CoreServiceList
|
||||
from tmtccmd.tc import DefaultPusQueueHelper
|
||||
from tmtccmd.tc.pus_11_tc_sched import create_time_tagged_cmd
|
||||
from tmtccmd.tc.pus_3_fsfw_hk import create_request_one_hk_command
|
||||
from tmtccmd.config import CmdTreeNode
|
||||
from tmtccmd.tmtc import DefaultPusQueueHelper
|
||||
from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd
|
||||
from tmtccmd.pus.tc.s3_fsfw_hk import create_request_one_hk_command
|
||||
|
||||
from common import (
|
||||
EXAMPLE_PUS_APID,
|
||||
HkOpCodes,
|
||||
make_addressable_id,
|
||||
RequestTargetId,
|
||||
AcsHkIds,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def pack_pus_telecommands(q: DefaultPusQueueHelper, service: str, op_code: str):
|
||||
if (
|
||||
service == CoreServiceList.SERVICE_17
|
||||
or service == CoreServiceList.SERVICE_17_ALT
|
||||
):
|
||||
if op_code == "ping":
|
||||
|
||||
def create_cmd_definition_tree() -> CmdTreeNode:
|
||||
|
||||
root_node = CmdTreeNode.root_node()
|
||||
|
||||
test_node = CmdTreeNode("test", "Test Node")
|
||||
test_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
|
||||
test_node.add_child(CmdTreeNode("trigger_event", "Send PUS test to trigger event"))
|
||||
root_node.add_child(test_node)
|
||||
|
||||
scheduler_node = CmdTreeNode("scheduler", "Scheduler Node")
|
||||
scheduler_node.add_child(
|
||||
CmdTreeNode(
|
||||
"schedule_ping_10_secs_ahead", "Schedule Ping to execute in 10 seconds"
|
||||
)
|
||||
)
|
||||
root_node.add_child(scheduler_node)
|
||||
|
||||
acs_node = CmdTreeNode("acs", "ACS Subsystem Node")
|
||||
mgm_node = CmdTreeNode("mgms", "MGM devices node")
|
||||
mgm_node.add_child(CmdTreeNode("one_shot_hk", "Request one shot HK"))
|
||||
acs_node.add_child(mgm_node)
|
||||
root_node.add_child(acs_node)
|
||||
|
||||
return root_node
|
||||
|
||||
|
||||
def pack_pus_telecommands(q: DefaultPusQueueHelper, cmd_path: str):
|
||||
# It should always be at least the root path "/", so we split of the empty portion left of it.
|
||||
cmd_path_list = cmd_path.split("/")[1:]
|
||||
if len(cmd_path_list) == 0:
|
||||
_LOGGER.warning("empty command path")
|
||||
return
|
||||
if cmd_path_list[0] == "test":
|
||||
assert len(cmd_path_list) >= 2
|
||||
if cmd_path_list[1] == "ping":
|
||||
q.add_log_cmd("Sending PUS ping telecommand")
|
||||
return q.add_pus_tc(PusTelecommand(service=17, subservice=1))
|
||||
elif op_code == "trigger_event":
|
||||
elif cmd_path_list[1] == "trigger_event":
|
||||
q.add_log_cmd("Triggering test event")
|
||||
return q.add_pus_tc(PusTelecommand(service=17, subservice=128))
|
||||
if service == CoreServiceList.SERVICE_11:
|
||||
q.add_log_cmd("Sending PUS scheduled TC telecommand")
|
||||
crt_time = CdsShortTimestamp.from_now()
|
||||
time_stamp = crt_time + datetime.timedelta(seconds=10)
|
||||
time_stamp = time_stamp.pack()
|
||||
return q.add_pus_tc(
|
||||
create_time_tagged_cmd(
|
||||
time_stamp,
|
||||
PusTelecommand(service=17, subservice=1),
|
||||
apid=EXAMPLE_PUS_APID,
|
||||
)
|
||||
)
|
||||
if service == CoreServiceList.SERVICE_3:
|
||||
if op_code in HkOpCodes.GENERATE_ONE_SHOT:
|
||||
q.add_log_cmd("Sending HK one shot request")
|
||||
q.add_pus_tc(
|
||||
create_request_one_hk_command(
|
||||
make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET)
|
||||
if cmd_path_list[0] == "scheduler":
|
||||
assert len(cmd_path_list) >= 2
|
||||
if cmd_path_list[1] == "schedule_ping_10_secs_ahead":
|
||||
q.add_log_cmd("Sending PUS scheduled TC telecommand")
|
||||
crt_time = CdsShortTimestamp.from_now()
|
||||
time_stamp = crt_time + datetime.timedelta(seconds=10)
|
||||
time_stamp = time_stamp.pack()
|
||||
return q.add_pus_tc(
|
||||
create_time_tagged_cmd(
|
||||
time_stamp,
|
||||
PusTelecommand(service=17, subservice=1),
|
||||
apid=EXAMPLE_PUS_APID,
|
||||
)
|
||||
)
|
||||
pass
|
||||
if cmd_path_list[0] == "acs":
|
||||
assert len(cmd_path_list) >= 2
|
||||
if cmd_path_list[1] == "mgm":
|
||||
assert len(cmd_path_list) >= 3
|
||||
if cmd_path_list[2] == "one_shot_hk":
|
||||
q.add_log_cmd("Sending HK one shot request")
|
||||
q.add_pus_tc(
|
||||
create_request_one_hk_command(
|
||||
make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET)
|
||||
)
|
||||
)
|
||||
|
@ -1,2 +1,2 @@
|
||||
tmtccmd == 5.0.0rc0
|
||||
tmtccmd == 8.0.0rc1
|
||||
# -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd
|
||||
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"com_if": "udp",
|
||||
"com_if": "tcp",
|
||||
"tcpip_udp_ip_addr": "127.0.0.1",
|
||||
"tcpip_udp_port": 7301,
|
||||
"tcpip_udp_recv_max_size": 1500
|
||||
}
|
||||
"tcpip_udp_recv_max_size": 1500,
|
||||
"tcpip_tcp_ip_addr": "127.0.0.1",
|
||||
"tcpip_tcp_port": 7301
|
||||
}
|
||||
|
117
satrs-example/src/acs.rs
Normal file
117
satrs-example/src/acs.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
|
||||
use log::{info, warn};
|
||||
use satrs_core::pus::verification::VerificationReporterWithSender;
|
||||
use satrs_core::pus::{EcssTmSender, PusTmWrapper};
|
||||
use satrs_core::spacepackets::ecss::hk::Subservice as HkSubservice;
|
||||
use satrs_core::{
|
||||
hk::HkRequest,
|
||||
spacepackets::{
|
||||
ecss::tm::{PusTmCreator, PusTmSecondaryHeader},
|
||||
time::cds::{DaysLen16Bits, TimeProvider},
|
||||
SequenceFlags, SpHeader,
|
||||
},
|
||||
};
|
||||
use satrs_example::config::{RequestTargetId, PUS_APID};
|
||||
|
||||
use crate::{
|
||||
hk::{AcsHkIds, HkUniqueId},
|
||||
requests::{Request, RequestWithToken},
|
||||
update_time,
|
||||
};
|
||||
|
||||
pub struct AcsTask {
|
||||
timestamp: [u8; 7],
|
||||
time_provider: TimeProvider<DaysLen16Bits>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
request_rx: mpsc::Receiver<RequestWithToken>,
|
||||
}
|
||||
|
||||
impl AcsTask {
|
||||
pub fn new(
|
||||
tm_sender: impl EcssTmSender,
|
||||
request_rx: mpsc::Receiver<RequestWithToken>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
) -> Self {
|
||||
Self {
|
||||
timestamp: [0; 7],
|
||||
time_provider: TimeProvider::new_with_u16_days(0, 0),
|
||||
verif_reporter,
|
||||
tm_sender: Box::new(tm_sender),
|
||||
request_rx,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hk_request(&mut self, target_id: u32, unique_id: u32) {
|
||||
assert_eq!(target_id, RequestTargetId::AcsSubsystem as u32);
|
||||
if unique_id == AcsHkIds::TestMgmSet as u32 {
|
||||
let mut sp_header = SpHeader::tm(PUS_APID, SequenceFlags::Unsegmented, 0, 0).unwrap();
|
||||
let sec_header = PusTmSecondaryHeader::new_simple(
|
||||
3,
|
||||
HkSubservice::TmHkPacket as u8,
|
||||
&self.timestamp,
|
||||
);
|
||||
let mut buf: [u8; 8] = [0; 8];
|
||||
let hk_id = HkUniqueId::new(target_id, unique_id);
|
||||
hk_id.write_to_be_bytes(&mut buf).unwrap();
|
||||
let pus_tm = PusTmCreator::new(&mut sp_header, sec_header, &buf, true);
|
||||
self.tm_sender
|
||||
.send_tm(PusTmWrapper::Direct(pus_tm))
|
||||
.expect("Sending HK TM failed");
|
||||
}
|
||||
// TODO: Verification failure for invalid unique IDs.
|
||||
}
|
||||
|
||||
pub fn try_reading_one_request(&mut self) -> bool {
|
||||
match self.request_rx.try_recv() {
|
||||
Ok(request) => {
|
||||
info!(
|
||||
"ACS thread: Received HK request {:?}",
|
||||
request.targeted_request
|
||||
);
|
||||
match request.targeted_request.request {
|
||||
Request::Hk(hk_req) => match hk_req {
|
||||
HkRequest::OneShot(unique_id) => self.handle_hk_request(
|
||||
request.targeted_request.target_id_with_apid.target_id(),
|
||||
unique_id,
|
||||
),
|
||||
HkRequest::Enable(_) => {}
|
||||
HkRequest::Disable(_) => {}
|
||||
HkRequest::ModifyCollectionInterval(_, _) => {}
|
||||
},
|
||||
Request::Mode(_mode_req) => {
|
||||
warn!("mode request handling not implemented yet")
|
||||
}
|
||||
Request::Action(_action_req) => {
|
||||
warn!("action request handling not implemented yet")
|
||||
}
|
||||
}
|
||||
let started_token = self
|
||||
.verif_reporter
|
||||
.start_success(request.token, Some(&self.timestamp))
|
||||
.expect("Sending start success failed");
|
||||
self.verif_reporter
|
||||
.completion_success(started_token, Some(&self.timestamp))
|
||||
.expect("Sending completion success failed");
|
||||
true
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => false,
|
||||
TryRecvError::Disconnected => {
|
||||
warn!("ACS thread: Message Queue TX disconnected!");
|
||||
false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
loop {
|
||||
if !self.try_reading_one_request() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,10 @@ use satrs_core::pus::verification::RequestId;
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcCreator;
|
||||
use satrs_core::spacepackets::ecss::tm::PusTmReader;
|
||||
use satrs_core::{
|
||||
spacepackets::ecss::{PusPacket, SerializablePusPacket},
|
||||
spacepackets::ecss::{PusPacket, WritablePusPacket},
|
||||
spacepackets::SpHeader,
|
||||
};
|
||||
use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||
use std::net::{IpAddr, SocketAddr, UdpSocket};
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -1,14 +1,22 @@
|
||||
use crate::tmtc::{MpscStoreAndSendError, PusTcSource};
|
||||
use satrs_core::pus::ReceivesEcssPusTc;
|
||||
use satrs_core::spacepackets::{CcsdsPacket, SpHeader};
|
||||
use satrs_core::tmtc::{CcsdsPacketHandler, ReceivesCcsdsTc};
|
||||
use satrs_example::PUS_APID;
|
||||
use satrs_example::config::PUS_APID;
|
||||
|
||||
pub struct CcsdsReceiver {
|
||||
pub tc_source: PusTcSource,
|
||||
#[derive(Clone)]
|
||||
pub struct CcsdsReceiver<
|
||||
TcSource: ReceivesCcsdsTc<Error = E> + ReceivesEcssPusTc<Error = E> + Clone,
|
||||
E,
|
||||
> {
|
||||
pub tc_source: TcSource,
|
||||
}
|
||||
|
||||
impl CcsdsPacketHandler for CcsdsReceiver {
|
||||
type Error = MpscStoreAndSendError;
|
||||
impl<
|
||||
TcSource: ReceivesCcsdsTc<Error = E> + ReceivesEcssPusTc<Error = E> + Clone + 'static,
|
||||
E: 'static,
|
||||
> CcsdsPacketHandler for CcsdsReceiver<TcSource, E>
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
fn valid_apids(&self) -> &'static [u16] {
|
||||
&[PUS_APID]
|
||||
|
142
satrs-example/src/config.rs
Normal file
142
satrs-example/src/config.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use satrs_core::res_code::ResultU16;
|
||||
use satrs_mib::res_code::ResultU16Info;
|
||||
use satrs_mib::resultcode;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs_core::{
|
||||
events::{EventU32TypedSev, SeverityInfo},
|
||||
pool::{StaticMemoryPool, StaticPoolConfig},
|
||||
};
|
||||
|
||||
pub const PUS_APID: u16 = 0x02;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum CustomPusServiceId {
|
||||
Mode = 200,
|
||||
Health = 201,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum RequestTargetId {
|
||||
AcsSubsystem = 1,
|
||||
}
|
||||
|
||||
pub const AOCS_APID: u16 = 1;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GroupId {
|
||||
Tmtc = 0,
|
||||
Hk = 1,
|
||||
}
|
||||
|
||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||
pub const SERVER_PORT: u16 = 7301;
|
||||
|
||||
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
|
||||
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
|
||||
|
||||
pub mod tmtc_err {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 0);
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 1);
|
||||
#[resultcode]
|
||||
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 3);
|
||||
|
||||
#[resultcode(
|
||||
info = "Not enough data inside the TC application data field. Optionally includes: \
|
||||
8 bytes of failure data containing 2 failure parameters, \
|
||||
P1 (u32 big endian): Expected data length, P2: Found data length"
|
||||
)]
|
||||
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
|
||||
|
||||
pub const TMTC_RESULTS: &[ResultU16Info] = &[
|
||||
INVALID_PUS_SERVICE_EXT,
|
||||
INVALID_PUS_SUBSERVICE_EXT,
|
||||
NOT_ENOUGH_APP_DATA_EXT,
|
||||
];
|
||||
}
|
||||
|
||||
pub mod hk_err {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 0);
|
||||
#[resultcode]
|
||||
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 1);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 2);
|
||||
#[resultcode]
|
||||
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 3);
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TmSenderId {
|
||||
PusVerification = 0,
|
||||
PusTest = 1,
|
||||
PusEvent = 2,
|
||||
PusHk = 3,
|
||||
PusAction = 4,
|
||||
PusSched = 5,
|
||||
AllEvents = 6,
|
||||
AcsSubsystem = 7,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TcReceiverId {
|
||||
PusTest = 1,
|
||||
PusEvent = 2,
|
||||
PusHk = 3,
|
||||
PusAction = 4,
|
||||
PusSched = 5,
|
||||
}
|
||||
pub mod pool {
|
||||
use super::*;
|
||||
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
|
||||
(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
])),
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
])),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_sched_tc_pool() -> StaticMemoryPool {
|
||||
StaticMemoryPool::new(StaticPoolConfig::new(vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod tasks {
|
||||
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
||||
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
|
||||
pub const FREQ_MS_AOCS: u64 = 500;
|
||||
pub const FREQ_MS_PUS_STACK: u64 = 200;
|
||||
}
|
187
satrs-example/src/events.rs
Normal file
187
satrs-example/src/events.rs
Normal file
@ -0,0 +1,187 @@
|
||||
use std::sync::mpsc::{self, SendError};
|
||||
|
||||
use satrs_core::{
|
||||
event_man::{
|
||||
EventManager, EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider,
|
||||
SendEventProvider,
|
||||
},
|
||||
events::EventU32,
|
||||
params::Params,
|
||||
pus::{
|
||||
event_man::{
|
||||
DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken,
|
||||
PusEventDispatcher,
|
||||
},
|
||||
verification::{TcStateStarted, VerificationReporterWithSender, VerificationToken},
|
||||
EcssTmSender,
|
||||
},
|
||||
spacepackets::time::cds::{self, TimeProvider},
|
||||
};
|
||||
use satrs_example::config::PUS_APID;
|
||||
|
||||
use crate::update_time;
|
||||
|
||||
pub type MpscEventManager = EventManager<SendError<(EventU32, Option<Params>)>>;
|
||||
|
||||
pub struct PusEventHandler {
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
pus_event_dispatcher: PusEventDispatcher<(), EventU32>,
|
||||
pus_event_man_rx: mpsc::Receiver<(EventU32, Option<Params>)>,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
time_provider: TimeProvider,
|
||||
timestamp: [u8; 7],
|
||||
verif_handler: VerificationReporterWithSender,
|
||||
}
|
||||
/*
|
||||
*/
|
||||
|
||||
impl PusEventHandler {
|
||||
pub fn new(
|
||||
verif_handler: VerificationReporterWithSender,
|
||||
event_manager: &mut MpscEventManager,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
tm_sender: impl EcssTmSender,
|
||||
) -> Self {
|
||||
let (pus_event_man_tx, pus_event_man_rx) = mpsc::channel();
|
||||
|
||||
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
|
||||
// telemetry for each event.
|
||||
let event_reporter = EventReporter::new(PUS_APID, 128).unwrap();
|
||||
let pus_tm_backend = DefaultPusMgmtBackendProvider::<EventU32>::default();
|
||||
let pus_event_dispatcher =
|
||||
PusEventDispatcher::new(event_reporter, Box::new(pus_tm_backend));
|
||||
let pus_event_man_send_provider = MpscEventU32SendProvider::new(1, pus_event_man_tx);
|
||||
|
||||
event_manager.subscribe_all(pus_event_man_send_provider.id());
|
||||
event_manager.add_sender(pus_event_man_send_provider);
|
||||
|
||||
Self {
|
||||
event_request_rx,
|
||||
pus_event_dispatcher,
|
||||
pus_event_man_rx,
|
||||
time_provider: cds::TimeProvider::new_with_u16_days(0, 0),
|
||||
timestamp: [0; 7],
|
||||
verif_handler,
|
||||
tm_sender: Box::new(tm_sender),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event_requests(&mut self) {
|
||||
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
|
||||
let started_token: VerificationToken<TcStateStarted> = event_req
|
||||
.token
|
||||
.try_into()
|
||||
.expect("expected start verification token");
|
||||
self.verif_handler
|
||||
.completion_success(started_token, Some(timestamp))
|
||||
.expect("Sending completion success failed");
|
||||
};
|
||||
// handle event requests
|
||||
if let Ok(event_req) = self.event_request_rx.try_recv() {
|
||||
match event_req.request {
|
||||
EventRequest::Enable(event) => {
|
||||
self.pus_event_dispatcher
|
||||
.enable_tm_for_event(&event)
|
||||
.expect("Enabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
EventRequest::Disable(event) => {
|
||||
self.pus_event_dispatcher
|
||||
.disable_tm_for_event(&event)
|
||||
.expect("Disabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_pus_event_tm(&mut self) {
|
||||
// Perform the generation of PUS event packets
|
||||
if let Ok((event, _param)) = self.pus_event_man_rx.try_recv() {
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
self.pus_event_dispatcher
|
||||
.generate_pus_event_tm_generic(
|
||||
self.tm_sender.upcast_mut(),
|
||||
&self.timestamp,
|
||||
event,
|
||||
None,
|
||||
)
|
||||
.expect("Sending TM as event failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventManagerWrapper {
|
||||
event_manager: MpscEventManager,
|
||||
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
|
||||
}
|
||||
|
||||
impl EventManagerWrapper {
|
||||
pub fn new() -> Self {
|
||||
// The sender handle is the primary sender handle for all components which want to create events.
|
||||
// The event manager will receive the RX handle to receive all the events.
|
||||
let (event_sender, event_man_rx) = mpsc::channel();
|
||||
let event_recv = MpscEventReceiver::<EventU32>::new(event_man_rx);
|
||||
Self {
|
||||
event_manager: EventManagerWithMpscQueue::new(Box::new(event_recv)),
|
||||
event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_event_sender(&self) -> mpsc::Sender<(EventU32, Option<Params>)> {
|
||||
self.event_sender.clone()
|
||||
}
|
||||
|
||||
pub fn event_manager(&mut self) -> &mut MpscEventManager {
|
||||
&mut self.event_manager
|
||||
}
|
||||
|
||||
pub fn try_event_routing(&mut self) {
|
||||
// Perform the event routing.
|
||||
self.event_manager
|
||||
.try_event_handling()
|
||||
.expect("event handling failed");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventHandler {
|
||||
pub event_man_wrapper: EventManagerWrapper,
|
||||
pub pus_event_handler: PusEventHandler,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(
|
||||
tm_sender: impl EcssTmSender,
|
||||
verif_handler: VerificationReporterWithSender,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
let mut event_man_wrapper = EventManagerWrapper::new();
|
||||
let pus_event_handler = PusEventHandler::new(
|
||||
verif_handler,
|
||||
event_man_wrapper.event_manager(),
|
||||
event_request_rx,
|
||||
tm_sender,
|
||||
);
|
||||
Self {
|
||||
event_man_wrapper,
|
||||
pus_event_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_event_sender(&self) -> mpsc::Sender<(EventU32, Option<Params>)> {
|
||||
self.event_man_wrapper.clone_event_sender()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn event_manager(&mut self) -> &mut MpscEventManager {
|
||||
self.event_man_wrapper.event_manager()
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.pus_event_handler.handle_event_requests();
|
||||
self.event_man_wrapper.try_event_routing();
|
||||
self.pus_event_handler.generate_pus_event_tm();
|
||||
}
|
||||
}
|
@ -1,4 +1,37 @@
|
||||
use derive_new::new;
|
||||
use satrs_core::spacepackets::ByteConversionError;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum AcsHkIds {
|
||||
TestMgmSet = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, new, Copy, Clone)]
|
||||
pub struct HkUniqueId {
|
||||
target_id: u32,
|
||||
set_id: u32,
|
||||
}
|
||||
|
||||
impl HkUniqueId {
|
||||
#[allow(dead_code)]
|
||||
pub fn target_id(&self) -> u32 {
|
||||
self.target_id
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn set_id(&self) -> u32 {
|
||||
self.set_id
|
||||
}
|
||||
|
||||
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
|
||||
if buf.len() < 8 {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: 8,
|
||||
});
|
||||
}
|
||||
buf[0..4].copy_from_slice(&self.target_id.to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
|
||||
|
||||
Ok(8)
|
||||
}
|
||||
}
|
||||
|
@ -1,98 +1,59 @@
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs_core::events::{EventU32TypedSev, SeverityInfo};
|
||||
use satrs_core::objects::ObjectId;
|
||||
use std::net::Ipv4Addr;
|
||||
use derive_new::new;
|
||||
use satrs_core::spacepackets::ecss::tc::IsPusTelecommand;
|
||||
use satrs_core::spacepackets::ecss::PusPacket;
|
||||
use satrs_core::spacepackets::{ByteConversionError, CcsdsPacket};
|
||||
use satrs_core::tmtc::TargetId;
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
use satrs_mib::res_code::{ResultU16, ResultU16Info};
|
||||
use satrs_mib::resultcode;
|
||||
pub mod config;
|
||||
|
||||
pub const PUS_APID: u16 = 0x02;
|
||||
pub type Apid = u16;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum CustomPusServiceId {
|
||||
Mode = 200,
|
||||
Health = 201,
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TargetIdCreationError {
|
||||
#[error("byte conversion")]
|
||||
ByteConversion(#[from] ByteConversionError),
|
||||
#[error("not enough app data to generate target ID")]
|
||||
NotEnoughAppData(usize),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum RequestTargetId {
|
||||
AcsSubsystem = 1,
|
||||
// TODO: can these stay pub?
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, new)]
|
||||
pub struct TargetIdWithApid {
|
||||
pub apid: Apid,
|
||||
pub target: TargetId,
|
||||
}
|
||||
|
||||
pub const ACS_OBJECT_ID: ObjectId = ObjectId {
|
||||
id: RequestTargetId::AcsSubsystem as u32,
|
||||
name: "ACS_SUBSYSTEM",
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GroupId {
|
||||
Tmtc = 0,
|
||||
Hk = 1,
|
||||
impl fmt::Display for TargetIdWithApid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.apid, self.target)
|
||||
}
|
||||
}
|
||||
|
||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||
pub const SERVER_PORT: u16 = 7301;
|
||||
|
||||
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
|
||||
EventU32TypedSev::<SeverityInfo>::const_new(0, 0);
|
||||
|
||||
pub mod tmtc_err {
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 0);
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 1);
|
||||
#[resultcode]
|
||||
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 3);
|
||||
|
||||
#[resultcode(
|
||||
info = "Not enough data inside the TC application data field. Optionally includes: \
|
||||
8 bytes of failure data containing 2 failure parameters, \
|
||||
P1 (u32 big endian): Expected data length, P2: Found data length"
|
||||
)]
|
||||
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::const_new(GroupId::Tmtc as u8, 2);
|
||||
|
||||
pub const TMTC_RESULTS: &[ResultU16Info] = &[
|
||||
INVALID_PUS_SERVICE_EXT,
|
||||
INVALID_PUS_SUBSERVICE_EXT,
|
||||
NOT_ENOUGH_APP_DATA_EXT,
|
||||
];
|
||||
impl TargetIdWithApid {
|
||||
pub fn apid(&self) -> Apid {
|
||||
self.apid
|
||||
}
|
||||
pub fn target_id(&self) -> TargetId {
|
||||
self.target
|
||||
}
|
||||
}
|
||||
|
||||
pub mod hk_err {
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 0);
|
||||
#[resultcode]
|
||||
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 1);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 2);
|
||||
#[resultcode]
|
||||
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::const_new(GroupId::Hk as u8, 3);
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TmSenderId {
|
||||
PusVerification = 0,
|
||||
PusTest = 1,
|
||||
PusEvent = 2,
|
||||
PusHk = 3,
|
||||
PusAction = 4,
|
||||
PusSched = 5,
|
||||
AllEvents = 6,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TcReceiverId {
|
||||
PusTest = 1,
|
||||
PusEvent = 2,
|
||||
PusHk = 3,
|
||||
PusAction = 4,
|
||||
PusSched = 5,
|
||||
impl TargetIdWithApid {
|
||||
pub fn from_tc(
|
||||
tc: &(impl CcsdsPacket + PusPacket + IsPusTelecommand),
|
||||
) -> Result<Self, TargetIdCreationError> {
|
||||
if tc.user_data().len() < 4 {
|
||||
return Err(ByteConversionError::FromSliceTooSmall {
|
||||
found: tc.user_data().len(),
|
||||
expected: 8,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
Ok(Self {
|
||||
apid: tc.apid(),
|
||||
target: u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,14 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(), //(std::thread::current().name().expect("unnamed_thread"),
|
||||
std::thread::current().name().expect("unnamed_thread"),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
@ -1,149 +1,117 @@
|
||||
mod acs;
|
||||
mod ccsds;
|
||||
mod events;
|
||||
mod hk;
|
||||
mod logging;
|
||||
mod logger;
|
||||
mod pus;
|
||||
mod requests;
|
||||
mod tcp;
|
||||
mod tm_funnel;
|
||||
mod tmtc;
|
||||
mod udp;
|
||||
|
||||
use log::{info, warn};
|
||||
use crate::events::EventHandler;
|
||||
use crate::pus::stack::PusStack;
|
||||
use crate::tm_funnel::{TmFunnelDynamic, TmFunnelStatic};
|
||||
use log::info;
|
||||
use pus::test::create_test_service_dynamic;
|
||||
use satrs_core::hal::std::tcp_server::ServerConfig;
|
||||
use satrs_core::hal::std::udp_server::UdpTcServer;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_example::config::pool::{create_sched_tc_pool, create_static_pools};
|
||||
use satrs_example::config::tasks::{
|
||||
FREQ_MS_AOCS, FREQ_MS_EVENT_HANDLING, FREQ_MS_PUS_STACK, FREQ_MS_UDP_TMTC,
|
||||
};
|
||||
use satrs_example::config::{RequestTargetId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID, SERVER_PORT};
|
||||
use tmtc::PusTcSourceProviderDynamic;
|
||||
use udp::DynamicUdpTmHandler;
|
||||
|
||||
use crate::hk::AcsHkIds;
|
||||
use crate::logging::setup_logger;
|
||||
use crate::pus::action::{Pus8Wrapper, PusService8ActionHandler};
|
||||
use crate::pus::event::Pus5Wrapper;
|
||||
use crate::pus::hk::{Pus3Wrapper, PusService3HkHandler};
|
||||
use crate::pus::scheduler::Pus11Wrapper;
|
||||
use crate::pus::test::Service17CustomWrapper;
|
||||
use crate::pus::PusTcMpscRouter;
|
||||
use crate::requests::{Request, RequestWithToken};
|
||||
use crate::tmtc::{core_tmtc_task, PusTcSource, TcArgs, TcStore, TmArgs, TmFunnel};
|
||||
use satrs_core::event_man::{
|
||||
EventManagerWithMpscQueue, MpscEventReceiver, MpscEventU32SendProvider, SendEventProvider,
|
||||
use crate::acs::AcsTask;
|
||||
use crate::ccsds::CcsdsReceiver;
|
||||
use crate::logger::setup_logger;
|
||||
use crate::pus::action::{create_action_service_dynamic, create_action_service_static};
|
||||
use crate::pus::event::{create_event_service_dynamic, create_event_service_static};
|
||||
use crate::pus::hk::{create_hk_service_dynamic, create_hk_service_static};
|
||||
use crate::pus::scheduler::{create_scheduler_service_dynamic, create_scheduler_service_static};
|
||||
use crate::pus::test::create_test_service_static;
|
||||
use crate::pus::{PusReceiver, PusTcMpscRouter};
|
||||
use crate::requests::RequestWithToken;
|
||||
use crate::tcp::{SyncTcpTmSource, TcpTask};
|
||||
use crate::tmtc::{
|
||||
PusTcSourceProviderSharedPool, SharedTcPool, TcSourceTaskDynamic, TcSourceTaskStatic,
|
||||
};
|
||||
use satrs_core::events::EventU32;
|
||||
use satrs_core::hk::HkRequest;
|
||||
use satrs_core::pool::{LocalPool, PoolCfg};
|
||||
use satrs_core::pus::event_man::{
|
||||
DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken,
|
||||
PusEventDispatcher,
|
||||
};
|
||||
use satrs_core::pus::event_srv::PusService5EventHandler;
|
||||
use satrs_core::pus::hk::Subservice as HkSubservice;
|
||||
use satrs_core::pus::scheduler::PusScheduler;
|
||||
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
|
||||
use satrs_core::pus::test::PusService17TestHandler;
|
||||
use satrs_core::pus::verification::{
|
||||
TcStateStarted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken,
|
||||
};
|
||||
use satrs_core::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender};
|
||||
use satrs_core::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore};
|
||||
use satrs_core::spacepackets::ecss::tm::{PusTmCreator, PusTmZeroCopyWriter};
|
||||
use satrs_core::spacepackets::{
|
||||
ecss::tm::PusTmSecondaryHeader, time::cds::TimeProvider, time::TimeWriter, SequenceFlags,
|
||||
SpHeader,
|
||||
};
|
||||
use satrs_core::tmtc::tm_helper::SharedTmStore;
|
||||
use satrs_core::tmtc::{AddressableId, TargetId};
|
||||
use crate::udp::{StaticUdpTmHandler, UdpTmtcServer};
|
||||
use satrs_core::pus::event_man::EventRequestWithToken;
|
||||
use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender};
|
||||
use satrs_core::pus::{EcssTmSender, MpscTmAsVecSender, MpscTmInSharedPoolSender};
|
||||
use satrs_core::spacepackets::{time::cds::TimeProvider, time::TimeWriter};
|
||||
use satrs_core::tmtc::{CcsdsDistributor, TargetId};
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_example::{
|
||||
RequestTargetId, TcReceiverId, TmSenderId, OBSW_SERVER_ADDR, PUS_APID, SERVER_PORT,
|
||||
};
|
||||
use satrs_example::TargetIdWithApid;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::mpsc::{channel, TryRecvError};
|
||||
use std::sync::mpsc::{self, channel};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
setup_logger().expect("setting up logging with fern failed");
|
||||
println!("Running OBSW example");
|
||||
let tm_pool = LocalPool::new(PoolCfg::new(vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
]));
|
||||
let shared_tm_store = SharedTmStore::new(Box::new(tm_pool));
|
||||
let tm_store_event = shared_tm_store.clone();
|
||||
let tc_pool = LocalPool::new(PoolCfg::new(vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
]));
|
||||
let tc_store = TcStore {
|
||||
pool: Arc::new(RwLock::new(Box::new(tc_pool))),
|
||||
};
|
||||
|
||||
let seq_count_provider = CcsdsSimpleSeqCountProvider::new();
|
||||
let mut msg_counter_map: HashMap<u8, u16> = HashMap::new();
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let (tc_source_tx, tc_source_rx) = channel();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = channel();
|
||||
let (tm_server_tx, tm_server_rx) = channel();
|
||||
let verif_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusVerification as ChannelId,
|
||||
"verif_sender",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
fn create_verification_reporter(verif_sender: impl EcssTmSender) -> VerificationReporterWithSender {
|
||||
let verif_cfg = VerificationReporterCfg::new(PUS_APID, 1, 2, 8).unwrap();
|
||||
// Every software component which needs to generate verification telemetry, gets a cloned
|
||||
// verification reporter.
|
||||
let verif_reporter = VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender));
|
||||
let reporter_event_handler = verif_reporter.clone();
|
||||
let reporter_aocs = verif_reporter.clone();
|
||||
VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender))
|
||||
}
|
||||
|
||||
// Create event handling components
|
||||
// These sender handles are used to send event requests, for example to enable or disable
|
||||
// certain events
|
||||
let (event_request_tx, event_request_rx) = channel::<EventRequestWithToken>();
|
||||
// The sender handle is the primary sender handle for all components which want to create events.
|
||||
// The event manager will receive the RX handle to receive all the events.
|
||||
let (event_sender, event_man_rx) = channel();
|
||||
let event_recv = MpscEventReceiver::<EventU32>::new(event_man_rx);
|
||||
let test_srv_event_sender = event_sender;
|
||||
let mut event_man = EventManagerWithMpscQueue::new(Box::new(event_recv));
|
||||
#[allow(dead_code)]
|
||||
fn static_tmtc_pool_main() {
|
||||
let (tm_pool, tc_pool) = create_static_pools();
|
||||
let shared_tm_pool = SharedTmPool::new(tm_pool);
|
||||
let shared_tc_pool = SharedTcPool {
|
||||
pool: Arc::new(RwLock::new(tc_pool)),
|
||||
};
|
||||
let (tc_source_tx, tc_source_rx) = channel();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = channel();
|
||||
let (tm_server_tx, tm_server_rx) = channel();
|
||||
|
||||
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
|
||||
// telemetry for each event.
|
||||
let event_reporter = EventReporter::new(PUS_APID, 128).unwrap();
|
||||
let pus_tm_backend = DefaultPusMgmtBackendProvider::<EventU32>::default();
|
||||
let mut pus_event_dispatcher =
|
||||
PusEventDispatcher::new(event_reporter, Box::new(pus_tm_backend));
|
||||
let (pus_event_man_tx, pus_event_man_rx) = channel();
|
||||
let pus_event_man_send_provider = MpscEventU32SendProvider::new(1, pus_event_man_tx);
|
||||
event_man.subscribe_all(pus_event_man_send_provider.id());
|
||||
event_man.add_sender(pus_event_man_send_provider);
|
||||
// Every software component which needs to generate verification telemetry, receives a cloned
|
||||
// verification reporter.
|
||||
let verif_reporter = create_verification_reporter(MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusVerification as ChannelId,
|
||||
"verif_sender",
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
));
|
||||
|
||||
let acs_target_id = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
|
||||
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
|
||||
// Some request are targetable. This map is used to retrieve sender handles based on a target ID.
|
||||
let mut request_map = HashMap::new();
|
||||
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
|
||||
request_map.insert(RequestTargetId::AcsSubsystem as TargetId, acs_thread_tx);
|
||||
request_map.insert(acs_target_id, acs_thread_tx);
|
||||
|
||||
let tc_source_wrapper = PusTcSource {
|
||||
tc_store: tc_store.clone(),
|
||||
// This helper structure is used by all telecommand providers which need to send telecommands
|
||||
// to the TC source.
|
||||
let tc_source = PusTcSourceProviderSharedPool {
|
||||
shared_pool: shared_tc_pool.clone(),
|
||||
tc_source: tc_source_tx,
|
||||
};
|
||||
|
||||
// Create clones here to allow moving the values
|
||||
let tc_args = TcArgs {
|
||||
tc_source: tc_source_wrapper.clone(),
|
||||
tc_receiver: tc_source_rx,
|
||||
};
|
||||
let tm_args = TmArgs {
|
||||
tm_store: shared_tm_store.clone(),
|
||||
tm_sink_sender: tm_funnel_tx.clone(),
|
||||
tm_server_rx,
|
||||
};
|
||||
// Create event handling components
|
||||
// These sender handles are used to send event requests, for example to enable or disable
|
||||
// certain events.
|
||||
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
|
||||
|
||||
let aocs_tm_funnel = tm_funnel_tx.clone();
|
||||
let aocs_tm_store = shared_tm_store.clone();
|
||||
// The event task is the core handler to perform the event routing and TM handling as specified
|
||||
// in the sat-rs documentation.
|
||||
let mut event_handler = EventHandler::new(
|
||||
MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::AllEvents as ChannelId,
|
||||
"ALL_EVENTS_TX",
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
),
|
||||
verif_reporter.clone(),
|
||||
event_request_rx,
|
||||
);
|
||||
|
||||
let (pus_test_tx, pus_test_rx) = channel();
|
||||
let (pus_event_tx, pus_event_rx) = channel();
|
||||
@ -157,335 +125,391 @@ fn main() {
|
||||
hk_service_receiver: pus_hk_tx,
|
||||
action_service_receiver: pus_action_tx,
|
||||
};
|
||||
let test_srv_tm_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusTest as ChannelId,
|
||||
"PUS_17_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
let pus_test_service = create_test_service_static(
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let test_srv_receiver = MpscTcInStoreReceiver::new(
|
||||
TcReceiverId::PusTest as ChannelId,
|
||||
"PUS_17_TC_RECV",
|
||||
verif_reporter.clone(),
|
||||
shared_tc_pool.pool.clone(),
|
||||
event_handler.clone_event_sender(),
|
||||
pus_test_rx,
|
||||
);
|
||||
let pus17_handler = PusService17TestHandler::new(
|
||||
Box::new(test_srv_receiver),
|
||||
tc_store.pool.clone(),
|
||||
Box::new(test_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
);
|
||||
let mut pus_17_wrapper = Service17CustomWrapper {
|
||||
pus17_handler,
|
||||
test_srv_event_sender,
|
||||
};
|
||||
|
||||
let sched_srv_tm_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusSched as ChannelId,
|
||||
"PUS_11_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
let pus_scheduler_service = create_scheduler_service_static(
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let sched_srv_receiver = MpscTcInStoreReceiver::new(
|
||||
TcReceiverId::PusSched as ChannelId,
|
||||
"PUS_11_TC_RECV",
|
||||
verif_reporter.clone(),
|
||||
tc_source.clone(),
|
||||
pus_sched_rx,
|
||||
create_sched_tc_pool(),
|
||||
);
|
||||
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
|
||||
.expect("Creating PUS Scheduler failed");
|
||||
let pus_11_handler = PusService11SchedHandler::new(
|
||||
Box::new(sched_srv_receiver),
|
||||
tc_store.pool.clone(),
|
||||
Box::new(sched_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
scheduler,
|
||||
);
|
||||
let mut pus_11_wrapper = Pus11Wrapper {
|
||||
pus_11_handler,
|
||||
tc_source_wrapper,
|
||||
};
|
||||
|
||||
let event_srv_tm_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusEvent as ChannelId,
|
||||
"PUS_5_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
let pus_event_service = create_event_service_static(
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let event_srv_receiver = MpscTcInStoreReceiver::new(
|
||||
TcReceiverId::PusEvent as ChannelId,
|
||||
"PUS_5_TC_RECV",
|
||||
pus_event_rx,
|
||||
);
|
||||
let pus_5_handler = PusService5EventHandler::new(
|
||||
Box::new(event_srv_receiver),
|
||||
tc_store.pool.clone(),
|
||||
Box::new(event_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
shared_tc_pool.pool.clone(),
|
||||
pus_event_rx,
|
||||
event_request_tx,
|
||||
);
|
||||
let mut pus_5_wrapper = Pus5Wrapper { pus_5_handler };
|
||||
|
||||
let action_srv_tm_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusAction as ChannelId,
|
||||
"PUS_8_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
let pus_action_service = create_action_service_static(
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let action_srv_receiver = MpscTcInStoreReceiver::new(
|
||||
TcReceiverId::PusAction as ChannelId,
|
||||
"PUS_8_TC_RECV",
|
||||
pus_action_rx,
|
||||
);
|
||||
let pus_8_handler = PusService8ActionHandler::new(
|
||||
Box::new(action_srv_receiver),
|
||||
tc_store.pool.clone(),
|
||||
Box::new(action_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
shared_tc_pool.pool.clone(),
|
||||
pus_action_rx,
|
||||
request_map.clone(),
|
||||
);
|
||||
let mut pus_8_wrapper = Pus8Wrapper { pus_8_handler };
|
||||
|
||||
let hk_srv_tm_sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::PusHk as ChannelId,
|
||||
"PUS_3_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
let pus_hk_service = create_hk_service_static(
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let hk_srv_receiver =
|
||||
MpscTcInStoreReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
|
||||
let pus_3_handler = PusService3HkHandler::new(
|
||||
Box::new(hk_srv_receiver),
|
||||
tc_store.pool.clone(),
|
||||
Box::new(hk_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
shared_tc_pool.pool.clone(),
|
||||
pus_hk_rx,
|
||||
request_map,
|
||||
);
|
||||
let mut pus_3_wrapper = Pus3Wrapper { pus_3_handler };
|
||||
let mut pus_stack = PusStack::new(
|
||||
pus_hk_service,
|
||||
pus_event_service,
|
||||
pus_action_service,
|
||||
pus_scheduler_service,
|
||||
pus_test_service,
|
||||
);
|
||||
|
||||
info!("Starting TMTC task");
|
||||
let jh0 = thread::Builder::new()
|
||||
.name("TMTC".to_string())
|
||||
let ccsds_receiver = CcsdsReceiver { tc_source };
|
||||
let mut tmtc_task = TcSourceTaskStatic::new(
|
||||
shared_tc_pool.clone(),
|
||||
tc_source_rx,
|
||||
PusReceiver::new(verif_reporter.clone(), pus_router),
|
||||
);
|
||||
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let udp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver.clone()));
|
||||
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(udp_ccsds_distributor))
|
||||
.expect("creating UDP TMTC server failed");
|
||||
let mut udp_tmtc_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: StaticUdpTmHandler {
|
||||
tm_rx: tm_server_rx,
|
||||
tm_store: shared_tm_pool.clone_backing_pool(),
|
||||
},
|
||||
};
|
||||
|
||||
let tcp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
|
||||
let tcp_server_cfg = ServerConfig::new(sock_addr, Duration::from_millis(400), 4096, 8192);
|
||||
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
|
||||
let mut tcp_server = TcpTask::new(
|
||||
tcp_server_cfg,
|
||||
sync_tm_tcp_source.clone(),
|
||||
tcp_ccsds_distributor,
|
||||
)
|
||||
.expect("tcp server creation failed");
|
||||
|
||||
let mut acs_task = AcsTask::new(
|
||||
MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::AcsSubsystem as ChannelId,
|
||||
"ACS_TASK_SENDER",
|
||||
shared_tm_pool.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
),
|
||||
acs_thread_rx,
|
||||
verif_reporter,
|
||||
);
|
||||
|
||||
let mut tm_funnel = TmFunnelStatic::new(
|
||||
shared_tm_pool,
|
||||
sync_tm_tcp_source,
|
||||
tm_funnel_rx,
|
||||
tm_server_tx,
|
||||
);
|
||||
|
||||
info!("Starting TMTC and UDP task");
|
||||
let jh_udp_tmtc = thread::Builder::new()
|
||||
.name("TMTC and UDP".to_string())
|
||||
.spawn(move || {
|
||||
core_tmtc_task(sock_addr, tc_args, tm_args, verif_reporter, pus_router);
|
||||
info!("Running UDP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
udp_tmtc_server.periodic_operation();
|
||||
tmtc_task.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TCP task");
|
||||
let jh_tcp = thread::Builder::new()
|
||||
.name("TCP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running TCP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
tcp_server.periodic_operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TM funnel task");
|
||||
let jh1 = thread::Builder::new()
|
||||
let jh_tm_funnel = thread::Builder::new()
|
||||
.name("TM Funnel".to_string())
|
||||
.spawn(move || {
|
||||
let tm_funnel = TmFunnel {
|
||||
tm_server_tx,
|
||||
tm_funnel_rx,
|
||||
};
|
||||
loop {
|
||||
if let Ok(addr) = tm_funnel.tm_funnel_rx.recv() {
|
||||
// Read the TM, set sequence counter and message counter, and finally update
|
||||
// the CRC.
|
||||
let shared_pool = shared_tm_store.clone_backing_pool();
|
||||
let mut pool_guard = shared_pool.write().expect("Locking TM pool failed");
|
||||
let tm_raw = pool_guard
|
||||
.modify(&addr)
|
||||
.expect("Reading TM from pool failed");
|
||||
let mut zero_copy_writer = PusTmZeroCopyWriter::new(tm_raw)
|
||||
.expect("Creating TM zero copy writer failed");
|
||||
zero_copy_writer.set_apid(PUS_APID);
|
||||
zero_copy_writer.set_seq_count(seq_count_provider.get_and_increment());
|
||||
let entry = msg_counter_map
|
||||
.entry(zero_copy_writer.service())
|
||||
.or_insert(0);
|
||||
zero_copy_writer.set_msg_count(*entry);
|
||||
if *entry == u16::MAX {
|
||||
*entry = 0;
|
||||
} else {
|
||||
*entry += 1;
|
||||
}
|
||||
|
||||
// This operation has to come last!
|
||||
zero_copy_writer.finish();
|
||||
tm_funnel
|
||||
.tm_server_tx
|
||||
.send(addr)
|
||||
.expect("Sending TM to server failed");
|
||||
}
|
||||
}
|
||||
.spawn(move || loop {
|
||||
tm_funnel.operation();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting event handling task");
|
||||
let jh2 = thread::Builder::new()
|
||||
let jh_event_handling = thread::Builder::new()
|
||||
.name("Event".to_string())
|
||||
.spawn(move || {
|
||||
let mut timestamp: [u8; 7] = [0; 7];
|
||||
let mut sender = MpscTmInStoreSender::new(
|
||||
TmSenderId::AllEvents as ChannelId,
|
||||
"ALL_EVENTS_TX",
|
||||
tm_store_event.clone(),
|
||||
tm_funnel_tx,
|
||||
);
|
||||
let mut time_provider = TimeProvider::new_with_u16_days(0, 0);
|
||||
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
|
||||
let started_token: VerificationToken<TcStateStarted> = event_req
|
||||
.token
|
||||
.try_into()
|
||||
.expect("expected start verification token");
|
||||
reporter_event_handler
|
||||
.completion_success(started_token, Some(timestamp))
|
||||
.expect("Sending completion success failed");
|
||||
};
|
||||
loop {
|
||||
// handle event requests
|
||||
if let Ok(event_req) = event_request_rx.try_recv() {
|
||||
match event_req.request {
|
||||
EventRequest::Enable(event) => {
|
||||
pus_event_dispatcher
|
||||
.enable_tm_for_event(&event)
|
||||
.expect("Enabling TM failed");
|
||||
update_time(&mut time_provider, &mut timestamp);
|
||||
report_completion(event_req, ×tamp);
|
||||
}
|
||||
EventRequest::Disable(event) => {
|
||||
pus_event_dispatcher
|
||||
.disable_tm_for_event(&event)
|
||||
.expect("Disabling TM failed");
|
||||
update_time(&mut time_provider, &mut timestamp);
|
||||
report_completion(event_req, ×tamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the event routing.
|
||||
event_man
|
||||
.try_event_handling()
|
||||
.expect("event handling failed");
|
||||
|
||||
// Perform the generation of PUS event packets
|
||||
if let Ok((event, _param)) = pus_event_man_rx.try_recv() {
|
||||
update_time(&mut time_provider, &mut timestamp);
|
||||
pus_event_dispatcher
|
||||
.generate_pus_event_tm_generic(&mut sender, ×tamp, event, None)
|
||||
.expect("Sending TM as event failed");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
}
|
||||
.spawn(move || loop {
|
||||
event_handler.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting AOCS thread");
|
||||
let jh3 = thread::Builder::new()
|
||||
let jh_aocs = thread::Builder::new()
|
||||
.name("AOCS".to_string())
|
||||
.spawn(move || {
|
||||
let mut timestamp: [u8; 7] = [0; 7];
|
||||
let mut time_provider = TimeProvider::new_with_u16_days(0, 0);
|
||||
loop {
|
||||
match acs_thread_rx.try_recv() {
|
||||
Ok(request) => {
|
||||
info!(
|
||||
"ACS thread: Received HK request {:?}",
|
||||
request.targeted_request
|
||||
);
|
||||
update_time(&mut time_provider, &mut timestamp);
|
||||
match request.targeted_request.request {
|
||||
Request::Hk(hk_req) => match hk_req {
|
||||
HkRequest::OneShot(unique_id) => {
|
||||
let target = request.targeted_request.target_id;
|
||||
assert_eq!(target, RequestTargetId::AcsSubsystem as u32);
|
||||
if request.targeted_request.target_id
|
||||
== AcsHkIds::TestMgmSet as u32
|
||||
{
|
||||
let mut sp_header = SpHeader::tm(
|
||||
PUS_APID,
|
||||
SequenceFlags::Unsegmented,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
let sec_header = PusTmSecondaryHeader::new_simple(
|
||||
3,
|
||||
HkSubservice::TmHkPacket as u8,
|
||||
×tamp,
|
||||
);
|
||||
let mut buf: [u8; 8] = [0; 8];
|
||||
let addressable_id = AddressableId {
|
||||
target_id: target,
|
||||
unique_id,
|
||||
};
|
||||
addressable_id.write_to_be_bytes(&mut buf).unwrap();
|
||||
let pus_tm = PusTmCreator::new(
|
||||
&mut sp_header,
|
||||
sec_header,
|
||||
Some(&buf),
|
||||
true,
|
||||
);
|
||||
let addr = aocs_tm_store
|
||||
.add_pus_tm(&pus_tm)
|
||||
.expect("Adding PUS TM failed");
|
||||
aocs_tm_funnel.send(addr).expect("Sending HK TM failed");
|
||||
}
|
||||
}
|
||||
HkRequest::Enable(_) => {}
|
||||
HkRequest::Disable(_) => {}
|
||||
HkRequest::ModifyCollectionInterval(_, _) => {}
|
||||
},
|
||||
Request::Mode(_mode_req) => {
|
||||
warn!("mode request handling not implemented yet")
|
||||
}
|
||||
Request::Action(_action_req) => {
|
||||
warn!("action request handling not implemented yet")
|
||||
}
|
||||
}
|
||||
let started_token = reporter_aocs
|
||||
.start_success(request.token, Some(×tamp))
|
||||
.expect("Sending start success failed");
|
||||
reporter_aocs
|
||||
.completion_success(started_token, Some(×tamp))
|
||||
.expect("Sending completion success failed");
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
warn!("ACS thread: Message Queue TX disconnected!")
|
||||
}
|
||||
},
|
||||
}
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
.spawn(move || loop {
|
||||
acs_task.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting PUS handler thread");
|
||||
let jh4 = thread::Builder::new()
|
||||
let jh_pus_handler = thread::Builder::new()
|
||||
.name("PUS".to_string())
|
||||
.spawn(move || loop {
|
||||
pus_11_wrapper.release_tcs();
|
||||
loop {
|
||||
let mut all_queues_empty = true;
|
||||
let mut is_srv_finished = |srv_handler_finished: bool| {
|
||||
if !srv_handler_finished {
|
||||
all_queues_empty = false;
|
||||
}
|
||||
};
|
||||
is_srv_finished(pus_17_wrapper.handle_next_packet());
|
||||
is_srv_finished(pus_11_wrapper.handle_next_packet());
|
||||
is_srv_finished(pus_5_wrapper.handle_next_packet());
|
||||
is_srv_finished(pus_8_wrapper.handle_next_packet());
|
||||
is_srv_finished(pus_3_wrapper.handle_next_packet());
|
||||
if all_queues_empty {
|
||||
break;
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
pus_stack.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
|
||||
})
|
||||
.unwrap();
|
||||
jh0.join().expect("Joining UDP TMTC server thread failed");
|
||||
jh1.join().expect("Joining TM Funnel thread failed");
|
||||
jh2.join().expect("Joining Event Manager thread failed");
|
||||
jh3.join().expect("Joining AOCS thread failed");
|
||||
jh4.join().expect("Joining PUS handler thread failed");
|
||||
|
||||
jh_udp_tmtc
|
||||
.join()
|
||||
.expect("Joining UDP TMTC server thread failed");
|
||||
jh_tcp
|
||||
.join()
|
||||
.expect("Joining TCP TMTC server thread failed");
|
||||
jh_tm_funnel
|
||||
.join()
|
||||
.expect("Joining TM Funnel thread failed");
|
||||
jh_event_handling
|
||||
.join()
|
||||
.expect("Joining Event Manager thread failed");
|
||||
jh_aocs.join().expect("Joining AOCS thread failed");
|
||||
jh_pus_handler
|
||||
.join()
|
||||
.expect("Joining PUS handler thread failed");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn dyn_tmtc_pool_main() {
|
||||
let (tc_source_tx, tc_source_rx) = channel();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = channel();
|
||||
let (tm_server_tx, tm_server_rx) = channel();
|
||||
// Every software component which needs to generate verification telemetry, gets a cloned
|
||||
// verification reporter.
|
||||
let verif_reporter = create_verification_reporter(MpscTmAsVecSender::new(
|
||||
TmSenderId::PusVerification as ChannelId,
|
||||
"verif_sender",
|
||||
tm_funnel_tx.clone(),
|
||||
));
|
||||
|
||||
let acs_target_id = TargetIdWithApid::new(PUS_APID, RequestTargetId::AcsSubsystem as TargetId);
|
||||
let (acs_thread_tx, acs_thread_rx) = channel::<RequestWithToken>();
|
||||
// Some request are targetable. This map is used to retrieve sender handles based on a target ID.
|
||||
let mut request_map = HashMap::new();
|
||||
request_map.insert(acs_target_id, acs_thread_tx);
|
||||
|
||||
let tc_source = PusTcSourceProviderDynamic(tc_source_tx);
|
||||
|
||||
// Create event handling components
|
||||
// These sender handles are used to send event requests, for example to enable or disable
|
||||
// certain events.
|
||||
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
|
||||
// The event task is the core handler to perform the event routing and TM handling as specified
|
||||
// in the sat-rs documentation.
|
||||
let mut event_handler = EventHandler::new(
|
||||
MpscTmAsVecSender::new(
|
||||
TmSenderId::AllEvents as ChannelId,
|
||||
"ALL_EVENTS_TX",
|
||||
tm_funnel_tx.clone(),
|
||||
),
|
||||
verif_reporter.clone(),
|
||||
event_request_rx,
|
||||
);
|
||||
|
||||
let (pus_test_tx, pus_test_rx) = channel();
|
||||
let (pus_event_tx, pus_event_rx) = channel();
|
||||
let (pus_sched_tx, pus_sched_rx) = channel();
|
||||
let (pus_hk_tx, pus_hk_rx) = channel();
|
||||
let (pus_action_tx, pus_action_rx) = channel();
|
||||
let pus_router = PusTcMpscRouter {
|
||||
test_service_receiver: pus_test_tx,
|
||||
event_service_receiver: pus_event_tx,
|
||||
sched_service_receiver: pus_sched_tx,
|
||||
hk_service_receiver: pus_hk_tx,
|
||||
action_service_receiver: pus_action_tx,
|
||||
};
|
||||
|
||||
let pus_test_service = create_test_service_dynamic(
|
||||
tm_funnel_tx.clone(),
|
||||
verif_reporter.clone(),
|
||||
event_handler.clone_event_sender(),
|
||||
pus_test_rx,
|
||||
);
|
||||
let pus_scheduler_service = create_scheduler_service_dynamic(
|
||||
tm_funnel_tx.clone(),
|
||||
verif_reporter.clone(),
|
||||
tc_source.0.clone(),
|
||||
pus_sched_rx,
|
||||
create_sched_tc_pool(),
|
||||
);
|
||||
|
||||
let pus_event_service = create_event_service_dynamic(
|
||||
tm_funnel_tx.clone(),
|
||||
verif_reporter.clone(),
|
||||
pus_event_rx,
|
||||
event_request_tx,
|
||||
);
|
||||
let pus_action_service = create_action_service_dynamic(
|
||||
tm_funnel_tx.clone(),
|
||||
verif_reporter.clone(),
|
||||
pus_action_rx,
|
||||
request_map.clone(),
|
||||
);
|
||||
let pus_hk_service = create_hk_service_dynamic(
|
||||
tm_funnel_tx.clone(),
|
||||
verif_reporter.clone(),
|
||||
pus_hk_rx,
|
||||
request_map,
|
||||
);
|
||||
let mut pus_stack = PusStack::new(
|
||||
pus_hk_service,
|
||||
pus_event_service,
|
||||
pus_action_service,
|
||||
pus_scheduler_service,
|
||||
pus_test_service,
|
||||
);
|
||||
|
||||
let ccsds_receiver = CcsdsReceiver { tc_source };
|
||||
|
||||
let mut tmtc_task = TcSourceTaskDynamic::new(
|
||||
tc_source_rx,
|
||||
PusReceiver::new(verif_reporter.clone(), pus_router),
|
||||
);
|
||||
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let udp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver.clone()));
|
||||
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(udp_ccsds_distributor))
|
||||
.expect("creating UDP TMTC server failed");
|
||||
let mut udp_tmtc_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: DynamicUdpTmHandler {
|
||||
tm_rx: tm_server_rx,
|
||||
},
|
||||
};
|
||||
|
||||
let tcp_ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
|
||||
let tcp_server_cfg = ServerConfig::new(sock_addr, Duration::from_millis(400), 4096, 8192);
|
||||
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
|
||||
let mut tcp_server = TcpTask::new(
|
||||
tcp_server_cfg,
|
||||
sync_tm_tcp_source.clone(),
|
||||
tcp_ccsds_distributor,
|
||||
)
|
||||
.expect("tcp server creation failed");
|
||||
|
||||
let mut acs_task = AcsTask::new(
|
||||
MpscTmAsVecSender::new(
|
||||
TmSenderId::AcsSubsystem as ChannelId,
|
||||
"ACS_TASK_SENDER",
|
||||
tm_funnel_tx.clone(),
|
||||
),
|
||||
acs_thread_rx,
|
||||
verif_reporter,
|
||||
);
|
||||
let mut tm_funnel = TmFunnelDynamic::new(sync_tm_tcp_source, tm_funnel_rx, tm_server_tx);
|
||||
|
||||
info!("Starting TMTC and UDP task");
|
||||
let jh_udp_tmtc = thread::Builder::new()
|
||||
.name("TMTC and UDP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running UDP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
udp_tmtc_server.periodic_operation();
|
||||
tmtc_task.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TCP task");
|
||||
let jh_tcp = thread::Builder::new()
|
||||
.name("TCP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running TCP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
tcp_server.periodic_operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TM funnel task");
|
||||
let jh_tm_funnel = thread::Builder::new()
|
||||
.name("TM Funnel".to_string())
|
||||
.spawn(move || loop {
|
||||
tm_funnel.operation();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting event handling task");
|
||||
let jh_event_handling = thread::Builder::new()
|
||||
.name("Event".to_string())
|
||||
.spawn(move || loop {
|
||||
event_handler.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting AOCS thread");
|
||||
let jh_aocs = thread::Builder::new()
|
||||
.name("AOCS".to_string())
|
||||
.spawn(move || loop {
|
||||
acs_task.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting PUS handler thread");
|
||||
let jh_pus_handler = thread::Builder::new()
|
||||
.name("PUS".to_string())
|
||||
.spawn(move || loop {
|
||||
pus_stack.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
jh_udp_tmtc
|
||||
.join()
|
||||
.expect("Joining UDP TMTC server thread failed");
|
||||
jh_tcp
|
||||
.join()
|
||||
.expect("Joining TCP TMTC server thread failed");
|
||||
jh_tm_funnel
|
||||
.join()
|
||||
.expect("Joining TM Funnel thread failed");
|
||||
jh_event_handling
|
||||
.join()
|
||||
.expect("Joining Event Manager thread failed");
|
||||
jh_aocs.join().expect("Joining AOCS thread failed");
|
||||
jh_pus_handler
|
||||
.join()
|
||||
.expect("Joining PUS handler thread failed");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
setup_logger().expect("setting up logging with fern failed");
|
||||
println!("Running OBSW example");
|
||||
#[cfg(not(feature = "dyn_tmtc"))]
|
||||
static_tmtc_pool_main();
|
||||
#[cfg(feature = "dyn_tmtc")]
|
||||
dyn_tmtc_pool_main();
|
||||
}
|
||||
|
||||
pub fn update_time(time_provider: &mut TimeProvider, timestamp: &mut [u8]) {
|
||||
|
@ -1,48 +1,106 @@
|
||||
use crate::requests::{ActionRequest, Request, RequestWithToken};
|
||||
use log::{error, warn};
|
||||
use satrs_core::pool::{SharedPool, StoreAddr};
|
||||
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
|
||||
use satrs_core::pus::verification::{
|
||||
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
|
||||
FailParams, TcStateAccepted, VerificationReporterWithSender, VerificationToken,
|
||||
};
|
||||
use satrs_core::pus::{
|
||||
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
|
||||
PusServiceHandler,
|
||||
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
|
||||
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
|
||||
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
|
||||
};
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs_core::spacepackets::ecss::PusPacket;
|
||||
use satrs_core::tmtc::TargetId;
|
||||
use satrs_example::tmtc_err;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_example::config::{tmtc_err, TcReceiverId, TmSenderId, PUS_APID};
|
||||
use satrs_example::TargetIdWithApid;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
|
||||
pub struct PusService8ActionHandler {
|
||||
psb: PusServiceBase,
|
||||
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
|
||||
pub fn create_action_service_static(
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_tx: mpsc::Sender<StoreAddr>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
|
||||
) -> Pus8Wrapper<EcssTcInSharedStoreConverter> {
|
||||
let action_srv_tm_sender = MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusAction as ChannelId,
|
||||
"PUS_8_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let action_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusAction as ChannelId,
|
||||
"PUS_8_TC_RECV",
|
||||
pus_action_rx,
|
||||
);
|
||||
let pus_8_handler = PusService8ActionHandler::new(
|
||||
Box::new(action_srv_receiver),
|
||||
Box::new(action_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
|
||||
request_map.clone(),
|
||||
);
|
||||
Pus8Wrapper { pus_8_handler }
|
||||
}
|
||||
|
||||
impl PusService8ActionHandler {
|
||||
pub fn create_action_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
|
||||
) -> Pus8Wrapper<EcssTcInVecConverter> {
|
||||
let action_srv_tm_sender = MpscTmAsVecSender::new(
|
||||
TmSenderId::PusAction as ChannelId,
|
||||
"PUS_8_TM_SENDER",
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let action_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusAction as ChannelId,
|
||||
"PUS_8_TC_RECV",
|
||||
pus_action_rx,
|
||||
);
|
||||
let pus_8_handler = PusService8ActionHandler::new(
|
||||
Box::new(action_srv_receiver),
|
||||
Box::new(action_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInVecConverter::default(),
|
||||
request_map.clone(),
|
||||
);
|
||||
Pus8Wrapper { pus_8_handler }
|
||||
}
|
||||
|
||||
pub struct PusService8ActionHandler<TcInMemConverter: EcssTcInMemConverter> {
|
||||
service_helper: PusServiceHelper<TcInMemConverter>,
|
||||
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
|
||||
}
|
||||
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusService8ActionHandler<TcInMemConverter> {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_pool: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
|
||||
verification_handler: VerificationReporterWithSender,
|
||||
tc_in_mem_converter: TcInMemConverter,
|
||||
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
psb: PusServiceBase::new(
|
||||
service_helper: PusServiceHelper::new(
|
||||
tc_receiver,
|
||||
shared_tc_pool,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler,
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
request_handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusService8ActionHandler {
|
||||
fn handle_action_request_with_id(
|
||||
&self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
@ -51,7 +109,8 @@ impl PusService8ActionHandler {
|
||||
) -> Result<(), PusPacketHandlingError> {
|
||||
let user_data = tc.user_data();
|
||||
if user_data.len() < 8 {
|
||||
self.psb()
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(
|
||||
@ -63,7 +122,8 @@ impl PusService8ActionHandler {
|
||||
"Expected at least 4 bytes".into(),
|
||||
));
|
||||
}
|
||||
let target_id = u32::from_be_bytes(user_data[0..4].try_into().unwrap());
|
||||
//let target_id = u32::from_be_bytes(user_data[0..4].try_into().unwrap());
|
||||
let target_id = TargetIdWithApid::from_tc(tc).unwrap();
|
||||
let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap());
|
||||
if let Some(sender) = self.request_handlers.get(&target_id) {
|
||||
sender
|
||||
@ -78,8 +138,9 @@ impl PusService8ActionHandler {
|
||||
.expect("Forwarding action request failed");
|
||||
} else {
|
||||
let mut fail_data: [u8; 4] = [0; 4];
|
||||
fail_data.copy_from_slice(&target_id.to_be_bytes());
|
||||
self.psb()
|
||||
fail_data.copy_from_slice(&target_id.target.to_be_bytes());
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(
|
||||
@ -97,37 +158,32 @@ impl PusService8ActionHandler {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PusServiceHandler for PusService8ActionHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase {
|
||||
&mut self.psb
|
||||
}
|
||||
fn psb(&self) -> &PusServiceBase {
|
||||
&self.psb
|
||||
}
|
||||
|
||||
fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
self.copy_tc_to_buf(addr)?;
|
||||
let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap();
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
self.service_helper
|
||||
.tc_in_mem_converter
|
||||
.cache_ecss_tc_in_memory(&ecss_tc_and_token.tc_in_memory)?;
|
||||
let tc = PusTcReader::new(self.service_helper.tc_in_mem_converter.tc_slice_raw())?.0;
|
||||
let subservice = tc.subservice();
|
||||
let mut partial_error = None;
|
||||
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
|
||||
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
|
||||
match subservice {
|
||||
128 => {
|
||||
self.handle_action_request_with_id(token, &tc, &time_stamp)?;
|
||||
self.handle_action_request_with_id(ecss_tc_and_token.token, &tc, &time_stamp)?;
|
||||
}
|
||||
_ => {
|
||||
let fail_data = [subservice];
|
||||
self.psb_mut()
|
||||
self.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_failure(
|
||||
token,
|
||||
ecss_tc_and_token.token,
|
||||
FailParams::new(
|
||||
Some(&time_stamp),
|
||||
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||
@ -147,13 +203,13 @@ impl PusServiceHandler for PusService8ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Pus8Wrapper {
|
||||
pub(crate) pus_8_handler: PusService8ActionHandler,
|
||||
pub struct Pus8Wrapper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub(crate) pus_8_handler: PusService8ActionHandler<TcInMemConverter>,
|
||||
}
|
||||
|
||||
impl Pus8Wrapper {
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> Pus8Wrapper<TcInMemConverter> {
|
||||
pub fn handle_next_packet(&mut self) -> bool {
|
||||
match self.pus_8_handler.handle_next_packet() {
|
||||
match self.pus_8_handler.handle_one_tc() {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
|
@ -1,14 +1,87 @@
|
||||
use log::{error, warn};
|
||||
use satrs_core::pus::event_srv::PusService5EventHandler;
|
||||
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub struct Pus5Wrapper {
|
||||
pub pus_5_handler: PusService5EventHandler,
|
||||
use log::{error, warn};
|
||||
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
|
||||
use satrs_core::pus::event_man::EventRequestWithToken;
|
||||
use satrs_core::pus::event_srv::PusService5EventHandler;
|
||||
use satrs_core::pus::verification::VerificationReporterWithSender;
|
||||
use satrs_core::pus::{
|
||||
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
|
||||
MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
|
||||
PusServiceHelper,
|
||||
};
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_example::config::{TcReceiverId, TmSenderId, PUS_APID};
|
||||
|
||||
pub fn create_event_service_static(
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_tx: mpsc::Sender<StoreAddr>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
event_request_tx: mpsc::Sender<EventRequestWithToken>,
|
||||
) -> Pus5Wrapper<EcssTcInSharedStoreConverter> {
|
||||
let event_srv_tm_sender = MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusEvent as ChannelId,
|
||||
"PUS_5_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let event_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusEvent as ChannelId,
|
||||
"PUS_5_TC_RECV",
|
||||
pus_event_rx,
|
||||
);
|
||||
let pus_5_handler = PusService5EventHandler::new(
|
||||
PusServiceHelper::new(
|
||||
Box::new(event_srv_receiver),
|
||||
Box::new(event_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool.clone(), 2048),
|
||||
),
|
||||
event_request_tx,
|
||||
);
|
||||
Pus5Wrapper { pus_5_handler }
|
||||
}
|
||||
|
||||
impl Pus5Wrapper {
|
||||
pub fn create_event_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
event_request_tx: mpsc::Sender<EventRequestWithToken>,
|
||||
) -> Pus5Wrapper<EcssTcInVecConverter> {
|
||||
let event_srv_tm_sender = MpscTmAsVecSender::new(
|
||||
TmSenderId::PusEvent as ChannelId,
|
||||
"PUS_5_TM_SENDER",
|
||||
tm_funnel_tx,
|
||||
);
|
||||
let event_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusEvent as ChannelId,
|
||||
"PUS_5_TC_RECV",
|
||||
pus_event_rx,
|
||||
);
|
||||
let pus_5_handler = PusService5EventHandler::new(
|
||||
PusServiceHelper::new(
|
||||
Box::new(event_srv_receiver),
|
||||
Box::new(event_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInVecConverter::default(),
|
||||
),
|
||||
event_request_tx,
|
||||
);
|
||||
Pus5Wrapper { pus_5_handler }
|
||||
}
|
||||
|
||||
pub struct Pus5Wrapper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub pus_5_handler: PusService5EventHandler<TcInMemConverter>,
|
||||
}
|
||||
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> Pus5Wrapper<TcInMemConverter> {
|
||||
pub fn handle_next_packet(&mut self) -> bool {
|
||||
match self.pus_5_handler.handle_next_packet() {
|
||||
match self.pus_5_handler.handle_one_tc() {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
|
@ -1,73 +1,121 @@
|
||||
use crate::requests::{Request, RequestWithToken};
|
||||
use log::{error, warn};
|
||||
use satrs_core::hk::{CollectionIntervalFactor, HkRequest};
|
||||
use satrs_core::pool::{SharedPool, StoreAddr};
|
||||
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
|
||||
use satrs_core::pus::verification::{
|
||||
FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken,
|
||||
FailParams, StdVerifReporterWithSender, VerificationReporterWithSender,
|
||||
};
|
||||
use satrs_core::pus::{
|
||||
EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase,
|
||||
PusServiceHandler,
|
||||
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
|
||||
EcssTcReceiver, EcssTmSender, MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender,
|
||||
PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper,
|
||||
};
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs_core::spacepackets::ecss::{hk, PusPacket};
|
||||
use satrs_core::tmtc::{AddressableId, TargetId};
|
||||
use satrs_example::{hk_err, tmtc_err};
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_example::config::{hk_err, tmtc_err, TcReceiverId, TmSenderId, PUS_APID};
|
||||
use satrs_example::TargetIdWithApid;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
|
||||
pub struct PusService3HkHandler {
|
||||
psb: PusServiceBase,
|
||||
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
|
||||
pub fn create_hk_service_static(
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_tx: mpsc::Sender<StoreAddr>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
|
||||
) -> Pus3Wrapper<EcssTcInSharedStoreConverter> {
|
||||
let hk_srv_tm_sender = MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusHk as ChannelId,
|
||||
"PUS_3_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let hk_srv_receiver =
|
||||
MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
|
||||
let pus_3_handler = PusService3HkHandler::new(
|
||||
Box::new(hk_srv_receiver),
|
||||
Box::new(hk_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
|
||||
request_map,
|
||||
);
|
||||
Pus3Wrapper { pus_3_handler }
|
||||
}
|
||||
|
||||
impl PusService3HkHandler {
|
||||
pub fn create_hk_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_map: HashMap<TargetIdWithApid, mpsc::Sender<RequestWithToken>>,
|
||||
) -> Pus3Wrapper<EcssTcInVecConverter> {
|
||||
let hk_srv_tm_sender = MpscTmAsVecSender::new(
|
||||
TmSenderId::PusHk as ChannelId,
|
||||
"PUS_3_TM_SENDER",
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let hk_srv_receiver =
|
||||
MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx);
|
||||
let pus_3_handler = PusService3HkHandler::new(
|
||||
Box::new(hk_srv_receiver),
|
||||
Box::new(hk_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInVecConverter::default(),
|
||||
request_map,
|
||||
);
|
||||
Pus3Wrapper { pus_3_handler }
|
||||
}
|
||||
|
||||
pub struct PusService3HkHandler<TcInMemConverter: EcssTcInMemConverter> {
|
||||
psb: PusServiceHelper<TcInMemConverter>,
|
||||
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
|
||||
}
|
||||
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusService3HkHandler<TcInMemConverter> {
|
||||
pub fn new(
|
||||
tc_receiver: Box<dyn EcssTcReceiver>,
|
||||
shared_tc_pool: SharedPool,
|
||||
tm_sender: Box<dyn EcssTmSender>,
|
||||
tm_apid: u16,
|
||||
verification_handler: StdVerifReporterWithSender,
|
||||
request_handlers: HashMap<TargetId, Sender<RequestWithToken>>,
|
||||
tc_in_mem_converter: TcInMemConverter,
|
||||
request_handlers: HashMap<TargetIdWithApid, Sender<RequestWithToken>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
psb: PusServiceBase::new(
|
||||
psb: PusServiceHelper::new(
|
||||
tc_receiver,
|
||||
shared_tc_pool,
|
||||
tm_sender,
|
||||
tm_apid,
|
||||
verification_handler,
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
request_handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusServiceHandler for PusService3HkHandler {
|
||||
fn psb_mut(&mut self) -> &mut PusServiceBase {
|
||||
&mut self.psb
|
||||
}
|
||||
fn psb(&self) -> &PusServiceBase {
|
||||
&self.psb
|
||||
}
|
||||
|
||||
fn handle_one_tc(
|
||||
&mut self,
|
||||
addr: StoreAddr,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
self.copy_tc_to_buf(addr)?;
|
||||
let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap();
|
||||
fn handle_one_tc(&mut self) -> Result<PusPacketHandlerResult, PusPacketHandlingError> {
|
||||
let possible_packet = self.psb.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(PusPacketHandlerResult::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
let tc = self
|
||||
.psb
|
||||
.tc_in_mem_converter
|
||||
.convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?;
|
||||
let subservice = tc.subservice();
|
||||
let mut partial_error = None;
|
||||
let time_stamp = self.psb().get_current_timestamp(&mut partial_error);
|
||||
let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error);
|
||||
let user_data = tc.user_data();
|
||||
if user_data.is_empty() {
|
||||
self.psb
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(
|
||||
token,
|
||||
ecss_tc_and_token.token,
|
||||
FailParams::new(Some(&time_stamp), &tmtc_err::NOT_ENOUGH_APP_DATA, None),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
@ -82,63 +130,58 @@ impl PusServiceHandler for PusService3HkHandler {
|
||||
&hk_err::UNIQUE_ID_MISSING
|
||||
};
|
||||
self.psb
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(token, FailParams::new(Some(&time_stamp), err, None))
|
||||
.start_failure(
|
||||
ecss_tc_and_token.token,
|
||||
FailParams::new(Some(&time_stamp), err, None),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(PusPacketHandlingError::NotEnoughAppData(
|
||||
"Expected at least 8 bytes of app data".into(),
|
||||
));
|
||||
}
|
||||
let addressable_id = AddressableId::from_raw_be(user_data).unwrap();
|
||||
if !self
|
||||
.request_handlers
|
||||
.contains_key(&addressable_id.target_id)
|
||||
{
|
||||
let target_id = TargetIdWithApid::from_tc(&tc).expect("invalid tc format");
|
||||
let unique_id = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap());
|
||||
if !self.request_handlers.contains_key(&target_id) {
|
||||
self.psb
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(
|
||||
token,
|
||||
ecss_tc_and_token.token,
|
||||
FailParams::new(Some(&time_stamp), &hk_err::UNKNOWN_TARGET_ID, None),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
let tgt_id = addressable_id.target_id;
|
||||
return Err(PusPacketHandlingError::NotEnoughAppData(format!(
|
||||
"Unknown target ID {tgt_id}"
|
||||
"Unknown target ID {target_id}"
|
||||
)));
|
||||
}
|
||||
let send_request = |target: TargetId, request: HkRequest| {
|
||||
let sender = self
|
||||
.request_handlers
|
||||
.get(&addressable_id.target_id)
|
||||
.unwrap();
|
||||
let send_request = |target: TargetIdWithApid, request: HkRequest| {
|
||||
let sender = self.request_handlers.get(&target).unwrap();
|
||||
sender
|
||||
.send(RequestWithToken::new(target, Request::Hk(request), token))
|
||||
.send(RequestWithToken::new(
|
||||
target,
|
||||
Request::Hk(request),
|
||||
ecss_tc_and_token.token,
|
||||
))
|
||||
.unwrap_or_else(|_| panic!("Sending HK request {request:?} failed"));
|
||||
};
|
||||
if subservice == hk::Subservice::TcEnableHkGeneration as u8 {
|
||||
send_request(
|
||||
addressable_id.target_id,
|
||||
HkRequest::Enable(addressable_id.unique_id),
|
||||
);
|
||||
send_request(target_id, HkRequest::Enable(unique_id));
|
||||
} else if subservice == hk::Subservice::TcDisableHkGeneration as u8 {
|
||||
send_request(
|
||||
addressable_id.target_id,
|
||||
HkRequest::Disable(addressable_id.unique_id),
|
||||
);
|
||||
send_request(target_id, HkRequest::Disable(unique_id));
|
||||
} else if subservice == hk::Subservice::TcGenerateOneShotHk as u8 {
|
||||
send_request(
|
||||
addressable_id.target_id,
|
||||
HkRequest::OneShot(addressable_id.unique_id),
|
||||
);
|
||||
send_request(target_id, HkRequest::OneShot(unique_id));
|
||||
} else if subservice == hk::Subservice::TcModifyHkCollectionInterval as u8 {
|
||||
if user_data.len() < 12 {
|
||||
self.psb
|
||||
.common
|
||||
.verification_handler
|
||||
.borrow_mut()
|
||||
.start_failure(
|
||||
token,
|
||||
ecss_tc_and_token.token,
|
||||
FailParams::new(
|
||||
Some(&time_stamp),
|
||||
&hk_err::COLLECTION_INTERVAL_MISSING,
|
||||
@ -151,9 +194,9 @@ impl PusServiceHandler for PusService3HkHandler {
|
||||
));
|
||||
}
|
||||
send_request(
|
||||
addressable_id.target_id,
|
||||
target_id,
|
||||
HkRequest::ModifyCollectionInterval(
|
||||
addressable_id.unique_id,
|
||||
unique_id,
|
||||
CollectionIntervalFactor::from_be_bytes(user_data[8..12].try_into().unwrap()),
|
||||
),
|
||||
);
|
||||
@ -162,13 +205,13 @@ impl PusServiceHandler for PusService3HkHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Pus3Wrapper {
|
||||
pub(crate) pus_3_handler: PusService3HkHandler,
|
||||
pub struct Pus3Wrapper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub(crate) pus_3_handler: PusService3HkHandler<TcInMemConverter>,
|
||||
}
|
||||
|
||||
impl Pus3Wrapper {
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> Pus3Wrapper<TcInMemConverter> {
|
||||
pub fn handle_next_packet(&mut self) -> bool {
|
||||
match self.pus_3_handler.handle_next_packet() {
|
||||
match self.pus_3_handler.handle_one_tc() {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
|
@ -1,27 +1,27 @@
|
||||
use crate::tmtc::MpscStoreAndSendError;
|
||||
use log::warn;
|
||||
use satrs_core::pool::StoreAddr;
|
||||
use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender};
|
||||
use satrs_core::pus::{PusPacketHandlerResult, TcAddrWithToken};
|
||||
use satrs_core::pus::{EcssTcAndToken, PusPacketHandlerResult, TcInMemory};
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs_core::spacepackets::ecss::PusServiceId;
|
||||
use satrs_core::spacepackets::time::cds::TimeProvider;
|
||||
use satrs_core::spacepackets::time::TimeWriter;
|
||||
use satrs_example::{tmtc_err, CustomPusServiceId};
|
||||
use satrs_example::config::{tmtc_err, CustomPusServiceId};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub mod action;
|
||||
pub mod event;
|
||||
pub mod hk;
|
||||
pub mod scheduler;
|
||||
pub mod stack;
|
||||
pub mod test;
|
||||
|
||||
pub struct PusTcMpscRouter {
|
||||
pub test_service_receiver: Sender<TcAddrWithToken>,
|
||||
pub event_service_receiver: Sender<TcAddrWithToken>,
|
||||
pub sched_service_receiver: Sender<TcAddrWithToken>,
|
||||
pub hk_service_receiver: Sender<TcAddrWithToken>,
|
||||
pub action_service_receiver: Sender<TcAddrWithToken>,
|
||||
pub test_service_receiver: Sender<EcssTcAndToken>,
|
||||
pub event_service_receiver: Sender<EcssTcAndToken>,
|
||||
pub sched_service_receiver: Sender<EcssTcAndToken>,
|
||||
pub hk_service_receiver: Sender<EcssTcAndToken>,
|
||||
pub action_service_receiver: Sender<EcssTcAndToken>,
|
||||
}
|
||||
|
||||
pub struct PusReceiver {
|
||||
@ -70,7 +70,7 @@ impl PusReceiver {
|
||||
impl PusReceiver {
|
||||
pub fn handle_tc_packet(
|
||||
&mut self,
|
||||
store_addr: StoreAddr,
|
||||
tc_in_memory: TcInMemory,
|
||||
service: u8,
|
||||
pus_tc: &PusTcReader,
|
||||
) -> Result<PusPacketHandlerResult, MpscStoreAndSendError> {
|
||||
@ -84,22 +84,33 @@ impl PusReceiver {
|
||||
match service {
|
||||
Ok(standard_service) => match standard_service {
|
||||
PusServiceId::Test => {
|
||||
self.pus_router
|
||||
.test_service_receiver
|
||||
.send((store_addr, accepted_token.into()))?;
|
||||
self.pus_router.test_service_receiver.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
PusServiceId::Housekeeping => {
|
||||
self.pus_router.hk_service_receiver.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
PusServiceId::Event => {
|
||||
self.pus_router
|
||||
.event_service_receiver
|
||||
.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
PusServiceId::Scheduling => {
|
||||
self.pus_router
|
||||
.sched_service_receiver
|
||||
.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
PusServiceId::Housekeeping => self
|
||||
.pus_router
|
||||
.hk_service_receiver
|
||||
.send((store_addr, accepted_token.into()))?,
|
||||
PusServiceId::Event => self
|
||||
.pus_router
|
||||
.event_service_receiver
|
||||
.send((store_addr, accepted_token.into()))?,
|
||||
PusServiceId::Scheduling => self
|
||||
.pus_router
|
||||
.sched_service_receiver
|
||||
.send((store_addr, accepted_token.into()))?,
|
||||
_ => {
|
||||
let result = self.verif_reporter.start_failure(
|
||||
accepted_token,
|
||||
|
@ -1,50 +1,89 @@
|
||||
use crate::tmtc::PusTcSource;
|
||||
use log::{error, info, warn};
|
||||
use satrs_core::pus::scheduler::TcInfo;
|
||||
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
|
||||
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Pus11Wrapper {
|
||||
pub pus_11_handler: PusService11SchedHandler,
|
||||
pub tc_source_wrapper: PusTcSource,
|
||||
use log::{error, info, warn};
|
||||
use satrs_core::pool::{PoolProvider, StaticMemoryPool, StoreAddr};
|
||||
use satrs_core::pus::scheduler::{PusScheduler, TcInfo};
|
||||
use satrs_core::pus::scheduler_srv::PusService11SchedHandler;
|
||||
use satrs_core::pus::verification::VerificationReporterWithSender;
|
||||
use satrs_core::pus::{
|
||||
EcssTcAndToken, EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcInVecConverter,
|
||||
MpscTcReceiver, MpscTmAsVecSender, MpscTmInSharedPoolSender, PusPacketHandlerResult,
|
||||
PusServiceHelper,
|
||||
};
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_example::config::{TcReceiverId, TmSenderId, PUS_APID};
|
||||
|
||||
use crate::tmtc::PusTcSourceProviderSharedPool;
|
||||
|
||||
pub trait TcReleaser {
|
||||
fn release(&mut self, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
|
||||
}
|
||||
|
||||
impl Pus11Wrapper {
|
||||
pub fn release_tcs(&mut self) {
|
||||
let releaser = |enabled: bool, info: &TcInfo| -> bool {
|
||||
if enabled {
|
||||
self.tc_source_wrapper
|
||||
.tc_source
|
||||
.send(info.addr())
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
};
|
||||
impl TcReleaser for PusTcSourceProviderSharedPool {
|
||||
fn release(&mut self, enabled: bool, _info: &TcInfo, tc: &[u8]) -> bool {
|
||||
if enabled {
|
||||
// Transfer TC from scheduler TC pool to shared TC pool.
|
||||
let released_tc_addr = self
|
||||
.shared_pool
|
||||
.pool
|
||||
.write()
|
||||
.expect("locking pool failed")
|
||||
.add(tc)
|
||||
.expect("adding TC to shared pool failed");
|
||||
self.tc_source
|
||||
.send(released_tc_addr)
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let mut pool = self
|
||||
.tc_source_wrapper
|
||||
.tc_store
|
||||
.pool
|
||||
.write()
|
||||
.expect("error locking pool");
|
||||
impl TcReleaser for mpsc::Sender<Vec<u8>> {
|
||||
fn release(&mut self, enabled: bool, _info: &TcInfo, tc: &[u8]) -> bool {
|
||||
if enabled {
|
||||
// Send released TC to centralized TC source.
|
||||
self.send(tc.to_vec())
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Pus11Wrapper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub pus_11_handler: PusService11SchedHandler<TcInMemConverter, PusScheduler>,
|
||||
pub sched_tc_pool: StaticMemoryPool,
|
||||
pub releaser_buf: [u8; 4096],
|
||||
pub tc_releaser: Box<dyn TcReleaser + Send>,
|
||||
}
|
||||
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> Pus11Wrapper<TcInMemConverter> {
|
||||
pub fn release_tcs(&mut self) {
|
||||
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
|
||||
self.tc_releaser.release(enabled, info, tc)
|
||||
};
|
||||
|
||||
self.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.update_time_from_now()
|
||||
.unwrap();
|
||||
if let Ok(released_tcs) = self
|
||||
let released_tcs = self
|
||||
.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.release_telecommands(releaser, pool.as_mut())
|
||||
{
|
||||
if released_tcs > 0 {
|
||||
info!("{released_tcs} TC(s) released from scheduler");
|
||||
}
|
||||
.release_telecommands_with_buffer(
|
||||
releaser,
|
||||
&mut self.sched_tc_pool,
|
||||
&mut self.releaser_buf,
|
||||
)
|
||||
.expect("releasing TCs failed");
|
||||
if released_tcs > 0 {
|
||||
info!("{released_tcs} TC(s) released from scheduler");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_next_packet(&mut self) -> bool {
|
||||
match self.pus_11_handler.handle_next_packet() {
|
||||
match self.pus_11_handler.handle_one_tc(&mut self.sched_tc_pool) {
|
||||
Ok(result) => match result {
|
||||
PusPacketHandlerResult::RequestHandled => {}
|
||||
PusPacketHandlerResult::RequestHandledPartialSuccess(e) => {
|
||||
@ -67,3 +106,79 @@ impl Pus11Wrapper {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_scheduler_service_static(
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_tx: mpsc::Sender<StoreAddr>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_releaser: PusTcSourceProviderSharedPool,
|
||||
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
sched_tc_pool: StaticMemoryPool,
|
||||
) -> Pus11Wrapper<EcssTcInSharedStoreConverter> {
|
||||
let sched_srv_tm_sender = MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusSched as ChannelId,
|
||||
"PUS_11_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let sched_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusSched as ChannelId,
|
||||
"PUS_11_TC_RECV",
|
||||
pus_sched_rx,
|
||||
);
|
||||
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
|
||||
.expect("Creating PUS Scheduler failed");
|
||||
let pus_11_handler = PusService11SchedHandler::new(
|
||||
PusServiceHelper::new(
|
||||
Box::new(sched_srv_receiver),
|
||||
Box::new(sched_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInSharedStoreConverter::new(tc_releaser.clone_backing_pool(), 2048),
|
||||
),
|
||||
scheduler,
|
||||
);
|
||||
Pus11Wrapper {
|
||||
pus_11_handler,
|
||||
sched_tc_pool,
|
||||
releaser_buf: [0; 4096],
|
||||
tc_releaser: Box::new(tc_releaser),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_scheduler_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_source_sender: mpsc::Sender<Vec<u8>>,
|
||||
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
sched_tc_pool: StaticMemoryPool,
|
||||
) -> Pus11Wrapper<EcssTcInVecConverter> {
|
||||
let sched_srv_tm_sender = MpscTmAsVecSender::new(
|
||||
TmSenderId::PusSched as ChannelId,
|
||||
"PUS_11_TM_SENDER",
|
||||
tm_funnel_tx,
|
||||
);
|
||||
let sched_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusSched as ChannelId,
|
||||
"PUS_11_TC_RECV",
|
||||
pus_sched_rx,
|
||||
);
|
||||
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
|
||||
.expect("Creating PUS Scheduler failed");
|
||||
let pus_11_handler = PusService11SchedHandler::new(
|
||||
PusServiceHelper::new(
|
||||
Box::new(sched_srv_receiver),
|
||||
Box::new(sched_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInVecConverter::default(),
|
||||
),
|
||||
scheduler,
|
||||
);
|
||||
Pus11Wrapper {
|
||||
pus_11_handler,
|
||||
sched_tc_pool,
|
||||
releaser_buf: [0; 4096],
|
||||
tc_releaser: Box::new(tc_source_sender),
|
||||
}
|
||||
}
|
||||
|
52
satrs-example/src/pus/stack.rs
Normal file
52
satrs-example/src/pus/stack.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use satrs_core::pus::EcssTcInMemConverter;
|
||||
|
||||
use super::{
|
||||
action::Pus8Wrapper, event::Pus5Wrapper, hk::Pus3Wrapper, scheduler::Pus11Wrapper,
|
||||
test::Service17CustomWrapper,
|
||||
};
|
||||
|
||||
pub struct PusStack<TcInMemConverter: EcssTcInMemConverter> {
|
||||
event_srv: Pus5Wrapper<TcInMemConverter>,
|
||||
hk_srv: Pus3Wrapper<TcInMemConverter>,
|
||||
action_srv: Pus8Wrapper<TcInMemConverter>,
|
||||
schedule_srv: Pus11Wrapper<TcInMemConverter>,
|
||||
test_srv: Service17CustomWrapper<TcInMemConverter>,
|
||||
}
|
||||
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> PusStack<TcInMemConverter> {
|
||||
pub fn new(
|
||||
hk_srv: Pus3Wrapper<TcInMemConverter>,
|
||||
event_srv: Pus5Wrapper<TcInMemConverter>,
|
||||
action_srv: Pus8Wrapper<TcInMemConverter>,
|
||||
schedule_srv: Pus11Wrapper<TcInMemConverter>,
|
||||
test_srv: Service17CustomWrapper<TcInMemConverter>,
|
||||
) -> Self {
|
||||
Self {
|
||||
event_srv,
|
||||
action_srv,
|
||||
schedule_srv,
|
||||
test_srv,
|
||||
hk_srv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.schedule_srv.release_tcs();
|
||||
loop {
|
||||
let mut all_queues_empty = true;
|
||||
let mut is_srv_finished = |srv_handler_finished: bool| {
|
||||
if !srv_handler_finished {
|
||||
all_queues_empty = false;
|
||||
}
|
||||
};
|
||||
is_srv_finished(self.test_srv.handle_next_packet());
|
||||
is_srv_finished(self.schedule_srv.handle_next_packet());
|
||||
is_srv_finished(self.event_srv.handle_next_packet());
|
||||
is_srv_finished(self.action_srv.handle_next_packet());
|
||||
is_srv_finished(self.hk_srv.handle_next_packet());
|
||||
if all_queues_empty {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,91 @@
|
||||
use log::{info, warn};
|
||||
use satrs_core::events::EventU32;
|
||||
use satrs_core::params::Params;
|
||||
use satrs_core::pool::{SharedStaticMemoryPool, StoreAddr};
|
||||
use satrs_core::pus::test::PusService17TestHandler;
|
||||
use satrs_core::pus::verification::FailParams;
|
||||
use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler};
|
||||
use satrs_core::pus::verification::{FailParams, VerificationReporterWithSender};
|
||||
use satrs_core::pus::{
|
||||
EcssTcAndToken, EcssTcInMemConverter, EcssTcInVecConverter, MpscTcReceiver, MpscTmAsVecSender,
|
||||
MpscTmInSharedPoolSender, PusPacketHandlerResult, PusServiceHelper,
|
||||
};
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs_core::spacepackets::ecss::PusPacket;
|
||||
use satrs_core::spacepackets::time::cds::TimeProvider;
|
||||
use satrs_core::spacepackets::time::TimeWriter;
|
||||
use satrs_example::{tmtc_err, TEST_EVENT};
|
||||
use std::sync::mpsc::Sender;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmPool;
|
||||
use satrs_core::ChannelId;
|
||||
use satrs_core::{events::EventU32, pus::EcssTcInSharedStoreConverter};
|
||||
use satrs_example::config::{tmtc_err, TcReceiverId, TmSenderId, PUS_APID, TEST_EVENT};
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
|
||||
pub struct Service17CustomWrapper {
|
||||
pub pus17_handler: PusService17TestHandler,
|
||||
pub fn create_test_service_static(
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_tx: mpsc::Sender<StoreAddr>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
tc_pool: SharedStaticMemoryPool,
|
||||
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
|
||||
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
) -> Service17CustomWrapper<EcssTcInSharedStoreConverter> {
|
||||
let test_srv_tm_sender = MpscTmInSharedPoolSender::new(
|
||||
TmSenderId::PusTest as ChannelId,
|
||||
"PUS_17_TM_SENDER",
|
||||
shared_tm_store.clone(),
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let test_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusTest as ChannelId,
|
||||
"PUS_17_TC_RECV",
|
||||
pus_test_rx,
|
||||
);
|
||||
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
|
||||
Box::new(test_srv_receiver),
|
||||
Box::new(test_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInSharedStoreConverter::new(tc_pool, 2048),
|
||||
));
|
||||
Service17CustomWrapper {
|
||||
pus17_handler,
|
||||
test_srv_event_sender: event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_test_service_dynamic(
|
||||
tm_funnel_tx: mpsc::Sender<Vec<u8>>,
|
||||
verif_reporter: VerificationReporterWithSender,
|
||||
event_sender: mpsc::Sender<(EventU32, Option<Params>)>,
|
||||
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
) -> Service17CustomWrapper<EcssTcInVecConverter> {
|
||||
let test_srv_tm_sender = MpscTmAsVecSender::new(
|
||||
TmSenderId::PusTest as ChannelId,
|
||||
"PUS_17_TM_SENDER",
|
||||
tm_funnel_tx.clone(),
|
||||
);
|
||||
let test_srv_receiver = MpscTcReceiver::new(
|
||||
TcReceiverId::PusTest as ChannelId,
|
||||
"PUS_17_TC_RECV",
|
||||
pus_test_rx,
|
||||
);
|
||||
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
|
||||
Box::new(test_srv_receiver),
|
||||
Box::new(test_srv_tm_sender),
|
||||
PUS_APID,
|
||||
verif_reporter.clone(),
|
||||
EcssTcInVecConverter::default(),
|
||||
));
|
||||
Service17CustomWrapper {
|
||||
pus17_handler,
|
||||
test_srv_event_sender: event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Service17CustomWrapper<TcInMemConverter: EcssTcInMemConverter> {
|
||||
pub pus17_handler: PusService17TestHandler<TcInMemConverter>,
|
||||
pub test_srv_event_sender: Sender<(EventU32, Option<Params>)>,
|
||||
}
|
||||
|
||||
impl Service17CustomWrapper {
|
||||
impl<TcInMemConverter: EcssTcInMemConverter> Service17CustomWrapper<TcInMemConverter> {
|
||||
pub fn handle_next_packet(&mut self) -> bool {
|
||||
let res = self.pus17_handler.handle_next_packet();
|
||||
let res = self.pus17_handler.handle_one_tc();
|
||||
if res.is_err() {
|
||||
warn!("PUS17 handler failed with error {:?}", res.unwrap_err());
|
||||
return true;
|
||||
@ -38,9 +105,13 @@ impl Service17CustomWrapper {
|
||||
warn!("PUS17: Subservice {subservice} not implemented")
|
||||
}
|
||||
PusPacketHandlerResult::CustomSubservice(subservice, token) => {
|
||||
let psb_mut = self.pus17_handler.psb_mut();
|
||||
let buf = psb_mut.pus_buf;
|
||||
let (tc, _) = PusTcReader::new(&buf).unwrap();
|
||||
let (tc, _) = PusTcReader::new(
|
||||
self.pus17_handler
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.tc_slice_raw(),
|
||||
)
|
||||
.unwrap();
|
||||
let time_stamper = TimeProvider::from_now_with_u16_days().unwrap();
|
||||
let mut stamp_buf: [u8; 7] = [0; 7];
|
||||
time_stamper.write_to_bytes(&mut stamp_buf).unwrap();
|
||||
@ -49,12 +120,17 @@ impl Service17CustomWrapper {
|
||||
self.test_srv_event_sender
|
||||
.send((TEST_EVENT.into(), None))
|
||||
.expect("Sending test event failed");
|
||||
let start_token = psb_mut
|
||||
let start_token = self
|
||||
.pus17_handler
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_success(token, Some(&stamp_buf))
|
||||
.expect("Error sending start success");
|
||||
psb_mut
|
||||
self.pus17_handler
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.completion_success(start_token, Some(&stamp_buf))
|
||||
@ -62,7 +138,8 @@ impl Service17CustomWrapper {
|
||||
} else {
|
||||
let fail_data = [tc.subservice()];
|
||||
self.pus17_handler
|
||||
.psb_mut()
|
||||
.service_helper
|
||||
.common
|
||||
.verification_handler
|
||||
.get_mut()
|
||||
.start_failure(
|
||||
|
@ -1,7 +1,8 @@
|
||||
use derive_new::new;
|
||||
use satrs_core::hk::HkRequest;
|
||||
use satrs_core::mode::ModeRequest;
|
||||
use satrs_core::pus::verification::{TcStateAccepted, VerificationToken};
|
||||
use satrs_core::tmtc::TargetId;
|
||||
use satrs_example::TargetIdWithApid;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
@ -19,18 +20,12 @@ pub enum Request {
|
||||
Action(ActionRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
#[derive(Clone, Eq, PartialEq, Debug, new)]
|
||||
pub struct TargetedRequest {
|
||||
pub(crate) target_id: TargetId,
|
||||
pub(crate) target_id_with_apid: TargetIdWithApid,
|
||||
pub(crate) request: Request,
|
||||
}
|
||||
|
||||
impl TargetedRequest {
|
||||
pub fn new(target_id: TargetId, request: Request) -> Self {
|
||||
Self { target_id, request }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub struct RequestWithToken {
|
||||
pub(crate) targeted_request: TargetedRequest,
|
||||
@ -39,7 +34,7 @@ pub struct RequestWithToken {
|
||||
|
||||
impl RequestWithToken {
|
||||
pub fn new(
|
||||
target_id: u32,
|
||||
target_id: TargetIdWithApid,
|
||||
request: Request,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
) -> Self {
|
||||
|
113
satrs-example/src/tcp.rs
Normal file
113
satrs-example/src/tcp.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use log::{info, warn};
|
||||
use satrs_core::{
|
||||
hal::std::tcp_server::{ServerConfig, TcpSpacepacketsServer},
|
||||
spacepackets::PacketId,
|
||||
tmtc::{CcsdsDistributor, CcsdsError, TmPacketSourceCore},
|
||||
};
|
||||
use satrs_example::config::PUS_APID;
|
||||
|
||||
pub const PACKET_ID_LOOKUP: &[PacketId] = &[PacketId::const_tc(true, PUS_APID)];
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct SyncTcpTmSource {
|
||||
tm_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||
max_packets_stored: usize,
|
||||
pub silent_packet_overwrite: bool,
|
||||
}
|
||||
|
||||
impl SyncTcpTmSource {
|
||||
pub fn new(max_packets_stored: usize) -> Self {
|
||||
Self {
|
||||
tm_queue: Arc::default(),
|
||||
max_packets_stored,
|
||||
silent_packet_overwrite: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tm(&mut self, tm: &[u8]) {
|
||||
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec");
|
||||
if tm_queue.len() > self.max_packets_stored {
|
||||
if !self.silent_packet_overwrite {
|
||||
warn!("TPC TM source is full, deleting oldest packet");
|
||||
}
|
||||
tm_queue.pop_front();
|
||||
}
|
||||
tm_queue.push_back(tm.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
impl TmPacketSourceCore for SyncTcpTmSource {
|
||||
type Error = ();
|
||||
|
||||
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
|
||||
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed");
|
||||
if !tm_queue.is_empty() {
|
||||
let next_vec = tm_queue.front().unwrap();
|
||||
if buffer.len() < next_vec.len() {
|
||||
panic!(
|
||||
"provided buffer too small, must be at least {} bytes",
|
||||
next_vec.len()
|
||||
);
|
||||
}
|
||||
let next_vec = tm_queue.pop_front().unwrap();
|
||||
buffer[0..next_vec.len()].copy_from_slice(&next_vec);
|
||||
if next_vec.len() > 9 {
|
||||
let service = next_vec[7];
|
||||
let subservice = next_vec[8];
|
||||
info!("Sending PUS TM[{service},{subservice}]")
|
||||
} else {
|
||||
info!("Sending PUS TM");
|
||||
}
|
||||
return Ok(next_vec.len());
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TcpTask<MpscErrorType: 'static> {
|
||||
server: TcpSpacepacketsServer<
|
||||
(),
|
||||
CcsdsError<MpscErrorType>,
|
||||
SyncTcpTmSource,
|
||||
CcsdsDistributor<MpscErrorType>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<MpscErrorType: 'static + core::fmt::Debug> TcpTask<MpscErrorType> {
|
||||
pub fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: SyncTcpTmSource,
|
||||
tc_receiver: CcsdsDistributor<MpscErrorType>,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
Ok(Self {
|
||||
server: TcpSpacepacketsServer::new(
|
||||
cfg,
|
||||
tm_source,
|
||||
tc_receiver,
|
||||
Box::new(PACKET_ID_LOOKUP),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
loop {
|
||||
let result = self.server.handle_next_connection();
|
||||
match result {
|
||||
Ok(conn_result) => {
|
||||
info!(
|
||||
"Served {} TMs and {} TCs for client {:?}",
|
||||
conn_result.num_sent_tms, conn_result.num_received_tcs, conn_result.addr
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("TCP server error: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
satrs-example/src/tm_funnel.rs
Normal file
156
satrs-example/src/tm_funnel.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use satrs_core::{
|
||||
pool::{PoolProvider, StoreAddr},
|
||||
seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore},
|
||||
spacepackets::{
|
||||
ecss::{tm::PusTmZeroCopyWriter, PusPacket},
|
||||
time::cds::MIN_CDS_FIELD_LEN,
|
||||
CcsdsPacket,
|
||||
},
|
||||
tmtc::tm_helper::SharedTmPool,
|
||||
};
|
||||
|
||||
use crate::tcp::SyncTcpTmSource;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CcsdsSeqCounterMap {
|
||||
apid_seq_counter_map: HashMap<u16, CcsdsSimpleSeqCountProvider>,
|
||||
}
|
||||
impl CcsdsSeqCounterMap {
|
||||
pub fn get_and_increment(&mut self, apid: u16) -> u16 {
|
||||
self.apid_seq_counter_map
|
||||
.entry(apid)
|
||||
.or_default()
|
||||
.get_and_increment()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmFunnelCommon {
|
||||
seq_counter_map: CcsdsSeqCounterMap,
|
||||
msg_counter_map: HashMap<u8, u16>,
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
}
|
||||
|
||||
impl TmFunnelCommon {
|
||||
pub fn new(sync_tm_tcp_source: SyncTcpTmSource) -> Self {
|
||||
Self {
|
||||
seq_counter_map: Default::default(),
|
||||
msg_counter_map: Default::default(),
|
||||
sync_tm_tcp_source,
|
||||
}
|
||||
}
|
||||
|
||||
// Applies common packet processing operations for PUS TM packets. This includes setting
|
||||
// a sequence counter
|
||||
fn apply_packet_processing(&mut self, mut zero_copy_writer: PusTmZeroCopyWriter) {
|
||||
// zero_copy_writer.set_apid(PUS_APID);
|
||||
zero_copy_writer.set_seq_count(
|
||||
self.seq_counter_map
|
||||
.get_and_increment(zero_copy_writer.apid()),
|
||||
);
|
||||
let entry = self
|
||||
.msg_counter_map
|
||||
.entry(zero_copy_writer.service())
|
||||
.or_insert(0);
|
||||
zero_copy_writer.set_msg_count(*entry);
|
||||
if *entry == u16::MAX {
|
||||
*entry = 0;
|
||||
} else {
|
||||
*entry += 1;
|
||||
}
|
||||
|
||||
Self::packet_printout(&zero_copy_writer);
|
||||
// This operation has to come last!
|
||||
zero_copy_writer.finish();
|
||||
}
|
||||
|
||||
fn packet_printout(tm: &PusTmZeroCopyWriter) {
|
||||
info!("Sending PUS TM[{},{}]", tm.service(), tm.subservice());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmFunnelStatic {
|
||||
common: TmFunnelCommon,
|
||||
shared_tm_store: SharedTmPool,
|
||||
tm_funnel_rx: Receiver<StoreAddr>,
|
||||
tm_server_tx: Sender<StoreAddr>,
|
||||
}
|
||||
|
||||
impl TmFunnelStatic {
|
||||
pub fn new(
|
||||
shared_tm_store: SharedTmPool,
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: Receiver<StoreAddr>,
|
||||
tm_server_tx: Sender<StoreAddr>,
|
||||
) -> Self {
|
||||
Self {
|
||||
common: TmFunnelCommon::new(sync_tm_tcp_source),
|
||||
shared_tm_store,
|
||||
tm_funnel_rx,
|
||||
tm_server_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
if let Ok(addr) = self.tm_funnel_rx.recv() {
|
||||
// Read the TM, set sequence counter and message counter, and finally update
|
||||
// the CRC.
|
||||
let shared_pool = self.shared_tm_store.clone_backing_pool();
|
||||
let mut pool_guard = shared_pool.write().expect("Locking TM pool failed");
|
||||
let mut tm_copy = Vec::new();
|
||||
pool_guard
|
||||
.modify(&addr, |buf| {
|
||||
let zero_copy_writer = PusTmZeroCopyWriter::new(buf, MIN_CDS_FIELD_LEN)
|
||||
.expect("Creating TM zero copy writer failed");
|
||||
self.common.apply_packet_processing(zero_copy_writer);
|
||||
tm_copy = buf.to_vec()
|
||||
})
|
||||
.expect("Reading TM from pool failed");
|
||||
self.tm_server_tx
|
||||
.send(addr)
|
||||
.expect("Sending TM to server failed");
|
||||
// We could also do this step in the update closure, but I'd rather avoid this, could
|
||||
// lead to nested locking.
|
||||
self.common.sync_tm_tcp_source.add_tm(&tm_copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmFunnelDynamic {
|
||||
common: TmFunnelCommon,
|
||||
tm_funnel_rx: Receiver<Vec<u8>>,
|
||||
tm_server_tx: Sender<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl TmFunnelDynamic {
|
||||
pub fn new(
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: Receiver<Vec<u8>>,
|
||||
tm_server_tx: Sender<Vec<u8>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
common: TmFunnelCommon::new(sync_tm_tcp_source),
|
||||
tm_funnel_rx,
|
||||
tm_server_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
if let Ok(mut tm) = self.tm_funnel_rx.recv() {
|
||||
// Read the TM, set sequence counter and message counter, and finally update
|
||||
// the CRC.
|
||||
let zero_copy_writer = PusTmZeroCopyWriter::new(&mut tm, MIN_CDS_FIELD_LEN)
|
||||
.expect("Creating TM zero copy writer failed");
|
||||
self.common.apply_packet_processing(zero_copy_writer);
|
||||
self.tm_server_tx
|
||||
.send(tm.clone())
|
||||
.expect("Sending TM to server failed");
|
||||
self.common.sync_tm_tcp_source.add_tm(&tm);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +1,68 @@
|
||||
use log::{info, warn};
|
||||
use satrs_core::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use log::warn;
|
||||
use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc};
|
||||
use satrs_core::spacepackets::SpHeader;
|
||||
use std::sync::mpsc::{self, Receiver, SendError, Sender, TryRecvError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ccsds::CcsdsReceiver;
|
||||
use crate::pus::{PusReceiver, PusTcMpscRouter};
|
||||
use satrs_core::pool::{SharedPool, StoreAddr, StoreError};
|
||||
use satrs_core::pus::verification::StdVerifReporterWithSender;
|
||||
use satrs_core::pus::{ReceivesEcssPusTc, TcAddrWithToken};
|
||||
use crate::pus::PusReceiver;
|
||||
use satrs_core::pool::{PoolProvider, SharedStaticMemoryPool, StoreAddr, StoreError};
|
||||
use satrs_core::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs_core::spacepackets::ecss::PusPacket;
|
||||
use satrs_core::spacepackets::SpHeader;
|
||||
use satrs_core::tmtc::tm_helper::SharedTmStore;
|
||||
use satrs_core::tmtc::{CcsdsDistributor, CcsdsError, ReceivesCcsdsTc};
|
||||
|
||||
pub struct TmArgs {
|
||||
pub tm_store: SharedTmStore,
|
||||
pub tm_sink_sender: Sender<StoreAddr>,
|
||||
pub tm_server_rx: Receiver<StoreAddr>,
|
||||
}
|
||||
|
||||
pub struct TcArgs {
|
||||
pub tc_source: PusTcSource,
|
||||
pub tc_receiver: Receiver<StoreAddr>,
|
||||
}
|
||||
|
||||
impl TcArgs {
|
||||
#[allow(dead_code)]
|
||||
fn split(self) -> (PusTcSource, Receiver<StoreAddr>) {
|
||||
(self.tc_source, self.tc_receiver)
|
||||
}
|
||||
}
|
||||
use satrs_core::tmtc::ReceivesCcsdsTc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum MpscStoreAndSendError {
|
||||
#[error("Store error: {0}")]
|
||||
Store(#[from] StoreError),
|
||||
#[error("TC send error: {0}")]
|
||||
TcSend(#[from] SendError<TcAddrWithToken>),
|
||||
TcSend(#[from] SendError<EcssTcAndToken>),
|
||||
#[error("TMTC send error: {0}")]
|
||||
TmTcSend(#[from] SendError<StoreAddr>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TcStore {
|
||||
pub pool: SharedPool,
|
||||
pub struct SharedTcPool {
|
||||
pub pool: SharedStaticMemoryPool,
|
||||
}
|
||||
|
||||
impl TcStore {
|
||||
impl SharedTcPool {
|
||||
pub fn add_pus_tc(&mut self, pus_tc: &PusTcReader) -> Result<StoreAddr, StoreError> {
|
||||
let mut pg = self.pool.write().expect("error locking TC store");
|
||||
let (addr, buf) = pg.free_element(pus_tc.len_packed())?;
|
||||
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
|
||||
let addr = pg.free_element(pus_tc.len_packed(), |buf| {
|
||||
buf[0..pus_tc.len_packed()].copy_from_slice(pus_tc.raw_data());
|
||||
})?;
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmFunnel {
|
||||
pub tm_funnel_rx: Receiver<StoreAddr>,
|
||||
pub tm_server_tx: Sender<StoreAddr>,
|
||||
}
|
||||
|
||||
pub struct UdpTmtcServer {
|
||||
udp_tc_server: UdpTcServer<CcsdsError<MpscStoreAndSendError>>,
|
||||
tm_rx: Receiver<StoreAddr>,
|
||||
tm_store: SharedPool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PusTcSource {
|
||||
pub struct PusTcSourceProviderSharedPool {
|
||||
pub tc_source: Sender<StoreAddr>,
|
||||
pub tc_store: TcStore,
|
||||
pub shared_pool: SharedTcPool,
|
||||
}
|
||||
|
||||
impl ReceivesEcssPusTc for PusTcSource {
|
||||
impl PusTcSourceProviderSharedPool {
|
||||
#[allow(dead_code)]
|
||||
pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool {
|
||||
self.shared_pool.pool.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ReceivesEcssPusTc for PusTcSourceProviderSharedPool {
|
||||
type Error = MpscStoreAndSendError;
|
||||
|
||||
fn pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
|
||||
let addr = self.tc_store.add_pus_tc(pus_tc)?;
|
||||
let addr = self.shared_pool.add_pus_tc(pus_tc)?;
|
||||
self.tc_source.send(addr)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReceivesCcsdsTc for PusTcSource {
|
||||
impl ReceivesCcsdsTc for PusTcSourceProviderSharedPool {
|
||||
type Error = MpscStoreAndSendError;
|
||||
|
||||
fn pass_ccsds(&mut self, _: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error> {
|
||||
let mut pool = self.tc_store.pool.write().expect("locking pool failed");
|
||||
let mut pool = self.shared_pool.pool.write().expect("locking pool failed");
|
||||
let addr = pool.add(tc_raw)?;
|
||||
drop(pool);
|
||||
self.tc_source.send(addr)?;
|
||||
@ -98,131 +70,138 @@ impl ReceivesCcsdsTc for PusTcSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn core_tmtc_task(
|
||||
socket_addr: SocketAddr,
|
||||
mut tc_args: TcArgs,
|
||||
tm_args: TmArgs,
|
||||
verif_reporter: StdVerifReporterWithSender,
|
||||
pus_router: PusTcMpscRouter,
|
||||
) {
|
||||
let mut pus_receiver = PusReceiver::new(verif_reporter, pus_router);
|
||||
// Newtype, can not implement necessary traits on MPSC sender directly because of orphan rules.
|
||||
#[derive(Clone)]
|
||||
pub struct PusTcSourceProviderDynamic(pub Sender<Vec<u8>>);
|
||||
|
||||
let ccsds_receiver = CcsdsReceiver {
|
||||
tc_source: tc_args.tc_source.clone(),
|
||||
};
|
||||
impl ReceivesEcssPusTc for PusTcSourceProviderDynamic {
|
||||
type Error = SendError<Vec<u8>>;
|
||||
|
||||
let ccsds_distributor = CcsdsDistributor::new(Box::new(ccsds_receiver));
|
||||
|
||||
let udp_tc_server = UdpTcServer::new(socket_addr, 2048, Box::new(ccsds_distributor))
|
||||
.expect("creating UDP TMTC server failed");
|
||||
|
||||
let mut udp_tmtc_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_rx: tm_args.tm_server_rx,
|
||||
tm_store: tm_args.tm_store.clone_backing_pool(),
|
||||
};
|
||||
|
||||
let mut tc_buf: [u8; 4096] = [0; 4096];
|
||||
loop {
|
||||
core_tmtc_loop(
|
||||
&mut udp_tmtc_server,
|
||||
&mut tc_args,
|
||||
&mut tc_buf,
|
||||
&mut pus_receiver,
|
||||
);
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
fn pass_pus_tc(&mut self, _: &SpHeader, pus_tc: &PusTcReader) -> Result<(), Self::Error> {
|
||||
self.0.send(pus_tc.raw_data().to_vec())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn core_tmtc_loop(
|
||||
udp_tmtc_server: &mut UdpTmtcServer,
|
||||
tc_args: &mut TcArgs,
|
||||
tc_buf: &mut [u8],
|
||||
pus_receiver: &mut PusReceiver,
|
||||
) {
|
||||
while poll_tc_server(udp_tmtc_server) {}
|
||||
match tc_args.tc_receiver.try_recv() {
|
||||
Ok(addr) => {
|
||||
let pool = tc_args
|
||||
.tc_source
|
||||
.tc_store
|
||||
.pool
|
||||
.read()
|
||||
.expect("locking tc pool failed");
|
||||
let data = pool.read(&addr).expect("reading pool failed");
|
||||
tc_buf[0..data.len()].copy_from_slice(data);
|
||||
drop(pool);
|
||||
match PusTcReader::new(tc_buf) {
|
||||
impl ReceivesCcsdsTc for PusTcSourceProviderDynamic {
|
||||
type Error = mpsc::SendError<Vec<u8>>;
|
||||
|
||||
fn pass_ccsds(&mut self, _: &SpHeader, tc_raw: &[u8]) -> Result<(), Self::Error> {
|
||||
self.0.send(tc_raw.to_vec())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TC source components where static pools are the backing memory of the received telecommands.
|
||||
pub struct TcSourceTaskStatic {
|
||||
shared_tc_pool: SharedTcPool,
|
||||
tc_receiver: Receiver<StoreAddr>,
|
||||
tc_buf: [u8; 4096],
|
||||
pus_receiver: PusReceiver,
|
||||
}
|
||||
|
||||
impl TcSourceTaskStatic {
|
||||
pub fn new(
|
||||
shared_tc_pool: SharedTcPool,
|
||||
tc_receiver: Receiver<StoreAddr>,
|
||||
pus_receiver: PusReceiver,
|
||||
) -> Self {
|
||||
Self {
|
||||
shared_tc_pool,
|
||||
tc_receiver,
|
||||
tc_buf: [0; 4096],
|
||||
pus_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.poll_tc();
|
||||
}
|
||||
|
||||
pub fn poll_tc(&mut self) -> bool {
|
||||
match self.tc_receiver.try_recv() {
|
||||
Ok(addr) => {
|
||||
let pool = self
|
||||
.shared_tc_pool
|
||||
.pool
|
||||
.read()
|
||||
.expect("locking tc pool failed");
|
||||
pool.read(&addr, &mut self.tc_buf)
|
||||
.expect("reading pool failed");
|
||||
drop(pool);
|
||||
match PusTcReader::new(&self.tc_buf) {
|
||||
Ok((pus_tc, _)) => {
|
||||
self.pus_receiver
|
||||
.handle_tc_packet(
|
||||
satrs_core::pus::TcInMemory::StoreAddr(addr),
|
||||
pus_tc.service(),
|
||||
&pus_tc,
|
||||
)
|
||||
.ok();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("error creating PUS TC from raw data: {e}");
|
||||
warn!("raw data: {:x?}", self.tc_buf);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => false,
|
||||
TryRecvError::Disconnected => {
|
||||
warn!("tmtc thread: sender disconnected");
|
||||
false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TC source components where the heap is the backing memory of the received telecommands.
|
||||
pub struct TcSourceTaskDynamic {
|
||||
pub tc_receiver: Receiver<Vec<u8>>,
|
||||
pus_receiver: PusReceiver,
|
||||
}
|
||||
|
||||
impl TcSourceTaskDynamic {
|
||||
pub fn new(tc_receiver: Receiver<Vec<u8>>, pus_receiver: PusReceiver) -> Self {
|
||||
Self {
|
||||
tc_receiver,
|
||||
pus_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.poll_tc();
|
||||
}
|
||||
|
||||
pub fn poll_tc(&mut self) -> bool {
|
||||
match self.tc_receiver.try_recv() {
|
||||
Ok(tc) => match PusTcReader::new(&tc) {
|
||||
Ok((pus_tc, _)) => {
|
||||
pus_receiver
|
||||
.handle_tc_packet(addr, pus_tc.service(), &pus_tc)
|
||||
self.pus_receiver
|
||||
.handle_tc_packet(
|
||||
satrs_core::pus::TcInMemory::Vec(tc.clone()),
|
||||
pus_tc.service(),
|
||||
&pus_tc,
|
||||
)
|
||||
.ok();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("error creating PUS TC from raw data: {e}");
|
||||
warn!("raw data: {tc_buf:x?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let TryRecvError::Disconnected = e {
|
||||
warn!("tmtc thread: sender disconnected")
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(recv_addr) = udp_tmtc_server.udp_tc_server.last_sender() {
|
||||
core_tm_handling(udp_tmtc_server, &recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_tc_server(udp_tmtc_server: &mut UdpTmtcServer) -> bool {
|
||||
match udp_tmtc_server.udp_tc_server.try_recv_tc() {
|
||||
Ok(_) => true,
|
||||
Err(e) => match e {
|
||||
ReceiveResult::ReceiverError(e) => match e {
|
||||
CcsdsError::ByteConversionError(e) => {
|
||||
warn!("packet error: {e:?}");
|
||||
true
|
||||
}
|
||||
CcsdsError::CustomError(e) => {
|
||||
warn!("mpsc store and send error {e:?}");
|
||||
warn!("raw data: {:x?}", tc);
|
||||
true
|
||||
}
|
||||
},
|
||||
ReceiveResult::IoError(e) => {
|
||||
warn!("IO error {e}");
|
||||
false
|
||||
}
|
||||
ReceiveResult::NothingReceived => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn core_tm_handling(udp_tmtc_server: &mut UdpTmtcServer, recv_addr: &SocketAddr) {
|
||||
while let Ok(addr) = udp_tmtc_server.tm_rx.try_recv() {
|
||||
let store_lock = udp_tmtc_server.tm_store.write();
|
||||
if store_lock.is_err() {
|
||||
warn!("Locking TM store failed");
|
||||
continue;
|
||||
}
|
||||
let mut store_lock = store_lock.unwrap();
|
||||
let pg = store_lock.read_with_guard(addr);
|
||||
let read_res = pg.read();
|
||||
if read_res.is_err() {
|
||||
warn!("Error reading TM pool data");
|
||||
continue;
|
||||
}
|
||||
let buf = read_res.unwrap();
|
||||
if buf.len() > 9 {
|
||||
let service = buf[7];
|
||||
let subservice = buf[8];
|
||||
info!("Sending PUS TM[{service},{subservice}]")
|
||||
} else {
|
||||
info!("Sending PUS TM");
|
||||
}
|
||||
let result = udp_tmtc_server.udp_tc_server.socket.send_to(buf, recv_addr);
|
||||
if let Err(e) = result {
|
||||
warn!("Sending TM with UDP socket failed: {e}")
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => false,
|
||||
TryRecvError::Disconnected => {
|
||||
warn!("tmtc thread: sender disconnected");
|
||||
false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
215
satrs-example/src/udp.rs
Normal file
215
satrs-example/src/udp.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::mpsc::Receiver,
|
||||
};
|
||||
|
||||
use log::{info, warn};
|
||||
use satrs_core::{
|
||||
hal::std::udp_server::{ReceiveResult, UdpTcServer},
|
||||
pool::{PoolProviderWithGuards, SharedStaticMemoryPool, StoreAddr},
|
||||
tmtc::CcsdsError,
|
||||
};
|
||||
|
||||
pub trait UdpTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
|
||||
}
|
||||
|
||||
pub struct StaticUdpTmHandler {
|
||||
pub tm_rx: Receiver<StoreAddr>,
|
||||
pub tm_store: SharedStaticMemoryPool,
|
||||
}
|
||||
|
||||
impl UdpTmHandler for StaticUdpTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, &recv_addr: &SocketAddr) {
|
||||
while let Ok(addr) = self.tm_rx.try_recv() {
|
||||
let store_lock = self.tm_store.write();
|
||||
if store_lock.is_err() {
|
||||
warn!("Locking TM store failed");
|
||||
continue;
|
||||
}
|
||||
let mut store_lock = store_lock.unwrap();
|
||||
let pg = store_lock.read_with_guard(addr);
|
||||
let read_res = pg.read_as_vec();
|
||||
if read_res.is_err() {
|
||||
warn!("Error reading TM pool data");
|
||||
continue;
|
||||
}
|
||||
let buf = read_res.unwrap();
|
||||
let result = socket.send_to(&buf, recv_addr);
|
||||
if let Err(e) = result {
|
||||
warn!("Sending TM with UDP socket failed: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DynamicUdpTmHandler {
|
||||
pub tm_rx: Receiver<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl UdpTmHandler for DynamicUdpTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
while let Ok(tm) = self.tm_rx.try_recv() {
|
||||
if tm.len() > 9 {
|
||||
let service = tm[7];
|
||||
let subservice = tm[8];
|
||||
info!("Sending PUS TM[{service},{subservice}]")
|
||||
} else {
|
||||
info!("Sending PUS TM");
|
||||
}
|
||||
let result = socket.send_to(&tm, recv_addr);
|
||||
if let Err(e) = result {
|
||||
warn!("Sending TM with UDP socket failed: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpTmtcServer<TmHandler: UdpTmHandler, SendError> {
|
||||
pub udp_tc_server: UdpTcServer<CcsdsError<SendError>>,
|
||||
pub tm_handler: TmHandler,
|
||||
}
|
||||
|
||||
impl<TmHandler: UdpTmHandler, SendError: core::fmt::Debug + 'static>
|
||||
UdpTmtcServer<TmHandler, SendError>
|
||||
{
|
||||
pub fn periodic_operation(&mut self) {
|
||||
while self.poll_tc_server() {}
|
||||
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
|
||||
self.tm_handler
|
||||
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_tc_server(&mut self) -> bool {
|
||||
match self.udp_tc_server.try_recv_tc() {
|
||||
Ok(_) => true,
|
||||
Err(e) => match e {
|
||||
ReceiveResult::ReceiverError(e) => match e {
|
||||
CcsdsError::ByteConversionError(e) => {
|
||||
warn!("packet error: {e:?}");
|
||||
true
|
||||
}
|
||||
CcsdsError::CustomError(e) => {
|
||||
warn!("mpsc custom error {e:?}");
|
||||
true
|
||||
}
|
||||
},
|
||||
ReceiveResult::IoError(e) => {
|
||||
warn!("IO error {e}");
|
||||
false
|
||||
}
|
||||
ReceiveResult::NothingReceived => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
net::IpAddr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use satrs_core::{
|
||||
spacepackets::{
|
||||
ecss::{tc::PusTcCreator, WritablePusPacket},
|
||||
SpHeader,
|
||||
},
|
||||
tmtc::ReceivesTcCore,
|
||||
};
|
||||
use satrs_example::config::{OBSW_SERVER_ADDR, PUS_APID};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TestReceiver {
|
||||
tc_vec: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl ReceivesTcCore for TestReceiver {
|
||||
type Error = CcsdsError<()>;
|
||||
fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
|
||||
self.tc_vec.lock().unwrap().push_back(tc_raw.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TestTmHandler {
|
||||
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
|
||||
}
|
||||
|
||||
impl UdpTmHandler for TestTmHandler {
|
||||
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
||||
let test_receiver = TestReceiver::default();
|
||||
let tc_queue = test_receiver.tc_vec.clone();
|
||||
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler,
|
||||
};
|
||||
udp_dyn_server.periodic_operation();
|
||||
assert!(tc_queue.lock().unwrap().is_empty());
|
||||
assert!(tm_handler_calls.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transactions() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
||||
let test_receiver = TestReceiver::default();
|
||||
let tc_queue = test_receiver.tc_vec.clone();
|
||||
let udp_tc_server = UdpTcServer::new(sock_addr, 2048, Box::new(test_receiver)).unwrap();
|
||||
let server_addr = udp_tc_server.socket.local_addr().unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler,
|
||||
};
|
||||
let mut sph = SpHeader::tc_unseg(PUS_APID, 0, 0).unwrap();
|
||||
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true)
|
||||
.to_vec()
|
||||
.unwrap();
|
||||
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
|
||||
let client_addr = client.local_addr().unwrap();
|
||||
client.connect(server_addr).unwrap();
|
||||
client.send(&ping_tc).unwrap();
|
||||
udp_dyn_server.periodic_operation();
|
||||
{
|
||||
let mut tc_queue = tc_queue.lock().unwrap();
|
||||
assert!(!tc_queue.is_empty());
|
||||
let received_tc = tc_queue.pop_front().unwrap();
|
||||
assert_eq!(received_tc, ping_tc);
|
||||
}
|
||||
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
udp_dyn_server.periodic_operation();
|
||||
assert!(tc_queue.lock().unwrap().is_empty());
|
||||
// Still tries to send to the same client.
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "satrs-mib"
|
||||
version = "0.1.0-alpha.0"
|
||||
version = "0.1.0-alpha.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.61"
|
||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||
@ -23,14 +23,15 @@ version = "1"
|
||||
optional = true
|
||||
|
||||
[dependencies.satrs-core]
|
||||
path = "../satrs-core"
|
||||
# version = "0.1.0-alpha.1"
|
||||
git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
|
||||
branch = "main"
|
||||
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
|
||||
# branch = "main"
|
||||
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89"
|
||||
|
||||
[dependencies.satrs-mib-codegen]
|
||||
path = "codegen"
|
||||
version = "0.1.0-alpha.0"
|
||||
version = "0.1.0-alpha.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "satrs-mib-codegen"
|
||||
version = "0.1.0-alpha.0"
|
||||
version = "0.1.0-alpha.1"
|
||||
edition = "2021"
|
||||
description = "satrs-mib proc macro implementation"
|
||||
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||
@ -20,9 +20,10 @@ quote = "1"
|
||||
proc-macro2 = "1"
|
||||
|
||||
[dependencies.satrs-core]
|
||||
path = "../../satrs-core"
|
||||
# version = "0.1.0-alpha.1"
|
||||
git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
|
||||
branch = "main"
|
||||
# git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git"
|
||||
# branch = "main"
|
||||
# rev = "35e1f7a983f6535c5571186e361fe101d4306b89"
|
||||
|
||||
[dev-dependencies]
|
||||
|
Reference in New Issue
Block a user